openvibers/openvibe.extensions Commits
Companion 0.9.2: bundle OpenVibe.Shared 4.212.0 (#23)
Changes
@@@ -4,6 +4,18 @@ The OpenVibe Companion's releases. Each one is a `browser/manifest.json` versionrelease](https://github.com/OpenVibers/OpenVibe.Extensions/releases); older entries are in the githistory.++## 0.9.2 — 2026-11-11++++- **The side panel's Dock is OpenVibe.Shared 4.212.0** (was 4.124.0), copied from the release with the++ Companion's own two patches kept: the read-only Dock token renders the four server controls as links++ to openvibe.codes, and a Live frame that refuses embedding falls back to the still and a **Watch on++ openvibe.live** link (Firefox and an unlisted Chrome id never try the frame).++- The newer Dock brings its **Code** and **Actor** tabs, the one window (`ov-window.js`), the composer,++ the glyph set, the theme loader and the agent sheet/session, all bundled under `browser/shared/`++ because the extension's CSP allows no remote script.++- `AGENT_EMBED_EXTENSION_ORIGINS` on OpenVibe.Live is set to the extension's fixed Chrome id++ (`chrome-extension://bjcjcfcecpkkmfndegkjekoipdmbgakd`), so the panel can frame the agent stream.++## 0.9.1 — 2026-11-07- **The side panel's agent view can read.** OpenVibe.Codes' two agent-view read routes now accept@@@ -6,7 +6,7 @@**Build and test:** `npm install && npm run pack` (Node 22 or newer) · **Tests:** `npm test`**Status:** alpha (roadmap Wave 18, client part). The extension builds, its tests pass, and headless--Chrome loads the unpacked 0.9.0 build with no console or CSP errors (2026-11-07, Chrome 156 via++Chrome loads the unpacked 0.9.2 build with no console or CSP errors (2026-11-07, Chrome 156 viaCDP `Extensions.loadUnpacked`; the side panel's Agents tab draws the Dock's agent view against stubbed first parties, see the credential note below). A development build ofthe coupon part looked up codes and sent a report against a local OpenVibe.Coupons. Mozilla's`web-ext lint` (what addons.mozilla.org runs on upload) passes with no errors, warnings or notices@@@ -111,7 +111,7 @@ covered in Coupons' own tests: lookups are identical for every caller, and the AThe **OpenVibe Dock** — the pill, the card with its Vibe, Agents, Chats and Activity tabs — sits inthe browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the--released OpenVibe.Shared Dock 4.124.0, bundled under `browser/shared/` because the extension's CSP allows no++released OpenVibe.Shared Dock 4.212.0, bundled under `browser/shared/` because the extension's CSP allows noremote script, and it reads and writes the same state on the Network as the Dock on a site, so itopens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent andunread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel.@@@ -119,14 +119,15 @@ unread counts) and re-reads it every 30 s, so a change on the page reaches the oThe Agents tab draws the person's own desk stream the same way a site's Dock does: the Live embed, thetask line, and the owner's **Pause/Resume**, **Curtain now**, **Mute/Unmute** and **End** controls. Theview is read with the person's token from OpenVibe.Codes (`GET /api/v1/me/agent-view`); the extension--allows Codes and Live's embed for exactly that. Codes' normal session check does not yet accept the--Dock's read-only OAuth token (issuer `<network>/dock`), so a production side panel keeps the view--hidden until Codes or the Network accepts that credential; `scripts/check-panel.js` proves the bundled--Dock itself draws it. Live's embed also allows first-party site origins, not an extension origin yet.++allows Codes and Live's embed for exactly that. Codes accepts the Dock's read-only OAuth token (issuer++`<network>/dock`), so the panel shows the stream; its four server controls are links to openvibe.codes,++where the person's own session does the same thing. The fixed Chrome id is in Live's++`AGENT_EMBED_EXTENSION_ORIGINS`, and a browser that cannot frame the embed gets the still and a++**Watch on openvibe.live** link instead. `scripts/check-panel.js` proves both in headless Chrome.It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: theNetwork's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no--secret), and the Dock gets a read-only token (`dock`, `activity`, `chat.read`) that refreshes itself.++secret), and the Dock gets a read-only token (`dock`, `activity`, `chat.read`, `agent.view`) that refreshes itself.The access token is kept in `storage.session`; the rotating refresh token in `storage.local`, on thisdevice. Sign out revokes it at the Network, and the account page (`/my`) lists it under **Signed-inapps**. The 0.7.0 pasted-token path is still in the settings page as a fallback until a later release@@@ -139,9 +140,10 @@ browser/manifest.json MV3; Chromium (minimum_chrome_version 114) and Firefox (gecko id, strict_min_version 128)panel.html / panel.js the Dock in the side panel: the Network's state, the same tabs as the Dock on a sitepanel.css the Dock fills the panel (no floating card, no pill)-- shared/ the released OpenVibe.Shared Dock 4.124.0, bundled: the CSP allows no remote script-- (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js,-- notification-ui.js, notification-live.js, ov-mark.js, kit.css)++ shared/ the released OpenVibe.Shared Dock 4.212.0, bundled: the CSP allows no remote script++ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js,++ agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js,++ brands.js, theme-loader.js, ov-mark.js, kit.css)popup.html / popup.js DOM wiring onlyoptions.html / .js connect (save a checked token), disconnect, grant the API host permissionlib/config.js API_ORIGIN (Coupons), NETWORK_ORIGIN and CHAT_ORIGIN (the Dock), CODES_ORIGIN (its agent view); HANDOFF_ORIGINS (Rent, Watch, Food, Reviews)@@@ -5,11 +5,11 @@"wave": "W18","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",-- "version": "0.9.0",++ "version": "0.9.2","surfaces": {"browser": {"name": "OpenVibe Companion",-- "version": "0.9.0",++ "version": "0.9.2","manifest": 3,"targets": ["chromium >= 114",@@@ -31,7 +31,7 @@],"credential": "optional cpx_ install token (coupons.lookup, coupons.report); the Dock one-click OAuth public client (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local); the 0.7.0 pasted OpenVibe token is still accepted as a fallback until a later release","publishedToStores": false,-- "loadedInChrome": "yes: headless Chrome 156 loads the 0.9.0 build via CDP Extensions.loadUnpacked; the side panel's Agents tab draws the Dock's agent view against a stubbed Network/Chat/Codes (the Live stream frame, the task line and meta, and Pause/Resume, Curtain now, Mute/Unmute and End) with its styles applied and no CSP refusal (scripts/check-panel.js); lib/autoapply.js passes three fixture checkout pages in headless Chrome; the one-click Dock sign-in is covered by test/auth.test.js",++ "loadedInChrome": "yes: headless Chrome 156 loads the 0.9.2 build via CDP Extensions.loadUnpacked; the side panel's Agents tab draws the Dock's agent view against a stubbed Network/Chat/Codes (the Live stream frame, the task line and meta, and Pause/Resume, Curtain now, Mute/Unmute and End) with its styles applied and no CSP refusal (scripts/check-panel.js); lib/autoapply.js passes three fixture checkout pages in headless Chrome; the one-click Dock sign-in is covered by test/auth.test.js","loadedInFirefox": "not yet loaded in a browser; Mozilla's web-ext lint (8.10.0, what AMO runs on upload) passes with 0 errors, 0 warnings and 0 notices, and runs in CI with warnings as errors","handoffs": ["https://openvibe.rent/board (keep a listing)",@@@ -44,7 +44,7 @@"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion","features": ["OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",-- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.124.0 bundled under browser/shared/ (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js, notification-ui.js, notification-live.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes"++ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.212.0 bundled under browser/shared/ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js, agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js, brands.js, theme-loader.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes"],"pending": ["owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",@@@ -67,7 +67,7 @@"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food","saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)","an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",-- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.124.0 bundled under browser/shared/ (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js, notification-ui.js, notification-live.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes",++ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.212.0 bundled under browser/shared/ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js, agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js, brands.js, theme-loader.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes","store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI","kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live","hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"@@@ -2,7 +2,7 @@"manifest_version": 3,"name": "OpenVibe Companion","short_name": "OpenVibe",-- "version": "0.9.1",++ "version": "0.9.2","key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB","description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; a mask in the email box of a sign-up form; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.","homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",@@@ -158,7 +158,6 @@.ovac-group { display: flex; align-items: center; gap: 8px; margin: 10px 10px 4px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #8b8b96); }.ovac-group span { font-variant-numeric: tabular-nums; letter-spacing: 0; color: var(--text-secondary, #b6b6c0); }.ovac-group[data-bucket="waiting"] { color: color-mix(in srgb, var(--warning, #f5a524) 70%, var(--text-primary, #ececf1)); }-- @media (prefers-reduced-motion: reduce) { .ovac { animation: none; } .ovac-item[data-bucket="running"] .ovac-state::before { animation: none; } }`;document.head.appendChild(st);}nnew file mode 100644@@@ -0,0 +1,283 @@++/* openvibe-shared/agent-session.css: the look of agent-session.js (4.32.0; it was a string inside the script before).++ Theme tokens only, light or dark; OpenVibe.Design can restyle it. agent-session.js links it from beside itself when a++ page has not; a page may link it in its <head> to paint styled from the first line. */++.ovas { --ovas-b: var(--border, rgba(255,255,255,.1)); --ovas-s: var(--bg-secondary, #121826); --ovas-t: var(--bg-tertiary, rgba(255,255,255,.05)); --ovas-mut: var(--text-muted, #8b98ad); --ovas-sec: var(--text-secondary, #a8b3c4); --ovas-ok: color-mix(in srgb, #22c55e 70%, var(--text-primary, #e6edf7)); --ovas-bad: var(--live-red, #f87171); --ovas-add: color-mix(in srgb, #22c55e 18%, transparent); --ovas-del: color-mix(in srgb, #ef4444 18%, transparent); --ovas-code: color-mix(in srgb, var(--bg-primary, #0b1020) 72%, var(--ovas-s));++ display: grid; gap: 12px; color: var(--text-primary, #e6edf7); font-family: inherit; min-width: 0; }++.ovas-ic { width: 16px; height: 16px; flex: none; }++.ovas-head { position: sticky; top: var(--ovnav-real-h, 0px); z-index: 2; display: grid; gap: 8px; padding: 12px 14px; border-radius: 16px; border: 1px solid var(--ovas-b); background: color-mix(in srgb, var(--ovas-s) 92%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }++.ovas-top { display: flex; align-items: center; gap: 10px; min-width: 0; }++.ovas-pill { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 26px; padding: 0 10px; border-radius: 999px; font-size: 12.5px; font-weight: 700; background: var(--ovas-t); color: var(--ovas-sec); }++.ovas-pill[data-s="running"], .ovas-pill[data-s="placed"], .ovas-pill[data-s="verifying"] { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }++.ovas-pill[data-s="succeeded"] { color: var(--ovas-ok); background: color-mix(in srgb, #22c55e 15%, transparent); }++.ovas-pill[data-s="failed"], .ovas-pill[data-s="expired"] { color: var(--ovas-bad); background: color-mix(in srgb, #ef4444 15%, transparent); }++.ovas-title { flex: 1; min-width: 0; font-weight: 700; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovas-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--ovas-mut); }++.ovas-meta b { color: var(--ovas-sec); font-weight: 650; font-variant-numeric: tabular-nums; }++.ovas-agents { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }++.ovas-agent { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 650; border: 1px solid var(--ovas-b); color: var(--ovas-sec); }++.ovas-agent.is-now { border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); color: var(--text-primary, #e6edf7); }++.ovas-plan { display: grid; gap: 6px; padding-top: 2px; }++.ovas-plan > summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 600; }++.ovas-plan > summary::-webkit-details-marker { display: none; }++.ovas-plan-now { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovas-plan-now::before { content: ''; display: inline-block; width: 8px; height: 8px; margin-right: 7px; border-radius: 50%; background: var(--accent, #3b82f6); vertical-align: 1px; }++.ovas-plan-n { flex: none; font-size: 12px; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }++.ovas-plan:not([open]) > ol { display: none; }++.ovas-plan-bar { height: 4px; border-radius: 4px; background: var(--ovas-t); overflow: hidden; }++.ovas-plan-bar i { display: block; height: 100%; background: var(--accent, #3b82f6); border-radius: inherit; transition: width .3s; }++.ovas-plan ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 3px; font-size: 13px; }++.ovas-plan li { display: flex; gap: 8px; align-items: baseline; color: var(--ovas-sec); }++.ovas-plan li::before { content: ''; flex: none; width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--ovas-mut); transform: translateY(1px); }++.ovas-plan li[data-s="in_progress"] { color: var(--text-primary, #e6edf7); font-weight: 600; }++.ovas-plan li[data-s="in_progress"]::before { border-color: var(--accent, #3b82f6); background: var(--accent, #3b82f6); animation: ovas-beat 1.2s ease-in-out infinite; }++.ovas-plan li[data-s="completed"] { color: var(--ovas-mut); text-decoration: line-through; }++.ovas-plan li[data-s="completed"]::before { background: var(--ovas-ok); border-color: var(--ovas-ok); }++.ovas-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }++.ovas-chip { height: 30px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: var(--ovas-sec); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; }++.ovas-chip[aria-pressed="true"] { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, #fff); }++.ovas-chip.ovas-follow { margin-left: auto; }++.ovas-feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; min-width: 0; }++.ovas-feed > li { min-width: 0; }++.ovas-msg { padding: 2px 4px; font-size: 15px; line-height: 1.55; overflow-wrap: anywhere; }++.ovas-msg p { margin: 0 0 8px; } .ovas-msg p:last-child { margin-bottom: 0; }++.ovas-msg ul, .ovas-msg ol { margin: 4px 0 8px; padding-left: 22px; } .ovas-msg li { margin: 2px 0; }++.ovas-msg code, .ovas-detail code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em; padding: 1px 5px; border-radius: 6px; background: var(--ovas-t); }++.ovas-msg a { color: var(--accent-light, var(--accent, #60a5fa)); }++.ovas-code, .ovas-out, .ovas-diff { margin: 0; padding: 10px 12px; border-radius: 12px; background: var(--ovas-code); border: 1px solid var(--ovas-b); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; overflow: auto; max-height: 50vh; white-space: pre; }++.ovas-code code { padding: 0; background: none; }++.ovas-group, .ovas-step { border: 1px solid var(--ovas-b); border-radius: 14px; background: var(--ovas-s); overflow: hidden; }++.ovas-group > summary, .ovas-step > summary { display: flex; align-items: center; gap: 10px; min-height: 46px; padding: 6px 12px; cursor: pointer; list-style: none; }++.ovas-group > summary::-webkit-details-marker, .ovas-step > summary::-webkit-details-marker { display: none; }++.ovas-chev { width: 14px; height: 14px; flex: none; color: var(--ovas-mut); transition: transform .15s; }++details[open] > summary .ovas-chev { transform: rotate(90deg); }++.ovas-icons { display: inline-flex; flex: none; }++.ovas-kind { display: grid; place-items: center; width: 28px; height: 28px; flex: none; border-radius: 9px; background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }++.ovas-icons .ovas-kind { width: 24px; height: 24px; border: 2px solid var(--ovas-s); border-radius: 8px; }++.ovas-icons .ovas-kind + .ovas-kind { margin-left: -7px; }++.ovas-kind .ovas-ic { width: 14px; height: 14px; }++.ovas-kind[data-k="edit"], .ovas-kind[data-k="write"] { color: #4ade80; background: color-mix(in srgb, #22c55e 14%, transparent); }++.ovas-kind[data-k="web"] { color: #38bdf8; background: color-mix(in srgb, #0ea5e9 14%, transparent); }++.ovas-kind[data-k="agent"] { color: #c084fc; background: color-mix(in srgb, #a855f7 16%, transparent); }++.ovas-sum { flex: 1; min-width: 0; display: grid; gap: 1px; }++.ovas-sum b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovas-sum small { font-size: 12px; color: var(--ovas-mut); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }++.ovas-delta { flex: none; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }++.ovas-delta .a { color: var(--ovas-ok); } .ovas-delta .r { color: var(--ovas-bad); margin-left: 4px; }++.ovas-state { flex: none; display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }++.ovas-state .ovas-ic { width: 15px; height: 15px; }++.ovas-state[data-s="ok"] .ovas-ic { color: var(--ovas-ok); } .ovas-state[data-s="fail"] { color: var(--ovas-bad); }++.ovas-spin { position: relative; display: inline-block; width: 4px; height: 4px; margin: 0 6px; border: 0; border-radius: 50%; background: var(--accent, #3b82f6); animation: ovas-hop 1s ease-in-out .15s infinite; }++.ovas-spin::before, .ovas-spin::after { content: ''; position: absolute; top: 0; width: 4px; height: 4px; border-radius: 50%; background: inherit; animation: ovas-hop 1s ease-in-out infinite; }++.ovas-spin::before { left: -6px; } .ovas-spin::after { left: 6px; animation-delay: .3s; }++@keyframes ovas-hop { 0%, 80%, 100% { transform: translateY(0); opacity: .45; } 40% { transform: translateY(-3px); opacity: 1; } }++.ovas-group > ol { list-style: none; margin: 0; padding: 0 8px 8px; display: grid; gap: 6px; }++.ovas-group.is-one { border: 0; background: none; }++.ovas-group.is-one > summary { display: none; }++.ovas-group.is-one > ol { padding: 0; }++.ovas-group.is-one .ovas-step { background: var(--ovas-s); border-color: var(--ovas-b); }++.ovas-group .ovas-step { background: var(--ovas-t); border-color: transparent; }++.ovas-detail { display: grid; gap: 10px; padding: 0 12px 12px; }++.ovas-label { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--ovas-mut); margin-bottom: -4px; }++.ovas-copy { display: inline-flex; align-items: center; gap: 4px; border: 0; background: none; color: var(--ovas-mut); font: inherit; font-size: 11.5px; font-weight: 650; letter-spacing: 0; text-transform: none; cursor: pointer; padding: 2px 4px; border-radius: 6px; }++.ovas-copy:hover { color: var(--text-primary, #e6edf7); background: var(--ovas-t); }++.ovas-copy .ovas-ic { width: 13px; height: 13px; }++.ovas-out.is-fail { box-shadow: inset 3px 0 0 var(--ovas-bad); }++.ovas-wait { font-size: 13px; color: var(--ovas-mut); display: flex; align-items: center; gap: 8px; }++.ovas-diff span { display: block; padding: 0 6px; margin: 0 -6px; }++.ovas-diff .a { background: var(--ovas-add); } .ovas-diff .r { background: var(--ovas-del); } .ovas-diff .h { color: var(--ovas-mut); }++.ovas-sub { margin: 0 8px 8px; padding: 8px 0 0 12px; border-left: 2px solid color-mix(in srgb, #a855f7 45%, transparent); display: grid; gap: 8px; }++.ovas-handoff { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--ovas-sec); }++.ovas-handoff::before, .ovas-handoff::after { content: ''; flex: 1; height: 1px; background: var(--ovas-b); }++.ovas-hand { display: grid; justify-items: center; gap: 4px; max-width: 82%; text-align: center; }++.ovas-hand-row { display: inline-flex; align-items: center; gap: 6px; }++.ovas-hand-row .ovas-ic { color: var(--ovas-mut); }++.ovas-hand small { font-size: 12.5px; color: var(--ovas-mut); line-height: 1.4; }++.ovas-note { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; padding: 9px 12px; border-radius: 12px; font-size: 13.5px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: var(--text-primary, #e6edf7); }++.ovas-note[data-level="error"] { background: color-mix(in srgb, #ef4444 13%, transparent); }++.ovas-note[data-level="info"] { background: color-mix(in srgb, #22c55e 10%, transparent); }++.ovas-note[data-level="info"] .ovas-ic { color: var(--ovas-ok); }++.ovas-note .ovas-ic { color: #f59e0b; margin-top: 2px; } .ovas-note[data-level="error"] .ovas-ic { color: var(--ovas-bad); }++.ovas-done { display: grid; gap: 6px; padding: 12px 14px; border-radius: 14px; border: 1px solid color-mix(in srgb, #22c55e 40%, transparent); background: color-mix(in srgb, #22c55e 8%, transparent); }++.ovas-done[data-ok="0"] { border-color: color-mix(in srgb, #ef4444 40%, transparent); background: color-mix(in srgb, #ef4444 8%, transparent); }++.ovas-done > b { display: flex; align-items: center; gap: 6px; font-size: 13px; }++.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 4px; font-size: 12.5px; color: var(--ovas-mut); }++.ovas-line::before { content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--ovas-mut); opacity: .7; }++.ovas-extra { display: grid; gap: 8px; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--ovas-b); background: var(--ovas-s); font-size: 13.5px; color: var(--ovas-sec); }++.ovas-empty { padding: 18px; text-align: center; color: var(--ovas-mut); font-size: 13.5px; border: 1px dashed var(--ovas-b); border-radius: 14px; }++.ovas [hidden] { display: none !important; }++@keyframes ovas-beat { 0%, 100% { transform: translateY(1px) scale(.8); opacity: .6; } 50% { transform: translateY(1px) scale(1); opacity: 1; } }++@media (hover: hover) { .ovas-group > summary:hover, .ovas-step > summary:hover { background: var(--bg-hover, rgba(255,255,255,.04)); } }++@media (max-width: 640px) {++ .ovas-head { padding: 10px 12px; border-radius: 14px; } .ovas-msg { font-size: 14.5px; }++ .ovas-code, .ovas-out, .ovas-diff { font-size: 12px; max-height: 45vh; }++ .ovas-bar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding: 0 2px; } .ovas-bar::-webkit-scrollbar { display: none; }++ .ovas-chip { flex: none; } .ovas-icons .ovas-kind + .ovas-kind { display: none; }++}++++/* Files changed (4.32.0): every file the agents edited or wrote, with its +/−; a tap opens its step. */++.ovas-files { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }++.ovas-file { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 46px; padding: 6px 12px; border-radius: 14px; border: 1px solid var(--ovas-b); background: var(--ovas-s); color: inherit; font: inherit; text-align: left; cursor: pointer; }++.ovas-file:hover { background: var(--bg-hover, rgba(255,255,255,.04)); }++.ovas-file:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++.ovas-file-n { flex: none; font-size: 12px; color: var(--ovas-mut); }++/* A step's raw input, folded. */++.ovas-raw > summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--ovas-mut); }++.ovas-raw > summary::-webkit-details-marker { display: none; }++.ovas-raw > summary .ovas-chev { width: 12px; height: 12px; }++.ovas-raw[open] > summary .ovas-chev { transform: rotate(90deg); }++.ovas-raw .ovas-label { margin: 8px 0 0; }++.ovas-raw pre { margin-top: 8px; }++/* Cost: per agent in the head, per step on its row when the agent reports it. */++.ovas-agent-cost { margin-left: 6px; font-weight: 600; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }++.ovas-cost { flex: none; font-size: 12px; font-weight: 650; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }++/* The action bar (Approve / Continue / Stop), sticky at the bottom while it is needed. */++.ovas-actions { position: sticky; bottom: max(8px, env(safe-area-inset-bottom, 0px)); z-index: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 16px; border: 1px solid var(--ovas-b); background: color-mix(in srgb, var(--ovas-s) 94%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: 0 10px 30px rgba(0,0,0,.25); }++.ovas-actions p { flex: 1 1 260px; min-width: 0; margin: 0; font-size: 13px; line-height: 1.4; color: var(--ovas-sec); }++.ovas-actions { justify-content: flex-end; }++.ovas-act { min-height: 38px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: var(--text-primary, #e6edf7); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }++.ovas-act.is-primary { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, var(--on-accent, #fff)); }++.ovas-act.is-danger { color: var(--ovas-bad); border-color: color-mix(in srgb, var(--ovas-bad) 45%, var(--ovas-b)); }++.ovas-act:disabled { opacity: .55; cursor: default; }++.ovas-act:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++++/* Attachments (4.50.0): the images and files a step read, wrote or produced. A group's header shows them while the++ group is folded; a message's sit under its text. The URLs come from the product; only the page's own origin and++ Media's load. */++.ovas-group > summary, .ovas-step > summary { flex-wrap: wrap; row-gap: 8px; }++.ovas-att-slot { flex-basis: 100%; order: 9; display: block; min-width: 0; }++.ovas-att-slot:empty { display: none; }++.ovas-att { display: grid; gap: 8px; width: 100%; min-width: 0; max-width: 100%; }++.ovas-att-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; min-width: 0; }++.ovas-att-img { box-sizing: border-box; display: block; width: 136px; flex: none; padding: 0; border: 1px solid var(--ovas-b); border-radius: 12px; overflow: hidden; background: var(--ovas-t); color: inherit; font: inherit; text-align: left; cursor: pointer; }++.ovas-att-img img { display: block; width: 136px; height: 92px; object-fit: cover; background: var(--ovas-t); }++.ovas-att-chip { box-sizing: border-box; display: inline-flex; align-items: center; gap: 7px; flex: none; max-width: 220px; min-height: 36px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--ovas-b); background: var(--ovas-t); color: var(--ovas-sec); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; }++.ovas-att-chip .ovas-ic { width: 15px; height: 15px; flex: none; color: var(--accent-light, var(--accent, #60a5fa)); }++.ovas-att-chip[data-role="read"] .ovas-ic { color: #38bdf8; }++.ovas-att-chip[data-role="wrote"] .ovas-ic { color: #4ade80; }++.ovas-att-cap { display: block; min-width: 0; font-size: 11.5px; line-height: 1.3; color: var(--ovas-mut); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }++.ovas-att-img .ovas-att-cap { padding: 5px 7px; }++.ovas-att-chip .ovas-att-cap { padding: 0; }++.ovas-att:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++.ovas-att-img:hover, .ovas-att-chip:hover { background: var(--bg-hover, rgba(255,255,255,.06)); }++/* The preview sheet: one attachment, opened by a tap. */++.ovas-sheet { box-sizing: border-box; position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 16px; background: color-mix(in srgb, var(--bg-primary, #0b1020) 80%, transparent); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }++.ovas-sheet[hidden] { display: none; }++.ovas-sheet-panel { box-sizing: border-box; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 10px; width: min(960px, 92vw); max-height: min(92vh, 920px); padding: 14px; border-radius: 18px; border: 1px solid var(--ovas-b); background: var(--ovas-s); color: var(--text-primary, #e6edf7); box-shadow: 0 24px 70px rgba(0,0,0,.45); }++.ovas-sheet-bar { display: flex; align-items: center; gap: 10px; min-width: 0; }++.ovas-sheet-bar b { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovas-sheet-x { width: 40px; height: 40px; flex: none; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: inherit; font: inherit; font-size: 15px; line-height: 1; cursor: pointer; }++.ovas-sheet-x:focus-visible, .ovas-sheet-foot a:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }++.ovas-sheet-body { min-width: 0; min-height: 0; overflow: auto; display: grid; }++.ovas-prev { min-width: 0; display: grid; }++.ovas-prev-img { display: block; width: 100%; height: auto; max-height: 74vh; object-fit: contain; border-radius: 12px; background: var(--ovas-t); touch-action: none; }++.ovas-prev-media { display: block; width: 100%; max-height: 74vh; border-radius: 12px; background: #000; }++.ovas-prev-audio { width: 100%; }++.ovas-prev-pdf { width: 100%; height: 72vh; border: 0; border-radius: 12px; background: #fff; }++.ovas-prev-code { margin: 0; padding: 12px; border-radius: 12px; border: 1px solid var(--ovas-b); background: var(--ovas-code); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; overflow: auto; max-height: 70vh; white-space: pre; }++.ovas-prev-text { min-width: 0; }++.ovas-prev-text .ovas-msg { padding: 0; }++.ovas-prev-note { margin: 0 0 10px; font-size: 13px; color: var(--ovas-mut); }++.ovas-sheet-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }++.ovas-sheet-foot a { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: inherit; text-decoration: none; font-size: 13px; font-weight: 700; }++.ovas-sheet-foot .ovas-sheet-open { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, #fff); }++.ovas-csv { width: 100%; border-collapse: collapse; font-size: 12.5px; }++.ovas-csv th, .ovas-csv td { border: 1px solid var(--ovas-b); padding: 5px 8px; text-align: left; white-space: nowrap; }++.ovas-csv th { background: var(--ovas-t); font-weight: 700; }++.ovas-tok-key { color: var(--accent-light, var(--accent, #60a5fa)); }++.ovas-tok-str { color: var(--ovas-ok); }++.ovas-tok-num { color: #fbbf24; }++.ovas-tok-kw { color: #c084fc; }++.ovas-tok-com { color: var(--ovas-mut); font-style: italic; }++++@media (max-width: 760px) {++ /* A phone: the head does not stick, so it never floats over the steps it sits above. */++ .ovas-head { position: static; }++}++@media (max-width: 640px) {++ /* Phones: bigger tap targets, and the preview sheet takes the whole screen. */++ .ovas-att-chip { min-height: 44px; padding: 0 14px; }++ .ovas-att-chip .ovas-att-cap { font-size: 12px; }++ .ovas-sheet { padding: 0; align-items: stretch; }++ .ovas-sheet-panel { width: 100%; max-height: 100vh; height: 100vh; border-radius: 0; border: 0; padding: 12px; }++ .ovas-sheet-x, .ovas-sheet-foot a { min-height: 44px; }++ .ovas-prev-img { max-height: 68vh; }++}++++/* ── Protocol 1.2 (4.205.0, ask 354): phases, thinking, sources, card mode ───────────────────────────────────── */++.ovas-phase { display: flex; align-items: center; gap: 9px; margin: 6px 2px 0; font-size: 12.5px; letter-spacing: .02em; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }++.ovas-phase b { font-weight: 750; color: var(--text-primary, #e6edf7); }++.ovas-phase-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--accent, #3b82f6); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); }++.ovas-phase[data-s="running"] .ovas-phase-dot { animation: ovasBreathe 1.6s ease-in-out infinite; }++.ovas-phase[data-s="done"] .ovas-phase-dot { background: var(--ovas-ok, #22c55e); box-shadow: none; }++.ovas-phase::after { content: ""; flex: 1; height: 1px; background: var(--ovas-b, rgba(255,255,255,.1)); }++@keyframes ovasBreathe { 50% { transform: scale(calc(1 - .25 * var(--ov-move, 1))); opacity: calc(1 - .5 * var(--ov-move, 1)); } }++.ovas-think details { border-radius: 12px; }++.ovas-think summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; font-size: 13px; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }++.ovas-think summary::-webkit-details-marker { display: none; }++.ovas-think-dots { width: 18px; height: 6px; flex: none; background: radial-gradient(circle, currentColor 2px, transparent 2.5px) 0 50% / 6px 6px repeat-x; animation: ovasDots 1.1s linear infinite; }++.ovas-think.is-done .ovas-think-dots { animation: none; opacity: .5; }++@keyframes ovasDots { 50% { opacity: .35; } }++.ovas-think-body { margin: 6px 0 0 26px; padding: 8px 12px; border-left: 2px solid var(--ovas-b, rgba(255,255,255,.1)); font-size: 13px; line-height: 1.5; white-space: pre-wrap; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); max-height: 320px; overflow: auto; }++.ovas-sources { border: 1px solid var(--ovas-b, rgba(255,255,255,.1)); border-radius: 14px; padding: 8px 12px; background: var(--ovas-t, rgba(255,255,255,.04)); }++.ovas-sources > summary { display: flex; align-items: center; gap: 10px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 650; }++.ovas-sources > summary::-webkit-details-marker { display: none; }++.ovas-src-tiles { display: inline-flex; }++.ovas-src-tile { display: inline-grid; place-items: center; width: 22px; height: 22px; margin-left: -6px; border-radius: 7px; font-size: 11px; font-weight: 800; color: #fff;++ background: hsl(var(--h, 210) 55% 42%); box-shadow: 0 0 0 2px var(--ovas-s, #121826); }++.ovas-src-tile:first-child { margin-left: 0; }++.ovas-sources ol { display: grid; gap: 6px; margin: 10px 0 2px; padding: 0; list-style: none; max-height: 340px; overflow: auto; }++.ovas-sources li { display: grid; gap: 1px; min-width: 0; }++.ovas-sources li a { color: var(--accent-light, var(--accent, #60a5fa)); text-decoration: none; font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovas-sources li a:hover { text-decoration: underline; }++.ovas-sources li small { color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); font-size: 11.5px; }++.ovas--card { gap: 8px; }++.ovas--card .ovas-head { position: static; padding: 10px 12px; }++.ovas--card :is(.ovas-bar, .ovas-files) { display: none !important; }++.ovas--card .ovas-feed > li:not(:nth-last-child(-n+3)) { display: none; }++.ovas--card .ovas-feed > li .ovas-out { max-height: 120px; }++.ovas-open { justify-self: start; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 10px; font-size: 13px; font-weight: 700; text-decoration: none;++ color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }++html[data-ov-motion="off"] :is(.ovas-phase-dot, .ovas-think-dots) { animation: none !important; }++/* 3. A card keeps its groups folded: the head says what they did. */++.ovas--card .ovas-group:not(.is-one) > :not(summary) { display: none; }++.ovas--card .ovas-group:not(.is-one) > summary .ovas-chev { display: none; }++++/* ── Compact runs (2026-11-11, the owner with the portal's run view open: "consider stuff like this … parse and expandable++ and compact mode … smart"): a group is one line, its state first, its words, the step it is on in mono, the chevron++ last; the steps inside are light rows; the head says what it is doing now; three dots while it works; a box to talk++ to it. Every motion follows --ov-move. */++.ovas-feed { gap: 6px; }++.ovas-group > summary { min-height: 42px; padding: 4px 10px; gap: 9px; }++.ovas-group:not(.is-one) > summary .ovas-state { order: -1; }++.ovas-group:not(.is-one) > summary .ovas-icons { display: none; }++.ovas-group:not(.is-one) > summary .ovas-chev { order: 99; transition: transform var(--ov-dur-2, .2s) var(--ov-ease-out, ease); }++.ovas-group[open]:not(.is-one) > summary .ovas-chev { transform: rotate(90deg); }++.ovas-group:not(.is-one) > summary .ovas-sum { display: flex; align-items: baseline; gap: 9px; }++.ovas-group:not(.is-one) > summary .ovas-sum b { flex: none; max-width: 60%; }++.ovas-group:not(.is-one) > summary .ovas-sum small { flex: 1 1 auto; min-width: 0; }++.ovas-group > ol .ovas-step { border: 0; background: transparent; border-radius: 10px; }++.ovas-group > ol .ovas-step > summary { min-height: 38px; padding: 3px 8px; }++.ovas-group > ol .ovas-step > summary:hover { background: color-mix(in srgb, var(--text-primary, #fff) 5%, transparent); }++.ovas-group > ol .ovas-kind { width: 24px; height: 24px; border-radius: 7px; }++.ovas-meta [data-m="now"] { flex: 1 1 100%; min-width: 0; order: -1; }++.ovas-now { display: flex; align-items: center; gap: 7px; min-width: 0; font: 12.5px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-primary, #e6edf7); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }++.ovas-now::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #3b82f6); animation: ovasBreathe 1.6s ease-in-out infinite; }++.ovas-working { display: flex; gap: 5px; padding: 4px 6px; }++.ovas-working[hidden] { display: none; }++.ovas-working i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #3b82f6); opacity: .35; animation: ovasWork 1.2s ease-in-out infinite; }++.ovas-working i:nth-child(2) { animation-delay: .16s; } .ovas-working i:nth-child(3) { animation-delay: .32s; }++@keyframes ovasWork { 40% { opacity: 1; transform: translateY(calc(-3px * var(--ov-move, 1))); } }++.ovas-you { justify-self: end; max-width: 85%; padding: 8px 12px; border-radius: 14px 14px 4px 14px; background: color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }++.ovas-you:not(.is-sent):not(.is-failed) { opacity: .6; }++.ovas-you.is-failed { outline: 1px solid var(--ovas-bad, #ef4444); }++.ovas-say { position: sticky; bottom: 0; display: grid; gap: 4px; padding: 8px; border-radius: 16px; border: 1px solid var(--ovas-b, rgba(255,255,255,.1)); background: var(--ovas-s, #121826); }++.ovas-say textarea { flex: 1; min-height: 40px; max-height: 160px; resize: none; padding: 9px 12px; border: 0; border-radius: 12px; background: var(--ovas-t, rgba(255,255,255,.05)); color: inherit; font: inherit; font-size: 15px; }++.ovas-say textarea:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 0; }++.ovas-say-send { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 12px; background: var(--accent-strong, var(--accent, #2563eb)); color: #fff; cursor: pointer; }++.ovas-say.is-busy .ovas-say-send { opacity: .5; }++html[data-ov-motion="off"] :is(.ovas-now, .ovas-working i)::before, html[data-ov-motion="off"] .ovas-working i { animation: none !important; }++.ovas-you-files { display: block; margin-top: 4px; font-size: 12px; opacity: .8; }++.ovas-say .ovt-c-row { display: flex; align-items: center; gap: 6px; }++.ovas-say .ovt-c-tools { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }++.ovas-say .ovt-c-note { margin: 0; font-size: 12px; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }++.ovas-say .ovt-c-note:empty { display: none; }nnew file mode 100644@@@ -0,0 +1,1056 @@++/**++ * openvibe-shared/agent-session.js — a coding agent's session as people read it: what it said, the steps it took++ * (commands, files read and changed, searches, sub-agents) grouped and folded, each step's command, diff and OUTPUT++ * (live while it runs), hand-offs between agents, the plan it keeps, and a summary that stays at the top (4.24.0).++ *++ * The owner, 2026-10-28, showing the Claude app's session view ("Ran 3 commands", a Bash sheet saying "Output++ * unavailable"): "OpenVibe.Codes should… actually show the output from the agent… more user friendly… handle that++ * between all agents… in a smart way… way better than this idea."++ *++ * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt, link })++ * link(url) → boolean: which https addresses in the agents' text become links (default:++ * all; a site whose agents read untrusted pages links only its own, e.g. a person's desk)++ * view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event++ * ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },++ * actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,++ * Command Code, Aider, DeepSeek, OpenVibe.AI, your own model) and every Actor agent speaks one++ * view.pushAll(events)++ * view.status('succeeded' | 'failed' | 'cancelled' | 'running' | 'queued' | …, label?)++ * view.actions([{ id, label, kind: 'primary' | 'danger' | undefined }], note?)++ * the sticky bar at the bottom (Approve, Continue, Stop…); opts.onAction(id) runs when one is++ * pressed (a returned promise keeps the buttons disabled until it settles); [] hides it++ * view.summary() → { agents, steps, commands, files, added, removed, problems, costUsd, durationMs }++ * view.destroy()++ * OpenVibeAgentSession.normalize(event) → [line] the stream-json → line mapping, for servers and other readers++ *++ * The lines (Codes' job protocol 1.1, additive to 1.0):++ * { t: 'agent', agent, model } an agent started (a hand-off starts the next one)++ * { t: 'text', text, parent? } what it said (light Markdown: paragraphs, lists, code, links)++ * { t: 'tool', id, name, what, input?, parent? } a step began: Bash, Read, Edit, Write, Grep, Glob, LS,++ * WebFetch, WebSearch, Task (a sub-agent), TodoWrite (the plan)++ * { t: 'tool_progress', id, chunk } output while it runs (agents whose steps the harness runs)++ * { t: 'tool_result', id, ok, output, cost_usd?, duration_ms? } how it ended, with its output (and its cost,++ * when the agent reports one: shown on the step's row)++ * { t: 'handoff', from, to?, reason } one agent stopped and the next took over++ * { t: 'note', text, level? } a warning or an error along the way++ * { t: 'step', text } the job's own progress (cloning, the branch, files changed, pushing)++ * { t: 'usage', input_tokens, output_tokens }++ * { t: 'done', ok, text, duration_ms, cost_usd, turns }++ * A step or a message may carry the images and files the agent read, wrote or produced:++ * attachments: [{ url, name, mime, size?, kind: 'image'|'video'|'audio'|'pdf'|'text'|'file', role: 'read'|'wrote'|'output' }]++ * The product supplies the URLs; the component never builds one from a local path. Any line may carry attachments,++ * and the component shows them for every line it draws. A line of a type it does not know (Codes' proof lines) is++ * drawn as a minimal row: its label or summary, then its attachments. A step's attachments show in its++ * group's header, visible while the group is folded; a message's show under its text. A tap opens a preview sheet: the++ * image at full width (pinch to zoom), video and audio in their players, a PDF in a frame, text as code, Markdown, a CSV++ * table or pretty JSON, plus Open and Download. An attachment loads only from the product's own origin or Media's,++ * never a third-party address the model printed; SVG is only ever an <img>, and text is set as text, never as markup.++ * A line from a sub-agent carries `parent`: the id of the Task step that started it, so it nests under that step.++ * Any line may carry `agent` (the head then follows who is working); a step without an id gets one, and the next result++ * of the same tool without an id is its result. { t: 'state', state } moves the head (Actor's task states).++ *++ * Everything is text: no line's content ever becomes markup (links are https only). The look is agent-session.css++ * (4.32.0), linked from beside this script unless the page links it; it follows the page's theme tokens, light or dark.++ * The Files tab lists every file edited or written with its +/−; each step keeps its raw input (folded); the head shows++ * each agent's cost from its done line.++ */++(function (root) {++ 'use strict';++++ // ── What each step is ─────────────────────────────────────++ const KIND = {++ Bash: 'command', bash: 'command', run_command: 'command', shell: 'command', exec: 'command',++ Read: 'read', read_file: 'read', Grep: 'search', grep: 'search', Glob: 'search', LS: 'search', list_files: 'search', search: 'search',++ Edit: 'edit', MultiEdit: 'edit', edit_file: 'edit', NotebookEdit: 'edit', Write: 'write', write_file: 'write', apply_patch: 'edit',++ WebFetch: 'web', WebSearch: 'web', Task: 'agent', Agent: 'agent', TodoWrite: 'plan',++ };++ // A tool no table knows is judged by the words of its name (run_shell, readFile, web-fetch), never by letters inside++ // a word ("Frobnicate" is not a cat).++ function kindOf(name) {++ if (KIND[name]) return KIND[name];++ const w = new Set(String(name || '').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z]+/).filter(Boolean));++ const any = (...xs) => xs.some((x) => w.has(x));++ // The web first: web_search and web_fetch are looking things up, not searching files.++ return any('web', 'fetch', 'http', 'browse', 'url', 'internet') ? 'web' : any('bash', 'shell', 'command', 'exec', 'run', 'terminal') ? 'command' : any('edit', 'patch', 'replace') ? 'edit'++ : any('write', 'create') ? 'write' : any('read', 'cat', 'view', 'open') ? 'read' : any('grep', 'glob', 'find', 'search', 'list', 'ls') ? 'search' : 'tool';++ }++ const NOUN = { command: ['command', 'commands'], read: ['read', 'reads'], search: ['search', 'searches'], edit: ['edit', 'edits'], write: ['new file', 'new files'], web: ['lookup', 'lookups'], agent: ['helper', 'helpers'], plan: ['plan', 'plan'], tool: ['tool', 'tools'] };++ const VERB = {++ command: ['Ran a command', 'Ran %d commands'], read: ['Read a file', 'Read %d files'], search: ['Searched once', 'Searched %d times'],++ edit: ['Edited a file', 'Edited %d files'], write: ['Wrote a file', 'Wrote %d files'], web: ['Looked something up', 'Looked up %d things'],++ agent: ['Started a helper', 'Started %d helpers'], plan: ['Updated the plan', 'Updated the plan'], tool: ['Used a tool', 'Used %d tools'],++ };++ const ICON = {++ command: '<path d="M4 6l5 5-5 5"/><path d="M11 17h9"/>',++ read: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/>',++ search: '<circle cx="11" cy="11" r="6"/><path d="m20 20-4.2-4.2"/>',++ edit: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="m13 7 4 4"/>',++ write: '<path d="M6 3h8l4 4v14H6z"/><path d="M12 11v6M9 14h6"/>',++ web: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.5 2.6 2.5 14.4 0 17M12 3.5c-2.5 2.6-2.5 14.4 0 17"/>',++ agent: '<rect x="5" y="7" width="14" height="11" rx="3"/><path d="M12 3v4M9 12h.01M15 12h.01"/>',++ plan: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17"/>',++ tool: '<path d="M14 7a4 4 0 0 0-5.6 5.1L4 16.5 7.5 20l4.4-4.4A4 4 0 0 0 17 10l-2.5 2.5-3-3z"/>',++ ok: '<path d="m5 12.5 4.5 4.5L19 7.5"/>', fail: '<path d="M6 6l12 12M18 6 6 18"/>', copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/>',++ handoff: '<path d="M4 12h13"/><path d="m13 7 5 5-5 5"/>', warn: '<path d="M12 4 2.5 20h19z"/><path d="M12 10v4M12 17h.01"/>',++ chevron: '<path d="m9 6 6 6-6 6"/>',++ img: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><circle cx="8.5" cy="9.5" r="1.6"/><path d="m4 17 4.5-4.5 3.5 3.5 3-3L20 17"/>',++ video: '<rect x="3" y="5" width="13" height="14" rx="2"/><path d="m16 10 5-3v10l-5-3z"/>',++ audio: '<path d="M9 17V6l10-2v11"/><circle cx="6.5" cy="17.5" r="2.5"/><circle cx="16.5" cy="15.5" r="2.5"/>',++ doc: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/><path d="M9 12.5h6M9 16h6"/>',++ code: '<path d="m9 8-5 4 5 4M15 8l5 4-5 4"/>',++ file: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/>',++ };++ const svg = (name, cls) => `<svg class="${cls || 'ovas-ic'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON[name] || ICON.tool}</svg>`;++ const AGENT_NAME = { claude: 'Claude Code', 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode', 'command-code': 'Command Code', cmd: 'Command Code', aider: 'Aider', deepseek: 'DeepSeek', 'openvibe-ai': 'OpenVibe.AI', ai: 'OpenVibe.AI', own: 'Your model' };++ const agentName = (a) => AGENT_NAME[String(a || '').toLowerCase()] || String(a || 'Agent');++++ // ── stream-json → lines (the same mapping as harness/job.js) ─++ const MAX_TEXT = 6000;++ const cut = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };++ function resultText(content) {++ if (typeof content === 'string') return content;++ if (Array.isArray(content)) return content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');++ return content == null ? '' : String(content);++ }++ // OpenVibe.Actor's task events (actor.task-event@1): { kind: 'state' | 'end', state } and { kind: 'output', step, agent,++ // text, tool, input, is_error }, step one of plan, text, handoff, check, tool_call, tool_result.++ function fromActor(e) {++ const agent = e.agent ? { agent: String(e.agent) } : {};++ if (e.kind === 'state' || e.kind === 'end') return e.state ? [{ t: 'state', state: String(e.state), ...agent }] : [];++ if (e.kind !== 'output') return [];++ const text = cut(e.text, MAX_TEXT);++ const att = e.attachments ? { attachments: e.attachments } : {};++ if (e.step === 'text') return [e.is_error ? { t: 'note', level: 'error', text, ...agent } : { t: 'text', text, ...att, ...agent }];++ if (e.step === 'plan') return [{ t: 'step', text, ...agent }];++ if (e.step === 'handoff') return [{ t: 'handoff', from: null, to: e.agent || null, reason: text, ...agent }];++ if (e.step === 'check') return [{ t: 'note', level: /^checked/i.test(text) ? 'info' : 'warning', text, ...agent }];++ if (e.step === 'tool_call') return [{ t: 'tool', name: String(e.tool || 'tool'), what: typeof e.input === 'string' ? cut(e.input, 300) : '', input: e.input && typeof e.input === 'object' ? e.input : { description: typeof e.input === 'string' ? cut(e.input, 300) : '' }, ...att, ...agent }];++ if (e.step === 'tool_result') return [{ t: 'tool_result', name: String(e.tool || 'tool'), ok: !e.is_error, output: cut(e.text, 8000), ...att, ...agent }];++ return text ? [{ t: 'text', text, ...agent }] : [];++ }++++ // ── AG-UI 1.0 (protocol 1.2, ask 354, 2026-11-11): every OpenVibe agent run streams AG-UI events (docs.ag-ui.com,++ // spec 1.0 of 2026-11-07) with OpenVibe's own words in metadata.ov (title, kind, sources, …), so another tool's agent++ // renders here and ours in theirs. Deltas (text, tool arguments, thinking) carry their ids; the view joins them.++ const pick = (e, ...k) => { for (const n of k) if (e[n] != null) return e[n]; return null; };++ function fromAgui(e) {++ const ov = (e.metadata && typeof e.metadata === 'object' && e.metadata.ov && typeof e.metadata.ov === 'object') ? e.metadata.ov : {};++ const src = Array.isArray(ov.sources) && ov.sources.length ? [{ t: 'sources', items: ov.sources }] : [];++ const id = (...k) => String(pick(e, ...k) || '');++ switch (e.type) {++ case 'RUN_STARTED': return [{ t: 'state', state: 'running' }];++ case 'RUN_FINISHED': return [...src, { t: 'done', ok: true, text: cut(typeof e.result === 'string' ? e.result : '', MAX_TEXT) }];++ case 'RUN_ERROR': return [{ t: 'done', ok: false, text: cut(String(e.message || 'The run failed.'), 2000) }];++ case 'STEP_STARTED': return [{ t: 'phase', id: id('stepName', 'step_name'), title: String(ov.title || pick(e, 'stepName', 'step_name') || 'Step'), status: 'running' }];++ case 'STEP_FINISHED': return [{ t: 'phase', id: id('stepName', 'step_name'), status: 'done' }];++ case 'TEXT_MESSAGE_START': return [{ t: 'text_start', id: id('messageId', 'message_id') }];++ case 'TEXT_MESSAGE_CONTENT': case 'TEXT_MESSAGE_CHUNK': return [{ t: 'text_delta', id: id('messageId', 'message_id'), delta: String(e.delta || '') }];++ case 'TOOL_CALL_START': return [{ t: 'tool', id: id('toolCallId', 'tool_call_id'), name: String(pick(e, 'toolCallName', 'tool_call_name') || 'tool'), input: {}, title: ov.title ? cut(ov.title, 160) : '', kind: ov.kind || null }];++ case 'TOOL_CALL_ARGS': case 'TOOL_CALL_CHUNK': return [{ t: 'tool_args', id: id('toolCallId', 'tool_call_id'), delta: String(e.delta || '') }];++ case 'TOOL_CALL_RESULT': return [...src, { t: 'tool_result', id: id('toolCallId', 'tool_call_id'), ok: !ov.error, output: cut(typeof e.content === 'string' ? e.content : JSON.stringify(e.content || ''), 8000), duration_ms: Number(ov.duration_ms) || 0, cost_usd: Number(ov.cost_usd) || 0 }];++ case 'REASONING_START': case 'REASONING_MESSAGE_START': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r' }];++ case 'REASONING_MESSAGE_CONTENT': case 'REASONING_MESSAGE_CHUNK': return [{ t: 'thinking_delta', id: id('messageId', 'message_id') || 'r', delta: String(e.delta || '') }];++ case 'REASONING_END': case 'REASONING_MESSAGE_END': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r', done: true }];++ case 'REASONING_ENCRYPTED_VALUE': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r', hidden: true }];++ case 'STATE_SNAPSHOT': {++ const plan = e.snapshot && Array.isArray(e.snapshot.plan) ? e.snapshot.plan : null;++ return plan ? [{ t: 'phases', items: plan.slice(0, 30).map((p) => ({ title: String(p.title || p.content || ''), status: String(p.status || 'pending') })) }] : [];++ }++ case 'ACTIVITY_SNAPSHOT': return e.content && e.content.title ? [{ t: 'activity', text: cut(String(e.content.title), 200) }] : [];++ case 'CUSTOM': {++ if (e.name === 'ov.sources' && Array.isArray(e.value)) return [{ t: 'sources', items: e.value }];++ if (e.name === 'ov.handoff' && e.value) return [{ t: 'handoff', from: e.value.from, to: e.value.to || null, reason: cut(e.value.reason, 600) }];++ if (e.name === 'ov.approval' && e.value) return [{ t: 'note', level: 'warning', text: cut(e.value.question || 'Waiting for your approval.', 600) }];++ return [];++ }++ default: return src;++ }++ }++ function normalize(e) {++ if (!e || typeof e !== 'object') return [];++ if (e.t) return [e];++ if (typeof e.type === 'string' && /^[A-Z][A-Z_]+$/.test(e.type)) return fromAgui(e);++ if (e.kind) return fromActor(e);++ const parent = e.parent_tool_use_id || null;++ if (e.type === 'system' && e.subtype === 'init') return [{ t: 'agent', agent: e.agent || 'claude', model: e.model || null }];++ if (e.type === 'system' && e.subtype === 'handoff') return [{ t: 'handoff', from: e.from, to: e.to || null, reason: cut(e.reason, 600) }];++ if (e.type === 'system' && (e.subtype === 'error' || e.subtype === 'warning')) return [{ t: 'note', level: e.subtype, text: cut(e.text, 600) }];++ if (e.type === 'system' && e.subtype === 'tool_progress') return [{ t: 'tool_progress', id: String(e.tool_use_id || ''), chunk: String(e.text || '') }];++ if (e.type === 'system' && e.subtype === 'usage' && e.usage) return [{ t: 'usage', input_tokens: e.usage.input_tokens || 0, output_tokens: e.usage.output_tokens || 0 }];++ if (e.type === 'assistant' && e.message) {++ const out = [];++ for (const c of e.message.content || []) {++ if (c.type === 'text' && c.text) out.push({ t: 'text', text: cut(c.text, MAX_TEXT), parent });++ if (c.type === 'tool_use') out.push({ t: 'tool', id: String(c.id || ''), name: String(c.name || ''), input: c.input || {}, parent });++ }++ if (e.message.usage) out.push({ t: 'usage', input_tokens: e.message.usage.input_tokens || 0, output_tokens: e.message.usage.output_tokens || 0 });++ return out;++ }++ if (e.type === 'user' && e.message && Array.isArray(e.message.content)) {++ return e.message.content.filter((c) => c && c.type === 'tool_result').map((c) => ({ t: 'tool_result', id: String(c.tool_use_id || ''), ok: !c.is_error, output: cut(resultText(c.content), 8000) }));++ }++ if (e.type === 'result') return [{ t: 'done', ok: !e.is_error, text: cut(e.result, MAX_TEXT), duration_ms: e.duration_ms || 0, cost_usd: e.total_cost_usd || 0, turns: e.num_turns || 0 }];++ return [];++ }++++ // ── Small helpers ─────────────────────────────────────────++ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));++ const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };++ const plural = (k, n) => (n === 1 ? VERB[k][0] : VERB[k][1].replace('%d', n));++ function dur(ms) {++ const s = Math.max(0, Math.round(ms / 1000));++ if (s < 60) return `${s} s`;++ const m = Math.floor(s / 60);++ return m < 60 ? `${m} min ${s % 60 ? `${s % 60} s` : ''}`.trim() : `${Math.floor(m / 60)} h ${m % 60} min`;++ }++ const base = (p) => String(p || '').split('/').filter(Boolean).pop() || String(p || '');++++ // ── Attachments: the images and files a step read, wrote or produced (4.50.0) ──────────++ // The shape a step or a message carries:++ // attachments: [{ url, name, mime, size?, kind: 'image'|'video'|'audio'|'pdf'|'text'|'file', role: 'read'|'wrote'|'output' }]++ // The product supplies the URLs; the component never builds one from a local path.++ const ATT_KINDS = new Set(['image', 'video', 'audio', 'pdf', 'text', 'file']);++ const ATT_ROLES = new Set(['read', 'wrote', 'output']);++ const ATT_ROLE_LABEL = { read: 'read', wrote: 'wrote', output: 'produced' };++ const ATT_ICON = { image: 'img', video: 'video', audio: 'audio', pdf: 'doc', text: 'code', file: 'file' };++ const ATT_EXT = {++ image: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'ico'],++ video: ['mp4', 'webm', 'mov', 'm4v', 'ogv'],++ audio: ['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac'],++ pdf: ['pdf'],++ text: ['md', 'markdown', 'txt', 'json', 'jsonl', 'csv', 'tsv', 'log', 'yaml', 'yml', 'html', 'css', 'js', 'mjs', 'cjs',++ 'ts', 'tsx', 'jsx', 'py', 'go', 'rs', 'sh', 'sql', 'xml', 'toml', 'ini', 'env', 'conf'],++ };++ const attExt = (name) => (String(name || '').split('?')[0].split('#')[0].split('.').pop() || '').toLowerCase();++ /** What kind of thing an attachment is: what it says, or what its name ends in. */++ function attKind(mime, name, kind) {++ if (ATT_KINDS.has(kind)) return kind;++ const m = String(mime || '').toLowerCase().split(';')[0].trim();++ if (m.startsWith('image/')) return 'image';++ if (m.startsWith('video/')) return 'video';++ if (m.startsWith('audio/')) return 'audio';++ if (m === 'application/pdf') return 'pdf';++ if (m.startsWith('text/')) return 'text';++ if (m === 'application/json' || m === 'application/xml' || m === 'application/x-yaml') return 'text';++ const ext = attExt(name);++ for (const k of Object.keys(ATT_EXT)) if (ATT_EXT[k].includes(ext)) return k;++ return 'file';++ }++ /** The attachments of a step or a message, in the shape the component draws them. */++ function normAttachments(list) {++ const out = [], seen = new Set();++ for (const a of Array.isArray(list) ? list : []) {++ if (!a || typeof a !== 'object') continue;++ const url = String(a.url || '').trim();++ if (!url || seen.has(url)) continue;++ seen.add(url);++ const name = String(a.name || '').trim() || base(String(url).split('?')[0]) || 'file';++ out.push({ url, name, mime: String(a.mime || ''), size: Number(a.size) || 0, kind: attKind(a.mime, name, a.kind), role: ATT_ROLES.has(a.role) ? a.role : 'output' });++ }++ return out;++ }++ /** The page's own origin, or Media's: an attachment loads from there and nowhere else. */++ function attAllowed(url, extra) {++ const raw = String(url || '').trim();++ if (!raw) return false;++ if (/^(blob:|data:image\/(png|jpe?g|gif|webp);)/i.test(raw)) return true; // the page made it itself++ if (raw.startsWith('/') && !raw.startsWith('//')) return true; // the product's own origin++ const here = (typeof location !== 'undefined' && location && location.hostname) ? String(location.hostname).toLowerCase() : '';++ let host = '';++ try { host = new URL(raw, here ? 'https://' + here : 'https://openvibe.network/').hostname.toLowerCase(); } catch { return false; }++ if (here && host === here) return true;++ if (host === 'openvibe.media' || host.endsWith('.openvibe.media')) return true;++ for (const o of Array.isArray(extra) ? extra : []) {++ const h = String(o || '').toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0];++ if (h && (host === h || host.endsWith('.' + h))) return true;++ }++ return false;++ }++ /** The tiles of a line: the images in one row, the other files as chips in a row under them. */++ function attStrip(list, onOpen) {++ const strip = el('div', 'ovas-att');++ const imgs = el('div', 'ovas-att-row');++ const chips = el('div', 'ovas-att-row');++ for (const a of list) {++ const b = el('button', a.kind === 'image' ? 'ovas-att-img' : 'ovas-att-chip');++ b.type = 'button';++ b.dataset.kind = a.kind;++ b.dataset.role = a.role;++ const role = ATT_ROLE_LABEL[a.role] || 'produced';++ b.title = role + ': ' + a.name;++ b.setAttribute('aria-label', 'Open ' + a.name + ' (' + role + ')');++ if (a.kind === 'image') {++ const img = el('img');++ img.loading = 'lazy'; img.decoding = 'async'; img.alt = a.name; img.src = a.url;++ img.addEventListener('error', () => { img.remove(); b.hidden = true; });++ b.appendChild(img);++ imgs.appendChild(b);++ } else {++ b.insertAdjacentHTML('beforeend', svg(ATT_ICON[a.kind] || 'file'));++ chips.appendChild(b);++ }++ b.appendChild(el('span', 'ovas-att-cap', a.name));++ b.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onOpen(a); });++ }++ if (imgs.childNodes.length) strip.appendChild(imgs);++ if (chips.childNodes.length) strip.appendChild(chips);++ return strip;++ }++++ /** A text file as the preview shows it: a CSV as a table, JSON pretty-printed, Markdown read, code highlighted. */++ function attText(name, text, linkOk) {++ const ext = attExt(name);++ let body = String(text == null ? '' : text);++ if (ext === 'json' || ext === 'jsonl') { try { body = JSON.stringify(JSON.parse(body), null, 2); } catch { /* not one document */ } }++ if (ext === 'csv' || ext === 'tsv') { const table = attCsv(body, ext === 'tsv' ? '\t' : ','); if (table) return table; }++ if (ext === 'md' || ext === 'markdown') {++ const frag = markdown(body, linkOk);++ frag.querySelectorAll('pre code').forEach((code) => { const hi = attHighlight(code.textContent, ''); code.textContent = ''; code.appendChild(hi); });++ return frag;++ }++ const pre = el('pre', 'ovas-prev-code');++ const code = el('code');++ code.appendChild(attHighlight(body, ext === 'json' ? 'json' : ''));++ pre.appendChild(code);++ return pre;++ }++ /** Pinch to zoom on a phone, double tap to double, so a screenshot can be read. */++ function attZoom(img) {++ let scale = 1, start = null;++ const apply = () => { img.style.transform = scale > 1 ? 'scale(' + scale + ')' : ''; };++ img.addEventListener('touchstart', (e) => {++ if (e.touches.length === 2) { const a = e.touches[0], b = e.touches[1]; start = { d: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY), s: scale }; }++ }, { passive: true });++ img.addEventListener('touchmove', (e) => {++ if (!start || e.touches.length !== 2) return;++ e.preventDefault();++ const a = e.touches[0], b = e.touches[1];++ const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);++ scale = Math.min(4, Math.max(1, start.s * (d / (start.d || 1)))); apply();++ }, { passive: false });++ img.addEventListener('touchend', () => { start = null; });++ img.addEventListener('dblclick', () => { scale = scale > 1 ? 1 : 2; apply(); });++ img.addEventListener('wheel', (e) => { if (!e.ctrlKey) return; e.preventDefault(); scale = Math.min(4, Math.max(1, scale - e.deltaY / 200)); apply(); }, { passive: false });++ }++ /** A small highlighter for the preview: strings, numbers, keywords and comments, as spans (never markup). */++ function attHighlight(text, lang) {++ const frag = document.createDocumentFragment();++ const json = lang === 'json';++ const re = json++ ? /("(?:[^"\\]|\\.)*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g++ : /(\/\/[^\n]*|#[^\n]*|--[^\n]*)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|\b(const|let|var|function|return|if|else|for|while|do|import|export|from|default|class|extends|new|await|async|try|catch|finally|throw|def|then|null|true|false|None|True|False)\b|(\b\d+(?:\.\d+)?\b)/g;++ const push = (s, cls) => { if (!s) return; frag.appendChild(cls ? el('span', cls, s) : document.createTextNode(s)); };++ let at = 0, m;++ while ((m = re.exec(text))) {++ push(text.slice(at, m.index));++ if (json) {++ if (m[1] != null) { push(m[1], m[2] != null ? 'ovas-tok-key' : 'ovas-tok-str'); if (m[2] != null) push(m[2], ''); }++ else if (m[3] != null) push(m[3], 'ovas-tok-kw');++ else push(m[4], 'ovas-tok-num');++ } else if (m[1] != null) push(m[1], 'ovas-tok-com');++ else if (m[2] != null) push(m[2], 'ovas-tok-str');++ else if (m[3] != null) push(m[3], 'ovas-tok-kw');++ else push(m[4], 'ovas-tok-num');++ at = m.index + m[0].length;++ }++ push(text.slice(at));++ return frag;++ }++ /** A CSV as a table (quoted cells kept whole). */++ function attCsv(text, delim) {++ const rows = String(text || '').split(/\r?\n/).filter((l) => l.trim() !== '');++ if (!rows.length) return null;++ const split = (line) => {++ const out = []; let cur = '', q = false;++ for (let i = 0; i < line.length; i++) {++ const ch = line[i];++ if (q) { if (ch === '"') { if (line[i + 1] === '"') { cur += '"'; i++; } else q = false; } else cur += ch; }++ else if (ch === '"') q = true;++ else if (ch === delim) { out.push(cur.trim()); cur = ''; }++ else cur += ch;++ }++ out.push(cur.trim());++ return out;++ };++ const head = split(rows[0]);++ const table = el('table', 'ovas-csv');++ const thead = el('thead'), tr = el('tr');++ head.forEach((h) => tr.appendChild(el('th', '', h || ' ')));++ thead.appendChild(tr); table.appendChild(thead);++ const tb = el('tbody');++ for (const line of rows.slice(1, 200)) {++ const r = el('tr'), cells = split(line);++ for (let i = 0; i < head.length; i++) r.appendChild(el('td', '', cells[i] == null ? '' : cells[i]));++ tb.appendChild(r);++ }++ table.appendChild(tb);++ return table;++ }++++ /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links (only++ * those `linkOk(url)` allows; the others stay text). */++ function markdown(text, linkOk) {++ const frag = document.createDocumentFragment();++ const inline = (s, into) => {++ const re = /(`[^`]+`|\*\*[^*]+\*\*|https:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;++ let at = 0, m;++ while ((m = re.exec(s))) {++ if (m.index > at) into.appendChild(document.createTextNode(s.slice(at, m.index)));++ const tok = m[0];++ if (tok[0] === '`') into.appendChild(el('code', '', tok.slice(1, -1)));++ else if (tok.startsWith('**')) into.appendChild(el('strong', '', tok.slice(2, -2)));++ else if (linkOk && !linkOk(tok)) into.appendChild(document.createTextNode(tok));++ else { const a = el('a', '', tok); a.href = tok; a.target = '_blank'; a.rel = 'noopener noreferrer'; into.appendChild(a); }++ at = m.index + tok.length;++ }++ if (at < s.length) into.appendChild(document.createTextNode(s.slice(at)));++ };++ const blocks = String(text || '').replace(/\r/g, '').split(/\n```/);++ String(text || '').replace(/\r/g, '').split(/(```[\s\S]*?(?:```|$))/).forEach((part) => {++ if (!part) return;++ if (part.startsWith('```')) { const pre = el('pre', 'ovas-code'); pre.appendChild(el('code', '', part.replace(/^```[^\n]*\n?/, '').replace(/```$/, ''))); frag.appendChild(pre); return; }++ part.split(/\n{2,}/).forEach((para) => {++ const lines = para.split('\n').filter((l) => l.trim());++ if (!lines.length) return;++ if (lines.every((l) => /^\s*([-*•]|\d+[.)])\s+/.test(l))) {++ const ol = /^\s*\d/.test(lines[0]);++ const list = el(ol ? 'ol' : 'ul');++ lines.forEach((l) => { const li = el('li'); inline(l.replace(/^\s*([-*•]|\d+[.)])\s+/, ''), li); list.appendChild(li); });++ frag.appendChild(list);++ } else { const p = el('p'); lines.forEach((l, i) => { if (i) p.appendChild(el('br')); inline(l.replace(/^#{1,6}\s+/, ''), p); }); frag.appendChild(p); }++ });++ });++ void blocks;++ return frag;++ }++++ /** A line diff of two texts (LCS for small inputs, both sides whole beyond): [['=', line] | ['-', line] | ['+', line]]. */++ function diffLines(a, b) {++ const x = String(a || '').split('\n'), y = String(b || '').split('\n');++ if (x.length * y.length > 90000) return x.map((l) => ['-', l]).concat(y.map((l) => ['+', l]));++ const n = x.length, m = y.length, L = Array.from({ length: n + 1 }, () => new Uint16Array(m + 1));++ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = x[i] === y[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);++ const out = []; let i = 0, j = 0;++ while (i < n && j < m) { if (x[i] === y[j]) { out.push(['=', x[i]]); i++; j++; } else if (L[i + 1][j] >= L[i][j + 1]) out.push(['-', x[i++]]); else out.push(['+', y[j++]]); }++ while (i < n) out.push(['-', x[i++]]);++ while (j < m) out.push(['+', y[j++]]);++ return out;++ }++++ /** What a step shows on its row and inside: title, the thing it touched, a diff or content, the counts. */++ function describe(step) {++ const i = step.input || {};++ const k = step.kind;++ const out = { title: '', sub: '', command: '', diff: null, preview: '', added: 0, removed: 0 };++ if (k === 'command') { out.command = String(i.command || i.cmd || step.what || ''); out.title = String(i.description || '') || out.command.split('\n')[0]; out.sub = i.description ? out.command.split('\n')[0] : ''; }++ else if (k === 'read') { const p = i.file_path || i.path || step.what; out.title = `Read ${base(p)}`; out.sub = String(p || ''); }++ else if (k === 'search') { out.title = i.pattern ? `Searched for ${i.pattern}` : `Listed ${base(i.path || step.what) || 'files'}`; out.sub = String(i.path || i.glob || step.what || ''); }++ else if (k === 'edit' || k === 'write') {++ const p = i.file_path || i.path || i.notebook_path || step.what;++ out.title = `${k === 'write' ? 'Wrote' : 'Edited'} ${base(p) || 'a file'}`; out.sub = String(p || '');++ const edits = Array.isArray(i.edits) ? i.edits : (i.old_string != null || i.new_string != null) ? [{ old_string: i.old_string, new_string: i.new_string }] : [];++ if (edits.length) { out.diff = []; edits.forEach((e, n) => { if (n) out.diff.push(['@', '…']); out.diff.push(...diffLines(e.old_string, e.new_string)); }); }++ else if (typeof i.content === 'string') { out.preview = i.content; out.diff = i.content.split('\n').map((l) => ['+', l]); }++ if (out.diff) out.diff.forEach(([op]) => { if (op === '+') out.added++; else if (op === '-') out.removed++; });++ } else if (k === 'web') {++ const asked = i.query || (!i.url && !/^https?:\/\//.test(String(i.description || step.what || '')) ? i.description || step.what : '');++ const url = i.url || (/^https?:\/\//.test(String(i.description || step.what || '')) ? i.description || step.what : '');++ out.title = asked ? `Looked up ${asked}` : `Opened ${String(url).replace(/^https?:\/\//, '').slice(0, 60)}`; out.sub = asked ? '' : String(url);++ }++ else if (k === 'agent') { out.title = String(i.description || 'A helper agent'); out.sub = i.subagent_type ? `${i.subagent_type} helper` : 'helper'; out.preview = String(i.prompt || ''); }++ else if (k === 'plan') { out.title = 'Updated the plan'; }++ else { out.title = String(step.name || 'Tool'); out.sub = String(step.what || ''); }++ if (!out.title) out.title = String(step.what || step.name || 'Step');++ if (step.title) out.title = String(step.title); // the agent's own words for it (AG-UI metadata.ov.title)++ return out;++ }++++ // ── Styles: agent-session.css, from beside this script (theme tokens; light and dark) ─────────────────++ const SELF_SRC = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';++ function styles() {++ if (typeof document === 'undefined' || document.getElementById('openvibe-agent-session-styles') || document.querySelector('link[href*="agent-session.css"]')) return;++ const m = /^(https?:\/\/[^?#]*\/)agent-session\.js(?:[?#]|$)/.exec(SELF_SRC);++ const link = document.createElement('link');++ link.id = 'openvibe-agent-session-styles';++ link.rel = 'stylesheet';++ link.href = `${m ? m[1] : 'https://openvibe.network/shared/'}agent-session.css`;++ document.head.appendChild(link);++ }++++ // ── The view ──────────────────────────────────────────────++ function mount(host, opts = {}) {++ styles();++ const linkOk = typeof opts.link === 'function' ? (u) => { try { return !!opts.link(u); } catch { return false; } } : null;++ // Attachments load from the product's own origin or Media's; a product that serves files from another host++ // says so: mount(el, { origins: ['files.openvibe.codes'] }).++ const origins = Array.isArray(opts.origins) ? opts.origins : (opts.origins ? [opts.origins] : []);++ const safeAtts = (list) => normAttachments(list).filter((a) => attAllowed(a.url, origins));++ const LIVE = ['running', 'placed', 'queued', 'verifying'];++ // The clock runs from startedAt, or from now for a run that is live as it mounts; a finished run replayed without++ // a start time shows the time only once its done line says it.++ const clock = Boolean(opts.startedAt) || LIVE.includes(opts.status || 'running');++ const S = { phases: [], thinks: new Map(), msgs: new Map(), sources: new Map(), activity: '', ownPlan: false, status: opts.status || 'running', label: opts.label || '', startedAt: opts.startedAt || Date.now(), endedAt: null, agents: [], steps: new Map(), plan: null, tokens: { in: 0, out: 0 }, cost: 0, agentCost: new Map(), durationMs: 0, problems: 0, filter: 'all', follow: opts.follow !== false, lastGroup: null };++ host.textContent = '';++ const box = el('div', 'ovas');++ box.innerHTML = `<div class="ovas-head"><div class="ovas-top"><span class="ovas-pill" data-s="${esc(S.status)}"></span><span class="ovas-title"></span></div><div class="ovas-meta"><span class="ovas-agents"></span><span data-m="now" hidden></span><span data-m="time"></span><span data-m="steps"></span><span data-m="files"></span><span data-m="cost"></span></div><details class="ovas-plan" hidden><summary><span class="ovas-plan-now"></span><span class="ovas-plan-n"></span></summary><div class="ovas-plan-bar"><i style="width:0"></i></div><ol></ol></details></div>`++ + '<div class="ovas-bar" role="toolbar" aria-label="Show"></div><ol class="ovas-files" hidden></ol><ol class="ovas-feed" aria-live="polite"></ol><div class="ovas-actions" hidden></div>';++ host.appendChild(box);++ const head = box.querySelector('.ovas-head'), feed = box.querySelector('.ovas-feed'), bar = box.querySelector('.ovas-bar');++ const filesEl = box.querySelector('.ovas-files'), actBar = box.querySelector('.ovas-actions');++ // The sources strip (protocol 1.2) under the feed; card mode (opts.mode 'card': a chat message, the Dock, a feed card)++ // keeps the head, the plan row, the newest three lines and the sources, with a way to the whole run (opts.href).++ const srcBox = el('details', 'ovas-sources'); srcBox.hidden = true;++ srcBox.innerHTML = '<summary><span class="ovas-src-tiles"></span><span class="ovas-src-n"></span></summary><ol></ol>';++ box.insertBefore(srcBox, actBar);++ if (opts.mode === 'card') {++ box.classList.add('ovas--card');++ if (typeof opts.href === 'string' && /^(https:\/\/|\/)/.test(opts.href)) { const more = el('a', 'ovas-open', 'Open the whole run'); more.href = opts.href; box.insertBefore(more, actBar); }++ }++ head.querySelector('.ovas-title').textContent = opts.title || 'Agent session';++ const FILTERS = [['all', 'Everything'], ['said', 'What it said'], ['steps', 'Steps'], ['files', 'Files'], ['problems', 'Problems']];++ FILTERS.forEach(([id, label]) => { const b = el('button', 'ovas-chip', label); b.type = 'button'; b.dataset.f = id; b.setAttribute('aria-pressed', String(id === 'all')); bar.appendChild(b); });++ const follow = el('button', 'ovas-chip ovas-follow', 'Follow'); follow.type = 'button'; follow.setAttribute('aria-pressed', String(S.follow)); follow.title = 'Keep the newest step in view'; bar.appendChild(follow);++ bar.addEventListener('click', (e) => {++ const b = e.target.closest('button'); if (!b) return;++ if (b === follow) { S.follow = !S.follow; follow.setAttribute('aria-pressed', String(S.follow)); return; }++ S.filter = b.dataset.f; bar.querySelectorAll('[data-f]').forEach((x) => x.setAttribute('aria-pressed', String(x === b))); applyFilter();++ });++ box.addEventListener('click', (e) => {++ const c = e.target.closest('.ovas-copy'); if (!c) return;++ e.preventDefault(); e.stopPropagation();++ const pre = c.parentElement && c.parentElement.nextElementSibling;++ const text = pre ? pre.textContent : '';++ try { navigator.clipboard.writeText(text); c.lastChild.textContent = 'Copied'; setTimeout(() => { c.lastChild.textContent = 'Copy'; }, 1400); } catch { /* no clipboard */ }++ });++ const empty = el('li', 'ovas-empty', opts.empty || 'Waiting for the agent to start…'); feed.appendChild(empty);++ // The run is still working: three dots after the newest line (gone when it ends).++ const working = el('div', 'ovas-working'); working.setAttribute('aria-hidden', 'true'); working.innerHTML = '<i></i><i></i><i></i>';++ working.hidden = !LIVE.includes(S.status); box.insertBefore(working, box.querySelector('.ovas-actions'));++++ // ── The preview sheet: one attachment, opened by a tap ──────────────────────────++ const sheet = el('div', 'ovas-sheet');++ sheet.hidden = true;++ sheet.innerHTML = '<div class="ovas-sheet-panel" role="dialog" aria-modal="true" aria-label="Preview"><div class="ovas-sheet-bar"><b class="ovas-sheet-name"></b><button type="button" class="ovas-sheet-x" aria-label="Close the preview">✕</button></div><div class="ovas-sheet-body"></div><div class="ovas-sheet-foot"><a class="ovas-sheet-open" target="_blank" rel="noopener noreferrer">Open</a><a class="ovas-sheet-dl" download>Download</a></div></div>';++ host.appendChild(sheet);++ let sheetBack = null;++ function closeSheet() {++ if (sheet.hidden) return;++ sheet.hidden = true;++ sheet.querySelector('.ovas-sheet-body').textContent = '';++ if (sheetBack && sheetBack.focus) sheetBack.focus();++ sheetBack = null;++ }++ function openSheet(a) {++ const body = sheet.querySelector('.ovas-sheet-body');++ body.textContent = '';++ sheet.querySelector('.ovas-sheet-name').textContent = a.name;++ const open = sheet.querySelector('.ovas-sheet-open'); open.href = a.url; open.setAttribute('aria-label', 'Open ' + a.name);++ const dl = sheet.querySelector('.ovas-sheet-dl'); dl.href = a.url; dl.setAttribute('download', a.name);++ const wrap = el('div', 'ovas-prev');++ if (a.kind === 'image') {++ const img = el('img', 'ovas-prev-img'); img.alt = a.name; img.src = a.url; wrap.appendChild(img); attZoom(img);++ } else if (a.kind === 'video') {++ const v = el('video', 'ovas-prev-media'); v.controls = true; v.playsInline = true; v.preload = 'metadata'; v.src = a.url; wrap.appendChild(v);++ } else if (a.kind === 'audio') {++ const au = el('audio', 'ovas-prev-audio'); au.controls = true; au.preload = 'metadata'; au.src = a.url; wrap.appendChild(au);++ } else if (a.kind === 'pdf') {++ const f = el('iframe', 'ovas-prev-pdf'); f.src = a.url; f.title = a.name; wrap.appendChild(f);++ } else if (a.kind === 'text') {++ const holder = el('div', 'ovas-prev-text');++ holder.appendChild(el('p', 'ovas-prev-note', 'Loading…'));++ wrap.appendChild(holder);++ fetch(a.url, { credentials: 'same-origin' })++ .then((r) => (r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status))))++ .then((t) => { holder.textContent = ''; holder.appendChild(attText(a.name, t, linkOk)); })++ .catch((err) => { holder.textContent = 'Could not open it: ' + err.message; });++ } else {++ wrap.appendChild(el('p', 'ovas-prev-note', 'There is no preview for this file. Open it or download it.'));++ }++ body.appendChild(wrap);++ sheetBack = (typeof document !== 'undefined' && document.activeElement) || null;++ sheet.hidden = false;++ const x = sheet.querySelector('.ovas-sheet-x'); if (x && x.focus) x.focus();++ }++ sheet.querySelector('.ovas-sheet-x').addEventListener('click', closeSheet);++ sheet.addEventListener('click', (e) => { if (e.target === sheet) closeSheet(); });++ document.addEventListener('keydown', (e) => {++ if (sheet.hidden) return;++ if (e.key === 'Escape') { e.preventDefault(); closeSheet(); return; }++ if (e.key !== 'Tab') return;++ const f = [...sheet.querySelectorAll('button, a[href], iframe')].filter((n) => !n.disabled && n.offsetParent !== null);++ if (!f.length) return;++ const first = f[0], last = f[f.length - 1];++ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }++ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }++ });++++ function applyFilter() {++ const files = S.filter === 'files';++ feed.hidden = files; filesEl.hidden = !files;++ if (files) { paintFiles(); return; }++ feed.querySelectorAll(':scope > li').forEach((li) => {++ const k = li.dataset.k || '';++ li.hidden = S.filter === 'all' ? false : S.filter === 'said' ? !(k === 'msg' || k === 'done' || k === 'handoff') : S.filter === 'steps' ? !(k === 'steps' || k === 'handoff') : !(li.dataset.p === '1');++ });++ }++ // The Files tab: one row per file, newest change last; a tap shows everything and opens that file's last step.++ function changedFiles() {++ const by = new Map();++ for (const st of S.steps.values()) {++ if (st.hidden || (st.kind !== 'edit' && st.kind !== 'write') || !st.info) continue;++ const p = st.info.sub || st.info.title;++ const f = by.get(p) || { path: p, added: 0, removed: 0, n: 0, last: null, wrote: false };++ f.added += st.info.added || 0; f.removed += st.info.removed || 0; f.n++; f.last = st; f.wrote = f.wrote || st.kind === 'write';++ by.set(p, f);++ }++ return [...by.values()];++ }++ function paintFiles() {++ filesEl.textContent = '';++ const list = changedFiles();++ if (!list.length) { filesEl.appendChild(el('li', 'ovas-empty', 'No files changed yet.')); return; }++ for (const f of list) {++ const li = el('li');++ const b = el('button', 'ovas-file'); b.type = 'button'; b.dataset.step = f.last.id;++ b.innerHTML = `<span class="ovas-kind" data-k="${f.wrote ? 'write' : 'edit'}">${svg(f.wrote ? 'write' : 'edit')}</span><span class="ovas-sum"><b></b><small></small></span><span class="ovas-delta"><span class="a">+${f.added}</span><span class="r">−${f.removed}</span></span><span class="ovas-file-n"></span>`;++ b.querySelector('b').textContent = base(f.path) || f.path;++ b.querySelector('small').textContent = f.path;++ b.querySelector('.ovas-file-n').textContent = f.n > 1 ? `${f.n} changes` : '';++ li.appendChild(b); filesEl.appendChild(li);++ }++ }++ filesEl.addEventListener('click', (e) => {++ const b = e.target.closest('.ovas-file'); if (!b) return;++ const st = S.steps.get(b.dataset.step); if (!st || !st.el) return;++ S.filter = 'all'; bar.querySelectorAll('[data-f]').forEach((x) => x.setAttribute('aria-pressed', String(x.dataset.f === 'all'))); applyFilter();++ for (let p = st.el; p; p = p.parentElement) if (p.tagName === 'DETAILS') p.open = true;++ if (st.el.scrollIntoView) st.el.scrollIntoView({ block: 'center', behavior: 'smooth' });++ });++++ // The action bar: what the person can do now (the page decides; this only draws and reports the press).++ function paintActions(list, note) {++ actBar.textContent = '';++ const acts = (Array.isArray(list) ? list : []).filter((a) => a && a.id && a.label).slice(0, 4);++ actBar.hidden = !acts.length;++ if (!acts.length) return;++ if (note) actBar.appendChild(el('p', '', note));++ for (const a of acts) { const b = el('button', `ovas-act${a.kind === 'primary' ? ' is-primary' : a.kind === 'danger' ? ' is-danger' : ''}`, a.label); b.type = 'button'; b.dataset.act = a.id; actBar.appendChild(b); }++ }++ actBar.addEventListener('click', async (e) => {++ const b = e.target.closest('.ovas-act'); if (!b || typeof opts.onAction !== 'function') return;++ const all = [...actBar.querySelectorAll('.ovas-act')]; all.forEach((x) => { x.disabled = true; });++ try { await opts.onAction(b.dataset.act); } catch { /* the page says what went wrong */ } finally { all.forEach((x) => { x.disabled = false; }); }++ });++ if (Array.isArray(opts.actions)) paintActions(opts.actions, opts.actionsNote);++++ const keep = () => { if (S.follow && S.status === 'running') { const last = feed.lastElementChild; if (last && last.scrollIntoView) last.scrollIntoView({ block: 'end', behavior: 'smooth' }); } };++++ // A step's element: <details> with the summary row and, once opened, its detail.++ function stepEl(step) {++ const d = el('details', 'ovas-step');++ d.dataset.id = step.id;++ const sum = el('summary');++ const info = describe(step); step.info = info;++ sum.innerHTML = `${svg('chevron', 'ovas-chev')}<span class="ovas-kind" data-k="${esc(step.kind)}">${svg(step.kind)}</span><span class="ovas-sum"><b></b><small></small></span>${info.added || info.removed ? `<span class="ovas-delta"><span class="a">+${info.added}</span><span class="r">−${info.removed}</span></span>` : ''}<span class="ovas-state" data-s="run"><span class="ovas-spin" aria-label="Running"></span><span data-t></span></span><span class="ovas-att-slot"></span>`;++ sum.querySelector('b').textContent = info.title;++ sum.querySelector('small').textContent = info.sub;++ if (!info.sub) sum.querySelector('small').hidden = true;++ d.appendChild(sum);++ const detail = el('div', 'ovas-detail'); d.appendChild(detail);++ const section = (label, node, copy) => { const l = el('div', 'ovas-label'); l.appendChild(el('span', '', label)); if (copy) { const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); } detail.append(l, node); };++ if (step.kind === 'command' && info.command) { const pre = el('pre', 'ovas-code'); pre.textContent = info.command; section('Command', pre, true); }++ if (info.diff) { const pre = el('pre', 'ovas-diff'); info.diff.slice(0, 600).forEach(([op, line]) => { const s = el('span', op === '+' ? 'a' : op === '-' ? 'r' : op === '@' ? 'h' : '', `${op === '=' ? ' ' : op === '@' ? '' : op} ${line}`); pre.appendChild(s); }); section(step.kind === 'write' ? 'New file' : 'Change', pre, false); }++ if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600), linkOk)); section('Asked to', p, false); }++ if (step.kind !== 'plan' && step.input && typeof step.input === 'object' && Object.keys(step.input).length) {++ const raw = el('details', 'ovas-raw');++ raw.innerHTML = `<summary>${svg('chevron', 'ovas-chev')}<span>Input</span></summary>`;++ const l = el('div', 'ovas-label'); l.appendChild(el('span', '', 'What the agent sent')); const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c);++ let text = ''; try { text = JSON.stringify(step.input, null, 2); } catch { text = String(step.input); }++ const pre = el('pre', 'ovas-code'); pre.textContent = text.length > 8000 ? `${text.slice(0, 8000)}\n…` : text;++ raw.append(l, pre); detail.appendChild(raw);++ }++ if (step.kind === 'agent') { const sub = el('div', 'ovas-sub'); step.subEl = sub; d.appendChild(sub); d.open = true; }++ if (step.kind !== 'agent' && step.kind !== 'plan') {++ const pre = el('pre', 'ovas-out'); pre.hidden = true; step.outEl = pre;++ const wait = el('div', 'ovas-wait'); wait.innerHTML = '<span class="ovas-spin"></span><span>Running — the output shows here as it arrives</span>'; step.waitEl = wait;++ const l = el('div', 'ovas-label'); l.appendChild(el('span', '', 'Output')); const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); step.outLabel = l;++ detail.append(l, pre, wait);++ }++ step.el = d; step.stateEl = sum.querySelector('.ovas-state');++ return d;++ }++ function setStepState(step) {++ const st = step.stateEl; if (!st) return;++ const t = step.ended ? dur(step.ended - step.started) : dur(Date.now() - step.started);++ if (step.done) {++ st.dataset.s = step.ok ? 'ok' : 'fail';++ const c = step.cost > 0 ? `$${step.cost < 0.01 ? step.cost.toFixed(4).replace(/0+$/, '') : step.cost.toFixed(2)}` : '';++ st.innerHTML = `${c ? `<span class="ovas-cost">${esc(c)}</span>` : ''}${svg(step.ok ? 'ok' : 'fail')}<span>${esc(step.ended - step.started > 1500 ? t : '')}</span>`;++ } else { const tt = st.querySelector('[data-t]'); if (tt && Date.now() - step.started > 2000) tt.textContent = t; }++ }++ // The group a new step goes in: the last item of its feed when that is a group of steps, else a new one.++ function groupFor(parentFeed) {++ const last = parentFeed.lastElementChild;++ if (last && last.classList.contains('ovas-group-li')) return last;++ const li = el('li', 'ovas-group-li'); li.dataset.k = 'steps';++ const g = el('details', 'ovas-group');++ g.innerHTML = `<summary>${svg('chevron', 'ovas-chev')}<span class="ovas-icons"></span><span class="ovas-sum"><b></b><small></small></span><span class="ovas-delta" hidden><span class="a"></span><span class="r"></span></span><span class="ovas-state"></span><span class="ovas-att-slot"></span></summary><ol></ol>`;++ li.appendChild(g); parentFeed.appendChild(li);++ li.steps = [];++ return li;++ }++ function paintGroup(li) {++ const steps = li.steps, g = li.firstChild, count = {};++ steps.forEach((s) => { count[s.kind] = (count[s.kind] || 0) + 1; });++ const order = ['command', 'edit', 'write', 'read', 'search', 'web', 'agent', 'plan', 'tool'];++ const kinds = order.filter((k) => count[k]);++ g.querySelector('.ovas-icons').innerHTML = kinds.slice(0, 3).map((k) => `<span class="ovas-kind" data-k="${k}">${svg(k)}</span>`).join('');++ // A few steps read as what was done ("Ran 2 commands"); more as counts ("2 commands · 1 edit · 3 reads").++ // Steps the agent named itself (AG-UI metadata.ov.title) read as its words: the newest title and how many there are.++ const titled = steps.filter((s2) => s2.title);++ // Rows under one shared title say what each one did instead of the title again: its address, query or file.++ const sharedTitle = titled.length > 1 && titled.every((s2) => s2.title === titled[0].title);++ for (const s2 of steps) {++ const b2 = s2.el && s2.el.querySelector(':scope>summary .ovas-sum b');++ if (!b2 || !s2.info) continue;++ // An address reads as its page and its site ("OpenVibe.Codes · github.com"), anything else as it is.++ let own = String(s2.info.sub || '');++ try { if (/^https?:\/\//.test(own)) { const u = new URL(own); const leaf = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || ''); own = leaf ? `${leaf} · ${u.hostname.replace(/^www\./, '')}` : u.hostname.replace(/^www\./, ''); } } catch { /* as it is */ }++ b2.textContent = sharedTitle && own ? own : s2.info.title;++ }++ g.querySelector('.ovas-sum b').textContent = titled.length++ ? `${titled[titled.length - 1].title}${steps.length > 1 ? ` · ${steps.length}` : ''}`++ : kinds.length === 1 ? plural(kinds[0], count[kinds[0]]) : kinds.map((k) => `${count[k]} ${NOUN[k][count[k] === 1 ? 0 : 1]}`).join(' · ');++ const running = steps.find((s) => !s.done) || steps[steps.length - 1];++ const small = g.querySelector('.ovas-sum small');++ small.textContent = running ? (running.info.sub || running.info.title) : '';++ const added = steps.reduce((n, s) => n + (s.info.added || 0), 0), removed = steps.reduce((n, s) => n + (s.info.removed || 0), 0);++ const delta = g.querySelector('.ovas-delta'); delta.hidden = !(added || removed); delta.firstChild.textContent = `+${added}`; delta.lastChild.textContent = `−${removed}`;++ const st = g.querySelector('.ovas-state'), fails = steps.filter((s) => s.done && !s.ok).length, live = steps.some((s) => !s.done);++ st.dataset.s = live ? 'run' : fails ? 'fail' : 'ok';++ st.innerHTML = live ? '<span class="ovas-spin" aria-label="Running"></span>' : fails ? `${svg('fail')}<span>${fails} failed</span>` : svg('ok');++ if (fails) li.dataset.p = '1';++ // One step alone reads as itself, not as a group of one.++ g.classList.toggle('is-one', steps.length === 1);++ if (steps.length === 1) g.open = true;++ // The group's attachments, in its header row, so they show while the group is folded.++ const atts = [], seenAtt = new Set();++ for (const s of steps) for (const a of (s.attachments || [])) { if (!seenAtt.has(a.url)) { seenAtt.add(a.url); atts.push(a); } }++ const slot = g.querySelector('.ovas-att-slot');++ slot.textContent = '';++ // A group of one hides its header, so there the step's own row carries the attachments instead.++ if (atts.length && steps.length > 1) slot.appendChild(attStrip(atts, openSheet));++ // A group of one shows the step's own row; there the attachments belong to the step.++ for (const s of steps) {++ const ss = s.el && s.el.querySelector(':scope>summary .ovas-att-slot');++ if (!ss) continue;++ ss.textContent = '';++ if (steps.length === 1 && s.attachments && s.attachments.length) ss.appendChild(attStrip(s.attachments, openSheet));++ }++ }++ function feedFor(parent) { const p = parent && S.steps.get(parent); return p && p.subEl ? p.subEl : feed; }++ function appendLi(target, li) { if (target === feed) { if (empty.parentNode) empty.remove(); feed.appendChild(li); } else target.appendChild(li); }++++ const pending = new Map(); // tool name → ids of its steps still waiting for a result (events without ids)++ // Any line may carry attachments, and a line of a type this component does not know still shows: a minimal++ // row with its label or summary, then its attachments (Codes' proof lines, for one).++ const KNOWN = new Set(['agent', 'text', 'tool', 'tool_progress', 'tool_result', 'handoff', 'note', 'step', 'usage', 'done', 'state',++ 'phase', 'phases', 'activity', 'thinking', 'thinking_delta', 'text_start', 'text_delta', 'tool_args', 'sources']);++ function extraLine(x, target) {++ const li = el('li', 'ovas-extra');++ li.dataset.k = 'steps'; // it is part of the work, so the Steps filter keeps it++ const label = String(x.label || x.kind || '');++ const summary = String(x.summary || x.text || '');++ const text = label && summary && label !== summary ? label + ' — ' + summary : (summary || label);++ if (text) li.appendChild(el('span', '', text));++ const atts = safeAtts(x.attachments);++ if (atts.length) li.appendChild(attStrip(atts, openSheet));++ appendLi(target, li);++ }++ function onLine(x) {++ const target = feedFor(x.parent);++ if (!KNOWN.has(x.t)) { extraLine(x, target); return; }++ // Who is working: a line that names a different agent than the last moves the head along.++ // A done line reports an agent's cost and time; it never moves the head.++ if (x.agent && x.t !== 'agent' && x.t !== 'handoff' && x.t !== 'done' && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.agent)) { S.agents.push({ agent: x.agent }); paintHead(); }++ if (x.t === 'tool' && !x.id) { x = { ...x, id: `a${S.steps.size + 1}` }; if (!pending.has(x.name)) pending.set(x.name, []); pending.get(x.name).push(x.id); }++ if (x.t === 'tool_result' && !x.id) { const q = pending.get(x.name); x = { ...x, id: q && q.length ? q.shift() : '' }; }++ if (x.t === 'state') { status(x.state); return; }++ if (x.t === 'agent') {++ const last = S.agents[S.agents.length - 1];++ if (last && last.agent === x.agent) { if (x.model) last.model = x.model; } else S.agents.push({ agent: x.agent, model: x.model });++ paintHead(); return;++ }++ if (x.t === 'text') {++ const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text, linkOk));++ const atts = safeAtts(x.attachments);++ if (atts.length) li.appendChild(attStrip(atts, openSheet));++ appendLi(target, li); return;++ }++ if (x.t === 'tool') {++ const kind = x.kind && VERB[x.kind] ? x.kind : kindOf(x.name);++ const step = { id: x.id || `s${S.steps.size}`, name: x.name, kind, input: x.input || {}, what: x.what || '', title: x.title || '', started: Date.now(), done: false, ok: null, out: '', attachments: safeAtts(x.attachments) };++ if (kind === 'plan') { if (Array.isArray(step.input.todos)) { S.ownPlan = true; S.plan = step.input.todos; paintPlan(); } step.hidden = true; S.steps.set(step.id, step); return; }++ S.steps.set(step.id, step);++ const g = groupFor(target === feed ? feed : target);++ if (target === feed && empty.parentNode) empty.remove();++ g.steps.push(step);++ g.firstChild.querySelector('ol').appendChild(stepEl(step));++ paintGroup(g); step.group = g; paintHead(); return;++ }++ if (x.t === 'phase' || x.t === 'phases' || x.t === 'activity') { onPhase(x, target); return; }++ if (x.t === 'thinking' || x.t === 'thinking_delta') { onThinking(x, target); return; }++ if (x.t === 'text_start' || x.t === 'text_delta') {++ let m = S.msgs.get(x.id);++ if (!m) { const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; m = { li, text: '' }; S.msgs.set(x.id, m); appendLi(target, li); }++ if (x.t === 'text_delta') { m.text = (m.text + x.delta).slice(0, MAX_TEXT); m.li.textContent = ''; m.li.appendChild(markdown(m.text, linkOk)); }++ return;++ }++ if (x.t === 'tool_args') {++ const step = S.steps.get(String(x.id)); if (!step) return;++ step.argText = (step.argText || '') + x.delta;++ try { step.input = JSON.parse(step.argText); step.info = describe(step); if (step.group) paintGroup(step.group); } catch { /* not whole yet */ }++ return;++ }++ if (x.t === 'sources') { addSources(x.items); return; }++ if (x.t === 'tool_progress') {++ const step = S.steps.get(String(x.id)); if (!step || !step.outEl) return;++ step.out = (step.out + x.chunk).slice(-20000); step.outEl.textContent = step.out; step.outEl.hidden = false; step.waitEl.hidden = true;++ step.outEl.scrollTop = step.outEl.scrollHeight; return;++ }++ if (x.t === 'tool_result') {++ const step = S.steps.get(String(x.id)); if (!step || step.hidden) return;++ // A result may carry the files the step produced; the group's header shows them.++ const extra = safeAtts(x.attachments);++ if (extra.length) {++ const have = step.attachments || (step.attachments = []);++ for (const a of extra) if (!have.some((b) => b.url === a.url)) have.push(a);++ }++ step.done = true; step.ok = x.ok !== false; step.ended = x.duration_ms > 0 ? step.started + Number(x.duration_ms) : Date.now();++ if (Number(x.cost_usd) > 0) step.cost = Number(x.cost_usd);++ if (step.outEl) {++ const out = String(x.output == null ? '' : x.output) || step.out || '';++ step.waitEl.hidden = true;++ if (out.trim()) { step.outEl.textContent = out; step.outEl.hidden = false; step.outEl.classList.toggle('is-fail', !step.ok); }++ else { step.outEl.hidden = true; step.outLabel.hidden = true; }++ }++ if (!step.ok) { S.problems++; if (step.el) step.el.open = true; }++ setStepState(step); if (step.group) paintGroup(step.group); paintHead(); return;++ }++ if (x.t === 'handoff') {++ const li = el('li', 'ovas-handoff'); li.dataset.k = 'handoff'; li.dataset.p = '1';++ const s = el('div', 'ovas-hand');++ const row = el('div', 'ovas-hand-row');++ const from = x.from || (S.agents.length ? S.agents[S.agents.length - 1].agent : '');++ if (x.to && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.to)) S.agents.push({ agent: x.to });++ row.appendChild(el('span', 'ovas-agent', agentName(from))); row.insertAdjacentHTML('beforeend', svg('handoff')); row.appendChild(el('span', 'ovas-agent is-now', x.to ? agentName(x.to) : 'the next agent'));++ s.appendChild(row); if (x.reason) s.appendChild(el('small', '', x.reason));++ const handAtts = safeAtts(x.attachments);++ if (handAtts.length) s.appendChild(attStrip(handAtts, openSheet));++ li.appendChild(s); appendLi(feed, li); S.problems++; paintHead(); return;++ }++ if (x.t === 'note') {++ const li = el('li', 'ovas-note'); li.dataset.k = 'note'; li.dataset.level = x.level === 'warning' || x.level === 'info' ? x.level : 'error';++ if (li.dataset.level !== 'info') li.dataset.p = '1';++ li.innerHTML = svg(li.dataset.level === 'info' ? 'ok' : 'warn'); li.appendChild(el('span', '', x.text));++ const noteAtts = safeAtts(x.attachments);++ if (noteAtts.length) li.appendChild(attStrip(noteAtts, openSheet));++ appendLi(target, li); if (li.dataset.level !== 'info') S.problems++; paintHead(); return;++ }++ if (x.t === 'step') {++ const li = el('li', 'ovas-line'); li.dataset.k = 'line'; li.appendChild(el('span', '', x.text));++ const lineAtts = safeAtts(x.attachments);++ if (lineAtts.length) li.appendChild(attStrip(lineAtts, openSheet));++ appendLi(feed, li); return;++ }++ if (x.t === 'usage') { S.tokens.in += Number(x.input_tokens) || 0; S.tokens.out += Number(x.output_tokens) || 0; return; }++ if (x.t === 'done') {++ if (x.parent) return;++ S.cost += Number(x.cost_usd) || 0; S.durationMs += Number(x.duration_ms) || 0;++ const who = x.agent || (S.agents[S.agents.length - 1] || {}).agent;++ if (who && Number(x.cost_usd) > 0) S.agentCost.set(who, (S.agentCost.get(who) || 0) + Number(x.cost_usd));++ const li = el('li', 'ovas-done'); li.dataset.k = 'done'; li.dataset.ok = x.ok ? '1' : '0';++ const b = el('b'); b.innerHTML = svg(x.ok ? 'ok' : 'fail'); b.appendChild(document.createTextNode(`${agentName((S.agents[S.agents.length - 1] || {}).agent)} ${x.ok ? 'finished' : 'stopped'}${x.duration_ms ? ` in ${dur(x.duration_ms)}` : ''}`));++ li.appendChild(b);++ if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text, linkOk)); li.appendChild(m); }++ const doneAtts = safeAtts(x.attachments);++ if (doneAtts.length) li.appendChild(attStrip(doneAtts, openSheet));++ if (!x.ok) li.dataset.p = '1';++ appendLi(feed, li); paintHead();++ }++ }++ // Phases: named parts of the run (AG-UI steps). The plan row in the head shows where the run is ("2 of 5"), and a++ // phase that starts is a divider in the feed. A plan the agent keeps itself (TodoWrite) wins over phases.++ function onPhase(x, target) {++ if (x.t === 'activity') { S.activity = x.text; paintPlan(); return; }++ if (x.t === 'phases') { S.phases = x.items.map((p, i) => ({ id: `p${i}`, title: p.title, status: p.status })); }++ else {++ const same = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();++ let p = S.phases.find((q) => q.id === x.id) || S.phases.find((q) => same(q.title, x.title || x.id));++ if (!p) { p = { id: x.id, title: x.title || x.id, status: 'running' }; S.phases.push(p); }++ if (x.title) p.title = x.title;++ p.status = x.status;++ if (x.status === 'running' && !p.li) {++ const li = el('li', 'ovas-phase'); li.dataset.k = 'phase';++ li.appendChild(el('span', 'ovas-phase-dot')); li.appendChild(el('b', '', p.title));++ p.li = li; appendLi(target === feed ? feed : target, li);++ }++ if (p.li) { p.li.dataset.s = p.status; p.li.querySelector('b').textContent = p.title; }++ }++ if (!S.ownPlan) {++ S.plan = S.phases.map((p) => ({ content: p.title, status: /done|complete/.test(p.status) ? 'completed' : /run|progress/.test(p.status) ? 'in_progress' : 'pending' }));++ paintPlan();++ }++ }++ // Thinking: folded to one line, "Thinking…" while it streams and "Thought for 1m 20s" after; its words open on a tap.++ function onThinking(x, target) {++ let th = S.thinks.get(x.id);++ if (!th) {++ const li = el('li', 'ovas-think'); li.dataset.k = 'msg';++ const d = el('details'); const sm = el('summary'); sm.appendChild(el('span', 'ovas-think-dots'));++ const label = el('span', '', 'Thinking…'); sm.appendChild(label); d.appendChild(sm);++ const body = el('div', 'ovas-think-body'); d.appendChild(body); li.appendChild(d);++ th = { li, label, body, text: '', started: Date.now() }; S.thinks.set(x.id, th); appendLi(target, li);++ }++ if (x.t === 'thinking_delta') { th.text = (th.text + x.delta).slice(-20000); th.body.textContent = th.text; }++ if (x.hidden) th.body.textContent = 'This model keeps its thinking to itself.';++ if (x.done || x.hidden) { th.li.classList.add('is-done'); th.label.textContent = `Thought for ${dur(x.duration_ms || Date.now() - th.started)}`; }++ }++ // Sources: every page the run read, once each, in one strip under the feed: the sites as letter tiles and a count;++ // opened, the titles as links (https only, a new tab).++ function addSources(items) {++ for (const it of (items || []).slice(0, 200)) {++ const url = it && String(it.url || '');++ if (!/^https:\/\//.test(url) || S.sources.has(url)) continue;++ let site = String(it.site || ''); try { if (!site) site = new URL(url).hostname.replace(/^www\./, ''); } catch { continue; }++ S.sources.set(url, { url, site, title: cut(String(it.title || site), 160) });++ }++ paintSources();++ }++ function paintSources() {++ const list = [...S.sources.values()];++ srcBox.hidden = !list.length; if (!list.length) return;++ const tiles = srcBox.querySelector('.ovas-src-tiles'); tiles.textContent = '';++ const sites = [...new Set(list.map((s) => s.site))];++ sites.slice(0, 5).forEach((site) => { const t = el('span', 'ovas-src-tile', site.charAt(0).toUpperCase()); t.title = site; let h = 0; for (const c of site) h = (h * 31 + c.charCodeAt(0)) % 360; t.style.setProperty('--h', String(h)); tiles.appendChild(t); });++ srcBox.querySelector('.ovas-src-n').textContent = `${list.length} source${list.length === 1 ? '' : 's'}${sites.length > 1 ? ` from ${sites.length} sites` : ''}`;++ const ol = srcBox.querySelector('ol'); ol.textContent = '';++ list.forEach((s) => { const li = el('li'); const a = el('a', '', s.title); a.href = s.url; a.target = '_blank'; a.rel = 'noopener noreferrer'; li.appendChild(a); li.appendChild(el('small', '', s.site)); ol.appendChild(li); });++ }++ function push(event) { normalize(event).forEach((x) => { try { onLine(x); } catch { /* one odd line never stops the view */ } }); applyFilter(); keep(); }++++ function paintPlan() {++ const box2 = head.querySelector('.ovas-plan'); const todos = (S.plan || []).filter((t) => t && (t.content || t.activeForm));++ if (!todos.length && S.activity) { box2.hidden = false; box2.querySelector('.ovas-plan-now').textContent = S.activity; box2.querySelector('.ovas-plan-n').textContent = ''; box2.querySelector('i').style.width = '0%'; return; }++ box2.hidden = !todos.length; if (!todos.length) return;++ const doneN = todos.filter((t) => t.status === 'completed').length;++ box2.querySelector('i').style.width = `${Math.round((doneN / todos.length) * 100)}%`;++ const now = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status !== 'completed');++ box2.querySelector('.ovas-plan-now').textContent = now ? (now.status === 'in_progress' && now.activeForm ? now.activeForm : now.content || now.activeForm) : 'Plan done';++ box2.querySelector('.ovas-plan-n').textContent = `${doneN} of ${todos.length}`;++ const ol = box2.querySelector('ol'); ol.textContent = '';++ todos.slice(0, 12).forEach((t) => { const li = el('li', '', t.status === 'in_progress' && t.activeForm ? t.activeForm : t.content || t.activeForm); li.dataset.s = t.status || 'pending'; ol.appendChild(li); });++ }++ function summary() {++ const steps = [...S.steps.values()].filter((s) => !s.hidden);++ const files = new Set(steps.filter((s) => s.kind === 'edit' || s.kind === 'write').map((s) => s.info && s.info.sub).filter(Boolean));++ return {++ agents: S.agents.map((a) => agentName(a.agent)), steps: steps.length, commands: steps.filter((s) => s.kind === 'command').length, files: files.size,++ added: steps.reduce((n, s) => n + ((s.info && s.info.added) || 0), 0), removed: steps.reduce((n, s) => n + ((s.info && s.info.removed) || 0), 0),++ problems: S.problems, costUsd: Math.round(S.cost * 1e4) / 1e4, costByAgent: Object.fromEntries([...S.agentCost].map(([k, v]) => [agentName(k), Math.round(v * 1e4) / 1e4])), durationMs: S.durationMs || (clock ? (S.endedAt || Date.now()) - S.startedAt : 0), tokens: { ...S.tokens },++ };++ }++ const LABEL = { queued: 'Waiting', placed: 'Starting', running: 'Running', verifying: 'Checking the answer', succeeded: 'Finished', failed: 'Failed', cancelled: 'Cancelled', expired: 'Expired' };++ function paintHead() {++ const sm = summary();++ const pill = head.querySelector('.ovas-pill');++ pill.dataset.s = S.status;++ pill.innerHTML = `${S.status === 'running' || S.status === 'placed' || S.status === 'verifying' ? '<span class="ovas-spin"></span>' : S.status === 'succeeded' ? svg('ok') : S.status === 'failed' || S.status === 'expired' ? svg('fail') : ''}<span></span>`;++ pill.lastChild.textContent = S.label || LABEL[S.status] || S.status;++ const ag = head.querySelector('.ovas-agents'); ag.textContent = '';++ const shown = new Set();++ S.agents.forEach((a, i) => {++ if (i) ag.appendChild(el('span', '', '→'));++ const c = el('span', `ovas-agent${i === S.agents.length - 1 ? ' is-now' : ''}`, agentName(a.agent)); if (a.model) c.title = a.model;++ // An agent's cost once, on its last chip (an agent may come back after a hand-off).++ const cost = S.agentCost.get(a.agent);++ if (cost && !shown.has(a.agent) && !S.agents.slice(i + 1).some((b) => b.agent === a.agent)) { shown.add(a.agent); c.appendChild(el('span', 'ovas-agent-cost', `$${cost < 0.01 ? cost.toFixed(4).replace(/0+$/, '') : cost.toFixed(2)}`)); }++ ag.appendChild(c);++ });++ const fc = bar.querySelector('[data-f="files"]'); if (fc) fc.textContent = sm.files ? `Files ${sm.files}` : 'Files';++ const m = (k, html) => { const n = head.querySelector(`[data-m="${k}"]`); n.innerHTML = html; n.hidden = !html; };++ // What it is doing now (the portal's sticky line, 2026-11-11): the newest step that has not ended.++ const nowStep = LIVE.includes(S.status) ? [...S.steps.values()].reverse().find((st) => !st.done && !st.hidden) : null;++ m('now', nowStep && nowStep.info ? `<span class="ovas-now">${esc(String(nowStep.info.command || nowStep.info.title || '').split('\n')[0].slice(0, 90))}</span>` : '');++ m('time', sm.durationMs ? `<b>${esc(dur(sm.durationMs))}</b>` : '');++ m('steps', sm.steps ? `<b>${sm.steps}</b> step${sm.steps === 1 ? '' : 's'}${sm.commands ? ` · <b>${sm.commands}</b> command${sm.commands === 1 ? '' : 's'}` : ''}` : '');++ m('files', sm.files ? `<b>${sm.files}</b> file${sm.files === 1 ? '' : 's'} <span style="color:var(--ovas-ok)">+${sm.added}</span> <span style="color:var(--ovas-bad)">−${sm.removed}</span>` : '');++ m('cost', sm.costUsd ? `<b>$${sm.costUsd.toFixed(sm.costUsd < 1 ? 3 : 2)}</b>` : '');++ }++ function status(s, label) {++ S.status = s; S.label = label || '';++ working.hidden = !LIVE.includes(s);++ if (!LIVE.includes(s)) { S.endedAt = S.endedAt || Date.now(); empty.textContent = 'Nothing was recorded for this run.'; }++ paintHead();++ }++ // Talking to the run while it works (opts.onSend(text, { files, uploads }) → a promise; the product sends it to its++ // agent: Codes' job message, Actor's task input, the portal's session). The box is the one composer on every site++ // (owner, 2026-11-11: "uses our custom modular composer across all sites"): files by picker, paste and drop, "/"++ // commands and the person's own, drafts, voice and their settings; loaded from beside this file when the page has++ // not loaded it. opts.composer: false keeps a plain box.++ if (typeof opts.onSend === 'function') {++ // The one composer's markup (thread.js composer()): the box, then a row with its tools and Send, then its note,++ // so ov-composer.js puts its + menu (files), "/" commands, voice and settings where every site has them.++ const form = el('form', 'ovas-say ovt-composer'); form.setAttribute('data-ovt-composer', '');++ const ta = el('textarea'); ta.name = 'text'; ta.rows = 1; ta.maxLength = 4000; ta.placeholder = opts.sayPlaceholder || 'Message the agent'; ta.setAttribute('aria-label', 'Message the agent'); ta.setAttribute('data-ovt-text', '');++ const row = el('div', 'ovt-c-row'); const toolsEl = el('div', 'ovt-c-tools');++ const send = el('button', 'ovas-say-send ovt-send'); send.type = 'submit'; send.setAttribute('aria-label', 'Send'); send.setAttribute('data-ovt-send', ''); send.innerHTML = svg('handoff');++ const noteEl = el('p', 'ovt-c-note'); noteEl.setAttribute('data-ovt-note', ''); noteEl.setAttribute('role', 'status');++ row.appendChild(toolsEl); row.appendChild(send);++ // A file input makes the box one that takes files (the composer's + menu, paste and drop); the files go to onSend.++ if (opts.files !== false) {++ // The shared "+" (thread.js attachHtml): a label holding the file input, first in the tools row.++ const plus = el('label', 'ovt-btn ovt-attach'); plus.title = 'Attach files';++ plus.innerHTML = '<svg class="ovt-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span class="ovt-sr">Attach files</span>';++ const fi = el('input'); fi.type = 'file'; fi.name = 'files'; fi.multiple = true; fi.hidden = true; fi.setAttribute('data-ovt-files', ''); if (opts.accept) fi.accept = String(opts.accept);++ plus.appendChild(fi); toolsEl.insertBefore(plus, toolsEl.firstChild);++ }++ form.appendChild(ta); form.appendChild(row); form.appendChild(noteEl);++ box.appendChild(form);++ ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit', { cancelable: true })); } });++ form.addEventListener('submit', async (e) => {++ e.preventDefault();++ const ovc = form.__ovc;++ const files = ovc ? ovc.files() : [], uploads = ovc && ovc.uploads ? ovc.uploads() : [];++ const text = ta.value.trim(); if ((!text && !files.length) || form.classList.contains('is-busy')) return;++ form.classList.add('is-busy');++ const li = el('li', 'ovas-you'); li.dataset.k = 'msg'; li.appendChild(el('span', '', text));++ if (files.length) li.appendChild(el('small', 'ovas-you-files', files.map((f) => f.name || 'a file').join(' · ')));++ appendLi(feed, li); keep();++ try { await opts.onSend(text, { files, uploads }); if (ovc && ovc.clear) ovc.clear(); else ta.value = ''; li.classList.add('is-sent'); }++ catch (err) { li.classList.add('is-failed'); li.title = (err && err.message) || 'Not sent'; }++ finally { form.classList.remove('is-busy'); }++ });++ if (opts.composer !== false) {++ form.id = form.id || `ovas-say-${Math.random().toString(36).slice(2, 8)}`;++ const join = () => { try { if (root.OpenVibeComposer && form.isConnected) root.OpenVibeComposer.enhance(form); } catch { /* a plain box still sends */ } };++ if (root.OpenVibeComposer) join();++ else {++ const m = /^(https?:\/\/[^?#]*\/)agent-session(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(SELF_SRC);++ if (m) {++ let sc = document.getElementById('ov-composer-script');++ if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = m[1] + 'ov-composer.js'; document.head.appendChild(sc); }++ sc.addEventListener('load', join, { once: true });++ }++ }++ }++ }++ const tick = setInterval(() => { if (S.status === 'running' || S.status === 'placed') { S.steps.forEach((st) => { if (!st.done) setStepState(st); }); paintHead(); } }, 1000);++ paintHead();++ return { push, pushAll: (list) => (list || []).forEach(push), status, summary, actions: (list, note) => paintActions(list, note), files: () => changedFiles().map((f) => ({ path: f.path, added: f.added, removed: f.removed, changes: f.n })), destroy() { clearInterval(tick); host.textContent = ''; } };++ }++++ const api = { mount, normalize, diffLines, kindOf };++ root.OpenVibeAgentSession = api;++ if (typeof module === 'object' && module.exports) module.exports = api;++})(typeof globalThis !== 'undefined' ? globalThis : this);nnew file mode 100644@@@ -0,0 +1,236 @@++// ═══════════════════════════════════════════════════════════════++// OpenVibe — an agent's sheet (openvibe-shared 4.211.0; Opus, 2026-11-11)++//++// One window (ov-window.js: a panel beside what opened it on a computer, a sheet on a phone) that says everything about++// one agent: who it is and its state, what it is doing now, what it asks you, the work it did, when it runs, what it++// remembers and its computer. The data is the agent's sheet (Contracts actor.agent-sheet@1 / codes.agent-sheet@1),++// read through Network's same-origin relay with the person's token:++// OVAgentSheet.open({ kind: 'actor' | 'codes', id, name, token, frameBase, base, from, openHref }) → { close() }++// The parts are tabs (a segmented row; arrow keys move between them); a part with nothing in it says so in its own++// words (asks_empty) rather than disappearing. While it reads, the window shows the mark's wait; a failed read says so++// with Retry. Everything an agent or a person wrote is set as text, never markup.++// ═══════════════════════════════════════════════════════════════++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OVAgentSheet) return;++++ const CSS = `.ovas { display: grid; gap: 12px; padding: 4px 2px 6px; }++.ovas-who { display: flex; align-items: center; gap: 12px; min-width: 0; }++.ovas-face { flex: none; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; overflow: hidden; font-weight: 800; font-size: 17px;++ color: #fff; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #7c3aed)); box-shadow: 0 0 0 3px var(--ovas-ring, transparent); }++.ovas-face img { width: 100%; height: 100%; object-fit: cover; }++.ovas[data-state="working"] { --ovas-ring: color-mix(in srgb, #22c55e 55%, transparent); }++.ovas[data-state="paused"] { --ovas-ring: color-mix(in srgb, #f59e0b 55%, transparent); }++.ovas-name { display: grid; gap: 2px; min-width: 0; flex: 1; }++.ovas-name b { font-size: 16px; overflow-wrap: anywhere; }++.ovas-name small { color: var(--text-secondary, #a8b3c4); font-size: 12.5px; overflow-wrap: anywhere; }++.ovas-state { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 650; width: fit-content;++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }++.ovas-state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ovas-dot, #94a3b8); }++.ovas[data-state="working"] .ovas-state { --ovas-dot: #22c55e; }++.ovas[data-state="paused"] .ovas-state { --ovas-dot: #f59e0b; }++.ovas-open { flex: none; display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border-radius: 10px; color: var(--accent-light, #60a5fa);++ font-size: 13px; font-weight: 650; text-decoration: none; }++.ovas-open:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }++.ovas-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 999px; overflow-x: auto; scrollbar-width: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }++.ovas-tabs::-webkit-scrollbar { display: none; }++@media (max-width: 520px) { .ovas-tabs { -webkit-mask-image: linear-gradient(90deg, #000 85%, transparent); mask-image: linear-gradient(90deg, #000 85%, transparent); } }++.ovas-tab { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 13px; border: 0; border-radius: 999px; background: none;++ color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; cursor: pointer; }++.ovas-tab[aria-selected="true"] { background: var(--bg-primary, #0b1220); color: var(--text-primary, #e6edf7); font-weight: 650; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); }++.ovas-tab em { font-style: normal; font-size: 11px; font-weight: 700; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, #f59e0b 22%, transparent); color: #fbbf24; }++.ovas-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }++.ovas-part { display: grid; align-content: start; gap: 8px; min-height: 90px; }++.ovas-part[hidden] { display: none; }++.ovas-empty { margin: 0; padding: 18px 8px; text-align: center; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }++.ovas-card { display: grid; gap: 4px; padding: 11px 13px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .1));++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent); min-width: 0; }++.ovas-card b { font-size: 14px; font-weight: 650; overflow-wrap: anywhere; }++.ovas-card p { margin: 0; color: var(--text-secondary, #a8b3c4); font-size: 13px; overflow-wrap: anywhere; }++.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--text-muted, #7d8aa0); font-size: 12px; font-variant-numeric: tabular-nums; }++.ovas-chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 650;++ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); color: var(--text-primary, #e6edf7); }++.ovas-chip.is-ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }++.ovas-chip.is-bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #f87171; }++.ovas-chip.is-run { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, #60a5fa); }++.ovas-card.is-now { border-color: color-mix(in srgb, #22c55e 45%, transparent); }++.ovas-card.is-ask { border-color: color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); }++.ovas-go { justify-self: start; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 12px; text-decoration: none; font-weight: 650; font-size: 14px;++ color: #fff; background: var(--accent, #3b82f6); }++.ovas-wait { display: grid; justify-items: center; gap: 10px; padding: 26px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }++.ovas-retry { min-height: 40px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border, rgba(255, 255, 255, .14)); background: none; color: inherit; font: inherit; cursor: pointer; }++@media (pointer: coarse) { .ovas-tab { min-height: 44px; } .ovas-open { min-height: 44px; } }`;++++ function styles(d) {++ if (d.getElementById('ovas-css')) return;++ const s = d.createElement('style');++ s.id = 'ovas-css';++ s.textContent = CSS;++ d.head.appendChild(s);++ }++ const money = (n) => (Number(n) > 0 ? `$${Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: Number(n) < 0.1 ? 4 : 2 })}` : '$0');++ function when(iso) {++ const t = Date.parse(iso || '');++ if (!Number.isFinite(t)) return '';++ const s = (t - Date.now()) / 1000;++ const a = Math.abs(s);++ const unit = a < 3600 ? [Math.max(1, Math.round(a / 60)), 'min'] : a < 86400 ? [Math.round(a / 3600), 'h'] : [Math.round(a / 86400), a < 172800 ? 'day' : 'days'];++ if (a < 60) return s > 0 ? 'in a moment' : 'just now';++ return s > 0 ? `in ${unit[0]} ${unit[1]}` : `${unit[0]} ${unit[1]} ago`;++ }++ const tone = (state) => (/^(done|succeeded|ok|passed|completed)$/i.test(state) ? 'is-ok' : /^(failed|error|refused|stopped)$/i.test(state) ? 'is-bad' : /^(running|working|started)$/i.test(state) ? 'is-run' : '');++ const STATE_WORD = { working: 'Working', idle: 'Resting', paused: 'Paused', ready: 'Ready' };++++ function open(opts = {}) {++ const W = root.OVWindow;++ if (!W) throw new Error('ov-window.js is not loaded');++ const d = document;++ styles(d);++ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; };++ const kind = opts.kind === 'codes' ? 'codes' : 'actor';++ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const aid = encodeURIComponent(String(opts.id || ''));++ // Through Network's relay on a site; `base` reaches the service itself (its own site, or a test).++ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';++ const url = direct ? (kind === 'codes' ? `${direct}/api/v1/me/agents/${aid}/sheet` : `${direct}/api/v1/me/agents/${aid}?view=sheet`)++ : `${frameBase}/${kind === 'codes' ? 'codes-agents' : 'agents'}/${aid}/sheet`;++ const body = h('div', 'ovas');++ // The window's title names what it is; the agent's own name leads the sheet under it (said once).++ const win = W.open({ id: `agent-sheet-${kind}`, title: kind === 'codes' ? 'Coding agent' : 'Your Actor', body, kind: 'panel', size: 'md', from: opts.from || null,++ onClose: () => { gone = true; } });++ let gone = false;++++ function wait() {++ const box = h('div', 'ovas-wait');++ const mark = h('span', 'ov-mark');++ mark.setAttribute('data-state', 'busy'); mark.setAttribute('data-size', '34'); mark.setAttribute('aria-hidden', 'true');++ box.append(mark, h('span', '', 'Reading your agent…'));++ body.replaceChildren(box);++ }++ function failed(text) {++ const box = h('div', 'ovas-wait');++ const retry = h('button', 'ovas-retry', 'Retry'); retry.type = 'button';++ retry.addEventListener('click', load);++ box.append(h('span', '', text), retry);++ body.replaceChildren(box);++ }++++ function card(cls, title, line, more) {++ const c = h('div', `ovas-card${cls ? ` ${cls}` : ''}`);++ c.append(h('b', '', title));++ if (more) c.append(h('p', '', more));++ if (line && line.childNodes.length) c.append(line);++ return c;++ }++ function workCard(w, isNow) {++ const line = h('div', 'ovas-line');++ if (w.state) line.append(h('span', `ovas-chip ${tone(w.state)}`, w.state));++ if (Number.isFinite(Number(w.cost_usd))) line.append(h('span', '', money(w.cost_usd)));++ if (w.updated_at || w.started_at || w.at) line.append(h('span', '', when(w.updated_at || w.started_at || w.at)));++ const result = typeof w.result === 'string' ? w.result : w.result && (w.result.summary || w.result.text) ? (w.result.summary || w.result.text) : '';++ return card(isNow ? 'is-now' : '', w.task || 'A task', line, result ? String(result).slice(0, 400) : '');++ }++++ function render(sheet) {++ const a = sheet.agent || {};++ body.dataset.state = a.state || '';++ const who = h('div', 'ovas-who');++ const face = h('span', 'ovas-face');++ if (a.face && /^https:\/\//.test(a.face)) { const img = h('img'); img.src = a.face; img.alt = ''; face.append(img); } else face.textContent = String(a.name || opts.name || 'A').trim().charAt(0).toUpperCase();++ const name = h('span', 'ovas-name');++ name.append(h('b', '', a.name || opts.name || 'Your agent'), h('span', 'ovas-state', STATE_WORD[a.state] || a.state || 'Ready'));++ if (a.instructions) name.append(h('small', '', String(a.instructions).slice(0, 160)));++ else if (a.kind) name.append(h('small', '', a.kind));++ who.append(face, name);++ if (opts.openHref) { const go = h('a', 'ovas-open', 'Open ↗'); go.href = opts.openHref; go.target = '_blank'; go.rel = 'noopener'; who.append(go); }++++ const asks = Array.isArray(sheet.asks) ? sheet.asks : [];++ const work = Array.isArray(sheet.work) ? sheet.work : [];++ const parts = [++ ['now', 'Now', () => (sheet.now ? [workCard(sheet.now, true)] : [h('p', 'ovas-empty', a.state === 'paused' ? 'Paused. Nothing runs until you resume it.' : 'Nothing running right now.')])],++ ['asks', 'Asks', () => (asks.length ? asks.map((x) => {++ const line = h('div', 'ovas-line');++ if (x.state) line.append(h('span', 'ovas-chip', x.state));++ if (x.touch && typeof x.touch === 'string') line.append(h('span', '', x.touch));++ return card('is-ask', x.what || 'A request', line, x.why || '');++ }) : [h('p', 'ovas-empty', sheet.asks_empty || 'Nothing to approve.')]), asks.length],++ ['work', 'Work', () => (work.length ? work.map((w) => workCard(w, false)) : [h('p', 'ovas-empty', 'No work yet.')])],++ ['when', 'Schedule', () => {++ const rows = (Array.isArray(sheet.schedules) ? sheet.schedules : []).filter((s) => s && s.schedule !== 'off');++ if (!rows.length) return [h('p', 'ovas-empty', 'It runs when you ask, on no schedule.')];++ return rows.map((s) => {++ const line = h('div', 'ovas-line');++ if (s.next_run_at) line.append(h('span', '', `Next ${when(s.next_run_at)}`));++ if (s.last_run_at) line.append(h('span', '', `Last ${when(s.last_run_at)}`));++ if (s.last_result) line.append(h('span', `ovas-chip ${tone(s.last_result)}`, s.last_result));++ return card('', String(s.schedule || 'On a schedule'), line, '');++ });++ }],++ ['memory', 'Memory', () => {++ const rows = Array.isArray(sheet.memory) ? sheet.memory : [];++ return rows.length ? rows.slice(0, 30).map((m) => { const line = h('div', 'ovas-line'); if (m.source) line.append(h('span', '', m.source)); return card('', String(m.text || '').slice(0, 300), line, ''); }) : [h('p', 'ovas-empty', 'Nothing remembered yet.')];++ }],++ ['computer', 'Computer', () => {++ const c = sheet.computer;++ if (!c) return [h('p', 'ovas-empty', 'No computer is attached to this agent.')];++ const out = [card('', c.name || 'Its computer', null, c.state || c.status || '')];++ const href = [c.url, c.open].find((u) => typeof u === 'string' && /^https:\/\//.test(u));++ if (href) { const go = h('a', 'ovas-go', 'Watch its computer'); go.href = href; go.target = '_blank'; go.rel = 'noopener'; out.push(go); }++ return out;++ }],++ ];++ const tabs = h('div', 'ovas-tabs');++ tabs.setAttribute('role', 'tablist');++ tabs.setAttribute('aria-label', 'About this agent');++ const panes = [];++ const first = asks.length ? 'asks' : 'now';++ parts.forEach(([id, label, build, count]) => {++ const tab = h('button', 'ovas-tab', label);++ tab.type = 'button';++ tab.setAttribute('role', 'tab');++ tab.id = `ovas-t-${id}`;++ tab.setAttribute('aria-controls', `ovas-p-${id}`);++ if (count) tab.append(h('em', '', count));++ const pane = h('div', 'ovas-part');++ pane.id = `ovas-p-${id}`;++ pane.setAttribute('role', 'tabpanel');++ pane.setAttribute('aria-labelledby', tab.id);++ pane.append(...build());++ const on = id === first;++ tab.setAttribute('aria-selected', on ? 'true' : 'false');++ tab.tabIndex = on ? 0 : -1;++ pane.hidden = !on;++ tab.addEventListener('click', () => select(id));++ tabs.append(tab);++ panes.push([id, tab, pane]);++ });++ function select(id) {++ panes.forEach(([pid, tab, pane]) => { const on = pid === id; tab.setAttribute('aria-selected', on ? 'true' : 'false'); tab.tabIndex = on ? 0 : -1; pane.hidden = !on; });++ }++ tabs.addEventListener('keydown', (e) => {++ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;++ const i = panes.findIndex(([, tab]) => tab.getAttribute('aria-selected') === 'true');++ const next = panes[(i + (e.key === 'ArrowRight' ? 1 : panes.length - 1)) % panes.length];++ select(next[0]); next[1].focus(); e.preventDefault();++ });++ body.replaceChildren(who, tabs, ...panes.map(([, , pane]) => pane));++ }++++ async function load() {++ wait();++ try {++ const t = typeof opts.token === 'function' ? await opts.token() : opts.token;++ if (!t) { failed('Sign in to see your agent.'); return; }++ const r = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });++ const b = await r.json().catch(() => null);++ if (gone) return;++ if (!r.ok || !b) { failed(r.status === 404 ? 'This agent is not there any more.' : 'Its sheet could not be read just now.'); return; }++ render(b.sheet || b);++ } catch { if (!gone) failed('Its sheet could not be read just now.'); }++ }++ load();++ return { close: () => win.close() };++ }++++ root.OVAgentSheet = { open };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,16 @@++/* Generated by scripts/build-ai-marks-client.js from ai-marks-data.js. */++(function (root) {++ 'use strict';++ if (!root || root.OpenVibeAIMarks) return;++ const DATA = {"OpenAI":"<path d=\"M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z\"></path>","Anthropic":"<path d=\"M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z\"></path>","Google":"<path d=\"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z\"></path>","Gemma":"<path d=\"M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z\"></path>","Meta":"<path d=\"M6.897 4c1.915 0 3.516.932 5.43 3.376l.282-.373c.19-.246.383-.484.58-.71l.313-.35C14.588 4.788 15.792 4 17.225 4c1.273 0 2.469.557 3.491 1.516l.218.213c1.73 1.765 2.917 4.71 3.053 8.026l.011.392.002.25c0 1.501-.28 2.759-.818 3.7l-.14.23-.108.153c-.301.42-.664.758-1.086 1.009l-.265.142-.087.04a3.493 3.493 0 01-.302.118 4.117 4.117 0 01-1.33.208c-.524 0-.996-.067-1.438-.215-.614-.204-1.163-.56-1.726-1.116l-.227-.235c-.753-.812-1.534-1.976-2.493-3.586l-1.43-2.41-.544-.895-1.766 3.13-.343.592C7.597 19.156 6.227 20 4.356 20c-1.21 0-2.205-.42-2.936-1.182l-.168-.184c-.484-.573-.837-1.311-1.043-2.189l-.067-.32a8.69 8.69 0 01-.136-1.288L0 14.468c.002-.745.06-1.49.174-2.23l.1-.573c.298-1.53.828-2.958 1.536-4.157l.209-.34c1.177-1.83 2.789-3.053 4.615-3.16L6.897 4zm-.033 2.615l-.201.01c-.83.083-1.606.673-2.252 1.577l-.138.199-.01.018c-.67 1.017-1.185 2.378-1.456 3.845l-.004.022a12.591 12.591 0 00-.207 2.254l.002.188c.004.18.017.36.04.54l.043.291c.092.503.257.908.486 1.208l.117.137c.303.323.698.492 1.17.492 1.1 0 1.796-.676 3.696-3.641l2.175-3.4.454-.701-.139-.198C9.11 7.3 8.084 6.616 6.864 6.616zm10.196-.552l-.176.007c-.635.048-1.223.359-1.82.933l-.196.198c-.439.462-.887 1.064-1.367 1.807l.266.398c.18.274.362.56.55.858l.293.475 1.396 2.335.695 1.114c.583.926 1.03 1.6 1.408 2.082l.213.262c.282.326.529.54.777.673l.102.05c.227.1.457.138.718.138.176.002.35-.023.518-.073.338-.104.61-.32.813-.637l.095-.163.077-.162c.194-.459.29-1.06.29-1.785l-.006-.449c-.08-2.871-.938-5.372-2.2-6.798l-.176-.189c-.67-.683-1.444-1.074-2.27-1.074z\"></path>","DeepSeek":"<path d=\"M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z\"></path>","Qwen":"<path d=\"M19.713 2.825h-5.907c0-.251-.066-.503-.196-.728L12.819.728A1.46 1.46 0 0 0 11.559 0h-1.58c-.52 0-1 .275-1.26.728l-2.42 4.19a1.45 1.45 0 0 0 0 1.453l.791 1.37c.26.448.74.727 1.26.727h11.36c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451l-.79-1.37a1.46 1.46 0 0 0-1.26-.728zM.195 9.905l2.951 5.115a1.46 1.46 0 0 0-.531.531l-.791 1.37a1.45 1.45 0 0 0 0 1.452l.79 1.37c.26.448.74.727 1.26.727h4.84c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451L5.086 7.083a1.46 1.46 0 0 0-1.26-.728h-1.58c-.52 0-1 .276-1.26.728l-.79 1.37a1.45 1.45 0 0 0 0 1.452m15.888 13.367 2.951-5.115c.217.125.469.196.728.196h1.582c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451l-2.42-4.191a1.46 1.46 0 0 0-1.259-.728h-1.582c-.52 0-1 .276-1.259.728l-5.678 9.838a1.45 1.45 0 0 0 0 1.452l.79 1.37c.26.448.74.727 1.26.727h1.582c.52 0 1-.275 1.26-.728z\"></path>","Mistral":"<path clip-rule=\"evenodd\" d=\"M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z\"></path>","Kimi":"<path d=\"M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z\"></path><path d=\"M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z\"></path>","GLM":"<path d=\"M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z\"></path>","MiniMax":"<path d=\"M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z\"></path>","Microsoft":"<path d=\"M11.49 2H2v9.492h9.492V2h-.002z\"></path><path d=\"M22 2h-9.492v9.492H22V2z\"></path><path d=\"M11.49 12.508H2V22h9.492v-9.492h-.002z\"></path><path d=\"M22 12.508h-9.492V22H22v-9.492z\"></path>","xAI":"<path d=\"M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815\"></path>","NVIDIA":"<path d=\"M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z\"></path>","Cohere":"<path clip-rule=\"evenodd\" d=\"M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z\"></path><path clip-rule=\"evenodd\" d=\"M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z\"></path><path d=\"M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z\"></path>","Perplexity":"<path d=\"M19.785 0v7.272H22.5V17.62h-2.935V24l-7.037-6.194v6.145h-1.091v-6.152L4.392 24v-6.465H1.5V7.188h2.884V0l7.053 6.494V.19h1.09v6.49L19.786 0zm-7.257 9.044v7.319l5.946 5.234V14.44l-5.946-5.397zm-1.099-.08l-5.946 5.398v7.235l5.946-5.234V8.965zm8.136 7.58h1.844V8.349H13.46l6.105 5.54v2.655zm-8.982-8.28H2.59v8.195h1.8v-2.576l6.192-5.62zM5.475 2.476v4.71h5.115l-5.115-4.71zm13.219 0l-5.115 4.71h5.115v-4.71z\"></path>"};++ const ALIAS = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google', gemma: 'Gemma', meta: 'Meta', deepseek: 'DeepSeek', qwen: 'Qwen', mistral: 'Mistral', kimi: 'Kimi', glm: 'GLM', minimax: 'MiniMax', microsoft: 'Microsoft', xai: 'xAI', nvidia: 'NVIDIA', cohere: 'Cohere', perplexity: 'Perplexity' };++ const mark = (maker, model) => {++ const text = String(maker || model || '').toLowerCase();++ const key = Object.keys(ALIAS).find((name) => text.includes(name)) || (/gpt|\bo[134]\b/.test(text) ? 'openai' : /claude/.test(text) ? 'anthropic' : /gemini/.test(text) ? 'google' : /grok/.test(text) ? 'xai' : null);++ if (!key || !DATA[ALIAS[key]]) return null;++ const tile = document.createElement('span'); tile.className = 'ovdk-run-maker'; tile.setAttribute('role', 'img'); tile.setAttribute('aria-label', ALIAS[key]);++ tile.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true">' + DATA[ALIAS[key]] + '</svg>';++ return tile;++ };++ root.OpenVibeAIMarks = { mark };++})(typeof window !== 'undefined' ? window : null);nnew file mode 100644@@@ -0,0 +1,30 @@++'use strict';++/* Generated by scripts/build-brands.js from simple-icons 16.36.0 (CC0-1.0, https://simpleicons.org). Do not edit. */++/**++ * openvibe-shared/brands — the brand logos OpenVibe shows, in place of Font Awesome's fa-brands (plan L54 K4):++ * 49 brands on a 24-unit grid, filled, each with its own colour. Brand names are their owners' marks; Simple Icons'++ * drawings are public domain.++ *++ * brands.get('youtube') → { title, hex, path } or null++ * brands.fromFa('fa-youtube') → 'youtube' (or null)++ * brands.svg('youtube', { size, title, color })++ */++(function (root, factory) {++ const api = factory();++ if (typeof module === 'object' && module.exports) module.exports = api;++ else root.OVBrands = api;++})(typeof globalThis !== 'undefined' ? globalThis : this, function () {++ const BRANDS = {"android":{"title":"Android","hex":"#3ddc84","path":"M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"},"apple":{"title":"Apple","hex":"#000000","path":"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"},"bitcoin":{"title":"Bitcoin","hex":"#f7931a","path":"M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z"},"bluesky":{"title":"Bluesky","hex":"#1185fe","path":"M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"},"brave":{"title":"Brave","hex":"#fb542b","path":"M15.68 0l2.096 2.38s1.84-.512 2.709.358c.868.87 1.584 1.638 1.584 1.638l-.562 1.381.715 2.047s-2.104 7.98-2.35 8.955c-.486 1.919-.818 2.66-2.198 3.633-1.38.972-3.884 2.66-4.293 2.916-.409.256-.92.692-1.38.692-.46 0-.97-.436-1.38-.692a185.796 185.796 0 01-4.293-2.916c-1.38-.973-1.712-1.714-2.197-3.633-.247-.975-2.351-8.955-2.351-8.955l.715-2.047-.562-1.381s.716-.768 1.585-1.638c.868-.87 2.708-.358 2.708-.358L8.321 0h7.36zm-3.679 14.936c-.14 0-1.038.317-1.758.69-.72.373-1.242.637-1.409.742-.167.104-.065.301.087.409.152.107 2.194 1.69 2.393 1.866.198.175.489.464.687.464.198 0 .49-.29.688-.464.198-.175 2.24-1.759 2.392-1.866.152-.108.254-.305.087-.41-.167-.104-.689-.368-1.41-.741-.72-.373-1.617-.69-1.757-.69zm0-11.278s-.409.001-1.022.206-1.278.46-1.584.46c-.307 0-2.581-.434-2.581-.434S4.119 7.152 4.119 7.849c0 .697.339.881.68 1.243l2.02 2.149c.192.203.59.511.356 1.066-.235.555-.58 1.26-.196 1.977.384.716 1.042 1.194 1.464 1.115.421-.08 1.412-.598 1.776-.834.364-.237 1.518-1.19 1.518-1.554 0-.365-1.193-1.02-1.413-1.168-.22-.15-1.226-.725-1.247-.95-.02-.227-.012-.293.284-.851.297-.559.831-1.304.742-1.8-.089-.495-.95-.753-1.565-.986-.615-.232-1.799-.671-1.947-.74-.148-.068-.11-.133.339-.175.448-.043 1.719-.212 2.292-.052.573.16 1.552.403 1.632.532.079.13.149.134.067.579-.081.445-.5 2.581-.541 2.96-.04.38-.12.63.288.724.409.094 1.097.256 1.333.256s.924-.162 1.333-.256c.408-.093.329-.344.288-.723-.04-.38-.46-2.516-.541-2.961-.082-.445-.012-.45.067-.579.08-.129 1.059-.372 1.632-.532.573-.16 1.845.009 2.292.052.449.042.487.107.339.175-.148.069-1.332.508-1.947.74-.615.233-1.476.49-1.565.986-.09.496.445 1.241.742 1.8.297.558.304.624.284.85-.02.226-1.026.802-1.247.95-.22.15-1.413.804-1.413 1.169 0 .364 1.154 1.317 1.518 1.554.364.236 1.355.755 1.776.834.422.079 1.08-.4 1.464-1.115.384-.716.039-1.422-.195-1.977-.235-.555.163-.863.355-1.066l2.02-2.149c.341-.362.68-.546.68-1.243 0-.697-2.695-3.96-2.695-3.96s-2.274.436-2.58.436c-.307 0-.972-.256-1.585-.461-.613-.205-1.022-.206-1.022-.206z"},"cloudflare":{"title":"Cloudflare","hex":"#f38020","path":"M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"},"discord":{"title":"Discord","hex":"#5865f2","path":"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"},"docker":{"title":"Docker","hex":"#2496ed","path":"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"},"ethereum":{"title":"Ethereum","hex":"#3c3c3d","path":"M11.944 17.97L4.58 13.62 11.943 24l7.37-10.38-7.372 4.35h.003zM12.056 0L4.69 12.223l7.365 4.354 7.365-4.35L12.056 0z"},"facebook":{"title":"Facebook","hex":"#0866ff","path":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"},"firefoxbrowser":{"title":"Firefox Browser","hex":"#ff7139","path":"M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 01-.13-.24 2.118 2.118 0 01-.172-.46.03.03 0 00-.027-.03.038.038 0 00-.021 0l-.006.001a.037.037 0 00-.01.005L15.624 0c-2.585 1.515-3.657 4.168-3.932 5.856a6.197 6.197 0 00-2.305.587.297.297 0 00-.147.37c.057.162.24.24.396.17a5.622 5.622 0 012.008-.523l.067-.005a5.847 5.847 0 011.957.222l.095.03a5.816 5.816 0 01.616.228c.08.036.16.073.238.112l.107.055a5.835 5.835 0 01.368.211 5.953 5.953 0 012.034 2.104c-.62-.437-1.733-.868-2.803-.681 4.183 2.09 3.06 9.292-2.737 9.02a5.164 5.164 0 01-1.513-.292 4.42 4.42 0 01-.538-.232c-1.42-.735-2.593-2.121-2.74-3.806 0 0 .537-2 3.845-2 .357 0 1.38-.998 1.398-1.287-.005-.095-2.029-.9-2.817-1.677-.422-.416-.622-.616-.8-.767a3.47 3.47 0 00-.301-.227 5.388 5.388 0 01-.032-2.842c-1.195.544-2.124 1.403-2.8 2.163h-.006c-.46-.584-.428-2.51-.402-2.913-.006-.025-.343.176-.389.206-.406.29-.787.616-1.136.974-.397.403-.76.839-1.085 1.303a9.816 9.816 0 00-1.562 3.52c-.003.013-.11.487-.19 1.073-.013.09-.026.181-.037.272a7.8 7.8 0 00-.069.667l-.002.034-.023.387-.001.06C.386 18.795 5.593 24 12.016 24c5.752 0 10.527-4.176 11.463-9.661.02-.149.035-.298.052-.448.232-1.994-.025-4.09-.753-5.844z"},"github":{"title":"GitHub","hex":"#181717","path":"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"},"gitlab":{"title":"GitLab","hex":"#fc6d26","path":"m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"},"google":{"title":"Google","hex":"#4285f4","path":"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"},"googlechrome":{"title":"Google Chrome","hex":"#4285f4","path":"M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-5.344 9.257c.206.01.413.016.621.016 6.627 0 12-5.373 12-12 0-1.54-.29-3.011-.818-4.364zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728Z"},"instagram":{"title":"Instagram","hex":"#ff0069","path":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"},"kick":{"title":"Kick","hex":"#53fc19","path":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z"},"kickstarter":{"title":"Kickstarter","hex":"#05ce78","path":"M7.168 0c-3.2 0-5.797 2.579-5.797 5.758v12.484C1.371 21.42 3.968 24 7.168 24c1.981 0 3.716-.978 4.768-2.479l.794.79c2.26 2.245 5.943 2.245 8.203 0a5.724 5.724 0 001.696-4.075 5.724 5.724 0 00-1.696-4.074l-2.182-2.168 2.182-2.156a5.724 5.724 0 001.696-4.074 5.724 5.724 0 00-1.696-4.074c-2.26-2.246-5.942-2.246-8.203 0l-.794.789A5.797 5.797 0 007.168 0Z"},"line":{"title":"LINE","hex":"#00c300","path":"M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"},"linux":{"title":"Linux","hex":"#fcc624","path":"M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z"},"mastodon":{"title":"Mastodon","hex":"#6364ff","path":"M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"},"matrix":{"title":"Matrix","hex":"#000000","path":"M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"},"medium":{"title":"Medium","hex":"#000000","path":"M4.21 0A4.201 4.201 0 0 0 0 4.21v15.58A4.201 4.201 0 0 0 4.21 24h15.58A4.201 4.201 0 0 0 24 19.79v-1.093c-.137.013-.278.02-.422.02-2.577 0-4.027-2.146-4.09-4.832a7.592 7.592 0 0 1 .022-.708c.093-1.186.475-2.241 1.105-3.022a3.885 3.885 0 0 1 1.395-1.1c.468-.237 1.127-.367 1.664-.367h.023c.101 0 .202.004.303.01V4.211A4.201 4.201 0 0 0 19.79 0Zm.198 5.583h4.165l3.588 8.435 3.59-8.435h3.864v.146l-.019.004c-.705.16-1.063.397-1.063 1.254h-.003l.003 10.274c.06.676.424.885 1.063 1.03l.02.004v.145h-4.923v-.145l.019-.005c.639-.144.994-.353 1.054-1.03V7.267l-4.745 11.15h-.261L6.15 7.569v9.445c0 .857.358 1.094 1.063 1.253l.02.004v.147H4.405v-.147l.019-.004c.705-.16 1.065-.397 1.065-1.253V6.987c0-.857-.358-1.094-1.064-1.254l-.018-.004zm19.25 3.668c-1.086.023-1.733 1.323-1.813 3.124H24V9.298a1.378 1.378 0 0 0-.342-.047Zm-1.862 3.632c-.1 1.756.86 3.239 2.204 3.634v-3.634z"},"nodedotjs":{"title":"Node.js","hex":"#5fa04e","path":"M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z"},"npm":{"title":"npm","hex":"#cb3837","path":"M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"},"patreon":{"title":"Patreon","hex":"#000000","path":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z"},"paypal":{"title":"PayPal","hex":"#002991","path":"M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"},"pinterest":{"title":"Pinterest","hex":"#bd081c","path":"M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"},"python":{"title":"Python","hex":"#3776ab","path":"M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"},"raspberrypi":{"title":"Raspberry Pi","hex":"#a22846","path":"m19.8955 10.8961-.1726-.3028c.0068-2.1746-1.0022-3.061-2.1788-3.7348.356-.0938.7237-.1711.8245-.6182.6118-.1566.7397-.4398.8011-.7398.16-.1066.6955-.4061.6394-.9211.2998-.2069.4669-.4725.3819-.8487.3222-.3515.407-.6419.2702-.9096.3868-.4805.2152-.7295.05-.9817.2897-.5254.0341-1.0887-.7758-.9944-.3221-.4733-1.0244-.3659-1.133-.3637-.1215-.1519-.2819-.2821-.7755-.219-.3197-.2851-.6771-.2364-1.0458-.0964-.4378-.3403-.7275-.0675-1.0584.0356-.53-.1706-.6513.0631-.9117.1583-.5781-.1203-.7538.1416-1.0309.4182l-.3224-.0063c-.8719.5061-1.305 1.5366-1.4585 2.0664-.1536-.5299-.5858-1.5604-1.4575-2.0664l-.3223.0063C9.942.5014 9.7663.2394 9.1883.3597 8.9279.2646 8.807.0309 8.2766.2015c-.2172-.0677-.417-.2084-.6522-.2012l.0004.0002C7.5017.0041 7.369.049 7.2185.166c-.3688-.1401-.7262-.1887-1.0459.0964-.4936-.0631-.654.0671-.7756.219C5.2887.4791 4.5862.3717 4.264.845c-.8096-.0943-1.0655.4691-.7756.9944-.1653.2521-.3366.5013.05.9819-.1367.2677-.0519.5581.2703.9096-.085.3763.0822.6418.3819.8487-.0561.515.4795.8144.6394.9211.0614.3001.1894.5832.8011.7398.1008.4472.4685.5244.8245.6183-1.1766.6737-2.1856 1.56-2.1788 3.7348l-.1724.3028c-1.3491.8082-2.5629 3.4056-.6648 5.5167.124.6609.3319 1.1355.5171 1.6609.2769 2.117 2.0841 3.1082 2.5608 3.2255.6984.524 1.4423 1.0212 2.449 1.3696.949.964 1.977 1.3314 3.0107 1.3308.0152 0 .0306.0002.0457 0 1.0337.0006 2.0618-.3668 3.0107-1.3308 1.0067-.3483 1.7506-.8456 2.4491-1.3696.4766-.1173 2.2838-1.1085 2.5607-3.2255.1851-.5253.3931-1 .517-1.6609 1.8981-2.1113.6843-4.7089-.6649-5.517zm-1.0386-.3715c-.0704.8759-4.6354-3.0504-3.8472-3.1808 2.1391-.3558 3.9191.896 3.8472 3.1808zm-2.0155 4.3649c-1.1481.7409-2.8025.2626-3.6953-1.0681-.8928-1.3306-.6858-3.0101.4623-3.7509 1.1481-.7409 2.8025-.2627 3.6953 1.068.8927 1.3307.6858 3.0101-.4623 3.751zM13.6591 1.3721c.0396.1967.0843.321.1354.3577.2537-.272.4611-.5506.7878-.8123.0011.1537-.0776.3205.1169.4425.1752-.2356.4119-.4459.7263-.6244-.1514.2611-.026.3404.0554.4486.24-.2059.4681-.4144.9109-.5759-.121.1474-.2902.2914-.1108.4607.2473-.1544.496-.3086 1.0833-.4183-.1323.1475-.4059.295-.2401.4426.3104-.1186.6539-.2047 1.034-.2546-.182.1496-.3337.2963-.1846.4122.3323-.1022.7899-.2398 1.2372-.1212l-.2832.2849c-.0314.0382.6623.0297 1.1202.0364-.167.2321-.3375.4562-.437.8548.0454.0459.2723.0204.4862 0-.2194.4618-.6004.5783-.6893.776.134.1015.32.075.5232.006-.158.3254-.4892.5484-.7509.8123.0662.047.1818.075.4555.0425-.2418.257-.5339.492-.8802.7032.0614.0708.2722.0681.4678.0727-.3136.3069-.7173.466-1.0955.6668.1885.1288.3234.0988.4678.097-.2676.2198-.7225.3342-1.1448.4668.0803.1249.1607.1589.3324.194-.447.2473-1.0873.1343-1.2679.2607.0435.1243.1665.2053.3139.2728-.7197.0418-2.6879-.0262-3.0652-1.5156.7367-.8094 2.0813-1.7593 4.394-2.934-1.7994.6022-3.4229 1.405-4.7817 2.5096-1.5978-.7436-.4965-2.6197.283-3.3645zm-1.6126 5.3718c1.1329-.0123 2.5356.8325 2.53 1.6286-.005.7027-.9851 1.2715-2.5213 1.2607-1.5043-.0177-2.5172-.7148-2.5137-1.3957.003-.5603 1.2282-1.5263 2.505-1.4936zm-5.7646-.6006c.1717-.0351.252-.0692.3323-.194-.4223-.1327-.8772-.247-1.1448-.4668.1444.0018.2792.0318.4678-.097-.3783-.2008-.782-.3599-1.0956-.6668.1955-.0048.4064-.002.4677-.0728-.3462-.2113-.6383-.4463-.8801-.7033.2738.0325.3893.0045.4555-.0425-.2617-.264-.593-.487-.7509-.8123.2032.069.3892.0954.5232-.006-.089-.1977-.47-.3142-.6894-.776.214.0204.4409.0459.4863 0-.0994-.3985-.2698-.6226-.4369-.8547.4579-.0067 1.1516.0018 1.1202-.0364l-.2831-.2849c.4472-.1186.9049.019 1.2371.1213.1492-.1159-.0026-.2626-.1847-.4123.3801.05.7236.1361 1.034.2547.1659-.1476-.1076-.2951-.24-.4426.5872.1097.8361.2639 1.0833.4183.1794-.1694.0103-.3133-.1108-.4607.4428.1615.6709.37.911.5759.0814-.1082.2068-.1875.0554-.4486.3143.1785.5511.3888.7263.6244.1945-.122.1159-.2888.1169-.4426.3267.2618.534.5404.7879.8124.0511-.0366.0959-.161.1354-.3577.7794.7448 1.8807 2.6208.2831 3.3646-1.3589-1.1039-2.9817-1.9064-4.78-2.5086 2.3115 1.174 3.6556 2.1239 4.392 2.9328-.3773 1.4895-2.3455 1.5575-3.0651 1.5157.1473-.0676.2703-.1485.3139-.2728-.1806-.1264-.8209-.0134-1.2679-.2607zm2.8175 1.1334c.7881.1304-3.7769 4.0567-3.8472 3.1809-.0719-2.2846 1.7079-3.5367 3.8472-3.1809zm-4.847 8.7567c-1.1094-.8789-1.4668-3.4529.5901-4.6097 1.2394-.3273.4184 5.051-.5901 4.6097zm4.2656 4.5989c-.6257.3719-2.1452.2187-3.2252-1.3095-.7283-1.2823-.6345-2.5872-.123-2.9705.7648-.4589 1.9464.1609 2.8559 1.2003.7923.9405 1.1536 2.5927.4923 3.0797zm-1.2415-5.6086c-1.1481-.7409-1.3551-2.4203-.4623-3.7511.8928-1.3307 2.5472-1.8089 3.6952-1.068 1.1481.7409 1.3551 2.4203.4623 3.7509-.8926 1.3308-2.5471 1.809-3.6952 1.0682zm4.7948 8.2279c-1.3763.0584-2.7258-1.1105-2.7081-1.5157-.0206-.594 1.6758-1.0578 2.782-1.0306 1.1131-.0479 2.6068.3531 2.6097.8851.0184.5166-1.3547 1.6838-2.6836 1.6612zm2.7584-5.8578c.0081 1.3899-1.226 2.5225-2.7562 2.5299-1.5302.0073-2.7773-1.1135-2.7854-2.5033v-.0265c-.008-1.3899 1.2259-2.5226 2.7562-2.5299 1.5302-.0073 2.7773 1.1134 2.7853 2.5033a.7794.7794 0 0 1 .0001.0265zm3.855 2.0029c-1.186 1.6208-2.7916 1.684-3.3896 1.2325-.6255-.5811-.148-2.3854.7094-3.3747v-.0003c.9812-1.0912 2.0302-1.8037 2.7609-1.2469.4919.4828.7805 2.3008-.0807 3.3894zm1.0724-3.4301c-1.0086.4413-1.8298-4.9372-.5901-4.61 2.0568 1.1569 1.6994 3.731.5901 4.61zm-.0256-8.3279h.2985v-.5304h.2986c.1502 0 .2053.0624.2262.2052.0152.1088.0113.2395.0477.3253h.2984c-.0533-.0763-.0515-.2358-.0571-.3213-.0097-.1373-.0513-.2796-.1977-.3176v-.0037c.1502-.061.2149-.1807.2149-.341 0-.2048-.1539-.3738-.3974-.3738h-.732v1.3573zm.2985-1.1255h.3269c.1333 0 .2054.0573.2054.188 0 .1369-.0721.1942-.2054.1942H20.03v-.3822zm-1.0337.4633c0 .7009.5682 1.2694 1.2695 1.2694s1.2695-.5684 1.2695-1.2694c0-.7013-.5683-1.2697-1.2695-1.2697-.7013 0-1.2695.5684-1.2695 1.2697zm2.3275 0c0 .5845-.4737 1.058-1.058 1.058s-1.058-.4735-1.058-1.058c0-.5849.4737-1.058 1.058-1.058s1.058.4731 1.058 1.058z"},"reddit":{"title":"Reddit","hex":"#ff4500","path":"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"},"signal":{"title":"Signal","hex":"#3b45fd","path":"M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125"},"snapchat":{"title":"Snapchat","hex":"#fffc00","path":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"},"soundcloud":{"title":"SoundCloud","hex":"#ff5500","path":"M23.999 14.165c-.052 1.796-1.612 3.169-3.4 3.169h-8.18a.68.68 0 0 1-.675-.683V7.862a.747.747 0 0 1 .452-.724s.75-.513 2.333-.513a5.364 5.364 0 0 1 2.763.755 5.433 5.433 0 0 1 2.57 3.54c.282-.08.574-.121.868-.12.884 0 1.73.358 2.347.992s.948 1.49.922 2.373ZM10.721 8.421c.247 2.98.427 5.697 0 8.672a.264.264 0 0 1-.53 0c-.395-2.946-.22-5.718 0-8.672a.264.264 0 0 1 .53 0ZM9.072 9.448c.285 2.659.37 4.986-.006 7.655a.277.277 0 0 1-.55 0c-.331-2.63-.256-5.02 0-7.655a.277.277 0 0 1 .556 0Zm-1.663-.257c.27 2.726.39 5.171 0 7.904a.266.266 0 0 1-.532 0c-.38-2.69-.257-5.21 0-7.904a.266.266 0 0 1 .532 0Zm-1.647.77a26.108 26.108 0 0 1-.008 7.147.272.272 0 0 1-.542 0 27.955 27.955 0 0 1 0-7.147.275.275 0 0 1 .55 0Zm-1.67 1.769c.421 1.865.228 3.5-.029 5.388a.257.257 0 0 1-.514 0c-.21-1.858-.398-3.549 0-5.389a.272.272 0 0 1 .543 0Zm-1.655-.273c.388 1.897.26 3.508-.01 5.412-.026.28-.514.283-.54 0-.244-1.878-.347-3.54-.01-5.412a.283.283 0 0 1 .56 0Zm-1.668.911c.4 1.268.257 2.292-.026 3.572a.257.257 0 0 1-.514 0c-.241-1.262-.354-2.312-.023-3.572a.283.283 0 0 1 .563 0Z"},"spotify":{"title":"Spotify","hex":"#1ed760","path":"M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"},"steam":{"title":"Steam","hex":"#000000","path":"M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"},"stripe":{"title":"Stripe","hex":"#635bff","path":"M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z"},"telegram":{"title":"Telegram","hex":"#26a5e4","path":"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"},"threads":{"title":"Threads","hex":"#000000","path":"M18.263 11.097c-.03-3.486-1.92-5.586-5.111-5.586-2.13 0-3.922.963-4.863 2.499l2.062 1.438c.535-.843 1.272-1.543 2.628-1.543 1.528 0 2.318.85 2.544 2.431a15 15 0 0 0-2.236-.173c-4.125 0-6.068 1.867-6.068 4.336s1.943 3.99 4.804 3.99c3.139 0 5.013-2.115 5.781-4.735.798.361 1.348 1.204 1.348 2.47 0 3.387-3.907 5.232-7.22 5.232-4.885 0-8.077-3.207-8.077-8.424 0-6.392 4.223-10.487 9.9-10.487 3.808 0 5.69 1.671 6.97 3.914l2.108-1.475C21.44 2.078 18.331 0 13.663 0 6.227 0 1.168 5.277 1.168 12.934c0 7 4.953 11.066 10.856 11.066 4.878 0 9.809-2.846 9.809-7.716 0-2.545-1.46-4.231-3.569-5.187m-6.33 4.855c-1.077 0-2.026-.512-2.026-1.453 0-1.483 1.822-1.934 3.606-1.934.678 0 1.34.045 1.927.173-.422 1.927-1.671 3.215-3.508 3.214Z"},"tiktok":{"title":"TikTok","hex":"#000000","path":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"},"tumblr":{"title":"Tumblr","hex":"#36465d","path":"M14.563 24c-5.093 0-7.031-3.756-7.031-6.411V9.747H5.116V6.648c3.63-1.313 4.512-4.596 4.71-6.469C9.84.051 9.941 0 9.999 0h3.517v6.114h4.801v3.633h-4.82v7.47c.016 1.001.375 2.371 2.207 2.371h.09c.631-.02 1.486-.205 1.936-.419l1.156 3.425c-.436.636-2.4 1.374-4.156 1.404h-.178l.011.002z"},"twitch":{"title":"Twitch","hex":"#9146ff","path":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"},"ubuntu":{"title":"Ubuntu","hex":"#e95420","path":"M17.61.455a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zM12.92.8C8.923.777 5.137 2.941 3.148 6.451a4.5 4.5 0 0 1 .26-.007 4.92 4.92 0 0 1 2.585.737A8.316 8.316 0 0 1 12.688 3.6 4.944 4.944 0 0 1 13.723.834 11.008 11.008 0 0 0 12.92.8zm9.226 4.994a4.915 4.915 0 0 1-1.918 2.246 8.36 8.36 0 0 1-.273 8.303 4.89 4.89 0 0 1 1.632 2.54 11.156 11.156 0 0 0 .559-13.089zM3.41 7.932A3.41 3.41 0 0 0 0 11.342a3.41 3.41 0 0 0 3.41 3.409 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zm2.027 7.866a4.908 4.908 0 0 1-2.915.358 11.1 11.1 0 0 0 7.991 6.698 11.234 11.234 0 0 0 2.422.249 4.879 4.879 0 0 1-.999-2.85 8.484 8.484 0 0 1-.836-.136 8.304 8.304 0 0 1-5.663-4.32zm11.405.928a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41z"},"vimeo":{"title":"Vimeo","hex":"#1ab7ea","path":"M23.9765 6.4168c-.105 2.338-1.739 5.5429-4.894 9.6088-3.2679 4.247-6.0258 6.3699-8.2898 6.3699-1.409 0-2.578-1.294-3.553-3.881l-1.9179-7.1138c-.719-2.584-1.488-3.878-2.312-3.878-.179 0-.806.378-1.8809 1.132l-1.129-1.457a315.06 315.06 0 003.501-3.1279c1.579-1.368 2.765-2.085 3.5539-2.159 1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.5069.5389 2.45 1.1309 3.674 1.7759 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.8679 3.434-5.7568 6.7619-5.6368 2.4729.06 3.6279 1.664 3.4929 4.7969z"},"whatsapp":{"title":"WhatsApp","hex":"#25d366","path":"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"},"wikipedia":{"title":"Wikipedia","hex":"#000000","path":"M12.09 13.119c-.936 1.932-2.217 4.548-2.853 5.728-.616 1.074-1.127.931-1.532.029-1.406-3.321-4.293-9.144-5.651-12.409-.251-.601-.441-.987-.619-1.139-.181-.15-.554-.24-1.122-.271C.103 5.033 0 4.982 0 4.898v-.455l.052-.045c.924-.005 5.401 0 5.401 0l.051.045v.434c0 .119-.075.176-.225.176l-.564.031c-.485.029-.727.164-.727.436 0 .135.053.33.166.601 1.082 2.646 4.818 10.521 4.818 10.521l.136.046 2.411-4.81-.482-1.067-1.658-3.264s-.318-.654-.428-.872c-.728-1.443-.712-1.518-1.447-1.617-.207-.023-.313-.05-.313-.149v-.468l.06-.045h4.292l.113.037v.451c0 .105-.076.15-.227.15l-.308.047c-.792.061-.661.381-.136 1.422l1.582 3.252 1.758-3.504c.293-.64.233-.801.111-.947-.07-.084-.305-.22-.812-.24l-.201-.021c-.052 0-.098-.015-.145-.051-.045-.031-.067-.076-.067-.129v-.427l.061-.045c1.247-.008 4.043 0 4.043 0l.059.045v.436c0 .121-.059.178-.193.178-.646.03-.782.095-1.023.439-.12.186-.375.589-.646 1.039l-2.301 4.273-.065.135 2.792 5.712.17.048 4.396-10.438c.154-.422.129-.722-.064-.895-.197-.172-.346-.273-.857-.295l-.42-.016c-.061 0-.105-.014-.152-.045-.043-.029-.072-.075-.072-.119v-.436l.059-.045h4.961l.041.045v.437c0 .119-.074.18-.209.18-.648.03-1.127.18-1.443.421-.314.255-.557.616-.736 1.067 0 0-4.043 9.258-5.426 12.339-.525 1.007-1.053.917-1.503-.031-.571-1.171-1.773-3.786-2.646-5.71l.053-.036z"},"x":{"title":"X","hex":"#000000","path":"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"},"ycombinator":{"title":"Y Combinator","hex":"#f0652f","path":"M0 24V0h24v24H0zM6.951 5.896l4.112 7.708v5.064h1.583v-4.972l4.148-7.799h-1.749l-2.457 4.875c-.372.745-.688 1.434-.688 1.434s-.297-.708-.651-1.434L8.831 5.896h-1.88z"},"youtube":{"title":"YouTube","hex":"#ff0000","path":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"}};++ const FA = {"youtube":"youtube","square-youtube":"youtube","twitch":"twitch","discord":"discord","github":"github","square-github":"github","x-twitter":"x","square-x-twitter":"x","twitter":"x","kick":"kick","kickstarter-k":"kick","kickstarter":"kickstarter","google":"google","tiktok":"tiktok","threads":"threads","spotify":"spotify","soundcloud":"soundcloud","reddit":"reddit","reddit-alien":"reddit","tumblr":"tumblr","hacker-news":"ycombinator","line":"line","paypal":"paypal","patreon":"patreon","mastodon":"mastodon","linux":"linux","instagram":"instagram","facebook":"facebook","facebook-f":"facebook","cloudflare":"cloudflare","brave":"brave","bluesky":"bluesky","bitcoin":"bitcoin","apple":"apple","telegram":"telegram","whatsapp":"whatsapp","steam":"steam","snapchat":"snapchat","pinterest":"pinterest","vimeo":"vimeo","vimeo-v":"vimeo","npm":"npm","node":"nodedotjs","node-js":"nodedotjs","docker":"docker","python":"python","android":"android","chrome":"googlechrome","firefox":"firefoxbrowser","firefox-browser":"firefoxbrowser","gitlab":"gitlab","ubuntu":"ubuntu","raspberry":"raspberrypi","raspberry-pi":"raspberrypi","stripe":"stripe","cc-stripe":"stripe","signal":"signal","matrix":"matrix","medium":"medium","wikipedia":"wikipedia","wikipedia-w":"wikipedia","ethereum":"ethereum"};++ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);++ const get = (slug) => (has(BRANDS, slug) ? BRANDS[slug] : null);++ const fromFa = (name) => { const n = String(name || '').replace(/^fa-/, ''); return has(FA, n) ? FA[n] : null; };++ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));++ function svg(slug, { size = 24, title = '', color = 'currentColor' } = {}) {++ const b = get(slug);++ if (!b) return '';++ const label = title ? ` role="img" aria-label="${esc(title)}"><title>${esc(title)}</title>` : ' aria-hidden="true">';++ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${Number(size) || 24}" height="${Number(size) || 24}" fill="${color === 'brand' ? b.hex : esc(color)}"${label}<path d="${b.path}"/></svg>`;++ }++ return { BRANDS, FA, get, fromFa, svg, SLUGS: Object.keys(BRANDS) };++});nnew file mode 100644@@@ -0,0 +1,381 @@++// ═══════════════════════════════════════════════════════════════++// OpenVibe — the Dock's Actor block (Actor v3 plan, stage A3; redrawn 2026-11-10)++//++// Your Actor at the top of the Dock's Agents tab, on every OpenVibe site. dock.js loads this file the first time that++// tab opens:++// OpenVibeDockActor.mount(container, { token, base, active }) → { refresh(), destroy() }++//++// It reads OpenVibe.Actor's GET /api/v1/me/actor with the person's Network token (scope actor.me.read); the answers,++// pause, resume and tell are POSTs with actor.me.write. On a site every call goes through Network's same-origin relay++// (/api/frame/actor…) because every site's connect-src allows openvibe.network and refuses openvibe.actor; `base`++// reaches Actor itself in a test, and `frameBase` names the relay. Nothing is asked without a token.++//++// The look (the owner, 2026-11-10: "more intuitive, minimalist, elegant"): one row and one line to type in.++// - Who: the face with a ring in the colour of its state, the name, and one quiet line (the state, today's spend++// against the cap). The row is the way to the Actor's page; there is no separate "Open".++// - One action beside it, the one that fits: "Resume" in the accent when it is paused, a quiet pause when it runs.++// - What it does, only when there is something to say: the step it is on (a soft pulse), what it looks at, the++// steps before it, and what comes next, as a short line of dots. A paused Actor shows none of this: the row says it.++// - What it asks you, with its own buttons, in the warning tone (a lean and a glow, never a bar).++// - One field to tell it something, with send inside the field.++// A person with no Actor sees nothing here. Everything people or the Actor wrote is set as text, never markup. The++// block is built once: a refresh repaints what changed and never touches the line being typed. Every control is 44 px.++// ═══════════════════════════════════════════════════════════════++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockActor) return;++++ const POLL_MS = 15000;++ const ACTOR_REV = { 'ov-window.js': 'd3d8f9e3e6d2', 'agent-sheet.js': '8d150490ad64' };++ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';++ const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };++ // The lines Actor sends as "the current step" when there is none: the state already says them.++ const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;++ const ICON = {++ play: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 5.6v12.8a.9.9 0 0 0 1.37.77l10.3-6.4a.9.9 0 0 0 0-1.54l-10.3-6.4a.9.9 0 0 0-1.37.77z" fill="currentColor"/></svg>',++ pause: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6.5" y="5" width="4" height="14" rx="1.3" fill="currentColor"/><rect x="13.5" y="5" width="4" height="14" rx="1.3" fill="currentColor"/></svg>',++ go: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 6.5 15 12l-5.5 5.5"/></svg>',++ send: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5.5M6 11l6-6 6 6"/></svg>',++ ask: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.2 9.3a2.9 2.9 0 1 1 4.4 2.5c-.9.6-1.6 1.2-1.6 2.3"/><path d="M12 17.6v.1"/></svg>',++ past: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 1 0 2.6-5.9"/><path d="M4 5v3.6h3.6"/><path d="M12 8.2V12l2.6 1.7"/></svg>',++ };++++ function styles(doc) {++ const d = doc || document;++ if (!d || d.getElementById('ovdac-styles')) return;++ const st = d.createElement('style');++ st.id = 'ovdac-styles';++ st.textContent = `++ .ovdac { display: none; flex: none; padding: 12px 12px 8px; border-bottom: 1px solid var(--border, rgba(255,255,255,.08));++ --ovdac-ok: var(--success, #22c55e); --ovdac-warn: var(--warning, #f5a524); --ovdac-ink: var(--text-primary, #ececf1); }++ .ovdac.on { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }++ .ovdac [hidden] { display: none !important; }++ .ovdac svg { width: 18px; height: 18px; flex: none; }++ .ovdac .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }++++ .ovdac-head { display: flex; align-items: center; gap: 8px; min-width: 0; }++ .ovdac-id { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 2px 8px 2px 4px; margin-left: -4px; border-radius: 14px;++ color: inherit; text-decoration: none; transition: background .15s ease; }++ .ovdac-id:hover { background: color-mix(in srgb, var(--ovdac-ink) 5%, transparent); }++ .ovdac-id:focus-visible, .ovdac-go:focus-visible, .ovdac-btn:focus-visible, .ovdac-link:focus-visible, .ovdac-send:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 2px; }++ .ovdac-face { position: relative; flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;++ background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font-weight: 800; font-size: 16px; transition: filter .2s ease, opacity .2s ease; }++ .ovdac-face img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }++ .ovdac-face::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid transparent; transition: border-color .2s ease; }++ .ovdac[data-state="working"] .ovdac-face::after { border-color: var(--ovdac-ok); animation: ovdac-ring 2.4s ease-in-out infinite; }++ .ovdac[data-state="waiting"] .ovdac-face::after { border-color: var(--ovdac-warn); }++ .ovdac[data-state="paused"] .ovdac-face { filter: saturate(.5); opacity: .78; }++ .ovdac-who { min-width: 0; display: grid; gap: 1px; }++ .ovdac-name { display: flex; align-items: center; gap: 1px; min-width: 0; font-weight: 750; font-size: 15px; line-height: 1.25; color: var(--ovdac-ink); }++ .ovdac-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++ .ovdac-name svg { width: 16px; height: 16px; color: var(--text-muted, #8b8b96); transition: transform .18s ease, color .18s ease; }++ .ovdac-id:hover .ovdac-name svg { transform: translateX(2px); color: var(--text-secondary, #b6b6c0); }++ .ovdac-line { display: flex; gap: 6px; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); white-space: nowrap; }++ .ovdac-state { flex: none; font-weight: 650; color: var(--text-secondary, #b6b6c0); }++ .ovdac[data-state="working"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-ok) 72%, var(--ovdac-ink)); }++ .ovdac[data-state="waiting"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-warn) 76%, var(--ovdac-ink)); }++ .ovdac-spend { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }++ .ovdac-spend::before { content: '·'; margin-right: 6px; }++++ .ovdac-go { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 44px; height: 44px; padding: 0; border-radius: 999px;++ border: 1px solid var(--border, rgba(255,255,255,.14)); background: transparent; color: var(--text-secondary, #b6b6c0);++ font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, color .15s ease, transform .12s ease, box-shadow .2s ease; }++ .ovdac-go:hover { color: var(--ovdac-ink); background: color-mix(in srgb, var(--ovdac-ink) 7%, transparent); }++ .ovdac-go.is-main { padding: 0 18px 0 13px; border-color: transparent; background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff);++ box-shadow: 0 8px 20px -10px var(--accent, #3b82f6); }++ .ovdac-go.is-main:hover { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); filter: brightness(1.08); }++ .ovdac-go:active, .ovdac-btn:active, .ovdac-send:active { transform: scale(.96); }++ .ovdac .is-busy { opacity: .6; pointer-events: none; animation: ovdac-pulse 1.1s ease-in-out infinite; }++++ .ovdac-rail { list-style: none; margin: 0; padding: 2px 4px 2px 6px; display: grid; gap: 9px; }++ .ovdac-rail li { position: relative; padding-left: 22px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }++ .ovdac-rail li::before { content: ''; position: absolute; left: 1px; top: .36em; width: 6px; height: 6px; border-radius: 50%; border: 1.5px solid var(--text-muted, #8b8b96); }++ .ovdac-rail li:not(:last-child)::after { content: ''; position: absolute; left: 5px; top: calc(.36em + 13px); bottom: -8px; width: 1px;++ background: color-mix(in srgb, var(--ovdac-ink) 14%, transparent); }++ .ovdac-rail .is-live { font-size: 13.5px; font-weight: 650; color: var(--ovdac-ink); }++ .ovdac-rail .is-live::before { border-color: var(--ovdac-ok); background: var(--ovdac-ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovdac-ok) 20%, transparent); animation: ovdac-pulse 1.8s ease-in-out infinite; }++ .ovdac-rail .is-next { color: var(--text-muted, #8b8b96); }++ .ovdac-rail .is-next::before { border-style: dashed; }++ .ovdac-rail b { font-weight: 650; }++ .ovdac-look { display: block; margin-top: 2px; font-size: 12px; font-weight: 400; color: var(--text-muted, #8b8b96); }++++ .ovdac-ask { display: grid; gap: 10px; padding: 12px 14px 10px; border-radius: 16px;++ background: linear-gradient(135deg, color-mix(in srgb, var(--ovdac-warn) 15%, transparent), color-mix(in srgb, var(--ovdac-warn) 5%, transparent) 62%);++ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ovdac-warn) 22%, transparent), 0 12px 28px -20px var(--ovdac-warn); }++ .ovdac-ask-k { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; color: color-mix(in srgb, var(--ovdac-warn) 74%, var(--ovdac-ink)); }++ .ovdac-ask-k i { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: color-mix(in srgb, var(--ovdac-warn) 24%, transparent); }++ .ovdac-ask-k svg { width: 16px; height: 16px; }++ .ovdac-ask p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ovdac-ink); overflow-wrap: anywhere; }++ .ovdac-ask .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }++ .ovdac-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 16px; border-radius: 999px;++ border: 1px solid color-mix(in srgb, var(--ovdac-ink) 18%, transparent); background: transparent; color: var(--ovdac-ink);++ font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, transform .12s ease; }++ .ovdac-btn:hover { background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); }++ .ovdac-btn.is-yes { background: var(--accent-strong, var(--accent, #2563eb)); border-color: transparent; color: var(--on-accent-strong, #fff); }++ .ovdac-btn.is-yes:hover { background: var(--accent-strong, var(--accent, #2563eb)); filter: brightness(1.08); }++ .ovdac-btn.is-quiet { padding: 0 10px; border-color: transparent; color: var(--text-secondary, #b6b6c0); font-weight: 650; }++ .ovdac-more { display: inline-flex; align-items: center; min-height: 44px; margin: -8px 0 -8px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); text-decoration: underline; text-underline-offset: 3px; }++++ .ovdac-tell { position: relative; display: flex; }++ .ovdac-tell input { flex: 1; min-width: 0; height: 52px; box-sizing: border-box; padding: 0 58px 0 18px; border-radius: 26px;++ border: 1px solid var(--border, rgba(255,255,255,.14)); background: var(--bg-input, color-mix(in srgb, var(--ovdac-ink) 4%, transparent));++ color: var(--ovdac-ink); font: inherit; font-size: 14.5px; outline: none; transition: border-color .15s ease, box-shadow .15s ease; }++ .ovdac-tell input::placeholder { color: var(--text-muted, #8b8b96); }++ .ovdac-tell input:focus { border-color: color-mix(in srgb, var(--accent, #3b82f6) 62%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }++ .ovdac-send { position: absolute; right: 4px; top: 4px; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;++ background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); color: var(--text-muted, #8b8b96); cursor: pointer;++ transition: background .18s ease, color .18s ease, transform .12s ease, box-shadow .2s ease; }++ .ovdac-tell.has-text .ovdac-send { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); box-shadow: 0 6px 16px -8px var(--accent, #3b82f6); }++++ .ovdac-foot { display: flex; align-items: center; gap: 10px; min-width: 0; margin-top: -6px; }++ .ovdac-link { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 8px; margin-left: -8px; border: 0; border-radius: 10px;++ background: none; color: var(--text-muted, #8b8b96); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; transition: color .15s ease; }++ .ovdac-link:hover, .ovdac-link[aria-expanded="true"] { color: var(--ovdac-ink); }++ .ovdac-link svg { width: 15px; height: 15px; }++ .ovdac-note { flex: 1; min-width: 0; text-align: right; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); overflow-wrap: anywhere; }++ .ovdac-note.is-ok { color: color-mix(in srgb, var(--ovdac-ok) 70%, var(--ovdac-ink)); }++ .ovdac-note.is-bad { color: color-mix(in srgb, var(--danger, #ef4444) 74%, var(--ovdac-ink)); }++ .ovdac-hist { list-style: none; margin: -4px 0 4px; padding: 0; display: grid; gap: 6px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }++ .ovdac-hist b { margin-right: 6px; font-weight: 650; color: var(--text-muted, #8b8b96); }++++ @keyframes ovdac-pulse { 50% { opacity: .45; } }++ @keyframes ovdac-ring { 50% { opacity: .35; transform: scale(1.06); } }++ @media (pointer: coarse) { .ovdac-tell input { font-size: 16px; } }++ html[data-ov-motion=off] .ovdac *, html[data-ov-motion=off] .ovdac *::before, html[data-ov-motion=off] .ovdac *::after { animation: none !important; transition: none !important; }++ `;++ (d.head || d.documentElement).appendChild(st);++ }++++ const money = (n) => { const v = Number(n) || 0; return v < 0.01 && v > 0 ? `$${v.toFixed(3)}` : `$${v.toFixed(2)}`; };++ /** "in 40 min", "in 3 h", "tomorrow": when the next goal is due; '' when unknown or already due. */++ function due(at, now = Date.now()) {++ const t = Date.parse(at || '');++ if (!Number.isFinite(t)) return '';++ const min = Math.round((t - now) / 60000);++ if (min < 1) return '';++ if (min < 60) return `in ${min} min`;++ if (min < 24 * 60) return `in ${Math.round(min / 60)} h`;++ return min < 48 * 60 ? 'tomorrow' : `in ${Math.round(min / 1440)} days`;++ }++++ function mount(container, opts = {}) {++ const d = container.ownerDocument || document;++ styles(d);++ const o = { ...opts };++ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };++ const icon = (name) => { const t = d.createElement('template'); t.innerHTML = ICON[name]; return t.content.firstChild; };++ const tip = (n, text) => { if (root.OVTip) n.setAttribute('data-ov-tip', text); else n.title = text; };++ let gone = false, timer = 0, busy = false, shown = false, last = '', actor = null, noteTimer = 0;++ container.replaceChildren();++ container.classList.add('ovdac');++ // Network's frame relay on a site; an explicit base reaches Actor itself (a test).++ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';++ const frameBase = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const endpoint = (path) => (direct ? direct + '/api/v1/me/actor' + path : frameBase + '/actor' + path);++ // The way to the Actor's page is a navigation, never a fetch: Actor's own site, wherever the API is.++ const openBase = direct || 'https://openvibe.actor';++++ async function api(path, init = {}) {++ const t = typeof o.token === 'function' ? await o.token() : o.token;++ if (!t) throw Object.assign(new Error('Sign in to use your Actor'), { status: 401 });++ const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), authorization: `Bearer ${t}` };++ const r = await fetch(endpoint(path), { method: init.method || 'GET', headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });++ const b = await r.json().catch(() => ({}));++ if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });++ return b;++ }++++ function stateOf(a) {++ if (Array.isArray(a.asks) && a.asks.length) return 'waiting';++ return STATE[a.status] ? a.status : 'paused';++ }++++ // ── The parts, built once ────────────────────────────────────────────────────────────────────────────────++ const head = h('div', 'ovdac-head');++ const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';++ function loadScript(file, ready) {++ if (ready()) return Promise.resolve(true);++ return new Promise((resolve) => {++ const sid = `ovdac-${file}`;++ let script = d.getElementById(sid);++ if (!script) { script = d.createElement('script'); script.id = sid; script.src = SCRIPT_BASE + file + (ACTOR_REV[file] ? `?v=${ACTOR_REV[file]}` : ''); script.async = true; d.head.appendChild(script); }++ script.addEventListener('load', () => resolve(!!ready()), { once: true });++ script.addEventListener('error', () => resolve(false), { once: true });++ });++ }++ // The row opens its sheet (agent-sheet.js in the one window: now, asks, work, schedule, memory, computer); the++ // Actor's page is the sheet's "Open". A modified click, or a sheet that cannot load, goes to the page as before.++ id.addEventListener('click', async (e) => {++ if (!actor || !(actor.agent_id || actor.id) || e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;++ e.preventDefault();++ const ok = (await Promise.all([loadScript('ov-window.js', () => !!root.OVWindow), loadScript('agent-sheet.js', () => !!root.OVAgentSheet)])).every(Boolean);++ if (!ok) { d.defaultView.open(id.href, '_blank', 'noopener'); return; }++ root.OVAgentSheet.open({ kind: 'actor', id: actor.agent_id || actor.id, name: actor.name || 'Your Actor', token: o.token,++ frameBase: o.frameBase, base: direct || null, from: id, openHref: openBase + '/you' });++ });++ const face = h('span', 'ovdac-face');++ const who = h('span', 'ovdac-who');++ const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));++ const line = h('span', 'ovdac-line'); const stateText = h('span', 'ovdac-state'); const spend = h('span', 'ovdac-spend'); line.append(stateText, spend);++ who.append(name, line); id.append(face, who);++ const go = h('button', 'ovdac-go'); go.type = 'button';++ go.addEventListener('click', () => {++ if (!actor) return;++ const paused = actor.status === 'paused';++ act(paused ? '/resume' : '/pause', {}, go, paused ? 'Resumed.' : 'Paused.');++ });++ head.append(id, go);++++ const rail = h('ul', 'ovdac-rail'); rail.hidden = true;++ const askBox = h('div', 'ovdac-ask'); askBox.hidden = true;++++ const form = h('form', 'ovdac-tell');++ const input = h('input'); input.type = 'text'; input.maxLength = 2000; input.autocomplete = 'off'; input.enterKeyHint = 'send';++ input.placeholder = 'Tell your Actor something'; input.setAttribute('aria-label', 'Tell your Actor something');++ const send = h('button', 'ovdac-send'); send.type = 'submit'; send.setAttribute('aria-label', 'Send'); send.append(icon('send'));++ input.addEventListener('input', () => form.classList.toggle('has-text', !!input.value.trim()));++ form.append(input, send);++ form.addEventListener('submit', (e) => {++ e.preventDefault();++ const text = input.value.trim();++ if (!text) { input.focus(); return; }++ const paused = actor && actor.status === 'paused';++ act('/tell', { text }, send, paused ? 'Sent. Your Actor reads it once you resume it.' : 'Sent. Your Actor reads it at its next step.')++ .then((ok) => { if (ok) { input.value = ''; form.classList.remove('has-text'); } });++ });++++ const foot = h('div', 'ovdac-foot');++ const past = h('button', 'ovdac-link'); past.type = 'button'; past.setAttribute('aria-expanded', 'false'); past.append(icon('past'), d.createTextNode('Recent history'));++ const note = h('div', 'ovdac-note'); note.setAttribute('role', 'status'); note.setAttribute('aria-live', 'polite');++ const hist = h('ul', 'ovdac-hist'); hist.hidden = true;++ foot.append(past, note);++ // The button that opens the list closes it.++ past.addEventListener('click', async () => {++ if (!hist.hidden) { hist.hidden = true; past.setAttribute('aria-expanded', 'false'); return; }++ past.disabled = true; past.classList.add('is-busy');++ const one = (text) => { hist.replaceChildren(h('li', '', text)); };++ try {++ const t = typeof o.token === 'function' ? await o.token() : o.token;++ if (!t) throw new Error('Sign in to read history.');++ const url = String(o.networkBase || 'https://openvibe.network').replace(/\/+$/, '') + '/api/v1/history/agents/recent?limit=10';++ const response = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });++ const result = await response.json().catch(() => ({}));++ if (response.status === 403) one('Your agent history setting is off. Turn it on in My settings to allow this read.');++ else if (!response.ok) one('Recent history is unavailable.');++ else {++ const items = (result.items || []).slice(0, 10);++ if (!items.length) one('No recent history.');++ else hist.replaceChildren(...items.map((x) => { const li = h('li'); li.append(h('b', '', String(x.type || '')), d.createTextNode(String(x.title || ''))); return li; }));++ }++ } catch { one('Recent history is unavailable.'); }++ finally { past.disabled = false; past.classList.remove('is-busy'); }++ hist.hidden = false; past.setAttribute('aria-expanded', 'true');++ });++++ /** One line under the field: what just happened. A good word goes by itself after a moment; a problem stays. */++ function say(text, tone) {++ clearTimeout(noteTimer);++ note.textContent = text || '';++ note.className = 'ovdac-note' + (tone ? ` is-${tone}` : '');++ if (text && tone === 'ok') noteTimer = setTimeout(() => { note.textContent = ''; note.className = 'ovdac-note'; }, 6000);++ }++++ // ── A refresh repaints the row, the steps and the ask; the field and the history stay as they are ────────++ function paint(a) {++ if (!a || (a.enabled === false && !a.id)) {++ actor = null; shown = false; last = '';++ container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state');++ return;++ }++ actor = a;++ if (!shown) { container.replaceChildren(head, rail, askBox, form, foot, hist); container.classList.add('on'); shown = true; }++ const sig = JSON.stringify([a.name, a.status, a.avatar, a.brain, a.caps, a.now, a.asks]);++ if (sig === last) return;++ last = sig;++ const state = stateOf(a), now = a.now || {};++ container.setAttribute('data-state', state);++++ // Who, and the one line about it.++ const label = String(a.name || 'Your Actor');++ const pic = a.avatar && typeof a.avatar.url === 'string' && /^https:\/\//.test(a.avatar.url) ? a.avatar.url : '';++ if (pic) { const img = h('img'); img.alt = ''; img.src = pic; face.replaceChildren(img); } else face.replaceChildren(d.createTextNode(label.trim().charAt(0).toUpperCase() || 'A'));++ nameText.textContent = label;++ id.setAttribute('aria-label', `${label}, ${STATE[state]}. Open your Actor's page`);++ stateText.textContent = STATE[state];++ const cap = a.caps && Number(a.caps.usd) > 0 ? ` of ${money(a.caps.usd)}` : '';++ spend.textContent = `${money(now.cost_today)}${cap} today`;++ tip(spend, `Model: ${a.brain && a.brain !== 'auto' ? a.brain : 'Auto'}`);++++ // The one action that fits.++ const paused = a.status === 'paused';++ go.className = 'ovdac-go' + (paused ? ' is-main' : '');++ go.replaceChildren(icon(paused ? 'play' : 'pause'), h('span', paused ? '' : 'sr', paused ? 'Resume' : 'Pause'));++ tip(go, paused ? 'Your Actor does nothing until you resume it' : 'Pause your Actor');++++ // What it does: the step it is on, the steps before it, what comes next. Nothing when there is nothing to say.++ const asks = Array.isArray(a.asks) ? a.asks : [];++ const ask = asks[0];++ const step = String(now.current_step || '').trim();++ const live = a.status === 'working' && step && !NOT_A_STEP.test(step) && !(ask && step === String(ask.text || '').trim()) ? step : '';++ const items = [];++ if (live) {++ const li = h('li', 'is-live'); li.append(d.createTextNode(live));++ const looking = now.looking_at && (now.looking_at.title || now.looking_at);++ if (looking) li.append(h('span', 'ovdac-look', `Looking at ${String(looking)}`));++ items.push(li);++ }++ const before = (Array.isArray(now.last_three_steps) ? now.last_three_steps : []).map((x) => String((x && x.text) || x || '').trim()).filter((t) => t && t !== live).slice(0, 3);++ if (!paused) before.forEach((t) => items.push(h('li', '', t)));++ if (!paused && now.next && (now.next.text || typeof now.next === 'string')) {++ const li = h('li', 'is-next'); const when = due(now.next.at);++ li.append(h('b', '', 'Next: '), d.createTextNode(String(now.next.text || now.next) + (when ? `, ${when}` : '')));++ items.push(li);++ }++ rail.replaceChildren(...items); rail.hidden = !items.length;++++ // The first thing it asks, with its own buttons.++ if (ask) {++ const k = h('div', 'ovdac-ask-k'); const chip = h('i'); chip.append(icon('ask')); k.append(chip, d.createTextNode(`${label} asks`));++ const row = h('div', 'row');++ const options = Array.isArray(ask.options) && ask.options.length ? ask.options : [{ id: 'yes', label: 'Yes' }];++ options.slice(0, 3).forEach((op, i) => {++ const b = h('button', `ovdac-btn${i === 0 ? ' is-yes' : ''}`, String(op.label || op)); b.type = 'button';++ b.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/answer`, { option: op.id || op }, b, 'Answered.'));++ row.append(b);++ });++ const no = h('button', 'ovdac-btn is-quiet', 'Not now'); no.type = 'button';++ no.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/decline`, {}, no));++ row.append(no);++ askBox.replaceChildren(k, h('p', '', String(ask.text || '')), row);++ if (asks.length > 1) { const more = h('a', 'ovdac-more', `${asks.length - 1} more on your Actor's page`); more.href = openBase + '/you'; more.target = '_blank'; more.rel = 'noopener'; askBox.append(more); }++ } else askBox.replaceChildren();++ askBox.hidden = !ask;++ }++++ async function act(path, body, btn, done) {++ if (busy) return false;++ busy = true; if (btn) { btn.disabled = true; btn.classList.add('is-busy'); }++ say('');++ try {++ await api(path, { method: 'POST', json: true, body });++ last = '';++ await refresh();++ if (done) say(done, 'ok');++ return true;++ } catch (err) { say(`That did not go through: ${err.message}`, 'bad'); return false; }++ finally { busy = false; if (btn) { btn.disabled = false; btn.classList.remove('is-busy'); } }++ }++++ async function refresh() {++ if (gone) return;++ if (o.active && !o.active()) return;++ try { const data = await api(''); if (!gone) paint(data); }++ catch (err) { if (!gone && (err.status === 401 || err.status === 403 || err.status === 404)) paint(null); }++ }++ refresh();++ timer = setInterval(refresh, POLL_MS);++ return { refresh, destroy() { gone = true; clearInterval(timer); clearTimeout(noteTimer); container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state'); } };++ }++++ root.OpenVibeDockActor = { mount };++})(typeof window !== 'undefined' ? window : globalThis);@@@ -1,19 +1,22 @@// ═══════════════════════════════════════════════════════════════--// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0++// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0; Float 4.154.0//// The person's own agent desk stream at the top of the Dock's Agents tab. dock.js loads this file the first time that// tab opens:// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }//--// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for pause, Curtain now, mute and End, and--// the frame). Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the++// The read is OpenVibe.Codes' GET /api/v1/me/agent-view (the same POST for pause, Curtain now, mute and End, and the++// frame). It asks Network's same-origin relay (/api/frame/agent-view…) because every site's connect-src allows++// openvibe.network and refuses openvibe.codes; `frameBase` overrides the relay, and `base` reaches Codes itself in a++// test or the extension (an explicit base wins). Nothing is asked when there is no token: a signed-out page can never++// read the view. Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the// Dock is the owner's own view, so it may always resume.// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the// curtain state and the viewer count. The embed renews its own ticket while it plays.//// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.--// There is no step line yet: steps are Agent Streams S2, and this view shows none until Live serves them.++// Live's S2 current step comes through Codes when available; an older stream simply has no step line.// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.// ═══════════════════════════════════════════════════════════════(function (root) {@@@ -55,6 +58,7 @@sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',++ float: '<rect x="3.5" y="5" width="17" height="14" rx="2.5"/><rect x="11.5" y="11" width="7" height="6" rx="1.2" fill="currentColor" stroke="none"/>',};// The style goes into the document the view lives in: the page, or the Dock's own window when it is popped out.@@@ -69,6 +73,7 @@.ovda-stage { position: relative; overflow: hidden; aspect-ratio: 16 / 9; border-radius: 14px; background: #0a0c11; border: 1px solid var(--border, rgba(255,255,255,.1)); }.ovda-frame, .ovda-still { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; object-fit: contain; background: #0a0c11; }.ovda-still { object-fit: cover; opacity: .92; }++ .ovda-stage[hidden] { display: none; }.ovda-wait { position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-muted, #8b8b96); font-size: 12.5px; text-align: center; padding: 12px; }.ovda-line { display: flex; align-items: center; gap: 6px; min-height: 34px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }.ovda-line b { color: var(--text-primary, #ececf1); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }@@@ -87,9 +92,14 @@.ovda-acts button:disabled { opacity: .5; cursor: default; }.ovda-acts svg { width: 14px; height: 14px; }.ovda-acts button[data-act="end"] { color: color-mix(in srgb, var(--live-red, #e5484d) 65%, var(--text-primary, #ececf1)); }++ /* A session that ended is two lines of words with its one way out beside them, not a button on a row of its own. */++ .ovda.is-ended .ovda-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; align-items: center; padding: 2px 4px 6px; }++ .ovda.is-ended .ovda-line, .ovda.is-ended .ovda-meta { grid-column: 1; }++ .ovda.is-ended .ovda-acts { grid-column: 2; grid-row: 1 / span 2; margin: 0; }++ .ovda.is-ended .ovda-acts:empty { display: none; }++ @media (pointer: coarse) { .ovda-acts button, .ovda-acts a { min-height: 44px; min-width: 44px; justify-content: center; } }.ovda svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }@keyframes ovda-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }-- @media (prefers-reduced-motion: reduce) { .ovda-line .dot.live { animation: none; } }`;(d.head || d.documentElement).appendChild(st);}@@@ -105,30 +115,35 @@const o = { ...opts };const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };const codesBase = String(o.base || CODES).replace(/\/+$/, '');++ // Network's frame relay on a site; an explicit base reaches Codes itself (a test, or the extension's panel).++ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';++ const frameBase = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const endpoint = (path) => (direct ? direct + path : frameBase + path.replace(/^\/api\/v1\/me\/agent-view/, '/agent-view'));let gone = false, timer = 0, busy = false, agent = null, frameUrl = null, lastFrame = '';let readOnly = false, framedFor = '', frameRefused = '', frameTimer = 0;container.innerHTML = '';container.classList.add('ovda');const stage = h('div', 'ovda-stage');const line = h('div', 'ovda-line');++ const step = h('div', 'ovda-meta ovda-step');const meta = h('div', 'ovda-meta');const watch = h('a', 'ovda-watch', 'Watch on openvibe.live');watch.target = '_blank';watch.rel = 'noopener';watch.hidden = true;const acts = h('div', 'ovda-acts');-- const head = h('div');-- head.append(line, meta, watch, acts);++ const head = h('div', 'ovda-head');++ head.append(line, meta, step, watch, acts);const body = h('div');body.append(stage, head);container.append(body);async function api(path, init = {}) {-- const base = String(o.base || CODES).replace(/\/+$/, '');const t = typeof o.token === 'function' ? await o.token() : o.token;readOnly = readOnlyToken(t);-- const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };-- const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });++ if (!t) throw Object.assign(new Error('Sign in to see your agent'), { status: 401 });++ const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), authorization: `Bearer ${t}` };++ const r = await fetch(endpoint(path), { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });const b = await r.json().catch(() => ({}));if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });return b;@@@ -138,8 +153,10 @@function still(url) {if (!url || frameUrl === url) return;frameUrl = url;-- Promise.resolve(typeof o.token === 'function' ? o.token() : o.token).then((t) => fetch(url, { headers: t ? { authorization: `Bearer ${t}` } : {}, credentials: 'omit' }))-- .then((r) => (r.ok ? r.blob() : null))++ Promise.resolve(typeof o.token === 'function' ? o.token() : o.token).then((t) => (t++ ? fetch(endpoint('/api/v1/me/agent-view/frame'), { headers: { authorization: `Bearer ${t}`, accept: 'image/jpeg, image/webp' }, credentials: 'omit' })++ : null))++ .then((r) => (r && r.ok ? r.blob() : null)).then((blob) => {if (!blob || gone || frameUrl !== url) return;const next = URL.createObjectURL(blob);@@@ -164,6 +181,7 @@const live = agent.state === 'live';const ended = agent.state === 'ended';++ container.classList.toggle('is-ended', ended);// The picture: the live embed when there is one and it may be framed, else the still and a plain link.// A refused frame looks loaded to its parent (the browser shows its own error page), so the embed// announces itself (Live's agent-embed.js); no hello in time means frame-ancestors kept it out, and the@@@ -192,14 +210,17 @@if (agent.last_frame_url) {if (!stage.querySelector('img.ovda-still')) frameUrl = null;still(agent.last_frame_url);-- } else if (!stage.querySelector('img.ovda-still') && !stage.querySelector('.ovda-wait')) {-- stage.append(h('div', 'ovda-wait', ended ? 'Nothing to show' : 'Waiting for the desk'));++ } else if (!ended && !stage.querySelector('img.ovda-still') && !stage.querySelector('.ovda-wait')) {++ stage.append(h('div', 'ovda-wait', 'Waiting for the desk'));}// The plain way to the stream, when this browser cannot frame it. It is a real link, not a control.const page = typeof agent.page_url === 'string' && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;if (page) watch.href = page;watch.hidden = !blocked || !page;}++ // A session that ended and left no picture has no stage: a black box saying "Nothing to show" told nobody++ // anything (the owner, 2026-11-10). The line under it still says what it was and how long it worked.++ stage.hidden = ended && !embed && !agent.last_frame_url && !stage.querySelector('img');// The line: what it is doing, one row.line.innerHTML = '';@@@ -211,6 +232,11 @@else title.textContent = text;line.append(title);++ const current = agent.current_step;++ step.textContent = current && typeof current.title === 'string' && current.title.trim()++ ? `Step: ${current.title} · ${current.status || 'started'}` : '';++ step.hidden = !step.textContent;++// The meta: the state, the viewers, the curtain, the worked time.meta.textContent = '';const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];@@@ -246,6 +272,16 @@if (live && !(agent.curtain && agent.curtain.up)) acts.append(control('curtain', `${svg(G.eye)}Curtain now`, 'Hide the screen now; your desk lifts it when it is clear'));if (!ended) acts.append(control('mute', `${svg(agent.voice_muted ? G.sound : G.mute)}${agent.voice_muted ? 'Unmute' : 'Mute'}`, agent.voice_muted ? 'Let the agent speak again' : 'Mute the agent\'s voice'));if (!ended) acts.append(control('end', `${svg(G.stop)}End`, 'End the agent stream'));++ // 4.154.0: Float puts the agent's screen in a floating card (overlay.js) that stays on screen as the person++ // moves around this site and on to other OpenVibe sites, so the Dock can close while they watch.++ if (embed && !ended) {++ const b = h('button', null);++ b.type = 'button';++ b.dataset.act = 'float';++ b.innerHTML = `${svg(G.float)}Float`;++ b.title = 'Keep watching in a small window that follows you';++ acts.append(b);++ }if (agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url)) {const a = h('a');a.href = agent.page_url;@@@ -325,8 +361,19 @@else if (b.dataset.act === 'curtain') control('curtain', b);else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });else if (b.dataset.act === 'end') control('end', b);++ else if (b.dataset.act === 'float') floatIt(b);});++ function floatIt(btn) {++ const embed = agent && typeof agent.embed_url === 'string' && EMBED.test(agent.embed_url) ? agent.embed_url : null;++ const nav = root.OpenVibeNavbar;++ if (!embed || !nav || typeof nav.overlay !== 'function') { btn.title = 'Floating is not available on this page.'; return; }++ const pageUrl = agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;++ nav.overlay({ id: 'agent', src: embed, title: (agent.task && agent.task.title) || agent.title || 'Your agent', live: agent.state === 'live', href: pageUrl || undefined })++ .then((card) => { if (card && root.OpenVibeDock && typeof root.OpenVibeDock.close === 'function') root.OpenVibeDock.close(); })++ .catch(() => { btn.title = 'That did not work. Try again.'; });++ }++refresh();return { refresh, destroy() { gone = true; clearTimeout(timer); clearTimeout(frameTimer); root.removeEventListener('message', onFrameHello); if (lastFrame) URL.revokeObjectURL(lastFrame); container.innerHTML = ''; container.classList.remove('ovda'); } };}nnew file mode 100644@@@ -0,0 +1,71 @@++/**++ * openvibe-shared/dock-card.js: a known OpenVibe link's card under a message in the Dock (4.147.0).++ *++ * OpenVibe.Chat resolves the card when a message is sent (its server/chat/link-cards.js) and the DM API carries it on++ * the message as `card`: { kind, url, title, line, note?, image, action: { label, href } | null }, for a video on++ * OpenVibe.Video ("Watch together"), a party invite ("Join the party"), a shared voice or a skill on OpenVibe.Actor.++ * dock.js loads this file the first time an open conversation shows a message with a card, so the Dock that every++ * signed-in page carries stays small.++ *++ * OpenVibeDockCard(card, h) → the card's node, or null when it is not a card this can draw++ *++ * Every field is data: set as text, cut to size, a link only when it is https, a picture only from openvibe.media.++ * The card is compact (a 72 px picture beside the words) because the Dock is narrow; the message stays the thing said.++ */++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockCard) return;++++ const CSS = `++.ovdk-card { display: flex; align-items: center; gap: 10px; box-sizing: border-box; max-width: min(100%, 300px); margin-top: 4px; padding: 8px 10px 8px 8px; border-radius: 14px; border: 1px solid var(--border, rgba(255,255,255,.1)); background: var(--bg-primary, #12141b); text-align: left; }++.ovdk-card-pic { flex: none; width: 72px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); }++.ovdk-card-body { min-width: 0; display: grid; justify-items: start; gap: 2px; }++.ovdk-card-t { font-size: 13.5px; font-weight: 700; line-height: 1.3; color: var(--text-primary, #ececf1); text-decoration: none; overflow-wrap: anywhere; }++a.ovdk-card-t:hover { text-decoration: underline; }++.ovdk-card-l { font-size: 12.5px; line-height: 1.35; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }++.ovdk-card-go { display: inline-flex; align-items: center; min-height: 32px; margin-top: 4px; padding: 0 12px; border-radius: 999px; font-size: 13px; font-weight: 700; text-decoration: none; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }++.ovdk-card-go:hover { filter: brightness(1.08); }++@media (pointer: coarse) { .ovdk-card-go { min-height: 44px; padding: 0 16px; } }++`;++++ const https = (u) => (typeof u === 'string' && /^https:\/\/[^\s"'<>]+$/.test(u) ? u : null);++ const cut = (v, n) => Array.from(String(v)).slice(0, n).join('');++++ function styles() {++ if (document.getElementById('ovdk-card-styles')) return;++ const st = document.createElement('style');++ st.id = 'ovdk-card-styles';++ st.textContent = CSS;++ document.head.appendChild(st);++ }++++ function card(c, h) {++ if (!c || typeof c !== 'object' || typeof c.title !== 'string' || !c.title || typeof h !== 'function') return null;++ styles();++ const url = https(c.url);++ const box = h('div', 'ovdk-card');++ if (c.kind) box.dataset.kind = cut(c.kind, 20);++ const pic = typeof c.image === 'string' && /^https:\/\/openvibe\.media\/[^\s"'<>]+$/.test(c.image) ? c.image : null;++ if (pic) {++ const img = h('img', 'ovdk-card-pic');++ img.src = pic; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';++ box.append(img);++ }++ const body = h('div', 'ovdk-card-body');++ const title = h(url ? 'a' : 'b', 'ovdk-card-t', cut(c.title, 120));++ if (url) { title.href = url; title.target = '_blank'; title.rel = 'noopener nofollow'; }++ body.append(title);++ if (typeof c.line === 'string' && c.line) body.append(h('span', 'ovdk-card-l', cut(c.line, 200)));++ if (typeof c.note === 'string' && c.note) body.append(h('span', 'ovdk-card-l', cut(c.note, 160)));++ const go = c.action && typeof c.action === 'object' && https(c.action.href) && typeof c.action.label === 'string' && c.action.label ? c.action : null;++ if (go) {++ const a = h('a', 'ovdk-card-go', cut(go.label, 40));++ a.href = https(go.href); a.target = '_blank'; a.rel = 'noopener nofollow';++ body.append(a);++ }++ box.append(body);++ return box;++ }++++ root.OpenVibeDockCard = card;++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,41 @@++/* Edit events for an open Dock DM. Loaded when the conversation opens. */++(function (root) {++ 'use strict';++ root.OpenVibeDockChatEdits = function ({ id, list, messages, error, tokenOf, chatCall, chatBase, active, load, h }) {++ let socket = null, timer = null, stopped = false;++ function apply(m) {++ const old = messages.find((x) => Number(x.id) === Number(m && m.id));++ const li = old && list.querySelector(`[data-id="${Number(m.id)}"]`);++ if (!li || !m.edited_at || (old.edited_at && old.edited_at >= m.edited_at)) return;++ Object.assign(old, { message: m.message, card: m.card, edited_at: m.edited_at });++ li.querySelector('.ovdk-text').textContent = String(m.message || '');++ let mark = li.querySelector('.ovdk-edited');++ if (!mark) { mark = h('small', 'ovdk-edited'); li.append(mark); }++ mark.textContent = `edited · ${String(m.edited_at).slice(11, 16)}`;++ const card = li.querySelector('.ovdk-card'); if (card) card.remove();++ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { if (ok && active() && li.isConnected) { const c = root.OpenVibeDockCard(m.card, h); if (c) li.append(c); } });++ }++ async function connect() {++ if (stopped || !active() || !root.WebSocket) return;++ const token = await tokenOf();++ if (!token || stopped || !active()) return;++ const ws = new root.WebSocket(chatBase().replace(/^http/, 'ws') + '/ws/chat');++ socket = ws;++ ws.onopen = () => ws.send(JSON.stringify({ type: 'join', token }));++ ws.onmessage = (event) => { let frame; try { frame = JSON.parse(event.data); } catch { return; } if (frame.type === 'dm_edited' && String(frame.conversation_id) === id) apply(frame.message); };++ ws.onclose = () => { if (socket === ws) socket = null; if (!stopped && active()) timer = setTimeout(connect, 3000); };++ }++ list.addEventListener('click', async (event) => {++ if (!event.target.classList.contains('ovdk-edit')) return;++ const li = event.target.closest('[data-id]');++ const old = li && messages.find((m) => Number(m.id) === Number(li.dataset.id));++ if (!old) return;++ const body = root.prompt('Edit message', old.message);++ if (body == null || body.trim() === old.message.trim()) return;++ try { const result = await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages/${encodeURIComponent(old.id)}`, { method: 'PATCH', body: JSON.stringify({ message: body }) }); apply(result.message); }++ catch (e) { error.textContent = e.message || 'Could not edit'; error.hidden = false; }++ });++ connect().catch(() => {});++ return { stop() { stopped = true; clearTimeout(timer); if (socket) { socket.onclose = null; socket.close(); socket = null; } } };++ };++})(typeof window === 'undefined' ? globalThis : window);nnew file mode 100644@@@ -0,0 +1,303 @@++// ═══════════════════════════════════════════════════════════════++// OpenVibe — the Code part (openvibe-shared 4.201.0). Ask 350, the owner: openvibe.codes "should be better interconnected++// and shared … so that you can do chat, codes, and agents all in the same place and in one area". The menu's Vibe place++// is Chat · Code · Agents, and the Dock has the same Code tab. dock.js loads this file the first time it opens:++// OpenVibeDockCode.mount(container, { token, frameBase, base, codes, active, take }) → { refresh(), focus(), destroy() }++// take() → the words another part handed over (Vibe's "Build it in Code"), which go in the box.++//++// One box to start a coding task, the way openvibe.codes starts one (Create: a new website, game or app; OpenVibe:++// a change to OpenVibe itself; My code: a repository of yours), and your latest tasks under it, running ones first,++// each with its state, its repository, how long ago, and Stop while it runs. The reads go through Network's same-origin++// relay (/api/frame/codes-tasks…), because every site's connect-src allows openvibe.network and refuses openvibe.codes;++// `base` reaches Codes itself in a test. While a task runs the list is read again every 8 seconds, only while the part++// is on screen. When Codes cannot be reached, the box still works: it opens openvibe.codes with what you wrote in it.++// Everything is set as text, never markup; a task's address is used only when it is on openvibe.codes.++// ═══════════════════════════════════════════════════════════════++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockCode) return;++++ const CODES = 'https://openvibe.codes';++ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';++ const POLL_MS = 8000;++ const MODES = [['create', 'Create', 'A new website, game or app'], ['openvibe', 'OpenVibe', 'A change to OpenVibe itself'], ['repo', 'My code', 'A repository of yours']];++ const STATE = { queued: 'Waiting', running: 'Running', 'needs-you': 'Needs you', done: 'Done', failed: 'Failed', stopped: 'Stopped' };++ const PLACEHOLDER = { create: 'A goal bar for my sub goal', openvibe: 'Make the menu remember my last tab', repo: 'Fix the login redirect and add a test' };++ const svg = (d) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;++ const G = {++ send: 'M5 12h13M13 6l6 6-6 6',++ stop: 'M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18zM9.5 9.5h5v5h-5z',++ open: 'M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10',++ code: 'M8 7l-5 5 5 5M16 7l5 5-5 5',++ };++++ function styles(d) {++ if (d.getElementById('ovdc-styles')) return;++ const st = d.createElement('style');++ st.id = 'ovdc-styles';++ st.textContent = `++ .ovdc { display: flex; flex-direction: column; gap: 12px; padding: 10px 10px 14px; }++ .ovdc-box { display: grid; gap: 8px; padding: 10px 10px 8px 12px; border-radius: 18px; border: 1px solid var(--border, rgba(255,255,255,.1)); background: var(--bg-card, rgba(255,255,255,.03)); }++ .ovdc-box:focus-within { border-color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--border, rgba(255,255,255,.1))); }++ .ovdc-text { display: block; width: 100%; box-sizing: border-box; min-height: 44px; max-height: 30vh; padding: 2px 0; border: 0; outline: 0; resize: none; background: none; color: var(--text-primary, #ececf1); font: inherit; font-size: 15.5px; line-height: 1.45; }++ .ovdc-repo { width: 100%; box-sizing: border-box; height: 36px; padding: 0 10px; border-radius: 10px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-input, rgba(0,0,0,.2)); color: inherit; font: inherit; font-size: 14px; }++ .ovdc-repo[hidden] { display: none; }++ .ovdc-row { display: flex; align-items: center; gap: 6px; }++ .ovdc-modes { flex: 1; min-width: 0; display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none; }++ .ovdc-mode { flex: none; height: 32px; padding: 0 11px; border-radius: 999px; border: 1px solid transparent; background: none; color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }++ .ovdc-mode:hover { color: var(--text-primary, #ececf1); background: var(--bg-hover, rgba(255,255,255,.06)); }++ .ovdc-mode[aria-checked="true"] { color: var(--accent-light, #93c5fd); border-color: color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }++ .ovdc-go { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 13px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); cursor: pointer; }++ .ovdc-go:disabled { opacity: .45; cursor: default; }++ .ovdc-note { margin: 0 2px; font-size: 12.5px; color: var(--text-muted, #8b8b96); }++ .ovdc-note:empty { display: none; }++ .ovdc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 4px; }++ .ovdc-head b { font-size: 13px; letter-spacing: .02em; color: var(--text-secondary, #a8b3c4); }++ .ovdc-all { display: inline-flex; align-items: center; gap: 5px; min-height: 32px; font-size: 13px; font-weight: 600; color: var(--accent-light, #93c5fd); text-decoration: none; }++ .ovdc-all svg { width: 15px; height: 15px; }++ .ovdc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }++ .ovdc-task { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; border-radius: 12px; }++ .ovdc-task:hover { background: var(--bg-hover, rgba(255,255,255,.05)); }++ .ovdc-a { min-width: 0; display: grid; gap: 3px; padding: 9px 4px 9px 10px; color: inherit; text-decoration: none; }++ .ovdc-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14.5px; font-weight: 600; color: var(--text-primary, #ececf1); }++ .ovdc-m { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-muted, #8b8b96); white-space: nowrap; overflow: hidden; }++ .ovdc-s { flex: none; display: inline-flex; align-items: center; gap: 5px; font-weight: 650; }++ .ovdc-s::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }++ .ovdc-s[data-s="running"], .ovdc-s[data-s="queued"] { color: var(--accent-light, #93c5fd); }++ .ovdc-s[data-s="running"]::before { animation: ovdc-beat 1.2s ease-in-out infinite; }++ .ovdc-s[data-s="needs-you"] { color: #fbbf24; }++ .ovdc-s[data-s="done"] { color: #4ade80; }++ .ovdc-s[data-s="failed"] { color: var(--live-red, #f87171); }++ .ovdc-s[data-s="stopped"] { color: var(--text-muted, #8b8b96); }++ .ovdc-stop { display: grid; place-items: center; width: 36px; height: 36px; margin-right: 4px; border: 0; border-radius: 10px; background: none; color: var(--text-secondary, #a8b3c4); cursor: pointer; }++ .ovdc-stop:hover, .ovdc-stop:focus-visible { background: color-mix(in srgb, var(--live-red, #ef4444) 14%, transparent); color: var(--live-red, #f87171); }++ .ovdc-stop svg { width: 19px; height: 19px; }++ .ovdc-none { padding: 18px 12px; text-align: center; color: var(--text-muted, #8b8b96); font-size: 13.5px; line-height: 1.45; }++ .ovdc-none a { color: var(--accent-light, #93c5fd); }++ @keyframes ovdc-beat { 50% { opacity: .35; } }++ html[data-ov-motion=off] .ovdc-s::before { animation: none; }`;++ d.head.appendChild(st);++ }++++ const ago = (t) => {++ const s = Math.max(0, (Date.now() - new Date(t).getTime()) / 1000);++ if (!isFinite(s)) return '';++ return s < 60 ? 'just now' : s < 3600 ? `${Math.floor(s / 60)} min ago` : s < 86400 ? `${Math.floor(s / 3600)} h ago` : `${Math.floor(s / 86400)} d ago`;++ };++ const onCodes = (u) => { try { const x = new URL(u); return x.protocol === 'https:' && (x.hostname === 'openvibe.codes' || x.hostname.endsWith('.openvibe.codes')) ? x.href : null; } catch { return null; } };++ const repoName = (r) => String(r || '').replace(/^https?:\/\/[^/]+\//, '').replace(/\.git$/, '');++++ function mount(container, opts = {}) {++ const d = container.ownerDocument || document;++ styles(d);++ const o = { ...opts };++ const tok = () => (typeof o.token === 'function' ? o.token() : o.token) || null;++ const codes = String(o.codes || CODES).replace(/\/+$/, '');++ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';++ const relay = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const url = (what, id) => (direct++ ? direct + ({ list: '/api/v1/me/tasks?limit=8', start: '/api/v1/tasks', stop: `/api/v1/tasks/${encodeURIComponent(id)}/stop` })[what]++ : relay + ({ list: '/codes-tasks?limit=8', start: '/codes-tasks', stop: `/codes-tasks/${encodeURIComponent(id)}/stop` })[what]);++ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };++ let gone = false, timer = 0, mode = 'create', tasks = null, reachable = true;++++ function loadScript(file, ready) {++ if (ready()) return Promise.resolve(true);++ return new Promise((resolve) => {++ const id = `ovdc-${file}`;++ let script = d.getElementById(id);++ if (!script) { script = d.createElement('script'); script.id = id; script.src = SCRIPT_BASE + file; script.async = true; d.head.appendChild(script); }++ script.addEventListener('load', () => resolve(!!ready()), { once: true });++ script.addEventListener('error', () => resolve(false), { once: true });++ });++ }++ function runUrl(id) {++ return (direct ? direct + '/api/v1/runs/' : relay + '/codes-runs/') + encodeURIComponent(id);++ }++ async function openRun(task, from) {++ const href = onCodes(task.url);++ if (!href || !tok()) return;++ const loaded = await Promise.all([++ loadScript('ov-window.js', () => !!root.OVWindow),++ loadScript('agent-session.js', () => !!root.OpenVibeAgentSession)++ ]);++ if (loaded.some((ok) => !ok)) { d.defaultView.location.assign(href); return; }++ const body = h('div');++ let stopped = false, controller = null, view = null;++ const win = root.OVWindow.open({ id: `codes-run-${task.id}`, title: task.title || 'Coding task', body, kind: 'dialog', size: 'md', from,++ onClose: () => { stopped = true; if (controller) controller.abort(); if (view) view.destroy(); } });++ body.appendChild(h('p', 'ovdc-none', 'Loading the run…'));++ const headers = () => ({ Authorization: `Bearer ${tok()}`, Accept: 'application/json' });++ try {++ const res = await fetch(runUrl(task.id), { headers: headers(), credentials: 'omit' });++ if (!res.ok) throw new Error('The run could not be read.');++ const run = await res.json();++ if (stopped) return;++ body.replaceChildren();++ view = root.OpenVibeAgentSession.mount(body, { title: run.title || task.title || 'Coding task',++ status: run.status || task.status || 'running', mode: 'card', href });++ let last = 0;++ while (!stopped) {++ controller = new AbortController();++ const events = await fetch(runUrl(task.id) + '/events', { credentials: 'omit',++ headers: { ...headers(), Accept: 'text/event-stream', 'Last-Event-ID': String(last) }, signal: controller.signal });++ if (!events.ok || !events.body) break;++ const reader = events.body.getReader();++ const decoder = new TextDecoder();++ let buffer = '', terminal = false;++ while (!stopped) {++ const part = await reader.read();++ if (part.done) break;++ buffer = (buffer + decoder.decode(part.value, { stream: true })).replace(/\r\n/g, '\n');++ let cut;++ while ((cut = buffer.indexOf('\n\n')) !== -1) {++ const frame = buffer.slice(0, cut); buffer = buffer.slice(cut + 2);++ const id = /^id: *(\d+)$/m.exec(frame), data = frame.split('\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');++ if (id) last = Number(id[1]);++ if (!data) continue;++ try { const event = JSON.parse(data); view.push(event); if (event.type === 'RUN_FINISHED' || event.type === 'RUN_ERROR') terminal = true; } catch { /* skip one malformed event */ }++ }++ }++ if (terminal || !/running|queued|placed/.test(run.status || '')) break;++ await new Promise((resolve) => setTimeout(resolve, 1500));++ }++ } catch (err) {++ if (!stopped) { body.replaceChildren(); const a = h('a', 'ovk-btn ovk-btn--quiet', 'Open on Codes'); a.href = href; body.appendChild(a); }++ }++ return win;++ }++++ container.innerHTML = '';++ container.classList.add('ovdc');++ const box = h('form', 'ovdc-box');++ box.setAttribute('aria-label', 'Start a coding task');++ const text = h('textarea', 'ovdc-text');++ text.rows = 2; text.maxLength = 16000; text.placeholder = PLACEHOLDER.create;++ text.setAttribute('aria-label', 'What should we make?');++ const repo = h('input', 'ovdc-repo');++ repo.placeholder = 'Repository: owner/name or its address'; repo.setAttribute('aria-label', 'Repository'); repo.hidden = true; repo.maxLength = 300;++ const row = h('div', 'ovdc-row');++ const modes = h('div', 'ovdc-modes');++ modes.setAttribute('role', 'radiogroup'); modes.setAttribute('aria-label', 'Where it works');++ for (const [id, label, hint] of MODES) {++ const b = h('button', 'ovdc-mode', label);++ b.type = 'button'; b.dataset.mode = id; b.title = hint;++ b.setAttribute('role', 'radio'); b.setAttribute('aria-checked', String(id === mode));++ modes.appendChild(b);++ }++ const go = h('button', 'ovdc-go'); go.type = 'submit'; go.innerHTML = svg(G.send); go.setAttribute('aria-label', 'Start'); go.disabled = true;++ row.append(modes, go);++ const note = h('p', 'ovdc-note'); note.setAttribute('role', 'status');++ box.append(text, repo, row, note);++ const head = h('div', 'ovdc-head');++ const all = h('a', 'ovdc-all'); all.href = `${codes}/`; all.innerHTML = `<span>Open Codes</span>${svg(G.open)}`;++ head.append(h('b', null, 'Your tasks'), all);++ const list = h('ul', 'ovdc-list');++ container.append(box, head, list);++++ const ready = () => { go.disabled = !text.value.trim() || (mode === 'repo' && !repo.value.trim()); };++ text.addEventListener('input', () => { text.style.height = 'auto'; text.style.height = Math.min(text.scrollHeight, d.defaultView.innerHeight * 0.3) + 'px'; ready(); });++ repo.addEventListener('input', ready);++ text.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !go.disabled) { e.preventDefault(); box.requestSubmit ? box.requestSubmit() : start(); } });++ modes.addEventListener('click', (e) => {++ const b = e.target.closest && e.target.closest('[data-mode]'); if (!b) return;++ mode = b.dataset.mode;++ modes.querySelectorAll('[data-mode]').forEach((x) => x.setAttribute('aria-checked', String(x === b)));++ text.placeholder = PLACEHOLDER[mode];++ repo.hidden = mode !== 'repo';++ ready();++ (mode === 'repo' && !repo.value ? repo : text).focus();++ });++++ const fallback = () => `${codes}/?${new URLSearchParams({ prompt: text.value.trim(), mode, ...(mode === 'repo' ? { repo: repo.value.trim() } : {}) })}`;++ async function call(what, id, body) {++ const t = tok(); if (!t) throw new Error('signed out');++ const r = await fetch(url(what, id), { method: body || what === 'stop' ? 'POST' : 'GET', headers: { authorization: `Bearer ${t}`, accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined, credentials: 'omit' });++ if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.status = r.status; try { e.body = await r.json(); } catch { /* */ } throw e; }++ return r.json().catch(() => ({}));++ }++ async function start() {++ if (go.disabled) return;++ if (!reachable) { d.defaultView.location.assign(fallback()); return; }++ go.disabled = true; note.textContent = 'Starting…';++ try {++ const r = await call('start', null, { text: text.value.trim(), mode, ...(mode === 'repo' ? { repo: repo.value.trim() } : {}) });++ text.value = ''; text.dispatchEvent(new Event('input'));++ note.textContent = '';++ const there = onCodes(r && r.url);++ if (there) { const a = h('a', null, 'Follow it on openvibe.codes'); a.href = there; note.textContent = 'Started. '; note.appendChild(a); }++ read();++ } catch (e) {++ ready();++ if (e.status === 404 || e.status === 502 || e.status === 503 || e.message === 'Failed to fetch') { d.defaultView.location.assign(fallback()); return; }++ note.textContent = (e.body && (e.body.message || e.body.error)) || 'It did not start. Try again in a moment.';++ }++ }++ box.addEventListener('submit', (e) => { e.preventDefault(); start(); });++++ function paint() {++ list.replaceChildren();++ if (!tok()) { list.appendChild(h('li', 'ovdc-none', 'Sign in to start coding tasks and follow them here.')); return; }++ if (!reachable) {++ const li = h('li', 'ovdc-none');++ li.append('Your tasks are on ');++ const a = h('a', null, 'openvibe.codes'); a.href = `${codes}/`; li.append(a, '. Starting one here opens it there.');++ list.appendChild(li); return;++ }++ if (!tasks) return;++ if (!tasks.length) { list.appendChild(h('li', 'ovdc-none', 'Nothing yet. Say what to make, and an agent builds it while you watch.')); return; }++ for (const t of tasks) {++ const li = h('li', 'ovdc-task');++ const there = onCodes(t.url);++ const a = h(there ? 'a' : 'div', 'ovdc-a');++ if (there) { a.href = there; a.addEventListener('click', (event) => {++ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;++ event.preventDefault(); openRun(t, a);++ }); }++ a.appendChild(h('span', 'ovdc-t', t.title || 'A coding task'));++ const m = h('span', 'ovdc-m');++ const s = h('span', 'ovdc-s', STATE[t.status] || t.status || ''); s.dataset.s = t.status || '';++ m.appendChild(s);++ const bits = [t.mode === 'repo' && t.repo ? repoName(t.repo) : t.mode === 'openvibe' ? 'OpenVibe' : null, t.steps ? `${t.steps} steps` : null, ago(t.updated_at || t.started_at)].filter(Boolean);++ if (bits.length) m.appendChild(h('span', null, bits.join(' · ')));++ a.appendChild(m);++ li.appendChild(a);++ if (t.status === 'running' || t.status === 'queued' || t.status === 'needs-you') {++ const x = h('button', 'ovdc-stop'); x.type = 'button'; x.innerHTML = svg(G.stop); x.setAttribute('aria-label', `Stop: ${t.title || 'this task'}`); x.title = 'Stop';++ x.addEventListener('click', async () => { x.disabled = true; try { await call('stop', t.id); } catch { /* the next read says */ } read(); });++ li.appendChild(x);++ }++ list.appendChild(li);++ }++ }++ async function read() {++ clearTimeout(timer);++ if (gone || !tok()) { paint(); return; }++ try {++ const j = await call('list');++ reachable = true;++ tasks = (Array.isArray(j) ? j : j.tasks || []).slice(0, 8);++ tasks.sort((a, b) => (/running|queued|needs-you/.test(b.status) ? 1 : 0) - (/running|queued|needs-you/.test(a.status) ? 1 : 0));++ } catch (e) {++ if (e.status === 401 || e.status === 403) tasks = [];++ else reachable = false;++ }++ if (gone) return;++ paint();++ if (reachable && tasks && tasks.some((t) => /running|queued/.test(t.status)) && (!o.active || o.active())) timer = setTimeout(read, POLL_MS);++ }++ // What another part handed over (Vibe's Build it in Code) goes in the box, ready to start.++ const take = () => { const t = typeof o.take === 'function' ? o.take() : ''; if (!t) return; text.value = t; text.dispatchEvent(new Event('input')); text.focus(); note.textContent = 'From your chat with Vibe. Start it, or change it first.'; };++ paint();++ read();++ take();++ return {++ refresh: () => { take(); read(); },++ focus: () => text.focus(),++ destroy() { gone = true; clearTimeout(timer); container.replaceChildren(); container.classList.remove('ovdc'); },++ };++ }++++ root.OpenVibeDockCode = { mount };++ if (typeof module === 'object' && module.exports) module.exports = { mount };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,27 @@++// The Dock body inside a host sheet. Loaded only when mountInto is first used.++(function (root) {++ 'use strict';++ function mount(host, first, bridge) {++ if (!document.getElementById('ovdk-mount-styles')) {++ const style = document.createElement('style');++ style.id = 'ovdk-mount-styles';++ style.textContent = `++ .ovdk.ovdk-inline, .ovdk.ovdk-inline[data-corner] { position: relative !important; inset: auto !important; z-index: auto !important; width: 100% !important; height: 100% !important; max-width: 100% !important; max-height: none !important; min-height: 0; padding: 0 !important; border: 0; border-radius: 0; box-shadow: none !important; transform: none !important; animation: none !important; }++ .ovdk-inline .ovdk-body { width: 100%; min-width: 0; border: 0; overflow: hidden; }++ .ovdk-inline .ovdk-panel { min-width: 0; overflow-x: hidden; }++ @media (max-width: 640px) { .ovdk-inline button, .ovdk-inline a, .ovdk-inline summary, .ovdk-inline input, .ovdk-inline textarea { min-height: 44px !important; max-height: none !important; box-sizing: border-box; } }++ `;++ document.head.append(style);++ }++ const wrap = document.createElement('section');++ wrap.className = 'ovdk ovdk-inline';++ wrap.setAttribute('aria-label', 'Dock panel');++ host.replaceChildren(wrap);++ bridge.start(wrap);++ let gone = false;++ const show = (tab) => { if (!gone) bridge.show(tab); };++ show(first);++ return { show, destroy() { if (gone) return; gone = true; bridge.end(); host.replaceChildren(); } };++ }++ root.OpenVibeDockMount = { mount };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,135 @@++/** The Dock's content-aware run list. All content comes from Network's action feed and Actor's skill catalogue. */++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockRuns) return;++ const CSS = `++.ovdk-overview{display:grid;gap:8px;padding:4px 10px 12px}.ovdk-tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}++.ovdk-tile{min-width:0;display:grid;align-content:start;gap:2px;padding:10px 11px;border-radius:14px;background:color-mix(in srgb,var(--text-primary,#ececf1) 5%,transparent)}++.ovdk-tile small{font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted,#8b8b96)}++.ovdk-tile b{font-size:22px;font-weight:800;line-height:1.1;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.ovdk-tile b em{font-style:normal;font-size:12px;font-weight:650;color:var(--text-secondary,#a8a8b2);letter-spacing:0;margin-left:4px}++.ovdk-tile span{white-space:pre-wrap;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--text-secondary,#a8a8b2);line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}++.ovdk-tile .ovdk-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px 1px 0;background:var(--ok,#3fb27f)}.ovdk-tile .ovdk-dot.is-bad{background:var(--live-red,#e5484d)}++.ovdk-tile-next b{font-size:15px;line-height:1.25;letter-spacing:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ovdk-tile-next span{color:var(--accent-light,var(--accent,#60a5fa));font-weight:700}++.ovdk-overview h3{font-size:11px;font-weight:750;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted,#8b8b96);margin:8px 2px 0}.ovdk-skills{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}++.ovdk-skills button{min-height:44px;padding:9px 11px;border-radius:12px;border:0;background:color-mix(in srgb,var(--accent,#3b82f6) 10%,transparent);color:inherit;text-align:left;cursor:pointer;font:inherit;font-size:12.5px;font-weight:650}++.ovdk-skills button:hover{background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)}.ovdk-overview .ovdk-runs{padding:0}.ovdk-run-filters{display:flex;gap:4px;overflow-x:auto;padding:8px 10px;scrollbar-width:none}.ovdk-run-filters::-webkit-scrollbar{display:none}++.ovdk-run-filters button{min-height:36px;padding:0 11px;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:99px;background:transparent;color:inherit;cursor:pointer;white-space:nowrap;font:inherit;font-size:12px}++.ovdk-run-filters button[aria-pressed=true]{background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}++.ovdk-runs{list-style:none;margin:0;padding:0 6px 10px;display:grid;gap:2px}.ovdk-run{display:grid;grid-template-columns:36px minmax(0,1fr) auto;gap:11px;padding:10px 8px;border-radius:14px;min-width:0}++.ovdk-run-more{position:relative;align-self:start}.ovdk-run-more summary{list-style:none;width:36px;height:36px;display:grid;place-items:center;border-radius:50%;cursor:pointer;color:var(--text-muted,#8b8b96)}++.ovdk-run-more summary::-webkit-details-marker{display:none}.ovdk-run-more summary svg{width:18px;height:18px;fill:currentColor}++@media (hover:hover){.ovdk-run-more summary:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 8%,transparent);color:var(--text-primary,#ececf1)}}++.ovdk-run-more menu{position:absolute;right:0;top:38px;z-index:3;min-width:210px;padding:4px;margin:0;list-style:none;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:12px;background:var(--bg-primary,#19191f);box-shadow:0 10px 28px rgba(0,0,0,.28)}++.ovdk-run-more menu a,.ovdk-run-more menu button{display:flex;align-items:center;width:100%;min-height:40px;padding:0 12px;border:0;border-radius:9px;background:transparent;color:inherit;font:inherit;font-size:13px;text-decoration:none;cursor:pointer;text-align:left}++.ovdk-run-more menu a:hover,.ovdk-run-more menu button:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}++@media(max-width:640px){.ovdk-run-more summary{width:44px;height:44px}.ovdk-run-more menu{top:46px}}++@media (hover:hover){.ovdk-run:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 4%,transparent)}}++.ovdk-run-st{font-weight:750;margin-right:4px}.ovdk-run[data-state=running] .ovdk-run-st{color:var(--accent-light,var(--accent,#60a5fa))}.ovdk-run[data-state=waiting] .ovdk-run-st{color:#f0a73a}++.ovdk-run[data-state=scheduled] .ovdk-run-st{color:var(--text-primary,#ececf1)}.ovdk-run[data-state=done] .ovdk-run-st{color:var(--ok,#3fb27f)}.ovdk-run[data-state=failed] .ovdk-run-st{color:var(--live-red,#e5484d)}++.ovdk-run-ic{width:34px;height:34px;display:grid;place-items:center;border-radius:10px;background:color-mix(in srgb,var(--run-color,var(--accent,#3b82f6)) 15%,transparent);color:var(--run-color,var(--accent,#3b82f6))}++.ovdk-run-sub{min-width:0;display:grid;gap:3px}.ovdk-run-sub>b,.ovdk-run-title b{display:block;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}++.ovdk-run-title{color:inherit;text-decoration:none;min-width:0}.ovdk-run-title:hover b{text-decoration:underline;text-underline-offset:2px}.ovdk-run-line{font-size:12.5px;color:var(--text-secondary,#a8a8b2);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}++.ovdk-run-model{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--text-secondary,#a8a8b2)}.ovdk-run-maker{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:6px;background:color-mix(in srgb,var(--text-primary,#ececf1) 8%,transparent)}.ovdk-run-maker svg{width:14px;height:14px;fill:currentColor}++.ovdk-run-bar{height:3px;margin:2px 0;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent);border-radius:9px;overflow:hidden}.ovdk-run-bar:after{content:'';display:block;width:40%;height:100%;border-radius:9px;background:var(--accent,#3b82f6);animation:ovdk-run-go 1.6s cubic-bezier(.45,0,.2,1) infinite}++@keyframes ovdk-run-go{from{transform:translateX(-100%)}to{transform:translateX(250%)}}html[data-ov-motion=off] .ovdk-run-bar:after{width:45%}++.ovdk-run-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:3px}.ovdk-run-actions a,.ovdk-run-actions button,.ovdk-run-actions summary{min-height:32px;display:inline-flex;align-items:center;padding:0 12px;border-radius:999px;color:inherit;background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent);border:0;cursor:pointer;font:inherit;font-size:12.5px;font-weight:650;text-decoration:none;list-style:none}++.ovdk-run-actions summary::-webkit-details-marker{display:none}.ovdk-run-actions .primary{background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}++@media (hover:hover){.ovdk-run-actions a:hover,.ovdk-run-actions button:not(.primary):hover,.ovdk-run-actions summary:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 12%,transparent)}}++.ovdk-run-actions details{position:relative}.ovdk-run-actions details menu{position:absolute;right:0;z-index:2;min-width:180px;padding:4px;margin:0;list-style:none;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:10px;background:var(--bg-primary,#19191f);box-shadow:0 8px 24px rgba(0,0,0,.2)}++.ovdk-run-repeat{display:block;width:100%;min-height:36px;padding:0 10px;text-align:left;border:0;border-radius:10px;background:transparent;color:var(--accent-light,var(--accent,#60a5fa));font:inherit;font-size:12.5px;font-weight:650;cursor:pointer}++@media(max-width:640px){.ovdk-run-filters button,.ovdk-run-actions a,.ovdk-run-actions button,.ovdk-run-actions summary,.ovdk-run-repeat{min-height:44px}.ovdk-skills button{min-height:52px}}++`;++ const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = String(text); return e; };++ const ago = (at) => { const n = Date.parse(at || ''); if (!Number.isFinite(n)) return ''; const m = Math.max(0, Math.round((Date.now() - n) / 60000)); return m < 1 ? 'now' : m < 60 ? `${m} min` : m < 1440 ? `${Math.floor(m / 60)} h` : `${Math.floor(m / 1440)} d`; };++ const until = (at) => { const m = Math.max(0, Math.ceil((Date.parse(at || '') - Date.now()) / 60000)); return m < 1 ? 'now' : m < 60 ? `in ${m} min` : m < 1440 ? `in ${Math.ceil(m / 60)} h` : `in ${Math.ceil(m / 1440)} d`; };++ const cash = (n) => n == null ? '' : Number(n) < .01 ? `$${Number(n).toFixed(4)}` : `$${Number(n).toFixed(2)}`;++ const iconOf = (it) => { const s = String(it.skill || it.subject && it.subject.kind || it.kind || ''); return /post|blog|write|changelog|draft|patch/.test(s) ? ['fa-pen','#d7974d'] : /wiki|article|research/.test(s) ? ['fa-book','#9875d8'] : /image|pic|meme/.test(s) ? ['fa-image','#d66bad'] : /chat|thread/.test(s) ? ['fa-comments','#61a9d7'] : /code|repo/.test(s) ? ['fa-code','#63b89c'] : /clip|stream|video/.test(s) ? ['fa-film','#dc866a'] : /quest/.test(s) ? ['fa-flag','#b9a74b'] : ['fa-sparkles','#8b92d9']; };++ const dots = () => { const NS = 'http://www.w3.org/2000/svg'; const s = document.createElementNS(NS, 'svg'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true');++ [5.5, 12, 18.5].forEach((x) => { const c = document.createElementNS(NS, 'circle'); c.setAttribute('cx', String(x)); c.setAttribute('cy', '12'); c.setAttribute('r', '1.7'); s.append(c); }); return s; };++ const isUrl = (u) => /^https:\/\//.test(String(u || ''));++ // Where a person picks the model a run uses, or adds their own key.++ const MODEL_PAGE = 'https://openvibe.actor/chat/model';++ function mount(panel, o) {++ const st = o.styles && o.styles(); if (st && !st.textContent.includes('.ovdk-overview{')) st.textContent += CSS;++ // A site's Dock passes no scopes (full behaviour); the extension passes its read-only set.++ const canDecide = !Array.isArray(o.scopes) || o.scopes.includes('actions.decide');++ let doc = null, busy = false, timer = 0, gone = false, filter = o.state().agent_filter || 'all';++ const request = async (path, init) => o.call(path, init);++ const all = () => doc ? ['waiting','running','scheduled','done','failed'].flatMap((b) => doc.buckets[b] || []) : [];++ function row(it) {++ const li = h('li', 'ovdk-run ovac-item'); li.dataset.state = it.bucket; li.dataset.bucket = it.bucket;++ const [glyph, color] = iconOf(it); li.style.setProperty('--run-color', color);++ const ic = h('span', 'ovdk-run-ic'); const i = h('i', `fa-solid ${glyph}`); i.setAttribute('aria-hidden', 'true'); ic.append(i);++ const body = h('div', 'ovdk-run-sub');++ const title = it.subject && it.subject.title || it.title || 'Agent run';++ const url = it.subject && it.subject.url || it.href;++ if (isUrl(url)) { const t = h('a', 'ovdk-run-title'); t.href = url; t.target = '_blank'; t.rel = 'noopener'; t.append(h('b', '', title)); body.append(t); } else body.append(h('b', '', title));++ const error = it.run_error; let st = 'Done', line = '';++ if (it.bucket === 'running') { st = 'Running'; line = it.step || it.summary || 'Working'; }++ else if (it.bucket === 'waiting') { st = 'Waiting'; line = it.summary || 'Your answer is needed'; }++ else if (it.bucket === 'scheduled') { st = 'Scheduled'; line = it.next_run_at ? until(it.next_run_at) : 'Time pending'; }++ else if (it.bucket === 'failed') { st = 'Failed'; line = error ? `${error.words} ${error.next}` : 'This run could not finish. Open it for details.'; }++ else line = [it.summary, cash(it.cost_usd), it.duration_ms != null ? `${Math.round(it.duration_ms / 1000)}s` : ''].filter(Boolean).join(' · ');++ const ln = h('span', 'ovdk-run-line'); ln.append(h('span', 'ovdk-run-st', st), document.createTextNode(line)); body.append(ln);++ if (it.model) { const model = h('span', 'ovdk-run-model'); const mark = root.OpenVibeAIMarks && root.OpenVibeAIMarks.mark(it.maker, it.model); if (mark) model.append(mark); model.append(String(it.model)); body.append(model); }++ if (it.bucket === 'running') body.append(h('span', 'ovdk-run-bar'));++ const acts = h('div', 'ovdk-run-actions'); let more = null;++ if (canDecide && it.bucket === 'waiting' && (it.actions || []).includes('approve')) for (const action of ['approve','decline']) {++ const b = h('button', action === 'approve' ? 'primary' : '', action === 'approve' ? 'Approve' : 'Decline'); b.type = 'button'; b.dataset.do = action;++ b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/${action}`, { method: 'POST', body: '{}' }); b.textContent = action === 'approve' ? 'Approved' : 'Declined'; } catch { b.disabled = false; b.textContent = 'Try again'; } }); acts.append(b);++ }++++ // The error's own action says what the person can do about it: pick a model, add a key, or wait.++ const what = error && error.action;++ if (it.bucket === 'failed' && (what === 'pick_model' || what === 'add_key')) { const a = h('a', '', what === 'pick_model' ? 'Pick a model' : 'Add a key'); a.href = MODEL_PAGE; a.target = '_blank'; a.rel = 'noopener'; acts.append(a); }++ if (canDecide && it.bucket === 'running' && (it.actions || []).includes('cancel')) { const b = h('button', '', 'Stop'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/cancel`, { method: 'POST', body: '{}' }); refresh(); } catch { b.disabled = false; } }); acts.append(b); }++ if (canDecide && it.bucket === 'failed' && (it.actions || []).includes('retry')) { const b = h('button', '', 'Retry'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/retry`, { method: 'POST', body: '{}' }); refresh(); } catch { b.disabled = false; } }); acts.append(b); }++ if (canDecide && it.service === 'actor' && (it.kind === 'agent_task') && ['done', 'failed'].includes(it.bucket)) { const menu = h('details', 'ovdk-run-more'); const summary = h('summary'); summary.setAttribute('aria-label', 'More'); summary.title = 'More'; summary.append(dots()); menu.append(summary); const list = h('menu');++ const choose = h('li'); const a = h('a', '', 'Run again with another model'); a.href = MODEL_PAGE; a.target = '_blank'; a.rel = 'noopener'; choose.append(a); list.append(choose);++ const rerun = h('li'); const b = h('button', '', 'Run again'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/actor/${encodeURIComponent(it.id)}/retry`, { method: 'POST', body: '{}' }); menu.open = false; refresh(); } catch { b.disabled = false; b.textContent = 'Try again'; } }); rerun.append(b); list.append(rerun); menu.append(list); more = menu; }++ if (acts.childNodes.length) body.append(acts); li.append(ic, body); if (more) li.append(more); if (root.OVGlyphs) root.OVGlyphs.swap(li); return li;++ }++ function grouped(items) { const map = new Map(); for (const it of items) { const day = String(it.updated_at || it.created_at || '').slice(0,10); const key = [it.skill || it.kind, it.subject && it.subject.kind || '', it.bucket, day].join('|'); if (!map.has(key)) map.set(key, []); map.get(key).push(it); } return [...map.values()]; }++ function rows(items, target) { const list = h('ul', 'ovdk-runs'); grouped(items).forEach((group) => { if (group.length === 1) list.append(row(group[0])); else { const li = row(group[0]); const b = h('button', 'ovdk-run-repeat', `×${group.length} similar runs · Show all`); const details = h('ul', 'ovdk-runs'); details.hidden = true; group.slice(1).forEach((it) => details.append(row(it))); b.addEventListener('click', () => { details.hidden = !details.hidden; b.textContent = details.hidden ? `×${group.length} similar runs · Show all` : 'Hide repeats'; }); li.querySelector('.ovdk-run-sub').append(b, details); list.append(li); } }); target.append(list); }++ async function skills(box) {++ try {++ // Through Network (every site's connect-src reaches openvibe.network and refuses openvibe.actor).++ const data = await request('/api/v1/me/actions/skills');++ const available = (data.listings || []).filter((x) => x && x.skill && x.scan && x.scan.state === 'passed' && x.curator && x.curator.state === 'active').slice(0, 6);++ if (gone || !available.length) return;++ box.append(h('h3', '', 'Things agents can do for you'));++ const grid = h('div', 'ovdk-skills');++ available.forEach((s) => {++ const b = h('button', '', s.skill.name); b.type = 'button'; b.setAttribute('aria-label', `Start ${s.skill.name}`);++ b.addEventListener('click', async () => {++ b.disabled = true;++ try {++ const out = await request(`/api/v1/me/actions/skills/${encodeURIComponent(s.id)}/run`, { method: 'POST', body: JSON.stringify({ input: {}, choice: { mode: 'balanced' } }) });++ if (out.run && out.run.id) root.location.href = `${o.actorBase}/tasks/${encodeURIComponent(out.run.id)}`;++ else refresh();++ } catch { b.disabled = false; b.textContent = `${s.skill.name} · Try again`; }++ });++ grid.append(b);++ });++ box.append(grid);++ } catch { /* catalogue unavailable */ }++ }++ function overview() { const box = h('section', 'ovdk-overview'); const tiles = h('div', 'ovdk-tiles'); const now = Date.now(); const items = all(); const next = (doc.buckets.scheduled || []).find((x) => x.next_run_at); const periods = [[1,'Today'],[7,'This week']];++ const first = h('div', 'ovdk-tile ovdk-tile-next'); first.append(h('small', '', 'Next'), h('b', '', next ? next.title : 'Nothing scheduled')); if (next) first.append(h('span', '', until(next.next_run_at))); tiles.append(first);++ periods.forEach(([days,label]) => { const recent = items.filter((x) => now - Date.parse(x.updated_at || x.created_at) < days * 86400000); const done = recent.filter((x) => x.bucket === 'done').length, failed = recent.filter((x) => x.bucket === 'failed').length; const cost = recent.reduce((n, x) => n + (Number(x.cost_usd) || 0), 0);++ const tile = h('div', 'ovdk-tile'); const n = h('b', '', String(recent.length)); n.append(h('em', '', recent.length === 1 ? 'run' : 'runs')); const sub = h('span');++ if (done) { sub.append(h('i', 'ovdk-dot'), document.createTextNode(`${done} `)); } if (failed) { sub.append(h('i', 'ovdk-dot is-bad'), document.createTextNode(`${failed} `)); } if (cost) sub.append(document.createTextNode(cash(cost))); if (!recent.length) sub.textContent = 'None yet';++ tile.setAttribute('aria-label', `${label}: ${recent.length} runs, ${done} done, ${failed} failed${cost ? `, ${cash(cost)}` : ''}`); sub.title = `${done} done · ${failed} failed`;++ tile.append(h('small', '', label), n, sub); tiles.append(tile); }); box.append(tiles);++ const recent = [...(doc.buckets.done || []), ...(doc.buckets.failed || [])].sort((a,b) => String(b.updated_at).localeCompare(String(a.updated_at))).slice(0,3); if (recent.length) { box.append(h('h3', '', 'Last results')); rows(recent, box); }++ skills(box); return box; }++ function paint() { if (gone || !doc) return; panel.textContent = ''; const items = all(); const filters = h('div', 'ovdk-run-filters'); for (const [id,label] of [['all','All'],['running','Running'],['waiting','Waiting'],['scheduled','Scheduled'],['done','Done'],['failed','Failed']]) { const b = h('button', '', label); b.type='button'; b.setAttribute('aria-pressed', String(filter===id)); b.addEventListener('click', () => { filter=id; o.save({agent_filter:id}); paint(); }); filters.append(b); } panel.append(filters);++ if (filter==='all' && !(doc.counts.running || doc.counts.waiting)) panel.append(overview());++ else { const shown = filter==='all' ? items : (doc.buckets[filter] || []); if (shown.length) rows(shown,panel); else panel.append(h('p','ovdk-none','No runs in this view.')); }++ const foot = o.foot || h('div','ovdk-foot'); foot.textContent=''; const allLink = h('a','','All runs'); allLink.href='https://openvibe.actor/tasks'; const give = h('a','','Give an agent a task'); give.href='https://openvibe.actor/skills'; foot.append(allLink,give); if (!o.foot) panel.append(foot); }++ async function refresh() { if (gone || busy || !o.active()) return; busy=true; clearTimeout(timer); try { doc = await request('/api/v1/me/actions'); if (gone) return; if (doc && doc.buckets) { paint(); if (o.onCounts) o.onCounts(doc.counts || {}); } } catch { if (!gone) { panel.textContent=''; panel.append(h('div','ovdk-none','Your runs did not load. Open Agents again to retry.')); } } finally { busy=false; if (!gone && o.active() && doc && doc.counts && doc.counts.running) timer=setTimeout(refresh,15000); } }++ refresh(); if (o.loadMarks) o.loadMarks().then((ok) => { if (!gone && ok && doc) paint(); });++ return { refresh, destroy: () => { gone = true; clearTimeout(timer); } };++ }++ root.OpenVibeDockRuns = { mount };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,91 @@++/**++ * openvibe-shared/dock-share.js: the Dock's "Send to…" (4.144.0). dock.js loads it the first time something is sent++ * from a page (OpenVibeDock.share, the share sheet's OpenVibe Chat), so the Dock that every signed-in page carries++ * stays small.++ *++ * OpenVibeDockShare.bar({ draft, h, chatCall, open, cancel, host }) → the bar, drawn above the person's conversations++ * OpenVibeDockShare.ready({ draft, h, panel, form, box, fit }) the text in a conversation's box, unsent, said so++ *++ * The bar says what is being sent and lets the person write to someone new by their OpenVibe name: Chat's++ * /api/dm/users/search finds them, POST /api/dm/conversations starts the conversation, both with the person's own++ * token (dock.js's chatCall). Nothing is sent from here: the message waits in the conversation's box until the person++ * presses Send there.++ */++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockShare) return;++++ const CSS = `++.ovdk-share { flex: none; display: grid; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border, rgba(255,255,255,.08)); background: color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent); }++.ovdk-share-h { display: flex; align-items: center; justify-content: space-between; gap: 10px; }++.ovdk-share-h b { font-size: 14px; color: var(--text-primary, #ececf1); }++.ovdk-share-x { min-height: 32px; padding: 0 10px; border: 0; border-radius: 8px; background: none; color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }++.ovdk-share-x:hover { color: var(--text-primary, #ececf1); }++.ovdk-share-what { margin: 0; font-size: 13px; color: var(--text-secondary, #b6b6c0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.ovdk-to { display: flex; gap: 8px; }++.ovdk-to input { flex: 1; min-width: 0; box-sizing: border-box; min-height: 40px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--border-light, var(--border, rgba(255,255,255,.14))); background: var(--bg-primary, #12141b); color: var(--text-primary, #ececf1); font: inherit; font-size: 14px; }++.ovdk-to input:focus { outline: none; border-color: var(--accent, #3b82f6); }++.ovdk-to-go { flex: none; min-height: 40px; padding: 0 14px; border: 0; border-radius: 12px; cursor: pointer; font: inherit; font-size: 14px; font-weight: 700; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }++.ovdk-to-go:disabled { opacity: .55; cursor: default; }++.ovdk-share .ovdk-err { margin: 0; }++.ovdk-ready { flex: none; margin: 6px 14px 0; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }++`;++ function style(host) {++ const st = (host && host.ownerDocument && host.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles');++ if (st && !st.textContent.includes('.ovdk-share {')) st.textContent += CSS;++ }++++ function bar(c) {++ const { draft, h, chatCall, open, cancel, host } = c;++ style(host);++ let busy = false;++ const el = h('div', 'ovdk-share');++ const top = h('div', 'ovdk-share-h');++ const x = h('button', 'ovdk-share-x', 'Cancel'); x.type = 'button';++ x.addEventListener('click', () => cancel());++ top.append(h('b', '', 'Send to…'), x);++ const what = h('p', 'ovdk-share-what', draft.title || draft.text); what.title = draft.text;++ const to = h('form', 'ovdk-to');++ const name = h('input'); name.type = 'text'; name.maxLength = 40; name.autocomplete = 'off'; name.spellcheck = false;++ name.placeholder = 'Someone new: their OpenVibe name'; name.setAttribute('aria-label', 'Their OpenVibe name');++ const next = h('button', 'ovdk-to-go', 'Next'); next.type = 'submit';++ const err = h('p', 'ovdk-err'); err.hidden = true;++ to.append(name, next);++ to.addEventListener('submit', async (e) => {++ e.preventDefault();++ const want = name.value.trim().replace(/^@/, '');++ if (want.length < 2 || busy) return;++ busy = true; next.disabled = true; err.hidden = true;++ try {++ const found = (await chatCall(`/api/dm/users/search?q=${encodeURIComponent(want)}`)).users || [];++ const exact = found.find((u) => String(u.username || '').toLowerCase() === want.toLowerCase());++ if (!exact) {++ err.textContent = found.length ? `Nobody is called @${want}. Did you mean @${found[0].username}?` : `Nobody on OpenVibe is called @${want}.`;++ err.hidden = false;++ return;++ }++ const made = await chatCall('/api/dm/conversations', { method: 'POST', body: JSON.stringify({ user_ids: [exact.id] }) });++ const id = (made.conversation && made.conversation.id) || made.id;++ if (!id) throw new Error('no conversation');++ await open(id);++ } catch (z) {++ err.textContent = z.status === 429 ? 'Too many new conversations just now. Try again in a little while.' : z.status === 403 ? (z.message || 'You can’t start a conversation with them.') : 'That did not work. Try again.';++ err.hidden = false;++ } finally { busy = false; next.disabled = false; }++ });++ el.append(top, what, to, err);++ return el;++ }++++ /** The conversation the person picked: the text in its box, and a line saying nothing is sent until they send it. */++ function ready({ draft, h, panel, form, box, fit }) {++ box.value = draft;++ const line = h('p', 'ovdk-ready', 'Ready to send: change it if you like, then press Send.');++ panel.insertBefore(line, form);++ box.addEventListener('input', () => line.remove(), { once: true });++ form.addEventListener('submit', () => line.remove(), { once: true });++ setTimeout(fit, 0);++ }++++ root.OpenVibeDockShare = { bar, ready };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,423 @@++// ═══════════════════════════════════════════════════════════════++// OpenVibe — the Dock's Agent panel (design 2026-11-07: one agent, one timeline behind the Dock), Shared 4.126.0++//++// OpenVibeDockTimeline.mount(container, { token, base, active, focus }) → { refresh(), destroy() }++//++// `token` is the person's own Network token: a string, or the function the navbar hands over (which++// resolves it when it is needed). Nothing is asked without it: a signed-out page can never read the timeline.++// OpenVibe.Actor's API is GET /api/v1/agent/timeline (the person's own steps, newest first, with the cursor and the++// mode and needs-you filters), POST /api/v1/agent/approvals/:product/:id/decide (an approval is answered in the mode++// that owns it) and POST /api/v1/agent/kill-switch (stop what is open and pause the agent). On a site every call goes++// through Network's same-origin relay (/api/frame/agent…, /api/frame/tasks…) because every site's connect-src allows++// openvibe.network and refuses openvibe.actor; `base` reaches Actor itself in a test, and `frameBase` names the relay.++// `focus` is the id of the step to point at (the task id a site opened the Dock with): the entry that++// carries it is marked and brought into view.++//++// The panel is what the person reads:++// Now one card for what the agent is doing this moment: the mode's word, the step in plain words, how long,++// and the mode's own controls (Stop, and Resume when it is paused). Nothing running: "Your agent is idle."++// Needs you every approval or question from any mode, oldest first, with its buttons in place.++// Timeline one entry per step, newest first: the mode, the title, where it ran and why, the outcome, the time++// and the cost, and a link to the step's own page. A mode change is a line of its own.++// Folding today's entries open; earlier days are counted folds ("Yesterday (14)"); a long run of one mode folds++// into "12 more Code steps".++// Filters a chip for each mode and for "Needs you", in the URL of the panel; a filter never hides without a++// count.++//++// Everything is set as text, never as markup: a title, a reason or a link label never becomes HTML.++// ═══════════════════════════════════════════════════════════════++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OpenVibeDockTimeline) return;++++ const POLL_MS = 15000;++ const MODES = [++ { id: 'talk', word: 'Talk' }, { id: 'act', word: 'Act' }, { id: 'run', word: 'Run' },++ { id: 'code', word: 'Code' }, { id: 'browse', word: 'Browse' }, { id: 'watch', word: 'Watch' },++ { id: 'stream', word: 'Stream' },++ ];++ const word = (mode) => (MODES.find((m) => m.id === mode) || { word: 'Agent' }).word;++ const OUTCOME = { done: 'Done', failed: 'Failed', stopped: 'Stopped by you', running: 'Running', waiting: 'Waiting', needs_you: 'Needs you' };++++ function el(doc, tag, cls, text) {++ const n = doc.createElement(tag);++ if (cls) n.className = cls;++ if (text !== undefined && text !== null) n.textContent = String(text);++ return n;++ }++++ function since(startedAt) {++ const t = Date.parse(String(startedAt || ''));++ if (!Number.isFinite(t)) return '';++ const s = Math.max(0, Math.round((Date.now() - t) / 1000));++ if (s < 60) return s + 's';++ if (s < 3600) return Math.round(s / 60) + ' min';++ return Math.round(s / 3600) + ' h';++ }++++ function dayKey(iso) {++ const d = new Date(String(iso || ''));++ if (Number.isNaN(d.getTime())) return '';++ return d.toISOString().slice(0, 10);++ }++++ function styles(doc) {++ if (!doc || doc.getElementById('ovdt-styles')) return;++ const st = doc.createElement('style');++ st.id = 'ovdt-styles';++ st.textContent = [++ // The site's own theme (Opus, 11-07): the Dock's tokens, so the panel is the navy of a dark site and the paper++ // of a light one, never a grey card of its own. The site's font, a gutter that clears the panel's edge, one++ // primary button per card, and a breathing dot only on what is really running.++ '.ovdt{font:inherit;font-size:14px;line-height:1.45;color:var(--text-primary,#ececf1);padding:4px 12px 20px}',++ '.ovdt h3{margin:14px 2px 8px;font-size:11px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted,#93a0b4)}',++ '.ovdt h3:first-child{margin-top:6px}',++ '.ovdt-card{border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:12px 14px;margin:0 0 8px;background:color-mix(in srgb,var(--text-primary,#ececf1) 4%,transparent)}',++ '.ovdt-needs .ovdt-card{border-color:color-mix(in srgb,var(--warning,#f5a524) 38%,var(--border,rgba(255,255,255,.1)));background:color-mix(in srgb,var(--warning,#f5a524) 7%,transparent)}',++ '.ovdt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',++ '.ovdt-needs .ovdt-card>.ovdt-mode{margin-bottom:2px;font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:color-mix(in srgb,var(--warning,#f5a524) 70%,var(--text-primary,#ececf1))}',++ '.ovdt-mode{font-weight:750}',++ '.ovdt-title{flex:1 1 100%;min-width:0;overflow-wrap:anywhere;font-size:14.5px;font-weight:550}',++ '.ovdt-meta{color:var(--text-muted,#93a0b4);font-size:12.5px;overflow-wrap:anywhere}',++ '.ovdt-outcome{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--text-secondary,#a8b3c4);background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}',++ '.ovdt-outcome[data-s="running"]{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));background:color-mix(in srgb,var(--accent,#3b82f6) 16%,transparent)}',++ '.ovdt-outcome[data-s="running"]::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;animation:ovdt-breathe 1.8s ease-in-out infinite}',++ '.ovdt-outcome[data-s="failed"]{color:var(--danger-light,#f87171);background:color-mix(in srgb,var(--danger,#ef4444) 14%,transparent)}',++ '@keyframes ovdt-breathe{50%{opacity:.35;transform:scale(.8)}}',++ '.ovdt-buttons{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}',++ '.ovdt-btn{min-height:44px;min-width:44px;padding:0 16px;border-radius:12px;border:1px solid var(--border,rgba(255,255,255,.14));background:transparent;color:inherit;font:inherit;font-weight:650;cursor:pointer}',++ '.ovdt-btn.is-primary{border-color:transparent;background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}',++ '.ovdt-btn.is-stop{border-color:color-mix(in srgb,var(--danger,#ef4444) 45%,transparent);color:var(--danger-light,#f87171)}',++ '.ovdt-btn:disabled{opacity:.55;cursor:default}',++ '.ovdt-btn:focus-visible,.ovdt-chip:focus-visible,.ovdt-fold:focus-visible{outline:2px solid var(--accent-light,var(--accent,#3b82f6));outline-offset:1px}',++ '@media (hover:hover){.ovdt-btn:not(.is-primary):hover,.ovdt-chip:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}.ovdt-btn.is-primary:hover{filter:brightness(1.08)}}',++ '.ovdt-chip{min-height:44px;padding:0 14px;border-radius:999px;border:1px solid var(--border,rgba(255,255,255,.12));background:transparent;color:var(--text-secondary,#a8b3c4);font:inherit;font-size:13.5px;font-weight:650;cursor:pointer;white-space:nowrap;flex:none}',++ '.ovdt-chip[aria-pressed="true"]{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));border-color:color-mix(in srgb,var(--accent,#3b82f6) 55%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}',++ '.ovdt-chips{display:flex;gap:8px;overflow-x:auto;margin:4px -12px 0;padding:6px 12px 10px;scrollbar-width:none;-webkit-overflow-scrolling:touch}',++ '.ovdt-chips::-webkit-scrollbar{display:none}',++ '.ovdt-fold{width:100%;text-align:left;min-height:44px;margin:0 0 8px;padding:0 14px;border-radius:12px;border:1px dashed var(--border,rgba(255,255,255,.16));background:transparent;color:var(--text-secondary,#a8b3c4);font:inherit;font-weight:650;cursor:pointer}',++ '.ovdt-card.ovdt-focus{border-color:color-mix(in srgb,var(--accent,#3b82f6) 55%,transparent)}',++ '.ovdt-empty{color:var(--text-muted,#93a0b4);padding:4px 2px 6px}',++ '.ovdt-card.ovdt-empty{padding:12px 14px}',++ '.ovdt-link{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));font-weight:650;text-decoration:none}',++ '@media (max-width:640px){.ovdt-link{display:inline-flex;align-items:center;min-height:44px;padding:0 4px}}',++ '',++ ].join('');++ (doc.head || doc.documentElement).appendChild(st);++ }++++ function mount(container, opts = {}) {++ const doc = container.ownerDocument || document;++ styles(doc);++ // Network's frame relay on a site; an explicit base reaches Actor itself (a test).++ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';++ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');++ const endpoint = (path) => (direct ? direct + path : frameBase + path.replace(/^\/api\/v1/, ''));++ const rawToken = opts.token;++ const active = typeof opts.active === 'function' ? opts.active : () => true;++ const focus = typeof opts.focus === 'function' ? opts.focus : () => opts.focus || null;++ const filters = new Set();++ let timer = null;++ let destroyed = false;++++ // The navbar hands the Dock a function (the token may change when the person signs in again),++ // so it is resolved when a call is made, never read once at mount.++ async function bearer() {++ const t = typeof rawToken === 'function' ? await rawToken() : rawToken;++ return t ? 'Bearer ' + t : null;++ }++++ const wrap = el(doc, 'div', 'ovdt');++ const nowHead = el(doc, 'h3', null, 'Now');++ const nowBox = el(doc, 'div', 'ovdt-now');++ const needsHead = el(doc, 'h3', null, 'Needs you');++ const needsBox = el(doc, 'div', 'ovdt-needs');++ const chips = el(doc, 'div', 'ovdt-chips');++ const list = el(doc, 'div', 'ovdt-list');++ wrap.append(nowHead, nowBox, needsHead, needsBox, chips, list);++ container.appendChild(wrap);++++ async function api(path, options = {}) {++ const auth = await bearer();++ if (!auth) throw new Error('Sign in to see your agent');++ const res = await fetch(endpoint(path), Object.assign({ headers: { Accept: 'application/json', Authorization: auth } }, options));++ const body = await res.json().catch(() => ({}));++ if (!res.ok) throw new Error((body && (body.detail || body.title)) || ('HTTP ' + res.status));++ return body;++ }++++ function chip(id, label, count) {++ const b = el(doc, 'button', 'ovdt-chip', label + (count === undefined || count === null ? '' : ' (' + count + ')'));++ b.type = 'button';++ b.setAttribute('aria-pressed', filters.has(id) ? 'true' : 'false');++ b.addEventListener('click', () => {++ if (filters.has(id)) filters.delete(id); else filters.add(id);++ refresh();++ });++ return b;++ }++++ /** The step's state as a small pill; data-s lets a running step breathe and a failed one show red. */++ function outcome(status) {++ const o = el(doc, 'span', 'ovdt-outcome', OUTCOME[status] || status);++ o.setAttribute('data-s', String(status || '').toLowerCase());++ return o;++ }++++ function renderNow(now) {++ nowBox.textContent = '';++ if (!now) {++ nowBox.appendChild(el(doc, 'div', 'ovdt-card ovdt-empty', 'Your agent is idle. Ask it something in Talk.'));++ return;++ }++ const card = el(doc, 'div', 'ovdt-card');++ const row = el(doc, 'div', 'ovdt-row');++ row.appendChild(el(doc, 'span', 'ovdt-mode', word(now.mode)));++ row.appendChild(outcome(now.status));++ row.appendChild(el(doc, 'span', 'ovdt-title', now.title));++ row.appendChild(el(doc, 'span', 'ovdt-meta', since(now.started_at)));++ card.appendChild(row);++ if (now.placement && (now.placement.where || now.placement.why)) {++ card.appendChild(el(doc, 'div', 'ovdt-meta', [now.placement.where, now.placement.why].filter(Boolean).join(': ')));++ }++ // The Computer this step works on (plan 2026-11-07): the same machine and cost the task record carries.++ if (now.computer) card.appendChild(el(doc, 'div', 'ovdt-meta', [now.computer.machine, now.computer.cost_hint].filter(Boolean).join(' · ')));++ const buttons = el(doc, 'div', 'ovdt-buttons');++ // Take over / Give it back: one API on the task at Actor, the same hand-off the agent's own++ // hand_to_person tool uses. Only a step that names its task (ref) and its Computer offers it.++ if (now.computer && now.ref) {++ const driving = now.computer.driving === true || /^You took over/.test(String(now.title || ''));++ const take = el(doc, 'button', 'ovdt-btn is-primary', driving ? 'Give it back' : 'Take over');++ take.type = 'button';++ take.addEventListener('click', async () => {++ take.disabled = true;++ const auth = await bearer();++ try {++ await api('/api/v1/tasks/' + encodeURIComponent(now.ref) + (driving ? '/giveback' : '/takeover'), {++ method: 'POST', headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) }, body: '{}',++ });++ } catch (err) { /* the panel refreshes either way */ }++ refresh();++ });++ buttons.appendChild(take);++ }++ const stop = el(doc, 'button', 'ovdt-btn is-stop', 'Stop');++ stop.type = 'button';++ stop.addEventListener('click', async () => {++ stop.disabled = true;++ const auth = await bearer();++ try { await api('/api/v1/agent/kill-switch', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) }, body: JSON.stringify({ action: 'stop' }) }); } catch (err) { /* the panel refreshes either way */ }++ refresh();++ });++ buttons.appendChild(stop);++ card.appendChild(buttons);++ nowBox.appendChild(card);++ }++++ function renderNeeds(needs) {++ needsBox.textContent = '';++ if (!needs.length) {++ needsBox.appendChild(el(doc, 'div', 'ovdt-empty', 'Nothing needs you.'));++ return;++ }++ for (const item of needs) {++ const card = el(doc, 'div', 'ovdt-card');++ card.appendChild(el(doc, 'div', 'ovdt-mode', word(item.mode) + ' needs you'));++ card.appendChild(el(doc, 'div', 'ovdt-title', item.title));++ if (item.reason) card.appendChild(el(doc, 'div', 'ovdt-meta', item.reason));++ const buttons = el(doc, 'div', 'ovdt-buttons');++ for (const [label, decision] of [['Approve', 'approve'], ['Decline', 'decline']]) {++ const b = el(doc, 'button', decision === 'approve' ? 'ovdt-btn is-primary' : 'ovdt-btn', label);++ b.type = 'button';++ b.addEventListener('click', async () => {++ b.disabled = true;++ const approval = item.approval || {};++ const auth = await bearer();++ try {++ await api('/api/v1/agent/approvals/' + encodeURIComponent(approval.product || item.product) + '/' + encodeURIComponent(approval.id) + '/decide', {++ method: 'POST',++ headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) },++ body: JSON.stringify({ decision }),++ });++ } catch (err) { /* the panel refreshes either way */ }++ refresh();++ });++ buttons.appendChild(b);++ }++ card.appendChild(buttons);++ needsBox.appendChild(card);++ }++ }++++ function entryNode(entry) {++ const card = el(doc, 'div', 'ovdt-card');++ const row = el(doc, 'div', 'ovdt-row');++ row.appendChild(el(doc, 'span', 'ovdt-mode', word(entry.mode)));++ row.appendChild(outcome(entry.status));++ row.appendChild(el(doc, 'span', 'ovdt-title', entry.title));++ const when = new Date(entry.started_at || 0);++ row.appendChild(el(doc, 'span', 'ovdt-meta', Number.isNaN(when.getTime()) ? '' : when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })));++ card.appendChild(row);++ if (entry.transition) card.appendChild(el(doc, 'div', 'ovdt-meta', entry.transition));++ else if (entry.placement && (entry.placement.where || entry.placement.why)) {++ card.appendChild(el(doc, 'div', 'ovdt-meta', [entry.placement.where, entry.placement.why].filter(Boolean).join(': ')));++ }++ if (entry.computer) card.appendChild(el(doc, 'div', 'ovdt-meta', [entry.computer.machine, entry.computer.cost_hint].filter(Boolean).join(' · ')));++ if (entry.reason) card.appendChild(el(doc, 'div', 'ovdt-meta', entry.reason));++ if (entry.cost_usd !== null && entry.cost_usd !== undefined) card.appendChild(el(doc, 'div', 'ovdt-meta', '$' + Number(entry.cost_usd).toFixed(2)));++ if (entry.link) {++ const a = el(doc, 'a', 'ovdt-link', 'Open');++ a.href = entry.link;++ a.rel = 'noopener noreferrer';++ a.target = '_blank';++ card.appendChild(a);++ }++ // The step a site opened the Dock at (its task id): it is marked, so the person sees where they landed.++ const wanted = focusId();++ if (wanted && (entry.id === wanted || entry.ref === wanted)) card.classList.add('ovdt-focus');++ return card;++ }++++ /** The step to point at: the id (a task id) a site opened the Dock with, if it is still there. */++ function focusId() {++ try { return String(focus() || '').trim() || null; } catch (err) { return null; }++ }++++ /** Bring the focused step into view (the Dock's panel is short; the step may be below the fold). */++ function scrollToFocus() {++ const wanted = focusId();++ if (!wanted) return;++ const node = list.querySelector('.ovdt-focus');++ if (!node || !node.scrollIntoView) return;++ try { node.scrollIntoView({ block: 'nearest' }); } catch (err) { node.scrollIntoView(); }++ }++++ function renderList(entries) {++ list.textContent = '';++ if (!entries.length) {++ list.appendChild(el(doc, 'div', 'ovdt-empty', 'Nothing yet. The agent\u2019s steps show up here as it works.'));++ return;++ }++ // Today's entries are open; earlier days are counted folds.++ const today = new Date().toISOString().slice(0, 10);++ const groups = new Map();++ for (const entry of entries) {++ const key = dayKey(entry.started_at);++ if (!groups.has(key)) groups.set(key, []);++ groups.get(key).push(entry);++ }++ for (const [key, group] of groups) {++ if (key === today) {++ for (const entry of group) list.appendChild(entryNode(entry));++ continue;++ }++ const fold = el(doc, 'button', 'ovdt-fold', labelOf(key, group.length));++ fold.type = 'button';++ fold.setAttribute('aria-expanded', 'false');++ fold.addEventListener('click', () => {++ const open = fold.getAttribute('aria-expanded') === 'true';++ fold.setAttribute('aria-expanded', open ? 'false' : 'true');++ const shown = fold.nextElementSibling;++ if (shown && shown.classList.contains('ovdt-fold-body')) shown.remove();++ if (!open) {++ const box = el(doc, 'div', 'ovdt-fold-body');++ for (const entry of group) box.appendChild(entryNode(entry));++ fold.after(box);++ }++ });++ list.appendChild(fold);++ }++ // A long run of one mode folds into "12 more Code steps".++ let run = 0;++ let runMode = null;++ const runs = [];++ for (const entry of entries) {++ if (entry.mode === runMode) { run += 1; continue; }++ if (run >= 3) runs.push({ mode: runMode, count: run });++ runMode = entry.mode;++ run = 1;++ }++ if (run >= 3) runs.push({ mode: runMode, count: run });++ for (const r of runs) {++ const more = el(doc, 'div', 'ovdt-meta', r.count + ' more ' + word(r.mode) + ' steps');++ list.appendChild(more);++ }++ }++++ function labelOf(day, count) {++ const today = new Date();++ const d = new Date(day + 'T00:00:00Z');++ const diff = Math.round((Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - d.getTime()) / 86400000);++ if (diff === 1) return 'Yesterday (' + count + ')';++ if (diff < 7) return 'This week (' + count + ')';++ return day + ' (' + count + ')';++ }++++ function renderChips(counts) {++ chips.textContent = '';++ for (const m of MODES) {++ const n = counts && counts[m.id];++ if (!n) continue;++ chips.appendChild(chip(m.id, m.word, n));++ }++ if (counts && counts.needs_you) chips.appendChild(chip('needs_you', 'Needs you', counts.needs_you));++ }++++ async function refresh() {++ if (destroyed || !active()) return;++ const mode = [...filters].find((f) => f !== 'needs_you');++ const query = [];++ if (mode) query.push('mode=' + encodeURIComponent(mode));++ if (filters.has('needs_you')) query.push('needs_you=1');++ const path = '/api/v1/agent/timeline' + (query.length ? '?' + query.join('&') : '');++ let data;++ try { data = await api(path); } catch (err) {++ if (destroyed) return;++ container.hidden = true;++ return;++ }++ if (destroyed) return;++ container.hidden = !data.now && !(data.needs_you || []).length && !(data.entries || []).length;++ renderNow(data.now);++ renderNeeds(data.needs_you || []);++ renderChips(data.counts || {});++ renderList(data.entries || []);++ scrollToFocus();++ writeUrl();++ }++++ function writeUrl() {++ try {++ const url = new URL(doc.defaultView.location.href);++ url.searchParams.delete('mode');++ url.searchParams.delete('needs_you');++ for (const f of filters) {++ if (f === 'needs_you') url.searchParams.set('needs_you', '1');++ else url.searchParams.set('mode', f);++ }++ doc.defaultView.history.replaceState(null, '', url.toString());++ } catch (err) { /* the panel keeps its filters in memory */ }++ }++++ function start() {++ if (timer) return;++ timer = setInterval(refresh, POLL_MS);++ if (timer.unref) timer.unref();++ }++++ refresh();++ start();++++ return {++ refresh,++ destroy() {++ destroyed = true;++ if (timer) clearInterval(timer);++ timer = null;++ wrap.remove();++ },++ };++ }++++ root.OpenVibeDockTimeline = { mount };++}(typeof window !== 'undefined' ? window : globalThis));@@@ -36,7 +36,6 @@.ovdk-tmenu .go { background: var(--accent, #3b82f6); border-color: transparent; color: var(--on-accent, #fff); }.ovdk-pill[data-tone="timer"] .ovdk-t { font-variant-numeric: tabular-nums; }.ovdk-pill[data-tone="timer-done"] { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); }--@media (prefers-reduced-motion: reduce) { .ovdk-timer-bar i { transition: none; } }`;const pad = (n) => String(n).padStart(2, '0');const left = (ms) => {@@@ -9,11 +9,23 @@if (typeof document === 'undefined' || root.OpenVibeDockVibe) return;const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };++ const PLUS = '<path d="M12 5.5v13M5.5 12h13"/>';++ const OUT = '<path d="M13.5 5.5h5v5M18.5 5.5l-7.5 7.5M17 13.5v4a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 5 17.5v-9A1.5 1.5 0 0 1 6.5 7h4"/>';const MIC = '<rect x="9" y="3.5" width="6" height="10.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5v3"/>';const CSS = `++ .ovdk-ib.is-new { background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }++ .ovdk-ib.is-new:hover, .ovdk-ib.is-new:focus-visible { background: color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); }.ovdk-vibe-h .ovdk-i { color: var(--accent, #3b82f6); margin-left: 6px; }-- .ovdk-away { margin-left: auto; font-size: 12.5px; font-weight: 650; color: var(--accent, #3b82f6); text-decoration: none; white-space: nowrap; }-- .ovdk-away:hover { text-decoration: underline; }++ .ovdk-vibe-list { list-style: none; padding: 2px 6px 6px; margin: 0; display: grid; gap: 2px; }++ .ovdk-vibe-list button { width: 100%; min-height: 56px; display: grid; grid-template-columns: 36px minmax(0,1fr) auto; grid-template-rows: auto auto; gap: 2px 12px; align-items: center; padding: 9px 10px; border: 0; border-radius: 14px; background: transparent; color: var(--text-primary,#ececf1); text-align: left; cursor: pointer; font: inherit; }++ .ovdk-vibe-list .ovdk-vibe-site { grid-row: 1 / 3; width: 36px; height: 36px; border-radius: 11px; display: grid; place-items: center; background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }++ .ovdk-vibe-list .ovdk-vibe-site svg { width: 19px; height: 19px; }++ .ovdk-vibe-list b { font-size: 14px; } .ovdk-vibe-list span { font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }++ .ovdk-vibe-list small { justify-self: end; }++ .ovdk-vibe-list button:hover, .ovdk-vibe-list button:focus-visible { background: var(--hover-bg,rgba(127,127,127,.12)); }++ .ovdk-vibe-list b, .ovdk-vibe-list span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++ .ovdk-vibe-list small, .ovdk-vibe-list time { color: var(--text-muted,#8b8b96); font-size: 12px; }++ .ovdk-vibe-back { min-width: 44px; min-height: 44px; display: grid; place-items: center; border: 0; background: transparent; color: inherit; cursor: pointer; }.ovdk-msg.is-vibe .ovdk-text a { color: var(--accent, #3b82f6); text-decoration: underline; word-break: break-all; }.ovdk-typing .ovdk-text { display: inline-flex; gap: 4px; align-items: center; padding: 10px 12px; }.ovdk-typing i { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted, #8b8b96); animation: ovdk-hop 1s ease-in-out infinite; }@@@ -23,7 +35,8 @@.ovdk-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 8px; }.ovdk-chip { padding: 7px 12px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 12.5px; cursor: pointer; }.ovdk-chip:hover { border-color: var(--accent, #3b82f6); }-- .ovdk-mic { width: 40px; height: 40px; flex: none; margin-left: -8px; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }++ .ovdk-mic, .ovdk-attach { width: 44px; height: 44px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }++ .ovdk-attach:hover { color: var(--text-primary,#ececf1); }.ovdk-mic:hover { color: var(--text-primary, #ececf1); }.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }.ovdk-res { display: grid; gap: 6px; margin-top: 6px; max-width: 100%; }@@@ -40,10 +53,28 @@.ovdk-res-a button:disabled { opacity: .55; cursor: default; }.ovdk-res-c.is-stopped { border-color: var(--border, rgba(255,255,255,.12)); background: transparent; }.ovdk-res-c.is-stopped .ovdk-i, .ovdk-res-c.is-stopped .ovdk-res-t { color: var(--text-muted, #8b8b96); }-- @media (prefers-reduced-motion: reduce) { .ovdk-typing i { animation: none; } }`;let ctx = null, G = {};++ // 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for++ // drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.++ const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';++ const VIBE_REV = { 'ov-composer.js': '9b3472802903' };++ function joinComposer(form) {++ if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box++ form.setAttribute('data-ovc-no-files', '');++ const go = () => { try { if (root.OpenVibeComposer && form.isConnected) root.OpenVibeComposer.enhance(form); } catch (e) { console.error(e); } };++ if (root.OpenVibeComposer) { go(); return; }++ const m = /^(https?:\/\/[^?#]*\/)dock-vibe(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(selfSrc);++ if (!m) return;++ let sc = document.getElementById('ov-composer-script');++ if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = m[1] + 'ov-composer.js' + (VIBE_REV['ov-composer.js'] ? '?v=' + VIBE_REV['ov-composer.js'] : ''); document.head.appendChild(sc); }++ sc.addEventListener('load', go, { once: true });++ }const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };++ /** A round icon button with its name as label and tooltip. */++ const iconButton = (tag, cls, path, label) => { const b = h(tag, cls); if (tag === 'button') b.type = 'button'; b.setAttribute('aria-label', label); b.setAttribute('data-ov-tip', label); b.title = label; b.innerHTML = svg(path); return b; };++ /** Open Vibe in OpenVibe.Chat, as an icon (the thread's own address when one is open). */++ const awayLink = () => { const a = iconButton('a', 'ovdk-ib ovdk-away', OUT, 'Open in Chat'); a.href = 'https://openvibe.chat/vibe'; a.target = '_blank'; a.rel = 'noopener'; return a; };// ── Vibe (plan L48 V2; the owner, 11-01: "access your agent or the AI… from across all websites… use a voice// feature to tell it to set a reminder… have a conversation with your Vibe AI") ──@@@ -54,7 +85,7 @@// browser without it (Firefox) records instead, up to 30 s, and Chat's /api/vibe/listen turns that into text// (OpenVibe.AI's speech-to-text on this network's own machines; the recording is never kept).// Everything is set as text; a link is a link only when it is https.-- const vibe = { panel: null, chat: null, msgs: [], last: 0, timer: 0, busy: false, waitUntil: 0, rec: null, talk: null };++ const vibe = { panel: null, chat: null, view: 'list', fresh: false, msgs: [], last: 0, timer: 0, busy: false, waitUntil: 0, rec: null, talk: null, generation: 0 };const SR = root.SpeechRecognition || root.webkitSpeechRecognition || null;const MR = !SR && root.MediaRecorder && root.navigator && root.navigator.mediaDevices && root.navigator.mediaDevices.getUserMedia ? root.MediaRecorder : null;const TALK = Boolean(SR || MR);@@@ -115,15 +146,52 @@if (m.at) li.title = new Date(m.at).toLocaleString();return li;}++ const age = (iso) => { const n = Date.parse(iso || ''); if (!Number.isFinite(n)) return ''; const m = Math.max(0, Math.floor((Date.now() - n) / 60000)); return m < 1 ? 'now' : m < 60 ? `${m} min ago` : m < 1440 ? `${Math.floor(m / 60)} h ago` : `${Math.floor(m / 1440)} d ago`; };++ async function openList() {++ const generation = ++vibe.generation;++ clearTimeout(vibe.timer); vibe.view = 'list'; vibe.chat = null;++ const panel = vibe.panel; if (!panel) return;++ panel.textContent = '';++ // No heading (ask 335, the owner: "remove the text … new conversation … an icon plus button … open in Chat a++ // little button icon"): one slim row of two icon buttons over your chats.++ const head = h('div', 'ovdk-bar');++ const newer = iconButton('button', 'ovdk-ib is-new ovdk-vibe-new', PLUS, 'New chat'); newer.addEventListener('click', () => { vibe.fresh = true; vibe.chat = null; ctx.save({ vibe: null }); openVibe(); });++ head.append(newer, awayLink());++ const scroll = h('div', 'ovdk-scroll'); const list = h('ul', 'ovdk-vibe-list');++ scroll.append(list); panel.append(head, scroll);++ try {++ const out = await ctx.chatCall('/api/vibe/conversations');++ if (generation !== vibe.generation || vibe.view !== 'list' || vibe.panel !== panel) return;++ const conversations = Array.isArray(out.conversations) ? out.conversations : [];++ if (!conversations.length) list.append(h('li', 'ovdk-none', 'Your chats with Vibe will appear here. Press + to start one.'));++ conversations.forEach((c) => {++ const li = h('li'), b = h('button'); b.type = 'button';++ const title = h('b', '', c.name || 'Conversation'); const time = h('time', '', age(c.last_message_at));++ const line = h('span', '', c.last_line || 'No messages yet');++ // The site it began on, by name ("Blog", not openvibe.blog), beside the time.++ const host = String(c.site || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '');++ const name = host ? (/^openvibe\.([a-z]+)$/.exec(host) || [])[1] : '';++ const site = h('small', '', name ? name.charAt(0).toUpperCase() + name.slice(1) : host);++ const mark = h('span', 'ovdk-vibe-site'); mark.append(icon(G.vibe));++ b.append(mark, title, time, line, site);++ b.setAttribute('aria-label', `${c.name || 'Conversation'}, ${age(c.last_message_at)}`);++ b.addEventListener('click', () => { vibe.fresh = false; vibe.chat = { slug: c.slug }; ctx.save({ vibe: { slug: c.slug, at: new Date().toISOString() } }); openVibe(); });++ li.append(b); list.append(li);++ });++ } catch (e) { if (generation === vibe.generation) list.append(h('li', 'ovdk-none', e && e.status === 401 ? 'Sign in to see your conversations.' : 'Conversations did not load. Open Vibe again to retry.')); }++ }async function openVibe() {++ const generation = ++vibe.generation;clearTimeout(vibe.timer);++ vibe.view = 'conversation';const panel = vibe.panel; if (!panel) return;vibe.msgs = []; vibe.last = 0;panel.textContent = '';const head = h('div', 'ovdk-thread-h ovdk-vibe-h');const title = h('b', '', 'Vibe');-- const away = h('a', 'ovdk-away', 'Open in Chat'); away.href = 'https://openvibe.chat/vibe'; away.target = '_blank'; away.rel = 'noopener';-- head.append(icon(G.vibe), title, away);++ const back = h('button', 'ovdk-vibe-back'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your chats'); back.textContent = '←'; back.addEventListener('click', () => { ctx.save({ vibe: null }); openList(); });++ const away = awayLink();++ head.append(back, icon(G.vibe), title, away);const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');const scroll = h('div', 'ovdk-scroll ovdk-thread'); scroll.append(list);const typing = h('li', 'ovdk-msg is-vibe ovdk-typing'); typing.innerHTML = '<span class="ovdk-text"><i></i><i></i><i></i></span>'; typing.setAttribute('aria-label', 'Vibe is writing');@@@ -131,6 +199,37 @@const box = h('textarea'); box.rows = 1; box.maxLength = 8000; box.placeholder = TALK ? 'Ask Vibe, or hold the mic' : 'Ask Vibe'; box.setAttribute('aria-label', 'Message Vibe');const go = h('button', 'ovdk-go'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(icon(G.send));const err = h('p', 'ovdk-err'); err.hidden = true;++ const reason = (e) => e && e.status === 401 ? 'Your sign in expired. Sign in again to continue.'++ : e && e.status === 429 ? 'Too many requests were sent. Wait a minute, then retry.'++ : e && e.code === 'vibe.off' ? 'Vibe is unavailable right now. Retry when it returns.'++ : e && e.status === 503 ? 'The AI service is unavailable right now. Retry shortly.'++ : 'The request could not reach Vibe. Check your connection and retry.';++ const showFailure = (e, retry) => { err.textContent = `Vibe did not answer: ${reason(e)} `; err.hidden = false;++ if (retry) { const b = h('button', '', 'Retry'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await retry(); } catch (x) { showFailure(x, retry); } finally { b.disabled = false; } }); err.append(b); }++ };++ const attach = h('button', 'ovdk-attach', '+'); attach.type = 'button'; attach.setAttribute('aria-label', 'Attach a picture'); attach.title = 'Attach a picture';++ const file = h('input'); file.type = 'file'; file.accept = 'image/*'; file.hidden = true;++ attach.addEventListener('click', () => file.click());++ file.addEventListener('change', async () => {++ const picture = file.files && file.files[0]; if (!picture) return;++ attach.disabled = true; err.hidden = true;++ try {++ const path = vibe.chat ? `/api/chat/rooms/${encodeURIComponent(vibe.chat.slug)}/pictures` : '/api/chat/vibe/picture';++ const out = await ctx.chatCall(path, { method: 'POST', body: picture, type: picture.type || 'image/jpeg' });++ if (out.room && out.room.slug) { vibe.chat = { slug: out.room.slug }; vibe.fresh = false; ctx.save({ vibe: { slug: out.room.slug, at: new Date().toISOString() } }); }++ openVibe();++ } catch { err.textContent = 'That picture could not be attached. Try again.'; err.hidden = false; }++ finally { attach.disabled = false; file.value = ''; }++ });++ // 350: Build it in Code. What you wrote, else Vibe's last answer, goes to the Code part as a new task's words.++ const build = iconButton('button', 'ovdk-attach ovdk-build', 'M8 7l-5 5 5 5M16 7l5 5-5 5', 'Build it in Code');++ build.addEventListener('click', () => {++ const last = [...vibe.msgs].reverse().find((m) => m.from !== 'you');++ const text = box.value.trim() || (last && last.text) || '';++ if (!text) { box.focus(); return; }++ build.dispatchEvent(new CustomEvent('ov-dock:part', { bubbles: true, detail: { part: 'code', text: text.slice(0, 16000) } }));++ });++ form.append(attach, file, build);let mic = null;if (TALK) {mic = h('button', 'ovdk-mic'); mic.type = 'button'; mic.append(icon(G.mic));@@@ -140,6 +239,8 @@form.append(box, go);panel.append(head, scroll, err, form);const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };++ vibe.fill = (text) => { box.value = String(text || '').slice(0, 8000); fit(); try { box.focus(); box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } };++ if (vibe.pendingAsk) { vibe.fill(vibe.pendingAsk); vibe.pendingAsk = null; }box.addEventListener('input', fit);box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });@@@ -148,8 +249,9 @@vibe.busy = true; go.disabled = true; err.hidden = true;try {let timezone = null; try { timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { /* */ }-- const out = await ctx.chatCall('/api/vibe/ask', { method: 'POST', body: JSON.stringify({ message: text, slug: vibe.chat ? vibe.chat.slug : undefined, timezone }) });-- if (out.chat) { vibe.chat = out.chat; away.href = out.chat.url; }++ const out = await ctx.chatCall('/api/vibe/ask', { method: 'POST', body: JSON.stringify({ message: text, slug: vibe.chat ? vibe.chat.slug : undefined, fresh: vibe.fresh, timezone }) });++ if (generation !== vibe.generation) return;++ if (out.chat) { vibe.chat = out.chat; vibe.fresh = false; away.href = out.chat.url; ctx.save({ vibe: { slug: out.chat.slug, at: new Date().toISOString() } }); }box.value = ''; fit();const empty = list.querySelector('.ovdk-vibe-start'); if (empty) empty.remove();if (out.message && !vibe.msgs.some((x) => x.id === out.message.id)) { list.append(vbubble(out.message, vibe.msgs[vibe.msgs.length - 1])); vibe.msgs.push(out.message); vibe.last = Math.max(vibe.last, out.message.id || 0); }@@@ -158,22 +260,22 @@scroll.scrollTop = scroll.scrollHeight;clearTimeout(vibe.timer); vibe.timer = setTimeout(() => pull(), 1200);} catch (x) {-- err.textContent = x.status === 429 ? 'That is a lot of questions at once: wait a minute, then ask again.' : x.status === 401 ? 'Sign in again to talk to Vibe.' : 'That did not reach Vibe. Try again.';-- err.hidden = false;++ showFailure(x, () => send(text));} finally { vibe.busy = false; go.disabled = false; box.focus(); }}form.addEventListener('submit', (e) => { e.preventDefault(); send(box.value.trim()); });++ joinComposer(form);async function pull(first) {let got;try {-- got = await ctx.chatCall(`/api/vibe/chat${vibe.chat ? `?slug=${encodeURIComponent(vibe.chat.slug)}${vibe.last ? `&after=${vibe.last}` : ''}` : ''}`);++ got = vibe.fresh && !vibe.chat ? { chat: null, messages: [] } : await ctx.chatCall(`/api/vibe/chat${vibe.chat ? `?slug=${encodeURIComponent(vibe.chat.slug)}${vibe.last ? `&after=${vibe.last}` : ''}` : ''}`);} catch (e) {-- if (first) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 401 ? 'Sign in to OpenVibe.Chat to talk to Vibe.' : 'Vibe did not answer just now. Try again in a moment.')); }++ if (first && generation === vibe.generation) { list.textContent = ''; showFailure(e, () => pull(true)); }return;}-- if (!ctx.active() || vibe.panel !== panel) return;-- if (got.chat) { vibe.chat = got.chat; away.href = got.chat.url; }++ if (generation !== vibe.generation || !ctx.active() || vibe.panel !== panel) return;++ if (got.chat) { vibe.chat = got.chat; away.href = got.chat.url; title.textContent = got.chat.name || 'Vibe'; }if (first) list.textContent = '';const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;let answered = false;@@@ -184,7 +286,13 @@if (m.from === 'vibe') answered = true;if (!first && Array.isArray(m.results) && m.results.some((r) => r && r.kind === 'timer') && ctx.refresh) ctx.refresh();});-- if (answered || Date.now() > vibe.waitUntil) { typing.remove(); vibe.waitUntil = 0; }++ if (answered || Date.now() > vibe.waitUntil) {++ typing.remove();++ if (!answered && vibe.waitUntil && Date.now() > vibe.waitUntil) {++ showFailure({ status: 503 }, vibe.chat ? async () => { await ctx.chatCall(`/api/chat/vibe/${encodeURIComponent(vibe.chat.slug)}/retry`, { method: 'POST', body: '{}' }); err.hidden = true; vibe.waitUntil = Date.now() + 90000; list.append(typing); pull(); } : null);++ }++ vibe.waitUntil = 0;++ }if (first && !vibe.msgs.length) {const start = h('li', 'ovdk-vibe-start');start.append(h('b', '', 'Ask Vibe anything'), h('span', '', TALK ? 'Type, or hold the mic and say it. It sets reminders, finds what is live, and answers from the whole network.' : 'It sets reminders, finds what is live, and answers from the whole network.'));@@@ -267,7 +375,9 @@list.append(h('li', 'ovdk-wait', null));list.firstChild.innerHTML = '<span></span>';await pull(true);-- setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);++ // A touch screen does not get the keyboard on opening (11-09, the owner: it "immediately opens the keyboard… you++ // can't see the chat input or what it did"): the answer and the box show first; a tap on the box types.++ if (!(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);}function mount(panel, c) {@@@ -277,11 +387,18 @@if (st && !st.textContent.includes('.ovdk-vibe-start')) st.textContent += CSS;else if (!st && !document.getElementById('ovdk-vibe-styles')) { const s = document.createElement('style'); s.id = 'ovdk-vibe-styles'; s.textContent = CSS; document.head.appendChild(s); }vibe.panel = panel;-- openVibe();++ // The Chats tab's AI chats row asks for the list, not for the last conversation.++ const onList = ctx.wantList ? ctx.wantList() : false;++ const selected = ctx.state && ctx.state().vibe;++ if (!onList && selected && selected.slug && Date.now() - Date.parse(selected.at) < 20 * 60000) { vibe.chat = { slug: selected.slug }; openVibe(); }++ else openList();return {-- refresh: () => { vibe.panel = panel; return openVibe(); },-- pause: () => { clearTimeout(vibe.timer); if (vibe.rec) { try { vibe.rec.abort(); } catch { /* */ } } },++ refresh: () => { vibe.panel = panel; const list = ctx.wantList ? ctx.wantList() : false; if (list) return openList(); return vibe.view === 'list' ? openList() : openVibe(); },++ list: () => { vibe.panel = panel; return openList(); },++ pause: () => { vibe.generation++; clearTimeout(vibe.timer); if (vibe.rec) { try { vibe.rec.abort(); } catch { /* */ } } },talk: () => { if (vibe.talk) vibe.talk(); },++ // A page's "Ask your agent" (4.135.0): the question waits in the box, with its context, for the person to send.++ ask: (text) => { if (vibe.view === 'list') { vibe.pendingAsk = text; vibe.fresh = true; openVibe(); } else if (vibe.fill) vibe.fill(text); else vibe.pendingAsk = text; },};}root.OpenVibeDockVibe = { mount };@@@ -1,70 +1,62 @@--// ═══════════════════════════════════════════════════════════════--// OpenVibe — the Dock (Dock plan 2026-10-30, K-D1): your agents and your activity at the edge of every OpenVibe page--//--// navbar.js loads this file from beside itself for a signed-in person, after the bell, and mounts it:--// OpenVibeDock.mount({ token: () => jwt, apiBase, sibling }) the pill; mount(null) takes it away (signed out)--// OpenVibeDock.open({ tab }) · close() · isOpen()--//--// The pill (the mark, a word and a dot) shows only while an agent works, one needs you, or the person pinned it, in the--// corner the Dock sits in. It paints at once from this site's copy (localStorage ov-dock), then from the Network, and--// re-reads every 30 s while something runs and when the page comes back. A 404 (Network without the route yet) keeps--// { off } for a day, and navbar.js does not load this file until then.--//--// State: GET/PUT <apiBase>/api/v1/me/dock (network.dock-result@1, network.dock-state@1): open, tab, pinned and where it--// sits for each device class, so it follows the person from site to site. A change shows at once (onChange paints the--// pill and the site's copy) and reaches the Network a beat later; before Network has the route the copy stands.--// Tabs: Agents (action-center.js mount(): what needs you, then what runs, approve or decline in place; K-D3: above it,--// the person's own desk stream — dock-agent.js loads the first time the tab opens — with Curtain now, End and the still--// when nothing is live), Chats (K-D2: the person's conversations and DMs from OpenVibe.Chat's own API,--// GET /api/dm/conversations, opened in place, read and answered there) and Activity (notification-ui.js renderDock():--// the bell's list).--// Desktop: a floating card at the pill's corner. Drag its head to move it (it snaps to the nearest corner), drag the--// free corner to resize (320-720 × 360-900 px). It is not modal: the page stays usable. Phones: a sheet from the--// bottom over a scrim; drag the head down, ×, Escape or a tap outside closes it. Motion is transform and opacity only.--// Pop out (K-D4): on desktop Chromium the panel moves into a window of its own (Document Picture-in-Picture) — the same--// DOM, the same tabs, the page's own styles copied in; the page keeps the pill, which says where the Dock went.--// ═══════════════════════════════════════════════════════════════++// The Dock: navbar.js calls mount({ token, apiBase, sibling }) for the pill. open({ tab }) opens the floating card;++// mountInto(host, { tab }) places its panel body inside another sheet. Both use the same state, counts and loaders.(function (root) {'use strict';if (typeof document === 'undefined' || root.OpenVibeDock) return;-- const W = [320, 720], H = [360, 900], DEF = { width: 400, height: 560 };++ const W = [320, 720], H = [360, 900], DEF = { width: 420, height: 640 };const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;++ // Close is the OpenVibe menu's orb in its × form (11-09, the owner: "replace this close button with our own… the++ // corner button… its close form… nice animations"): nine dots that fold into an × as the Dock opens.++ // The close button is the menu button, already crossed (navbar.js, root.OpenVibeOrb): it forms as the sheet opens.++ const ORBX = () => (root.OpenVibeOrb ? root.OpenVibeOrb.svg(22, true) : '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7L7 17" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>');const G = {++ code: '<path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/>',spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',bell: '<path d="M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 2h-14z"/><path d="M10 20.5a2.2 2.2 0 0 0 4 0"/>',-- pin: '<path d="M14.5 3.5l6 6-3 1.5-3.5 3.5.5 4-1.5 1.5-4-4-4.5 4.5M9 15l-4-4 1.5-1.5 4 .5L14 6.5z"/>',++ // An upright pushpin whose lines never cross (owner, 11-09: the old one had "a line going through it").++ pin: '<path d="M7.5 3.5h9M10 3.5v5.3L6.5 13h11L14 8.8V3.5M12 13v7.5"/>',close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',open: '<path d="M13.5 5.5h5v5M18.5 5.5l-8 8M16.5 13.5v4a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h4"/>',chat: '<path d="M5 6.5h14a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-7l-4 3v-3H5A1.5 1.5 0 0 1 3.5 16V8A1.5 1.5 0 0 1 5 6.5z"/>',back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',++ // Three filled dots on the exact centre line (a stroked h.01 path drew them a hair off centre: the owner, 11-09).++ more: '<g fill="currentColor" stroke="none"><circle cx="5.5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="18.5" cy="12" r="1.7"/></g>',clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',};// Vibe first (L48 V2): the person's AI, on every site.-- const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Chats', G.chat], ['activity', 'Activity', G.bell]];++ const ROBOT = '<rect x="5" y="8" width="14" height="10" rx="3"/><path d="M12 8V4.5M9.5 13h.01M14.5 13h.01M3 12.5v2M21 12.5v2"/>'; // as in the menu's row++ const TABS = [['vibe', 'Vibe', G.spark], ['code', 'Code', G.code], ['agent', 'Agents', ROBOT], ['chats', 'Chat', G.chat], ['activity', 'Activity', G.bell]];// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a// newer one stays on this site's copy until the Network learns it.-- const SAVED_TABS = ['vibe', 'agent', 'chats', 'activity'];++ const SAVED_TABS = ['vibe', 'code', 'agent', 'chats', 'activity'];const DEFAULT_STATE = { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } } };-- let el = null, o = {}, doc = null, tab = 'activity', saveT = 0, pending = {}, lastFocus = null;++ let el = null, bodyEl = null, inline = null, panelEpoch = 0, o = {}, doc = null, tab = 'activity', saveT = 0, pending = {}, lastFocus = null, dockSurface = null;// Pop out (K-D4): the window the panel lives in while it is popped out, and whether it is in one.let pipWin = null, popped = false, pipClosing = false, popOk = true;const mounts = {};const phone = () => { try { return root.matchMedia('(max-width: 640px)').matches; } catch { return false; } };-- const reduced = () => { try { return root.matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.getAttribute('data-ov-motion') === 'reduced'; } catch { return false; } };++ // Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.++ const reduced = () => document.documentElement.getAttribute('data-ov-motion') === 'off';const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));const state = () => (doc && doc.state) || DEFAULT_STATE;const counts = () => (doc && doc.counts) || {};const kind = () => (phone() ? 'phone' : 'desktop');const place = () => ({ ...DEFAULT_STATE.place[kind()], ...((state().place || {})[kind()] || {}) });const sibling = (name) => (typeof o.sibling === 'function' ? o.sibling(name) : `https://openvibe.network/shared/${name}`);-- // K-D3: the agent stream comes from OpenVibe.Codes' API (it mints the view ticket with the person's own token).-- const agentBase = () => String(o.agentBase || 'https://openvibe.codes').replace(/\/+$/, '');++ // K-D3: the agent stream is OpenVibe.Codes' (it mints the view ticket with the person's own token) and the one++ // timeline behind the Dock is OpenVibe.Actor's (design 2026-11-07). On a site both go through Network's frame++ // relay: every site's connect-src allows openvibe.network and refuses openvibe.codes and openvibe.actor. A test++ // or the extension passes agentBase/actorBase and reaches the product itself; actorBase also names the Actor's++ // pages in the run list.++ const frameBase = () => String(o.frameBase || (String(o.apiBase || 'https://openvibe.network').replace(/\/+$/, '') + '/api/frame')).replace(/\/+$/, '');++ const directBase = (v) => (v ? String(v).replace(/\/+$/, '') : '');++ const actorBase = () => String(o.actorBase || 'https://openvibe.actor').replace(/\/+$/, '');async function call(path, init = {}) {const base = String(o.apiBase || 'https://openvibe.network').replace(/\/+$/, '');@@@ -96,12 +88,14 @@} catch { /* offline, or no route yet: the copy stands and the next change tries again */ }}++ // Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.++ const DOCK_REV = { 'dock-vibe.js': 'ee110f2a0c1d', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };function load(src, test) {return new Promise((ok) => {if (test()) return ok(true);const id = `ov-dock-${src.replace(/\W/g, '')}`;let sc = document.getElementById(id);-- if (!sc) { sc = document.createElement('script'); sc.id = id; sc.src = sibling(src); sc.async = true; document.head.appendChild(sc); }++ if (!sc) { sc = document.createElement('script'); sc.id = id; sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : ''); sc.async = true; document.head.appendChild(sc); }sc.addEventListener('load', () => ok(test()));sc.addEventListener('error', () => ok(false));});@@@ -112,7 +106,7 @@const st = document.createElement('style');st.id = 'ovdk-styles';st.textContent = `-- .ovdk-pill { --t: var(--accent, #3b82f6); --bg: var(--bg-elevated, var(--bg-secondary, #252530)); position: fixed; z-index: 9995; right: max(16px, env(safe-area-inset-right, 0px)); bottom: calc(max(16px, env(safe-area-inset-bottom, 0px)) + var(--ovnav-appbar-h, 0px));++ .ovdk-pill { --t: var(--accent, #3b82f6); --bg: var(--bg-elevated, var(--bg-secondary, #252530)); position: fixed; z-index: var(--ovb-z-pill, 9995); right: max(16px, env(safe-area-inset-right, 0px)); bottom: max(calc(16px + var(--ovb-dock, calc(max(var(--ovnav-appbar-h, 0px), env(safe-area-inset-bottom, 0px)) + var(--ovb-mini, 0px)))), var(--ovb-composer, 0px)); translate: 0 calc(-1 * var(--ovb-lift, 0px));display: inline-flex; align-items: center; gap: 9px; height: 46px; padding: 0 18px 0 9px; border-radius: 99px; border: 1px solid var(--border, #333340); background: var(--bg); color: var(--text-primary, #e0e0e0);font: inherit; font-size: 13.5px; font-weight: 700; letter-spacing: -.005em; box-shadow: 0 14px 34px -14px rgba(0,0,0,.6); cursor: pointer; -webkit-tap-highlight-color: transparent; animation: ovdk-in .3s cubic-bezier(.2,.9,.25,1.2); }.ovdk-pill[hidden] { display: none; }@@@ -130,9 +124,7 @@.ovdk-pill:has(.ovdk-t:empty) { padding-right: 8px; }@keyframes ovdk-in { from { opacity: 0; transform: translateY(10px) scale(.9); } }@keyframes ovdk-ping { to { transform: scale(2.6); opacity: 0; } }-- @media (prefers-reduced-motion: reduce) { .ovdk-pill, .ovdk-m::before { animation: none !important; } }-- html[data-ov-motion="reduced"] .ovdk-pill, html[data-ov-motion="reduced"] .ovdk-m::before { animation: none; }-- .ovdk { --ovdk-gap: 16px; position: fixed; z-index: 9996; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box;++ .ovdk { --ovdk-gap: 16px; position: fixed; z-index: var(--ovb-z-dock, 9996); display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box;right: max(var(--ovdk-gap), env(safe-area-inset-right, 0px)); bottom: calc(max(var(--ovdk-gap), env(safe-area-inset-bottom, 0px)) + var(--ovnav-appbar-h, 0px));width: ${DEF.width}px; height: ${DEF.height}px; max-width: calc(100vw - 32px); max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px);background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); color: var(--text-primary, #ececf1); border: 1px solid var(--border, rgba(255,255,255,.1));@@@ -148,6 +140,8 @@@keyframes ovdk-grow { from { opacity: 0; transform: scale(.82); } }@keyframes ovdk-rise { from { transform: translateY(100%); } }.ovdk svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }++ /* The menu button's drawing (navbar.js) has its own fills: the line style above is not its. */++ .ovdk svg.ovmb { fill: currentColor; stroke: none; stroke-width: 0; }.ovdk-head { position: relative; flex: none; display: flex; align-items: center; gap: 8px; padding: 10px 8px 8px 10px; cursor: grab; touch-action: none; user-select: none; }.ovdk-tabs { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #ececf1) 6%, transparent); min-width: 0; flex: 0 1 auto; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }.ovdk-tabs::-webkit-scrollbar { display: none; }@@@ -156,7 +150,7 @@color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }.ovdk-tabs button svg { width: 17px; height: 17px; flex: none; }.ovdk-tabs button:hover { color: var(--text-primary, #ececf1); }-- .ovdk-tabs button[aria-selected="true"] { background: var(--bg-primary, #12141b); color: var(--text-primary, #ececf1); box-shadow: 0 1px 3px rgba(0,0,0,.25); }++ .ovdk-tabs button[aria-selected="true"] { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, var(--accent, #8ab4ff)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent); }.ovdk-tabs button[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }.ovdk-tabs button:focus-visible, .ovdk-acts button:focus-visible, .ovdk-foot :focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 1px; }.ovdk-tabs .n { min-width: 18px; padding: 1px 6px; box-sizing: border-box; border-radius: 999px; font-size: 11px; font-weight: 800; line-height: 1.35; text-align: center; font-variant-numeric: tabular-nums;@@@ -166,9 +160,44 @@.ovdk-acts { display: flex; gap: 2px; margin-left: auto; }.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }-- .ovdk-acts button svg { width: 19px; height: 19px; }-- .ovdk-acts [data-act="pin"][aria-pressed="true"] { color: var(--accent-light, var(--accent, #60a5fa)); }-- .ovdk-acts [data-act="pin"][aria-pressed="true"] svg { fill: color-mix(in srgb, currentColor 22%, transparent); }++ .ovdk-acts button svg { width: 19px; height: 19px; display: block; }++ .ovdk-acts .ovdk-tomenu span { display: none; }++ .ovdk-orbx svg { width: 22px !important; height: 22px !important; overflow: visible; }++ .ovdk-orbx { --ovo-a: var(--accent-light, var(--accent, #60a5fa)); }++ .ovdk.is-in .ovdk-orbx .ovmb :is(.ovmb-bar, .ovmb-app) { animation: ovmb-form .5s cubic-bezier(.2,1.3,.3,1) both; }++ .ovdk-orbx:hover .ovmb, .ovdk-orbx:focus-visible .ovmb { transform: rotate(90deg); }++ .ovdk-orbx .ovmb { transition: transform .35s cubic-bezier(.2,1.3,.3,1); }++ .ovdk-orbx:active .ovmb { transform: rotate(90deg) scale(.8); }++ /* A phone's sheet opens out of the navbar's orb as the menu's does (4.186.0; see CHANGELOG): one move, one close button. */++ .ovdk.from-orb.is-in { animation: ovdk-reveal .38s cubic-bezier(.2,.9,.25,1) both; }++ .ovdk.from-orb.is-out { animation: ovdk-fold .28s cubic-bezier(.5,0,.75,.2) both; pointer-events: none; }++ @keyframes ovdk-reveal { from { clip-path: circle(28px at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } }++ @keyframes ovdk-fold { from { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(20px at var(--ovdk-ox) var(--ovdk-oy)); opacity: 0; } }++ .ovdk.from-orb .ovdk-head { padding: 4px 14px 6px 8px; }++ .from-orb .ovdk-acts .ovdk-orbx { position: relative; width: 40px; height: 40px; margin: 2px; border-radius: 50%; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, var(--bg-primary, #0b0f19));++ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 35%, transparent), 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); transition: background-color .3s, box-shadow .3s; }++ .from-orb .ovdk-acts .ovdk-orbx.is-from { background: transparent; box-shadow: none; }++ .from-orb.is-in .ovdk-orbx::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--accent-light, #60a5fa); pointer-events: none; animation: ovdk-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }++ @keyframes ovdk-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }++ .from-orb .ovdk-orbx { color: var(--text-primary, #ececf1); }++ .from-orb .ovdk-orbx svg { transition: transform .42s cubic-bezier(.2, 1.1, .3, 1); }++ .ovdk-orbx.is-set, .ovdk-orbx.is-set * { transition: none !important; }++ .from-orb .is-from svg { transform: translate(var(--orb-dx, 0), var(--orb-dy, 0)) scale(var(--orb-s, 1)); }++ .ovnav-orb.is-dock { transform: translate(var(--dock-dx, 0), var(--dock-dy, 0)) scale(var(--dock-s, 1)) !important; color: var(--text-primary, #e6edf7) !important; }++ .ovdk-more { position: absolute; z-index: 6; right: 8px; top: calc(100% - 4px); width: min(320px, calc(100% - 16px)); display: grid; gap: 2px; padding: 6px; border-radius: 16px;++ border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); box-shadow: 0 18px 40px -12px rgba(0,0,0,.55); cursor: default; }++ .ovdk-more[hidden] { display: none; }++ .ovdk-more button { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 12px; background: transparent; color: var(--text-primary, #ececf1); font: inherit; text-align: left; cursor: pointer; }++ .ovdk-more button[hidden] { display: none; }++ .ovdk-more button:hover, .ovdk-more button:focus-visible { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); outline: none; }++ .ovdk-more button > svg { flex: none; width: 20px; height: 20px; color: var(--text-muted, #8b8b96); }++ .ovdk-more span { flex: 1; min-width: 0; display: grid; gap: 2px; }++ .ovdk-more b { font-size: 14px; font-weight: 650; }++ .ovdk-more small { font-size: 12px; color: var(--text-muted, #8b8b96); line-height: 1.35; }++ .ovdk-sw { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--bg-tertiary, rgba(255,255,255,.14)); transition: background .15s; }++ .ovdk-sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }++ .ovdk-more [aria-checked="true"] .ovdk-sw { background: var(--accent-strong, var(--accent, #2563eb)); }++ .ovdk-more [aria-checked="true"] .ovdk-sw::after { transform: translateX(16px); }.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }.ovdk-panel[hidden] { display: none; }@@@ -186,7 +215,7 @@.ovdk-none { padding: 40px 24px; text-align: center; color: var(--text-muted, #8b8b96); font-size: 13.5px; line-height: 1.45; }.ovdk-size { position: absolute; z-index: 2; width: 22px; height: 22px; top: 0; left: 0; cursor: nwse-resize; touch-action: none; }.ovdk-size::after { content: ''; position: absolute; top: 9px; left: 9px; width: 7px; height: 7px; border-top: 2px solid var(--text-muted, #8b8b96); border-left: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 3px; opacity: 0; transition: opacity .15s; }-- .ovdk:hover .ovdk-size::after, .ovdk-size:active::after { opacity: .75; }++ .ovdk .ovdk-size::after { opacity: .75; }.ovdk[data-corner="bottom-left"] .ovdk-size { left: auto; right: 0; cursor: nesw-resize; }.ovdk[data-corner="bottom-left"] .ovdk-size::after { left: auto; right: 9px; border-left: 0; border-right: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 0; border-top-right-radius: 3px; }.ovdk[data-corner="top-right"] .ovdk-size { top: auto; bottom: 0; cursor: nesw-resize; }@@@ -195,10 +224,21 @@.ovdk[data-corner="top-left"] .ovdk-size::after { top: auto; bottom: 9px; left: auto; right: 9px; border: 0; border-bottom: 2px solid var(--text-muted, #8b8b96); border-right: 2px solid var(--text-muted, #8b8b96); border-radius: 0 0 3px 0; }@media (pointer: coarse) { .ovdk-acts button { width: 44px; height: 44px; } .ovdk-tabs button { height: 40px; } }.ovdk { container-type: inline-size; }-- @container (max-width: 440px) { .ovdk-tabs button { padding: 0 8px; gap: 6px; font-size: 13px; } .ovdk-tabs button svg { display: none; } .ovdk-acts { gap: 0; } .ovdk-acts button { width: 34px; } }++ @container (max-width: 440px) { .ovdk-tabs button { padding: 0 8px; gap: 6px; font-size: 13px; } .ovdk-acts { gap: 0; } .ovdk-acts button { width: 34px; } }++ /* A narrow Dock (Opus, 11-07: "Activity" was cut off at the strip's edge): the open tab keeps its icon and name,++ the others show their icon and count only, their names kept for screen readers. All four always fit. */++ @container (max-width: 480px) {++ .ovdk-tabs button:not([aria-selected="true"]) { padding: 0 10px; gap: 4px; }++ .ovdk-tabs button:not([aria-selected="true"]) span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }++ }.ovdk-muted { color: var(--text-muted, #8b8b96); }.ovdk-i { display: inline-grid; place-items: center; }.ovdk-i svg { width: 18px; height: 18px; }++ .ovdk-bar { flex: none; display: flex; justify-content: flex-end; gap: 4px; padding: 4px 8px 0; }++ .ovdk-ib { flex: none; width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 12px; background: transparent; color: var(--text-secondary, #b6b6c0); cursor: pointer; text-decoration: none; }++ .ovdk-ib svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; }++ .ovdk-ib:hover, .ovdk-ib:focus-visible { background: var(--hover-bg, rgba(127,127,127,.12)); color: var(--text-primary, #ececf1); }++ .ovdk-thread-h .ovdk-ib { margin-left: auto; }.ovdk-chats { list-style: none; margin: 0; padding: 6px; display: grid; gap: 2px; }.ovdk-chat { display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 12px; width: 100%; min-height: 60px; padding: 8px 10px; border: 0; border-radius: 14px;background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }@@@ -207,6 +247,11 @@.ovdk-av { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; overflow: hidden; font-weight: 800; font-size: 15px;color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }.ovdk-av img { width: 100%; height: 100%; object-fit: cover; }++ .ovdk-av-vibe { color: #fff; background: radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--accent, #3b82f6) 70%, #a78bfa), var(--accent-strong, var(--accent, #2563eb))); }++ .ovdk-av-vibe svg { width: 22px; height: 22px; }++ .ovdk-av-group { display: flex; align-items: center; overflow: visible; background: transparent; }++ .ovdk-av-group .ovdk-av { width: 27px; height: 27px; margin-left: -12px; border: 2px solid var(--bg-elevated, var(--bg-secondary, #1d1f27)); font-size: 11px; }++ .ovdk-av-group .ovdk-av:first-child { margin-left: 0; }.ovdk-chat-b { display: grid; gap: 1px; min-width: 0; }.ovdk-chat-b b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.ovdk-chat-p { font-size: 12.5px; color: var(--text-secondary, #b6b6c0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }@@@ -230,24 +275,53 @@border: 1px solid var(--border-light, var(--border, rgba(255,255,255,.14))); background: var(--bg-primary, #12141b); }.ovdk-send:focus-within { border-color: var(--accent, #3b82f6); }.ovdk-send textarea { flex: 1; min-width: 0; box-sizing: border-box; min-height: 36px; max-height: 140px; padding: 8px 0; resize: none; border: 0; outline: none; background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 14px; line-height: 1.4; }-- .ovdk-go { width: 40px; height: 40px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center;++ .ovdk-go { width: 44px; height: 44px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center;color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }.ovdk-go:disabled { opacity: .55; cursor: default; }.ovdk-err { margin: 0 14px; font-size: 12.5px; color: color-mix(in srgb, var(--live-red, #e5484d) 60%, var(--text-primary, #ececf1)); }.ovdk-err[hidden] { display: none; }.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }@media (max-width: 640px) {-- .ovdk, .ovdk[data-corner] { left: 0; right: 0; top: auto; bottom: 0; width: auto !important; height: min(86vh, 720px) !important; height: min(86dvh, 720px) !important; max-width: none; max-height: none;-- border-radius: 24px 24px 0 0; border-bottom: 0; padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: 0 0 0 100vmax rgba(0,0,0,.5), 0 -12px 40px rgba(0,0,0,.35); transform-origin: 50% 100%; }++ .ovdk, .ovdk[data-corner] { z-index: 10010; left: 0; right: 0; top: 0; bottom: var(--ov-kb, 0px); width: auto !important; height: auto !important; max-width: none; max-height: none;++ border: 0; border-radius: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: none; transform-origin: 50% 100%; }.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }-- .ovdk-head { padding-top: 22px; cursor: default; }-- .ovdk-head::before { content: ''; position: absolute; top: 8px; left: 50%; width: 40px; height: 4px; margin-left: -20px; border-radius: 2px; background: var(--border-light, var(--border, rgba(255,255,255,.22))); }++ .ovdk-head { cursor: default; }.ovdk-size { display: none; }.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }++ /* A thumb, not a cursor: every control on the sheet is 44 px (design 2026-11-07 §1, Phone). */++ .ovdk-tabs button { height: 44px; }++ .ovdk-acts button { width: 44px; height: 44px; }++ /* Two rows (4.150.0, the owner: the strip was "cut off", "Activity" read "Ac"): the tools sit beside the grab++ handle; the four tabs share the full width below, each icon over its name, every one whole. */++ .ovdk-head { flex-wrap: nowrap; gap: 4px; padding: 6px 6px 6px 8px; }++ .ovdk-acts { flex: none; gap: 0; }++ .ovdk-acts button[data-act="pop"] { display: none !important; }++ .ovdk-tabs { order: 0; flex: 1 1 auto; min-width: 0; padding: 3px; border-radius: 16px; }++ .ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { flex: 1 1 0; min-width: 0; height: 54px; padding: 4px 2px; flex-direction: column; justify-content: center; gap: 3px; border-radius: 12px; font-size: 11.5px; font-weight: 650; }++ .ovdk-tabs button span, .ovdk-tabs button:not([aria-selected="true"]) span { position: static; width: auto; height: auto; overflow: hidden; clip: auto; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }++ .ovdk-tabs button svg { width: 19px; height: 19px; }++ .ovdk-tabs .n { position: absolute; top: 4px; left: calc(50% + 6px); min-width: 17px; padding: 1px 5px; font-size: 10.5px; }++ .ovdk-foot a, .ovdk-foot button { min-height: 44px; }++ /* Two rows, as the menu's head (4.189.0): the way back to the menu, More and Close; under them the four tabs,++ full width, in the look of the menu's row that leads here. */++ .ovdk-head, .ovdk.from-orb .ovdk-head { flex-wrap: wrap; gap: 6px 4px; padding: 6px 12px 8px; }++ .ovdk-acts { order: 0; flex: 1 0 100%; align-items: center; gap: 2px; }++ .ovdk-acts .ovdk-tomenu { width: auto; margin-right: auto; padding: 0 14px 0 10px; gap: 8px; display: inline-flex; align-items: center; border-radius: 999px; font: inherit; font-size: 13.5px; font-weight: 700;++ color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }++ .ovdk-acts .ovdk-tomenu span { display: inline; }++ .ovdk-tabs { order: 1; flex: 1 0 100%; gap: 4px; padding: 0; border-radius: 0; background: transparent; overflow: visible; }++ .ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { height: 50px; gap: 2px; background: color-mix(in srgb, var(--accent, #3b82f6) 7%, transparent); }++ .ovdk-tabs button[aria-selected="true"] { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }++ .ovdk-tabs button:not([aria-selected="true"]) svg { color: var(--accent-light, var(--accent, #60a5fa)); }++ }++ @media (min-width: 641px) and (max-width: 1023px) {++ .ovdk, .ovdk[data-corner] { z-index: 10010; top: 0; bottom: 0; right: 0; left: auto; width: min(460px, calc(100vw - 48px)) !important; height: auto !important;++ max-width: none; max-height: none; border-radius: 22px 0 0 22px; border-right: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);++ box-shadow: 0 0 0 100vmax rgba(0,0,0,.4), -16px 0 40px rgba(0,0,0,.3); transform-origin: 100% 50%; }++ .ovdk-size { display: none; }++ .ovdk-head { cursor: default; }}html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }-- @media (prefers-reduced-motion: reduce) { .ovdk.is-in, .ovdk-wait span { animation: none; } }-- html[data-ov-motion="reduced"] .ovdk.is-in { animation: none; }// In a window of its own the panel is the window: full bleed, no card, no phone sheet..ovdk-pip .ovdk, .ovdk-pip .ovdk[data-corner] { left: 0 !important; top: 0 !important; right: 0 !important; bottom: 0 !important;width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important;@@@ -270,15 +344,20 @@<header class="ovdk-head"><div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div><div class="ovdk-acts">-- <button type="button" data-act="timer" title="A timer" aria-label="Start a timer">${svg(G.clock)}</button>-- <button type="button" data-act="pop" hidden title="Open the Dock in a window of its own" aria-label="Open the Dock in a window of its own">${svg(G.open)}</button>-- <button type="button" data-act="pin" aria-pressed="false" title="Keep the Dock's button on every page" aria-label="Keep the Dock's button on every page">${svg(G.pin)}</button>-- <button type="button" data-act="close" title="Close" aria-label="Close the Dock">${svg(G.close)}</button>++ <button type="button" data-act="menu" class="ovdk-tomenu" title="The OpenVibe menu" aria-label="The OpenVibe menu: every site, the feed, you">${root.OpenVibeOrb ? root.OpenVibeOrb.svg(20) : ''}<span>Menu</span></button>++ <button type="button" data-act="more" aria-haspopup="true" aria-expanded="false" title="More" aria-label="More: a timer, the Dock's button">${svg(G.more)}</button>++ <button type="button" data-act="close" class="ovdk-orbx" title="Close" aria-label="Close the Dock">${ORBX()}</button>++ </div>++ <div class="ovdk-more" role="menu" aria-label="More" hidden>++ <button type="button" role="menuitem" data-act="timer">${svg(G.clock)}<span><b>Start a timer</b><small>A countdown or a focus round, on the Dock's button</small></span></button>++ <button type="button" role="menuitemcheckbox" aria-checked="false" data-act="pin">${svg(G.pin)}<span><b>Keep the Dock's button on every page</b><small>When off, it shows only while something works or needs you</small></span><i class="ovdk-sw" aria-hidden="true"></i></button>++ <button type="button" role="menuitem" data-act="pop" hidden>${svg(G.open)}<span><b>Open the Dock in a window of its own</b><small>Keep it beside whatever you are doing</small></span></button></div></header><div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div><span class="ovdk-size" aria-hidden="true"></span>`;document.body.appendChild(el);++ bodyEl = el.querySelector('.ovdk-body');const tabs = [...el.querySelectorAll('[role="tab"]')];tabs.forEach((b) => b.addEventListener('click', () => setTab(b.dataset.tab, true)));@@@ -288,14 +367,39 @@if (j >= 0) { e.preventDefault(); setTab(tabs[j].dataset.tab, true); tabs[j].focus(); }});el.querySelector('[data-act="close"]').addEventListener('click', () => close());++ // The way back to the menu: this sheet folds, the menu opens out of the same button.++ { const m = el.querySelector('[data-act="menu"]'); if (m) { if (!root.OpenVibeNavbar || !root.OpenVibeOrb) m.hidden = true; m.addEventListener('click', () => { close(); setTimeout(() => { try { root.OpenVibeNavbar.openSwitcher(); } catch { /* no bar */ } }, 300); }); } }el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });-- el.querySelector('[data-act="timer"]').addEventListener('click', (e) => { const a = e.currentTarget; withTimer((t) => t.menu(a)); });++ el.querySelector('[data-act="timer"]').addEventListener('click', () => { const a = el.querySelector('[data-act="more"]'); closeMore(); withTimer((t) => t.menu(a)); });++ const moreBtn = el.querySelector('[data-act="more"]'), moreMenu = el.querySelector('.ovdk-more');++ function closeMore() { moreMenu.hidden = true; moreBtn.setAttribute('aria-expanded', 'false'); }++ moreBtn.addEventListener('click', () => {++ const open = moreMenu.hidden;++ closeMore();++ const tm = el.querySelector('.ovdk-tmenu'); if (tm) tm.remove();++ if (open) { moreMenu.hidden = false; moreBtn.setAttribute('aria-expanded', 'true'); const first = moreMenu.querySelector('button:not([hidden])'); if (first) first.focus(); }++ });++ moreMenu.addEventListener('click', (e) => { const b = e.target.closest('button'); if (b && b.dataset.act !== 'pin') closeMore(); });++ moreMenu.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); closeMore(); moreBtn.focus(); } });++ // A pop-over (More, the timer) closes on a tap anywhere else, in the Dock or on the page (owner, 11-09: "clicked off++ // something else, and it didn't actually close").++ const awayFrom = (e) => {++ const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];++ if (!moreMenu.hidden && !path.includes(moreMenu) && !path.includes(moreBtn)) closeMore();++ const tm = el.querySelector('.ovdk-tmenu');++ if (tm && !path.includes(tm) && !path.includes(moreMenu) && !path.includes(moreBtn)) tm.remove();++ };++ el.addEventListener('pointerdown', awayFrom, true);++ document.addEventListener('pointerdown', (e) => { if (!el.contains(e.target)) awayFrom(e); }, true);el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a// chat) leaves its target detached, and must not read as outside.document.addEventListener('click', (e) => {if (!isOpen() || !phone()) return;++ // The tap that opened the sheet (a page's button, the share sheet's OpenVibe Chat, Ask your agent) is still on++ // its way up when the sheet opens: an event made before the sheet opened is never "a tap outside" (4.145.1).++ if (e.timeStamp && e.timeStamp <= openedAt) return;const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];if (path.includes(el) || (pill && path.includes(pill)) || (e.target && e.target.isConnected === false)) return;close();@@@ -366,7 +470,7 @@setTimeout(() => { el.style.transition = ''; }, 300);};head.addEventListener('pointerdown', (e) => {-- if (e.button || e.target.closest('.ovdk-acts')) return;++ if (e.button || e.target.closest('.ovdk-acts, .ovdk-more')) return;s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);});@@@ -410,62 +514,92 @@n('agent', agent, c.waiting ? 'waiting' : '');n('activity', c.unread || 0);n('chats', chats.unread || 0);-- el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chats, ${chats.unread} unread` : 'Chats');++ el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chat, ${chats.unread} unread` : 'Chat');el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');}function paintPin() {const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;-- b.setAttribute('aria-pressed', String(on));-- const t = on ? "The Dock's button stays on every page" : "Keep the Dock's button on every page";-- b.title = t; b.setAttribute('aria-label', t);++ b.setAttribute('aria-checked', String(on));++ b.title = on ? "On: the Dock's button stays on every page" : "Off: the Dock's button shows only while something works or needs you";}function setTab(id, remember) {++ if (id === 'agents') id = 'agent';++ if (inline && tab !== id) disposePanel();tab = TABS.some(([t]) => t === id) ? id : 'activity';if (tab !== 'chats') clearTimeout(chats.timer);if (tab !== 'vibe' && vibe.api) vibe.api.pause();el.querySelectorAll('[role="tab"]').forEach((b) => { const on = b.dataset.tab === tab; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });-- el.querySelectorAll('[data-panel]').forEach((p) => { p.hidden = p.dataset.panel !== tab; });++ bodyEl.querySelectorAll('[data-panel]').forEach((p) => { p.hidden = p.dataset.panel !== tab; });if (remember && state().tab !== tab && SAVED_TABS.includes(tab)) save({ tab });fill(tab);++ if (inline && tab === 'chats') readUnread();++ }++ const panelActive = (id) => tab === id && (isOpen() || !!inline);++ function disposePanel() {++ panelEpoch++;++ clearTimeout(chats.timer); clearTimeout(chats.unreadT);++ if (vibe.api) { vibe.api.pause(); vibe.api = null; }++ for (const k of Object.keys(mounts)) { if (mounts[k] && mounts[k].destroy) mounts[k].destroy(); delete mounts[k]; }++ if (root.OpenVibeNotifications && root.OpenVibeNotifications.unmountDock && bodyEl) root.OpenVibeNotifications.unmountDock(bodyEl.querySelector('[data-panel="activity"]'));++ chats.panel = null; chats.open = null; vibe.panel = null;++ if (bodyEl) bodyEl.querySelectorAll('[data-panel]').forEach((p) => { p.replaceChildren(); p.hidden = true; });}async function fill(id) {-- const panel = el.querySelector(`[data-panel="${id}"]`);++ const epoch = panelEpoch;++ const panel = bodyEl.querySelector(`[data-panel="${id}"]`);if (id === 'vibe') {vibe.panel = panel;if (vibe.api) return vibe.api.refresh();panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));++ if (epoch !== panelEpoch || !panelActive(id)) return;if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }-- vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });++ vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => panelActive('vibe'), state, save, wantList: () => { const v = vibeOnList; vibeOnList = false; return v; }, styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });return;}-- if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }-- if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); return; }++ if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; if (chats.draft) return chatList(); return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }++ // The Agent panel reads the step.++ if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); if (mounts[id + 'Timeline']) mounts[id + 'Timeline'].refresh(); return; }mounts[id] = { refresh: null };panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';++ if (id === 'code') {++ const ok = await load('dock-code.js', () => !!(root.OpenVibeDockCode && root.OpenVibeDockCode.mount));++ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }++ if (!ok) return failed(panel, id);++ mounts[id] = root.OpenVibeDockCode.mount(panel, { token: o.token, frameBase: frameBase(), active: () => panelActive('code') });++ return;++ }if (id === 'agent') {-- const ok = await load('action-center.js', () => !!(root.OpenVibeActionCenter && root.OpenVibeActionCenter.mount));++ const ok = await load('dock-runs.js', () => !!(root.OpenVibeDockRuns && root.OpenVibeDockRuns.mount));++ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }if (!ok) return failed(panel, id);// K-D3: the person's own agent stream above the list; it hides itself when there is none.-- panel.innerHTML = `<div class="ovdk-agent"></div><div class="ovdk-scroll"></div><div class="ovdk-foot"><button type="button" data-act="all">${svg(G.open)}Scheduled, done and failed</button><a href="https://openvibe.actor/">Give an agent a task →</a></div>`;++ panel.innerHTML = '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div><div class="ovdk-scroll"></div><div class="ovdk-runs-foot ovdk-foot"></div>';++ // The Agent panel: Now, Needs you and the one timeline (design 2026-11-07). It is first in the tab, so on a++ // phone Now and Needs you are on screen before anything else.++ load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {++ if (on && epoch === panelEpoch && !mounts[id + 'Timeline'] && panel.isConnected && panelActive(id)) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), active: () => panelActive('agent'), focus: () => o.task || null });++ });++ // Actor v3 (A3): your Actor first, on every site; it shows nothing for a person without one.++ load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {++ if (on && epoch === panelEpoch && !mounts[id + 'Actor'] && panel.isConnected && panelActive(id)) mounts[id + 'Actor'] = root.OpenVibeDockActor.mount(panel.querySelector('.ovdk-actor'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), networkBase: o.apiBase || 'https://openvibe.network', active: () => panelActive('agent') });++ });load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {-- if (on && !mounts[id + 'View'] && panel.isConnected) mounts[id + 'View'] = root.OpenVibeDockAgent.mount(panel.firstChild, { token: o.token, base: agentBase(), active: () => isOpen() && tab === 'agent' });++ if (on && epoch === panelEpoch && !mounts[id + 'View'] && panel.isConnected && panelActive(id)) mounts[id + 'View'] = root.OpenVibeDockAgent.mount(panel.querySelector('.ovdk-agent'), { token: o.token, base: directBase(o.agentBase), frameBase: frameBase(), active: () => panelActive('agent') });});-- const m = root.OpenVibeActionCenter.mount(panel.querySelector('.ovdk-scroll'), {-- token: o.token, apiBase: o.apiBase, scopes: o.scopes, buckets: ['waiting', 'running'],++ const m = root.OpenVibeDockRuns.mount(panel.querySelector('.ovdk-scroll'), {++ call, state, save, scopes: o.scopes, active: () => panelActive('agent'), actorBase: actorBase(), loadMarks: () => load('ai-marks-client.js', () => !!root.OpenVibeAIMarks),++ styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'),++ foot: panel.querySelector('.ovdk-runs-foot'),onCounts: (c) => { doc = { ...(doc || {}), state: state(), counts: { ...counts(), running: c.running || 0, waiting: c.waiting || 0 } }; badges(); notify(); },});mounts[id] = m;-- panel.querySelector('[data-act="all"]').addEventListener('click', (e) => {-- const anchor = e.currentTarget;-- close();-- try { root.OpenVibeActionCenter.open({ token: o.token, apiBase: o.apiBase, scopes: o.scopes, anchor: (pill && !pill.hidden) ? pill : anchor }); } catch { /* */ }-- });return;}const ok = await load('notification-ui.js', () => !!(root.OpenVibeNotifications && root.OpenVibeNotifications.renderDock));++ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }if (!ok) return failed(panel, id);const N = root.OpenVibeNotifications;if (!N.__ovNavInit) { try { N.init({ token: o.token, apiBase: o.apiBase || 'https://openvibe.network', scopes: o.scopes }); N.__ovNavInit = true; } catch { /* */ } }@@@ -482,33 +616,36 @@// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.-- const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, unread: 0, unreadT: 0, busy: false };++ const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, edits: null, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');const me = () => String(o.me || '');const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;const canWriteChat = () => !scopes() || scopes().includes('chat.write');const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };++ const toChat = (href) => { const a = h('a', 'ovdk-ib ovdk-away'); a.href = href; a.target = '_blank'; a.rel = 'noopener'; a.title = 'Open in Chat'; a.setAttribute('aria-label', 'Open in Chat'); a.setAttribute('data-ov-tip', 'Open in Chat'); a.innerHTML = svg('<path d="M13.5 5.5h5v5M18.5 5.5l-7.5 7.5M17 13.5v4a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 5 17.5v-9A1.5 1.5 0 0 1 6.5 7h4"/>'); return a; };async function chatCall(path, init = {}) {const t = await tokenOf();const headers = { accept: 'application/json', ...(init.body ? { 'content-type': init.type || 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };const { type: _type, ...rest } = init;return fetch(chatBase() + path, { ...rest, headers, credentials: 'omit' }).then(async (r) => {const b = await r.json().catch(() => ({}));-- if (!r.ok) { const e = new Error(b.error || `HTTP ${r.status}`); e.status = r.status; throw e; }++ if (!r.ok) { const e = new Error(b.error || `HTTP ${r.status}`); e.status = r.status; e.code = b.code || null; throw e; }return b;});}function readUnread() {clearTimeout(chats.unreadT);chatCall('/api/dm/unread').then((b) => { chats.unread = Number(b && b.unread) || 0; badges(); }).catch(() => { /* Chat away: no count */ });-- if (isOpen()) chats.unreadT = setTimeout(readUnread, 30000);++ if (panelActive('chats')) chats.unreadT = setTimeout(readUnread, 30000);}const when = (iso) => {const t = Date.parse(iso || ''); if (!Number.isFinite(t)) return '';const m = Math.max(0, Math.round((Date.now() - t) / 60000));return m < 1 ? 'now' : m < 60 ? `${m} min` : m < 1440 ? `${Math.floor(m / 60)} h` : `${Math.floor(m / 1440)} d`;};++ // "answered 4 min ago", and "answered just now" when it was this minute.++ const answered = (iso) => { const w = when(iso); return !w ? '' : w === 'now' ? 'answered just now' : `answered ${w} ago`; };function avatar(p, group) {const a = h('span', 'ovdk-av');const url = p && /^https:\/\//.test(String(p.avatar_url || '')) ? p.avatar_url : '';@@@ -516,30 +653,54 @@else a.textContent = group ? '#' : String((p && (p.display_name || p.username)) || '?').trim().charAt(0).toUpperCase() || '?';return a;}++ function groupAvatar(c) {++ const people = others(c).slice(0, 3);++ if (!people.length) return avatar({}, false);++ const stack = h('span', 'ovdk-av ovdk-av-group');++ people.forEach((p) => stack.append(avatar(p, false)));++ return stack;++ }const others = (c) => (c.participants || []).filter((p) => p.username !== me());const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';-- function chatFoot(panel) {-- const foot = h('div', 'ovdk-foot');-- const a = h('a', '', 'Open Chat →'); a.href = `${chatBase()}/messages`; a.target = '_blank'; a.rel = 'noopener';-- foot.append(h('span', 'ovdk-muted', 'Your conversations and DMs'), a);-- panel.append(foot);-- }async function chatList() {clearTimeout(chats.timer);++ if (chats.edits) { chats.edits.stop(); chats.edits = null; }chats.open = null;const panel = chats.panel; if (!panel) return;if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }-- try { chats.list = (await chatCall('/api/dm/conversations')).conversations || []; } catch (e) { panel.textContent = ''; panel.append(h('div', 'ovdk-none', e.status === 401 ? 'Sign in to Chat to see your conversations.' : 'Your chats did not load. Check your connection, then open them again.')); chatFoot(panel); return; }-- if (chats.open || tab !== 'chats') return;++ const sharing = chats.draft && canWriteChat() ? load('dock-share.js', () => !!(root.OpenVibeDockShare && root.OpenVibeDockShare.bar)) : null;++ try {++ const [dm, ai] = await Promise.all([chatCall('/api/dm/conversations'), chatCall('/api/vibe/conversations').catch(() => null)]);++ chats.list = dm.conversations || [];++ chats.vibeSummary = ai && ai.count ? { kind: 'vibe', count: ai.count, latest: ai.conversations && ai.conversations[0], last_message_at: ai.conversations && ai.conversations[0] && ai.conversations[0].last_message_at } : null;++ } catch (e) { if (panel !== chats.panel || !panelActive('chats')) return; panel.textContent = ''; panel.append(h('div', 'ovdk-none', e.status === 401 ? 'Sign in to Chat to see your conversations.' : 'Your chats did not load. Check your connection, then open them again.')); chatFoot(panel); return; }++ if (sharing) await sharing;++ if (panel !== chats.panel || !panelActive('chats') || chats.open) return;panel.textContent = '';++ if (chats.draft && canWriteChat() && root.OpenVibeDockShare) panel.append(root.OpenVibeDockShare.bar({ draft: chats.draft, h, chatCall, host: el, cancel: () => { chats.draft = null; chatList(); }, open: (id) => { chats.list = null; return openChat(String(id), true); } }));++ // Open in Chat, as an icon over your conversations (ask 336).++ const bar = h('div', 'ovdk-bar'); bar.append(toChat('https://openvibe.chat/messages')); panel.append(bar);const scroll = h('div', 'ovdk-scroll');-- if (!chats.list.length) {++ if (!chats.list.length && !chats.vibeSummary) {const none = h('div', 'ovdk-none');none.append(h('b', '', 'No conversations yet'), document.createTextNode('A message to someone on OpenVibe starts one, and it shows here on every site.'));scroll.append(none);} else {const ul = h('ul', 'ovdk-chats');-- chats.list.forEach((c) => {++ [...chats.list.filter((c) => !chats.filter || titleOf(c).toLowerCase().includes(chats.filter)), ...(chats.vibeSummary ? [chats.vibeSummary] : [])].sort((a, b) => String(b.last_message_at || b.updated_at || '').localeCompare(String(a.last_message_at || a.updated_at || ''))).forEach((c) => {++ if (c.kind === 'vibe') {++ const li = h('li'), b = h('button', 'ovdk-chat'); b.type = 'button';++ const body = h('span', 'ovdk-chat-b'); body.append(h('b', '', 'Vibe'), h('span', 'ovdk-chat-p', `${c.count} chat${c.count === 1 ? '' : 's'}${c.latest && c.latest.name ? ` · ${c.latest.name}` : ''}`));++ const meta = h('span', 'ovdk-chat-m'); const at = c.latest && c.latest.last_answer_at;++ const t = h('time', '', when(at || c.last_message_at)); if (at) t.title = answered(at); meta.append(t);++ const av = h('span', 'ovdk-av ovdk-av-vibe'); av.append(icon(G.vibe));++ b.append(av, body, meta); b.setAttribute('aria-label', 'Open Vibe');++ b.addEventListener('click', () => {++ if (!b.dispatchEvent(new CustomEvent('openvibe:open-vibe', { bubbles: true, cancelable: true }))) return;++ vibeOnList = true; save({ vibe: null }); setTab('vibe', true);++ });++ li.append(b); ul.append(li); return;++ }const li = h('li'), b = h('button', 'ovdk-chat');b.type = 'button';const unread = Number(c.unread_count) || 0;@@@ -549,7 +710,7 @@const meta = h('span', 'ovdk-chat-m');meta.append(h('time', '', when(c.last_message_at || c.updated_at)));if (unread) meta.append(h('b', 'ovdk-unread', unread > 99 ? '99+' : String(unread)));-- b.append(avatar(others(c)[0] || {}, !!c.is_group), body, meta);++ b.append(c.is_group ? groupAvatar(c) : avatar(others(c)[0] || {}, false), body, meta);b.setAttribute('aria-label', `${titleOf(c)}${unread ? `, ${unread} unread` : ''}`);if (unread) li.className = 'is-unread';b.addEventListener('click', () => openChat(String(c.id), true));@@@ -558,20 +719,27 @@scroll.append(ul);}panel.append(scroll);-- chatFoot(panel);}function bubble(m, prev) {const mine = m.username === me();const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));++ li.dataset.id = String(m.id);li.append(h('span', 'ovdk-text', String(m.message || '')));++ if (m.edited_at) li.append(h('small', 'ovdk-edited', `edited · ${String(m.edited_at).slice(11, 16)}`));++ if (mine && canWriteChat()) { const edit = h('button', 'ovk-btn ovk-btn--quiet ovdk-edit', 'Edit'); edit.type = 'button'; li.append(edit); }++ // A known OpenVibe link's card (Chat's link cards): dock-card.js draws it, loaded the first time one shows.++ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { const c = ok && root.OpenVibeDockCard(m.card, h); if (c) li.append(c); });li.title = new Date(m.created_at).toLocaleString();return li;}async function openChat(id, remember) {clearTimeout(chats.timer);++ if (chats.edits) { chats.edits.stop(); chats.edits = null; }const panel = chats.panel; if (!panel) return;chats.open = String(id); chats.msgs = []; chats.last = 0;++ const draft = chats.draft && canWriteChat() ? chats.draft.text : '';++ chats.draft = null;if (remember) save({ chat: chats.open });const conv = (chats.list || []).find((c) => String(c.id) === chats.open);panel.textContent = '';@@@ -579,7 +747,7 @@const back = h('button', 'ovdk-back'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your chats'); back.append(icon(G.back));back.addEventListener('click', () => { save({ chat: null }); chatList(); });const title = h('b', '', conv ? titleOf(conv) : 'Conversation');-- head.append(back, title);++ head.append(back, title, toChat('https://openvibe.chat/messages'));// Opened from the Dock's state on another page: the list is not here yet, so the title comes with it.if (!conv) chatCall('/api/dm/conversations').then((b) => { chats.list = b.conversations || []; const c = chats.list.find((x) => String(x.id) === String(id)); if (c && chats.open === String(id)) title.textContent = titleOf(c); }).catch(() => { /* the title stays */ });const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');@@@ -594,6 +762,7 @@panel.append(form);const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };box.addEventListener('input', fit);++ if (draft && root.OpenVibeDockShare) root.OpenVibeDockShare.ready({ draft, h, panel, form, box, fit });box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });form.addEventListener('submit', async (e) => {e.preventDefault();@@@ -615,17 +784,18 @@list.append(h('li', 'ovdk-wait', null));list.firstChild.innerHTML = '<span></span>';await pull(list, scroll, true);-- if (canWriteChat()) setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);++ load('dock-chat-edits.js', () => !!root.OpenVibeDockChatEdits).then((ok) => { if (ok && chats.open === String(id)) chats.edits = root.OpenVibeDockChatEdits({ id: String(id), list, messages: chats.msgs, error: err, tokenOf, chatCall, chatBase, active: () => chats.open === String(id) && panelActive('chats'), load, h }); });++ if (canWriteChat() && !(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); if (draft) box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } }, 0);}/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */async function pull(list, scroll, first) {const id = chats.open;let got = [];try { got = (await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages?limit=50${chats.last ? `&after=${chats.last}` : ''}`)).messages || []; } catch (e) {-- if (first) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 403 ? 'This conversation is not yours.' : 'The messages did not load. Try again.')); }++ if (first && panelActive('chats')) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 403 ? 'This conversation is not yours.' : 'The messages did not load. Try again.')); }return;}-- if (id !== chats.open) return;++ if (id !== chats.open || !panelActive('chats')) return;if (first) list.textContent = '';const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;got.forEach((m) => { if (chats.msgs.some((x) => x.id === m.id)) return; list.append(bubble(m, chats.msgs[chats.msgs.length - 1])); chats.msgs.push(m); chats.last = Math.max(chats.last, Number(m.id) || 0); });@@@ -633,13 +803,15 @@if (first || atEnd) scroll.scrollTop = scroll.scrollHeight;if (got.length || first) chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/read`, { method: 'POST', body: '{}' }).then(readUnread).catch(() => { /* */ });clearTimeout(chats.timer);-- if (isOpen() && tab === 'chats' && chats.open === id) chats.timer = setTimeout(() => pull(list, scroll), 5000);++ if (panelActive('chats') && chats.open === id) chats.timer = setTimeout(() => pull(list, scroll), 5000);}// ── Vibe (plan L48 V2): the person's Vibe, in dock-vibe.js, loaded the first time the tab opens (the Dock is on// every page for signed-in people; Vibe's conversation and mic are not needed until then). Alt+V talks to Vibe from// any page: it opens the Dock on Vibe and starts listening (only where the browser can listen).const vibe = { panel: null, api: null };++ // The Chats tab's AI chats row opens the Vibe tab on its list, not on the last conversation.++ let vibeOnList = false;const canListen = () => !!(root.SpeechRecognition || root.webkitSpeechRecognition);document.addEventListener('keydown', (e) => {if (!e.altKey || e.ctrlKey || e.metaKey || String(e.key).toLowerCase() !== 'v' || !canListen() || !o || typeof o.token !== 'function') return;@@@ -653,14 +825,14 @@function withTimer(fn) {if (timerMod) return fn(timerMod);timerLoading = timerLoading || load('dock-timer.js', () => !!(root.OpenVibeDockTimer && root.OpenVibeDockTimer.attach)).then((ok) => {-- if (ok && !timerMod) timerMod = root.OpenVibeDockTimer.attach({ state, save, host: () => el, repaint: () => paintPill() });++ if (ok && !timerMod) timerMod = root.OpenVibeDockTimer.attach({ state, save, host: () => inline || el, repaint: () => paintPill() });return timerMod;});return timerLoading.then((m) => (m ? fn(m) : null));}// ── The pill ─────────────────────────────────────────────-- let pill = null, readT = 0, bound = false;++ let pill = null, readT = 0, bound = false, openedAt = 0;function store(v) {try { if (v === undefined) return JSON.parse(localStorage.getItem('ov-dock') || 'null'); localStorage.setItem('ov-dock', JSON.stringify(v)); } catch { /* */ }return null;@@@ -702,7 +874,7 @@const next = r.ok ? await r.json() : null;if (!next || !next.state || !pill) return;update(next);-- if (!isOpen() && next.state.open && !phone()) open();++ if (!isOpen() && !inline && next.state.open && !phone()) open();const c = next.counts || {};if (c.running || c.waiting || next.state.timer) readT = setTimeout(read, 30000);} catch { /* offline: the copy stands */ }@@@ -710,6 +882,7 @@/** The pill for a signed-in person (navbar.js), or none (null: signed out, or the site turned the Dock off). */function mount(opts) {if (!opts) {++ if (mountHandle) mountHandle.destroy();clearTimeout(readT);if (pill) { pill.remove(); pill = null; }if (isOpen()) close();@@@ -747,7 +920,7 @@if (!b) return;b.hidden = !pip();const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';-- b.title = t; b.setAttribute('aria-label', t);++ b.title = t; const name = b.querySelector('b'); if (name) name.textContent = t;}/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */@@@ -809,8 +982,54 @@}// ── Open and close ───────────────────────────────────────++ // Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.++ function ask({ text = '' } = {}, n = 0) { if (!n) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }function isOpen() { return !!(el && !el.hidden); }++ // A phone's sheet and the navbar's orb are one button (.from-orb).++ let orbLink = null, closing = 0;++ function navOrb() {++ if (!phone()) return null;++ const list = document.querySelectorAll('.openvibe-navbar .ovnav-orb');++ for (let i = 0; i < list.length; i++) { const r = list[i].getBoundingClientRect(); if (r.width && r.left > root.innerWidth / 2 && r.top < 120 && r.bottom > 0) return list[i]; }++ return null;++ }++ function fromOrb() {++ unlinkOrb();++ const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');++ if (!nav || !mineSvg || reduced() || popped) return;++ el.classList.add('from-orb'); // first: it sets the header's layout, and the close orb is measured where it will rest++ const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();++ if (!a.width || !b.width) { el.classList.remove('from-orb'); return; }++ const ax = a.left + a.width / 2, ay = a.top + a.height / 2, bx = b.left + b.width / 2, by = b.top + b.height / 2, s = a.width / b.width;++ el.style.setProperty('--ovdk-ox', `${Math.round(ax - box.left)}px`); el.style.setProperty('--ovdk-oy', `${Math.round(ay - box.top)}px`);++ mine.style.setProperty('--orb-dx', `${(ax - bx).toFixed(1)}px`); mine.style.setProperty('--orb-dy', `${(ay - by).toFixed(1)}px`); mine.style.setProperty('--orb-s', s.toFixed(3));++ nav.style.setProperty('--dock-dx', `${(bx - ax).toFixed(1)}px`); nav.style.setProperty('--dock-dy', `${(by - ay).toFixed(1)}px`); nav.style.setProperty('--dock-s', (1 / s).toFixed(3));++ mine.classList.add('is-set', 'is-from');++ orbLink = { nav, mine };++ void mine.offsetWidth; // the starting pose is put on, not animated into++ mine.classList.remove('is-set');++ void mine.offsetWidth;++ nav.style.transition = 'transform .42s cubic-bezier(.2, 1.1, .3, 1), color .3s';++ root.requestAnimationFrame(() => { if (!orbLink || orbLink.mine !== mine) return; mine.classList.remove('is-from'); nav.classList.add('is-dock'); });++ }++ function unlinkOrb() {++ if (closing) { clearTimeout(closing); closing = 0; }++ if (el) el.classList.remove('from-orb', 'is-out');++ if (!orbLink) return;++ orbLink.nav.classList.remove('is-dock'); orbLink.nav.style.transition = ''; orbLink.mine.classList.remove('is-from', 'is-set');++ orbLink = null;++ }++ function share({ text = '', title = '' } = {}) {++ const t = String(text || '').trim().slice(0, 4000);++ if (!t) return;++ chats.draft = { text: t, title: String(title || '').trim().slice(0, 140) }; chats.open = null;++ // A bar left from an earlier share would show the old words until the list reloads: it goes now.++ const stale = chats.panel && chats.panel.querySelector('.ovdk-share'); if (stale) stale.remove();++ open({ tab: 'chats' });++ }function open(opts = {}) {++ if (mountHandle) mountHandle.destroy();++ openedAt = typeof performance !== 'undefined' && performance.now ? performance.now() : 0;o = { ...o, ...opts };if (opts.doc) doc = opts.doc;if (!el) build();@@@ -822,8 +1041,18 @@paintPin();paintPop();el.hidden = false;-- el.classList.remove('is-in'); void el.offsetWidth; el.classList.add('is-in');-- if (phone()) { el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open'); } else el.removeAttribute('aria-modal');++ el.classList.remove('is-in');++ fromOrb();++ void el.offsetWidth; el.classList.add('is-in');++ // On a computer it grows out of its pill; a phone's sheet opens out of the navbar's orb (fromOrb), or rises.++ if (!phone() && root.OpenVibeNavbar && root.OpenVibeNavbar.pop) {++ const pill = document.getElementById('ovdk-pill');++ root.OpenVibeNavbar.pop(el, pill?.offsetWidth ? pill : null);++ }++ if (phone()) {++ el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open');++ if (root.OVSurfaces && !dockSurface) { dockSurface = root.OVSurfaces.sheet({ el, body: el.querySelector('.ovdk-body'), close }); dockSurface.open(); }++ } else { el.removeAttribute('aria-modal'); if (dockSurface) { dockSurface.destroy(); dockSurface = null; } }setTab(want, false);readUnread();if (!state().open || (state().tab !== tab && SAVED_TABS.includes(tab))) save({ open: true, ...(SAVED_TABS.includes(tab) ? { tab } : {}) }); else notify();@@@ -831,16 +1060,29 @@}function close() {if (!isOpen()) return;++ // Fold back into the navbar's orb first, then go.++ if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {++ el.classList.remove('is-in'); el.classList.add('is-out');++ orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');++ closing = setTimeout(() => { closing = 0; shut(); }, 280);++ return;++ }++ shut();++ }++ function shut(keepState = false) {++ if (!isOpen()) return;++ unlinkOrb();const wasPopped = popped;if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }const inside = el.contains(document.activeElement);el.hidden = true;++ if (dockSurface) { dockSurface.destroy(); dockSurface = null; }el.style.transform = '';el.removeAttribute('aria-modal');document.documentElement.classList.remove('ovdk-sheet-open');clear(0);clearTimeout(chats.timer); clearTimeout(chats.unreadT);-- save({ open: false });++ if (!keepState) save({ open: false });if (inside) {const back = pill && !pill.hidden ? pill : (lastFocus && lastFocus.isConnected ? lastFocus : null);try { if (back) back.focus({ preventScroll: true }); } catch { /* */ }@@@ -859,5 +1101,29 @@if (isOpen()) layout();}-- root.OpenVibeDock = { mount, open, close, isOpen, update, _state: () => ({ doc, tab, pending }) };++ let mountHandle = null;++ /** Place the same panel body under another sheet's head. The helper and its layout load on first use. */++ function mountInto(host, { tab: first = 'activity' } = {}) {++ if (!(host instanceof Element)) throw new TypeError('mountInto needs a host element');++ if (mountHandle) mountHandle.destroy();++ if (isOpen()) { clearTimeout(closing); closing = 0; shut(true); }++ let api = null, gone = false, wanted = first;++ const handle = {++ show(id) { if (gone) return; wanted = id; if (api) api.show(id); },++ destroy() { if (gone) return; gone = true; if (api) api.destroy(); if (mountHandle === handle) mountHandle = null; },++ };++ mountHandle = handle;++ load('dock-mount.js', () => !!root.OpenVibeDockMount).then((ok) => {++ if (ok && !gone) api = root.OpenVibeDockMount.mount(host, wanted, {++ start(wrap) { if (!el) build(); inline = wrap; wrap.append(bodyEl); },++ show(id) { setTab(id, false); badges(); },++ end() { disposePanel(); inline = null; if (el) el.insertBefore(bodyEl, el.querySelector('.ovdk-size')); paintPill(); },++ });++ });++ return handle;++ }++++ root.OpenVibeDock = { mount, mountInto, open, close, isOpen, update, ask, share,++ filterChats: (v) => { chats.filter = v.toLowerCase(); chatList(); },++ more: a => el.querySelector(`[data-act="${a}"]`).click(), canSend: () => canWriteChat(), _state: () => ({ doc, tab, pending }) };})(typeof window !== 'undefined' ? window : globalThis);@@@ -51,7 +51,7 @@--ovk-radius-sm: var(--ov-radius-1);--ovk-tap: 44px;--ovk-top: var(--ovnav-real-h, 52px); /* the navbar's height: sticky things stop under it */-- --ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */++ --ovk-bottom: var(--ovb-dock, var(--ovnav-appbar-h, 0px)); /* the app bar (and a small player above it) on phones, measured by bottom.js */--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);}@@@ -63,9 +63,16 @@:is([data-theme=neon-tokyo], [data-theme=vapor], [data-theme=matrix], [data-theme=hacker], [data-theme=high-contrast], [data-theme=dracula], [data-theme=monokai], [data-theme=abyss]) {--ov-dur-1: .09s; --ov-dur-2: .15s; --ov-dur-3: .24s; --ov-dur-4: .42s; --ov-ease-spring: cubic-bezier(.3, 1.8, .6, 1);}--html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; }++/* Motion (4.183.0; the owner, 2026-11-10: "it should still have full motion but actually just reduce it instead of++ getting rid of it"). Calm keeps every effect and makes it smaller and slower: the calm feel's lengths, no++ overshoot, and --ov-move, the factor a moving thing multiplies its distance by (calc(12px * var(--ov-move, 1))).++ Only Off stops motion. theme-loader.js writes data-ov-motion (reduced for Calm, the device's own setting counted++ in; off; full); the media rule serves a page the loader has not reached. */++:where(html) { --ov-move: 1; }++html[data-ov-motion=reduced] { --ov-move: .35; --ov-dur-1: .15s; --ov-dur-2: .25s; --ov-dur-3: .4s; --ov-dur-4: .62s; --ov-ease-spring: var(--ov-ease-out); }++@media (prefers-reduced-motion: reduce) { html:not([data-ov-motion]) { --ov-move: .35; --ov-ease-spring: var(--ov-ease-out); } }/* :root:root outranks ov-fx.js's own :root and theme feels, which load later. */--@media (prefers-reduced-motion: reduce) { :root:root { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; } }++:root:root[data-ov-motion=off] { --ov-move: 0; --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; }/* Shape personalities (plan L45: "Paper is quiet and square, Neon glows and bounces"). A quiet look has square corners* and lies flat, with a hairline where a shadow would be; a loud one is round and its raised things glow in its accent.* The ids are Network's theme catalog (as ov-fx.js's motion feels); any other look keeps the defaults above. */@@@ -111,6 +118,83 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -.ovk-icon-btn[hidden] { display: none; }.ovk-icon-btn.is-done { color: var(--ovk-ok-text); }.ovk-icon-btn--danger:hover { color: var(--ovk-bad-text); }++++/* ── Buttons that show what they do and what is happening (4.204.0) ───────────────────────────────────────────────++ The owner, 2026-11-11: "improve our buttons across the site and add better animations and effects for them, and++ different things we can use them for, and little indicators or statuses … idle animation … more important buttons …++ more intuitive and more modular … shared across everything through openvibe.design". docs/kit.md#buttons.++ Every motion is scaled by --ov-move (Calm makes it smaller and slower, Off removes it; the durations follow). */++.ovk-btn, .ovk-icon-btn { position: relative; -webkit-tap-highlight-color: transparent;++ transition: transform var(--ov-dur-1, .12s) var(--ov-ease-out, ease), box-shadow var(--ov-dur-2, .2s) var(--ov-ease-out, ease),++ background-color var(--ov-dur-1, .12s), border-color var(--ov-dur-1, .12s), color var(--ov-dur-1, .12s), filter var(--ov-dur-1, .12s); }++.ovk-btn:hover:not([disabled]):not([aria-disabled="true"]) { transform: translateY(calc(-1px * var(--ov-move, 1))); box-shadow: 0 8px 18px -12px rgba(0, 0, 0, .6); }++.ovk-btn:active:not([disabled]), .ovk-icon-btn:active:not([disabled]) { transform: scale(calc(1 - .03 * var(--ov-move, 1))); box-shadow: none; transition-duration: .06s; }++.ovk-btn .ovk-i, .ovk-icon-btn .ovk-i { transition: transform var(--ov-dur-2, .2s) var(--ov-ease-spring, var(--ov-ease-out, ease)); }++/* The icon moves the way the button does (data-nudge): forward slides on, add turns, open lifts, down drops, refresh turns. */++.ovk-btn[data-nudge="forward"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="forward"]:hover .ovk-i { transform: translateX(calc(3px * var(--ov-move, 1))); }++.ovk-btn[data-nudge="back"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="back"]:hover .ovk-i { transform: translateX(calc(-3px * var(--ov-move, 1))); }++.ovk-btn[data-nudge="add"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="add"]:hover .ovk-i { transform: rotate(calc(90deg * var(--ov-move, 1))); }++.ovk-btn[data-nudge="open"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="open"]:hover .ovk-i { transform: translate(calc(2px * var(--ov-move, 1)), calc(-2px * var(--ov-move, 1))); }++.ovk-btn[data-nudge="down"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="down"]:hover .ovk-i { transform: translateY(calc(2px * var(--ov-move, 1))); }++.ovk-btn[data-nudge="refresh"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="refresh"]:hover .ovk-i { transform: rotate(calc(180deg * var(--ov-move, 1))); }++++/* Importance: hero (the one thing a page is for), soft (a tinted second choice), live (on air now). */++.ovk-btn--hero { min-height: 52px; padding: 12px 24px; font-size: 1.02rem; color: var(--ovk-on-fill); border: 0;++ background: linear-gradient(135deg, var(--ovk-accent), var(--ovk-fill)); box-shadow: 0 10px 26px -14px var(--ovk-fill); }++.ovk-btn--hero:hover:not([disabled]) { box-shadow: 0 14px 30px -12px var(--ovk-fill); filter: brightness(1.06); }++.ovk-btn--soft { color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 14%, transparent); border-color: color-mix(in srgb, var(--ovk-accent) 22%, transparent); }++.ovk-btn--soft:hover { background: color-mix(in srgb, var(--ovk-accent) 20%, transparent); }++.ovk-btn--live { color: #fff; background: var(--ovk-live); }++.ovk-btn--live::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #fff; flex: none; animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }++@keyframes ovkBreathe { 50% { opacity: calc(1 - .65 * var(--ov-move, 1)); transform: scale(calc(1 - .2 * var(--ov-move, 1))); } }++++/* A toggle says it is on (aria-pressed): filled tint and a check-weight border. */++.ovk-btn[aria-pressed="true"], .ovk-icon-btn[aria-pressed="true"] { color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, transparent); border-color: color-mix(in srgb, var(--ovk-accent) 45%, transparent); }++++/* The one next step (data-attention): a soft ring goes out from it now and then, until it is pressed. */++.ovk-btn[data-attention]:not([disabled])::after, .ovk-icon-btn[data-attention]:not([disabled])::after {++ content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none;++ box-shadow: 0 0 0 0 color-mix(in srgb, var(--ovk-accent) 55%, transparent); animation: ovkAttention 3.2s var(--ov-ease-out, ease-out) infinite; }++@keyframes ovkAttention { 0%, 55% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ovk-accent) 55%, transparent); } 85%, 100% { box-shadow: 0 0 0 calc(10px * var(--ov-move, 1)) transparent; } }++++/* A count (data-badge="3") and a status dot (data-dot="ok|warn|bad|live") at the top right. */++.ovk-btn[data-badge]:not([data-badge=""])::before, .ovk-icon-btn[data-badge]:not([data-badge=""])::before {++ content: attr(data-badge); position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;++ border-radius: 999px; background: var(--ovk-bad); color: #fff; font-size: 11px; font-weight: 800; line-height: 18px; text-align: center;++ box-shadow: 0 0 0 2px var(--ovk-bg, var(--bg-primary, #0b0f19)); animation: ovkPop var(--ov-dur-2, .2s) var(--ov-ease-spring, ease-out); }++.ovk-btn--live[data-badge]::before { animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }++.ovk-btn[data-dot], .ovk-icon-btn[data-dot] { --ovk-dot: var(--ovk-muted); }++.ovk-btn[data-dot="ok"], .ovk-icon-btn[data-dot="ok"] { --ovk-dot: var(--ovk-ok); }++.ovk-btn[data-dot="warn"], .ovk-icon-btn[data-dot="warn"] { --ovk-dot: var(--warning, #f59e0b); }++.ovk-btn[data-dot="bad"], .ovk-icon-btn[data-dot="bad"], .ovk-btn[data-dot="live"], .ovk-icon-btn[data-dot="live"] { --ovk-dot: var(--ovk-bad); }++.ovk-btn[data-dot]:not([data-badge])::before, .ovk-icon-btn[data-dot]:not([data-badge])::before {++ content: ""; position: absolute; top: 4px; right: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-dot);++ box-shadow: 0 0 0 2px var(--ovk-bg, var(--bg-primary, #0b0f19)); }++.ovk-btn[data-dot="live"]:not([data-badge])::before, .ovk-icon-btn[data-dot="live"]:not([data-badge])::before { animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }++@keyframes ovkPop { from { transform: scale(calc(1 - .6 * var(--ov-move, 1))); opacity: 0; } }++++/* Progress inside the button (style="--ovk-progress: .4"): an upload, a countdown, a hold. */++.ovk-btn[style*="--ovk-progress"] { background-image: linear-gradient(90deg, color-mix(in srgb, currentColor 18%, transparent) calc(var(--ovk-progress, 0) * 100%), transparent 0); }++++/* What is happening (data-state, set by OpenVibeKitWeb.busy): busy keeps the words and adds the mark's wait; done shows++ a check for a moment; error shakes once and turns red. */++.ovk-btn[data-state="busy"] { cursor: progress; pointer-events: none; }++.ovk-btn[data-state="busy"] > :not(.ovk-btn-wait) { opacity: .62; }++.ovk-btn-wait { display: inline-flex; gap: 3px; align-items: center; }++.ovk-btn-wait i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; animation: ovkWait 1.1s var(--ov-ease-in-out, ease-in-out) infinite; }++.ovk-btn-wait i:nth-child(2) { animation-delay: .15s; } .ovk-btn-wait i:nth-child(3) { animation-delay: .3s; }++@keyframes ovkWait { 0%, 80%, 100% { opacity: .25; transform: translateY(0); } 40% { opacity: 1; transform: translateY(calc(-3px * var(--ov-move, 1))); } }++.ovk-btn[data-state="done"] { color: #fff; background: var(--ovk-ok); border-color: transparent; }++.ovk-btn[data-state="done"]::after { content: ""; flex: none; width: 16px; height: 16px; background: currentColor; animation: ovkPop var(--ov-dur-2, .2s) var(--ov-ease-spring, ease-out);++ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;++ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat; }++.ovk-btn[data-state="error"] { color: var(--ovk-bad-text); border-color: var(--ovk-bad); background: color-mix(in srgb, var(--ovk-bad) 10%, transparent); animation: ovkShake .36s var(--ov-ease-out, ease-out); }++@keyframes ovkShake { 25% { transform: translateX(calc(-4px * var(--ov-move, 1))); } 50% { transform: translateX(calc(4px * var(--ov-move, 1))); } 75% { transform: translateX(calc(-2px * var(--ov-move, 1))); } }++++/* Hold to confirm (data-hold="1200" on a destructive button): the fill runs while it is held; letting go early cancels. */++.ovk-btn[data-hold] { background-image: linear-gradient(90deg, color-mix(in srgb, var(--ovk-bad) 26%, transparent) var(--ovk-hold, 0%), transparent 0); }++.ovk-btn[data-hold].is-holding { transition: none; }++@media (hover: none) { .ovk-btn:hover:not([disabled]):not([aria-disabled="true"]) { transform: none; box-shadow: none; } }.ovk-icon-btn .ovk-i { width: 20px; height: 20px; }@media (pointer: coarse) { .ovk-icon-btn { width: var(--ovk-tap); height: var(--ovk-tap); } }.ovk-badge { display: inline-flex; align-items: center; flex: none; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--ovk-line); font-size: .75rem; font-weight: 800; letter-spacing: .03em; text-transform: uppercase; color: var(--ovk-muted); white-space: nowrap; }@@@ -128,10 +212,16 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -.ovk-head-row { display: flex; align-items: center; gap: 6px; min-height: 48px; padding-bottom: 6px; }.ovk-head-row > .ovk-back { margin-left: -8px; }.ovk-head-id { flex: 1; min-width: 0; }--.ovk-head-id.ovk-head-id h1, .ovk-head-id.ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }--.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++/* The title and its line wrap to two lines before anything is cut (openvibe.design names-are-never-cut, 2026-11-11: the++ Arena's "One prompt, two models, side b…" lost its point on a phone). */++.ovk-head-id.ovk-head-id h1, .ovk-head-id.ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }++.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; line-height: 1.35; overflow: hidden; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }.ovk-head-tools form { margin: 0; }++.ovk-heading-icon { display: grid; place-items: center; flex: 0 0 40px; width: 40px; height: 40px; border-radius: var(--ovk-radius-sm); color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); }++.ovk-heading-icon > .ovk-i { width: 22px; height: 22px; }++.ovk-heading-icon.ovk-i { box-sizing: border-box; width: 40px; height: 40px; padding: 9px; vertical-align: middle; }++.ovk-titled.ovk-titled { display: flex; align-items: center; gap: 10px; white-space: normal; overflow-wrap: anywhere; }.ovk-tabs { position: relative; display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }.ovk-tabs::-webkit-scrollbar { display: none; }.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }@@@ -173,6 +263,12 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -/* ── Sections (settings groups, a page's parts) and folds ── */.ovk-section { margin: 0 0 12px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }.ovk-section.ovk-section > h2 { display: flex; align-items: center; gap: 6px; margin: 0; padding: 12px 14px 6px; color: var(--ovk-muted); font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }++.ovk-section-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px; }++.ovk-section-head.ovk-section-head > h2 { flex: 1 1 160px; min-width: 0; margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--ovk-text); font-family: var(--ovk-font); font-size: 1rem; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }++.ovk-section-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; max-width: 100%; }++.ovk-section-actions .ovk-btn { min-height: 44px; white-space: normal; text-align: center; }++.ovk-section-actions form { margin: 0; }++.ovk-section > .ovk-empty { margin: 0 14px 14px; }.ovk-count { color: var(--ovk-text); font-weight: 800; letter-spacing: 0; }.ovk-note { margin: 0; padding: 0 14px 10px; color: var(--ovk-muted); font-size: .86rem; }.ovk-section > .ovk-form, .ovk-section > form:not(.ovk-find):not(.ovk-inline) { padding: 4px 14px 14px; }@@@ -205,6 +301,12 @@ div.ovk-set-row { cursor: default; }.ovk-set-row select { flex: none; max-width: 52%; margin: 0; padding: 7px 8px; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .9rem; }.ovk-set-range input[type=range] { flex: 0 1 45%; min-width: 120px; margin: 0; accent-color: var(--ovk-fill); }.ovk-set-chips { flex-wrap: wrap; }++/* A switch row (4.164.0): the icon tile, the title and its one line, then the switch; the whole 56 px row is the++ target, and the switch is the native checkbox (role="switch") drawn as one, so it works without JavaScript. */++.ovk-set-switch { min-height: 56px; }++.ovk-set-ic { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--ov-radius-1); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); color: var(--ovk-accent); }++.ovk-set-ic .ovk-i { width: 19px; height: 19px; }++.ovk-set-row .ovk-switch { margin-left: auto; }.ovk-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative; width: 44px; height: 26px; margin: 0; padding: 0; border-radius: 999px; border: 0; background: var(--ovk-line); cursor: pointer; transition: background .15s; }.ovk-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: var(--ov-elev-1); transition: transform .15s; }.ovk-switch:checked { background: var(--ovk-fill); }@@@ -215,10 +317,17 @@ div.ovk-set-row { cursor: default; }.ovk-seg span { display: block; padding: 6px 12px; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }.ovk-seg input:checked + span { background: var(--ovk-fill); color: var(--ovk-on-fill); }.ovk-seg input:focus-visible + span, .ovk-chip input:focus-visible + span { outline: 2px solid var(--ovk-accent); outline-offset: 1px; }++/* A segmented control (4.164.0): three or four equal choices in one 44 px control; the icons sit before the words. */++.ovk-seg--fill { display: flex; flex: 1 1 auto; flex-wrap: nowrap; width: 100%; min-width: 0; }++.ovk-seg--fill .ovk-seg-item { flex: 1 1 0; min-width: 0; }++.ovk-seg--fill span { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 38px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }++.ovk-seg--fill .ovk-i { flex: none; width: 16px; height: 16px; }++/* Straight in a section (no label row around it), it keeps the rows' inset instead of running to the card's edges. */++.ovk-section > .ovk-seg--fill { width: auto; margin: 10px 12px 6px; }.ovk-chips { display: flex; flex-wrap: wrap; gap: 6px; }.ovk-chip span { display: inline-flex; align-items: center; min-height: 32px; box-sizing: border-box; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--ovk-line); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }.ovk-chip input:checked + span { border-color: color-mix(in srgb, var(--ovk-accent) 55%, transparent); background: color-mix(in srgb, var(--ovk-accent) 14%, transparent); color: var(--ovk-text); }--.ovk-save { position: sticky; bottom: var(--ovk-bottom); z-index: 30; margin: 0; padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px)); background: linear-gradient(transparent, var(--ovk-bg) 35%); }++.ovk-save { position: sticky; bottom: var(--ovk-bottom); translate: 0 calc(-1 * var(--ovb-lift, 0px)); z-index: 30; margin: 0; padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px)); background: linear-gradient(transparent, var(--ovk-bg) 35%); }.ovk-save .ovk-btn { width: 100%; }.ovk-form { display: grid; gap: 12px; margin: 0; }.ovk-form label { display: grid; gap: 5px; color: var(--ovk-muted); font-size: .9rem; }@@@ -242,7 +351,7 @@ div.ovk-set-row { cursor: default; }.ovk-go { display: inline-grid; place-items: center; flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; background: var(--ovk-fill); color: var(--ovk-on-fill); cursor: pointer; }.ovk-go .ovk-i { width: 18px; height: 18px; stroke-width: 2.4; }.ovk-go--send { width: var(--ovk-tap); height: var(--ovk-tap); }--.ovk-dock { position: sticky; bottom: var(--ovk-bottom); z-index: 30; display: flex; align-items: flex-end; gap: 6px; margin: 8px -16px 0; padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); background: color-mix(in srgb, var(--ovk-bg) 92%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }++.ovk-dock { position: sticky; bottom: var(--ovk-bottom); translate: 0 calc(-1 * var(--ovb-lift, 0px)); z-index: 30; display: flex; align-items: flex-end; gap: 6px; margin: 8px -16px 0; padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); background: color-mix(in srgb, var(--ovk-bg) 92%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }.ovk-compose { flex: 1; min-width: 0; display: flex; align-items: flex-end; gap: 8px; margin: 0; }.ovk-compose textarea { flex: 1; min-width: 0; min-height: var(--ovk-tap); max-height: 40vh; box-sizing: border-box; margin: 0; padding: 10px 16px; border-radius: 22px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; line-height: 1.4; resize: none; field-sizing: content; }.ovk-compose textarea:focus-visible { outline: none; border-color: var(--ovk-accent); }@@@ -275,14 +384,18 @@ div.ovk-set-row { cursor: default; }.ovk-empty > .ovk-i { width: 28px; height: 28px; }.ovk-empty.ovk-empty p { margin: 0; max-width: 46ch; }.ovk-empty .ovk-empty-title { color: var(--ovk-text); font-weight: 750; }--.ovk-notice { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); font-size: .92rem; }--.ovk-notice > .ovk-i { margin-top: 2px; }--.ovk-notice--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, transparent); }--.ovk-notice--ok > .ovk-i { color: var(--ovk-ok-text); }--.ovk-notice--error { border-color: color-mix(in srgb, var(--ovk-bad) 50%, transparent); }--.ovk-notice--error > .ovk-i { color: var(--ovk-bad-text); }--.ovk-notice--warn > .ovk-i { color: var(--ovk-warn-text); }--.ovk-notice--info > .ovk-i { color: var(--ovk-accent); }++/* A tone is a lean and a glow, never a coloured bar (4.196.0; the owner, 2026-11-10: "instead of just like this red and++ crappy border … shadow, glow … something nice. Also add some icons"). A notice leans toward its tone from the side its++ icon is on, carries a hairline tinted with it and a soft glow in it; its icon sits in a round chip of the tone. The same++ three properties are a class of their own (.ovk-tone, with data-tone) for a card, a toast or a row that must say++ "good", "careful" or "wrong" without a stripe. */++.ovk-notice, .ovk-tone { --ovk-tone: var(--ovk-accent); --ovk-tone-text: var(--ovk-accent); border: 1px solid color-mix(in srgb, var(--ovk-tone) 32%, var(--ovk-line)); background: linear-gradient(100deg, color-mix(in srgb, var(--ovk-tone) 13%, var(--ovk-panel)) 0, var(--ovk-panel) 58%); box-shadow: 0 12px 28px -20px var(--ovk-tone); }++.ovk-notice--ok, .ovk-tone[data-tone="good"] { --ovk-tone: var(--ovk-ok); --ovk-tone-text: var(--ovk-ok-text); }++.ovk-notice--error, .ovk-tone[data-tone="alert"] { --ovk-tone: var(--ovk-bad); --ovk-tone-text: var(--ovk-bad-text); }++.ovk-notice--warn, .ovk-tone[data-tone="warning"] { --ovk-tone: var(--ovk-warn); --ovk-tone-text: var(--ovk-warn-text); }++.ovk-notice { display: flex; align-items: flex-start; gap: 11px; margin: 0 0 12px; padding: 10px 14px 10px 10px; border-radius: calc(var(--ov-radius-1) + 4px); color: var(--ovk-text); font-size: .92rem; }++.ovk-notice > .ovk-i { flex: none; box-sizing: border-box; width: 26px; height: 26px; margin: -2px 0; padding: 5px; border-radius: 50%; background: color-mix(in srgb, var(--ovk-tone) 20%, transparent); color: var(--ovk-tone-text); }++.ovk-notice > span { align-self: center; min-width: 0; }.ovk-snip { position: relative; }.ovk-snip > .ovk-icon-btn { position: absolute; top: 6px; right: 6px; z-index: 1; width: 34px; height: 34px; background: var(--ovk-panel); border-color: var(--ovk-line); }.ovk-snip > pre { max-height: 50vh; margin: 0 0 10px; padding: 12px 48px 12px 12px; overflow: auto; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font-size: .84rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }@@@ -312,7 +425,6 @@ div.ovk-set-row { cursor: default; }.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }}--@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }/* Touch (4.46.0, the fleet tap audit): small buttons are 44px tall on a touch screen, the find bar's go button reaches@@@ -370,12 +482,11 @@ div.ovk-set-row { cursor: default; }.ovk-sidebar-foot { padding-top: 8px; border-top: 1px solid var(--ovk-line); }@media (max-width: 760px) {.ovk-with-sidebar { grid-template-columns: minmax(0, 1fr); }-- .ovk-sidebar { position: fixed; z-index: 10060; top: 0; left: 0; bottom: 0; height: auto; width: min(320px, 86vw); padding-top: max(12px, env(safe-area-inset-top, 0px)); padding-bottom: max(10px, env(safe-area-inset-bottom, 0px)); border-right: 0; translate: -100% 0; visibility: hidden; transition: translate .25s ease, visibility 0s linear .25s; box-shadow: 12px 0 32px rgba(0, 0, 0, .25); }-- .ovk-sidebar.is-open { translate: 0 0; visibility: visible; transition: translate .25s ease; }++ .ovk-sidebar { position: fixed; z-index: 10060; top: 0; left: 0; bottom: 0; height: auto; width: min(320px, 86vw); padding-top: max(12px, env(safe-area-inset-top, 0px)); padding-bottom: max(10px, env(safe-area-inset-bottom, 0px)); border-right: 0; translate: calc(-100% * var(--ov-move, 1)) 0; opacity: 0; visibility: hidden; transition: translate .25s ease, opacity .2s ease, visibility 0s linear .25s; box-shadow: 12px 0 32px rgba(0, 0, 0, .25); }++ .ovk-sidebar.is-open { translate: 0 0; opacity: 1; visibility: visible; transition: translate .25s ease, opacity .2s ease; }.ovk-sidebar-close, .ovk-sidebar-open { display: inline-grid; }.ovk-sidebar-scrim { position: fixed; inset: 0; z-index: 10050; background: rgba(0, 0, 0, .4); }}--@media (prefers-reduced-motion: reduce) { .ovk-sidebar, .ovk-sidebar.is-open { transition: none; } .ovk-sidebar-dot.is-busy { animation: none; } }/* ── Scroll areas (4.69.0; the owner, 2026-11-01: "you can't scroll down the page even when you're at the bottom ofthe chat… this should be consistent across websites"). The network's rule: a scroll area inside a page that scrolls@@@ -412,16 +523,14 @@ div.ovk-set-row { cursor: default; }.ovk-skel-dot { flex: none; width: 22px; height: 22px; border-radius: 50%; }.ovk-skel-stat { display: grid; gap: 6px; padding: 12px; border-radius: var(--ov-radius-2); border: 1px solid var(--border, rgba(255,255,255,.08)); }.ovk-skel-num { height: 22px; width: 48%; }--@media (prefers-reduced-motion: reduce) { .ovk-skel { animation: none; } }--html[data-ov-motion=reduced] .ovk-skel { animation: none; }/* ── Moving between pages (4.70.0, plan L52) ── A server-rendered site's pages cross-fade while the navbar and the app bar-- stay put (cross-document view transitions; a browser without them just loads the page). Off under reduced motion. */++ stay put (cross-document view transitions; a browser without them just loads the page). A cross-fade moves nothing,++ so Calm keeps it; with motion off the loader gives it no length. */@view-transition { navigation: auto; }::view-transition-old(root), ::view-transition-new(root) { animation-duration: .18s; }#navbar-mount, .openvibe-navbar { view-transition-name: ov-navbar; }.ovnav-appbar { view-transition-name: ov-appbar; }--@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }/* Content labels (L1): quiet chips beside a title, the adult one in the warning colour. They say what a thing carries;each viewer's own choice (ov-labels.js) decides whether it shows, blurs or hides. */@@@ -429,3 +538,43 @@ html[data-ov-motion=reduced] .ovk-skel { animation: none; }.ovk-label { display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px; border-radius: 999px; font-size: .7rem; font-weight: 700; letter-spacing: .02em;color: var(--text-secondary, #a8b3c7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); border: 1px solid var(--border, rgba(255,255,255,.1)); }.ovk-label--adult { color: var(--warning-text, #fbbf24); border-color: color-mix(in srgb, var(--warning-text, #fbbf24) 45%, transparent); background: color-mix(in srgb, var(--warning-text, #fbbf24) 9%, transparent); }++++/* "Ask your agent" (4.134.0): the chip that hands this page's question to your Vibe in the Dock. */++.ovk-ask { border-color: color-mix(in srgb, var(--accent, #3b82f6) 45%, var(--border, #2a3345)); }++.ovk-ask .ovk-i { color: var(--accent-light, var(--accent, #60a5fa)); }++.ovk-ask:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }++++/* A phone's drawer holds the page still: the page cannot scroll behind it, the drawer scrolls on its own and never hands++ the scroll on to the page, and the scrim takes no scroll gestures (11-09). */++@media (max-width: 760px) {++ html.ovk-drawer-open, html.ovk-drawer-open body { overflow: hidden; overscroll-behavior: none; }++ .ovk-sidebar { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }++ .ovk-sidebar-scrim { touch-action: none; overscroll-behavior: contain; }++}++/* A page's head (kit.pageHead; designed 11-09, the owner: "less websitey… modern… an icon system"): a small named way++ back, the page's icon tile and name with its actions, and one line about it, whole (one copy: readers and search see++ the same words; nothing behind an "About this"). */++.ovk-page-head { margin: 0 0 20px; min-width: 0; }++.ovk-page-back { position: relative; display: inline-flex; align-items: center; gap: 2px; height: 32px; margin: 0 0 12px; padding: 0 12px 0 8px;++ border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); color: var(--text-secondary, #a8b3c4);++ font-size: 13.5px; font-weight: 650; text-decoration: none; transition: background .15s, color .15s; }++.ovk-page-back::after { content: ""; position: absolute; inset: -6px -4px; } /* 44 px under the finger */++.ovk-page-back .ovk-i { width: 18px; height: 18px; }++@media (hover: hover) { .ovk-page-back:hover { background: color-mix(in srgb, var(--text-primary, #e8ecf3) 11%, transparent); color: var(--text-primary, #e8ecf3); } }++/* The icon and the actions sit on the title's FIRST line (centred on it), so a title that wraps to three lines (a home++ page's hero, Space 11-09) keeps them beside its start instead of floating in the middle. --ovk-page-line is the++ title's line (1.15 × its size, in rem and vw so it means the same on every child). */++.ovk-page-head-row { --ovk-page-line: calc(1.15 * clamp(1.45rem, 2.4vw + .7rem, 2.2rem)); display: flex; align-items: flex-start; gap: 12px; min-width: 0; }++.ovk-page-head-row > .ovk-heading-icon, .ovk-page-head-row > .ovk-page-actions { margin-top: calc(var(--ovk-page-line) / 2 - 22px); }++.ovk-page-head-row > .ovk-heading-icon { flex-basis: 44px; width: 44px; height: 44px; border-radius: 13px;++ background: linear-gradient(150deg, color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent), color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent));++ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); }++.ovk-page-head-row > .ovk-heading-icon > .ovk-i { width: 24px; height: 24px; }++.ovk-page-head-row h1 { flex: 1 1 auto; min-width: 0; margin: 0; font-size: clamp(1.45rem, 2.4vw + .7rem, 2.2rem); line-height: 1.15; letter-spacing: -.015em; overflow-wrap: anywhere; }++.ovk-page-actions { display: flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }++.ovk-page-actions .ovk-icon-btn { width: 44px; height: 44px; border-radius: 50%; }++.ovk-page-lede { max-width: 68ch; margin: 10px 0 0; color: var(--text-secondary, #a8b3c4); font-size: 15.5px; line-height: 1.5; }++.ovk-page-back:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 3px; }++@media (max-width: 760px) { .ovk-page-head { margin-bottom: 16px; } .ovk-page-lede { margin-top: 8px; font-size: 14.5px; } }++/* A phone hero's 1fr track shrinks around the head. */++.sc-hero-main:has(>.ovk-page-head),.sc-room-main:has(>.ovk-page-head){min-width:0}@@@ -49,7 +49,10 @@stream: 'Live', social: 'Social', chat: 'Chat', game: 'Games', economy: 'Money & deals',achievement: 'Achievements', service: 'Services', moderation: 'Moderation', system: 'System', admin: 'Announcements',};-- const SERVICE_LABELS = { live: 'Live', tools: 'Tools', games: 'Games', media: 'Media', network: 'Network', host: 'Host', deals: 'Deals', coupons: 'Coupons', trade: 'Trade', chat: 'Chat' };++ const SERVICE_LABELS = {++ live: 'Live', tools: 'Tools', games: 'Games', media: 'Media', network: 'Network', host: 'Host', deals: 'Deals', coupons: 'Coupons', trade: 'Trade', chat: 'Chat',++ actor: 'Agents', watch: 'Watch', connect: 'Connect', link: 'Link', community: 'Community', billing: 'Wallet', blog: 'Blog', design: 'Design', ai: 'AI', events: 'Events',++ };// Line glyphs (24×24, stroked like the open-loop mark): one per kind of notice.const GLYPH = {@@@ -70,7 +73,12 @@bellSolid: '<path d="M5.8 16.2V11a6.2 6.2 0 0 1 12.4 0v5.2l1.7 2.3H4.1ZM9.9 19.6h4.2a2.1 2.1 0 0 1-4.2 0Z"/>',key: '<circle cx="8.2" cy="14.8" r="4"/><path d="M11.1 12l8.2-8.2M16.4 6.7l2.3 2.3M14.2 8.9l1.8 1.8"/>',spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',++ agent: '<rect x="5" y="8.2" width="14" height="10.8" rx="3.6"/><path d="M12 8.2V5.4M9.4 13.4h.01M14.6 13.4h.01M3 12.6v2.6M21 12.6v2.6"/><circle cx="12" cy="4.4" r="1"/>',++ eye: '<path d="M2.8 12s3.4-6.2 9.2-6.2 9.2 6.2 9.2 6.2-3.4 6.2-9.2 6.2S2.8 12 2.8 12z"/><circle cx="12" cy="12" r="2.8"/>',++ screen: '<rect x="3.5" y="4.5" width="17" height="11.5" rx="2.2"/><path d="M9 20h6M12 16v4"/>',};++ const SERVICE_GLYPH = { actor: 'agent', watch: 'eye', connect: 'screen', link: 'screen', billing: 'coin' };++ const TYPE_GLYPH = { WELCOME: 'spark', AGENT_TASK_DONE: 'agent', AGENT_TASK_FAILED: 'agent', AGENT_NEEDS_YOU: 'agent' };const CATEGORY_GLYPH = { stream: 'live', social: 'people', chat: 'chat', game: 'game', economy: 'coin', achievement: 'trophy', moderation: 'shield', system: 'gear', service: 'grid', admin: 'bell' };const TONE = {live: 'var(--live-red, #e5484d)', danger: 'var(--live-red, #e5484d)', warn: 'var(--warning, #f5a524)', ok: 'var(--success, #30a46c)',@@@ -92,6 +100,7 @@let _pollTimer = null;let _lastSeenAt = null; // newest created_at we have toasted / seenlet _unreadCount = 0;++ let _msgs = 0; // new messages (navbar.js setMessages): on the bell's badge with the notifications, never in the list's own countlet _toastContainer = null;let _panelEl = null;let _bellEl = null;@@@ -196,6 +205,7 @@.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }.openvibe-notif-dock .openvibe-nv-head { display: none; }.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }++.openvibe-notif-dock.is-preview .openvibe-nv-tools, .openvibe-notif-dock.is-preview .openvibe-nv-foot, .openvibe-notif-dock.is-preview .openvibe-nv-head, .openvibe-notif-dock.is-preview .openvibe-nv-more { display: none; }.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }@@@ -231,6 +241,7 @@.openvibe-nv-unread svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }.openvibe-nv-tabs { display: flex; overflow-x: auto; scrollbar-width: none; margin: 0 -12px; padding: 0 6px; }.openvibe-nv-tabs::-webkit-scrollbar { display: none; }++ .openvibe-nv-tabs.is-more { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); }.openvibe-nv-tabs button { flex: none; position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 0 10px; border: 0; background: transparent; color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }.openvibe-nv-tabs button:hover, .openvibe-nv-tabs button[aria-selected="true"] { color: var(--text-primary, #ececf1); }.openvibe-nv-tabs button[aria-selected="true"]::after { content: ''; position: absolute; left: 10px; right: 10px; bottom: 0; height: 2.5px; border-radius: 2px 2px 0 0; background: var(--accent, #3b82f6); }@@@ -304,12 +315,7 @@}html.ovn-sheet-open, html.ovn-sheet-open body { overflow: hidden; }@media (max-height: 520px) { .openvibe-notif-item .msg { -webkit-line-clamp: 1; } }-- @media (prefers-reduced-motion: reduce) {-- .openvibe-toast, .ovn-phone .openvibe-toast, .openvibe-notif-panel { animation: none; }-- .openvibe-toast.removing { animation: none; opacity: 0; }-- .openvibe-bell.ringing svg, .openvibe-bell .badge { animation: none; }-- .openvibe-toast-progress { display: none; }-- }++ html[data-ov-motion=off] .openvibe-toast.removing{opacity: 0}html[data-ov-motion=off] .openvibe-toast-progress{display: none}`;document.head.appendChild(style);}@@@ -407,17 +413,54 @@label: 'System', url: rich.runbook_url || urlOf(n), actionLabel: 'Open status',};}++ // JSON in a message (a watch sends a feed entry, cut short): its title and summary, never code.++ function readableMessage(raw) {++ const s = String(raw || '').trim();++ if (!/^[{[]\s*"/.test(s)) return s;++ const field = (name) => {++ const m = new RegExp(`"${name}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)`).exec(s);++ if (!m) return '';++ try { return JSON.parse(`"${m[1].replace(/\\$/, '')}"`); } catch { return m[1]; }++ };++ const title = field('title') || field('name');++ const summary = field('summary') || field('description');++ return [title, summary].filter(Boolean).join(' — ').replace(/\s+/g, ' ').trim();++ }++ const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);++ // What kind of step it is, as a label (a noun, so it never reads as a button).++ const ASK_CHIP = { approve: 'Approval', confirm: 'Approval', answer: 'Question', choose: 'Choice', pick: 'Choice', review: 'Review', 'sign in': 'Sign-in', 'log in': 'Sign-in' };++ // An agent's notice: the task, not its wording ("Done: x" → x; "approve: step (task)" → step + chip).++ function agentText(n, rich) {++ const t = String(n.type || '');++ let msg = String(n.message || '').trim();++ const chips = [];++ if (t === 'AGENT_TASK_DONE') msg = rich.task_title || msg.replace(/^Done:\s*/i, '');++ else if (t === 'AGENT_TASK_FAILED') msg = rich.task_title ? `${rich.task_title}${rich.detail ? ` — ${rich.detail}` : ''}` : msg.replace(/^Not finished:\s*/i, '');++ else if (t === 'AGENT_NEEDS_YOU') {++ if (rich.step) msg = String(rich.step);++ else if (/\)$/.test(msg) && msg.lastIndexOf(' (') > 0) msg = msg.slice(0, msg.lastIndexOf(' ('));++ const m = /^(approve|answer|confirm|choose|pick|review|sign in|log in)\s*:\s*/i.exec(msg);++ const k = (m ? m[1] : String(rich.kind || '')).toLowerCase();++ if (m) msg = msg.slice(m[0].length);++ if (ASK_CHIP[k]) chips.push(ASK_CHIP[k]);++ }++ return { message: cap(msg.trim()), chips };++ }function present(n) {const rich = richOf(n);if (isOpsAlert(n, rich)) return presentOps(n, rich);const cat = n.category || 'system';const t = String(n.type || '');-- const glyph = /DEAL|COUPON|PRICE|TRADE/.test(t) ? 'tag' : /CONFIRM|APPROV|SECUR|SIGN/.test(t) ? 'key' : (CATEGORY_GLYPH[cat] || 'bell');-- const tone = n.priority === 'critical' ? 'danger' : (CATEGORY_TONE[cat] || 'accent');-- const chips = Array.isArray(rich.chips) ? rich.chips.filter(c => typeof c === 'string').slice(0, 4) : [];++ const agent = /^AGENT_/.test(t);++ const glyph = /DEAL|COUPON|PRICE|TRADE/.test(t) ? 'tag' : /CONFIRM|APPROV|SECUR|SIGN/.test(t) ? 'key'++ : (TYPE_GLYPH[t] || SERVICE_GLYPH[n.service] || CATEGORY_GLYPH[cat] || 'bell');++ const tone = n.priority === 'critical' ? 'danger'++ : t === 'AGENT_TASK_DONE' ? 'ok' : t === 'AGENT_TASK_FAILED' ? 'warn' : (CATEGORY_TONE[cat] || 'accent');++ const said = agent ? agentText(n, rich) : { message: readableMessage(n.message), chips: [] };++ const chips = said.chips.concat(Array.isArray(rich.chips) ? rich.chips.filter(c => typeof c === 'string') : []).slice(0, 4);return {-- kind: 'plain', key: [t, n.title || '', n.service || '', cat].join('\u0001'), tone, glyph,-- title: String(n.title || ''), message: String(n.message || ''), chips, severity: null, command: null,++ kind: 'plain', key: [t, n.title || '', n.service || '', cat, rich.task_id || ''].join('\u0001'), tone, glyph,++ title: String(n.title || ''), message: said.message, chips, severity: null, command: null,label: SERVICE_LABELS[n.service] || (n.service && n.service !== 'network' ? n.service : '') || CATEGORY_LABELS[cat] || '',url: urlOf(n), actionLabel: null,};@@@ -509,6 +552,9 @@if (msg.type === 'unread') setBadge(msg.count, { quiet: true });else if (msg.type === 'read') { for (const v of _views.values()) applyReadLocally(v, msg.ids || [], msg.all); }}++ function announceCount() {++ try { document.dispatchEvent(new CustomEvent('openvibe-notifications-count', { detail: { count: _unreadCount } })); } catch { /* */ }++ }// ── Sound ───────────────────────────────────────────────function playSound(priority) {@@@ -784,33 +830,43 @@}// ── Bell / badge ────────────────────────────────────────++ let _bellAction = null;function createBellEl() {const bell = document.createElement('div');bell.className = 'openvibe-bell';bell.setAttribute('role', 'button'); bell.setAttribute('aria-label', 'Notifications'); bell.tabIndex = 0;bell.innerHTML = `<svg class="ovnav-ovg has-s" viewBox="0 0 24 24" aria-hidden="true">${GLYPH.bell}<g class="ovg-s">${GLYPH.bellSolid}</g></svg><span class="badge hidden">0</span>`;-- bell.addEventListener('click', () => togglePanel());-- bell.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePanel(); } });++ // With the Dock on the page, its Activity tab is this list: the bell opens it there (4.150.0, the owner: the bell++ // "doesn't actually open the notifications … you have to open the chat button and click this manually"); the++ // panel opened under the Dock's sheet, so the tap seemed to do nothing.++ const press = () => { if (_bellAction) { try { if (_bellAction() === true) return; } catch { /* the panel then */ } } togglePanel(); };++ bell.addEventListener('click', press);++ bell.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); press(); } });_bellEl = bell;return bell;}++ // The bell's own number: notifications and new messages together, each named in its label.++ function paintBell(ring) {++ if (!_bellEl) return;++ const badge = _bellEl.querySelector('.badge'), n = _unreadCount + _msgs;++ if (n > 0) {++ badge.textContent = n > 99 ? '99+' : String(n);++ badge.classList.remove('hidden');++ if (ring) { _bellEl.classList.add('ringing'); setTimeout(() => _bellEl.classList.remove('ringing'), 700); }++ } else badge.classList.add('hidden');++ const parts = [_unreadCount ? `${_unreadCount} unread` : '', _msgs ? `${_msgs} new ${_msgs === 1 ? 'message' : 'messages'}` : ''].filter(Boolean);++ _bellEl.setAttribute('aria-label', parts.length ? `Notifications, ${parts.join(', ')}` : 'Notifications');++ }function setBadge(count, { quiet = false } = {}) {const prev = _unreadCount;_unreadCount = Math.max(0, Number(count) || 0);-- if (_bellEl) {-- const badge = _bellEl.querySelector('.badge');-- if (_unreadCount > 0) {-- badge.textContent = _unreadCount > 99 ? '99+' : String(_unreadCount);-- badge.classList.remove('hidden');-- if (!quiet && _unreadCount > prev) { _bellEl.classList.add('ringing'); setTimeout(() => _bellEl.classList.remove('ringing'), 700); }-- } else badge.classList.add('hidden');-- _bellEl.setAttribute('aria-label', _unreadCount ? `Notifications, ${_unreadCount} unread` : 'Notifications');-- }++ paintBell(!quiet && _unreadCount > prev);for (const v of _views.values()) {const c = v.root.querySelector('.openvibe-nv-head .count');if (c) { c.textContent = _unreadCount > 99 ? '99+' : String(_unreadCount); c.classList.toggle('zero', _unreadCount === 0); }}if (!quiet) broadcast({ type: 'unread', count: _unreadCount });++ announceCount();if (_unreadCount !== prev && listShowing()) loadTabCounts();}@@@ -872,6 +928,14 @@};tabs.forEach((btn) => btn.addEventListener('click', () => selectTab(btn)));const tabRow = root.querySelector('.openvibe-nv-tabs');++ // More filters past the edge: a fade says the row scrolls (4.150.0; "System" was cut off with no sign of more).++ if (tabRow && !tabRow.__more) {++ tabRow.__more = true;++ const more = () => tabRow.classList.toggle('is-more', tabRow.scrollWidth - tabRow.clientWidth - tabRow.scrollLeft > 4);++ tabRow.addEventListener('scroll', more, { passive: true });++ if (typeof ResizeObserver === 'function') new ResizeObserver(more).observe(tabRow);++ setTimeout(more, 0);++ }const fade = () => { tabRow.classList.toggle('is-more', tabRow.scrollLeft + tabRow.clientWidth < tabRow.scrollWidth - 4); };tabRow.addEventListener('scroll', fade, { passive: true });v.fade = fade;@@@ -924,6 +988,7 @@}, true);attachRowSwipe(v, list);refreshPushButton(v);++ v.destroy = () => { clearTimeout(qTimer); v.seq++; };_views.set(root, v);paintTabCounts();return v;@@@ -1038,7 +1103,7 @@}async function fetchPage(v, offset) {-- const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset) });++ const params = new URLSearchParams({ limit: String(v.pageSize || PAGE_SIZE), offset: String(offset) });if (v.unread) params.set('unread_only', '1');if (v.tab && v.tab !== 'all') params.set('tab', v.tab);if (v.q) params.set('q', v.q);@@@ -1056,6 +1121,7 @@if (seq !== v.seq) return;v.items = data.notifications || []; v.items.forEach(n => v.ids.add(String(n.id)));v.hasMore = !!data.has_more; v.total = data.total ?? null; v.offset = v.items.length;++ if (typeof v.onCount === 'function') { try { v.onCount(v.total); } catch { /* */ } }renderItems(v);} catch (e) {if (seq !== v.seq) return;@@@ -1190,6 +1256,8 @@}function stopPolling() { if (_pollTimer) clearInterval(_pollTimer); _pollTimer = null; }++ // notification-live.js's content hash (scripts/sibling-revs.js writes it), so a release gives it a new URL.++ const NOTIFY_REV = { 'notification-live.js': 'cd2d64cc77d0' };// ── Realtime (notification-live.js) ─────────────────────function liveOpen() { return !!(_live && _live.state().state === 'open'); }/** One re-read for a burst of events (a go-live fan-out, a replay after a reconnect); a gap re-reads open lists too. */@@@ -1207,7 +1275,7 @@function liveSrc() {if (_config.liveSrc) return _config.liveSrc;const m = /^(https?:\/\/[^?#]*\/)notification-ui\.js(?:[?#]|$)/.exec(SELF_SRC);-- return `${m ? m[1] : 'https://openvibe.network/shared/'}notification-live.js`;++ return `${m ? m[1] : 'https://openvibe.network/shared/'}notification-live.js${NOTIFY_REV['notification-live.js'] ? `?v=${NOTIFY_REV['notification-live.js']}` : ''}`;}let _liveLoading = null;function loadLive() {@@@ -1386,6 +1454,12 @@_toastContainer?.remove(); _panelEl?.remove();_toastContainer = null; if (_panelEl) _views.delete(_panelEl); _panelEl = null; _queue.length = 0;},++ /** What a tap on the bell does instead of its own panel: fn() returns true when it handled it (navbar.js: the Dock). */++ setBellAction(fn) { _bellAction = typeof fn === 'function' ? fn : null; },++ /** New messages, counted on the bell with the notifications (navbar.js): 0 takes them off. */++ setMessages(count) { const prev = _msgs; _msgs = Math.max(0, Number(count) || 0); paintBell(_msgs > prev); },++ /** How many notifications are unread (not counting messages). */++ unread() { return _unreadCount; },/** Create and inject the bell icon. Returns the DOM element. */createBell(container) {injectStyles();@@@ -1416,6 +1490,23 @@reload(v);return v;},++ /** The You tab's preview (4.211.0): the newest three, no tools; Show all opens the whole list in place. */++ renderMenuNotifications(container, onCount) {++ injectStyles();++ if (!container) return null;++ container.classList.add('openvibe-notif-dock', 'is-preview');++ const v = buildView(container, { mode: 'dock' });++ v.pageSize = 3; v.onCount = onCount;++ reload(v);++ return { showAll() { container.classList.remove('is-preview'); v.pageSize = PAGE_SIZE; reload(v); } };++ },++ unmountDock(container) {++ const v = _views.get(container);++ if (!v || v.mode !== 'dock') return;++ if (v.destroy) v.destroy();++ _views.delete(container);++ container.classList.remove('openvibe-notif-dock');++ },/** Programmatically show a toast (real-time events). Marks nothing on the server. */push(notification) {showToast(notification);nnew file mode 100644@@@ -0,0 +1,605 @@++/**++ * ov-composer.js (openvibe-shared 4.201.0): one composer on every site. Ask 349, the owner: "how come on this composer++ * there's no file attaching system and all the other features … a better composer system unified across all sites …++ * moddable and fully customizable". ov-thread.js loads it for each thread.composer() form, and a site's own box joins++ * with OpenVibeComposer.enhance(form). The form's markup and its submit stay the site's; this adds:++ * - files: the + menu, a paste, a drop; each one a chip above the box (a picture's thumbnail, the name and size, ×).++ * A long paste becomes a text file, which you can put back in the box. Files go with the form (multipart); where a++ * mod gives upload(file, onProgress), they go ahead of it, the chip shows the progress, and the form carries the ids;++ * - "/" for commands (the site's, its mods', your own snippets) and "@" for mentions (the mods' sources);++ * - voice, where the browser can listen; a draft kept per box until it is sent; Ctrl/⌘+B, I, E for bold, italic and++ * code; the count near the box's limit;++ * - your settings (what sends, voice, compact, your snippets), from "/settings" or the slash list's last row.++ *++ * Mods (a site, or a script on it): OpenVibeComposer.mod({++ * id, match(form) → bool, which boxes it joins (all when absent)++ * tools: [{ id, label, path (24-unit path data), run(ctx) }],++ * commands: [{ name, hint, insert } | { name, hint, run(ctx) }],++ * mentions: { search(q) → Promise<[{ label, handle, sub, avatar, insert }]> },++ * upload(file, onProgress) → Promise<{ id }>, files go to the site's store before the send++ * beforeSend(ctx) → false to hold the send })++ * ctx: { form, text, value, set(v), insert(s), files(), add(files), note(s), send() }.++ * A box its page sends by script (a chat, an agent's run): form.__ovc.files() → the files (File objects), uploads(): the++ * ids a mod's upload gave, clear() after the send (the text, the chips, the draft); a form 'reset' clears the same.++ * A site without script says the same in data: data-ovt-commands='[{"name","hint","insert"}]', data-ovc-max-files,++ * data-ovc-max-mb, data-ovc-no-draft.++ */++(function (root) {++ 'use strict';++ if (root.OpenVibeComposer || typeof document === 'undefined') return;++++ // 4.203.0: the composer carries its own look (it was in thread.css), so any box on any page gets it: the Dock's++ // Vibe and Code boxes included. Injected once, before the first box is joined.++ const CSS = `.ovt-composer { position: relative; display: grid; gap: 6px; max-width: 780px; margin: 0 auto; padding: 10px 12px; border: 1px solid var(--border, rgba(148,163,184,.18)); border-radius: 18px; background: var(--bg-card, #131c2e); color: var(--text-primary, #e6edf7); }++.ovt-composer textarea { width: 100%; min-height: 48px; box-sizing: border-box; padding: 6px 2px; border: 0; outline: 0; resize: vertical; background: transparent; color: inherit; font: inherit; }++.ovt-c-row { display: flex; align-items: center; gap: 8px; }++.ovt-c-tools { flex: 1; display: flex; align-items: center; gap: 6px; }++.ovt-send { min-height: 40px; padding: 0 14px; border: 0; border-radius: 12px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font: inherit; font-weight: 650; cursor: pointer; }++.ovt-send:disabled { opacity: .5; }++.ovt-composer .ovc-file.ovc-image { height: auto; min-height: 100px; flex-wrap: wrap; overflow: visible; }++.ovt-composer .ovc-image input { flex: 1 0 100%; min-width: 0; min-height: 36px; box-sizing: border-box; border: 1px solid var(--border, rgba(148,163,184,.18)); border-radius: 8px; background: var(--bg-primary, #0b1220); color: inherit; font: inherit; }++.ovc-files { display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 8px; margin: 0 0 2px -4px; scrollbar-width: thin; }++.ovc-files[hidden] { display: none; }++.ovc-file { position: relative; flex: none; display: flex; align-items: center; gap: 9px; max-width: 230px; height: 52px; padding: 5px 30px 5px 5px; box-sizing: border-box; border-radius: 14px;++ border: 1px solid var(--ovt-line, var(--border, rgba(148, 163, 184, .18))); background: color-mix(in srgb, var(--text-primary, #fff) 4%, transparent); overflow: hidden; animation: ovc-in .2s cubic-bezier(.2, .9, .3, 1) both; }++.ovc-file.is-bad { border-color: color-mix(in srgb, var(--live-red, #ef4444) 70%, transparent); }++.ovc-file.is-up::after { content: ""; position: absolute; left: 0; bottom: 0; height: 3px; width: calc(var(--p, 0) * 100%); background: var(--ovt-accent, var(--accent, #3b82f6)); transition: width .2s; }++.ovc-file-th { flex: none; width: 40px; height: 40px; border-radius: 10px; object-fit: cover; background: #0b1220; }++.ovc-file-ic { display: grid; place-items: center; background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 16%, transparent); color: var(--ovt-accent, var(--accent, #3b82f6)); font-size: 11px; font-weight: 750; letter-spacing: .02em; }++.ovc-file-t { min-width: 0; display: grid; line-height: 1.25; }++.ovc-file-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 600; color: var(--text-primary, #e6edf7); }++.ovc-file-s { font-size: 12px; color: var(--text-muted, #7386a3); white-space: nowrap; }++.ovc-file.is-bad .ovc-file-s { color: var(--live-red, #ef4444); }++.ovc-file-x, .ovc-file-b { position: absolute; top: 4px; right: 4px; display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--text-secondary, #b4c0d4); cursor: pointer; }++.ovc-file-b { top: auto; bottom: 4px; }++.ovc-file-x:hover, .ovc-file-b:hover, .ovc-file-x:focus-visible, .ovc-file-b:focus-visible { background: var(--bg-hover, color-mix(in srgb, var(--text-primary, #fff) 10%, transparent)); color: var(--text-primary, #e6edf7); }++.ovc-file-x svg, .ovc-file-b svg { width: 15px; height: 15px; }++.ovc-dropping { border-color: var(--ovt-accent, var(--accent, #3b82f6)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 30%, transparent), 0 8px 30px rgba(0, 0, 0, .25); }++.ovc-dropping::before { content: "Drop to attach"; position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; border-radius: inherit; background: color-mix(in srgb, var(--bg-card, #131c2e) 86%, var(--ovt-accent, var(--accent, #3b82f6))); color: var(--text-primary, #e6edf7); font-weight: 650; pointer-events: none; }++.ovc-pop { position: absolute; z-index: 40; left: 6px; right: 6px; bottom: calc(100% + 8px); max-height: min(320px, 50vh); overflow: auto; padding: 6px; border-radius: 16px; border: 1px solid var(--ovt-line, var(--border, rgba(148, 163, 184, .18)));++ background: var(--bg-card, #131c2e); box-shadow: 0 18px 50px rgba(0, 0, 0, .35); animation: ovc-in .16s cubic-bezier(.2, .9, .3, 1) both; }++.ovt.is-start .ovc-pop { bottom: auto; top: calc(100% + 8px); }++.ovc-pop[hidden] { display: none; }++.ovc-pop-i { display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 6px 10px; box-sizing: border-box; border: 0; border-radius: 11px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }++.ovc-pop-i[aria-selected="true"], .ovc-pop-i:hover { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 14%, transparent); }++.ovc-pop-ic, .ovc-pop-av { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--ovt-accent, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 12%, transparent); object-fit: cover; }++.ovc-pop-av { border-radius: 50%; }++.ovc-pop-ic svg { width: 17px; height: 17px; }++.ovc-pop-t { min-width: 0; display: grid; line-height: 1.3; }++.ovc-pop-t b { font-weight: 650; font-size: 14px; color: var(--text-primary, #e6edf7); }++.ovc-pop-t span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-muted, #7386a3); }++.ovc-pop-tag { font-size: 11px; font-weight: 650; padding: 2px 7px; border-radius: 999px; color: var(--ovt-accent, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 12%, transparent); }++.ovc-mic[aria-pressed="true"] { color: var(--live-red, #ef4444); background: color-mix(in srgb, var(--live-red, #ef4444) 14%, transparent); animation: ovc-listen 1.4s ease-in-out infinite; }++.ovc-count { flex: none; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text-muted, #7386a3); }++.ovc-count.is-low { color: var(--live-red, #ef4444); }++.ovc-count[hidden] { display: none; }++.ovt-composer.ovc-compact { padding: 6px 8px 6px 12px; border-radius: 18px; }++.ovt-composer.ovc-compact .ovt-c-row { margin-top: 0; }++.ovt-composer.ovc-compact .ovt-btn, .ovt-composer.ovc-compact .ovt-send { width: 36px; min-width: 36px; height: 36px; }++@keyframes ovc-in { from { opacity: 0; transform: translateY(4px) scale(.98); } }++@keyframes ovc-listen { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--live-red, #ef4444) 18%, transparent); } }++html[data-ov-motion=off] .ovc-file, html[data-ov-motion=off] .ovc-pop, html[data-ov-motion=off] .ovc-mic { animation: none; }++.ovc-set { width: min(460px, calc(100vw - 24px)); max-height: min(640px, calc(100vh - 32px)); padding: 0; border: 1px solid var(--ovt-line, rgba(148, 163, 184, .2)); border-radius: 20px; background: var(--bg-card, #131c2e); color: var(--text-primary, #e6edf7); box-shadow: 0 30px 80px rgba(0, 0, 0, .5); }++.ovc-set::backdrop { background: rgba(2, 6, 14, .55); }++.ovc-set.ovc-set-in { width: auto; max-height: none; border: 0; border-radius: 0; background: none; box-shadow: none; }++.ovc-set-in .ovc-set-f { padding: 0; }++.ovc-set-f { display: grid; gap: 14px; padding: 6px 18px 18px; }++.ovc-set-h { display: flex; align-items: center; justify-content: space-between; margin: 0 -8px 0 0; font-size: 17px; }++.ovc-set-g { margin: 0; padding: 0; border: 0; display: grid; gap: 6px; }++.ovc-set-g legend { padding: 0 0 6px; font-weight: 650; font-size: 14px; }++.ovc-set label { display: flex; align-items: center; gap: 10px; min-height: 40px; font-size: 14.5px; cursor: pointer; }++.ovc-set small, .ovc-set-p { color: var(--text-muted, #7386a3); font-size: 12.5px; margin: 0; }++.ovc-set input[type=radio], .ovc-set input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent, #3b82f6); }++.ovc-set-l { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }++.ovc-set-l li { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 4px 4px 4px 10px; border-radius: 10px; background: color-mix(in srgb, var(--text-primary, #fff) 4%, transparent); }++.ovc-set-l span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary, #b4c0d4); font-size: 13px; }++.ovc-set-add { display: grid; grid-template-columns: 110px 1fr auto; gap: 8px; align-items: start; }++.ovc-set-add input, .ovc-set-add textarea { box-sizing: border-box; width: 100%; padding: 9px 10px; border-radius: 10px; border: 1px solid var(--ovt-line, rgba(148, 163, 184, .25)); background: var(--bg-input, rgba(2, 6, 14, .35)); color: inherit; font: inherit; font-size: 14px; resize: vertical; }++.ovc-set-btn { height: 40px; padding: 0 14px; border: 0; border-radius: 12px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font: inherit; font-weight: 650; cursor: pointer; }++@media (max-width: 480px) { .ovc-set-add { grid-template-columns: 1fr; } .ovc-file { max-width: 200px; } }++/* 4.205.0: the composer's own tool buttons and row on a page without thread.css (an agent's run, a card, any box), so it++ is the same composer everywhere. Where thread.css is loaded its rules are the same. */++.ovc .ovt-btn { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 12px; border: 0; background: none; color: var(--text-secondary, #b4c0d4); cursor: pointer; text-decoration: none; }++.ovc .ovt-btn:hover, .ovc .ovt-btn:focus-visible { background: var(--bg-hover, color-mix(in srgb, var(--text-primary, #fff) 6%, transparent)); color: var(--text-primary, #e6edf7); }++.ovc .ovt-btn .ovt-ic, .ovc .ovt-btn svg { width: 22px; height: 22px; }++.ovc .ovt-c-row { display: flex; align-items: center; gap: 6px; }++.ovc .ovt-c-tools { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }++.ovc .ovt-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }++`;++ function styles() { if (document.getElementById('ovc-styles')) return; const st = document.createElement('style'); st.id = 'ovc-styles'; st.textContent = CSS; document.head.appendChild(st); }++ const mods = [];++ // 4.202.0: your settings open in the one window (ov-window.js: a sheet on a phone, swiped down to close), loaded++ // from beside this file; without it, a plain <dialog>.++ const COMPOSER_REV = { 'ov-window.js': 'd3d8f9e3e6d2' };++ const selfSrc = (document.currentScript && document.currentScript.src) || '';++ function needWindow() {++ return new Promise((ok) => {++ if (root.OVWindow) return ok(root.OVWindow);++ const m = /^(https?:\/\/[^?#]*\/)ov-composer(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(selfSrc);++ if (!m) return ok(null);++ const sc = document.createElement('script');++ sc.src = m[1] + 'ov-window.js' + (COMPOSER_REV['ov-window.js'] ? '?v=' + COMPOSER_REV['ov-window.js'] : '');++ sc.onload = () => ok(root.OVWindow || null); sc.onerror = () => ok(null);++ document.head.appendChild(sc);++ });++ }++ const live = new Set();++ const PREFS_KEY = 'ov:composer';++ const DEFAULTS = { send_key: 'enter', voice: true, density: 'comfy', snippets: [] };++ const PATH_OK = /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]{1,2000}$/;++ const Speech = root.SpeechRecognition || root.webkitSpeechRecognition || null;++ const mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || '');++ const IC = {++ mic: 'M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21',++ x: 'M7 7l10 10M17 7L7 17',++ file: 'M7 3h7l5 5v12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM14 3v5h5',++ slash: 'M15 4L9 20',++ at: 'M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-3.5 7.1',++ gear: 'M12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7',++ back: 'M9 14l-5-5 5-5M4 9h11a5 5 0 0 1 0 10h-3',++ };++ const svg = (d, cls) => `<svg${cls ? ` class="${cls}"` : ''} viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;++ const el = (tag, cls, html) => { const n = document.createElement(tag); if (cls) n.className = cls; if (html != null) n.innerHTML = html; return n; };++ const store = {++ get(k) { try { return JSON.parse(root.localStorage.getItem(k) || 'null'); } catch { return null; } },++ set(k, v) { try { if (v == null) root.localStorage.removeItem(k); else root.localStorage.setItem(k, JSON.stringify(v)); } catch { /* private mode */ } },++ };++ const size = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${Math.round(n / 1024)} KB` : `${(n / 1048576).toFixed(n < 10485760 ? 1 : 0)} MB`);++++ // ── Your settings: this browser's copy, and your account's when the Network keeps them (the same on every site).++ let prefs = Object.assign({}, DEFAULTS, store.get(PREFS_KEY) || {});++ let synced = false, rev = null;++ const token = () => { try { const n = root.OpenVibeNavbar; return n && typeof n.token === 'function' ? n.token() : null; } catch { return null; } };++ const NET = 'https://openvibe.network/api/v1/me/composer';++ async function pull() {++ const t = token(); if (!t) return;++ try {++ const r = await fetch(NET, { headers: { authorization: `Bearer ${t}`, accept: 'application/json' }, credentials: 'omit' });++ if (!r.ok) return;++ const j = await r.json();++ synced = true; rev = j.rev != null ? j.rev : null;++ prefs = Object.assign({}, DEFAULTS, j.prefs || j);++ store.set(PREFS_KEY, prefs);++ live.forEach((c) => c.refresh());++ } catch { /* local only */ }++ }++ async function push() {++ const t = token(); if (!t || !synced) return;++ try {++ const r = await fetch(NET, { method: 'PUT', headers: { authorization: `Bearer ${t}`, 'content-type': 'application/json', accept: 'application/json' }, credentials: 'omit', body: JSON.stringify({ prefs, rev }) });++ if (r.status === 409) { await pull(); return; }++ if (r.ok) { const j = await r.json().catch(() => ({})); if (j.rev != null) rev = j.rev; }++ } catch { /* kept locally */ }++ }++ function setPrefs(next) {++ prefs = Object.assign({}, DEFAULTS, prefs, next);++ prefs.snippets = (prefs.snippets || []).filter((s) => s && /^[\w-]{1,30}$/.test(s.trigger || '') && s.text).slice(0, 50);++ store.set(PREFS_KEY, prefs);++ live.forEach((c) => c.refresh());++ push();++ }++ root.addEventListener('storage', (e) => { if (e.key === PREFS_KEY) { prefs = Object.assign({}, DEFAULTS, store.get(PREFS_KEY) || {}); live.forEach((c) => c.refresh()); } });++++ // ── One box.++ function enhance(form) {++ if (!form || form.__ovc) return form && form.__ovc;++ styles();++ const text = form.querySelector('[data-ovt-text]') || form.querySelector('textarea');++ if (!text) return null;++ const row = form.querySelector('.ovt-c-row');++ const tools = form.querySelector('.ovt-c-tools');++ const note = form.querySelector('[data-ovt-note]');++ const send = form.querySelector('[data-ovt-send]') || form.querySelector('[type="submit"]');++ const inputs = () => [...form.querySelectorAll('input[type="file"]')];++ const mine = () => mods.filter((m) => !m.match || m.match(form));++ const uploader = () => { const m = mine().find((x) => typeof x.upload === 'function'); return m ? m.upload : null; };++ // data-ovc-no-files: the box handles its own files (the Dock's Vibe box uploads a picture as it is picked).++ const noFiles = form.hasAttribute('data-ovc-no-files');++ const canFile = () => !noFiles && !!(inputs().length || uploader());++ const maxFiles = Number(form.getAttribute('data-ovc-max-files')) || 10;++ const maxBytes = (Number(form.getAttribute('data-ovc-max-mb')) || 25) * 1048576;++ const files = [];++ let quiet = '';++ const wasRequired = !!text.required; // a box that was not required stays so (the Dock's Vibe box)++++ const say = (s) => { if (!note) return; if (!quiet) quiet = note.textContent; note.textContent = s; clearTimeout(say.t); say.t = setTimeout(() => { note.textContent = quiet; quiet = ''; }, 5000); };++ const fire = () => text.dispatchEvent(new Event('input', { bubbles: true }));++ const ctx = {++ form, text,++ get value() { return text.value; },++ set(v) { text.value = v; fire(); },++ insert(s) { const a = text.selectionStart, b = text.selectionEnd; text.setRangeText(s, a, b, 'end'); fire(); text.focus(); },++ files: () => files.map((f) => f.file),++ add: (list) => add(list),++ note: say,++ send: () => { if (form.requestSubmit) form.requestSubmit(send || undefined); else form.submit(); },++ };++++ // Files: a strip of chips above the box.++ const strip = el('div', 'ovc-files');++ strip.hidden = true;++ strip.setAttribute('aria-label', 'Attached files');++ const firstLabel = form.querySelector(`label[for="${text.id}"]`);++ form.insertBefore(strip, firstLabel || text);++ const accepts = (file) => {++ const acc = (inputs()[0] && inputs()[0].getAttribute('accept')) || '';++ if (!acc) return true;++ const name = (file.name || '').toLowerCase(), type = (file.type || '').toLowerCase();++ return acc.split(',').map((x) => x.trim().toLowerCase()).some((a) => !a || (a.startsWith('.') ? name.endsWith(a) : a.endsWith('/*') ? type.startsWith(a.slice(0, -1)) : type === a));++ };++ function syncInputs() {++ if (noFiles) return;++ if (uploader()) { inputs().forEach((i) => { i.value = ''; }); return; }++ const [first, ...rest] = inputs();++ if (!first || typeof DataTransfer === 'undefined') return;++ const dt = new DataTransfer();++ files.forEach((f) => dt.items.add(f.file));++ first.files = dt.files;++ rest.forEach((i) => { i.value = ''; });++ }++ function paintFile(f) {++ const kind = (f.file.type || '').split('/')[0];++ const chip = f.chip || el('div', 'ovc-file');++ f.chip = chip;++ chip.classList.toggle('is-bad', !!f.error);++ chip.classList.toggle('is-up', f.progress != null && f.progress < 1 && !f.error);++ chip.style.setProperty('--p', String(f.progress == null ? 1 : f.progress));++ const ext = ((f.file.name || '').split('.').pop() || 'file').slice(0, 4).toUpperCase();++ const thumb = f.url && kind === 'image' ? `<img class="ovc-file-th" src="${f.url}" alt="">` : f.url && kind === 'video' ? `<video class="ovc-file-th" src="${f.url}" muted preload="metadata"></video>` : `<span class="ovc-file-th ovc-file-ic">${f.pasted ? svg(IC.file) : ext}</span>`;++ chip.innerHTML = `${thumb}<span class="ovc-file-t"><span class="ovc-file-n"></span><span class="ovc-file-s"></span></span>`++ + (f.pasted ? `<button type="button" class="ovc-file-b" data-ovc-back aria-label="Put the text back in the box" title="Put it back in the box">${svg(IC.back)}</button>` : '')++ + `<button type="button" class="ovc-file-x" data-ovc-x aria-label="Remove">${svg(IC.x)}</button>`;++ chip.querySelector('.ovc-file-n').textContent = f.pasted ? 'Pasted text' : f.file.name || 'File';++ chip.querySelector('.ovc-file-s').textContent = f.error || (f.pasted ? `${f.lines} lines · ${size(f.file.size)}` : f.progress != null && f.progress < 1 ? `${Math.round(f.progress * 100)}%` : size(f.file.size));++ chip.querySelector('[data-ovc-x]').setAttribute('aria-label', `Remove ${f.pasted ? 'the pasted text' : f.file.name || 'the file'}`);++ if (!chip.isConnected) strip.appendChild(chip);++ }++ function remove(f) {++ const i = files.indexOf(f); if (i < 0) return;++ files.splice(i, 1);++ if (f.url) URL.revokeObjectURL(f.url);++ if (f.hidden) f.hidden.remove();++ form.dispatchEvent(new CustomEvent('ovc:file-removed', { detail: { file: f.file, chip: f.chip } }));++ f.chip.remove();++ strip.hidden = !files.length;++ syncInputs();++ syncSend();++ }++ function add(list, pasted) {++ const up = uploader();++ for (const file of [...(list || [])]) {++ if (files.length >= maxFiles) { say(`Up to ${maxFiles} files at a time.`); break; }++ if (!accepts(file)) { say(`${file.name || 'That file'}: this box does not take that kind of file.`); continue; }++ if (file.size > maxBytes) { say(`${file.name || 'That file'} is ${size(file.size)}: the limit is ${size(maxBytes)}.`); continue; }++ const kind = (file.type || '').split('/')[0];++ const f = { file, url: kind === 'image' || kind === 'video' ? URL.createObjectURL(file) : null, pasted: !!pasted, lines: pasted ? pasted.lines : 0, text: pasted ? pasted.text : null };++ files.push(f);++ paintFile(f);++ form.dispatchEvent(new CustomEvent('ovc:file-added', { detail: { file, chip: f.chip } }));++ if (up) {++ f.progress = 0; paintFile(f);++ Promise.resolve(up(file, (p) => { f.progress = Math.max(0, Math.min(0.99, Number(p) || 0)); paintFile(f); syncSend(); }))++ .then((r) => {++ if (!files.includes(f)) return;++ f.progress = 1; f.id = r && r.id;++ if (f.id) { f.hidden = el('input'); f.hidden.type = 'hidden'; f.hidden.name = form.getAttribute('data-ovc-ids') || 'attachments'; f.hidden.value = f.id; form.appendChild(f.hidden); }++ paintFile(f); syncSend();++ }, (e) => { f.error = (e && e.message) || 'Did not upload'; paintFile(f); syncSend(); });++ }++ }++ strip.hidden = !files.length;++ syncInputs();++ syncSend();++ }++ strip.addEventListener('click', (e) => {++ const chip = e.target.closest && e.target.closest('.ovc-file'); if (!chip) return;++ const f = files.find((x) => x.chip === chip); if (!f) return;++ if (e.target.closest('[data-ovc-x]')) { remove(f); text.focus(); }++ else if (e.target.closest('[data-ovc-back]')) { const t = f.text; remove(f); ctx.insert(t); }++ });++ form.addEventListener('change', (e) => {++ const i = e.target; if (noFiles || !i || i.type !== 'file' || i.__ovcSync) return;++ const picked = [...(i.files || [])];++ i.__ovcSync = true; i.value = ''; i.__ovcSync = false;++ add(picked);++ });++ text.addEventListener('paste', (e) => {++ const cd = e.clipboardData; if (!cd || !canFile()) return;++ const got = [...(cd.files || [])];++ if (got.length) { e.preventDefault(); add(got); return; }++ const t = cd.getData('text/plain') || '';++ const lines = t.split('\n').length;++ if (t.length > 6000 || lines > 120) {++ // Only where the box takes a text file (or a mod uploads): a box for pictures keeps the paste as text.++ const asFile = new File([t], 'pasted-text.txt', { type: 'text/plain' });++ if (!uploader() && !accepts(asFile)) return;++ e.preventDefault();++ add([asFile], { lines, text: t });++ say('The long paste went in as a file: ↩ on it puts it back in the box.');++ }++ });++ const zone = form.closest('[data-ovt]') || form;++ let depth = 0;++ const hasFiles = (e) => e.dataTransfer && [...(e.dataTransfer.types || [])].includes('Files');++ zone.addEventListener('dragenter', (e) => { if (!hasFiles(e) || !canFile()) return; depth++; form.classList.add('ovc-dropping'); });++ zone.addEventListener('dragleave', () => { if (depth && --depth === 0) form.classList.remove('ovc-dropping'); });++ zone.addEventListener('dragover', (e) => { if (hasFiles(e) && canFile()) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } });++ zone.addEventListener('drop', (e) => { if (!hasFiles(e) || !canFile()) return; e.preventDefault(); depth = 0; form.classList.remove('ovc-dropping'); add(e.dataTransfer.files); text.focus(); });++++ // Send waits for words or a file, and for uploads to finish.++ const busy = () => files.some((f) => f.progress != null && f.progress < 1 && !f.error);++ function syncSend() {++ if (!send || form.classList.contains('is-busy')) return;++ send.disabled = busy() || (!text.value.trim() && !files.length);++ if (wasRequired && text.required && files.length) text.required = false;++ else if (wasRequired && !files.length && !text.required && !form.classList.contains('is-busy')) text.required = true;++ }++ text.addEventListener('input', syncSend);++++ // The slash list and the mention list: one popover above the box.++ const pop = el('div', 'ovc-pop');++ pop.setAttribute('role', 'listbox');++ pop.id = `ovc-pop-${Math.random().toString(36).slice(2, 8)}`;++ pop.hidden = true;++ form.appendChild(pop);++ text.setAttribute('aria-controls', pop.id);++ let items = [], at = 0, trig = null, seq = 0;++ function siteCommands() {++ let list = [];++ try { list = JSON.parse(form.getAttribute('data-ovt-commands') || '[]'); } catch { /* none */ }++ return list.filter((c) => c && /^[\w-]{1,30}$/.test(c.name || ''));++ }++ function commands() {++ const own = (prefs.snippets || []).map((s) => ({ name: s.trigger, hint: s.label || s.text.slice(0, 60), insert: s.text, mine: true }));++ const fromMods = mine().flatMap((m) => m.commands || []);++ return [...siteCommands(), ...fromMods, ...own, { name: 'settings', hint: 'Your composer: what sends, voice, your own commands', run: () => openSettings() }];++ }++ function caretWord() {++ const upto = text.value.slice(0, text.selectionStart);++ let m = upto.match(/(?:^|\n)\/([\w-]{0,30})$/);++ if (m) return { kind: 'slash', q: m[1], from: upto.length - m[1].length - 1 };++ m = upto.match(/(?:^|[\s(])@([\w.-]{0,30})$/);++ if (m && mine().some((x) => x.mentions && typeof x.mentions.search === 'function')) return { kind: 'at', q: m[1], from: upto.length - m[1].length - 1 };++ return null;++ }++ function close() { pop.hidden = true; items = []; trig = null; text.removeAttribute('aria-activedescendant'); text.setAttribute('aria-expanded', 'false'); }++ function paintPop() {++ if (!items.length) { close(); return; }++ pop.innerHTML = '';++ items.forEach((it, i) => {++ const b = el('button', 'ovc-pop-i');++ b.type = 'button'; b.id = `${pop.id}-${i}`;++ b.setAttribute('role', 'option');++ b.setAttribute('aria-selected', i === at ? 'true' : 'false');++ const lead = it.avatar ? el('img', 'ovc-pop-av') : el('span', 'ovc-pop-ic', svg(trig && trig.kind === 'at' ? IC.at : it.name === 'settings' ? IC.gear : IC.slash));++ if (it.avatar) { lead.src = it.avatar; lead.alt = ''; }++ const t = el('span', 'ovc-pop-t');++ const n = el('b'); n.textContent = trig && trig.kind === 'at' ? it.label : `/${it.name}`;++ const h = el('span'); h.textContent = trig && trig.kind === 'at' ? (it.sub || (it.handle ? `@${it.handle}` : '')) : it.hint || '';++ t.append(n, h);++ b.append(lead, t);++ if (it.mine) b.appendChild(el('span', 'ovc-pop-tag', 'Yours'));++ b.addEventListener('mousedown', (e) => e.preventDefault());++ b.addEventListener('click', () => choose(i));++ pop.appendChild(b);++ });++ pop.hidden = false;++ text.setAttribute('aria-expanded', 'true');++ text.setAttribute('aria-activedescendant', `${pop.id}-${at}`);++ const cur = pop.children[at]; if (cur && cur.scrollIntoView) cur.scrollIntoView({ block: 'nearest' });++ }++ async function look() {++ const w = caretWord();++ if (!w) { close(); return; }++ trig = w; const mineSeq = ++seq;++ if (w.kind === 'slash') {++ const q = w.q.toLowerCase();++ items = commands().filter((c) => !q || c.name.toLowerCase().startsWith(q) || (c.hint || '').toLowerCase().includes(q)).slice(0, 12);++ at = 0; paintPop(); return;++ }++ const srcs = mine().filter((m) => m.mentions && typeof m.mentions.search === 'function');++ const lists = await Promise.all(srcs.map((m) => Promise.resolve(m.mentions.search(w.q)).catch(() => [])));++ if (mineSeq !== seq) return;++ items = lists.flat().filter((x) => x && x.label).slice(0, 8);++ at = 0; paintPop();++ }++ function choose(i) {++ const it = items[i]; if (!it || !trig) return;++ const end = text.selectionStart;++ const from = trig.from, kind = trig.kind;++ close();++ if (kind === 'at') { text.setRangeText(it.insert || `@${it.handle || it.label} `, from, end, 'end'); fire(); text.focus(); return; }++ text.setRangeText('', from, end, 'end');++ if (typeof it.run === 'function') { fire(); try { it.run(ctx); } catch (e) { console.error(e); } return; }++ text.setRangeText(String(it.insert || ''), from, from, 'end'); fire(); text.focus();++ }++ let lookT = 0;++ text.addEventListener('input', () => { clearTimeout(lookT); lookT = setTimeout(() => { lookT = 0; look(); }, trig && trig.kind === 'at' ? 140 : 0); });++ text.addEventListener('blur', () => setTimeout(() => { if (document.activeElement !== text) close(); }, 120));++ text.setAttribute('aria-autocomplete', 'list');++++ // Keys, before the thread's own: the list's arrows and Enter, what sends, and the marks.++ form.addEventListener('keydown', (e) => {++ if (e.target !== text) return;++ // Enter right after the last letter: the slash list is read now, so "/settings" + Enter picks it.++ if (e.key === 'Enter' && lookT) { clearTimeout(lookT); lookT = 0; const w = caretWord(); if (w && w.kind === 'slash') look(); }++ if (!pop.hidden && items.length) {++ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); e.stopPropagation(); at = (at + (e.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length; paintPop(); return; }++ if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); choose(at); return; }++ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); return; }++ }++ const mod = mac ? e.metaKey : e.ctrlKey;++ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {++ if (mod) { e.preventDefault(); e.stopPropagation(); if (!send || !send.disabled) ctx.send(); return; }++ if (prefs.send_key === 'mod-enter') { e.stopPropagation(); return; }++ if (busy()) { e.preventDefault(); e.stopPropagation(); say('A file is still uploading.'); return; }++ if (!text.value.trim() && files.length && (root.matchMedia ? root.matchMedia('(pointer: fine)').matches : true)) { e.preventDefault(); e.stopPropagation(); ctx.send(); return; }++ }++ const wrap = { b: '**', i: '_', e: '`' }[mod && !e.altKey && !e.shiftKey ? e.key.toLowerCase() : ''];++ if (wrap) {++ e.preventDefault(); e.stopPropagation();++ const a = text.selectionStart, z = text.selectionEnd, sel = text.value.slice(a, z);++ text.setRangeText(wrap + sel + wrap, a, z, 'select');++ if (!sel) text.setSelectionRange(a + wrap.length, a + wrap.length);++ else text.setSelectionRange(a + wrap.length, z + wrap.length);++ fire();++ }++ }, true);++++ // The send: the mods may hold it; files still uploading hold it; the draft goes.++ const draftKey = form.hasAttribute('data-ovc-no-draft') ? null : `ov:draft:${location.pathname}#${form.id || form.getAttribute('action') || ''}`;++ form.addEventListener('submit', (e) => {++ if (busy()) { e.preventDefault(); e.stopImmediatePropagation(); say('A file is still uploading.'); return; }++ for (const m of mine()) {++ if (typeof m.beforeSend !== 'function') continue;++ let ok; try { ok = m.beforeSend(ctx); } catch (err) { console.error(err); }++ if (ok === false) { e.preventDefault(); e.stopImmediatePropagation(); return; }++ }++ if (draftKey) store.set(draftKey, null);++ close();++ }, true);++ if (draftKey) {++ const d = store.get(draftKey);++ if (d && d.t && !text.value && Date.now() - (d.at || 0) < 7 * 864e5) { text.value = d.t; fire(); }++ let dt = 0;++ text.addEventListener('input', () => { clearTimeout(dt); dt = setTimeout(() => store.set(draftKey, text.value.trim() ? { t: text.value, at: Date.now() } : null), 400); });++ }++++ // The count, near the box's limit.++ let count = null;++ if (text.maxLength > 0 && row) {++ count = el('span', 'ovc-count'); count.setAttribute('aria-live', 'polite');++ row.insertBefore(count, send && send.parentNode === row ? send : null);++ // The count shows only near the limit: the last 15% of a box, at most 200 and at least 20 characters (a 120-character++ // search box said "120 left" while empty, 2026-11-11).++ const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.min(200, Math.max(20, Math.round(text.maxLength * 0.15))); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };++ text.addEventListener('input', paintCount); paintCount();++ }++++ // Voice: the browser listens, the words go in at the caret as they come.++ let mic = null, rec = null;++ function micButton() {++ if (!Speech || !tools) return;++ if (!prefs.voice) { if (mic) { mic.remove(); mic = null; } return; }++ if (mic) return;++ mic = el('button', 'ovt-btn ovc-mic', svg(IC.mic));++ mic.type = 'button'; mic.setAttribute('aria-label', 'Speak'); mic.title = 'Speak'; mic.setAttribute('aria-pressed', 'false');++ mic.addEventListener('click', () => {++ if (rec) { rec.stop(); return; }++ rec = new Speech();++ rec.lang = document.documentElement.lang || navigator.language || 'en-US';++ rec.interimResults = true; rec.continuous = true;++ const a = text.selectionStart, z = text.selectionEnd, head = text.value.slice(0, a), tail = text.value.slice(z);++ const pad = head && !/\s$/.test(head) ? ' ' : '';++ rec.onresult = (ev) => { let s = ''; for (let i = 0; i < ev.results.length; i++) s += ev.results[i][0].transcript; text.value = head + pad + s + tail; text.setSelectionRange((head + pad + s).length, (head + pad + s).length); fire(); };++ rec.onend = () => { rec = null; mic.setAttribute('aria-pressed', 'false'); mic.title = 'Speak'; text.focus(); };++ rec.onerror = (ev) => { if (ev.error === 'not-allowed') say('The microphone is blocked for this site.'); };++ try { rec.start(); mic.setAttribute('aria-pressed', 'true'); mic.title = 'Stop listening'; } catch { rec = null; }++ });++ tools.appendChild(mic);++ }++++ // The mods' tools: buttons in the row, in the order the mods came.++ let modTools = [];++ function paintTools() {++ modTools.forEach((b) => b.remove()); modTools = [];++ if (!tools) return;++ for (const t of mine().flatMap((m) => m.tools || [])) {++ if (!t || !t.label || typeof t.run !== 'function') continue;++ const b = el('button', 'ovt-btn ovc-tool', svg(PATH_OK.test(t.path || '') ? t.path : IC.slash));++ b.type = 'button'; b.setAttribute('aria-label', t.label); b.title = t.label;++ b.addEventListener('click', () => { try { t.run(ctx); } catch (e) { console.error(e); } });++ tools.insertBefore(b, mic && mic.parentNode === tools ? mic : null);++ modTools.push(b);++ }++ }++++ const api = {++ form, ctx,++ refresh() { form.classList.toggle('ovc-compact', prefs.density === 'compact'); micButton(); paintTools(); syncSend(); },++ add, files: () => files.map((f) => f.file), clearFiles: () => { [...files].forEach(remove); },++ uploads: () => files.map((f) => f.id).filter(Boolean),++ clear() { files.slice().forEach(remove); text.value = ''; if (draftKey) store.set(draftKey, null); fire(); },++ destroy() { live.delete(api); pop.remove(); strip.remove(); if (mic) mic.remove(); modTools.forEach((b) => b.remove()); form.__ovc = null; },++ };++ form.__ovc = api;++ form.addEventListener('reset', () => setTimeout(() => api.clear(), 0));++ live.add(api);++ form.classList.add('ovc');++ api.refresh();++ return api;++ }++++ // ── Your settings, in a sheet: what sends, voice, compact, your own commands.++ let sheet = null;++ function openSettings(from) {++ const opener = from || document.activeElement;++ needWindow().then((W) => settingsIn(W, opener));++ }++ function settingsIn(W, opener) {++ let win = null;++ if (!W && !sheet) {++ sheet = el('dialog', 'ovc-set');++ sheet.setAttribute('aria-label', 'Your composer');++ sheet.addEventListener('click', (e) => { if (e.target === sheet) sheet.close(); });++ document.body.appendChild(sheet);++ }++ const k = mac ? '⌘' : 'Ctrl';++ const host = W ? el('div', 'ovc-set ovc-set-in') : sheet;++ host.innerHTML = `<form method="dialog" class="ovc-set-f">${W ? '' : `<header class="ovc-set-h"><b>Your composer</b><button class="ovt-btn" value="done" aria-label="Done">${svg(IC.x)}</button></header>`}++<fieldset class="ovc-set-g"><legend>Send with</legend><label><input type="radio" name="send_key" value="enter"> Enter <small>(Shift+Enter for a new line)</small></label><label><input type="radio" name="send_key" value="mod-enter"> ${k}+Enter <small>(Enter for a new line)</small></label></fieldset>++<label class="ovc-set-c"><input type="checkbox" name="voice"> A microphone button${Speech ? '' : ' <small>(this browser cannot listen)</small>'}</label>++<label class="ovc-set-c"><input type="checkbox" name="compact"> A compact box</label>++<fieldset class="ovc-set-g"><legend>Your commands</legend><p class="ovc-set-p">Type / and the word, and its text goes in. Up to 50.</p><ul class="ovc-set-l"></ul>++<div class="ovc-set-add"><input name="trigger" placeholder="word" maxlength="30" pattern="[A-Za-z0-9_-]+" aria-label="The word after /"><textarea name="snippet" rows="2" maxlength="4000" placeholder="The text it puts in" aria-label="The text it puts in"></textarea><button type="button" class="ovc-set-btn" data-ovc-add>Add</button></div></fieldset>++<p class="ovc-set-p">${synced ? 'Saved to your account: the same on every OpenVibe site.' : 'Saved in this browser.'}</p></form>`;++ const f = host.querySelector('form');++ f.send_key.value = prefs.send_key;++ f.voice.checked = !!prefs.voice; f.voice.disabled = !Speech;++ f.compact.checked = prefs.density === 'compact';++ const list = host.querySelector('.ovc-set-l');++ const paintList = () => {++ list.innerHTML = '';++ (prefs.snippets || []).forEach((s, i) => {++ const li = el('li');++ const w = el('b'); w.textContent = `/${s.trigger}`;++ const t = el('span'); t.textContent = s.text.slice(0, 80);++ const x = el('button', 'ovt-btn', svg(IC.x)); x.type = 'button'; x.setAttribute('aria-label', `Remove /${s.trigger}`);++ x.addEventListener('click', () => { const next = prefs.snippets.slice(); next.splice(i, 1); setPrefs({ snippets: next }); paintList(); });++ li.append(w, t, x); list.appendChild(li);++ });++ };++ paintList();++ f.addEventListener('change', (e) => {++ if (e.target.name === 'send_key') setPrefs({ send_key: f.send_key.value });++ else if (e.target.name === 'voice') setPrefs({ voice: f.voice.checked });++ else if (e.target.name === 'compact') setPrefs({ density: f.compact.checked ? 'compact' : 'comfy' });++ });++ host.querySelector('[data-ovc-add]').addEventListener('click', () => {++ const w = f.trigger.value.trim().replace(/^\//, ''), t = f.snippet.value;++ if (!/^[\w-]{1,30}$/.test(w)) { f.trigger.focus(); return; }++ if (!t.trim()) { f.snippet.focus(); return; }++ setPrefs({ snippets: (prefs.snippets || []).filter((s) => s.trigger !== w).concat([{ trigger: w, text: t }]) });++ f.trigger.value = ''; f.snippet.value = '';++ paintList(); f.trigger.focus();++ });++ f.addEventListener('submit', (e) => e.preventDefault());++ if (W) win = W.open({ id: 'ov-composer-settings', title: 'Your composer', body: host, kind: 'dialog', size: 'sm', from: opener, focus: 'input[name=send_key]:checked' });++ else { if (typeof sheet.showModal === 'function') sheet.showModal(); else sheet.setAttribute('open', ''); const first = f.querySelector('input[name=send_key]:checked'); if (first) first.focus(); }++ return win;++ }++++ function mod(m) {++ if (!m || !m.id) return;++ const i = mods.findIndex((x) => x.id === m.id);++ if (i >= 0) mods[i] = m; else mods.push(m);++ live.forEach((c) => c.refresh());++ }++ function scan(scope) { (scope || document).querySelectorAll('[data-ovt-composer], [data-ovc]').forEach((f) => enhance(f)); }++++ root.OpenVibeComposer = { enhance, mod, scan, settings: openSettings, get prefs() { return Object.assign({}, prefs); }, setPrefs };++ if (typeof module === 'object' && module.exports) module.exports = { enhance, mod, scan };++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => scan(), { once: true }); else scan();++ document.addEventListener('ov:boost:load', () => { live.forEach((c) => { if (!c.form.isConnected) c.destroy(); }); scan(); });++ pull();++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,815 @@++/**++ * ov-float.js (openvibe-shared 4.208.0): the move engine behind a picture-in-picture, a panel window or any box that++ * floats over a page. It is Live's PiP move (public/js/stream-pip.js, 2026-11-11: "move it anywhere … mobile and++ * desktop … more solid feeling and more responsive") lifted out whole, so every floating thing behaves the same:++ *++ * const f = OVFloat.attach(el, { frame, key, defaults, aspect, minPx, onChange });++ *++ * - Docked in `frame` (an element) or floating over the page (position: fixed). It follows the finger or the mouse++ * with a transform each animation frame and lands with a spring (FLIP); a flick carries on where it was thrown.++ * - Dropped over its frame it docks; anywhere else it floats. Near an edge it snaps; thrown past a side it tucks into++ * that side, leaving a tab with its picture still showing. Two fingers pinch it, the corner sizes it, a double tap++ * toggles small and large, and with focus the arrows move it, + and − size it, T tucks, F pops it out of its frame++ * (or back in) and Home puts it back where it started.++ * - Its place is kept as fractions of its frame: in this browser under `key`, and in the person's account when the++ * page has GET/PATCH /api/me/layout (the `pip` map Live keeps). `OVFloat.store` is the account plug:++ * { get(key), put(key, value), load?() }; assign your own to keep it somewhere else.++ * - `f.geom()`, `f.set(patch, { save })`, `f.tuck()`, `f.untuck()`, `f.toggleMode()`, `f.toggleSize()`, `f.reset()`,++ * `f.paint()`, `f.settle()`, `f.save()`, `f.relayout()`, `f.destroy()`. `OVFloat.layout()` lists every attached box,++ * and each save dispatches `ov:float` ({ key, geom, el }).++ *++ * The CSS (injected once) is behaviour, not looks: positioning, the lift, the tuck's translate, the tab and the resize++ * grip. A component adds its own colours, border, shadow and children. `aspect: null`, `size: false` and++ * `resize: false` let a panel window keep its own height and width while it gets the move, the tuck and the place.++ */++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OVFloat) return;++++ const BASE = 16 / 9; // the shape `w` is measured for++ const MIN_W = 0.08, MAX_W = 0.60;++ const MAX_H = 0.85; // of the frame's height, whatever the shape++ const MIN_PX = 150; // the smallest 16:9 width on a phone-sized frame++ const SNAP = 0.04; // within this fraction of an edge, a drop snaps to it++ const EDGE_PX = 12; // the margin it snaps to++ const TAB_PX = 30; // what stays on screen of a tucked box++ const DRAG_PX = 4; // a press that moves less is a tap++ const BIG_W = 0.5; // a double tap's large size++ const PROJECT_MS = 120; // how far a flick carries++ const TAP_MS = 300;++ const ACTIVE_MS = 3200;++ const LAND_MS = 300;++ const DEFAULT_IGNORE = 'a, button, input, textarea, select, [contenteditable], .ov-float-resize, [data-ov-float-resize], [data-ov-float-no-drag]';++++ const CSS = `:where(.ov-float) { position: absolute; box-sizing: border-box; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; }++.ov-float.is-floating { position: fixed; z-index: 900; }++.ov-float.is-lifted { position: fixed; z-index: 905; will-change: transform; transition: none; }++.ov-float.is-dragging { cursor: grabbing; }++.ov-float.is-tucked-left { translate: calc(-100% + var(--ov-float-tab, ${TAB_PX}px)) 0; }++.ov-float.is-tucked-right { translate: calc(100% - var(--ov-float-tab, ${TAB_PX}px)) 0; }++.ov-float.is-tucked .ov-float-tab { display: grid; }++.ov-float-tab { position: absolute; top: 0; bottom: 0; z-index: 4; width: var(--ov-float-tab, ${TAB_PX}px); display: none; place-items: center; padding: 0; border: 0; cursor: pointer;++ color: #fff; font: 600 15px/1 system-ui, sans-serif; background: rgba(10, 12, 18, .55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }++.ov-float.is-tucked-left .ov-float-tab { right: 0; }++.ov-float.is-tucked-right .ov-float-tab { left: 0; background: linear-gradient(270deg, rgba(0, 0, 0, .55), rgba(0, 0, 0, .25)); }++.ov-float-tab-l, .ov-float-tab-r { display: none; }++.ov-float.is-tucked-left .ov-float-tab-r, .ov-float.is-tucked-right .ov-float-tab-l { display: block; }++.ov-float-resize { position: absolute; right: 0; bottom: 0; z-index: 3; width: 22px; height: 22px; cursor: nwse-resize; touch-action: none; opacity: 0;++ background: linear-gradient(135deg, transparent 52%, rgba(255, 255, 255, .72) 52%, rgba(255, 255, 255, .72) 60%, transparent 60%, transparent 70%, rgba(255, 255, 255, .72) 70%, rgba(255, 255, 255, .72) 78%, transparent 78%); }++.ov-float:hover .ov-float-resize, .ov-float:focus-within .ov-float-resize, .ov-float.is-resizing .ov-float-resize { opacity: 1; }++@media (pointer: coarse) { .ov-float-resize { width: 30px; height: 30px; opacity: .85; } }++html[data-ov-motion="off"] .ov-float { transition: none !important; }++html[data-ov-motion="off"] .ov-float-tab, html[data-ov-motion="off"] .ov-float-resize { transition: none !important; }++@media (prefers-reduced-motion: reduce) {++ html:not([data-ov-motion="on"]) .ov-float { transition: none !important; }++}`;++++ const instances = new Set();++ let uid = 0;++++ function clamp(v, lo, hi, dflt) {++ const n = Number(v);++ if (!Number.isFinite(n)) return dflt;++ return Math.min(hi, Math.max(lo, n));++ }++++ function viewport(inst) {++ const de = document.documentElement;++ // A floating element's percentage place is measured on the document's client box, not on innerWidth when a++ // scrollbar takes 15 px of it. Live's PiP keeps the window size it always used; a free panel asks for `client`.++ const client = inst && inst.options.viewport === 'client' && de;++ const w = (client ? de.clientWidth : root.innerWidth) || 0;++ const h = (client ? de.clientHeight : root.innerHeight) || 0;++ return { left: 0, top: 0, width: w, height: h, right: w, bottom: h };++ }++++ function rectOf(el) {++ return el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : viewport();++ }++++ /** The box an absolutely positioned child measures its percentages on: the frame's padding box, not its border. */++ function contentRect(el) {++ const r = rectOf(el);++ if (!el || !el.clientWidth || !el.clientHeight) return r;++ const left = r.left + (el.clientLeft || 0), top = r.top + (el.clientTop || 0);++ return { left, top, width: el.clientWidth, height: el.clientHeight, right: left + el.clientWidth, bottom: top + el.clientHeight };++ }++++ function reduced() {++ const m = document.documentElement && document.documentElement.getAttribute('data-ov-motion');++ if (m === 'off') return true;++ try { return m !== 'on' && root.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; }++ }++++ function inFullscreen(inst) {++ const fs = typeof document !== 'undefined' && document.fullscreenElement;++ return !!(fs && inst && inst.frame && (fs === inst.frame || fs.contains(inst.frame)));++ }++++ function frameRect(inst) {++ if (inst.geom.mode === 'float' && !inFullscreen(inst)) return viewport(inst);++ return inst.frame ? contentRect(inst.frame) : viewport(inst);++ }++++ /* ── geometry (pure; the tests call these) ─────────────────────────────────────────────────────────────── */++++ /**++ * The box as fractions of its frame: width, height and the place it may go, for the source's own shape.++ * aspect: null keeps the element's own height (a panel window); `elHeight` is its measured height.++ */++ function boxOf(geom, rect, opts) {++ const o = opts || {};++ const a = o.aspect === null ? null : (Number(o.aspect) > 0 ? Number(o.aspect) : BASE);++ const minW = o.minW == null ? MIN_W : o.minW;++ const maxW = o.maxW == null ? MAX_W : o.maxW;++ const maxH = o.maxH == null ? MAX_H : o.maxH;++ const rw = (rect && rect.width) || 0, rh = (rect && rect.height) || 0;++ const rl = (rect && rect.left) || 0, rt = (rect && rect.top) || 0;++ let w = clamp(geom && geom.w, minW, maxW, Math.min(0.25, maxW));++ if (a == null) {++ return { a: null, wf: w, hf: rh && o.elHeight ? o.elHeight / rh : 0, rw, rh, rl, rt };++ }++ // On a phone-sized frame a quarter of the width is a postage stamp: never smaller than a 150 px wide 16:9 box++ // (the saved size is unchanged, so a desktop shows it as saved).++ const minPx = o.minPx == null ? MIN_PX : o.minPx;++ if (rw && rw < (o.phoneFramePx == null ? 640 : o.phoneFramePx)) w = Math.max(w, Math.min(maxW, minPx / rw));++ let wf = w * Math.sqrt(a / BASE); // the same area as a 16:9 box of width w++ if (rw && rh && (wf * rw) / a > maxH * rh) wf = (maxH * rh * a) / rw;++ wf = Math.min(wf, 1);++ const hf = rw && rh ? (wf * rw) / a / rh : wf / a;++ return { a, wf, hf, rw, rh, rl, rt };++ }++++ /** After a drop: keep it inside, and snap it to an edge it was dropped close to. */++ function settleGeom(geom, box, opts) {++ const o = opts || {};++ const edge = o.edgePx == null ? EDGE_PX : o.edgePx;++ const snap = o.snap == null ? SNAP : o.snap;++ const mx = box.rw ? edge / box.rw : 0, my = box.rh ? edge / box.rh : 0;++ const maxX = Math.max(0, 1 - box.wf), maxY = Math.max(0, 1 - (box.hf || 0));++ let x = clamp(geom.x, 0, maxX, 0), y = clamp(geom.y, 0, maxY, 0);++ if (x < snap) x = Math.min(mx, maxX); else if (x > maxX - snap) x = Math.max(0, maxX - mx);++ if (y < snap) y = Math.min(my, maxY); else if (y > maxY - snap) y = Math.max(0, maxY - my);++ geom.x = x; geom.y = y;++ return geom;++ }++++ /** The last 90 ms of a drag: the speed it was let go at, and where a flick carries it. */++ function flick(hist, from, opts) {++ const o = opts || {};++ const h = Array.isArray(hist) && hist.length ? hist : [{ x: 0, y: 0, t: 0 }];++ const a = h[0], z = h[h.length - 1];++ const dt = Math.max(16, (z.t || 0) - (a.t || 0));++ const vx = ((z.x || 0) - (a.x || 0)) / dt, vy = ((z.y || 0) - (a.y || 0)) / dt;++ const fast = Math.hypot(vx, vy) > (o.fastAt == null ? 0.6 : o.fastAt);++ return { vx, vy, fast, left: from.left + (fast ? vx * PROJECT_MS : 0), top: from.top + (fast ? vy * PROJECT_MS : 0) };++ }++++ /** Past a side of its frame by more than a third of itself (or thrown at it): tucked into that side. */++ function tuckFor(point, from, frame, speed, opts) {++ const o = opts || {};++ const edge = o.edgePx == null ? EDGE_PX : o.edgePx;++ const out = from.width / 3;++ if (point.left < frame.left - out || (speed.fast && speed.vx < -1.2 && point.left < frame.left + edge)) return 'left';++ if (point.left + from.width > frame.right + out || (speed.fast && speed.vx > 1.2 && point.left + from.width > frame.right - edge)) return 'right';++ return null;++ }++++ function overFrame(cx, cy, frame) {++ return !!frame && cx >= frame.left && cx <= frame.right && cy >= frame.top && cy <= frame.bottom;++ }++++ /** Draw a geometry: the percentages, the shape, and the state classes the component styles. */++ function paint(el, geom, box, opts) {++ const o = opts || {};++ const tuck = geom.tuck;++ const floating = !!o.floating;++ const classes = el.classList;++ classes.toggle('is-floating', floating);++ classes.toggle('is-tucked', !!tuck);++ classes.toggle('is-tucked-left', tuck === 'left');++ classes.toggle('is-tucked-right', tuck === 'right');++ classes.toggle('is-portrait', !!box.a && box.a < 1);++ // Shown inside its frame even when a turn of the phone or a smaller window would push it out; the saved place++ // is not changed, so it comes back when there is room again. A tucked one sits at its side (CSS `translate`++ // leaves only its tab on screen; `transform` stays free for the landing).++ const left = tuck === 'left' ? 0 : tuck === 'right' ? Math.max(0, 1 - box.wf) : Math.min(Math.max(0, geom.x), Math.max(0, 1 - box.wf));++ el.style.left = `${left * 100}%`;++ el.style.top = `${Math.min(Math.max(0, geom.y), Math.max(0, 1 - (box.hf || 0))) * 100}%`;++ if (o.size !== false) {++ el.style.width = `${box.wf * 100}%`;++ if (box.a) el.style.aspectRatio = String(box.a);++ }++ }++++ /* ── the account plug (GET/PATCH /api/me/layout, the shape Live's stream-pip.js keeps) ───────────────── */++++ function makeDefaultStore() {++ const state = { data: null, loading: null, pending: {}, timer: 0, off: false };++ function load() {++ if (state.off || typeof fetch !== 'function') return Promise.resolve(null);++ if (!state.loading) {++ state.loading = fetch('/api/me/layout', { credentials: 'same-origin', headers: { accept: 'application/json' } })++ .then((r) => {++ if (r.status === 400 || r.status === 401 || r.status === 403 || r.status === 404) { state.off = true; return null; }++ return r.ok ? r.json() : null;++ })++ .then((d) => { state.data = d && typeof d === 'object' ? d : {}; return state.data; })++ .catch(() => null);++ }++ return state.loading;++ }++ function get(key) {++ const pip = state.data && state.data.pip;++ return pip && pip[key] ? pip[key] : null;++ }++ function put(key, value) {++ if (state.off || typeof fetch !== 'function') return;++ state.pending[key] = value;++ if (state.data) { state.data.pip = state.data.pip || {}; state.data.pip[key] = value; }++ clearTimeout(state.timer);++ state.timer = setTimeout(() => {++ const pip = state.pending; state.pending = {};++ fetch('/api/me/layout', {++ method: 'PATCH', credentials: 'same-origin',++ headers: { 'content-type': 'application/json', accept: 'application/json' },++ body: JSON.stringify({ pip }),++ }).then((r) => { if (r.status === 400 || r.status === 401 || r.status === 403 || r.status === 404) state.off = true; })++ .catch(() => { /* the next move tries again */ });++ }, 1500);++ }++ return { load, get, put, _state: state };++ }++++ function normalizeDefaults(raw, hasFrame) {++ const d = raw && typeof raw === 'object' ? raw : {};++ return {++ x: clamp(d.x, 0, 1, 0.72), y: clamp(d.y, 0, 1, 0.70), w: clamp(d.w, MIN_W, MAX_W, 0.25),++ mode: hasFrame ? (d.mode === 'float' ? 'float' : 'dock') : 'float',++ tuck: d.tuck === 'left' || d.tuck === 'right' ? d.tuck : null,++ muted: typeof d.muted === 'boolean' ? d.muted : undefined,++ at: 0,++ };++ }++++ function normalize(raw, fallback, opts) {++ const o = opts || {};++ if (!raw || typeof raw !== 'object') return Object.assign({}, fallback);++ const g = Object.assign({}, fallback);++ g.x = clamp(raw.x, 0, 1, fallback.x);++ g.y = clamp(raw.y, 0, 1, fallback.y);++ g.w = clamp(raw.w, o.minW == null ? MIN_W : o.minW, o.maxW == null ? MAX_W : o.maxW, fallback.w);++ g.mode = o.hasFrame === false ? 'float' : (raw.mode === 'float' ? 'float' : raw.mode === 'dock' ? 'dock' : fallback.mode);++ g.tuck = raw.tuck === 'left' || raw.tuck === 'right' ? raw.tuck : null;++ if (typeof raw.muted === 'boolean') g.muted = raw.muted;++ g.at = Number(raw.at) || 0;++ return g;++ }++++ function readSaved(key, legacyKeys) {++ const keys = [key].concat(Array.isArray(legacyKeys) ? legacyKeys : []);++ for (const k of keys) {++ try {++ const raw = root.localStorage && root.localStorage.getItem(k);++ if (!raw) continue;++ const v = JSON.parse(raw);++ if (v && typeof v === 'object') return v;++ } catch { /* private mode, or a key from another shape */ }++ }++ return null;++ }++++ /* ── the instance ──────────────────────────────────────────────────────────────────────────────────────── */++++ function box(inst) {++ return boxOf(inst.geom, frameRect(inst), {++ aspect: inst.aspect, minPx: inst.minPx, minW: inst.minW, maxW: inst.maxW,++ elHeight: inst.el.offsetHeight || 0,++ });++ }++++ function paintNow(inst) {++ const b = box(inst);++ paint(inst.el, inst.geom, b, {++ floating: inst.geom.mode === 'float' && !inFullscreen(inst),++ size: inst.options.size !== false,++ });++ return b;++ }++++ function settleInst(inst) {++ return settleGeom(inst.geom, box(inst));++ }++++ function placeIn(inst) {++ const fs = document.fullscreenElement;++ let want;++ if (inst.geom.mode === 'float' && !inFullscreen(inst)) want = document.body;++ else if (inst.frame) want = fs && !fs.contains(inst.frame) ? fs : inst.frame;++ else want = document.body;++ if (want && inst.el.parentNode !== want) {++ const v = inst.el.querySelector('video');++ want.appendChild(inst.el);++ // A media element moved in one task keeps playing; play() covers a browser that paused it.++ if (v && v.paused) v.play().catch(() => {});++ }++ }++++ function save(inst) {++ if (inst.destroyed) return;++ // A drop clears the lifted inline left/top before it saves; draw the final place first so an onChange that++ // reads the element's rect (a window's absolute place, for one) sees where it landed, not its static position.++ if (!inst.el.classList.contains('is-lifted')) paintNow(inst);++ inst.geom.at = Date.now();++ try { if (root.localStorage) root.localStorage.setItem(inst.key, JSON.stringify(inst.geom)); } catch { /* private mode */ }++ if (inst.store && typeof inst.store.put === 'function') {++ try { inst.store.put(inst.accountKey, Object.assign({}, inst.geom)); } catch { /* the page's own store */ }++ }++ if (typeof inst.options.onChange === 'function') {++ try { inst.options.onChange(Object.assign({}, inst.geom), inst.api); } catch (err) { console.error(err); }++ }++ try {++ root.dispatchEvent(new root.CustomEvent('ov:float', { detail: { key: inst.key, geom: Object.assign({}, inst.geom), el: inst.el } }));++ } catch { /* an environment without CustomEvent */ }++ }++++ /** Move it to its place with a spring from where it was (`from`: its rect on screen before). */++ function land(inst, from) {++ placeIn(inst);++ const el = inst.el;++ el.style.transition = 'none';++ el.style.transform = '';++ paintNow(inst);++ if (!from || reduced()) { el.style.transition = ''; return; }++ const to = el.getBoundingClientRect();++ if (!to.width) { el.style.transition = ''; return; }++ const dx = from.left - to.left, dy = from.top - to.top, s = from.width / to.width;++ if (Math.abs(dx) < 1 && Math.abs(dy) < 1 && Math.abs(s - 1) < 0.01) { el.style.transition = ''; return; }++ el.style.transformOrigin = '0 0';++ el.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(${s})`;++ void el.offsetWidth;++ el.style.transition = `transform ${LAND_MS}ms var(--ov-ease-spring, cubic-bezier(.2, 1.25, .3, 1))`;++ el.style.transform = '';++ clearTimeout(inst.landTimer);++ inst.landTimer = setTimeout(() => {++ if (!inst.destroyed) { el.style.transition = ''; el.style.transformOrigin = ''; }++ }, LAND_MS + 40);++ }++++ /** Switch frames keeping its size and place on screen (`rect`: where it is now). */++ function toMode(inst, mode, rect) {++ if (!inst.frame) mode = 'float';++ inst.geom.mode = mode;++ const b0 = box(inst);++ if (!b0.rw || !b0.rh) return;++ const a = inst.aspect || BASE;++ const k = Math.sqrt(a / BASE) || 1;++ if (inst.options.size !== false) inst.geom.w = clamp(rect.width / b0.rw / k, inst.minW, inst.maxW, inst.geom.w);++ const b = box(inst);++ inst.geom.x = (rect.left - b.rl) / b.rw;++ inst.geom.y = (rect.top - b.rt) / b.rh;++ settleInst(inst);++ }++++ function tuck(inst, side) {++ const from = inst.el.getBoundingClientRect();++ if (side === null) { inst.geom.tuck = null; }++ else {++ const b = box(inst);++ inst.geom.tuck = side || (inst.geom.x + b.wf / 2 < 0.5 ? 'left' : 'right');++ }++ save(inst);++ land(inst, from);++ }++++ function untuck(inst) {++ const from = inst.el.getBoundingClientRect();++ const b = box(inst);++ inst.geom.tuck = null;++ inst.geom.x = from.left + from.width / 2 < (b.rl + b.rw / 2)++ ? (b.rw ? EDGE_PX / b.rw : 0)++ : Math.max(0, 1 - b.wf - (b.rw ? EDGE_PX / b.rw : 0));++ settleInst(inst);++ save(inst);++ land(inst, from);++ }++++ function toggleMode(inst) {++ if (!inst.frame) return false;++ const from = inst.el.getBoundingClientRect();++ toMode(inst, inst.geom.mode === 'float' ? 'dock' : 'float', from);++ save(inst);++ land(inst, from);++ return true;++ }++++ function toggleSize(inst) {++ const from = inst.el.getBoundingClientRect();++ const big = inst.options.bigW == null ? BIG_W : inst.options.bigW;++ inst.geom.w = inst.geom.w < big - 0.01 ? big : clamp(inst.defaults.w, inst.minW, inst.maxW, inst.defaults.w);++ inst.geom.tuck = null;++ settleInst(inst);++ save(inst);++ land(inst, from);++ }++++ function reset(inst) {++ const from = inst.el.getBoundingClientRect();++ const d = inst.defaults;++ Object.assign(inst.geom, { x: d.x, y: d.y, w: d.w, mode: inst.frame ? d.mode : 'float', tuck: null });++ settleInst(inst);++ save(inst);++ land(inst, from);++ }++++ function relayout(inst) {++ if (inst.destroyed || inst.el.classList.contains('is-lifted')) return;++ placeIn(inst);++ paintNow(inst);++ }++++ function syncAccount(inst) {++ const store = inst.store;++ if (!store || typeof store.get !== 'function') return;++ const apply = () => {++ if (inst.destroyed || inst.touched) return;++ const remote = normalize(store.get(inst.accountKey), inst.defaults, { hasFrame: !!inst.frame, minW: inst.minW, maxW: inst.maxW });++ if ((remote.at || 0) > (inst.geom.at || 0)) {++ inst.geom = remote;++ placeIn(inst);++ paintNow(inst);++ }++ };++ if (typeof store.load === 'function') {++ let p;++ try { p = store.load(); } catch { p = null; }++ if (p && typeof p.then === 'function') p.then(apply).catch(() => { /* no account on this page */ });++ else apply();++ } else apply();++ }++++ /* ── gestures ──────────────────────────────────────────────────────────────────────────────────────────── */++++ function wireMove(inst) {++ const el = inst.el;++ const handle = inst.options.handle && inst.options.handle.addEventListener ? inst.options.handle : el;++ const ignore = inst.options.ignore || DEFAULT_IGNORE;++ const hintClass = inst.options.hintClass || 'ov-float-dock-hint';++ const pts = new Map(); // pointerId → { x, y }++ let drag = null, pinch = null, raf = 0, lastTap = 0, activeTimer = 0;++ const gap = () => { const [a, b] = [...pts.values()]; return Math.hypot(a.x - b.x, a.y - b.y) || 1; };++ const hint = (on) => { if (inst.frame && inst.frame.classList) inst.frame.classList.toggle(hintClass, !!on); };++ const paint = () => {++ raf = 0;++ if (!drag || !drag.lifted) return;++ el.style.transform = `translate3d(${drag.dx}px, ${drag.dy}px, 0)`;++ const c = drag.r;++ const p = inst.frame ? contentRect(inst.frame) : null;++ if (p) hint(overFrame(c.left + drag.dx + c.width / 2, c.top + drag.dy + c.height / 2, p));++ };++ const lift = () => {++ drag.lifted = true;++ // Over the page from the first move, at the same place on screen, so it can go anywhere.++ const r = drag.r;++ const layer = document.fullscreenElement || document.body;++ if (el.parentNode !== layer) layer.appendChild(el);++ // Moving the element in the DOM releases any pointer capture it had; take the pointers it is following back.++ for (const id of pts.keys()) { try { el.setPointerCapture(id); } catch { /* a pointer the browser no longer tracks */ } }++ el.classList.add('is-dragging', 'is-lifted');++ el.style.transition = 'none';++ el.style.left = `${r.left}px`; el.style.top = `${r.top}px`; el.style.width = `${r.width}px`;++ const v = el.querySelector('video');++ if (v && v.paused) v.play().catch(() => {});++ if (typeof inst.options.onLift === 'function') { try { inst.options.onLift(inst.api); } catch { /* */ } }++ };++ const showActive = () => {++ el.classList.add('is-active');++ clearTimeout(activeTimer);++ activeTimer = setTimeout(() => el.classList.remove('is-active'), ACTIVE_MS);++ };++ const onDown = (e) => {++ if (e.button > 0) return;++ if (e.target.closest && e.target.closest(ignore)) return;++ if (typeof inst.options.canDrag === 'function' && !inst.options.canDrag(e)) return;++ inst.touched = true;++ pts.set(e.pointerId, { x: e.clientX, y: e.clientY });++ try { el.setPointerCapture(e.pointerId); } catch { /* a pointer the browser no longer tracks */ }++ e.preventDefault();++ if (pts.size === 2) {++ if (drag && drag.lifted) el.style.transform = '';++ drag = null;++ pinch = { d: gap(), w: inst.geom.w };++ el.classList.add('is-resizing');++ return;++ }++ if (pts.size > 2) return;++ drag = {++ sx: e.clientX, sy: e.clientY, dx: 0, dy: 0, r: el.getBoundingClientRect(), lifted: false,++ hist: [{ x: e.clientX, y: e.clientY, t: performance.now() }],++ };++ };++ const onMove = (e) => {++ if (pts.has(e.pointerId)) pts.set(e.pointerId, { x: e.clientX, y: e.clientY });++ if (pinch && pts.size === 2) {++ inst.geom.w = clamp(pinch.w * (gap() / pinch.d), inst.minW, inst.maxW, pinch.w);++ paintNow(inst);++ return;++ }++ if (!drag) return;++ const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;++ if (!drag.lifted && Math.hypot(dx, dy) < DRAG_PX) return;++ if (!drag.lifted) lift();++ // Anywhere on screen; most of it may go past a side (that is how it tucks), never past the top or bottom.++ const vp = viewport(inst);++ const r = drag.r, vw = vp.width, vh = vp.height, tab = inst.options.tabPx == null ? TAB_PX : inst.options.tabPx;++ drag.dx = Math.min(Math.max(dx, -r.left - r.width + tab), vw - r.left - tab);++ drag.dy = Math.min(Math.max(dy, -r.top), vh - r.top - r.height);++ const now = performance.now();++ drag.hist.push({ x: e.clientX, y: e.clientY, t: now });++ while (drag.hist.length > 2 && now - drag.hist[0].t > 90) drag.hist.shift();++ if (!raf) raf = requestAnimationFrame(paint);++ };++ const onUp = (e) => {++ pts.delete(e.pointerId);++ try { el.releasePointerCapture(e.pointerId); } catch { /* */ }++ if (pinch) {++ if (pts.size < 2) {++ pinch = null;++ el.classList.remove('is-resizing');++ // A pinch that began after the lift put the box over the page; put it back in its frame for its mode.++ if (el.classList.contains('is-lifted')) {++ el.classList.remove('is-dragging', 'is-lifted');++ el.style.left = ''; el.style.top = ''; el.style.width = ''; el.style.transform = '';++ placeIn(inst);++ }++ settleInst(inst);++ paintNow(inst);++ save(inst);++ }++ return;++ }++ if (!drag) return;++ const d = drag; drag = null;++ if (raf) { cancelAnimationFrame(raf); raf = 0; }++ hint(false);++ if (!d.lifted) {++ // A tap: the bar for a moment (a touch screen has no hover); a second within 300 ms toggles the size.++ const now = performance.now();++ if (inst.options.doubleTap !== false && now - lastTap < TAP_MS) { lastTap = 0; toggleSize(inst); }++ else {++ lastTap = now;++ if (e.pointerType !== 'mouse' && inst.options.activeBar !== false) showActive();++ }++ if (inst.geom.tuck && e.type === 'pointerup') untuck(inst);++ return;++ }++ el.classList.remove('is-dragging', 'is-lifted');++ // Where it was let go, and where a flick carries it (the last 90 ms, 120 ms on).++ const fl = flick(d.hist, { left: d.r.left + d.dx, top: d.r.top + d.dy });++ const from = { left: d.r.left + d.dx, top: d.r.top + d.dy, width: d.r.width, height: d.r.height };++ const vp = viewport(inst);++ const vhei = vp.height;++ const top = Math.min(Math.max(0, fl.top), vhei - from.height);++ const p = inst.frame ? contentRect(inst.frame) : null;++ const fs = inFullscreen(inst);++ const overPlayer = !!p && (fs || overFrame(fl.left + from.width / 2, top + from.height / 2, p));++ const frame = overPlayer ? p : vp;++ const tuckSide = tuckFor({ left: fl.left }, from, frame, fl);++ el.style.transform = '';++ el.style.left = ''; el.style.top = ''; el.style.width = '';++ toMode(inst, overPlayer ? 'dock' : 'float', {++ left: Math.min(Math.max(frame.left, fl.left), frame.right - from.width),++ top,++ width: from.width,++ height: from.height,++ });++ inst.geom.tuck = tuckSide;++ save(inst);++ land(inst, from);++ };++ handle.addEventListener('pointerdown', onDown);++ el.addEventListener('pointermove', onMove);++ el.addEventListener('pointerup', onUp);++ el.addEventListener('pointercancel', onUp);++ inst.cleanups.push(() => {++ handle.removeEventListener('pointerdown', onDown);++ el.removeEventListener('pointermove', onMove);++ el.removeEventListener('pointerup', onUp);++ el.removeEventListener('pointercancel', onUp);++ clearTimeout(activeTimer);++ hint(false);++ });++ }++++ function wireKeys(inst) {++ const el = inst.el;++ const onKey = (e) => {++ if (e.target !== el || e.altKey || e.ctrlKey || e.metaKey) return;++ const step = e.shiftKey ? 0.1 : 0.02;++ const k = e.key;++ let moved = true;++ if (k === 'ArrowLeft') inst.geom.x -= step;++ else if (k === 'ArrowRight') inst.geom.x += step;++ else if (k === 'ArrowUp') inst.geom.y -= step;++ else if (k === 'ArrowDown') inst.geom.y += step;++ else if (k === '+' || k === '=') inst.geom.w = clamp(inst.geom.w + 0.03, inst.minW, inst.maxW, inst.geom.w);++ else if (k === '-' || k === '_') inst.geom.w = clamp(inst.geom.w - 0.03, inst.minW, inst.maxW, inst.geom.w);++ else if (k === 'Home') { e.preventDefault(); reset(inst); return; }++ else if (k === 't' || k === 'T' || k === 'h' || k === 'H') { e.preventDefault(); inst.geom.tuck ? untuck(inst) : tuck(inst); return; }++ else if (k === 'f' || k === 'F') { e.preventDefault(); toggleMode(inst); return; }++ else return;++ e.preventDefault();++ if (!moved) return;++ const b = box(inst);++ inst.geom.tuck = null;++ inst.geom.x = Math.min(Math.max(0, inst.geom.x), Math.max(0, 1 - b.wf));++ inst.geom.y = Math.min(Math.max(0, inst.geom.y), Math.max(0, 1 - b.hf));++ paintNow(inst);++ save(inst);++ };++ el.addEventListener('keydown', onKey);++ inst.cleanups.push(() => el.removeEventListener('keydown', onKey));++ }++++ function ensureTab(inst) {++ let tab = inst.el.querySelector('[data-ov-float-tab]');++ if (!tab) {++ tab = document.createElement('button');++ tab.type = 'button';++ tab.className = 'ov-float-tab';++ tab.dataset.ovFloatTab = '';++ tab.setAttribute('aria-label', 'Bring it back');++ tab.innerHTML = '<span class="ov-float-tab-l" aria-hidden="true">‹</span><span class="ov-float-tab-r" aria-hidden="true">›</span>';++ inst.el.append(tab);++ inst.ownedTab = tab;++ }++ const onClick = (e) => { e.preventDefault(); e.stopPropagation(); untuck(inst); };++ tab.addEventListener('click', onClick);++ inst.cleanups.push(() => tab.removeEventListener('click', onClick));++ }++++ function ensureResize(inst) {++ let handle = inst.el.querySelector('[data-ov-float-resize]');++ if (!handle) {++ handle = document.createElement('span');++ handle.className = 'ov-float-resize';++ handle.dataset.ovFloatResize = '';++ handle.setAttribute('aria-hidden', 'true');++ inst.el.append(handle);++ inst.ownedResize = handle;++ }++ let start = null;++ const onDown = (e) => {++ if (e.button > 0) return;++ const b = box(inst);++ start = { px: e.clientX, py: e.clientY, w: inst.geom.w, rw: b.rw };++ inst.touched = true;++ inst.el.classList.add('is-resizing');++ try { handle.setPointerCapture(e.pointerId); } catch { /* */ }++ e.preventDefault();++ e.stopPropagation();++ };++ const onMove = (e) => {++ if (!start || !start.rw) return;++ // The handle's diagonal pull is the box's own width; `w` is the 16:9 width of the same area.++ const k = inst.aspect ? Math.sqrt(inst.aspect / BASE) || 1 : 1;++ const pull = inst.aspect ? ((e.clientX - start.px) + (e.clientY - start.py) * inst.aspect) / 2 : (e.clientX - start.px);++ inst.geom.w = clamp(start.w + pull / start.rw / k, inst.minW, inst.maxW, start.w);++ const b = box(inst);++ inst.geom.x = Math.min(inst.geom.x, Math.max(0, 1 - b.wf));++ inst.geom.y = Math.min(inst.geom.y, Math.max(0, 1 - b.hf));++ paintNow(inst);++ };++ const end = (e) => {++ if (!start) return;++ start = null;++ inst.el.classList.remove('is-resizing');++ try { handle.releasePointerCapture(e.pointerId); } catch { /* */ }++ settleInst(inst);++ paintNow(inst);++ save(inst);++ };++ handle.addEventListener('pointerdown', onDown);++ handle.addEventListener('pointermove', onMove);++ handle.addEventListener('pointerup', end);++ handle.addEventListener('pointercancel', end);++ inst.cleanups.push(() => {++ handle.removeEventListener('pointerdown', onDown);++ handle.removeEventListener('pointermove', onMove);++ handle.removeEventListener('pointerup', end);++ handle.removeEventListener('pointercancel', end);++ });++ }++++ function wireWindow(inst) {++ const onResize = () => relayout(inst);++ root.addEventListener('resize', onResize);++ document.addEventListener('fullscreenchange', onResize);++ let ro = null;++ if (typeof ResizeObserver === 'function') {++ ro = new ResizeObserver(onResize);++ if (inst.frame) ro.observe(inst.frame);++ ro.observe(inst.el);++ }++ inst.cleanups.push(() => {++ root.removeEventListener('resize', onResize);++ document.removeEventListener('fullscreenchange', onResize);++ try { ro && ro.disconnect(); } catch { /* */ }++ });++ }++++ /* ── public API ───────────────────────────────────────────────────────────────────────────────────────── */++++ function attach(el, options) {++ if (!el || typeof el !== 'object' || !el.classList) throw new TypeError('OVFloat.attach(el, options): el is required');++ const o = options || {};++ const frame = o.frame || null;++ const key = String(o.key || `ov-float-${++uid}`);++ const minW = clamp(o.minW == null ? MIN_W : o.minW, 0, 1, MIN_W);++ const maxW = clamp(o.maxW == null ? MAX_W : o.maxW, minW, 1, MAX_W);++ const defaults = normalizeDefaults(o.defaults, !!frame);++ const inst = {++ el, frame, key,++ accountKey: o.accountKey == null ? key : String(o.accountKey),++ options: o, defaults, minW, maxW,++ aspect: o.aspect === null ? null : (typeof o.aspect === 'function' ? Number(o.aspect()) || BASE : (Number(o.aspect) > 0 ? Number(o.aspect) : BASE)),++ minPx: Number(o.minPx) > 0 ? Number(o.minPx) : MIN_PX,++ store: o.store === null ? null : (o.store || root.OVFloat.store || null),++ geom: null, touched: false, destroyed: false, cleanups: [], landTimer: 0, ownedTab: null, ownedResize: null, api: null,++ };++ inst.geom = normalize(readSaved(key, o.legacyKeys), defaults, { hasFrame: !!frame, minW, maxW });++ injectStyles();++ el.classList.add('ov-float');++ if (o.tab !== false) ensureTab(inst);++ if (o.resize !== false) ensureResize(inst);++ wireMove(inst);++ wireKeys(inst);++ wireWindow(inst);++ placeIn(inst);++ paintNow(inst);++++ inst.api = {++ el, key,++ geom: () => Object.assign({}, inst.geom),++ set(patch, opts) {++ Object.assign(inst.geom, patch || {});++ if (opts && opts.settle) settleInst(inst);++ placeIn(inst);++ paintNow(inst);++ if (opts && opts.save) save(inst);++ return Object.assign({}, inst.geom);++ },++ setAspect(a) {++ inst.aspect = a === null ? null : (Number(a) > 0 ? Number(a) : BASE);++ paintNow(inst);++ },++ metrics: () => box(inst),++ paint: () => paintNow(inst),++ settle: () => { settleInst(inst); paintNow(inst); return Object.assign({}, inst.geom); },++ save: () => save(inst),++ tuck: (side) => tuck(inst, side),++ untuck: () => untuck(inst),++ toggleTuck: () => (inst.geom.tuck ? untuck(inst) : tuck(inst)),++ toggleMode: () => toggleMode(inst),++ toggleSize: () => toggleSize(inst),++ reset: () => reset(inst),++ relayout: () => relayout(inst),++ destroy: () => destroy(inst),++ };++ instances.add(inst);++ syncAccount(inst);++ return inst.api;++ }++++ function destroy(inst) {++ if (inst.destroyed) return;++ inst.destroyed = true;++ instances.delete(inst);++ clearTimeout(inst.landTimer);++ for (const fn of inst.cleanups) { try { fn(); } catch { /* */ } }++ inst.cleanups = [];++ if (inst.ownedTab) { try { inst.ownedTab.remove(); } catch { /* */ } }++ if (inst.ownedResize) { try { inst.ownedResize.remove(); } catch { /* */ } }++ inst.el.classList.remove('ov-float', 'is-floating', 'is-lifted', 'is-dragging', 'is-tucked', 'is-tucked-left', 'is-tucked-right', 'is-resizing', 'is-active');++ }++++ function injectStyles() {++ if (document.getElementById('ov-float-styles')) return;++ const st = document.createElement('style');++ st.id = 'ov-float-styles';++ st.textContent = CSS;++ (document.head || document.documentElement).appendChild(st);++ }++++ const defaultStore = makeDefaultStore();++ root.OVFloat = {++ attach,++ store: defaultStore,++ newStore: makeDefaultStore,++ layout: () => [...instances].map((i) => ({ key: i.key, geom: Object.assign({}, i.geom), el: i.el })),++ _test: {++ boxOf, settle: settleGeom, flick, tuckFor, overFrame, paint, normalize,++ BASE, MAX_H, MIN_W, MAX_W, EDGE_PX, SNAP, PROJECT_MS,++ },++ };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,139 @@++/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */++/**++ * openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its++ * markup (<i class="fa-solid fa-tower-broadcast">) and shows the network's own icon instead: the same hand as the mark++ * (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet stays Font++ * Awesome's. A brand logo (fa-brands) is drawn from brands.js (Simple Icons, CC0; 4.72.0), fetched from beside this++ * file the first time a page shows one, filled in the text's colour with the brand's own as --ovg-brand (add++ * ovg-brand-color to wear it). A spinner is three dots that take turns, never a spin.++ *++ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,++ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the++ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light++ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that++ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured. The glyphs'++ * own motion: ovg-draw draws the lines in once, ovg-once plays an animation a single time (4.63.0).++ *++ * <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later++ * OVGlyphs.swap(rootEl) swaps inside one element now++ * OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null++ *++ * 272 glyphs for 449 Font Awesome names (openvibe.design/icons shows them).++ */++(function () {++ 'use strict';++ if (typeof window === 'undefined' || window.OVGlyphs) return;++ const G = {"airplane":"<path d=\"M12 3.2c0.9 0 1.4 0.9 1.4 2.4V9.8l7 4.2V15.4l-7-2.4V17.2l1.9 1.4V20L12 19.1l-3.3 0.9v-1.4l1.9-1.4v-4.2L3.6 15.4v-1.4L10.6 9.8V5.6C10.6 4.2 11.1 3.2 12 3.2Z\"/>","alert":"<path d=\"M12 4.5L3.6 18.8h16.9Z\"/><path d=\"M12 10v3.7M12 16.5h0\"/>","alert-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5v5.5M12 16.5h.01\"/>","align-left":"<path d=\"M4 5.9h16.6M4 10h10.4M4 14.3h16.6M4 18.5h10.4\"/>","archive":"<rect x=\"3.5\" y=\"4.8\" width=\"17\" height=\"4.7\" rx=\"0.9\"/><path d=\"M5.4 9.5v9.4a0.9 0.9 0 0 0 0.9 0.9h11.3a0.9 0.9 0 0 0 0.9-0.9V9.5M10.1 13.3h3.8\"/>","arrow-down":"<path d=\"M12 3.2v16.7M5.7 13.7l6.3 6.3l6.3-6.3\"/>","arrow-left":"<path d=\"M20.8 12H4M10.3 5.7l-6.3 6.3l6.3 6.3\"/>","arrow-right":"<path d=\"M3.2 12h16.7M13.7 5.7l6.3 6.3l-6.3 6.3\"/>","arrow-up":"<path d=\"M12 20.8V4M5.7 10.3l6.3-6.3l6.3 6.3\"/>","arrows-h":"<path d=\"M3.8 12h16.5M7.6 8.1l-3.9 3.9l3.9 3.9M16.4 8.1l3.9 3.9l-3.9 3.9\"/>","at":"<circle cx=\"12\" cy=\"12\" r=\"3.4\"/><path d=\"M15.4 12v1.3a2.5 2.5 0 0 0 4.9 0V12a8.3 8.3 0 1 0-3.2 6.5\"/>","back":"<path d=\"M16.1 3.6l-8.4 8.4l8.4 8.4\"/>","ban":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m6 6 12 12\"/>","bell":"<path d=\"M6 14.6V9.5a6.1 6.1 0 0 1 12.2 0v5.1l1.5 2.1h-15.3ZM10 18.7a2.1 2.1 0 0 0 4 0\"/>","book":"<path d=\"M5 5.7a1.9 1.9 0 0 1 1.9-1.9h12.1v14.9H6.8a1.9 1.9 0 0 0-1.9 1.9Z\"/><path d=\"M5 18.7V5.7\"/>","book-open":"<path d=\"M12 6.3C10 4.8 7 4.3 3.5 4.9v13.2c3.5-0.6 6.5-0.1 8.5 1.4c2-1.5 5-2 8.5-1.4V4.9C17 4.3 14 4.8 12 6.3Z\"/><path d=\"M12 6.3v13.2\"/>","box":"<path d=\"M3.6 7.9L11.9 3.9l8.4 3.9v8.4L11.9 20.1l-8.4-3.9Z\"/><path d=\"M3.6 7.9L11.9 11.8l8.4-3.9M11.9 11.8v8.4M7.8 5.9l8.4 3.9\"/>","brain":"<path d=\"M9.2 4.2a2.8 2.8 0 0 0-2.8 2.8a2.8 2.8 0 0 0-1.9 4.8a2.8 2.8 0 0 0 1.9 4.8a2.8 2.8 0 0 0 5.8 1.1V5.8A1.9 1.9 0 0 0 9.2 4.2ZM14.9 4.2a2.8 2.8 0 0 1 2.8 2.8a2.8 2.8 0 0 1 1.9 4.8a2.8 2.8 0 0 1-1.9 4.8a2.8 2.8 0 0 1-5.8 1.1\"/>","branch":"<circle cx=\"6.7\" cy=\"5.6\" r=\"1.9\"/><circle cx=\"6.7\" cy=\"18.6\" r=\"1.9\"/><circle cx=\"17.9\" cy=\"7.5\" r=\"1.9\"/><path d=\"M6.7 7.5v9.3M17.9 9.3c0 5.1-6 3.7-10.2 7.9\"/>","bridge":"<path d=\"M3.5 16h17M7 19.5V5M17 19.5V5\"/><path d=\"M3.5 10c1.8 0 2.8-2.3 3.5-5c2.4 5.2 7.6 5.2 10 0c.7 2.7 1.7 5 3.5 5\"/><path d=\"M12 10.9V16\"/>","briefcase":"<rect x=\"3.5\" y=\"7.2\" width=\"17\" height=\"12.3\" rx=\"1.9\"/><path d=\"M9.2 7.2V5.7A1.4 1.4 0 0 1 10.6 4.3h2.8A1.4 1.4 0 0 1 14.8 5.7V7.2M3.5 12.3h7.1M13.4 12.3H20.5M10.6 11.4h2.8v2.4h-2.8Z\"/>","bug":"<rect x=\"7.2\" y=\"6.2\" width=\"9.6\" height=\"13.3\" rx=\"4.8\"/><path d=\"M9.3 6.5a2.7 2.7 0 0 1 5.3 0M12 10.9v8.5M7.2 12H3.5M20.5 12h-3.7M7.6 8.3L4.6 5.6M16.4 8.3L19.4 5.6M7.6 16.3L4.6 18.9M16.4 16.3L19.4 18.9\"/>","burst":"<path d=\"M11.9 3.7L13.6 7.8L17.8 6.2L16.2 10.4L20.3 12.1L16.2 13.8L17.8 18L13.6 16.4L11.9 20.4L10.2 16.4L6 18L7.6 13.8L3.6 12.1L7.6 10.4L6 6.2L10.2 7.8Z\"/>","busy":"<path d=\"M4.7 12h0M12.1 12h0M19.4 12h0\" stroke-width=\"4.2\"/>","calendar":"<rect x=\"4\" y=\"5.5\" width=\"16\" height=\"15\" rx=\"2\"/><path d=\"M4 10.5h16M9 3.5v4M15 3.5v4\"/>","camera":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><circle cx=\"12\" cy=\"13.3\" r=\"3.4\"/>","camera-flip":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><path d=\"M15 12.6a3.1 3.1 0 0 0-5.6-1.2M9 14.4a3.1 3.1 0 0 0 5.6 1.2M9.3 9.5v2h2M14.7 17.5v-2h-2\"/>","camera-off":"<path d=\"M3.7 7.9h3.3l1.7-2.7h6.4L16.9 7.9h3.3v11.2h-16.5Z\"/><circle cx=\"11.9\" cy=\"13\" r=\"3.3\"/><path d=\"M3.7 3.5l16.5 16.5\"/>","car":"<path d=\"M3.5 15.8v-4.5L6.2 5.8h11.6l2.7 5.5V15.8Z\"/><path d=\"M3.5 11.3h17M6.5 15.8v2.5M17.5 15.8v2.5M7.2 13.8h0M16.8 13.8h0\"/>","card":"<rect x=\"3.7\" y=\"5.8\" width=\"16.7\" height=\"12.3\" rx=\"1.8\"/><path d=\"M3.7 10.3h16.7M6.7 14.6h3.5\"/>","care":"<path d=\"M3.3 16.5h3.2l4.2 3.2h5.8a2.2 2.2 0 0 0 0-4.2H12.8M7.5 16.5l2.4-2.2h3.5\"/><path d=\"M16.6 10.6s-3.4-2.2-3.4-4.7a2.1 2.1 0 0 1 3.4-1.4a2.1 2.1 0 0 1 3.4 1.4c0 2.6-3.4 4.7-3.4 4.7Z\"/>","cart":"<path d=\"M3.5 4.1h1.9l2.2 10.2h10.2L20.3 6.9H6.5\"/><circle cx=\"9.1\" cy=\"18.6\" r=\"1.2\"/><circle cx=\"16.5\" cy=\"18.6\" r=\"1.2\"/>","cc":"<rect x=\"3.5\" y=\"5.4\" width=\"17\" height=\"13.2\" rx=\"2.4\"/><path d=\"M10.7 10.2a2.3 2.3 0 1 0 0 3.6M16.8 10.2a2.3 2.3 0 1 0 0 3.6\"/>","chart":"<path d=\"M3.9 3.4v16.7h16.7\"/><path d=\"M7.7 14.5l3.6-4.1l3.2 2.7l5.2-6.3\"/>","chat":"<path d=\"M3.8 4.3h16.8v11.7H8l-4.2 4.2Z\"/>","chat-off":"<path d=\"M4.2 5.1h15.7v10.9H8l-4 4Z\"/><path d=\"M3.7 3.7l16.7 16.7\"/>","check":"<path d=\"M3.7 12.1l5.4 5.4L20.4 6.1\"/>","check-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8 12.4 2.8 2.8L16 9.6\"/>","check-double":"<path d=\"M3.6 11.7l3.6 3.6L14.6 7.7M11.1 15.2l0.9 0.9L20.4 7.7\"/>","chevron":"<path d=\"M7.9 3.6l8.4 8.4l-8.4 8.4\"/>","chevrons-left":"<path d=\"M10.9 19.3l-7.3-7.3l7.3-7.3M20.4 19.3l-7.3-7.3l7.3-7.3\"/>","chevrons-right":"<path d=\"M3.6 19.3l7.3-7.3l-7.3-7.3M13.1 19.3l7.3-7.3l-7.3-7.3\"/>","child":"<circle cx=\"12\" cy=\"5.8\" r=\"2.1\"/><path d=\"M7.9 10.5L12 9.2l4.1 1.4M12 9.2v5.6M9.2 20.4l2.8-5.6l2.8 5.6\"/>","chip":"<rect x=\"7.3\" y=\"7.3\" width=\"9.4\" height=\"9.4\" rx=\"1.4\"/><path d=\"M10.1 3.5v3.8M13.9 3.5v3.8M10.1 16.7v3.8M13.9 16.7v3.8M3.5 10.1h3.8M3.5 13.9h3.8M16.7 10.1h3.8M16.7 13.9h3.8\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/>","city":"<path d=\"M3.5 19.7h17M5.4 19.7V8.3h4.7v11.3M10.1 19.7V3.6h5.7v16.1M15.8 19.7v-7.6h3.3v7.6M7.8 11.2h0M7.8 14.5h0M12.9 6.9h0M12.9 10.2h0M12.9 13.5h0\"/>","clapper":"<path d=\"M4 10.4h16.3v8.6a1 1 0 0 1-1 1h-14.4a1 1 0 0 1-1-1Z\"/><path d=\"M4 10.4L3.4 7.2l15.7-2.9l0.7 3.3ZM8.5 6.3l2.2 3.2M13.3 5.4l2.2 3.2\"/>","clipboard":"<rect x=\"5.7\" y=\"5\" width=\"12.6\" height=\"15.5\" rx=\"1.9\"/><path d=\"M9.1 5V3.5h5.8V5M9.1 10.8h5.8M9.1 14.7h3.9\"/>","clipboard-check":"<rect x=\"5.7\" y=\"4.9\" width=\"12.6\" height=\"15.5\" rx=\"1.9\"/><path d=\"M9.1 4.9V3.5h5.8V4.9M9.1 13.2l1.9 1.9l3.9-3.9\"/>","clock":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5V12l3 2\"/>","close":"<path d=\"M3.7 3.7l16.7 16.7M20.4 3.7L3.7 20.4\"/>","cloud":"<path d=\"M7.8 17.6h8.9a3.4 3.4 0 0 0 0.5-7A5.2 5.2 0 0 0 6.9 10.1A3.7 3.7 0 0 0 7.8 17.6Z\"/>","cloud-off":"<path d=\"M7.5 17.6h9.5a3.8 3.8 0 0 0 0.5-7.5A5.7 5.7 0 0 0 6.4 9.6A4.1 4.1 0 0 0 7.5 17.6Z\"/><path d=\"M4 3.9l16.1 16.1\"/>","code":"<path d=\"M7.8 7.8l-4.2 4.2l4.2 4.2M16.2 7.8l4.2 4.2l-4.2 4.2M14.2 4.6l-4.2 14.8\"/>","coin":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11\"/>","columns":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"15\" rx=\"2\"/><path d=\"M12 4.5v15M3.5 9h17\"/>","commit":"<circle cx=\"12\" cy=\"12\" r=\"3.4\"/><path d=\"M3.5 12h5.1M15.4 12H20.5\"/>","compass":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m15.6 8.4-2.1 5.1-5.1 2.1 2.1-5.1Z\"/>","compress":"<path d=\"M8.8 3.5v5.3H3.5M15.2 3.5v5.3h5.3M8.8 20.5v-5.3H3.5M15.2 20.5v-5.3h5.3\"/>","computer":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"11.3\" rx=\"1.9\"/><path d=\"M8.2 19.7h7.6M12 15.9v3.8\"/>","contrast":"<circle cx=\"11.7\" cy=\"12\" r=\"8.4\"/><path d=\"M11.7 3.6v16.7M11.7 7.9h6.5M11.7 12h8.4M11.7 16.2h6.5\"/>","cookie":"<path d=\"M20.6 12.3a8.4 8.4 0 1 1-8.9-8.9a3 3 0 0 0 3.9 3.5a3 3 0 0 0 4.9 5.3Z\"/><path d=\"M8.8 9.4h0M12.2 13.8h0M8.3 15.3h0M15.7 15.3h0\"/>","copy":"<rect x=\"8\" y=\"8\" width=\"12.5\" height=\"12.5\" rx=\"2.1\"/><path d=\"M16.3 8V5.8a2.1 2.1 0 0 0-2.1-2.1H5.8a2.1 2.1 0 0 0-2.1 2.1v8.4a2.1 2.1 0 0 0 2.1 2.1h2.1\"/>","couch":"<path d=\"M5.4 10.1V7.2a1.9 1.9 0 0 1 1.9-1.9h9.4a1.9 1.9 0 0 1 1.9 1.9v2.8\"/><path d=\"M3.5 11.9a1.9 1.9 0 0 1 3.8 0v1.4h9.4V11.9a1.9 1.9 0 0 1 3.8 0v4.7H3.5ZM5.4 16.7v1.9M18.6 16.7v1.9\"/>","crown":"<path d=\"M3.5 8.3L7.8 12L11.9 5.5l4.2 6.5l4.2-3.7L18.4 18.1H5.4Z\"/>","cubes":"<path d=\"M12 3.2L8.1 5.2v4.4l3.9 2l3.9-2V5.2ZM7.6 11.6L3.6 13.6v4.4l3.9 2l3.9-2V13.6ZM16.4 11.6l-3.9 2v4.4l3.9 2l3.9-2V13.6Z\"/>","database":"<ellipse cx=\"12\" cy=\"6.4\" rx=\"6.6\" ry=\"2.8\"/><path d=\"M5.4 6.4v11.3c0 1.6 2.9 2.8 6.6 2.8s6.6-1.2 6.6-2.8V6.4M5.4 12c0 1.6 2.9 2.8 6.6 2.8s6.6-1.2 6.6-2.8\"/>","dice":"<rect x=\"3.7\" y=\"3.6\" width=\"16.7\" height=\"16.7\" rx=\"3\"/><path d=\"M8.6 8.6h0M15.5 8.6h0M12.1 12h0M8.6 15.4h0M15.5 15.4h0\" stroke-width=\"2.6\"/>","dish":"<path d=\"M3.8 9.2A10.9 10.9 0 0 0 14.8 20.2Z\"/><path d=\"M9 15l5.2-5.2M14.8 3.4a5.8 5.8 0 0 1 5.8 5.8M14.8 7.5a1.8 1.8 0 0 1 1.8 1.8\"/>","dollar":"<path d=\"M15.7 8C15 6.6 13.7 5.6 12 5.6C9.6 5.6 8.3 6.9 8.3 8.5s1.6 2.5 3.7 2.8s3.7 1.3 3.7 3.1s-1.5 3-3.7 3c-1.8 0-3.2-0.9-3.7-2.4M12 3.7v16.7\"/>","donate":"<circle cx=\"11.9\" cy=\"8.4\" r=\"5.2\"/><path d=\"M11.9 6v4.3M3.7 15.6h16.5V20H3.7ZM8.3 17.7h7.2\"/>","door":"<path d=\"M13.7 4.7L6.3 3.2v16.7l7.5-1.4ZM13.7 5.1h3.3a0.9 0.9 0 0 1 0.9 0.9v14M3.5 20h16.7M11 11.6h0\"/>","down":"<path d=\"M3.6 7.9l8.4 8.4l8.4-8.4\"/>","download":"<path d=\"M12 3.2v11.5M6.8 9.5l5.2 5.2l5.2-5.2M4.7 20h14.7\"/>","drive":"<rect x=\"3.5\" y=\"12.3\" width=\"17\" height=\"6.6\" rx=\"1.9\"/><path d=\"M3.9 12.3L6.8 4.7h10.4l2.8 7.6M16.7 15.6h0M13.8 15.6h0\"/>","drop":"<path d=\"M12 3.5s5.9 6.4 5.9 10.8a5.9 5.9 0 0 1-11.8 0c0-4.4 5.9-10.8 5.9-10.8Z\"/>","ear":"<path d=\"M6.2 9.5a5.7 5.7 0 1 1 10.8 2.4c-0.9 1.9-3.1 3-3.1 5.2a3.4 3.4 0 0 1-6.5 1.6\"/><path d=\"M9.7 9.9a2.4 2.4 0 1 1 3.8 1.5\"/>","enter":"<path d=\"M13.8 3.6h4.1a2.1 2.1 0 0 1 2.1 2.1v12.5a2.1 2.1 0 0 1-2.1 2.1h-4.1M9.5 7.9l4.1 4.1l-4.1 4.1M13.8 12H3.2\"/>","eraser":"<path d=\"M14.3 4.4l5.5 5.5l-9 9H6.6l-2.6-2.6a1.5 1.5 0 0 1 0-2.1Z\"/><path d=\"M9.3 9.4l5.5 5.5M10.8 18.9H20.3\"/>","expand":"<path d=\"M3.5 8.8V3.5h5.3M20.5 8.8V3.5h-5.3M3.5 15.2v5.3h5.3M20.5 15.2v5.3h-5.3\"/>","eye":"<path d=\"M3.5 12S6.6 6.2 12 6.2S20.5 12 20.5 12S17.4 17.8 12 17.8S3.5 12 3.5 12Z\"/><circle cx=\"12\" cy=\"12\" r=\"2.7\"/>","eye-off":"<path d=\"M3.7 12s3.2-6.1 8.3-6.1c1.5 0 2.7 0.5 4 1.1M20.5 12s-3.2 6.1-8.3 6.1c-1.5 0-2.7-0.5-4-1.1\"/><path d=\"M10.1 9.9a2.7 2.7 0 0 0 3.9 3.9M4.6 4.5l14.9 14.9\"/>","fast-forward":"<path d=\"M4 6.4v11.2L11.6 12ZM13.1 6.4v11.2L20.7 12Z\"/>","file":"<path d=\"M6.4 3.5h7.6l3.8 3.8v13.2H6.4Z\"/><path d=\"M13.9 3.5v3.8h3.8\"/>","file-code":"<path d=\"M13.9 3.5H7.4a1.9 1.9 0 0 0-1.9 1.9v13a1.9 1.9 0 0 0 1.9 1.9h9.3a1.9 1.9 0 0 0 1.9-1.9V8.2Z\"/><path d=\"M13.9 3.5v4.6h4.6M10.1 12.4L8.3 14.3l1.9 1.9M13.9 12.4l1.9 1.9l-1.9 1.9\"/>","film":"<rect x=\"3.5\" y=\"4.4\" width=\"17\" height=\"15.1\" rx=\"1.9\"/><path d=\"M7.7 4.4v15.1M16.3 4.4v15.1M3.5 9.2h4.3M3.5 14.8h4.3M16.3 9.2H20.5M16.3 14.8H20.5\"/>","filter":"<path d=\"M3.5 6h17M6.7 12.4h10.6M9.9 18.7h4.2\"/>","finish":"<path d=\"M5.2 20.7V4\"/><rect x=\"5.2\" y=\"4.6\" width=\"13.9\" height=\"8.6\" rx=\"1\"/><path d=\"M5.2 8.8h13.9M9.6 4.6v8.6M14.3 4.6v8.6\"/>","fire":"<path d=\"M12 20.5a6.8 6.8 0 0 0 6.8-6.8c0-4.3-2.9-6.8-4.2-10.1c-2 2.3-2.6 4.5-2.6 4.5S10.7 5.8 9.5 3.8C7 6.6 5.2 10 5.2 13.7a6.8 6.8 0 0 0 6.8 6.8Z\"/>","fish":"<path d=\"M8.1 12c1.9-3.1 4.3-4.6 7.2-4.6c2.5 0 4.3 1.9 5.2 4.6c-1 2.7-2.8 4.6-5.2 4.6c-2.9 0-5.2-1.6-7.2-4.6Z\"/><path d=\"M8.1 12L3.7 8.8v6.7Z\"/><path d=\"M16.5 10.7h0\"/>","fist":"<path d=\"M6.4 10.1V6.4a1.6 1.6 0 0 1 3.2 0V9.1M9.5 8.5v-3.2a1.6 1.6 0 0 1 3.2 0v3.2M12.6 8.5v-2.1a1.6 1.6 0 0 1 3.2 0V9.1M15.7 9.1V8a1.6 1.6 0 0 1 3.2 0v5.2a7.3 7.3 0 0 1-7.3 7.3H11.1a5.7 5.7 0 0 1-5.7-5.7V12.3A2.1 2.1 0 0 1 7.4 11.2h2.1a1.6 1.6 0 0 1 0 3.2H8\"/>","flag":"<path d=\"M6.7 20.7V4M6.7 4h10.8l-2 3.9l2 3.9H6.7\"/>","flask":"<path d=\"M9.5 3.2h4.9M10 3.2V9.2L4.9 17.7a1.5 1.5 0 0 0 1.3 2.3h11.6a1.5 1.5 0 0 0 1.3-2.3L14 9.2V3.2\"/><path d=\"M7.3 14.6h9.5\"/>","focus":"<circle cx=\"12\" cy=\"12\" r=\"2.8\"/><path d=\"M3.5 9V5.9A2.4 2.4 0 0 1 5.9 3.5H9M15 3.5h3.1A2.4 2.4 0 0 1 20.5 5.9V9M20.5 15v3.1a2.4 2.4 0 0 1-2.4 2.4H15M9 20.5H5.9A2.4 2.4 0 0 1 3.5 18.1V15\"/>","folder":"<path d=\"M3.8 7.3a1.9 1.9 0 0 1 1.9-1.9h3.6l1.9 1.9h7.4a1.9 1.9 0 0 1 1.9 1.9v7.4a1.9 1.9 0 0 1-1.9 1.9H5.8a1.9 1.9 0 0 1-1.9-1.9Z\"/>","folder-tree":"<path d=\"M4.1 3.3v13.2a1 1 0 0 0 1 1H9.7M4.1 8.3H9.7\"/><path d=\"M10.2 5.3h3.1l1 1.5h5.6V10.9h-9.7ZM10.2 14.5h3.1l1 1.5h5.6V20.1h-9.7Z\"/>","food":"<path d=\"M8.2 4v6.5M6.3 4v4.2a1.9 1.9 0 0 0 3.7 0V4M8.2 10.5v10.2M17.5 20.8V4c-1.9 1.1-3 3.3-3 6.5v2.8H17.5\"/>","game":"<rect x=\"3.7\" y=\"7.2\" width=\"16.7\" height=\"9.7\" rx=\"4.4\"/><path d=\"M7.6 10.7v2.7M6.4 12h2.7M15.1 11.6h0M17.4 12.9h0\"/>","gauge":"<path d=\"M3.6 16.9a8.4 8.4 0 1 1 16.7 0\"/><path d=\"M11.9 13.2l4.1-5.7\"/><circle cx=\"11.9\" cy=\"13.2\" r=\"1.4\"/>","gavel":"<path d=\"M13.7 4.1l5.1 5.1M10.5 7.4l5.1 5.1M12.1 5.8l-5.6 5.6l5 5l5.6-5.6M3.5 19.5l5.9-5.9M13.7 19.5h6.5\"/>","gear":"<circle cx=\"12\" cy=\"12.1\" r=\"2.3\"/><path d=\"M17.6 14.3a1.3 1.3 0 0 0 0.2 1.5l0.1 0.1a1.5 1.5 0 1 1-2.4 2.4l-0.1-0.1a1.3 1.3 0 0 0-1.5-0.2a1.3 1.3 0 0 0-0.8 1.3V18.8a1.5 1.5 0 1 1-3.4 0v-0.1a1.3 1.3 0 0 0-0.9-1.3a1.3 1.3 0 0 0-1.5 0.2l-0.1 0.1a1.5 1.5 0 1 1-2.4-2.4l0.1-0.1a1.3 1.3 0 0 0 0.2-1.5a1.3 1.3 0 0 0-1.3-0.8H5.4a1.5 1.5 0 1 1 0-3.4h0.1a1.3 1.3 0 0 0 1.3-0.9a1.3 1.3 0 0 0-0.2-1.5l-0.1-0.1a1.5 1.5 0 1 1 2.4-2.4l0.1 0.1a1.3 1.3 0 0 0 1.5 0.2H9.8a1.3 1.3 0 0 0 0.8-1.3V5.4a1.5 1.5 0 1 1 3.4 0v0.1a1.3 1.3 0 0 0 0.8 1.3a1.3 1.3 0 0 0 1.5-0.2l0.1-0.1a1.5 1.5 0 1 1 2.4 2.4l-0.1 0.1a1.3 1.3 0 0 0-0.2 1.5V9.8a1.3 1.3 0 0 0 1.3 0.8H18.7a1.5 1.5 0 1 1 0 3.4h-0.1a1.3 1.3 0 0 0-1.3 0.8Z\"/>","gem":"<path d=\"M6.8 4.9h10.3L20.2 9.5l-8.3 10.3L3.6 9.5Z\"/><path d=\"M3.6 9.5h16.7M9.6 4.9L8.1 9.5l3.8 10.3M14.2 4.9L15.7 9.5l-3.8 10.3\"/>","ghost":"<path d=\"M4.7 20V10.6a7.3 7.3 0 0 1 14.7 0v9.4l-2.4-1.9l-2.5 1.9l-2.4-1.9L9.6 20l-2.5-1.9Z\"/><path d=\"M9.4 10.1h0M14.6 10.1h0\"/>","gift":"<rect x=\"4.4\" y=\"8.8\" width=\"15.1\" height=\"10.4\" rx=\"0.9\"/><path d=\"M3.5 8.8h17M12 8.8v10.4M12 8.8c-1.4-2.8-4.7-3.8-4.7-1.4S12 8.8 12 8.8s4.7-0.9 4.7-3.3S13.4 6 12 8.8\"/>","globe":"<circle cx=\"11.9\" cy=\"11.9\" r=\"8.3\"/><path d=\"M3.6 11.9h16.7M11.9 3.6c2.7 2.7 2.7 13.9 0 16.7M11.9 3.6c-2.7 2.7-2.7 13.9 0 16.7\"/>","grid":"<rect x=\"3.5\" y=\"3.5\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"13.1\" y=\"3.5\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"3.5\" y=\"13.1\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"13.1\" y=\"13.1\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/>","grip":"<path d=\"M8.3 4.5h0M15.8 4.5h0M8.3 12h0M15.8 12h0M8.3 19.5h0M15.8 19.5h0\" stroke-width=\"3.8\"/>","group":"<path d=\"M3.5 7.3V3.5h3.8M16.7 3.5h3.8v3.8M20.5 16.7v3.8h-3.8M7.3 20.5H3.5v-3.8\"/><rect x=\"7.3\" y=\"7.3\" width=\"5.7\" height=\"4.7\" rx=\"0.9\"/><rect x=\"11.1\" y=\"12\" width=\"5.7\" height=\"4.7\" rx=\"0.9\"/>","hammer":"<path d=\"M13.1 3.7L19.3 9.9l-2.7 2.7l-6.2-6.2Z\"/><path d=\"M13.6 9.4L5 18.1a1.3 1.3 0 0 0 1.9 1.9l8.6-8.6\"/>","hand":"<path d=\"M9.9 13.6V6.2a1.5 1.5 0 0 1 3 0V11.7M12.8 10.7V5.3a1.5 1.5 0 0 1 3 0V11.7M15.8 11.2V6.7a1.5 1.5 0 0 1 3 0v6.9c0 3.9-2.5 6.9-6.4 6.9c-2.6 0-4.1-1.3-5.6-3.5L4.9 14.2a1.5 1.5 0 0 1 2.4-1.8L9.9 15.6\"/>","handshake":"<path d=\"M3.6 9.9v5.3M20.3 9.9v5.3\"/><path d=\"M3.6 10.8h2.2l3.5-2.7l2.9 1.3L14.5 8.2l3.5 2.7h2.2\"/><path d=\"M5.7 10.8l6 6a1.2 1.2 0 0 0 1.7-1.6M13.4 15.3l0.5 0.5a1.2 1.2 0 0 0 1.7-1.6M15.6 14.2l0.3 0.3a1.2 1.2 0 0 0 1.7-1.6L18 10.8M17.5 12.8l-4-4l-2.3 2.1a1.3 1.3 0 0 1-1.9-1.9l1.9-2\"/>","hash":"<path d=\"M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17\"/>","heading":"<path d=\"M5.6 3.5v17M18.4 3.5v17M5.6 12h12.7M3.5 3.5h4.2M16.2 3.5h4.2M3.5 20.5h4.2M16.2 20.5h4.2\"/>","headphones":"<path d=\"M4.4 13.9v-2.8a7.5 7.5 0 0 1 14.9 0v2.8\"/><rect x=\"3.5\" y=\"13\" width=\"4.2\" height=\"6\" rx=\"1.4\"/><rect x=\"16.1\" y=\"13\" width=\"4.2\" height=\"6\" rx=\"1.4\"/>","heart":"<path d=\"M11.9 20.3s-8.1-5.1-8.1-11.6a4.6 4.6 0 0 1 8.1-3A4.6 4.6 0 0 1 20 9.2c0 6.5-8.1 11.6-8.1 11.6Z\"/>","heart-broken":"<path d=\"M11.9 20.3s-8.1-5.1-8.1-11.6a4.6 4.6 0 0 1 8.1-3A4.6 4.6 0 0 1 20 9.2c0 6.5-8.1 11.6-8.1 11.6Z\"/><path d=\"M11.9 5.8L10 9.8l3.5 2.3l-1.6 3.9\"/>","heart-pulse":"<path d=\"M19 12.3c-1.7 3.6-7 6.7-7 6.7s-5.3-3.1-7-6.7M4.3 9.7A4.3 4.3 0 0 1 12 7.4a4.3 4.3 0 0 1 7.7 2.2\"/><path d=\"M3.5 11.9h4l1.3-2.2l2.2 4.5l1.8-3.1h7.6\"/>","help":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01\"/>","history":"<path d=\"M3.8 12.1a8.3 8.3 0 1 0 2.5-5.9L3.8 8.5M3.8 4.2v4.2h4.2\"/><path d=\"M12 7.9v4.3l3.1 1.9\"/>","home":"<path d=\"M3.6 10.6L12 3.3l8.4 7.3v8.4a1 1 0 0 1-1 1h-4.2v-6.3h-6.3v6.3H4.7a1 1 0 0 1-1-1Z\"/>","hot-tub":"<circle cx=\"8.6\" cy=\"5.8\" r=\"1.7\"/><path d=\"M3.8 11.9h16.6v3.3a4.2 4.2 0 0 1-4.2 4.2h-8.4A4.2 4.2 0 0 1 3.8 15.2ZM6.8 11.9v-1.4A2.4 2.4 0 0 1 8.9 8.2H9.4M13.7 9.5c0.8-0.9-0.8-1.9 0-2.8M17.3 9.5c0.8-0.9-0.8-1.9 0-2.8\"/>","hourglass":"<path d=\"M6 3.5h12M6 20.5h12M7.5 3.5c0 4.5 4.5 5.5 4.5 8.5s-4.5 4-4.5 8.5M16.5 3.5c0 4.5-4.5 5.5-4.5 8.5s4.5 4 4.5 8.5\"/>","id":"<rect x=\"3.7\" y=\"5.5\" width=\"16.7\" height=\"13\" rx=\"1.9\"/><circle cx=\"9.2\" cy=\"11\" r=\"1.9\"/><path d=\"M6.4 15.6c0.5-1.4 1.6-2.2 2.8-2.2s2.4 0.8 2.8 2.2M14.8 10.1h2.8M14.8 13.4h2.8\"/>","idea":"<path d=\"M9.1 17.6h5.8M10.1 20.5h3.9M8.7 14.3A5.8 5.8 0 1 1 15.2 14.3c-0.7 0.6-1.4 1.5-1.4 2.3h-3.9c0-0.9-0.7-1.7-1.4-2.3Z\"/>","image":"<rect x=\"3.5\" y=\"5.4\" width=\"16.7\" height=\"13\" rx=\"2.8\"/><circle cx=\"9.1\" cy=\"10\" r=\"1.5\"/><path d=\"M20.3 15.6l-4.7-4.7l-7.4 7.4\"/>","inbox":"<path d=\"M3.5 12.7L5.9 4.8h12.1l2.4 7.9V17.8a0.9 0.9 0 0 1-0.9 0.9H4.4a0.9 0.9 0 0 1-0.9-0.9ZM3.5 12.7h4.6l1.4 2.4h4.6l1.4-2.4h4.6\"/>","infinity":"<path d=\"M12 12c-1.8-2.4-3.3-3.6-4.9-3.6a3.6 3.6 0 0 0 0 7.2c1.6 0 3.1-1.2 4.9-3.6Zm0 0c1.8 2.4 3.3 3.6 4.9 3.6a3.6 3.6 0 0 0 0-7.2c-1.6 0-3.1 1.2-4.9 3.6Z\"/>","info":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 11v5M12 8h.01\"/>","invite":"<circle cx=\"9.2\" cy=\"8.2\" r=\"2.9\"/><path d=\"M3.8 18.8c0-3 2.4-4.8 5.3-4.8s5.3 2 5.3 4.8M17.9 8.2v5.3M15.3 10.9h5.3\"/>","key":"<circle cx=\"8.1\" cy=\"15.9\" r=\"4.3\"/><path d=\"M11.1 12.9l9.4-9.4M16.8 7.3l3.2 3.2M14.6 9.4l2.2 2.2\"/>","keyboard":"<rect x=\"3.5\" y=\"6.6\" width=\"17\" height=\"10.7\" rx=\"1.8\"/><path d=\"M6.6 10.2h0M9.3 10.2h0M12 10.2h0M14.7 10.2h0M17.4 10.2h0M8.4 13.8h7.2\"/>","language":"<path d=\"M4.6 5.4h8.4M8.8 3.6v1.9M6.4 5.4c0.5 2.8 2.3 5.6 5.6 7M11.1 5.4c-0.6 3.2-2.6 6.3-6 7.9M12 20.3l3.7-8.4l3.7 8.4M13.3 17.5h4.8\"/>","laugh":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8 13.5h8a4 4 0 0 1-8 0ZM7.5 8.6l2.2 1.2-2.2 1.2M16.5 8.6l-2.2 1.2 2.2 1.2\"/>","layers":"<path d=\"M12 5.4l8.3 4.6l-8.3 4.6l-8.3-4.6Z\"/><path d=\"M3.8 14l8.3 4.6l8.3-4.6\"/>","leaf":"<path d=\"M3.8 20.1c0-9.5 5.6-15.2 16.7-16.7c-1 11.1-6.7 16.7-16.7 16.7ZM3.8 20.1l8.9-8.9\"/>","leave":"<path d=\"M12.7 3.5h4.2a2.1 2.1 0 0 1 2.1 2.1v12.7a2.1 2.1 0 0 1-2.1 2.1h-4.2M8.5 16.2l-4.2-4.2l4.2-4.2M4.2 12h10.6\"/>","lifebuoy":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"3.8\"/><path d=\"M6 6l3.4 3.4M14.6 14.7l3.4 3.4M18 6l-3.4 3.4M9.4 14.7l-3.4 3.4\"/>","link":"<path d=\"M9.9 14.1a4.3 4.3 0 0 0 6.1 0l3.2-3.2a4.3 4.3 0 0 0-6.1-6.1l-1.1 1.1M14.1 9.9a4.3 4.3 0 0 0-6.1 0l-3.2 3.2a4.3 4.3 0 0 0 6.1 6.1l1.1-1.1\"/>","link-off":"<path d=\"M10 13.8a4 4 0 0 0 5.6 0l2.9-2.9a4 4 0 0 0-5.6-5.6l-1 1M13.8 10a4 4 0 0 0-5.6 0l-2.9 2.9a4 4 0 0 0 5.6 5.6l1-1\"/><path d=\"M3.6 3.6l16.7 16.7\"/>","list":"<path d=\"M8.2 5.5h11.9M8.2 12h11.9M8.2 18.5h11.9M3.3 5.5h0M3.3 12h0M3.3 18.5h0\"/>","list-ordered":"<path d=\"M9.7 6.8h10.8M9.7 12.8h10.8M9.7 18.8h10.8M3.8 5.4L4.7 5v4.4M3.8 9.3h2M3.8 13.7a1 1 0 0 1 2 0.3c0 1.1-2 1.7-2 2.7h2.2\"/>","live":"<circle cx=\"12\" cy=\"11.8\" r=\"2\"/><path d=\"M8.6 8.4a4.9 4.9 0 0 0 0 7.1M15.4 8.4a4.9 4.9 0 0 1 0 7.1M6.3 6.1a8.1 8.1 0 0 0 0 11.9M17.7 6.1a8.1 8.1 0 0 1 0 11.9\"/>","lock":"<rect x=\"4.7\" y=\"10.7\" width=\"14.5\" height=\"9.4\" rx=\"2.1\"/><path d=\"M7.8 10.7V7.5a4 4 0 0 1 8.3 0v3.2\"/>","lungs":"<path d=\"M12 3.5v8M12 9.4l-3 2.1M12 9.4l3 2.1M8.4 6.5C5.6 6.5 3.5 10.4 3.5 15.2c0 2.8 1.1 4.8 3.2 4.8s2.7-2.1 2.7-4.8V8.3M15.6 6.5C18.4 6.5 20.5 10.4 20.5 15.2c0 2.8-1.1 4.8-3.2 4.8s-2.7-2.1-2.7-4.8V8.3\"/>","mail":"<rect x=\"3.5\" y=\"5.5\" width=\"17\" height=\"13.2\" rx=\"1.9\"/><path d=\"M4.4 7.4l7.6 5.7l7.6-5.7\"/>","map":"<path d=\"M9.1 4.6L3.5 6.5v13l5.6-1.9l5.6 1.9l5.6-1.9V4.6l-5.6 1.9Z\"/><path d=\"M9.1 4.6v13M14.7 6.5v13\"/>","mask":"<path d=\"M3.5 7c3 1.3 5.8 1.8 8.5 1.8S17.5 8.3 20.5 7c0 6-2.5 10-5 10-1.4 0-2-1.5-3.5-1.5S9.9 17 8.5 17c-2.5 0-5-4-5-10Z\"/><path d=\"M7.5 11.5h2M14.5 11.5h2\"/>","medical":"<rect x=\"3.5\" y=\"7.1\" width=\"17\" height=\"12.3\" rx=\"1.9\"/><path d=\"M9.2 7.1V5.2a0.9 0.9 0 0 1 0.9-0.9h3.8a0.9 0.9 0 0 1 0.9 0.9v1.9M12 10.4v5.7M9.2 13.3h5.7\"/>","megaphone":"<path d=\"M3.6 9.5v4a1 1 0 0 0 1 1H7.1l9 4.5v-15L7.1 8.5H4.5a1 1 0 0 0-1 1Z\"/><path d=\"M7.1 14.5l1.3 5h2.4L10.1 15.1M19.1 9a3.2 3.2 0 0 1 0 5\"/>","members":"<circle cx=\"9.2\" cy=\"7.9\" r=\"3\"/><path d=\"M3.6 19.1c0-3.1 2.6-5.1 5.6-5.1s5.6 2.1 5.6 5.1M15.7 5a3 3 0 0 1 0 5.9M17.6 14.3c1.8 0.6 2.8 2.3 2.8 4.8\"/>","menu":"<path d=\"M3.5 6.7h17M3.5 12h17M3.5 17.3h17\"/>","mic":"<rect x=\"9.2\" y=\"3.5\" width=\"5.7\" height=\"10.4\" rx=\"2.8\"/><path d=\"M5.4 11.1a6.6 6.6 0 0 0 13.2 0M12 17.7v2.8\"/>","mic-off":"<rect x=\"9.2\" y=\"3.6\" width=\"5.7\" height=\"10.4\" rx=\"2.8\"/><path d=\"M5.5 11.1a6.6 6.6 0 0 0 11 5M18.7 11.1c0 0.8-0.1 1.5-0.4 2.2M12.1 17.7v2.8M4.5 4.5l15.1 15.1\"/>","minus":"<path d=\"M3.5 12h17\"/>","minus-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8 12h8\"/>","money":"<rect x=\"3.5\" y=\"6.6\" width=\"17\" height=\"10.7\" rx=\"1.8\"/><circle cx=\"12\" cy=\"12\" r=\"2.2\"/><path d=\"M6.6 9.8v0M17.4 14.2v0\"/>","moon":"<path d=\"M20.5 14.6A8.6 8.6 0 1 1 9.4 3.5a6.9 6.9 0 0 0 11.2 11.2Z\"/>","more":"<circle cx=\"5.2\" cy=\"12\" r=\"1.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/><circle cx=\"18.8\" cy=\"12\" r=\"1.6\"/>","mountain":"<path d=\"M3.6 17.5L9.3 6l3.7 6.3L15.5 9.1L20.3 17.5Z\"/>","move":"<path d=\"M11.9 3.6v16.7M3.6 11.9h16.7M9 6.5l2.9-2.9l2.9 2.9M9 17.3l2.9 2.9l2.9-2.9M6.5 9l-2.9 2.9l2.9 2.9M17.3 9l2.9 2.9l-2.9 2.9\"/>","mug":"<path d=\"M4.8 9.1h11v7a4 4 0 0 1-4 4h-3a4 4 0 0 1-4-4Z\"/><path d=\"M15.8 10.5H17.3a2.5 2.5 0 0 1 0 5h-1.5M8.3 3.5c-0.8 1 0.8 2 0 3M11.8 3.5c-0.8 1 0.8 2 0 3\"/>","music":"<path d=\"M8.6 17.7V5.6l11.1-2.1v12.2\"/><circle cx=\"6\" cy=\"17.7\" r=\"2.6\"/><circle cx=\"17.2\" cy=\"15.7\" r=\"2.6\"/>","mute":"<path d=\"M3.9 9.4h3.6L12.2 5.8v12.4l-4.6-3.6H3.9Z\"/><path d=\"M16.3 9.9l4.1 4.1M20.5 9.9l-4.1 4.1\"/>","news":"<path d=\"M3.5 3.5h13.8v14.9a2.1 2.1 0 0 0 2.1 2.1H5.6a2.1 2.1 0 0 1-2.1-2.1Z\"/><path d=\"M17.3 7.7h3.2v10.6a2.1 2.1 0 0 1-2.1 2.1M7.2 7.7h6.4M7.2 11.5h6.4M7.2 15.2h3.7\"/>","nodes":"<circle cx=\"12\" cy=\"5.4\" r=\"2.1\"/><circle cx=\"5.8\" cy=\"17.9\" r=\"2.1\"/><circle cx=\"18.2\" cy=\"17.9\" r=\"2.1\"/><path d=\"M11 7.4L6.8 15.9M13 7.4l4.3 8.6M7.9 17.9h8.3\"/>","open":"<path d=\"M14 3.8h6.3v6.3M20.2 3.8l-9.4 9.4M18.2 14.2v4.2a2.1 2.1 0 0 1-2.1 2.1H5.6a2.1 2.1 0 0 1-2.1-2.1V7.9a2.1 2.1 0 0 1 2.1-2.1h4.2\"/>","palette":"<path d=\"M12.2 3.8a8.4 8.4 0 1 0 0 16.8c1.1 0 1.7-0.9 1.7-1.6c0-1.1-0.9-1.5-0.9-2.6s0.9-1.6 1.9-1.6H16.9a3.7 3.7 0 0 0 3.7-3.7C20.6 7.2 16.9 3.8 12.2 3.8Z\"/><circle cx=\"8\" cy=\"11.4\" r=\"1.1\"/><circle cx=\"10.3\" cy=\"7.9\" r=\"1.1\"/><circle cx=\"14.5\" cy=\"7.9\" r=\"1.1\"/>","parking":"<rect x=\"3.7\" y=\"3.7\" width=\"16.7\" height=\"16.7\" rx=\"3\"/><path d=\"M9.6 17V7.2h3.4a3 3 0 0 1 0 5.9H9.6\"/>","pause":"<path d=\"M7.1 3.5v17M16.9 3.5v17\"/>","paw":"<path d=\"M7.8 18.5c0-3 1.9-5.3 4.2-5.3s4.2 2.3 4.2 5.3c0 1.4-1.1 2.1-2.1 2.1c-0.8 0-1.4-0.5-2.1-0.5s-1.3 0.5-2.1 0.5c-1.1 0-2.1-0.7-2.1-2.1Z\"/><circle cx=\"5.4\" cy=\"10.6\" r=\"1.9\"/><circle cx=\"9.2\" cy=\"5.7\" r=\"1.9\"/><circle cx=\"14.8\" cy=\"5.7\" r=\"1.9\"/><circle cx=\"18.6\" cy=\"10.6\" r=\"1.9\"/>","pencil":"<path d=\"M11.9 19.6h8.4M16.1 4.2a2 2 0 0 1 2.8 2.8L7.2 18.7l-3.7 0.9l0.9-3.7Z\"/>","phone":"<path d=\"M5.7 3.4h3.9l2 4.9l-2.5 1.5a10.8 10.8 0 0 0 4.9 4.9L15.6 12.3l4.9 2v3.9a2 2 0 0 1-2 2A15.8 15.8 0 0 1 3.8 5.4a2 2 0 0 1 2-2\"/>","phone-off":"<path d=\"M5.6 4.1h3.7l1.9 4.7l-2.4 1.4a10.3 10.3 0 0 0 4.7 4.7L15.1 12.5l4.7 1.9v3.7a1.9 1.9 0 0 1-1.9 1.9A15.1 15.1 0 0 1 3.8 5.9a1.9 1.9 0 0 1 1.9-1.9\"/><path d=\"M4.3 3.6l16.1 16.1\"/>","piggy":"<path d=\"M3.5 11.4c0-3.2 3.3-5.7 7.7-5.7c1.3 0 2.4 0.2 3.4 0.6L17.4 4.7v3.3c0.8 0.7 1.4 1.6 1.6 2.5h1.4v3.4H18.8c-0.5 1-1.3 1.7-2.3 2.3V19.1h-2.9v-1.9c-0.8 0.2-1.7 0.3-2.5 0.3c-0.8 0-1.6-0.1-2.3-0.3v1.9h-2.9v-2.9C4.4 15.1 3.5 13.4 3.5 11.4Z\"/><path d=\"M14.9 10.1h0M8.7 8.1h2.9\"/>","pin":"<path d=\"M12 20.7s-6.2-5.4-6.2-10.5a6.2 6.2 0 0 1 12.4 0C18.2 15.2 12 20.7 12 20.7Z\"/><circle cx=\"12\" cy=\"10\" r=\"2.3\"/>","plane":"<path d=\"M20 4L3.5 10.8l6.6 3.1l3.1 6.6Z\"/><path d=\"M10.1 13.9l9.9-9.9\"/>","play":"<path d=\"M5.6 3.6v16.9l13.4-8.5Z\"/>","plug":"<path d=\"M9.2 4v4.6M14.8 4v4.6M6.9 8.7h10.2v2.8a5.1 5.1 0 0 1-10.2 0ZM12 16.6V20.7\"/>","plus":"<path d=\"M12 3.5v17M3.5 12h17\"/>","plus-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 8v8M8 12h8\"/>","print":"<path d=\"M7.1 8.9V3.5h9.9V8.9\"/><rect x=\"3.6\" y=\"8.9\" width=\"16.7\" height=\"7.9\" rx=\"2\"/><path d=\"M7.1 13.8h9.9v6.4H7.1ZM16.4 11.9h0\"/>","puzzle":"<path d=\"M3.8 7h4.2a2.6 2.6 0 1 1 4.8 0h4.2v4.2a2.6 2.6 0 1 1 0 4.8v4.2h-4.2a2.6 2.6 0 1 0-4.8 0H3.8v-4.2a2.6 2.6 0 1 0 0-4.8Z\"/>","quote":"<path d=\"M9.8 6.2C5.8 7.2 3.6 10 3.6 13.9v3.3h5.6v-5.6h-5.6M20.3 6.2c-3.9 1.1-6.1 3.9-6.1 7.8v3.3h5.6v-5.6h-5.6\"/>","radio":"<rect x=\"3.5\" y=\"8.3\" width=\"16.8\" height=\"11.3\" rx=\"1.9\"/><path d=\"M7.3 8.3L16.7 3.9\"/><circle cx=\"15.3\" cy=\"14\" r=\"2.4\"/><path d=\"M6.8 12.4h3.8M6.8 15.4h3.8\"/>","ranking":"<path d=\"M3.5 19.9h17M4.5 19.9V13.8h4.7M9.2 19.9V10h5.7v9.9M14.9 19.9V15.7h4.7v4.3\"/><path d=\"M12 3.4l0.9 1.7l1.9 0.3l-1.4 1.3l0.3 1.9L12 7.7l-1.7 0.9l0.3-1.9L9.3 5.4l1.9-0.3Z\"/>","record":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>","recycle":"<path d=\"M7.8 18.2H5.7a1.2 1.2 0 0 1-1-1.8l2.3-3.9M10 5.5l0.9-1.5a1.2 1.2 0 0 1 2.1 0l2.3 3.9M17.7 12.4l1.6 2.7a1.2 1.2 0 0 1-1 1.8h-4.5\"/><path d=\"M11.1 16.5l1.8 1.8l-1.8 1.8M4.5 10.4l2.4-0.7l0.7 2.5M17.2 6.1l-0.7 2.5l-2.5-0.7\"/>","reply":"<path d=\"M9.2 5.8L4 11l5.1 5.1M4 11h10.3a6.2 6.2 0 0 1 6.2 6.2v1\"/>","restroom":"<circle cx=\"7.1\" cy=\"5.3\" r=\"1.7\"/><circle cx=\"16.4\" cy=\"5.3\" r=\"1.7\"/><path d=\"M11.7 3.6v16.7M5.6 20.3v-5.1H4.7V10.6A1.4 1.4 0 0 1 6.1 9.2h1.9a1.4 1.4 0 0 1 1.4 1.4v4.6H8.5V20.3M15.5 20.3v-3.7h-2.4l1.9-6.1a0.9 0.9 0 0 1 0.9-0.9h0.9a0.9 0.9 0 0 1 0.9 0.9l1.9 6.1H17.3v3.7\"/>","retry":"<path d=\"M3.7 12a8.2 8.2 0 0 1 14.1-5.9L20.2 8.4M20.2 3.8v4.6h-4.6M20.2 12a8.2 8.2 0 0 1-14.1 5.9L3.7 15.6M3.7 20.3v-4.6h4.6\"/>","rewind":"<path d=\"M20 6.4v11.2L12.4 12ZM10.9 6.4v11.2L3.3 12Z\"/>","road":"<path d=\"M8.2 3.5L4.4 20.5M15.8 3.5l3.8 17M12 4.4v2.4M12 10.6v2.8M12 17.2V19.6\"/>","robot":"<rect x=\"5.4\" y=\"8.3\" width=\"13.2\" height=\"10.4\" rx=\"2.8\"/><path d=\"M12 4.6v3.8M9.2 13.1h0M14.8 13.1h0M9.6 16.4h4.7M3.5 13.1v1.9M20.5 13.1v1.9\"/>","rocket":"<path d=\"M11.8 15.7L8.4 12.3C9.9 6.9 13.8 3.9 20.2 3.9C20.2 10.3 17.3 14.2 11.8 15.7ZM8.4 12.3L4.9 11.8L7.4 8.3h3.9M11.8 15.7l0.5 3.4l3.4-2.5v-3.9M6.4 17.7c-1.5 0-3 1.5-3 3c1.5 0 3-1.5 3-3Z\"/><circle cx=\"15.3\" cy=\"8.8\" r=\"1.5\"/>","route":"<circle cx=\"6\" cy=\"18\" r=\"2.4\"/><circle cx=\"18\" cy=\"6\" r=\"2.4\"/><path d=\"M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6\"/>","rss":"<path d=\"M4 3.5a16.5 16.5 0 0 1 16.5 16.5M4 10.4A9.6 9.6 0 0 1 13.6 20.1\"/><path d=\"M4.6 19.4h0\" stroke-width=\"3.9\"/>","save":"<path d=\"M4.7 3.5h11.5l3.2 3.2v13.6H4.7Z\"/><path d=\"M7.8 3.5v5.2h7.3V3.5M7.8 20.2v-6.3h8.4v6.3\"/>","scale":"<path d=\"M12 4.3V19.5M8.5 19.5h6.9M5.5 7.4h13M12 4.3h0M3.7 13.4L5.9 7.8L8.1 13.4M15.9 13.4L18.1 7.8l2.2 5.6M3.7 13.4a2.2 2.2 0 0 0 4.3 0M15.9 13.4a2.2 2.2 0 0 0 4.3 0\"/>","scissors":"<circle cx=\"6\" cy=\"7\" r=\"2.6\"/><circle cx=\"6\" cy=\"17\" r=\"2.6\"/><path d=\"M8.2 8.6 20 18M8.2 15.4 20 6\"/>","scroll":"<path d=\"M8.3 3.8h10.1a1.9 1.9 0 0 1 1.9 1.9v1.5H17M8.3 3.8a1.9 1.9 0 0 0-1.9 1.9v11.7a2.9 2.9 0 0 1-2.9 2.9h10.7a2.9 2.9 0 0 0 2.9-2.9v-11.7a1.9 1.9 0 0 0-1.9-1.9M10.2 9.2h3.9M10.2 13h3.9\"/>","search":"<circle cx=\"10.6\" cy=\"10.6\" r=\"7\"/><path d=\"M20.4 20.4l-4.5-4.5\"/>","seedling":"<path d=\"M12 20.8v-9.8M12 11C12 6.7 9.3 4 4.4 4c0 4.3 2.7 7 7.6 7ZM12 13.7c0-4.3 2.7-7 7.6-7c0 4.3-2.7 7-7.6 7Z\"/>","server":"<rect x=\"3.5\" y=\"3.5\" width=\"17\" height=\"7.4\" rx=\"2.1\"/><rect x=\"3.5\" y=\"13.1\" width=\"17\" height=\"7.4\" rx=\"2.1\"/><path d=\"M7.8 7.2h0M7.8 16.8h0\"/>","shapes":"<circle cx=\"7.5\" cy=\"7.7\" r=\"3.4\"/><path d=\"M16.4 3.7l3.9 6.9h-7.9ZM13.9 14.1h5.9v5.9h-5.9ZM3.6 20l3.4-5.9l3.4 5.9Z\"/>","share":"<path d=\"M12 15.2V3.3M7.4 8.1L12 3.3L16.6 8.1M4.9 12.9v6.1a1 1 0 0 0 1 1h12.2a1 1 0 0 0 1-1v-6.1\"/>","shelter":"<path d=\"M3.7 10.5L12 4l8.3 6.5M5.9 9.2v10.4M18.1 9.2v10.4\"/><circle cx=\"12\" cy=\"11\" r=\"1.6\"/><path d=\"M9.8 19.6v-3.9a2.2 2.2 0 0 1 4.3 0V19.6\"/>","shield":"<path d=\"M12 3.7L5.5 6.6v5.6c0 4.1 2.8 7.1 6.5 8.4c3.7-1.3 6.5-4.2 6.5-8.4V6.6Z\"/>","shirt":"<path d=\"M8.2 3.8L3.7 6.6l1.8 3.8l1.8-0.9v11.2h9.3V9.4l1.8 0.9l1.8-3.8l-4.7-2.7c-0.5 1.4-1.8 2.3-3.8 2.3S8.8 5.1 8.2 3.8Z\"/>","signature":"<path d=\"M3.9 14c2.4 0 3.3-8.4 5.6-8.4c1.4 0 0.9 5.6 2.4 5.6c0.9 0 1.4-2.4 2.4-2.4s0.8 1.9 1.9 1.9c0.8 0 1.4-0.6 1.9-1.1M3.9 17.8h16.7\"/>","skip":"<path d=\"M4.8 3.5l10.8 8.4l-10.8 8.4Z\"/><path d=\"M19.7 3.5v16.7\"/>","skull":"<path d=\"M12 3.7a6.5 6.5 0 0 0-6.5 6.5c0 2.2 1 3.6 2.4 4.6v2.8h8.4v-2.8c1.3-1 2.4-2.6 2.4-4.6a6.5 6.5 0 0 0-6.5-6.5Z\"/><circle cx=\"9.6\" cy=\"10.7\" r=\"1.3\"/><circle cx=\"14.4\" cy=\"10.7\" r=\"1.3\"/><path d=\"M10.6 17.7v2.8M13.4 17.7v2.8\"/>","slash":"<path d=\"M3.6 3.6l16.7 16.7\"/>","sliders":"<path d=\"M3.5 6.8h10M18.7 6.8H20.3M3.5 17.2h3.6M12.4 17.2H20.3\"/><circle cx=\"16.2\" cy=\"6.8\" r=\"2.4\"/><circle cx=\"9.8\" cy=\"17.2\" r=\"2.4\"/>","smile":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01\"/>","snow":"<path d=\"M12 3.5v17M4.6 7.8l14.7 8.5M4.6 16.3l14.7-8.5M9.6 4.9L12 7.3l2.4-2.4M9.6 19.1L12 16.7l2.4 2.4\"/>","sort":"<path d=\"M3.5 5.8h9.2M3.5 12h6.2M3.5 18.2h3M17.5 4.8v14.4M14.3 16.2l3 3l3-3\"/>","sort-asc":"<path d=\"M3.4 5.4h3M3.4 11.6h6.3M3.4 17.9h9.3M17.5 4.3v14.6M14.3 16l3 3l3-3\"/>","spark":"<path d=\"M10.4 3.6l1.7 4.8L17 10.1l-4.8 1.7L10.4 16.7l-1.7-4.8L3.9 10.1l4.8-1.7ZM17 14.8l0.8 2.1L19.8 17.6l-2.1 0.8L17 20.3l-0.8-2.1L14.3 17.6l2.1-0.8Z\"/>","square":"<rect x=\"3.5\" y=\"3.5\" width=\"17\" height=\"17\" rx=\"2.7\"/>","star":"<path d=\"M12 4l2.6 5.3l5.7 0.6l-4.3 3.9l1.2 5.7L12 16.8l-5.2 2.9l1.2-5.7l-4.3-3.9l5.7-0.6Z\"/>","steps":"<path d=\"M9.7 5.7h11M9.7 11.8h11M9.7 17.8h11\"/><path d=\"M3.7 5.7l1.5 1.5L7.7 4.8M3.7 11.8l1.5 1.5L7.7 10.8M3.7 17.8l1.5 1.5L7.7 16.8\"/>","stop":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8.5 8.5 7 7\"/>","store":"<path d=\"M3.7 8.8L5.3 3.7h13.4L20.3 8.8M3.7 8.8h16.5v1.6a2.8 2.8 0 0 1-5.4 0a2.8 2.8 0 0 1-5.6 0A2.8 2.8 0 0 1 3.7 10.4ZM4.8 12.9v7.2h14.5v-7.2M9.9 20.1v-4.1h4.1v4.1\"/>","storm":"<path d=\"M7.8 16a3.8 3.8 0 1 1 0.8-7.4A4.5 4.5 0 0 1 17.4 10.5a3.1 3.1 0 0 1-0.4 6.2\"/><path d=\"M12.9 12.2l-2.1 3.3h2.5L11.3 18.9\"/>","sun":"<circle cx=\"11.9\" cy=\"11.9\" r=\"3.4\"/><path d=\"M11.9 3.5v1.8M11.9 18.6v1.8M6 6l1.2 1.2M16.7 16.7l1.2 1.2M3.5 11.9h1.8M18.6 11.9h1.8M6 17.9l1.2-1.2M16.7 7.2l1.2-1.2\"/>","swap":"<path d=\"M3.5 7.8h14.9l-3.7-3.7M20.5 16.3H5.6l3.7 3.7\"/>","sword":"<path d=\"M20.3 3.7v3.7L10.4 17.3L6.7 13.6L16.7 3.7Z\"/><path d=\"M5.2 12L12 18.8M8.3 15.7L3.6 20.4\"/>","table":"<rect x=\"3.5\" y=\"4.3\" width=\"17\" height=\"15.1\" rx=\"1.9\"/><path d=\"M3.5 9.5h17M3.5 14.7h17M9.2 9.5V19.4M14.8 9.5V19.4\"/>","tack":"<path d=\"M9.3 4h5.6l-0.9 4.6l2.8 2.8H7.4l2.8-2.8ZM12 11.4v9.3\"/>","tag":"<path d=\"M3.8 11.2V3.8h7.4l9.3 9.3l-7.4 7.4Z\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\"/>","target":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/>","tent":"<path d=\"M3.5 18.4h17M12 4.9L4.8 18.4M12 4.9l7.2 13.4M12 12.5L9.3 18.4M12 12.5l2.7 5.8\"/>","terminal":"<rect x=\"3.7\" y=\"4.6\" width=\"16.7\" height=\"14.9\" rx=\"1.9\"/><path d=\"M7.4 9.2l2.8 2.8l-2.8 2.8M12.9 14.8h3.7\"/>","text":"<path d=\"M5.2 20.3L10.9 3.6h2.1l5.7 16.7M7.4 14h9.2\"/>","thumbs-up":"<path d=\"M7.3 10.4v9.6H4.8a1 1 0 0 1-1-1v-7.6a1 1 0 0 1 1-1Z\"/><path d=\"M7.3 10.4L10.9 3.3a2.2 2.2 0 0 1 2.5 2.6L12.7 9.4H18.5a2 2 0 0 1 2 2.4l-1.3 6.6a2 2 0 0 1-2 1.6H7.3\"/>","ticket":"<path d=\"M3.6 7.4h16.7v2.8a1.9 1.9 0 0 0 0 3.7v2.8H3.6v-2.8a1.9 1.9 0 0 0 0-3.7Z\"/><path d=\"M13.8 7.4v9.3\"/>","timeline":"<path d=\"M3.5 12h0.9M8.2 12h1.9M13.9 12h1.9M19.6 12h0.9\"/><circle cx=\"6.3\" cy=\"12\" r=\"1.9\"/><circle cx=\"12\" cy=\"12\" r=\"1.9\"/><circle cx=\"17.7\" cy=\"12\" r=\"1.9\"/><path d=\"M6.3 10.1V5.9M12 13.9v4.3M17.7 10.1V5.9\"/>","toolbox":"<rect x=\"3.5\" y=\"8.1\" width=\"17\" height=\"11.3\" rx=\"1.9\"/><path d=\"M9.2 8.1V5.8A1.4 1.4 0 0 1 10.6 4.3h2.8A1.4 1.4 0 0 1 14.8 5.8V8.1M3.5 12.8h17M8.2 11.4v2.8M15.8 11.4v2.8\"/>","train":"<rect x=\"6.4\" y=\"3.8\" width=\"11.2\" height=\"12.1\" rx=\"2.8\"/><path d=\"M6.4 10.3h11.2M9.2 13.1h0M14.8 13.1h0M8.7 20.5l1.9-3.3M15.3 20.5l-1.9-3.3\"/>","trash":"<path d=\"M4.1 6.7h15.8M10 10.6v5.9M14 10.6v5.9M6.1 6.7l1 11.8a2 2 0 0 0 2 2h5.9a2 2 0 0 0 2-2l1-11.8M9 6.7V3.7h5.9v3\"/>","tree":"<path d=\"M12.1 3.6L7 10.6h2.8L5.6 16.6h13l-4.2-6h2.8ZM12.1 16.6v3.7\"/>","trend-down":"<path d=\"M3.2 7.2l5.6 5.6l3.8-3.8l7.4 7.4\"/><path d=\"M14.4 16.6h5.6v-5.6\"/>","trend-up":"<path d=\"M3.2 16.8L8.8 11.3l3.8 3.8l7.4-7.4\"/><path d=\"M14.4 7.4h5.6v5.6\"/>","trophy":"<path d=\"M7.9 4h8.4v5.2a4.1 4.1 0 0 1-8.4 0ZM7.9 6.1H4.7a3.2 3.2 0 0 0 3.2 4.1M16.2 6.1h3.2a3.2 3.2 0 0 1-3.2 4.1M12 13.4v4.1M7.9 20.8h8.4\"/>","tv":"<rect x=\"3.5\" y=\"7.8\" width=\"17\" height=\"11.6\" rx=\"1.8\"/><path d=\"M8.4 4.3l3.6 3.6l3.6-3.6\"/>","umbrella":"<path d=\"M3.8 12.5a8.4 8.4 0 0 1 16.6 0ZM12.1 12.5v6a1.9 1.9 0 0 1-3.7 0M12.1 4.1v0\"/>","undo":"<path d=\"M9.2 14.2L4 8.9l5.2-5.2\"/><path d=\"M4 8.9h11a5.8 5.8 0 0 1 0 11.5H11.3\"/>","up":"<path d=\"M3.7 16.1l8.3-8.3l8.3 8.3\"/>","upload":"<path d=\"M12 20.8V9.2M6.8 14.5l5.2-5.2l5.2 5.2M4.7 4h14.7\"/>","user":"<circle cx=\"12\" cy=\"7.4\" r=\"3.8\"/><path d=\"M4.3 20.5c0-3.9 3.4-6.6 7.7-6.6s7.7 2.6 7.7 6.6\"/>","user-check":"<circle cx=\"9.2\" cy=\"8.1\" r=\"2.8\"/><path d=\"M4 19c0-2.9 2.4-4.9 5.4-4.9s5.4 1.9 5.4 4.9M15.4 10.8l1.8 1.8l3.6-3.6\"/>","user-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"10\" r=\"3\"/><path d=\"M6.6 18.3a6.5 6.5 0 0 1 10.8 0\"/>","user-lock":"<circle cx=\"9.8\" cy=\"7.3\" r=\"3.3\"/><path d=\"M3.5 18.8c0-3.4 2.8-5.7 6.2-5.7c1.2 0 2.2 0.3 3.1 0.7\"/><rect x=\"14.6\" y=\"14.5\" width=\"5.7\" height=\"4.8\" rx=\"1\"/><path d=\"M16 14.5v-1.2a1.5 1.5 0 0 1 2.9 0v1.2\"/>","user-minus":"<circle cx=\"10.1\" cy=\"8\" r=\"3.3\"/><path d=\"M4 19.3c0-3.3 2.7-5.7 6.1-5.7c1.1 0 2.2 0.3 3 0.7\"/><path d=\"M15.1 17h5.7\"/>","user-off":"<circle cx=\"12\" cy=\"8.2\" r=\"3.5\"/><path d=\"M5.1 20c0-3.6 3-5.9 6.9-5.9s6.9 2.3 6.9 5.9\"/><path d=\"M3.7 3.7l16.7 16.7\"/>","user-pen":"<circle cx=\"9.8\" cy=\"8\" r=\"3.3\"/><path d=\"M3.8 19.1c0-3.3 2.7-5.6 6-5.6c1.1 0 2.1 0.3 3 0.7\"/><path d=\"M19.1 12.1l1.4 1.4l-4.7 4.7l-1.9 0.5l0.5-1.9Z\"/>","user-tag":"<circle cx=\"10\" cy=\"7.6\" r=\"3.3\"/><path d=\"M3.8 18.9c0-3.3 2.8-5.6 6.1-5.6c1.1 0 2.2 0.3 3.1 0.7\"/><path d=\"M14.2 14.2h3.7l2.4 2.4l-2.4 2.4h-3.7Z\"/><path d=\"M16.1 16.5h0\"/>","video":"<rect x=\"3.4\" y=\"6.4\" width=\"12.3\" height=\"11.3\" rx=\"1.9\"/><path d=\"M15.6 10.2l4.7-2.8v9.4l-4.7-2.8\"/>","volume":"<path d=\"M3.5 9.5h3.7L11.8 5.8v12.6l-4.7-3.7H3.5Z\"/><path d=\"M15.5 9.5a3.7 3.7 0 0 1 0 5.2M18.1 6.9a7.3 7.3 0 0 1 0 10.5\"/>","walk":"<circle cx=\"12.3\" cy=\"5.1\" r=\"1.7\"/><path d=\"M8.8 20.3l2.4-5.5l1.9 2.2V20.3M11 14.8l0.9-5.2l-2.7 1.4l-1.4 2.7M11.9 9.6l1.8 2.7l2.7 0.9\"/>","wallet":"<path d=\"M3.5 6.8A2.7 2.7 0 0 1 6.2 4.2H18.4v4.2M3.5 6.8V18a2.1 2.1 0 0 0 2.1 2.1h14.9V8.4H6.2A2.7 2.7 0 0 1 3.5 6.8Z\"/><path d=\"M16.2 14.2h0\"/>","water":"<path d=\"M3.5 10.2c2.1 0 2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8s2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8M3.5 15.6c2.1 0 2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8s2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8\"/>","wave":"<path d=\"M3.7 16.2c2.1 0 2.1-1.7 4.2-1.7s2.1 1.7 4.2 1.7s2.1-1.7 4.2-1.7s2.1 1.7 4.2 1.7\"/><path d=\"M6.5 12.7A5.6 5.6 0 0 1 16.4 9.4c-1.9-0.4-3.5 0.3-4 2\"/>","weight":"<circle cx=\"12\" cy=\"5.2\" r=\"2\"/><path d=\"M7.1 8.7h9.9l3 11.3H4.1Z\"/>","wifi":"<path d=\"M3.7 9.4a12.4 12.4 0 0 1 16.7 0M6.4 12.5a8.4 8.4 0 0 1 11.5 0M9.1 15.6a4.4 4.4 0 0 1 6.2 0M12.1 18.6h0\"/>","wind":"<path d=\"M3.5 9.9h10.4a2.8 2.8 0 1 0-2.8-2.8M3.5 13.7h14.2a2.8 2.8 0 1 1-2.8 2.8M3.5 17.5h6.6\"/>","window":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"15\" rx=\"2\"/><path d=\"M3.5 9h17\"/>","window-restore":"<rect x=\"3.5\" y=\"8.5\" width=\"12\" height=\"11\" rx=\"2\"/><path d=\"M8.5 8.5v-2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3\"/>","wizard":"<path d=\"M3.6 20h16.7M6.3 20L9.9 9.1L15.7 3.3L14.1 10.1L17.8 20\"/><path d=\"M8.4 15.4h7.3\"/>","wrench":"<path d=\"M14.6 3.6a5.2 5.2 0 0 0-4.6 7.3L3.7 17.3V20.3h3.1l6.4-6.4a5.2 5.2 0 0 0 7.3-4.6l-3.2 1.2l-2.5-2.5l1.2-3.2Z\"/>","x-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m9 9 6 6M15 9l-6 6\"/>","zap":"<path d=\"M13.4 3.5L5.9 12.9h5.7l-0.9 7.6l7.6-9.4h-5.7Z\"/>"};++ const S = {"airplane":"<path d=\"M11.8 2.6c1.3 0 2 1.2 2 3V9.5l7.2 4.2v2.5L13.8 13.7v3.5l2 1.5v2.5L11.8 19.7l-3.7 1.2v-2.5l2-1.5v-3.5l-7.2 2.5v-2.5L9.8 9.5V5.3c0-1.8 0.7-3 2-3Z\"/>","alert":"<path fill-rule=\"evenodd\" d=\"M10.5 4.1a1.7 1.7 0 0 1 3 0l7.7 13.3a1.7 1.7 0 0 1-1.5 2.6H4.3a1.7 1.7 0 0 1-1.5-2.6ZM11 8.8h2v5.2h-2ZM12 15.2a1.2 1.2 0 1 1 0 2.4a1.2 1.2 0 0 1 0-2.4Z\"/>","alert-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.7 6.9h2.5v7.1h-2.5ZM12.9 15.3a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","archive":"<rect x=\"2.6\" y=\"3.4\" width=\"18.8\" height=\"5.4\" rx=\"1.2\"/><path fill-rule=\"evenodd\" d=\"M4.4 10h15.2v9.2a1.6 1.6 0 0 1-1.6 1.6H6a1.6 1.6 0 0 1-1.6-1.6ZM9.6 12.4v1.8h4.8v-1.8Z\"/>","bell":"<path d=\"M5.2 15.4V9.6a6.8 6.8 0 0 1 13.7 0v5.7l1.9 2.6H3.3ZM9.7 19.3h4.6a2.3 2.3 0 0 1-4.6 0Z\"/>","book":"<path d=\"M4.3 5.2A2.4 2.4 0 0 1 6.8 2.7h12.2v14.6H6.8a2.4 2.4 0 0 0-2.4 2.4Z\"/><path class=\"ln\" d=\"M7.2 20.2h11.2\"/>","book-open":"<path d=\"M10.9 5.8C8.8 4.4 6 3.8 2.5 4.5v14c3.5-0.6 6.3 0 8.4 1.5ZM13.1 5.8c2.1-1.5 5-2 8.4-1.4v14c-3.5-0.6-6.3 0-8.4 1.5Z\"/>","box":"<path d=\"M12 2.7l9.4 4.5l-9.4 4.5l-9.4-4.5ZM2.6 8.9l8.6 4.1v8.2l-8.6-4.3ZM21.4 8.9l-8.6 4.1v8.2l8.6-4.3Z\"/>","brain":"<path d=\"M8.9 3.8a3.2 3.2 0 0 0-3.2 3.1a3.1 3.1 0 0 0-2.2 5.3a3.1 3.1 0 0 0 2.2 5.2a3.2 3.2 0 0 0 5.7 1.3V5.6A2.3 2.3 0 0 0 8.9 3.8Z\"/><path d=\"M15.1 3.8a3.2 3.2 0 0 1 3.2 3.1a3.1 3.1 0 0 1 2.2 5.3a3.1 3.1 0 0 1-2.2 5.2a3.2 3.2 0 0 1-5.7 1.3V5.6A2.3 2.3 0 0 1 15.1 3.8Z\"/>","bridge":"<path d=\"M5.7 4.6h2.6v15.4H5.7ZM15.7 4.6h2.6v15.4h-2.6ZM3.4 14.6h17.2v2.8H3.4Z\"/><path class=\"ln\" d=\"M4 10c1.6 0 2.5-2.3 3-5c2.4 5.2 7.6 5.2 10 0c.5 2.7 1.4 5 3 5M12 10.9V15\"/>","briefcase":"<path d=\"M2.7 9.1a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.8H2.7ZM2.7 13.1h7.2v1.5h4.3v-1.5h7.2V17.9a2.5 2.5 0 0 1-2.5 2.5H5.2a2.5 2.5 0 0 1-2.5-2.5Z\"/><path class=\"ln\" d=\"M9.1 6.6V5.7A1.5 1.5 0 0 1 10.5 4.2h2.9A1.5 1.5 0 0 1 14.9 5.7v0.9\"/>","bug":"<path fill-rule=\"evenodd\" d=\"M12 7a4.8 4.8 0 0 1 4.8 4.8v3.4a4.8 4.8 0 0 1-9.6 0v-3.4A4.8 4.8 0 0 1 12 8Zm-0.7 4.4v7.4h1.4v-7.4Z\"/><path d=\"M9.4 5.8a2.6 2.6 0 0 1 5.2 0Z\"/><path class=\"ln\" d=\"M7.2 12.5H3.8M20.2 12.5h-3.4M7.6 9.4L5 7M16.4 9.4L19 7M7.6 16.6L5 19M16.4 16.6L19 19\"/>","burst":"<path d=\"M12.1 2.6l2 4.6l4.7-1.9l-1.9 4.7l4.6 2l-4.6 2l1.9 4.7l-4.7-1.9l-2 4.6l-2-4.6l-4.7 1.9l1.9-4.7L2.7 12l4.6-2l-1.9-4.7l4.7 1.9Z\"/>","calendar":"<path fill-rule=\"evenodd\" d=\"M4.1 8.1a2 2 0 0 1 2-2h11.8a2 2 0 0 1 2 2V19.5a2 2 0 0 1-2 2H6.1a2 2 0 0 1-2-2Zm2.2 3.9V19.3h11.4v-7.3Z\"/><path class=\"ln\" d=\"M9 3.9v3.5M15 3.9v3.5\"/>","camera":"<path fill-rule=\"evenodd\" d=\"M2.5 7.8a1.9 1.9 0 0 1 1.9-1.9h2.6L8.8 3.3h6.3l1.8 2.6h2.6A1.9 1.9 0 0 1 21.5 7.8v10.7a1.9 1.9 0 0 1-1.9 1.9H4.4A1.9 1.9 0 0 1 2.5 18.5Zm9.5 2a3.6 3.6 0 1 0 0 7.2a3.6 3.6 0 0 0 0-7.2Z\"/>","camera-flip":"<path fill-rule=\"evenodd\" d=\"M2.5 7.8a1.9 1.9 0 0 1 1.9-1.9h2.6L8.8 3.3h6.3l1.8 2.6h2.6A1.9 1.9 0 0 1 21.5 7.8v10.7a1.9 1.9 0 0 1-1.9 1.9H4.4A1.9 1.9 0 0 1 2.5 18.5ZM7.7 11.7A4.5 4.5 0 0 1 16.3 11.7L13.9 12.6A2 2 0 0 0 10.1 12.6ZM16.3 14.8A4.5 4.5 0 0 1 7.7 14.8L10.1 14A2 2 0 0 0 13.9 14Z\"/>","camera-off":"<path d=\"M8.5 4.6L9 3.8h6.1l1.7 2.5h2.5A1.8 1.8 0 0 1 21.2 8.1v9.1ZM3.8 7.3L16.7 20.2H4.8A1.8 1.8 0 0 1 3 18.4V8.1Z\"/><path class=\"ln\" d=\"M4 3.8l16.2 16.2\"/>","car":"<path fill-rule=\"evenodd\" d=\"M5.6 5.2h12.7l2.7 5.5H3ZM2.6 12.2h18.7v4.5H2.6ZM5.9 14.4a1.1 1.1 0 1 0 2.3 0a1.1 1.1 0 1 0-2.3 0ZM15.8 14.4a1.1 1.1 0 1 0 2.3 0a1.1 1.1 0 1 0-2.3 0ZM5 17.5h2.7v1.8H5ZM16.3 17.5h2.7v1.8h-2.7Z\"/>","card":"<path fill-rule=\"evenodd\" d=\"M2.6 7.4a2.4 2.4 0 0 1 2.4-2.4h13.9a2.4 2.4 0 0 1 2.4 2.4v9.3a2.4 2.4 0 0 1-2.4 2.4h-13.9a2.4 2.4 0 0 1-2.4-2.4ZM2.6 9.5v1.7h18.7V9.5ZM5.9 14.2v1.5h4.8v-1.5Z\"/>","care":"<path d=\"M17 11s-4.1-2.4-4.1-5.4a2.3 2.3 0 0 1 4.1-1.6a2.3 2.3 0 0 1 4.1 1.6c0 2.9-4.1 5.4-4.1 5.4Z\"/><path class=\"ln\" d=\"M3.5 16.2h3.2l4.3 3.2h5.9a2.1 2.1 0 0 0 0-4.3H13.3M7.9 16.2l2.3-2.1h3.6\"/>","cart":"<path d=\"M6.3 7h15l-2.5 8.3H8.1Z\"/><circle cx=\"9.5\" cy=\"19.4\" r=\"1.6\"/><circle cx=\"17\" cy=\"19.4\" r=\"1.6\"/><path class=\"ln\" d=\"M3.8 4.8H5.8l2.3 10.5\"/>","cc":"<path fill-rule=\"evenodd\" d=\"M5.6 4.6h12.7a3 3 0 0 1 3 3v8.8a3 3 0 0 1-3 3h-12.7a3 3 0 0 1-3-3v-8.8a3 3 0 0 1 3-3ZM10.8 9.5A3.2 3.2 0 1 0 10.8 14.5L9.6 13A1.3 1.3 0 1 1 9.6 11ZM17.4 9.5A3.2 3.2 0 1 0 17.4 14.5L16.2 13A1.3 1.3 0 1 1 16.2 11Z\"/>","chart":"<path class=\"ln\" d=\"M4 3.5v16.2h16.2\"/><path d=\"M7 14.9L11 10.3l3 2.5l5.2-6.2V17.7H7Z\"/>","chat":"<path d=\"M2.7 4.7A1.8 1.8 0 0 1 4.5 3h15.2a1.8 1.8 0 0 1 1.8 1.8v10.9a1.8 1.8 0 0 1-1.8 1.8H8.3L2.7 21.8Z\"/>","chat-off":"<path d=\"M8.1 4.6H19a1.6 1.6 0 0 1 1.6 1.6v9.5a1.6 1.6 0 0 1-0.4 1ZM3.4 7.3L12.9 16.8H8.3l-4.9 4.6Z\"/><path class=\"ln\" d=\"M4 4.2l16 16\"/>","check-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM17.3 8.9l1.6 1.6l-7 7l-4.1-4.1l1.6-1.6l2.6 2.6Z\"/>","child":"<circle cx=\"12\" cy=\"5.2\" r=\"2.6\"/><path class=\"ln\" d=\"M7.7 10L12 8.6l4.3 1.4M12 8.6v5.8M9.1 20.1l2.9-5.8l2.9 5.8\"/>","chip":"<path fill-rule=\"evenodd\" d=\"M8.6 7.2h6.8a1.4 1.4 0 0 1 1.4 1.4v6.8a1.4 1.4 0 0 1-1.4 1.4H8.6a1.4 1.4 0 0 1-1.4-1.4V8.6a1.4 1.4 0 0 1 1.4-1.4Zm1.7 3.1v3.4h3.4v-3.4Z\"/><path class=\"ln\" d=\"M10.2 4v3.2M13.8 4v3.2M10.2 16.8V20M13.8 16.8V20M4 10.2h3.2M4 13.8h3.2M16.8 10.2H20M16.8 13.8H20\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"9.5\"/>","city":"<path fill-rule=\"evenodd\" d=\"M5.4 7.7h4.6v10.9H5.4Zm1.9 2.2v1.4h1.2v-1.4Zm0 2.9v1.4h1.2v-1.4Z\"/><path fill-rule=\"evenodd\" d=\"M10.7 3.4h5.1v15.2h-5.1Zm1.7 2.4v1.4h1.4V5.8Zm0 2.9v1.4h1.4V8.7Zm0 2.9v1.4h1.4v-1.4Z\"/><path d=\"M16.5 11.3h2.2v7.4h-2.2Z\"/><path class=\"ln\" d=\"M3.9 18.6h16.1\"/>","clapper":"<path fill-rule=\"evenodd\" d=\"M2.6 5.9L19.9 2.7l0.9 4.3L3.2 8.7ZM7 5.1L9.2 8.1l2.6-0.2L9.6 4.6ZM12.6 4l2.2 3.6l2.6-0.3L15.2 3.6Z\"/><path d=\"M3.1 9.9h18.2v9a1.6 1.6 0 0 1-1.6 1.6h-15a1.6 1.6 0 0 1-1.6-1.6Z\"/>","clipboard":"<path fill-rule=\"evenodd\" d=\"M7.5 4.2h9a2 2 0 0 1 2 2v13.1a2 2 0 0 1-2 2H7.5a2 2 0 0 1-2-2V6.3a2 2 0 0 1 2-2ZM9.1 9.9v1.6h5.8v-1.6Zm0 3.9v1.6h3.9v-1.6Z\"/><rect x=\"8.7\" y=\"2.5\" width=\"6.6\" height=\"3.7\" rx=\"1.2\"/>","clipboard-check":"<path fill-rule=\"evenodd\" d=\"M7.5 4.2h9a2 2 0 0 1 2 2v13.1a2 2 0 0 1-2 2H7.5a2 2 0 0 1-2-2V6.3a2 2 0 0 1 2-2ZM8 13.1L11 16.1l5-5l-1.6-1.6l-3.4 3.4l-1.5-1.5Z\"/><rect x=\"8.7\" y=\"2.5\" width=\"6.6\" height=\"3.7\" rx=\"1.2\"/>","clock":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.9 6.6h2.3v5l3.3 2l-1.1 2l-4.3-2.7Z\"/>","cloud":"<path d=\"M7.9 18.2h9.6a4.2 4.2 0 0 0 0.6-8.3A6.1 6.1 0 0 0 6.5 9.2A4.5 4.5 0 0 0 7.9 18.2Z\"/>","cloud-off":"<path d=\"M10.1 5.7A6 6 0 0 1 17.9 9.8a4.1 4.1 0 0 1 2.8 6.5ZM6.6 9.1L15.5 18H7.9A4.4 4.4 0 0 1 6.4 9.2Z\"/><path class=\"ln\" d=\"M5.1 4.2l15 15\"/>","coin":"<path fill-rule=\"evenodd\" d=\"M13 2.6a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM12 5.8h2.1v1.2c1.1 0.2 2.1 0.8 2.6 1.7l-1.7 1c-0.3-0.6-1-1-1.8-1c-0.9 0-1.5 0.4-1.5 1s0.5 0.8 1.8 1.1c1.8 0.4 3.5 1 3.5 3.1c0 1.4-1 2.5-2.8 2.8V18h-2.1v-1.3c-1.2-0.2-2.3-0.9-2.8-2l1.7-1c0.4 0.7 1.1 1.1 2.1 1.1c1 0 1.6-0.4 1.6-1s-0.6-0.8-2-1.1c-1.7-0.4-3.4-1-3.4-3c0-1.3 1-2.4 2.7-2.7Z\"/>","columns":"<path d=\"M2.6 6.3a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v1.5H2.6ZM2.6 9.9h8.3v10.4H5.3a2.7 2.7 0 0 1-2.7-2.7ZM13 9.9h8.3v7.7a2.7 2.7 0 0 1-2.7 2.7H13Z\"/>","compass":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM17.4 7.7L14.9 14.1L8.5 16.5L11 10.1Z\"/>","compress":"<path d=\"M9.7 9.7H2.7L9.7 2.7ZM14.3 9.7h7l-7-7ZM9.7 14.3H2.7l7 7ZM14.3 14.3h7l-7 7Z\"/>","computer":"<rect x=\"2.6\" y=\"3.4\" width=\"18.8\" height=\"12.8\" rx=\"2.2\"/><path class=\"ln\" d=\"M8 20h8M12 16.2v3.8\"/>","contrast":"<path fill-rule=\"evenodd\" d=\"M12.4 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 2.1a7.2 7.2 0 0 1 0 14.4Z\"/>","cookie":"<path fill-rule=\"evenodd\" d=\"M21.6 12.4a9.4 9.4 0 1 1-10-10a3.4 3.4 0 0 0 4.4 3.9a3.4 3.4 0 0 0 5.5 5.9ZM7 9.1a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM10.9 14a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM6.5 15.7a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM14.8 15.7a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0Z\"/>","copy":"<rect x=\"7.5\" y=\"7.5\" width=\"13.8\" height=\"13.8\" rx=\"2.5\"/><path class=\"ln\" d=\"M16.4 7.5V5.9a2.1 2.1 0 0 0-2.1-2.1H5.9a2.1 2.1 0 0 0-2.1 2.1v8.5a2.1 2.1 0 0 0 2.1 2.1h1.7\"/>","couch":"<path d=\"M7.1 4.2h9.7A2.5 2.5 0 0 1 19.4 6.7v1.9a3.1 3.1 0 0 0-3.1 3.1v0.1H7.7v-0.1A3.1 3.1 0 0 0 4.6 8.7V6.7A2.5 2.5 0 0 1 7.1 4.2ZM2.7 11.6a2.5 2.5 0 0 1 5.1 0v1.9h8.6V11.6a2.5 2.5 0 0 1 5.1 0v5.5H2.7Z\"/><path class=\"ln\" d=\"M5.2 17.1v1.9M18.8 17.1v1.9\"/>","crown":"<path d=\"M2.6 7.3L7.4 11.5L12 4.1L16.6 11.5l4.8-4.2l-2 11.7H4.6Z\"/>","cubes":"<path d=\"M12 2.3L7.8 4.4v4.8l4.2 2.1l4.2-2.1V4.4ZM6.8 12.1L2.7 14.2v4.8l4.2 2.1l4.2-2.1v-4.8ZM17.2 12.1l-4.2 2.1v4.8l4.2 2.1l4.2-2.1v-4.8Z\"/>","database":"<ellipse cx=\"12\" cy=\"5.7\" rx=\"7.9\" ry=\"3.2\"/><path d=\"M4.1 7.5a7.9 3.2 0 0 0 15.9 0v4.3a7.9 3.2 0 0 1-15.9 0ZM4.1 13.4a7.9 3.2 0 0 0 15.9 0v4.7a7.9 3.2 0 0 1-15.9 0Z\"/>","dice":"<path fill-rule=\"evenodd\" d=\"M6.2 2.7h11.5a3.6 3.6 0 0 1 3.6 3.6v11.5a3.6 3.6 0 0 1-3.6 3.6H6.2a3.6 3.6 0 0 1-3.6-3.6V6.2a3.6 3.6 0 0 1 3.6-3.6ZM6.2 8.2a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM13.8 8.2a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM10 12a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM6.2 15.8a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM13.8 15.8a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0Z\"/>","dish":"<path d=\"M2.7 9.7A11.7 11.7 0 0 0 14.4 21.4Z\"/><path class=\"ln\" d=\"M8.8 15.3l4.9-4.9M14.2 4a5.8 5.8 0 0 1 5.8 5.8M14.2 8.1a1.8 1.8 0 0 1 1.8 1.8\"/>","donate":"<path fill-rule=\"evenodd\" d=\"M12 2.2a5.9 5.9 0 1 0 0 11.8a5.9 5.9 0 0 0 0-11.8Zm-0.8 3.4h1.7v5.1h-1.7ZM2.9 15h18.2v6H2.9ZM7.8 17.1v1.6h8.5V17.1Z\"/>","door":"<path fill-rule=\"evenodd\" d=\"M14.3 4.5L6.2 3v17.1l8.2-1.5ZM10.2 11.5a1 1 0 1 0 2 0a1 1 0 1 0-2 0Z\"/><path class=\"ln\" d=\"M15.5 5.3h1.4a0.9 0.9 0 0 1 0.9 0.9v13.3M15.5 19.5H20M4 19.5h2.1\"/>","download":"<path d=\"M10.6 2.3h2.5v7h4.7L11.9 15.4l-5.9-5.9h4.7Z\"/><path class=\"ln\" d=\"M4.5 19.7h14.9\"/>","drive":"<path fill-rule=\"evenodd\" d=\"M5.3 11.4h13.5a2.7 2.7 0 0 1 2.7 2.7v3a2.7 2.7 0 0 1-2.7 2.7H5.3a2.7 2.7 0 0 1-2.7-2.7v-3a2.7 2.7 0 0 1 2.7-2.7ZM16.8 14.4a1.1 1.1 0 1 1 0 2.3a1.1 1.1 0 0 1 0-2.3ZM13.7 14.4a1.1 1.1 0 1 1 0 2.3a1.1 1.1 0 0 1 0-2.3Z\"/><path class=\"ln\" d=\"M4.1 11.9L6.8 4.7h10.4l2.8 7.2\"/>","drop":"<path d=\"M12 2.2s7 7.4 7 12.6a7 7 0 0 1-13.9 0C5 9.6 12 2.2 12 2.2Z\"/>","eraser":"<path d=\"M14 3.9l6.1 6.1-5.2 5.2-6.1-6.1ZM7.4 10l6.2 6.2-3.2 3.2H6.1l-2.9-2.9a1.6 1.6 0 0 1 0-2.3Z\"/><path class=\"ln\" d=\"M13 19.4h7\"/>","expand":"<path d=\"M2.7 2.7h7L2.7 9.7ZM21.3 2.7h-7l7 7ZM2.7 21.3h7l-7-7ZM21.3 21.3h-7l7-7Z\"/>","eye":"<path fill-rule=\"evenodd\" d=\"M12 5.3C6.4 5.3 2.6 12 2.6 12s3.9 6.7 9.4 6.7S21.5 12 21.5 12S17.6 5.3 12 5.3Zm0 3.3a3.3 3.3 0 1 1 0 6.7a3.3 3.3 0 0 1 0-6.7Z\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/>","eye-off":"<path d=\"M9 5.8A10 10 0 0 1 21.4 12a10 10 0 0 1-2.5 3.8L15.3 12.2a3.3 3.3 0 0 0-3.5-3.5ZM15 18.2A10 10 0 0 1 2.6 12a10 10 0 0 1 2.5-3.8L8.7 11.8a3.3 3.3 0 0 0 3.5 3.5Z\"/><path class=\"ln\" d=\"M4.6 4.6l14.8 14.8\"/>","fast-forward":"<path d=\"M3 5.5v13.1L12 12ZM12.7 5.5v13.1L21.8 12Z\"/>","file":"<path d=\"M5.8 2.4h7.6v5.2h5V21.2H5.8ZM15 2.7l3.1 3.3h-3.1Z\"/>","file-code":"<path fill-rule=\"evenodd\" d=\"M5.8 2.4h7.6v5.2h5V21.2H5.8ZM9.8 11l1.5 1.4l-1.6 1.7l1.6 1.7l-1.5 1.4l-2.9-3.1ZM14.4 11l-1.5 1.4l1.6 1.7l-1.6 1.7l1.5 1.4l2.9-3.1Z\"/><path d=\"M15 2.7l3.1 3.3h-3.1Z\"/>","film":"<path fill-rule=\"evenodd\" d=\"M4.8 3.6h14.4a2.3 2.3 0 0 1 2.3 2.3v12.3a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 18.2V5.8A2.3 2.3 0 0 1 4.8 3.6Zm3.8 2v12.9h6.8V5.5ZM4.4 6v2h2.1V6Zm0 5v1.8h2.1v-1.8Zm0 4.9v2h2.1v-2Zm13.1-10v2h2.1V6Zm0 5v1.8h2.1v-1.8Zm0 4.9v2h2.1v-2Z\"/>","finish":"<path d=\"M6.5 4.2h3.9v4.6H6.5ZM14.8 4.2h4a0.9 0.9 0 0 1 0.9 0.9v3.7h-4.9ZM10.5 8.6h4.5v4.6H10.5Z\"/><path class=\"ln\" d=\"M6 20.3V4.1\"/>","fire":"<path fill-rule=\"evenodd\" d=\"M12.1 19.8a6.8 6.8 0 0 0 6.8-6.8c0-4.3-2.9-6.8-4.2-10.3c-2.1 2.3-2.6 4.6-2.6 4.6S10.8 5.1 9.6 3C7.1 5.9 5.2 9.2 5.2 12.9A6.8 6.8 0 0 0 12.1 21.4Zm0-2.8a2.8 2.8 0 0 0 2.8-2.8c0-1.6-1.2-2.7-1.7-4c-0.9 1-1.1 1.8-1.1 1.8s-0.5-0.9-1-1.6c-1 1.2-1.7 2.5-1.7 3.9a2.8 2.8 0 0 0 2.7 2.7Z\"/>","fish":"<path fill-rule=\"evenodd\" d=\"M7.6 12c2.1-3.6 4.8-5.4 7.9-5.4 2.9 0 5 2.2 6.1 5.4-1.1 3.2-3.2 5.4-6.1 5.4-3.1 0-5.8-1.8-7.9-5.4ZM15.7 10.6a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0Z\"/><path d=\"M8.4 12 2.9 7.4v9.2Z\"/>","flag":"<path class=\"ln\" d=\"M7.2 20.5V4.3\"/><path d=\"M7.8 4.1h10.9l-2 4.1l2 4.1H7.8Z\"/>","focus":"<circle cx=\"12\" cy=\"12\" r=\"3.6\"/><path class=\"ln\" d=\"M3.8 9V6.1A2.3 2.3 0 0 1 6.1 3.8H9M15 3.8h2.9A2.3 2.3 0 0 1 20.2 6.1V9M20.2 15v2.9a2.3 2.3 0 0 1-2.3 2.3H15M9 20.2H6.1A2.3 2.3 0 0 1 3.8 17.9V15\"/>","folder":"<path d=\"M2.7 6.4a2.1 2.1 0 0 1 2.1-2.1h4.4l2.1 2.1h8.1a2.1 2.1 0 0 1 2.1 2.1v8.7a2.1 2.1 0 0 1-2.1 2.1H4.8a2.1 2.1 0 0 1-2.1-2.1Z\"/>","folder-tree":"<path d=\"M9.6 5h4l1 1.5h6.4v5.3H9.6ZM9.6 14.3h4l1 1.5h6.4v5.3H9.6Z\"/><path class=\"ln\" d=\"M4.1 3.6v13.2a1 1 0 0 0 1 1H9.6M4.1 8.8H9.6\"/>","food":"<path d=\"M4.7 3.3h1.5v4.7h0.6V3.3h1.4v4.7h0.6V3.3h1.5v4.9A2.9 2.9 0 0 1 8.6 10.8V21.7H6.5V10.8a2.9 2.9 0 0 1-1.8-2.7ZM18.9 21.7h-2.3v-7.4h-3V10.6c0-3.9 1.7-6.6 5.2-7.7Z\"/>","game":"<path fill-rule=\"evenodd\" d=\"M7.6 6.3h8.8a5.8 5.8 0 0 1 0 11.5H7.6a5.8 5.8 0 0 1 0-11.5Zm-1.4 3.7v1.6H4.7v1.8h1.6v1.6H7.9v-1.6h1.6v-1.8H7.9v-1.6Zm9.2 1a1.2 1.2 0 1 0 0 2.3a1.2 1.2 0 0 0 0-2.3Zm2.5 1.6a1.2 1.2 0 1 0 0 2.3a1.2 1.2 0 0 0 0-2.3Z\"/>","gauge":"<path fill-rule=\"evenodd\" d=\"M3.7 18A9.4 9.4 0 1 1 19.4 19.6ZM15 8.6l-2.2 5.6l-2.1 0.3l-0.4-2.1Z\"/>","gavel":"<path d=\"M11.3 5.5L5.5 11.3l6.1 6.1l5.8-5.8Z\"/><path class=\"ln\" d=\"M13.7 4.2l5 5M3.8 19.1l5.7-5.7M13.7 19.1h6.3\"/>","gear":"<path fill-rule=\"evenodd\" d=\"M18.2 14.6a1.4 1.4 0 0 0 0.3 1.7l0.1 0.1a1.7 1.7 0 1 1-2.7 2.7l-0.1-0.1a1.4 1.4 0 0 0-1.7-0.3a1.4 1.4 0 0 0-0.9 1.4V19.6a1.7 1.7 0 1 1-3.8 0v-0.1a1.4 1.4 0 0 0-1-1.4a1.4 1.4 0 0 0-1.7 0.3l-0.1 0.1a1.7 1.7 0 1 1-2.7-2.7l0.1-0.1a1.4 1.4 0 0 0 0.3-1.7a1.4 1.4 0 0 0-1.4-0.9H4.6a1.7 1.7 0 1 1 0-3.8h0.1a1.4 1.4 0 0 0 1.4-1a1.4 1.4 0 0 0-0.3-1.7l-0.1-0.1a1.7 1.7 0 1 1 2.7-2.7l0.1 0.1a1.4 1.4 0 0 0 1.7 0.3H9.6a1.4 1.4 0 0 0 0.9-1.4V4.6a1.7 1.7 0 1 1 3.8 0v0.1a1.4 1.4 0 0 0 0.9 1.4a1.4 1.4 0 0 0 1.7-0.3l0.1-0.1a1.7 1.7 0 1 1 2.7 2.7l-0.1 0.1a1.4 1.4 0 0 0-0.3 1.7V9.6a1.4 1.4 0 0 0 1.4 0.9H19.6a1.7 1.7 0 1 1 0 3.8h-0.1a1.4 1.4 0 0 0-1.4 0.9ZM12.1 9.3a2.8 2.8 0 1 0 0 6.5a2.8 2.8 0 0 0 0-6.5Z\"/>","gem":"<path d=\"M6.4 3.9h11.2l3.8 5.3l-9.4 11.6L2.6 9.2Z\"/>","ghost":"<path fill-rule=\"evenodd\" d=\"M3.4 21.3V9.9a8.7 8.7 0 0 1 17.2 0v11.5l-2.9-2.1l-2.9 2.1l-2.9-2.1l-2.9 2.1l-2.9-2.1ZM8.8 7.8a1.6 1.6 0 1 0 0 3.1a1.6 1.6 0 0 0 0-3.1Zm6.2 0a1.6 1.6 0 1 0 0 3.1a1.6 1.6 0 0 0 0-3.1Z\"/>","gift":"<path d=\"M3.1 8.6h17.7v3.3H3.1ZM4.4 12.9h6.6v8.2H5.4a1 1 0 0 1-1-1ZM13 12.9h6.6v7.2a1 1 0 0 1-1 1H13Z\"/><path class=\"ln\" d=\"M12 8.6c-1.5-3.1-5.1-4.1-5.1-1.5S12 8.6 12 8.6s5.1-1 5.1-3.6s-3.6-1.5-5.1 3.6\"/>","globe":"<path d=\"M2.6 11.1A9.5 9.5 0 0 1 12 2.6a4.5 9.5 0 0 0-4.5 8.5ZM21.4 11.1A9.5 9.5 0 0 0 12 2.6a4.5 9.5 0 0 1 4.5 8.5ZM2.6 12.9A9.5 9.5 0 0 0 12 21.5a4.5 9.5 0 0 1-4.5-8.5ZM21.4 12.9A9.5 9.5 0 0 1 12 21.5a4.5 9.5 0 0 0 4.5-8.5ZM12 2.6a2.7 9.5 0 0 0-2.7 8.5h5.3A2.7 9.5 0 0 0 12 2.6ZM12 21.5a2.7 9.5 0 0 1-2.7-8.5h5.3A2.7 9.5 0 0 1 12 21.5Z\"/>","grid":"<rect x=\"2.7\" y=\"2.7\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"13.3\" y=\"2.7\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"2.7\" y=\"13.3\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"13.3\" y=\"13.3\" width=\"8\" height=\"8\" rx=\"2.2\"/>","group":"<rect x=\"6.9\" y=\"6.8\" width=\"6.4\" height=\"4.8\" rx=\"1.1\"/><rect x=\"10.8\" y=\"12.5\" width=\"6.4\" height=\"4.6\" rx=\"1.1\"/><path class=\"ln\" d=\"M4 7.6V4h3.6M16.5 4h3.6v3.6M20 16.4v3.6h-3.6M7.6 20H4v-3.6\"/>","hammer":"<path d=\"M13.1 2.5l7.5 7.5l-3.7 3.7l-7.5-7.5ZM12.9 8.9L3.7 18.1a2.1 2.1 0 0 0 2.9 2.9l9.2-9.2Z\"/>","hand":"<path d=\"M9 14.3V4.9a1.4 1.4 0 0 1 2.8 0v5.2a0.5 0.5 0 0 0 1.1 0V3.8a1.4 1.4 0 0 1 2.8 0v6.4a0.5 0.5 0 0 0 1.1 0V5.7a1.4 1.4 0 0 1 2.8 0v7.7c0 4.5-2.8 7.7-7.3 7.7c-2.9 0-4.7-1.5-6.3-4l-1.8-2.9a1.7 1.7 0 0 1 2.8-2Z\"/>","headphones":"<path class=\"ln\" d=\"M4 14.5V12a8 8 0 0 1 16 0v2.5\"/><rect x=\"2.6\" y=\"13.4\" width=\"5.4\" height=\"7.6\" rx=\"1.8\"/><rect x=\"16\" y=\"13.4\" width=\"5.4\" height=\"7.6\" rx=\"1.8\"/>","heart":"<path d=\"M12 21.6s-9.3-5.7-9.3-13a5.3 5.3 0 0 1 9.3-3.6a5.3 5.3 0 0 1 9.3 3.6c0 7.4-9.3 13-9.3 13Z\"/>","heart-broken":"<path fill-rule=\"evenodd\" d=\"M12 21.6s-9.3-5.7-9.3-13a5.3 5.3 0 0 1 9.3-3.6a5.3 5.3 0 0 1 9.3 3.6c0 7.4-9.3 13-9.3 13ZM10.9 5.9l2.7 0.6l-1.4 2.7l3.6 2.5l-2.6 5.9l-2.5-1.1l1.5-3.7l-3.7-2.5Z\"/>","help":"<path fill-rule=\"evenodd\" d=\"M13 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM9.3 9.4A3.7 3.7 0 1 1 13.9 12.8v1.6h-2.1V10.9h1a1.6 1.6 0 1 0-1.6-1.6ZM13 15.8a1.3 1.3 0 1 1 0 2.6a1.3 1.3 0 0 1 0-2.6Z\"/>","history":"<path d=\"M5.8 5.8A9.1 9.1 0 1 1 3.2 12.2h3a6.1 6.1 0 1 0 1.8-4.3ZM2.5 2.7v7.2h7.2ZM11.2 8.1h2.2v3.7l2.7 1.6l-1.1 1.8l-3.8-2.3Z\"/>","home":"<path d=\"M2.7 10.4L12 2.3l9.3 8.1v9.5a1.1 1.1 0 0 1-1.1 1.1h-5.6v-6.9h-5.4v6.9H3.8a1.1 1.1 0 0 1-1.1-1.1Z\"/>","hot-tub":"<circle cx=\"8.2\" cy=\"5.4\" r=\"2.4\"/><path d=\"M2.6 10.9h18.8v4.2a5.1 5.1 0 0 1-5.1 5.1h-8.5a5.1 5.1 0 0 1-5.1-5.1Z\"/><path class=\"ln\" d=\"M6.3 10.9v-0.6A2.4 2.4 0 0 1 8.7 8H9.2M13.5 9.4c0.8-0.9-0.8-1.9 0-2.8M17.3 9.4c0.8-0.9-0.8-1.9 0-2.8\"/>","id":"<path fill-rule=\"evenodd\" d=\"M4.8 4.6h14.4a2.3 2.3 0 0 1 2.3 2.3v10.3a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 17.2V6.9A2.3 2.3 0 0 1 4.8 4.6Zm4.1 4.2a2.2 2.2 0 1 0 0 4.3a2.2 2.2 0 0 0 0-4.3Zm-3.2 7.7h6.4c-0.6-1.7-1.8-2.7-3.2-2.7s-2.6 0.9-3.2 2.7Zm8.8-7.2v1.5h4V9.4Zm0 3.6v1.5h4v-1.5Z\"/>","idea":"<path d=\"M8.3 14.9A6.5 6.5 0 1 1 15.7 14.9c-0.7 0.6-1.3 1.3-1.4 2.2H9.7c-0.1-0.9-0.7-1.6-1.4-2.2Z\"/><path class=\"ln\" d=\"M9.6 20.5h4.8\"/>","image":"<path fill-rule=\"evenodd\" d=\"M2.8 6.1a2.7 2.7 0 0 1 2.7-2.7h13.3A2.7 2.7 0 0 1 21.5 6.1v12.3a2.7 2.7 0 0 1-2.7 2.7H5.4A2.7 2.7 0 0 1 2.8 18.4Zm6.2 2.2a2 2 0 1 0 0 3.9a2 2 0 0 0 0-3.9Zm7.5 3.5l-8.3 7.8h10.7a1.5 1.5 0 0 0 1.5-1.5v-3.1Z\"/>","inbox":"<path fill-rule=\"evenodd\" d=\"M2.7 12.4L5.2 3.7h13.6l2.5 8.8V17.9a1.6 1.6 0 0 1-1.6 1.6H4.2A1.6 1.6 0 0 1 2.7 17.9ZM6.4 5.6L4.6 11.5h3.8l1.5 2.4h4.3l1.5-2.4h3.8L17.6 5.6Z\"/>","info":"<path fill-rule=\"evenodd\" d=\"M12.9 2.6a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.7 10.5h2.5V16.9h-2.5ZM12.9 6.4a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","invite":"<circle cx=\"8.8\" cy=\"7.7\" r=\"3.2\"/><path d=\"M2.9 19.2c0-3.4 2.7-5.8 5.8-5.8s5.8 2.4 5.8 5.8Z\"/><path class=\"ln\" d=\"M17.6 7.8v5.3M15 10.5h5.3\"/>","key":"<path fill-rule=\"evenodd\" d=\"M8.3 10.1a5.4 5.4 0 1 1 0 10.9a5.4 5.4 0 0 1 0-10.9Zm0 3.6a1.9 1.9 0 1 0 0 3.8a1.9 1.9 0 0 0 0-3.8Z\"/><path class=\"ln\" d=\"M11.7 12.2l8.7-8.7M16.7 7.2l3.1 3.1M14.4 9.5l2.1 2.1\"/>","keyboard":"<path fill-rule=\"evenodd\" d=\"M4.8 5.7h14.4A2.3 2.3 0 0 1 21.5 8v8a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 16V8a2.3 2.3 0 0 1 2.3-2.3ZM5.3 9.1h1.9v1.9H5.3ZM8.2 9.1h1.9v1.9H8.2ZM11 9.1h1.9v1.9h-1.9ZM13.9 9.1h1.9v1.9h-1.9ZM16.8 9.1h1.9v1.9h-1.9ZM7.8 13.3h8.4v1.7H7.8Z\"/>","laugh":"<path fill-rule=\"evenodd\" d=\"M13 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM7.9 8.1l3.5 1.7l-3.5 1.7l-0.7-1.4l0.7-0.4l-0.7-0.4ZM18 8.1l-3.5 1.7l3.5 1.7l0.7-1.4l-0.7-0.4l0.7-0.4ZM8.5 13.5h8.9a4.4 4.4 0 0 1-8.9 0Z\"/>","layers":"<path d=\"M12 4.3l8.6 5l-8.6 5l-8.6-5Z\"/><path class=\"ln\" d=\"M3.9 13.9l8.1 4.5l8.1-4.5\"/>","leaf":"<path fill-rule=\"evenodd\" d=\"M5.4 18.6c0-8.9 5.2-14.2 15.7-15.7c-1 10.4-6.3 15.7-15.7 15.7Zm3.1-2L16.8 7.1l-9.5 8.5Z\"/><path class=\"ln\" d=\"M3.7 20.3L6 18\"/>","lifebuoy":"<path fill-rule=\"evenodd\" d=\"M13.1 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 5.3a4.1 4.1 0 1 1 0 8.2a4.1 4.1 0 0 1 0-8.2ZM15.5 16.2L18.4 19.1L20.2 17.3L17.2 14.3ZM10.8 16.2L7.9 19.1L6 17.3L9 14.3ZM15.5 7.8L18.4 4.9L20.2 6.7L17.2 9.7ZM10.8 7.8L7.9 4.9L6 6.7L9 9.7Z\"/>","live":"<circle cx=\"12\" cy=\"12\" r=\"2.5\"/><path class=\"ln\" d=\"M8.5 8.5a4.9 4.9 0 0 0 0 6.9M15.5 8.5a4.9 4.9 0 0 1 0 6.9M6.2 6.2a8.2 8.2 0 0 0 0 11.5M17.8 6.2a8.2 8.2 0 0 1 0 11.5\"/>","lock":"<path d=\"M4.6 11.1a1.6 1.6 0 0 1 1.6-1.6h11.5a1.6 1.6 0 0 1 1.6 1.6v8.4a1.6 1.6 0 0 1-1.6 1.6H6.2A1.6 1.6 0 0 1 4.6 19.6Z\"/><path class=\"ln\" d=\"M7.9 9.4V6.8a4.1 4.1 0 0 1 8 0V9.4\"/>","mail":"<path d=\"M2.5 7.3v-0.4A2.3 2.3 0 0 1 4.8 4.6h14.4A2.3 2.3 0 0 1 21.5 6.9v0.4l-8.7 5.6a1.6 1.6 0 0 1-1.7 0ZM2.5 9.4v7.6A2.3 2.3 0 0 0 4.8 19.4h14.4a2.3 2.3 0 0 0 2.3-2.3V9.4l-8 5.2a2.7 2.7 0 0 1-3 0Z\"/>","map":"<path d=\"M2.6 6.1 8.3 4.2v14l-5.7 1.9ZM9.7 4.2l4.6 1.6v14l-4.6-1.6ZM15.7 5.8l5.7-1.9v14l-5.7 1.9Z\"/>","mask":"<path fill-rule=\"evenodd\" d=\"M2.7 6.1c3.3 1.4 6.4 2.1 9.4 2.1s6.1-0.6 9.4-2.1c0 7.1-2.9 11.7-5.8 11.7c-1.5 0-2.2-1.5-3.6-1.5s-2.1 1.5-3.6 1.5c-2.9 0-5.8-4.6-5.8-11.7ZM6.8 11.6a1.6 1.1 0 1 0 3.3 0a1.6 1.1 0 1 0-3.3 0ZM14 11.6a1.6 1.1 0 1 0 3.3 0a1.6 1.1 0 1 0-3.3 0Z\"/>","medical":"<path fill-rule=\"evenodd\" d=\"M5.3 6.3h13.5a2.7 2.7 0 0 1 2.7 2.7v8.7a2.7 2.7 0 0 1-2.7 2.7H5.3a2.7 2.7 0 0 1-2.7-2.7V9a2.7 2.7 0 0 1 2.7-2.7ZM10.9 10h2.3v2.2h2.2v2.3h-2.2V16.7h-2.3v-2.2H8.7v-2.3h2.2Z\"/><path class=\"ln\" d=\"M9.2 6.3V5.3a0.9 0.9 0 0 1 0.9-0.9h3.8a0.9 0.9 0 0 1 0.9 0.9v1\"/>","megaphone":"<path d=\"M2.7 9.1a1.1 1.1 0 0 1 1.1-1.1h2.7l9.7-4.8v16.1l-9.7-4.8H3.8A1.1 1.1 0 0 1 2.7 13.2Z\"/><path class=\"ln\" d=\"M6.6 14.5l1.2 5M18.8 8.5a3.4 3.4 0 0 1 0 5.4\"/>","members":"<circle cx=\"9.1\" cy=\"7.1\" r=\"3.5\"/><path d=\"M2.6 19.6c0-3.7 2.9-6.3 6.4-6.3s6.4 2.6 6.4 6.3Z\"/><circle cx=\"16.5\" cy=\"6.6\" r=\"2.8\"/><path d=\"M16.7 12.4c2.8 0.4 4.7 2.7 4.7 6.2V19.6h-4.3c0-2.8-1-5.1-2.7-6.5Z\"/>","mic":"<path d=\"M8.9 5.8a3.1 3.1 0 0 1 6.2 0V10.8a3.1 3.1 0 0 1-6.2 0Z\"/><path class=\"ln\" d=\"M5.9 10.8a6.1 6.1 0 0 0 12.3 0M12 16.9V20.2\"/>","mic-off":"<path d=\"M9 5.4A3.1 3.1 0 0 1 15.2 5.8V10.7a3.1 3.1 0 0 1-0.1 0.8Z\"/><path class=\"ln\" d=\"M5.4 10.7a6.6 6.6 0 0 0 11 5M18.7 10.7c0 0.8-0.1 1.5-0.4 2.2M12.1 17.3v2.8M4.5 4.1l15.1 15.1\"/>","minus-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM7.9 10.9v2.5h10.2v-2.5Z\"/>","money":"<path fill-rule=\"evenodd\" d=\"M4.8 5.7h14.4a2.1 2.1 0 0 1 2.1 2.1v8.4a2.1 2.1 0 0 1-2.1 2.1H4.8a2.1 2.1 0 0 1-2.1-2.1V7.8a2.1 2.1 0 0 1 2.1-2.1ZM12 8.9a3.1 3.1 0 1 0 0 6.3a3.1 3.1 0 0 0 0-6.3ZM6.3 8.7a1 1 0 1 0 0 2.1a1 1 0 0 0 0-2.1Zm11.4 4.6a1 1 0 1 0 0 2.1a1 1 0 0 0 0-2.1Z\"/><circle cx=\"12\" cy=\"12\" r=\"1.4\"/>","moon":"<path d=\"M21.7 14.6A9.7 9.7 0 1 1 9.4 2.3a8 8 0 0 0 12.3 12.3Z\"/>","more":"<circle cx=\"4.8\" cy=\"12\" r=\"2.2\"/><circle cx=\"12\" cy=\"12\" r=\"2.2\"/><circle cx=\"19.2\" cy=\"12\" r=\"2.2\"/>","mountain":"<path d=\"M2.8 18.1L9.2 5.3l4.1 7.1L16.1 8.9L21.5 18.1Z\"/>","mug":"<path d=\"M3.8 8.9h12.2v7.6a4.6 4.6 0 0 1-4.6 4.6h-3a4.6 4.6 0 0 1-4.6-4.6Z\"/><path class=\"ln\" d=\"M16 11H16.9a2.5 2.5 0 0 1 0 5h-0.9M7.9 3.7c-0.8 1 0.8 2 0 3M11.4 3.7c-0.8 1 0.8 2 0 3\"/>","music":"<path d=\"M7.6 5.4l13.3-2.5v3.5l-13.3 2.5Z\"/><path class=\"ln\" d=\"M8.8 17.9V6.5M19.7 15.6V4.2\"/><circle cx=\"5.5\" cy=\"17.9\" r=\"3.2\"/><circle cx=\"16.7\" cy=\"15.6\" r=\"3.2\"/>","mute":"<path d=\"M2.9 8.8h4l5.5-4.2v14.8l-5.5-4.2H2.9Z\"/><path class=\"ln\" d=\"M15.5 9.6l4.7 4.7M20.3 9.6l-4.7 4.7\"/>","news":"<path fill-rule=\"evenodd\" d=\"M2.8 2.6h14.2v15.2a2.2 2.2 0 0 0 2.2 2.2H5.1A2.2 2.2 0 0 1 2.8 18.9ZM6.6 6.6h6.8v1.7H6.6Zm0 3.7h6.8v1.7H6.6Zm0 3.7h4v1.7H6.6Z\"/><path class=\"ln\" d=\"M17.1 7.7H19.8V17.9a2.1 2.1 0 0 1-2.1 2.1\"/>","nodes":"<circle cx=\"12\" cy=\"5.2\" r=\"3\"/><circle cx=\"5.6\" cy=\"18\" r=\"3\"/><circle cx=\"18.4\" cy=\"18\" r=\"3\"/><path class=\"ln\" d=\"M10.9 7.6L6.8 15.5M13.1 7.6l4.1 7.9M8.4 18h7.1\"/>","palette":"<path fill-rule=\"evenodd\" d=\"M12.1 2.7a9.4 9.4 0 1 0 0 18.8c1.3 0 2-0.9 2-1.9c0-1.2-1-1.5-1-2.5s0.8-1.6 2-1.6H17.3a4.4 4.4 0 0 0 4.4-4.4C21.7 6.7 17.4 2.7 12.1 2.7ZM7.8 9.7a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm2.5-3.7a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm4.6 0a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","parking":"<path fill-rule=\"evenodd\" d=\"M6.2 2.6h11.6a3.6 3.6 0 0 1 3.6 3.6v11.6a3.6 3.6 0 0 1-3.6 3.6H6.2a3.6 3.6 0 0 1-3.6-3.6V6.2a3.6 3.6 0 0 1 3.6-3.6ZM8.4 18V6H13a4 4 0 0 1 0 8h-2.4v4ZM10.6 8.2v3.6H13a1.8 1.8 0 0 0 0-3.6Z\"/>","pause":"<rect x=\"4.8\" y=\"2.6\" width=\"5.1\" height=\"18.8\" rx=\"1.5\"/><rect x=\"14.2\" y=\"2.6\" width=\"5.1\" height=\"18.8\" rx=\"1.5\"/>","paw":"<path d=\"M6.9 18.5c0-3.5 2.2-6.3 5.1-6.3s5.1 2.8 5.1 6.3c0 1.8-1.3 2.9-2.9 2.9c-0.9 0-1.5-0.6-2.2-0.6s-1.3 0.6-2.2 0.6c-1.6 0-2.9-1.1-2.9-2.9Z\"/><circle cx=\"5.1\" cy=\"10.2\" r=\"2.6\"/><circle cx=\"9.1\" cy=\"5.1\" r=\"2.6\"/><circle cx=\"14.9\" cy=\"5.1\" r=\"2.6\"/><circle cx=\"18.9\" cy=\"10.2\" r=\"2.6\"/>","pencil":"<path d=\"M15.6 3.1a2.2 2.2 0 0 1 3.2 3.2L6.9 18.1l-4.5 1.2l1.2-4.5Z\"/><path class=\"ln\" d=\"M11.8 19.7H20.1\"/>","phone":"<path d=\"M5.1 2.4h4.9l2.4 6l-2.7 1.6a10.7 10.7 0 0 0 4.2 4.2l1.6-2.7l6 2.4v4.9a2.3 2.3 0 0 1-2.3 2.3A17.3 17.3 0 0 1 2.8 4.6a2.3 2.3 0 0 1 2.3-2.3Z\"/>","piggy":"<path fill-rule=\"evenodd\" d=\"M2.6 11.4c0-3.6 3.7-6.5 8.7-6.5c1.4 0 2.7 0.2 3.9 0.7L18.3 3.8v3.7c0.9 0.8 1.5 1.7 1.7 2.8h1.5v3.8H20c-0.5 1.1-1.4 2-2.6 2.6V20.1h-3.3v-2.2c-0.9 0.2-1.8 0.3-2.8 0.3c-0.9 0-1.7-0.1-2.6-0.3v2.2h-3.3v-3.3C3.7 15.5 2.6 13.6 2.6 11.4ZM14.5 9.8a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0ZM8.3 6.8v1.5h3.7V6.8Z\"/>","pin":"<path fill-rule=\"evenodd\" d=\"M12 2.8a7.1 7.1 0 0 0-7.1 7.1c0 5.5 7.1 11.8 7.1 11.8s7.1-6.3 7.1-11.8A7.1 7.1 0 0 0 12 2.8Zm0 4.5a2.5 2.5 0 1 1 0 5.1a2.5 2.5 0 0 1 0-5.1Z\"/>","plane":"<path d=\"M21.2 2.9L2.4 10.7l6.9 3.2ZM21.2 2.9l-11 12l3.2 6.9Z\"/>","play":"<path d=\"M5.1 3.5v17a0.9 0.9 0 0 0 1.3 0.8l12.8-8.5a0.9 0.9 0 0 0 0-1.6L6.5 2.7a0.9 0.9 0 0 0-1.3 0.8Z\"/>","plug":"<path d=\"M6.5 8h10.9v3.3a5.4 5.4 0 0 1-10.9 0Z\"/><path class=\"ln\" d=\"M9.2 4.2v3.8M14.6 4.2v3.8M11.9 16.8v3.5\"/>","plus-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm-1.2 4.2h2.5v3.8H18v2.5h-3.8V17.1h-2.5v-3.8H7.9v-2.5h3.8Z\"/>","print":"<path d=\"M6.2 2.6h11.5v4.4H6.2ZM2.6 11a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v4.1a2.7 2.7 0 0 1-2.7 2.7h-0.3v-4.5H5.6v4.5h-0.3A2.7 2.7 0 0 1 2.6 15.1ZM7.1 14.7h9.9v6.7H7.1Z\"/>","puzzle":"<path d=\"M2.8 5.4h5a2.9 2.9 0 1 1 5.8 0h5v5a2.9 2.9 0 1 1 0 5.8v5h-5a2.9 2.9 0 1 0-5.8 0H2.8v-5a2.9 2.9 0 1 0 0-5.8Z\"/>","radio":"<path fill-rule=\"evenodd\" d=\"M5.2 7.8h13.6a2.5 2.5 0 0 1 2.5 2.5v7.8a2.5 2.5 0 0 1-2.5 2.5H5.2A2.5 2.5 0 0 1 2.6 18.1v-7.8A2.5 2.5 0 0 1 5.2 7.8ZM12.9 14.2a2.5 2.5 0 1 0 5.1 0a2.5 2.5 0 1 0-5.1 0ZM6.1 11.6v2h4.7v-2ZM6.1 15v2h4.7v-2Z\"/><path class=\"ln\" d=\"M7.1 7.8L16.9 4\"/>","ranking":"<path d=\"M3.5 13.7h4.9V9.9h7.1v5.7h4.9v5.4H3.5ZM12 2.2l1 2l2.2 0.3l-1.6 1.6l0.4 2.2L12 7.5l-2 1l0.4-2.2l-1.6-1.6l2.2-0.3Z\"/>","record":"<path fill-rule=\"evenodd\" d=\"M13.2 2.7a9.3 9.3 0 1 0 0 18.5a9.3 9.3 0 0 0 0-18.5Zm0 3.3a6 6 0 1 1 0 11.9a6 6 0 0 1 0-11.9Z\"/><circle cx=\"13.2\" cy=\"12\" r=\"3.7\"/>","restroom":"<circle cx=\"7.2\" cy=\"5.1\" r=\"1.8\"/><circle cx=\"16.2\" cy=\"5.1\" r=\"1.8\"/><path d=\"M6 9h1.8a1.5 1.5 0 0 1 1.5 1.5v4.7H8.6v5H5.5v-5H4.7v-4.7A1.5 1.5 0 0 1 6 9Z\"/><path d=\"M15.7 9.4h1.1a0.9 0.9 0 0 1 0.9 0.7l1.9 6.3H17.4v3.6h-2.3v-3.6h-2.1l1.9-6.3a0.9 0.9 0 0 1 0.9-0.7Z\"/><path class=\"ln\" d=\"M11.6 3.7v16.3\"/>","retry":"<path class=\"ln\" d=\"M4 12a8 8 0 0 1 13.4-5.9M20 12a8 8 0 0 1-13.4 5.9\"/><path d=\"M21.3 2.8v7h-7ZM2.7 21.2v-7h7Z\"/>","rewind":"<path d=\"M21 5.5v13.1L12 12ZM11.3 5.5v13.1L2.2 12Z\"/>","robot":"<path fill-rule=\"evenodd\" d=\"M8.5 8.7h6.9a3.1 3.1 0 0 1 3.1 3.1v4.3A3.1 3.1 0 0 1 15.5 19.3H8.5A3.1 3.1 0 0 1 5.4 16.2v-4.3A3.1 3.1 0 0 1 8.5 8.7Zm0.9 3.8a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4Zm5.2 0a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4ZM9.7 16v1.3h4.5v-1.3Z\"/><path class=\"ln\" d=\"M12 5.3v3.5M3.9 13.1v2.8M20.2 13.1v2.8\"/>","rocket":"<path fill-rule=\"evenodd\" d=\"M11.9 16.6L7.4 12.2C9.1 6.1 13.6 2.9 21.1 2.9c0 7.5-3.2 12-9.2 13.7ZM13.6 8.7a1.7 1.7 0 1 0 3.5 0a1.7 1.7 0 1 0-3.5 0Z\"/><path d=\"M3.7 12.2L6.8 7.5h3.5L6.6 12.6ZM11.4 17.4L11.9 20.3l4.6-3.3v-3.5ZM6.3 17.7c-2 0-3.9 2-3.9 3.9c2 0 3.9-2 3.9-3.9Z\"/>","save":"<path fill-rule=\"evenodd\" d=\"M4.4 2.7h12.4l3.5 3.5V20.8a0.7 0.7 0 0 1-0.7 0.7H4.4a0.7 0.7 0 0 1-0.7-0.7V3.4a0.7 0.7 0 0 1 0.7-0.7Zm3.4 1.4v4.9h7.4V4.1ZM7.2 13.6v6.4h9.6v-6.4Z\"/>","scissors":"<path fill-rule=\"evenodd\" d=\"M6.2 3.8a3.2 3.2 0 1 1 0 6.5a3.2 3.2 0 0 1 0-6.5Zm0 2a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4ZM6.2 13.7a3.2 3.2 0 1 1 0 6.5a3.2 3.2 0 0 1 0-6.5Zm0 2a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4Z\"/><path class=\"ln\" d=\"M8.7 8.8L20.4 18.1M8.7 15.2L20.4 5.9\"/>","search":"<path fill-rule=\"evenodd\" d=\"M10.6 3a7.7 7.7 0 1 1 0 15.4a7.7 7.7 0 0 1 0-15.4Zm0 3.2a4.5 4.5 0 1 0 0 9.2a4.5 4.5 0 0 0 0-9.2Z\"/><path class=\"ln\" d=\"M16.3 16.4l3.8 3.8\"/>","seedling":"<path d=\"M12.4 10.8C12.4 6.1 9.5 2.9 4 2.9c0 4.9 3 7.8 8.4 7.8ZM11.6 13.4c0-4.7 3-7.8 8.4-7.8c0 4.9-3 7.8-8.4 7.8Z\"/><path class=\"ln\" d=\"M12 20.4V12.5\"/>","server":"<path fill-rule=\"evenodd\" d=\"M5.2 2.6h13.4a2.7 2.7 0 0 1 2.7 2.7v3.4a2.7 2.7 0 0 1-2.7 2.7H5.2A2.7 2.7 0 0 1 2.5 8.6V5.3A2.7 2.7 0 0 1 5.2 2.6Zm2.2 2.9a1.5 1.5 0 1 0 0 2.9a1.5 1.5 0 0 0 0-2.9ZM5.2 12.7h13.4a2.7 2.7 0 0 1 2.7 2.7v3.4a2.7 2.7 0 0 1-2.7 2.7H5.2A2.7 2.7 0 0 1 2.5 18.7v-3.4A2.7 2.7 0 0 1 5.2 12.7Zm2.2 2.9a1.5 1.5 0 1 0 0 2.9a1.5 1.5 0 0 0 0-2.9Z\"/>","shapes":"<circle cx=\"7.3\" cy=\"7.6\" r=\"4.3\"/><path d=\"M16.5 2.7l5 8.6h-9.8ZM13.4 13.7h7.4v7.4h-7.4ZM2.5 21.1L6.8 13.7l4.3 7.4Z\"/>","shelter":"<path fill-rule=\"evenodd\" d=\"M12 2.7L2.5 10.2l1.1 1.5l1-0.8v10h14.8V10.9l1 0.8l1.1-1.5ZM9.5 20.9v-4.8a2.5 2.5 0 0 1 4.9 0v4.8ZM12 9.1a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4Z\"/>","shield":"<path d=\"M12 2.6 4.6 5.8V12c0 4.6 3.1 8 7.4 9.5 4.3-1.5 7.4-4.9 7.4-9.5V5.8Z\"/>","shirt":"<path d=\"M8 2.7L2.5 5.9l2.5 4.9l1.5-0.8v11.2h11V10.1l1.5 0.8L21.5 5.9l-5.5-3.2c-0.6 1.5-1.9 2.5-4 2.5S8.6 4.2 8 2.7Z\"/>","skip":"<path d=\"M3.3 2.6v18.9L15.8 12Z\"/><rect x=\"17.8\" y=\"2.7\" width=\"3.2\" height=\"18.7\" rx=\"1.5\"/>","skull":"<path fill-rule=\"evenodd\" d=\"M12 2.6a7.9 7.9 0 0 0-7.9 7.9c0 2.4 1.1 4.2 2.5 5.4V19.5a2 2 0 0 0 2 2h6.8a2 2 0 0 0 2-2v-3.6c1.4-1.2 2.5-3 2.5-5.4A7.9 7.9 0 0 0 12 2.6ZM9.2 9.4a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4ZM14.8 9.4a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4ZM12 13.3l1 1.8h-2ZM10.1 18.1h0.9v2.4h-0.9ZM13 18.1h0.9v2.4H13Z\"/>","sliders":"<path class=\"ln\" d=\"M4 7h8.8M19.2 7H20M4 17h2.8M13.2 17H20\"/><circle cx=\"16\" cy=\"7\" r=\"3.2\"/><circle cx=\"10\" cy=\"17\" r=\"3.2\"/>","smile":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM9.9 8.6a1.4 1.4 0 1 1 0 2.8a1.4 1.4 0 0 1 0-2.8ZM16 8.6a1.4 1.4 0 1 1 0 2.8a1.4 1.4 0 0 1 0-2.8ZM8.8 13.5h8.3a4.1 4.1 0 0 1-8.3 0Z\"/>","spark":"<path d=\"M10.7 2.6l2 5.6l5.6 2l-5.6 2L10.7 17.8l-2-5.6l-5.6-2l5.6-2ZM17.6 14.7l0.9 2.5l2.5 0.9l-2.5 0.9l-0.9 2.5l-0.9-2.5l-2.5-0.9l2.5-0.9Z\"/>","square":"<rect x=\"2.7\" y=\"2.7\" width=\"18.7\" height=\"18.7\" rx=\"3.3\"/>","star":"<path d=\"M12 2.8l2.9 6l6.6 0.8l-4.9 4.5l1.3 6.6L12 17.4l-5.9 3.2l1.3-6.6l-4.9-4.5l6.6-0.8Z\"/>","steps":"<path fill-rule=\"evenodd\" d=\"M2.8 4h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3ZM2.8 9.4h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3ZM2.8 15h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3Z\"/><path class=\"ln\" d=\"M10.6 6.4H20.3M10.6 11.9H20.3M10.6 17.4H20.3\"/>","store":"<path d=\"M4.4 3h15l2 6.2v1.4a3.2 3.2 0 0 1-6.3 0a3.2 3.2 0 0 1-6.3 0a3.2 3.2 0 0 1-6.3 0V9.2ZM4 14.9h15.9v5.6a1.1 1.1 0 0 1-1.1 1.1H14.1v-5.1h-4.4v5.1H5.1a1.1 1.1 0 0 1-1.1-1.1Z\"/>","sun":"<circle cx=\"12\" cy=\"12\" r=\"3.8\"/><path class=\"ln\" d=\"M12 3.9v1.3M12 18.7v1.3M6.3 6.2l0.9 0.9M16.8 16.8L17.8 17.8M4 12h1.3M18.7 12h1.3M6.3 17.8l0.9-0.9M16.8 7.2L17.8 6.2\"/>","sword":"<path d=\"M21.2 2.8v4.7l-10.8 10.8l-4.7-4.7L16.5 2.8Z\"/><path class=\"ln\" d=\"M5.2 11.9L12.1 18.8M8.4 15.6L3.7 20.3\"/>","table":"<path d=\"M2.7 6.2a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.3H2.7ZM2.7 10.5h5.3v9.9H5.2a2.5 2.5 0 0 1-2.5-2.5ZM9.8 10.5h4.5v9.9H9.8ZM16.1 10.5h5.3V17.9a2.5 2.5 0 0 1-2.5 2.5h-2.7Z\"/>","tack":"<path d=\"M8.5 3h7.2a0.8 0.8 0 0 1 0.8 1l-0.8 4l2.8 2.8a0.7 0.7 0 0 1-0.6 1.4H6.1a0.7 0.7 0 0 1-0.6-1.4l2.8-2.8l-0.8-4a0.8 0.8 0 0 1 0.8-1Z\"/><path class=\"ln\" d=\"M12 12.2V20.5\"/>","tag":"<path fill-rule=\"evenodd\" d=\"M2.9 3.4A1.5 1.5 0 0 1 4.4 2.9h7.2l10 10l-8.7 8.7L2.9 10.6Zm5 2.5a1.9 1.9 0 1 0 0 3.7a1.9 1.9 0 0 0 0-3.7Z\"/>","target":"<path fill-rule=\"evenodd\" d=\"M12 2.6a9.4 9.4 0 1 1 0 18.8a9.4 9.4 0 0 1 0-18.8Zm0 3a6.4 6.4 0 1 0 0 12.9a6.4 6.4 0 0 0 0-12.9Zm0 2.7a3.7 3.7 0 1 1 0 7.4a3.7 3.7 0 0 1 0-7.4Z\"/>","tent":"<path fill-rule=\"evenodd\" d=\"M12 4.2L19.3 17.4H4.7ZM12 11.6L9.5 17.4h5.1Z\"/><path class=\"ln\" d=\"M3.9 17.7h16.2\"/>","terminal":"<path fill-rule=\"evenodd\" d=\"M5 3.6h14a2.4 2.4 0 0 1 2.4 2.4v12a2.4 2.4 0 0 1-2.4 2.4H5A2.4 2.4 0 0 1 2.6 18V6A2.4 2.4 0 0 1 5 3.6Zm1.4 6 1.3-1.3 3.6 3.7-3.6 3.7-1.3-1.3 2.4-2.4Zm6.4 4.6h4.4v1.7h-4.4Z\"/>","text":"<path fill-rule=\"evenodd\" d=\"M10.2 2.4h3.7l6.8 18.7h-3.4l-1.6-4.7H8.3l-1.6 4.7H3.3ZM9.3 13.6h5.4L12 5.7Z\"/>","thumbs-up":"<path d=\"M6.5 10.1v10.3H3.6a1.1 1.1 0 0 1-1.1-1.1v-8a1.1 1.1 0 0 1 1.1-1.1ZM7.9 10.1l3.6-7.3a2.4 2.4 0 0 1 2.7 2.8l-0.6 3.5h5.6a2.2 2.2 0 0 1 2.2 2.6l-1.4 7a2.2 2.2 0 0 1-2.2 1.7H7.9Z\"/>","ticket":"<path fill-rule=\"evenodd\" d=\"M2.6 6.4h18.8v3.4a2.2 2.2 0 0 0 0 4.4v3.4H2.6v-3.4a2.2 2.2 0 0 0 0-4.4ZM13.3 8.4v1.8h1.4V8.4Zm0 2.7v1.8h1.4v-1.8Zm0 2.7v1.8h1.4v-1.8Z\"/>","toolbox":"<path d=\"M2.7 10.1a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.2H2.7ZM2.7 13.7h4.7v1.5h1.6v-1.5h6.2v1.5h1.6v-1.5h4.7V17.8a2.5 2.5 0 0 1-2.5 2.5H5.2A2.5 2.5 0 0 1 2.7 17.8Z\"/><path class=\"ln\" d=\"M9.1 7.5V5.7A1.5 1.5 0 0 1 10.5 4.2h2.9A1.5 1.5 0 0 1 14.9 5.7v1.9\"/>","train":"<path fill-rule=\"evenodd\" d=\"M9.1 2.7h5.8a2.9 2.9 0 0 1 2.9 2.9v6.8a2.9 2.9 0 0 1-2.9 2.9H9.1a2.9 2.9 0 0 1-2.9-2.9V5.6a2.9 2.9 0 0 1 2.9-2.9Zm-1.2 2.5v4.1h8.2V5.2ZM9.1 11.4a1.1 1.1 0 1 0 0 2.1A1.1 1.1 0 0 0 9.1 11.4Zm5.8 0a1.1 1.1 0 1 0 0 2.1a1.1 1.1 0 0 0 0-2.1Z\"/><path class=\"ln\" d=\"M8.6 20.2l1.9-3.4M15.4 20.2l-1.9-3.4\"/>","trash":"<path d=\"M5.8 7.6h12.4l-1 12a2 2 0 0 1-2 1.8H8.8a2 2 0 0 1-2-1.8Z\"/><path class=\"ln\" d=\"M3.8 6.6h16.4M9 6.6V4h6v2.6\"/>","tree":"<path d=\"M12 2.5L5.9 10.7h3L4.4 16.9h15.1l-4.5-6.2h3Z\"/><path class=\"ln\" d=\"M12 16.2v4\"/>","trophy":"<path d=\"M7.3 3h9.6V8.6a4.7 4.7 0 0 1-9.6 0Z\"/><path class=\"ln\" d=\"M7.3 5.6H5a3.1 3.1 0 0 0 3.1 4.1M16.7 5.6h2.3a3.1 3.1 0 0 1-3.1 4.1M12 13.7V16.9M8 20.4h8.1\"/>","tv":"<rect x=\"2.7\" y=\"7.4\" width=\"18.6\" height=\"13.3\" rx=\"2.3\"/><path class=\"ln\" d=\"M8.2 3.8l3.8 3.4l3.8-3.4\"/>","umbrella":"<path d=\"M2.6 12.7a9.4 9.4 0 0 1 18.8 0Z\"/><path class=\"ln\" d=\"M12 12.7v5.6a1.9 1.9 0 0 1-3.8 0\"/>","upload":"<path d=\"M10.6 21.7h2.5v-7h4.7L11.9 8.6l-5.9 5.9h4.7Z\"/><path class=\"ln\" d=\"M4.5 4.3h14.9\"/>","user":"<circle cx=\"12\" cy=\"6.7\" r=\"4.5\"/><path d=\"M3.5 21c0-4.7 3.8-7.8 8.5-7.8s8.5 3.2 8.5 7.8Z\"/>","user-check":"<circle cx=\"8.9\" cy=\"7.5\" r=\"3.2\"/><path d=\"M2.9 19.3c0-3.4 2.7-5.9 5.9-5.9s5.9 2.4 5.9 5.9Z\"/><path class=\"ln\" d=\"M14.8 10.6l1.9 1.9l3.8-3.8\"/>","user-circle":"<path fill-rule=\"evenodd\" d=\"M13 2.9a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 3.8a3.3 3.3 0 1 1 0 6.5a3.3 3.3 0 0 1 0-6.5ZM7.2 17.9a6.7 6.7 0 0 1 11.5 0a8.1 8.1 0 0 1-11.5 0Z\"/>","user-lock":"<circle cx=\"9.9\" cy=\"7.2\" r=\"4.2\"/><path d=\"M2.7 20c0-4.2 3.2-7 7.2-7c1.2 0 2.3 0.3 3.2 0.8v6.2Z\"/><rect x=\"14.5\" y=\"14.5\" width=\"6.9\" height=\"5.7\" rx=\"1.3\"/><path class=\"ln\" d=\"M16.4 14.5v-1a1.5 1.5 0 0 1 3 0v1\"/>","user-minus":"<circle cx=\"9.6\" cy=\"7.6\" r=\"3.9\"/><path d=\"M2.8 19.7c0-3.9 3.1-6.6 6.8-6.6c1.2 0 2.2 0.3 3.1 0.8v5.9Z\"/><path class=\"ln\" d=\"M15.1 16.7h5.2\"/>","user-off":"<path d=\"M9.1 4.7A4.2 4.2 0 1 1 15 10.6ZM4.2 20.8c0-4.3 2.8-6.9 6.3-7.2L17.5 20.8Z\"/><path class=\"ln\" d=\"M4 3.5l16.2 16.2\"/>","user-pen":"<circle cx=\"9.8\" cy=\"7.6\" r=\"4\"/><path d=\"M2.9 19.9c0-4 3.1-6.7 6.9-6.7c1.2 0 2.2 0.3 3.1 0.8v5.9Z\"/><path d=\"M19.5 11.4l2.1 2.1l-5.2 5.2l-2.8 0.7l0.7-2.8Z\"/>","user-tag":"<circle cx=\"10\" cy=\"7.4\" r=\"4\"/><path d=\"M2.9 19.9c0-4 3.2-6.8 7-6.8c1.2 0 2.3 0.3 3.2 0.8v6Z\"/><path fill-rule=\"evenodd\" d=\"M14 13.7h4.5l3.1 3.1l-3.1 3.1h-4.5ZM15.2 16.8a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0Z\"/>","video":"<path d=\"M3 7.7a2 2 0 0 1 2-2H14.4a2 2 0 0 1 2 2v8.7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM17.5 10.1l4.2-2.6v8.9l-4.2-2.6Z\"/>","volume":"<path d=\"M2.5 8.9h4l5.4-4.2v14.6l-5.4-4.2H2.5Z\"/><path class=\"ln\" d=\"M15.2 9.3a3.8 3.8 0 0 1 0 5.5M17.7 6.5a7.5 7.5 0 0 1 0 11\"/>","walk":"<circle cx=\"12.4\" cy=\"4.6\" r=\"2.2\"/><path class=\"ln\" d=\"M8.7 19.8l2.4-5.5l1.9 2.2V19.8M11.1 14.3l0.9-5.2l-2.8 1.5l-1.4 2.8M12 9.1l1.8 2.8l2.8 0.9\"/>","wallet":"<path fill-rule=\"evenodd\" d=\"M5.7 2.8H19v4.6h1.6a0.9 0.9 0 0 1 0.9 0.9v12.4H5.1a2.5 2.5 0 0 1-2.5-2.5V5.9a3.1 3.1 0 0 1 3.1-3.1Zm0 2.1a1 1 0 0 0 0 2.1h11.2V4.9ZM16.7 12.9a1.4 1.4 0 1 0 0 2.8a1.4 1.4 0 0 0 0-2.8Z\"/>","water":"<path d=\"M2.7 7.9c2.4 0 2.4-2 4.7-2s2.4 2 4.6 2s2.4-2 4.7-2s2.4 2 4.6 2v3.2c-2.3 0-2.3-2-4.6-2s-2.4 2-4.7 2s-2.3-2-4.6-2s-2.4 2-4.7 2ZM2.7 14.8c2.4 0 2.4-2 4.7-2s2.4 2 4.6 2s2.4-2 4.7-2s2.4 2 4.6 2v3.2c-2.3 0-2.3-2-4.6-2s-2.4 2-4.7 2s-2.3-2-4.6-2s-2.4 2-4.7 2Z\"/>","weight":"<circle cx=\"12\" cy=\"4.8\" r=\"2.6\"/><path d=\"M6.5 8.7h10.9l3.3 12.3H3.2Z\"/>","window":"<path d=\"M2.6 6.2a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v1.5H2.6ZM2.6 9.8h18.7v7.7a2.7 2.7 0 0 1-2.7 2.7h-13.4a2.7 2.7 0 0 1-2.7-2.7Z\"/>","window-restore":"<path d=\"M2.7 10.4a2.7 2.7 0 0 1 2.7-2.7h8.2a2.7 2.7 0 0 1 2.7 2.7v7.2a2.7 2.7 0 0 1-2.7 2.7h-8.2a2.7 2.7 0 0 1-2.7-2.7ZM7.9 6.4V6.3a2.7 2.7 0 0 1 2.7-2.7h8.2a2.7 2.7 0 0 1 2.7 2.7v7.2a2.7 2.7 0 0 1-2.7 2.7h-1.1v-5.8a4 4 0 0 0-4-4Z\"/>","wrench":"<path d=\"M14.9 2.5a6 6 0 0 0-5.5 8.3L2.7 17.5V21.3H6.5l6.7-6.7a6 6 0 0 0 8.3-5.5l-3.5 1.3l-2.9-2.9l1.3-3.5Z\"/>","x-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM10.1 7.9L12.9 10.7l2.9-2.9l1.4 1.4l-2.9 2.9l2.9 2.9l-1.4 1.4l-2.9-2.9l-2.9 2.9l-1.4-1.4l2.9-2.9l-2.9-2.9Z\"/>","zap":"<path d=\"M13.9 2.5L5.2 13.3h6.1l-1.2 8.1l8.7-10.8h-6.1Z\"/>"};++ const FA = {"address-card":"id","adjust":"contrast","align-left":"align-left","angle-down":"down","angle-right":"chevron","angles-left":"chevrons-left","angles-right":"chevrons-right","arrow-down":"arrow-down","arrow-down-short-wide":"sort-asc","arrow-down-wide-short":"sort","arrow-left":"arrow-left","arrow-right":"arrow-right","arrow-right-arrow-left":"swap","arrow-right-from-bracket":"leave","arrow-right-to-bracket":"enter","arrow-rotate-right":"retry","arrow-trend-down":"trend-down","arrow-trend-up":"trend-up","arrow-up":"arrow-up","arrow-up-from-bracket":"share","arrow-up-right-from-square":"open","arrows-left-right":"arrows-h","arrows-rotate":"retry","arrows-to-dot":"focus","arrows-up-down-left-right":"move","at":"at","backward":"rewind","backward-fast":"rewind","balance-scale":"scale","ban":"ban","bars":"menu","basket-shopping":"cart","bell":"bell","bolt":"zap","bolt-lightning":"zap","book":"book","book-open":"book-open","box":"box","box-archive":"archive","box-open":"box","brain":"brain","bridge":"bridge","briefcase":"briefcase","briefcase-medical":"medical","broadcast-tower":"live","bug":"bug","bullhorn":"megaphone","bullseye":"target","burst":"burst","calendar":"calendar","calendar-day":"calendar","calendar-days":"calendar","camera":"camera","camera-cctv":"camera","camera-retro":"camera","camera-rotate":"camera-flip","camera-slash":"camera-off","campground":"tent","car":"car","caret-down":"down","cart":"cart","cart-plus":"cart","cart-shopping":"cart","chart-area":"chart","chart-bar":"chart","chart-column":"chart","chart-line":"chart","chart-simple":"chart","check":"check","check-circle":"check-circle","check-double":"check-double","chevron-down":"down","chevron-left":"back","chevron-right":"chevron","chevron-up":"up","child":"child","circle":"circle","circle-check":"check-circle","circle-dollar-to-slot":"donate","circle-dot":"record","circle-exclamation":"alert-circle","circle-half-stroke":"contrast","circle-info":"info","circle-minus":"minus-circle","circle-nodes":"nodes","circle-notch":"busy","circle-play":"play","circle-plus":"plus-circle","circle-question":"help","circle-user":"user-circle","circle-xmark":"x-circle","city":"city","clapperboard":"clapper","clipboard":"clipboard","clipboard-check":"clipboard-check","clipboard-list":"clipboard","clock":"clock","clock-rotate-left":"history","clone":"copy","closed-captioning":"cc","cloud":"cloud","cloud-arrow-up":"upload","cloud-bolt":"storm","cloud-moon":"cloud","cloud-rain":"cloud","cloud-showers-heavy":"cloud","cloud-slash":"cloud-off","cloud-sun":"cloud","code":"code","code-branch":"branch","code-commit":"commit","code-fork":"branch","coin":"coin","coins":"coin","comment":"chat","comment-dots":"chat","comment-slash":"chat-off","comments":"chat","compass":"compass","compress":"compress","cookie":"cookie","cookie-bite":"cookie","copy":"copy","couch":"couch","credit-card":"card","crown":"crown","cube":"box","cubes":"cubes","cut":"scissors","database":"database","desktop":"computer","diagram-project":"nodes","dice":"dice","dice-five":"dice","dice-six":"dice","display":"computer","dollar-sign":"dollar","door-closed":"door","door-open":"door","down-left-and-up-right-to-center":"compress","download":"download","droplet":"drop","ear-listen":"ear","earth-americas":"globe","earth-europe":"globe","ellipsis":"more","ellipsis-vertical":"more","envelope":"mail","eraser":"eraser","exchange-alt":"swap","expand":"expand","external-link":"open","eye":"eye","eye-slash":"eye-off","face-grin":"laugh","face-grin-squint":"laugh","face-grin-squint-tears":"laugh","face-grin-stars":"smile","face-laugh":"laugh","face-smile":"smile","faucet":"drop","faucet-drip":"drop","file":"file","file-audio":"file","file-code":"file-code","file-image":"image","file-lines":"file","file-pdf":"file","file-pen":"pencil","film":"film","filter":"filter","fire":"fire","fish":"fish","fist-raised":"fist","flag":"flag","flag-checkered":"finish","flask":"flask","flask-vial":"flask","floppy-disk":"save","folder":"folder","folder-open":"folder","folder-tree":"folder-tree","font":"text","forward":"fast-forward","forward-fast":"fast-forward","forward-step":"skip","gamepad":"game","gauge":"gauge","gauge-high":"gauge","gauge-simple":"gauge","gauge-simple-high":"gauge","gauge-simple-low":"gauge","gavel":"gavel","gear":"gear","gears":"gear","gem":"gem","ghost":"ghost","gift":"gift","globe":"globe","grid":"grid","grip":"grip","grip-vertical":"grip","hammer":"hammer","hand":"hand","hand-fist":"fist","hand-holding-dollar":"coin","hand-holding-heart":"care","hand-point-right":"hand","hand-pointer":"hand","handshake":"handshake","handshake-angle":"handshake","hard-drive":"drive","hashtag":"hash","hat-wizard":"wizard","hdd":"drive","heading":"heading","headphones":"headphones","headphones-simple":"headphones","headset":"headphones","heart":"heart","heart-crack":"heart-broken","heart-pulse":"heart-pulse","heartbeat":"heart-pulse","hiking":"mountain","hot-tub":"hot-tub","hot-tub-person":"hot-tub","hourglass":"hourglass","hourglass-end":"hourglass","hourglass-half":"hourglass","hourglass-start":"hourglass","house":"home","house-chimney":"home","house-signal":"home","icicles":"snow","icons":"shapes","id-card":"id","image":"image","images":"image","inbox":"inbox","infinity":"infinity","info-circle":"info","key":"key","keyboard":"keyboard","kit-medical":"medical","language":"language","laptop":"computer","layer-group":"layers","leaf":"leaf","left-right":"arrows-h","life-ring":"lifebuoy","lightbulb":"idea","link":"link","link-slash":"link-off","list":"list","list-check":"steps","list-ol":"list-ordered","list-ul":"list","location-dot":"pin","location-pin":"pin","lock":"lock","lungs":"lungs","magnifying-glass":"search","map":"map","map-location-dot":"map","map-marker-alt":"pin","map-pin":"pin","mask":"mask","message":"chat","microchip":"chip","microphone":"mic","microphone-lines":"mic","microphone-slash":"mic-off","minus":"minus","minus-circle":"minus-circle","mobile-screen":"phone","mobile-screen-button":"phone","money-bill":"money","money-bill-1":"money","money-bill-transfer":"money","money-bill-wave":"money","money-check":"money","moon":"moon","mountain":"mountain","mountain-sun":"mountain","mug-hot":"mug","mug-saucer":"mug","music":"music","network-wired":"nodes","newspaper":"news","object-group":"group","paint-brush":"palette","paintbrush":"palette","palette":"palette","paper-plane":"plane","parking":"parking","paste":"clipboard","pause":"pause","paw":"paw","pen":"pencil","pen-nib":"pencil","pen-to-square":"pencil","pencil":"pencil","people-arrows":"members","people-group":"members","person-hiking":"mountain","person-shelter":"shelter","person-swimming":"water","person-walking":"walk","phone":"phone","phone-slash":"phone-off","photo-film":"film","piggy-bank":"piggy","plane":"airplane","plane-departure":"airplane","play":"play","plug":"plug","plug-circle-bolt":"plug","plug-circle-check":"plug","plug-circle-xmark":"plug","plus":"plus","plus-circle":"plus-circle","print":"print","puzzle":"puzzle","puzzle-piece":"puzzle","puzzle-piece-simple":"puzzle","question":"help","quote-left":"quote","quote-right":"quote","radio":"radio","ranking-star":"ranking","recycle":"recycle","reply":"reply","restroom":"restroom","right-from-bracket":"leave","right-left":"swap","right-to-bracket":"enter","road":"road","robot":"robot","rocket":"rocket","rotate":"retry","rotate-back":"undo","rotate-left":"retry","rotate-right":"retry","route":"route","rss":"rss","satellite-dish":"dish","save":"save","scale-balanced":"scale","scissors":"scissors","screwdriver-wrench":"wrench","scroll":"scroll","search":"search","seedling":"seedling","server":"server","shapes":"shapes","share-nodes":"nodes","shield":"shield","shield-halved":"shield","shirt":"shirt","signal":"wifi","signature":"signature","sitemap":"nodes","skull":"skull","skull-crossbones":"skull","slash":"slash","sliders":"sliders","sliders-h":"sliders","smog":"cloud","snowflake":"snow","sparkles":"spark","spinner":"busy","square":"square","square-parking":"parking","star":"star","star-of-life":"spark","step-forward":"skip","stop":"stop","store":"store","suitcase":"briefcase","suitcase-medical":"medical","sun":"sun","sword":"sword","table":"table","table-cells":"grid","table-columns":"columns","table-list":"table","tag":"tag","tags":"tag","television":"tv","terminal":"terminal","text-height":"text","th":"grid","thumbs-up":"thumbs-up","thumbtack":"tack","ticket":"ticket","timeline":"timeline","times":"close","toolbox":"toolbox","tower-broadcast":"live","tower-cell":"live","train":"train","trash":"trash","trash-can":"trash","tree":"tree","triangle-exclamation":"alert","trophy":"trophy","tv":"tv","umbrella":"umbrella","umbrella-beach":"umbrella","undo":"undo","unlock":"lock","up-right-and-down-left-from-center":"expand","up-right-from-square":"open","upload":"upload","user":"user","user-astronaut":"user","user-check":"user-check","user-circle":"user-circle","user-edit":"user-pen","user-group":"members","user-lock":"user-lock","user-minus":"user-minus","user-pen":"user-pen","user-plus":"invite","user-secret":"user","user-shield":"shield","user-slash":"user-off","user-tag":"user-tag","users":"members","utensils":"food","video":"video","video-slash":"video","volume-high":"volume","volume-low":"volume","volume-off":"mute","volume-xmark":"mute","wallet":"wallet","wand-magic":"spark","wand-magic-sparkles":"spark","water":"water","wave-square":"wave","weight-hanging":"weight","wifi":"wifi","wind":"wind","window-maximize":"window","window-restore":"window-restore","wrench":"wrench","xmark":"close","xmark-circle":"x-circle"};++ const FB = {"youtube":"youtube","square-youtube":"youtube","twitch":"twitch","discord":"discord","github":"github","square-github":"github","x-twitter":"x","square-x-twitter":"x","twitter":"x","kick":"kick","kickstarter-k":"kick","kickstarter":"kickstarter","google":"google","tiktok":"tiktok","threads":"threads","spotify":"spotify","soundcloud":"soundcloud","reddit":"reddit","reddit-alien":"reddit","tumblr":"tumblr","hacker-news":"ycombinator","line":"line","paypal":"paypal","patreon":"patreon","mastodon":"mastodon","linux":"linux","instagram":"instagram","facebook":"facebook","facebook-f":"facebook","cloudflare":"cloudflare","brave":"brave","bluesky":"bluesky","bitcoin":"bitcoin","apple":"apple","telegram":"telegram","whatsapp":"whatsapp","steam":"steam","snapchat":"snapchat","pinterest":"pinterest","vimeo":"vimeo","vimeo-v":"vimeo","npm":"npm","node":"nodedotjs","node-js":"nodedotjs","docker":"docker","python":"python","android":"android","chrome":"googlechrome","firefox":"firefoxbrowser","firefox-browser":"firefoxbrowser","gitlab":"gitlab","ubuntu":"ubuntu","raspberry":"raspberrypi","raspberry-pi":"raspberrypi","stripe":"stripe","cc-stripe":"stripe","signal":"signal","matrix":"matrix","medium":"medium","wikipedia":"wikipedia","wikipedia-w":"wikipedia","ethereum":"ethereum"};++ // Font Awesome's style, size, layout and animation classes: they say how, not what.++ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;++ const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';++ const CSS = ".ovg::before,.ovg::after{content:none!important;display:none!important}.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:calc(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25));stroke-linecap:round;stroke-linejoin:round;overflow:visible}html[data-ov-icons=solid]{--ovg-pref:99999px}html[data-ov-icons=line]{--ovg-pref:0px}.ovg>svg.has-s{stroke-width:min(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25), max(0px, (1.15em - var(--ovg-pref, var(--ovg-solid-below, 18.4px))) * 999))}.ovg>svg>.ovg-s{fill:currentColor;stroke:none;clip-path:inset(max(0px, (1.15em - var(--ovg-pref, var(--ovg-solid-below, 18.4px))) * 999))}.ovg>svg>.ovg-s .ln{fill:none;stroke:currentColor;stroke-width:2.6px}.ovg>svg>.ovg-s,.ovg>svg>.ovg-s *{stroke-dasharray:none!important;animation:none!important}.ovg.ovg-solid>svg.has-s{stroke-width:0}.ovg.ovg-solid>svg>.ovg-s{clip-path:none}.ovg.ovg-line>svg.has-s{stroke-width:calc(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25))}.ovg.ovg-line>svg>.ovg-s{clip-path:inset(50%)}.ovg.ovg-thin{--ovg-stroke:1}.ovg.ovg-light{--ovg-stroke:1.5}.ovg.ovg-regular{--ovg-stroke:2}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.ovg-duotone>svg>.ovg-s{clip-path:none;opacity:.2;fill:var(--ovg-duotone-color,currentColor)}.ovg.ovg-duotone>svg.has-s{stroke-width:var(--ovg-stroke,2)}:where([aria-current=page],[aria-current=true],[aria-pressed=true],[aria-selected=true],.is-here)>.ovg:not(.ovg-line)>svg.has-s{stroke-width:0}:where([aria-current=page],[aria-current=true],[aria-pressed=true],[aria-selected=true],.is-here)>.ovg:not(.ovg-line)>svg>.ovg-s{clip-path:none}.ovg.ovg-brand>svg{fill:currentColor;stroke:none}.ovg.ovg-brand.ovg-brand-color{color:var(--ovg-brand)}.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.fa-solid{--ovg-solid-below:99999px}.ovg.fa-duotone{--ovg-duotone-color:var(--fa-secondary-color,currentColor)}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}.ovg.fa-1x{font-size:1em}.ovg.fa-2x{font-size:2em}.ovg.fa-3x{font-size:3em}.ovg.fa-4x{font-size:4em}.ovg.fa-5x{font-size:5em}.ovg.fa-6x{font-size:6em}.ovg.fa-7x{font-size:7em}.ovg.fa-8x{font-size:8em}.ovg.fa-9x{font-size:9em}.ovg.fa-10x{font-size:10em}.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}.ovg.fa-beat{animation:ovgBeat var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-bounce{animation:ovgBounce var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-fade{animation:ovgFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-beat-fade{animation:ovgBeatFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-flip{animation:ovgFlip var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-shake{animation:ovgShake var(--fa-animation-duration,1s) var(--fa-animation-timing,linear) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}.ovg.ovg-draw>svg>*{stroke-dasharray:1;stroke-dashoffset:1;animation:ovgDraw var(--ovg-draw-duration,.6s) cubic-bezier(.4,0,.2,1) var(--ovg-draw-delay,0s) forwards}.ovg.ovg-draw>svg>*:nth-child(2){animation-delay:calc(var(--ovg-draw-delay,0s) + .12s)}.ovg.ovg-draw>svg>*:nth-child(n+3){animation-delay:calc(var(--ovg-draw-delay,0s) + .24s)}@keyframes ovgDraw{to{stroke-dashoffset:0}}.ovg.ovg-once{--fa-animation-iteration-count:1}.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}html[data-ov-motion=off] .ovg,html[data-ov-motion=off] .ovg[data-ovg=busy] circle,html[data-ov-motion=off] .ovg.ovg-draw>svg>*{animation:none!important;stroke-dashoffset:0!important}";++ const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };++ const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };++ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).++ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;++ function transformOf(spec) {++ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;++ for (const p of String(spec || '').toLowerCase().split(/\s+/)) {++ const m = TF.exec(p);++ if (!m) continue;++ any = true;++ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }++ const n = Number(m[2]);++ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;++ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;++ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;++ }++ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';++ }++ // Brand logos (4.72.0): brands.js from beside this script, once, then every waiting element is drawn.++ const SRC = (document.currentScript && document.currentScript.src) || '';++ let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable++ const waiting = new Set();++ const BRANDS_REV = '0da501c80e60';++ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits++ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.++ let brandSeen = null;++ function wantBrands(el) {++ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }++ const r = el.getBoundingClientRect();++ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }++ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });++ brandSeen.observe(el);++ }++ function loadBrands() {++ if (brandState) return;++ if (window.OVBrands) { brandState = 2; return; }++ brandState = 1;++ // No backslashes here: this text is inside a template literal.++ const base = SRC ? SRC.split(/[?#]/)[0] : '';++ const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';++ const sc = document.createElement('script');++ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');++ sc.async = true;++ sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };++ sc.onerror = () => { brandState = 3; waiting.clear(); };++ document.head.appendChild(sc);++ }++ function drawBrand(el) {++ let fa = null;++ for (const c of el.classList) if (c.startsWith('fa-') && c !== 'fa-brands' && !MOD.test(c) && Object.prototype.hasOwnProperty.call(FB, c.slice(3))) { fa = c.slice(3); break; }++ if (!fa) return;++ const slug = FB[fa];++ if (el.dataset.ovg === 'brand:' + slug) return;++ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }++ const b = window.OVBrands.get(slug);++ if (!b || (!el.dataset.ovg && el.textContent.trim())) return;++ el.classList.add('ovg', 'ovg-brand');++ el.dataset.ovg = 'brand:' + slug; el.dataset.ovgFa = fa;++ // A near-black brand (GitHub, X, Apple…) would vanish on a dark page: it wears the text's colour instead.++ const n = parseInt(b.hex.slice(1), 16), lum = (0.2126 * (n >> 16) + 0.7152 * ((n >> 8) & 255) + 0.0722 * (n & 255)) / 255;++ el.style.setProperty('--ovg-brand', lum < 0.12 ? 'currentColor' : b.hex);++ const title = el.getAttribute('title');++ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }++ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');++ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="' + b.path + '"/></svg>';++ }++ function draw(el) {++ if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return drawBrand(el);++ const fa = nameOf(el);++ const g = fa && fromFa(fa);++ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }++ const tf = el.getAttribute('data-fa-transform') || '';++ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself forever++ if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon++ el.classList.add('ovg');++ el.dataset.ovg = g; el.dataset.ovgFa = fa;++ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;++ const title = el.getAttribute('title');++ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }++ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');++ const t = transformOf(tf);++ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (S[g] ? ' class="has-s"' : '') + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g] + (S[g] ? '<g class="ovg-s">' + S[g] + '</g>' : '')) + '</svg>';++ // ovg-draw draws each line in: every shape measures 1 along its length.++ if (el.classList.contains('ovg-draw')) el.firstChild.childNodes.forEach((n) => { if (n.setAttribute) n.setAttribute('pathLength', '1'); });++ }++ const SEL = 'i[class*="fa-"],span[class*="fa-"]';++ function swap(root) {++ const r = root || document;++ if (r.matches && r.matches(SEL)) draw(r);++ if (r.querySelectorAll) r.querySelectorAll(SEL).forEach(draw);++ }++ function start() {++ if (!document.getElementById('ov-glyphs-css')) { const s = document.createElement('style'); s.id = 'ov-glyphs-css'; s.textContent = CSS; document.head.appendChild(s); }++ swap(document);++ try {++ new MutationObserver((records) => {++ for (const r of records) {++ if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }++ r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });++ }++ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });++ } catch (e) { /* no observer: what was there is swapped */ }++ }++ window.OVGlyphs = { swap, fromFa };++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();++})();@@@ -26,7 +26,11 @@* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);-- * - busy: the V bobs and the O breathes, in turn; ok: the V becomes a tick, green, and hops (the tick only ever++ * - busy (working): the O breaks into eight dots and three bright ones go round it, dot to dot, while the V bobs++ * (the owner, 2026-11-10: "make the O turn into a bunch of little circles… a spinner that has little dots around++ * it… that actually looks like it's loading"); think (your agent is thinking or writing): three dots rise in turn++ * inside the O and the V's two strokes glimmer one after the other, the one sign used wherever tokens are on++ * their way; ok: the V becomes a tick, green, and hops (the tick only ever* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and* ripples go out from it; sleep: the O closes like an eye and the V sinks.@@@ -36,19 +40,25 @@* visit, from the first time a mark is on screen in this tab; later pages of a multi-page site open with the mark at* rest), while it or its link is hovered or focused (and LINGER_MS after), and while it shows a state, a page load or* progress. Otherwise its CSS animations are paused where they are. It never moves while the page is hidden, while it-- * is off-screen (IntersectionObserver), with data-static, under prefers-reduced-motion or under the network's own-- * reduced-motion setting (html[data-ov-motion=reduced], theme-loader.js); its colours and shapes still change.++ * is off-screen (IntersectionObserver), with data-static, or with motion off (html[data-ov-motion=off],++ * theme-loader.js); its colours and shapes still change. Calm (html[data-ov-motion=reduced]: the person's choice, or++ * the device asking for less motion) keeps the intro, the hover and every state, drops the linger after a hover and++ * lets fewer leaves fall a shorter way.** The season (theme-loader.js sets <html data-ov-season-mark="halloween|winter"> for anyone who has not turned seasonal* themes off, on any theme): the mark wears it, the O a pumpkin with its stem and leaf in October, caps of snow in* December. Now and then (every 4 to 8 minutes) a mark on screen flickers for a moment, and on the first visit of the-- * day a few leaves (snowflakes in December) drift down from the first mark seen. Never under either reduced-motion setting.++ * day a few leaves (snowflakes in December) drift down from the first mark seen: seven in full, three under Calm, none++ * with motion off.*/(function () {'use strict';if (window.__ovMark) return; window.__ovMark = true;let n = 0;const f = (v) => +v.toFixed(2);++ // The ground around the V where it crosses the O: the wide gap (the owner's pick, 2026-11-10, the Arena's P07:++ // "more ground around the V where it crosses"); it was 7.++ const HALO = 14;// The Pair in a 100-unit box, centred on (50, 50) (openvibe-shared/mark: the same numbers; test/mark.test.js).function pair(r, sw, vsw, a = 0.86, t = 1.5, b = 2.16) {const R = r + sw / 2, right = b * R + vsw / 2, cx = 50 - (right - R) / 2, cy = 50;@@@ -66,7 +76,7 @@err: [mx, A[1], mx, T[1] - vsw * 1.28, mx, T[1], mx, T[1] + 0.01],};for (const k in shapes) shapes[k] = shapes[k].map(f);-- return { cx: f(cx), cy, r, sw, R: f(R), vsw, halo: vsw + 7, A, T, B, ring, box, wideBox, shapes };++ return { cx: f(cx), cy, r, sw, R: f(R), vsw, halo: vsw + HALO, A, T, B, ring, box, wideBox, shapes };}const FORMS = { pair: pair(20, 13, 13), heavy: pair(18, 18, 17) };const formOf = (v) => (v === 'heavy' || v === 'solid' ? 'heavy' : 'pair');@@@ -131,7 +141,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}/* The states. */.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}--.ov-mark[data-state=busy] .og{animation:ovmBreathe .9s ease-in-out .45s infinite}++/* Working: the O as eight dots (a dash of no length with a round cap is a dot), three bright ones hopping round. */++.ov-mark[data-state=busy] :is(.o,.ot){stroke-linecap:round;stroke-width:var(--ovm-dot-w)}++.ov-mark[data-state=busy] .ot{opacity:.3;stroke-dasharray:0 12.5}++.ov-mark[data-state=busy] .o{stroke-dasharray:0 12.5 0 12.5 0 75;animation:ovmDots .96s steps(8,end) infinite}++.ov-mark[data-state=busy] .og{animation:ovmSettle .4s cubic-bezier(.2,1.3,.3,1) 1}++/* Thinking: three dots inside the O rise in turn, the ring quiets, the V's strokes glimmer one after the other. */++.ov-mark .td{display:none;fill:var(--ovm-o);transform-box:fill-box;transform-origin:center}++.ov-mark[data-state=think] .td{display:inline;animation:ovmThink 1.2s ease-in-out infinite}++.ov-mark[data-state=think] .td2{animation-delay:.16s}++.ov-mark[data-state=think] .td3{animation-delay:.32s}++.ov-mark[data-state=think] .o{stroke-opacity:.5;transition:stroke-opacity .3s}++.ov-mark[data-state=think] .va{animation:ovmSpeak 1.2s ease-in-out infinite}++.ov-mark[data-state=think] .vb{animation:ovmSpeak 1.2s ease-in-out .6s infinite}.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}@@@ -168,6 +190,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}++@keyframes ovmDots{to{stroke-dashoffset:-100}}++@keyframes ovmSettle{from{transform:scale(.86)}}++@keyframes ovmThink{0%,55%,100%{transform:translateY(0);opacity:.4}28%{transform:translateY(-2.6px);opacity:1}}++@keyframes ovmSpeak{0%,100%{opacity:1}50%{opacity:.5}}@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}@@@ -186,8 +212,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}--@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *,.ov-mark::before{transition:none!important}.ovm-fall{display:none}}--html[data-ov-motion=reduced] .ov-mark *{animation:none!important}++html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-count:1!important}html[data-ov-motion=off] .ovm-fall{display:none}.ov-busy{cursor:progress;pointer-events:none}.ov-busy-mark{display:inline-block;vertical-align:-.2em;margin-right:.45em;flex:none}`;@@@ -206,7 +231,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs><g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>--<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>++<g mask="url(#${id}h)"><g class="og" style="${oc};--ovm-dot-w:${f(G.sw * 0.62)}"><path class="ot" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>${[-1, 0, 1].map((k, n) => `<circle class="td${n ? ` td${n + 1}` : ''}" cx="${f(G.cx + k * ri * 0.52)}" cy="${G.cy}" r="${f(ri * 0.21)}"/>`).join('')}<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g><g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>@@@ -251,21 +276,29 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}if (!end || end > now + INTRO_MS) { end = now + INTRO_MS; try { sessionStorage.setItem(INTRO_KEY, String(end)); } catch { /* */ } }return end;}-- function reduced() {-- return !!(rm && rm.matches) || document.documentElement.getAttribute('data-ov-motion') === 'reduced';++ // Inside [data-fx-demo] (ov-fx.js: a motion page's own container, set when the person asks) a mark plays in full.++ // 'full' | 'calm' | 'off': <html data-ov-motion> is the signal (theme-loader.js, the device's setting counted in);++ // a page without the loader asks the device.++ function motion(el) {++ if (el && el.closest && el.closest('[data-fx-demo]')) return 'full';++ const m = document.documentElement.getAttribute('data-ov-motion');++ if (m === 'off') return 'off';++ return m === 'reduced' || (!m && rm && rm.matches) ? 'calm' : 'full';}++ const off = (el) => motion(el) === 'off';function update(el) {const st = state.get(el); if (!st) return;const svgEl = el.querySelector('svg');if (!el.isConnected || !svgEl) { forget(el); return; }const now = Date.now();-- const may = !reduced() && !el.hasAttribute('data-static') && !document.hidden && st.onScreen === true;-- if (may && !st.introEnd) { st.introEnd = introDeadline(now); if (season()) setTimeout(() => fallOnce(el), 900); }++ const may = !el.hasAttribute('data-static') && !document.hidden && st.onScreen === true;++ if (may && !off(el) && !st.introEnd) { st.introEnd = introDeadline(now); if (season()) setTimeout(() => fallOnce(el), 900); }const s = el.getAttribute('data-state');// Anything the mark is showing keeps it moving: a state, a page on its way, a known progress.const showing = (s && s !== 'idle') || el.hasAttribute('data-nav') || el.hasAttribute('data-progress');const intro = now < st.introEnd;-- const run = may && (intro || st.hover || st.focus || now < st.lingerEnd || showing);++ // Calm plays the intro and answers a hover or focus, and rests as soon as that ends (no linger).++ const run = may && (showing || (!off(el) && (intro || st.hover || st.focus || (motion(el) === 'full' && now < st.lingerEnd))));if (run !== el.hasAttribute('data-ovm-run')) { if (run) el.setAttribute('data-ovm-run', ''); else el.removeAttribute('data-ovm-run'); }// The intro's pop and growth are on only inside the intro (paused with the rest while the mark cannot move).if ((may && intro) !== el.hasAttribute('data-ovm-intro') && (may || !intro)) { if (may && intro) el.setAttribute('data-ovm-intro', ''); else el.removeAttribute('data-ovm-intro'); }@@@ -292,7 +325,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}clearTimeout(flourishTimer); flourishTimer = 0;if (!season()) return;flourishTimer = setTimeout(() => {-- if (season() && !reduced() && !document.hidden) {++ if (season() && !off() && !document.hidden) {document.querySelectorAll('.ov-mark[data-ov]:not([data-static])').forEach((el) => {const st = state.get(el); if (!st || st.onScreen === false) return;el.setAttribute('data-ovm-flourish', ''); setTimeout(() => el.removeAttribute('data-ovm-flourish'), 1700);@@@ -303,7 +336,8 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}}const FALL_KEY = 'ov-season-fall';function fallOnce(el) {-- const id = season(); if (!id || reduced() || document.hidden || !el.isConnected) return;++ const id = season(); if (!id || off() || document.hidden || !el.isConnected) return;++ const calm = motion() === 'calm';const day = id + ':' + new Date().toDateString();try { const ls = window.localStorage; if (ls.getItem(FALL_KEY) === day) return; ls.setItem(FALL_KEY, day); } catch { return; }const r = el.getBoundingClientRect(); if (!r.width || !document.body) return;@@@ -316,11 +350,11 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}const rb = bar && bar.getBoundingClientRect();const floor = rb && rb.bottom > y0 + 8 ? rb.bottom - y0 - 4 : 0;const colors = id === 'winter' ? ['#f4fbff', '#d8f3ff', '#ffffff'] : ['#ff7a1a', '#f5a524', '#d9481f', '#7fd04e'];-- for (let i = 0; i < 7; i++) {++ for (let i = 0; i < (calm ? 3 : 7); i++) {const s = document.createElement('i');-- const dx = Math.max(8 - x0, Math.min((window.innerWidth || 0) - 8 - x0, Math.round((Math.random() - 0.3) * 200)));++ const dx = Math.max(8 - x0, Math.min((window.innerWidth || 0) - 8 - x0, Math.round((Math.random() - 0.3) * (calm ? 70 : 200))));const dy = floor ? Math.round(floor - Math.random() * 3) : Math.round(36 + Math.random() * 28);-- s.style.cssText = `--x:${dx}px;--y:${dy}px;--r:${Math.round(Math.random() * 540 - 270)}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;++ s.style.cssText = `--x:${dx}px;--y:${dy}px;--r:${Math.round((Math.random() * 540 - 270) * (calm ? 0.35 : 1))}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;box.appendChild(s);}document.body.appendChild(box);@@@ -380,7 +414,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}new MutationObserver((records) => {let added = false;for (const r of records) {-- if (r.type === 'attributes') { if (r.target === document.documentElement) { updateAll(); if (r.attributeName === 'data-ov-season-mark') scheduleFlourish(); } else if (state.has(r.target)) update(r.target); continue; }++ if (r.type === 'attributes') { if (r.attributeName === 'data-fx-demo') { updateAll(); continue; } if (r.target === document.documentElement) { updateAll(); if (r.attributeName === 'data-ov-season-mark') scheduleFlourish(); } else if (state.has(r.target)) update(r.target); continue; }if (r.addedNodes.length) added = true;r.removedNodes.forEach((node) => {if (node.nodeType !== 1) return;@@@ -389,7 +423,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}});}if (added) mount();-- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-state', 'data-static', 'data-nav', 'data-progress', 'data-ov-motion', 'data-ov-season-mark'] });++ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-state', 'data-static', 'data-nav', 'data-progress', 'data-ov-motion', 'data-ov-season-mark', 'data-fx-demo'] });} catch { /* */ }}// ── A page on its way: the navbar's mark draws its ring round as the next page loads ──@@@ -457,9 +491,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}window.addEventListener('pageshow', (e) => { if (e.persisted) { navStop(); clearNav(); } });// This page came from a click on the last one: learn how long it took, and finish the ring here.let t = 0; try { t = Number(sessionStorage.getItem(NAV_FLAG)) || 0; sessionStorage.removeItem(NAV_FLAG); } catch { /* */ }++ // soft.js ends the ring itself at the arriving page's first paint (it starts at the press, not the click); the++ // pace is still learned here, from the click's marker, before soft.js's ending takes over.if (t && Date.now() - t < 20000) {learn('page', Date.now() - t);-- const finish = () => { const marks = navMarks(); if (!marks.length) return false; setFill(marks, 0.9); setTimeout(() => navEnd(null), 60); return true; };++ if (window.__ovSoftNavEnded) return;++ const finish = () => { if (window.__ovSoftNavEnded) return true; const marks = navMarks(); if (!marks.length) return false; setFill(marks, 0.9); setTimeout(() => navEnd(null), 60); return true; };let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();}}nnew file mode 100644@@@ -0,0 +1,319 @@++/**++ * ov-window.js (openvibe-shared 4.202.0): one window for every little interface. Asks 304 and 318, the owner: "a system++ * that's used for all the windows [in] harmony … people can make their own or … custom … everything … feels connected".++ * One frame (a head with the title and ×, a body, a foot) in three kinds, the same motion, the same keys:++ * dialog: centred on a computer; a sheet from the bottom on a phone++ * sheet: a sheet from the bottom on every screen++ * panel: floats beside what opened it on a computer, draggable by its head (where you leave it is kept per id);++ * a sheet on a phone++ * It grows out of the button that opened it (the navbar's pop motion), closes with ×, Escape, a press outside (a dialog++ * or a sheet; a panel stays until closed, and the page under it stays usable) or a swipe down on a phone, keeps the focus inside while open, and gives it back to that button. Windows stack: Escape closes the++ * top one. The look follows the person's theme and their window style, <html data-ov-window="glass|solid|outline">++ * (OVWindow.setStyle), on every window at once.++ *++ * const w = OVWindow.open({ id, title, body: node | text, foot: node, kind: 'dialog', size: 'md', from: button,++ * label, onClose, bare, width }); bare: the content draws its own head (the title is still++ * read); width: px, for content that wants its own++ * w.el, w.body, w.foot, w.setTitle(text), w.close()++ * A panel can be resizable (4.204.0, the owner: a robot's controls "can be popped out … and resized"): { resizable: true,++ * minWidth, minHeight, onResize({ width, height }) } adds a grip at its corner that a mouse, a finger or a pen drags and++ * the arrow keys move (Shift for bigger steps, Enter for the first size); the size is kept with its place per id.++ * A site's content goes in as nodes (or text, set as text): the window never parses markup it is given.++ */++(function (root) {++ 'use strict';++ if (typeof document === 'undefined' || root.OVWindow) return;++ const HERE = document.currentScript && document.currentScript.src;++ const WINDOW_REV = { 'ov-float.js': '5c8251ac3738' };++++ const STYLES = ['glass', 'solid', 'outline'];++ const CSS = `.ovw-scrim { position: fixed; inset: 0; z-index: var(--ovw-z, 10040); background: rgba(3, 7, 15, .5); animation: ovw-fade .2s ease-out both; }++.ovw-scrim.is-clear { background: transparent; }++.ovw { position: fixed; z-index: var(--ovw-z, 10040); box-sizing: border-box; display: flex; flex-direction: column; max-height: min(86vh, 760px); color: var(--text-primary, #e6edf7);++ font: 500 15px/1.45 var(--font, Inter, system-ui, -apple-system, 'Segoe UI', sans-serif); border-radius: 20px; overflow: hidden;++ background: var(--ovw-bg); border: 1px solid var(--ovw-line); box-shadow: var(--ovw-shadow); -webkit-backdrop-filter: var(--ovw-blur); backdrop-filter: var(--ovw-blur);++ transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ovw-in .28s cubic-bezier(.2, 1.2, .3, 1) both; }++:root { --ovw-bg: color-mix(in srgb, var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))) 92%, transparent); --ovw-line: var(--border, rgba(255, 255, 255, .12));++ --ovw-shadow: 0 28px 70px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .2); --ovw-blur: blur(16px) saturate(1.3); }++html[data-ov-window="solid"] { --ovw-bg: var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))); --ovw-blur: none; }++html[data-ov-window="outline"] { --ovw-bg: var(--bg-primary, #0b1220); --ovw-blur: none; --ovw-line: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); --ovw-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), 0 18px 40px rgba(0, 0, 0, .35); }++.ovw.is-out { animation: ovw-out .16s ease-in both; }++.ovw-h { flex: none; display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 6px 8px 6px 18px; border-bottom: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }++.ovw-t { flex: 1; min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 700; letter-spacing: -.01em; }++.ovw-x { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 12px; background: none; color: var(--text-secondary, #a8b3c4); cursor: pointer; }++.ovw-x:hover, .ovw-x:focus-visible { background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); color: var(--text-primary, #e6edf7); }++.ovw-x:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }++.ovw-b { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 16px 18px 18px; }++.ovw-f { flex: none; display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding: 12px 18px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }++.ovw-f:empty { display: none; }++.ovw.is-dialog { left: 50%; top: 50%; width: min(var(--ovw-w), calc(100vw - 32px)); transform: translate(-50%, -50%); animation-name: ovw-in-c; }++.ovw.is-dialog.is-out { animation-name: ovw-out-c; }++.ovw.is-panel { width: min(var(--ovw-w), calc(100vw - 24px)); }++.ovw.is-panel .ovw-h { cursor: grab; touch-action: none; }++.ovw.is-panel.is-dragging { animation: none; transition: none; box-shadow: 0 34px 90px rgba(0, 0, 0, .55); }++.ovw.is-panel.is-dragging .ovw-h { cursor: grabbing; }++.ovw.is-sheet { left: 0; right: 0; bottom: 0; width: auto; max-height: 92vh; margin: 0 auto; max-width: 720px; border-radius: 22px 22px 0 0; border-bottom: 0; animation: ovw-up .32s cubic-bezier(.2, 1, .3, 1) both; }++.ovw.is-sheet.is-out { animation: ovw-down .2s ease-in both; }++.ovw.is-sheet .ovw-h { position: relative; padding-top: 16px; touch-action: none; }++.ovw.is-sheet .ovw-h::before { content: ""; position: absolute; left: 50%; top: 6px; width: 38px; height: 4px; margin-left: -19px; border-radius: 4px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 28%, transparent); }++.ovw.is-sheet.is-dragging { animation: none; }++.ovw.is-resizable { max-height: none; }++.ovw-grip { position: absolute; right: 2px; bottom: 2px; z-index: 2; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 8px; background: none; cursor: nwse-resize; touch-action: none; color: var(--text-secondary, #a8b3c4); }++.ovw-grip::before { content: ""; position: absolute; right: 6px; bottom: 6px; width: 10px; height: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; border-radius: 0 0 3px 0; opacity: .55; }++.ovw-grip:hover::before, .ovw-grip:focus-visible::before, .ovw.is-resizing .ovw-grip::before { opacity: 1; color: var(--accent, #3b82f6); }++.ovw-grip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }++.ovw.is-resizing { animation: none; transition: none; user-select: none; }++.ovw { --ovw-w: 520px; }++.ovw.is-sm { --ovw-w: 380px; }++.ovw.is-lg { --ovw-w: 760px; }++.ovw.is-full { --ovw-w: 1100px; }++@keyframes ovw-in { from { opacity: 0; transform: translateY(calc(-6px * var(--ov-move, 1))) scale(.94); } }++@keyframes ovw-out { to { opacity: 0; transform: scale(.96); } }++@keyframes ovw-in-c { from { opacity: 0; transform: translate(-50%, calc(-50% + 10px * var(--ov-move, 1))) scale(.95); } }++@keyframes ovw-out-c { to { opacity: 0; transform: translate(-50%, -50%) scale(.97); } }++@keyframes ovw-up { from { transform: translateY(100%); } }++@keyframes ovw-down { to { transform: translateY(100%); } }++@keyframes ovw-fade { from { opacity: 0; } }++html[data-ov-motion=off] .ovw, html[data-ov-motion=off] .ovw-scrim { animation: none !important; }++html.ovw-locked { overflow: hidden; }++/* bare: the content carries its own head (a card's banner); the title is for screen readers, × floats on top, and++ the top strip is still the handle (drag a panel, swipe a sheet). */++.ovw.is-bare .ovw-h { position: absolute; left: 0; right: 0; top: 0; z-index: 3; min-height: 52px; padding: 6px; justify-content: flex-end; border: 0; background: none; }++.ovw.is-bare .ovw-t { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }++.ovw.is-bare .ovw-x { width: 36px; height: 36px; border-radius: 50%; color: #fff; background: rgba(4, 8, 16, .42); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }++.ovw.is-bare .ovw-x:hover, .ovw.is-bare .ovw-x:focus-visible { background: rgba(4, 8, 16, .62); color: #fff; }++.ovw.is-bare .ovw-b { padding: 0; }++.ovw.is-bare.is-sheet .ovw-h { padding-top: 14px; }`;++++ const stack = [];++ let uid = 0;++ function styles() { if (document.getElementById('ovw-styles')) return; const st = document.createElement('style'); st.id = 'ovw-styles'; st.textContent = CSS; document.head.appendChild(st); }++++ // The panel's move, tuck and place are OVFloat's (ov-float.js, loaded from beside this file the first time a panel++ // opens). A page that already loaded it uses that.++ let floatLoading = null;++ const floatWait = [];++ function withFloat(cb) {++ if (root.OVFloat) { cb(root.OVFloat); return; }++ if (!HERE || floatLoading === false) return;++ if (floatLoading) { floatWait.push(cb); return; }++ floatWait.push(cb);++ const s = document.createElement('script');++ const rev = WINDOW_REV['ov-float.js'];++ s.src = HERE.replace(/ov-window\.js(?:\?.*)?$/, 'ov-float.js') + (rev ? `?v=${rev}` : '');++ s.onload = () => {++ floatLoading = null;++ for (const fn of floatWait.splice(0)) { try { fn(root.OVFloat); } catch (e) { console.error(e); } }++ };++ s.onerror = () => { floatLoading = false; floatWait.length = 0; };++ floatLoading = s;++ (document.head || document.documentElement).appendChild(s);++ }++ const phone = () => !!(root.matchMedia && root.matchMedia('(max-width: 640px)').matches);++ const focusables = (el) => [...el.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')].filter((x) => x.offsetParent !== null || x === document.activeElement);++ const place = { get(id) { try { return JSON.parse(root.localStorage.getItem(`ov:window:${id}`) || 'null'); } catch { return null; } }, set(id, v) { try { root.localStorage.setItem(`ov:window:${id}`, JSON.stringify(v)); } catch { /* private mode */ } } };++ function put(parent, v) { if (v == null) return; if (typeof v === 'string') parent.textContent = v; else parent.appendChild(v); }++++ function open(o = {}) {++ styles();++ const kind0 = ['dialog', 'sheet', 'panel'].includes(o.kind) ? o.kind : 'dialog';++ const kind = phone() ? 'sheet' : kind0;++ const id = o.id || `ovw-${++uid}`;++ const opener = o.from || document.activeElement;++ const modal = o.modal !== false && kind !== 'panel';++ const z = 10040 + stack.length * 2;++ const scrim = document.createElement('div');++ scrim.className = `ovw-scrim${modal ? '' : ' is-clear'}`;++ scrim.style.setProperty('--ovw-z', String(z));++ const el = document.createElement('section');++ el.className = `ovw is-${kind} is-${['sm', 'md', 'lg', 'full'].includes(o.size) ? o.size : 'md'}${o.bare ? ' is-bare' : ''}`;++ el.style.setProperty('--ovw-z', String(z + 1));++ if (Number(o.width) > 0) el.style.setProperty('--ovw-w', `${Math.round(Number(o.width))}px`);++ el.setAttribute('role', 'dialog');++ if (modal) el.setAttribute('aria-modal', 'true');++ el.dataset.ovWindow = id;++ const head = document.createElement('header'); head.className = 'ovw-h';++ const title = document.createElement('h2'); title.className = 'ovw-t'; title.id = `${id}-t`; title.textContent = o.title || '';++ const x = document.createElement('button'); x.type = 'button'; x.className = 'ovw-x'; x.setAttribute('aria-label', 'Close');++ x.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';++ head.append(title, x);++ const body = document.createElement('div'); body.className = 'ovw-b'; put(body, o.body);++ const foot = document.createElement('footer'); foot.className = 'ovw-f'; put(foot, o.foot);++ el.append(head, body, foot);++ if (o.title) el.setAttribute('aria-labelledby', title.id); else el.setAttribute('aria-label', o.label || 'Window');++ if (modal) document.body.append(scrim, el); else document.body.append(el);++ if (modal) document.documentElement.classList.add('ovw-locked');++++ // Where it opens: a panel beside its opener (or where it was left), a dialog centred, a sheet at the bottom.++ let panelLeft = 0, panelTop = 0;++ if (kind === 'panel') {++ const saved = o.id ? place.get(o.id) : null;++ const cs = document.documentElement;++ const r = el.getBoundingClientRect(), vw = cs.clientWidth || root.innerWidth, vh = cs.clientHeight || root.innerHeight;++ let left, top;++ if (saved) { left = saved.x; top = saved.y; }++ else if (opener && opener.getBoundingClientRect) { const f = opener.getBoundingClientRect(); left = f.left; top = f.bottom + 8; if (top + r.height > vh - 12) top = Math.max(12, f.top - r.height - 8); }++ else { left = vw - r.width - 24; top = 72; }++ left = Math.max(8, Math.min(left, vw - r.width - 8));++ top = Math.max(8, Math.min(top, vh - 60));++ panelLeft = left; panelTop = top;++ el.style.left = `${left}px`;++ el.style.top = `${top}px`;++ }++ if (opener && opener.getBoundingClientRect && kind !== 'sheet') {++ const r = el.getBoundingClientRect(), f = opener.getBoundingClientRect();++ el.style.setProperty('--ov-pop-x', `${Math.round(f.left + f.width / 2 - r.left)}px`);++ el.style.setProperty('--ov-pop-y', `${Math.round(f.top + f.height / 2 - r.top)}px`);++ }++++ let closed = false;++ const w = {++ id, el, body, foot,++ setTitle(t) { title.textContent = t || ''; },++ close(why) {++ if (closed) return; closed = true;++ if (w.float) { try { w.float.destroy(); } catch { /* */ } w.float = null; }++ const i = stack.indexOf(w); if (i >= 0) stack.splice(i, 1);++ if (!stack.some((s) => s.modal)) document.documentElement.classList.remove('ovw-locked');++ const gone = () => { el.remove(); scrim.remove(); };++ if (document.documentElement.getAttribute('data-ov-motion') === 'off') gone();++ else { el.classList.add('is-out'); scrim.style.opacity = '0'; scrim.style.transition = 'opacity .16s'; let done = false; const end = () => { if (!done) { done = true; gone(); } }; el.addEventListener('animationend', end, { once: true }); setTimeout(end, 260); }++ if (opener && opener.focus && document.contains(opener)) { try { opener.focus({ preventScroll: true }); } catch { /* */ } }++ if (typeof o.onClose === 'function') { try { o.onClose(why || 'close'); } catch (e) { console.error(e); } }++ },++ modal,++ float: null,++ };++ stack.push(w);++ if (kind === 'panel') {++ const panelId = o.id ? String(o.id) : '';++ withFloat((OVFloat) => {++ if (closed) return;++ w.float = OVFloat.attach(el, {++ frame: null, key: panelId ? `ov:window:float:${panelId}` : undefined,++ defaults: {++ x: Math.max(0, Math.min(1, panelLeft / Math.max(1, (document.documentElement.clientWidth || root.innerWidth)))),++ y: Math.max(0, Math.min(1, panelTop / Math.max(1, (document.documentElement.clientHeight || root.innerHeight)))),++ mode: 'float',++ },++ aspect: null, size: false, resize: false, doubleTap: false, activeBar: false,++ viewport: 'client', handle: head, store: null, hintClass: 'ovw-dock-hint',++ onChange(geom) {++ if (!panelId) return;++ // The fractions convert back to the same pixels without reading a rect mid-landing. (The old++ // key stays as the absolute place older releases wrote, and for the size the grip keeps there.)++ const vw = document.documentElement.clientWidth || root.innerWidth;++ const vh = document.documentElement.clientHeight || root.innerHeight;++ place.set(panelId, Object.assign(place.get(panelId) || {}, { x: Math.round(geom.x * vw), y: Math.round(geom.y * vh) }));++ },++ });++ });++ }++ x.addEventListener('click', () => w.close('x'));++ if (modal) scrim.addEventListener('pointerdown', () => w.close('outside'));++ el.addEventListener('keydown', (e) => {++ if (e.key === 'Tab') {++ const f = focusables(el); if (!f.length) return;++ const first = f[0], last = f[f.length - 1];++ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }++ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }++ }++ });++++ // A sheet goes down when swiped down on a phone; a panel's head is OVFloat's handle (with its tuck and place).++ if (kind === 'sheet') {++ head.addEventListener('pointerdown', (e) => {++ if (e.target.closest('.ovw-x') || (e.button !== undefined && e.button !== 0)) return;++ const r = el.getBoundingClientRect(), sx = e.clientX, sy = e.clientY;++ let moved = false;++ el.classList.add('is-dragging');++ try { head.setPointerCapture(e.pointerId); } catch { /* */ }++ const move = (ev) => {++ const dx = ev.clientX - sx, dy = ev.clientY - sy;++ if (!moved && Math.hypot(dx, dy) < 4) return;++ moved = true;++ if (kind === 'panel') {++ el.style.left = `${Math.max(8, Math.min(r.left + dx, root.innerWidth - r.width - 8))}px`;++ el.style.top = `${Math.max(8, Math.min(r.top + dy, root.innerHeight - 56))}px`;++ } else el.style.transform = `translateY(${Math.max(0, dy)}px)`;++ };++ const up = (ev) => {++ head.removeEventListener('pointermove', move); head.removeEventListener('pointerup', up); head.removeEventListener('pointercancel', up);++ el.classList.remove('is-dragging');++ if (kind === 'panel') { if (moved && o.id) { const b = el.getBoundingClientRect(); place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(b.left), y: Math.round(b.top) })); } return; }++ const dy = ev.clientY - sy;++ if (moved && dy > Math.min(140, r.height * 0.28)) w.close('swipe');++ else { el.style.transition = 'transform .2s cubic-bezier(.2, 1, .3, 1)'; el.style.transform = ''; setTimeout(() => { el.style.transition = ''; }, 220); }++ };++ head.addEventListener('pointermove', move); head.addEventListener('pointerup', up); head.addEventListener('pointercancel', up);++ });++ }++++ if (kind === 'panel' && o.resizable) resizable(el, o);++++ const first = o.focus ? body.querySelector(o.focus) : focusables(body)[0];++ (first || x).focus({ preventScroll: true });++ return w;++ }++++ /** A panel's corner grip: pointer drag (mouse, finger, pen) and the arrow keys; the size is kept with its place. */++ function resizable(el, o) {++ el.classList.add('is-resizable');++ const minW = Math.max(200, Number(o.minWidth) || 260), minH = Math.max(120, Number(o.minHeight) || 160);++ const grip = document.createElement('button');++ grip.type = 'button'; grip.className = 'ovw-grip';++ grip.setAttribute('aria-label', 'Resize: drag, or use the arrow keys');++ el.append(grip);++ const first = { w: el.offsetWidth, h: el.offsetHeight };++ const set = (w, h, save) => {++ const r = el.getBoundingClientRect();++ const W = Math.round(Math.max(minW, Math.min(w, root.innerWidth - r.left - 8)));++ const H = Math.round(Math.max(minH, Math.min(h, root.innerHeight - r.top - 8)));++ el.style.width = `${W}px`; el.style.height = `${H}px`;++ if (save && o.id) place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(r.left), y: Math.round(r.top), w: W, h: H }));++ if (typeof o.onResize === 'function') { try { o.onResize({ width: W, height: H }); } catch { /* the page's own */ } }++ };++ const saved = o.id ? place.get(o.id) : null;++ if (saved && saved.w && saved.h) set(saved.w, saved.h, false);++ grip.addEventListener('pointerdown', (e) => {++ if (e.button !== undefined && e.button !== 0) return;++ e.preventDefault();++ const sx = e.clientX, sy = e.clientY, w0 = el.offsetWidth, h0 = el.offsetHeight;++ el.classList.add('is-resizing');++ try { grip.setPointerCapture(e.pointerId); } catch { /* */ }++ const move = (ev) => set(w0 + ev.clientX - sx, h0 + ev.clientY - sy, false);++ const up = () => {++ grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', up);++ el.classList.remove('is-resizing');++ set(el.offsetWidth, el.offsetHeight, true);++ };++ grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', up);++ });++ grip.addEventListener('keydown', (e) => {++ const step = e.shiftKey ? 80 : 24;++ const d = { ArrowRight: [step, 0], ArrowLeft: [-step, 0], ArrowDown: [0, step], ArrowUp: [0, -step] }[e.key];++ if (e.key === 'Enter' || e.key === 'Home') { e.preventDefault(); set(first.w, first.h, true); return; }++ if (!d) return;++ e.preventDefault();++ set(el.offsetWidth + d[0], el.offsetHeight + d[1], true);++ });++ }++++ document.addEventListener('keydown', (e) => {++ if (e.key !== 'Escape' || !stack.length || e.defaultPrevented) return;++ e.preventDefault();++ stack[stack.length - 1].close('escape');++ });++++ // The style is the person's display setting (theme-loader: saved, synced to their account, painted at boot as++ // <html data-ov-window>); setStyle saves through it, or straight onto the page where the theme loader is absent.++ function setStyle(name) {++ const v = STYLES.includes(name) ? name : 'glass';++ const T = root.OpenVibeThemeLoader;++ if (T && T.display && typeof T.display.set === 'function') { try { T.display.set({ window: v }); } catch { /* */ } }++ if (v === 'glass') document.documentElement.removeAttribute('data-ov-window'); else document.documentElement.setAttribute('data-ov-window', v);++ return v;++ }++++ root.OVWindow = { open, setStyle, styles: STYLES.slice(), top: () => stack[stack.length - 1] || null, closeAll: () => [...stack].reverse().forEach((w) => w.close('all')) };++})(typeof window !== 'undefined' ? window : globalThis);nnew file mode 100644@@@ -0,0 +1,738 @@++// OpenVibe — Theme Loader (browser): the person's look on <html> before first paint. Load it before navbar.js.++++(function (root) {++ 'use strict';++++ const COOKIE_NAME = 'ov_theme_id';++ const STORAGE_KEY = 'ov_theme';++ // The account's look lives on openvibe.network for every site (init({ apiBase }) overrides it).++ const DEFAULT_API_BASE = 'https://openvibe.network';++ let _config = { apiBase: DEFAULT_API_BASE };++++ // ── Condensed built-in theme variable maps ───────────────++ // Kept in sync with packages/openvibe-shared/builtin-themes.js++ // THEMES:BEGIN — generated by scripts/build-theme-loader.js from builtin-themes.js; do not edit by hand++ const THEME_KEYS = ['--bg-primary','--bg-secondary','--bg-tertiary','--bg-card','--bg-hover','--bg-input','--text-primary','--text-secondary','--text-muted','--accent','--accent-light','--accent-dark','--border','--border-light','--live-red','--success','--warning','--danger','--info','--shadow','--shadow-lg','--success-text','--warning-text','--danger-text','--info-text','--live-red-text','--accent-strong','--on-accent-strong','--on-accent','--accent-rgb','--accent-glow','--color-scheme'];++ const THEME_ROWS = {++ 'vibe': ['#0a0f1c','#101828','#172238','#131c2e','#1c2a44','#0d1424','#e6edf7','#96a7c2','#8192ac','#3b82f6','#60a5fa','#1d4ed8','#1f2d47','#2c3d5c','#ef4444','#22c55e','#f59e0b','#ef4444','#38bdf8','0 2px 12px rgba(0,0,0,0.45)','0 8px 32px rgba(0,0,0,0.6)','#22c55e','#f59e0b','#f26262','#38bdf8','#f26262','#3472d8','#ffffff','#ffffff','59,130,246','rgba(59,130,246,0.35)','dark'],++ 'violet-hour': ['#0d0d0f','#16161a','#1e1e24','#1a1a20','#242430','#12121a','#e8e6e3','#9a9a9a','#8d8c8c','#8b5cf6','#a78bfa','#6d28d9','#2a2a32','#3a3a44','#e74c3c','#2ecc71','#f39c12','#e74c3c','#3498db','0 2px 12px rgba(0,0,0,0.4)','','#2ecc71','#f39c12','#e95e50','#3498db','#e95e50','#8558ec','','','139,92,246','rgba(139,92,246,0.35)'],++ 'midnight': ['#0a0e17','#111827','#1a2235','#151d2e','#1e2a42','#0d1220','#e2e8f0','#94a3b8','#8693a5','#6366f1','#818cf8','#4f46e5','#1e293b','#334155','','','','','','','','','','','','','#6164ec','','','99,102,241','rgba(99,102,241,0.35)'],++ 'forest': ['#0a120a','#121e12','#1a2a1a','#152015','#1e3420','#0d160d','#d4e8d4','#8aaa8a','#849e84','#4ade80','#86efac','#22c55e','#1a3a1a','#2a4a2a','','','','','','','','','','#f36d6d','','#f36d6d','#4ade80','#0b0d10','#0b0d10','74,222,128','rgba(74,222,128,0.35)'],++ 'neon-tokyo': ['#0a0a0f','#12101a','#1a1625','#15111f','#221a32','#0d0b14','#f0e6ff','#b4a0d0','#8e81a2','#ff006e','#ff4d94','#c9005a','#2a1a3a','#3a2a4a','','','','','','','','','','#ef4848','','#ef4848','#e60063','','','255,0,110','rgba(255,0,110,0.35)'],++ 'dracula': ['#282a36','#1e1f29','#343746','#2c2e3c','#3a3d50','#21222c','#f8f8f2','#bfbfb2','#9ea8c5','#bd93f9','#d4b8ff','#9b6ddb','#44475a','#565970','#ff5555','#50fa7b','#f1fa8c','#ff5555','#8be9fd','','','#50fa7b','#f1fa8c','#ff8585','#8be9fd','#ff8585','#bd93f9','#0b0d10','#0b0d10','189,147,249','rgba(189,147,249,0.35)'],++ 'monokai': ['#272822','#1e1f1c','#3e3d32','#2d2e27','#49483e','#22231e','#f8f8f2','#cfcfc2','#b8b6ab','#a6e22e','#c4ff50','#7dac1c','#49483e','#5b5a50','','','#e6db74','#f92672','#66d9ef','','','#45ce78','#e6db74','#fc97bb','#66d9ef','#f79e9e','#a6e22e','#0b0d10','#0b0d10','166,226,46','rgba(166,226,46,0.35)'],++ 'ocean-deep': ['#0a1215','#0f1a1f','#162228','#121c22','#1a2a32','#0c1418','#d0e8ef','#7aa8ba','#7494a1','#00bcd4','#4dd0e1','#0097a7','#1a3040','#2a4050','','','','','','','','','','#f15e5e','','#f15e5e','#00bcd4','#0b0d10','#0b0d10','0,188,212','rgba(0,188,212,0.35)'],++ 'sunset': ['#120a0a','#1a1010','#241616','#1e1212','#2e1a1a','#140c0c','#ffe8e0','#c49080','#9d8179','#ff6b35','#ff8f5e','#d04a1a','#3a2020','#4a3030','','','','','','','','','','#f04b4b','','#f04b4b','#ff6b35','#0b0d10','#0b0d10','255,107,53','rgba(255,107,53,0.35)'],++ 'arctic': ['#0b1620','#0f1e2b','#163040','#122534','#1a3a4c','#0c1a26','#eaf6ff','#9ec3d8','#81a5bb','#22d3ee','#a5f3fc','#0891b2','#1e3f52','#2a5268','','#34d399','','','#7dd3fc','0 2px 12px rgba(2,12,20,0.5)','0 8px 32px rgba(2,12,20,0.65)','#34d399','','#f47c7c','#7dd3fc','#f47c7c','#22d3ee','#0b0d10','#0b0d10','34,211,238','rgba(34,211,238,0.35)'],++ 'ember': ['#120808','#1a0e0e','#241414','#1e1010','#2e1818','#140a0a','#f0dada','#b0808a','#9b8084','#ef4444','#f87171','#b91c1c','#3a1a20','#4a2a30','','','','','','','','','','#ef4848','','#ef4848','#d73d3d','','','239,68,68','rgba(239,68,68,0.35)'],++ 'vapor': ['#0e0a14','#14101c','#1c1628','#181220','#241a34','#100c16','#f0e0ff','#c090e0','#967bbb','#e040fb','#ea80fc','#aa00d4','#2a1a40','#3a2a50','','#69f0ae','','','#00e5ff','','','#69f0ae','','#f04b4b','#00e5ff','#f04b4b','#bc36d3','','','224,64,251','rgba(224,64,251,0.35)'],++ 'slate': ['#0f1118','#161820','#1e2028','#1a1c24','#24262e','#12141c','#e4e6ea','#a0a4b0','#8a8d97','#a78bfa','#c4b5fd','#7c3aed','#2a2c38','#3a3c48','','','','','','','','','','#f15a5a','','#f15a5a','#a78bfa','#0b0d10','#0b0d10','167,139,250','rgba(167,139,250,0.35)'],++ 'matrix': ['#000000','#050a05','#0a140a','#071007','#0e1e0e','#030803','#00ff41','#00b030','#149836','#00ff41','#66ff8a','#00aa2a','#0a2a0a','#144014','','','','','','','','','','#ef4444','','#ef4444','#00ff41','#0b0d10','#0b0d10','0,255,65','rgba(0,255,65,0.35)'],++ 'nord': ['#2e3440','#272c36','#3b4252','#333a48','#434c5e','#2a303c','#eceff4','#d8dee9','#b5bcca','#88c0d0','#8fbcbb','#5e81ac','#3b4252','#4c566a','','#a3be8c','#ebcb8b','#bf616a','#81a1c1','','','#acc598','#ebcb8b','#dfb0b5','#a9bfd5','#f8a9a9','#88c0d0','#0b0d10','#0b0d10','136,192,208','rgba(136,192,208,0.35)'],++ 'gruvbox-dark': ['#1d2021','#282828','#3c3836','#32302f','#504945','#242424','#ebdbb2','#d5c4a1','#c2b8aa','#fabd2f','#fce566','#d79921','#3c3836','#504945','','#b8bb26','#fe8019','#fb4934','#83a598','','','#bec037','#fea65e','#fda49a','#a8c0b7','#f7a5a5','#fabd2f','#0b0d10','#0b0d10','250,189,47','rgba(250,189,47,0.35)'],++ 'abyss': ['#000000','#080808','#111111','#0a0a0a','#181818','#050505','#cccccc','#888888','#828282','#ffffff','#ffffff','#aaaaaa','#1a1a1a','#252525','','','','','','','','','','#ef4444','','#ef4444','#ffffff','#0b0d10','#0b0d10','255,255,255','rgba(255,255,255,0.35)'],++ 'copper': ['#110d09','#1a140e','#231c14','#1e1710','#2c2218','#13100b','#e8ddd0','#b49a80','#978976','#cd7f32','#daa060','#a06020','#2a2016','#3a3020','','','','','','','','','','#f15757','','#f15757','#a46628','','','205,127,50','rgba(205,127,50,0.35)'],++ 'sakura-night': ['#0e0a14','#150f1c','#1c1526','#181020','#221830','#100c16','#f0e0f0','#c0a0c0','#997d99','#f472b6','#f9a8d4','#db2777','#2a1a30','#3a2a40','','','','','','','','','','#ef4848','','#ef4848','#f472b6','#0b0d10','#0b0d10','244,114,182','rgba(244,114,182,0.35)'],++ 'hacker': ['#000000','#0a0800','#141000','#0e0c00','#1c1800','#060400','#ffb000','#cc8d00','#a97a0f','#ffb000','#ffd050','#cc8d00','#1a1400','#2a2200','','','','','','','','','','#ef4444','','#ef4444','#ffb000','#0b0d10','#0b0d10','255,176,0','rgba(255,176,0,0.35)'],++ 'daylight': ['#ffffff','#f7f7f8','#eeeef0','#ffffff','#f0f0f2','#f5f5f7','#1a1a2e','#4a4a6a','#68687b','#7a4ddb','#725faa','#6d28d9','#e0e0e6','#d0d0d8','','','','','','0 2px 12px rgba(0,0,0,0.08)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#c43838','#227195','#c43838','#7a4ddb','','','122,77,219','rgba(122,77,219,0.35)','light'],++ 'paper': ['#faf8f5','#f0ece6','#e8e2d8','#faf8f5','#f0ece6','#f5f2ed','#2c2416','#5a4e3e','#6c6355','#7d5f12','#7b6122','#6a5010','#ddd6c8','#ccc4b0','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7d5f12','','','125,95,18','rgba(125,95,18,0.35)','light'],++ 'cloud': ['#f0f5ff','#e8eef8','#dde5f0','#f5f8ff','#e4ecf8','#edf2fa','#1a2040','#4a5070','#5e637b','#245dd5','#3c669b','#2563eb','#d0d8ea','#bcc8e0','','','','','','0 2px 12px rgba(0,20,60,0.06)','0 8px 32px rgba(0,20,60,0.10)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#245dd5','','','36,93,213','rgba(36,93,213,0.35)','light'],++ 'meadow': ['#f2f8f0','#e8f0e4','#dce8d6','#f5faf2','#e4f0de','#edf5ea','#1a2e16','#3a5a32','#526b4a','#137538','#147638','#15803d','#c8dcc0','#b0ccaa','','','','','','0 2px 12px rgba(0,40,0,0.06)','0 8px 32px rgba(0,40,0,0.10)','#147638','#895806','#ba3535','#1f6a8b','#ba3535','#137538','','','19,117,56','rgba(19,117,56,0.35)','light'],++ 'peach': ['#fff5f0','#fbeae2','#f5ddd2','#fff7f2','#fce8dc','#fdf0ea','#2e1a14','#6a4438','#7e5d51','#ad4107','#925523','#ea580c','#f0d0c0','#e4bfab','','','','','','0 2px 12px rgba(60,20,0,0.06)','0 8px 32px rgba(60,20,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#ad4107','','','173,65,7','rgba(173,65,7,0.35)','light'],++ 'lavender': ['#f8f5ff','#f0eaf8','#e6ddf0','#faf7ff','#eee6f8','#f4f0fa','#1e1430','#4a3a60','#6a5d80','#7443d5','#68569b','#7c3aed','#ddd0ee','#ccc0dd','','','','','','0 2px 12px rgba(40,0,60,0.06)','0 8px 32px rgba(40,0,60,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7443d5','','','116,67,213','rgba(116,67,213,0.35)','light'],++ 'gruvbox-light': ['#fbf1c7','#f2e5bc','#ebdbb2','#fbf1c7','#f2e5bc','#f5ecc4','#3c3836','#504945','#685e54','#a73e06','#934a0e','#af3a03','#d5c4a1','#bdae93','','#98971a','#d79921','#cc241d','#458588','0 2px 12px rgba(40,30,0,0.08)','0 8px 32px rgba(40,30,0,0.12)','#646411','#7d5913','#bc211b','#36686a','#b13232','#a73e06','','','167,62,6','rgba(167,62,6,0.35)','light'],++ 'snow': ['#ffffff','#f8fafc','#f1f5f9','#ffffff','#f1f5f9','#f8fafc','#0f172a','#334155','#657185','#0275b0','#23759a','#0284c7','#e2e8f0','#cbd5e1','','','','','','0 2px 12px rgba(0,0,0,0.05)','0 8px 32px rgba(0,0,0,0.08)','#167e3c','#986207','#c93939','#23759a','#c93939','#0275b0','','','2,117,176','rgba(2,117,176,0.35)','light'],++ 'sand': ['#f8f4ef','#f0e8de','#e8ddd0','#faf6f0','#f0e8de','#f5f0e8','#2e2418','#5a4e3e','#6b6154','#7e5d06','#7a5c12','#8b6508','#ddd0be','#ccc0aa','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#136e35','#895806','#b13232','#1e6686','#b13232','#7e5d06','','','126,93,6','rgba(126,93,6,0.35)','light'],++ 'rose': ['#fff5f7','#fce7ec','#f5d5de','#fff7f9','#fce0e8','#fdeef2','#2e1420','#6a3850','#81536c','#bf183d','#b02d44','#be123c','#f0c8d4','#e4b0c0','','','','','','0 2px 12px rgba(60,0,20,0.06)','0 8px 32px rgba(60,0,20,0.10)','#136e35','#845506','#b13232','#1e6686','#b13232','#bf183d','','','191,24,61','rgba(191,24,61,0.35)','light'],++ 'solarized-dark': ['#002b36','#073642','#0a3f4e','#053542','#0d4a58','#01303d','#fdf6e3','#a6b2b2','#a8b2b0','#b58900','#d4a017','#8a6a00','#0a4050','#105060','','#859900','#cb4b16','#dc322f','#268bd2','','','#aab84d','#e4a186','#ee9997','#78b7e3','#f69696','#947000','','','181,137,0','rgba(181,137,0,0.35)'],++ 'solarized-light': ['#fdf6e3','#eee8d5','#e4ddc6','#fdf6e3','#eee8d5','#f5eedb','#002b36','#51656c','#4e6467','#795d00','#7b5d0d','#8a6a00','#d6ceb5','#c8c0a5','','#859900','#cb4b16','#dc322f','#268bd2','0 2px 12px rgba(0,30,40,0.08)','0 8px 32px rgba(0,30,40,0.12)','#5a6800','#a63e12','#b92a27','#1b6497','#b13232','#795d00','','','121,93,0','rgba(121,93,0,0.35)','light'],++ 'catppuccin-mocha': ['#1e1e2e','#181825','#313244','#1e1e2e','#313244','#1a1a2c','#cdd6f4','#bac2de','#989caf','#cba6f7','#dcc0ff','#b07de0','#313244','#45475a','#f38ba8','#a6e3a1','#fab387','#f38ba8','#89b4fa','','','#a6e3a1','#fab387','#f38ba8','#89b4fa','#f38ba8','#cba6f7','#0b0d10','#0b0d10','203,166,247','rgba(203,166,247,0.35)'],++ 'catppuccin-latte': ['#eff1f5','#e6e9ef','#dce0e8','#eff1f5','#dce0e8','#e8ebf0','#4c4f69','#5c5f77','#5f6176','#7d34dc','#7a46c2','#7028d4','#ccd0da','#bcc0cc','#d20f39','#40a02b','#fe640b','#d20f39','#1e66f5','0 2px 12px rgba(0,0,0,0.06)','0 8px 32px rgba(0,0,0,0.10)','#2d701e','#a84207','#c50e36','#1a5ad8','#c50e36','#7d34dc','','','125,52,220','rgba(125,52,220,0.35)','light'],++ 'high-contrast': ['#000000','#0a0a0a','#1a1a1a','#0f0f0f','#222222','#0a0a0a','#ffffff','#f0f0f0','#aaaaaa','#ffd700','#ffed4a','#ccac00','#444444','#666666','#ff0000','#00ff00','#ffff00','#ff0000','#00ffff','','','#00ff00','#ffff00','#ff3d3d','#00ffff','#ff3d3d','#ffd700','#0b0d10','#0b0d10','255,215,0','rgba(255,215,0,0.35)'],++ 'halloween': ['','#0f1626','#151e33','#111a2b','#1a2540','#0c1322','#eef1f6','#a9b4c8','#8c99b0','#ff7a1a','#ffa14f','#d65d00','#1c2840','#2a3858','','#4ade80','#fbbf24','#f87171','#60a5fa','0 2px 12px rgba(0,4,14,0.5)','0 8px 32px rgba(0,4,14,0.65)','#4ade80','#fbbf24','#f87171','#60a5fa','#f15757','#ff7a1a','#0b0d10','#0b0d10','255,122,26','rgba(255,122,26,0.35)'],++ 'frost': ['#08111b','#0d1926','#132334','#0f1c2b','#18293d','#0a1520','#eaf4fb','#a3b9cc','#7f97ad','#2dd4bf','#7ee8da','#0f9e8c','#1b2d41','#294057','','#4ade80','#fbbf24','#f87171','#93c5fd','0 2px 12px rgba(0,6,14,0.5)','0 8px 32px rgba(0,6,14,0.65)','#4ade80','#fbbf24','#f87171','#93c5fd','#f15e5e','#2dd4bf','#0b0d10','#0b0d10','45,212,191','rgba(45,212,191,0.35)'],++ 'dawn': ['#f5f6fc','#eceefa','#e1e4f5','#fafbff','#e6e9f8','#f0f2fb','#161a33','#474d6b','#5e647f','#4f46e5','#5558cf','#3730a3','#d6daef','#c3c8e6','','','','','','0 2px 12px rgba(10,14,40,0.07)','0 8px 32px rgba(10,14,40,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#4f46e5','','','79,70,229','rgba(79,70,229,0.35)','light'],++ 'tokyo-day': ['#fff6fa','#ffedf4','#fde1ec','#fffafc','#fde7f0','#fff0f6','#24101a','#5c3a4a','#7a5566','#cd0058','#c22261','#9c0043','#f3d3e0','#ebbfd1','','','','','','0 2px 12px rgba(40,0,16,0.07)','0 8px 32px rgba(40,0,16,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#cd0058','','','205,0,88','rgba(205,0,88,0.35)','light'],++ 'alucard': ['#fffbeb','#f6f0d8','#ece4c6','#fffdf3','#f2ead0','#fbf6e2','#1f1f1f','#4f4b38','#6c664b','#644ac9','#6b53bc','#4a33a3','#e3dab8','#d6cba3','','','','','','0 2px 12px rgba(40,30,0,0.07)','0 8px 32px rgba(40,30,0,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#644ac9','','','100,74,201','rgba(100,74,201,0.35)','light'],++ 'monokai-light': ['#fafaf6','#f1f1ea','#e7e7de','#fdfdfa','#ecece3','#f6f6f0','#272822','#4e4f44','#67685c','#4b7100','#537112','#436600','#dcdcd0','#cdcdbf','','','','','','0 2px 12px rgba(20,20,10,0.07)','0 8px 32px rgba(20,20,10,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#4b7100','','','75,113,0','rgba(75,113,0,0.35)','light'],++ 'shallows': ['#f0fafb','#e4f4f6','#d5ecef','#f7fcfd','#dcf0f2','#ebf7f8','#0c2226','#3a5458','#516b70','#007384','#00737f','#005662','#c9e3e7','#b4d6db','','','','','','0 2px 12px rgba(0,30,36,0.07)','0 8px 32px rgba(0,30,36,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#007384','','','0,115,132','rgba(0,115,132,0.35)','light'],++ 'ash': ['#faf7f6','#f3eeec','#eae2df','#fdfbfa','#efe7e4','#f7f3f1','#231716','#54403d','#725b57','#c22727','#bc2f2b','#8e1b1b','#e2d6d2','#d4c4bf','','','','','','0 2px 12px rgba(40,10,8,0.07)','0 8px 32px rgba(40,10,8,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#c22727','','','194,39,39','rgba(194,39,39,0.35)','light'],++ 'vapor-day': ['#fcf6ff','#f6ecfc','#eedff8','#fefaff','#f2e5fa','#f9f1fe','#22122c','#533f5f','#715c7e','#a21caf','#a921ba','#701a75','#e6d3f1','#dbc0ea','','','','','','0 2px 12px rgba(30,0,40,0.07)','0 8px 32px rgba(30,0,40,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#a21caf','','','162,28,175','rgba(162,28,175,0.35)','light'],++ 'greenbar': ['#f4faf3','#e9f4e7','#dcecd9','#f9fdf8','#e2f0df','#eff7ed','#0d1f10','#3b5240','#556e5a','#13782b','#18772c','#0b5a1f','#cfe3cb','#bcd7b7','','','','','','0 2px 12px rgba(0,30,6,0.07)','0 8px 32px rgba(0,30,6,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#13782b','','','19,120,43','rgba(19,120,43,0.35)','light'],++ 'nord-light': ['#eceff4','#e5e9f0','#d8dee9','#f2f4f8','#dfe4ec','#e8ecf2','#2e3440','#434c5e','#4c566a','#476283','#506478','#4c6a91','#d0d7e2','#c2cad8','','','','','','0 2px 12px rgba(20,26,40,0.07)','0 8px 32px rgba(20,26,40,0.12)','#136e35','#845506','#b13232','#1e6686','#b13232','#476283','','','71,98,131','rgba(71,98,131,0.35)','light'],++ 'bright': ['#ffffff','#f6f6f6','#ececec','#ffffff','#f0f0f0','#fafafa','#0a0a0a','#3d3d3d','#5c5c5c','#111111','#333333','#000000','#e2e2e2','#d0d0d0','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#bf3636','#227195','#bf3636','#111111','','','17,17,17','rgba(17,17,17,0.35)','light'],++ 'high-contrast-light': ['#ffffff','#ffffff','#f2f2f2','#ffffff','#e8e8e8','#ffffff','#000000','#1a1a1a','#2b2b2b','#0000c8','#1a1aff','#00008b','#000000','#333333','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#0000c8','','','0,0,200','rgba(0,0,200,0.35)','light'],++ 'hayride': ['#fbf6ee','#f6eee2','#efe3d1','#ffffff','#f3e9da','#fffaf2','#23180f','#5a4838','#6b5a4a','#b63d0b','#ad4109','#9a3412','#eadfcd','#e0cfb6','','','','','','0 2px 12px rgba(40,24,8,0.07)','0 8px 32px rgba(40,24,8,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#b63d0b','','','182,61,11','rgba(182,61,11,0.35)','light'],++ 'snowfall': ['#f2f8fb','#e8f2f7','#dbeaf1','#f8fbfd','#e1eef4','#edf5f9','#0e1d26','#3b5260','#526976','#0f746c','#0c7267','#115e59','#cfe2ea','#bcd5df','','','','','','0 2px 12px rgba(0,20,30,0.07)','0 8px 32px rgba(0,20,30,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#0f746c','','','15,116,108','rgba(15,116,108,0.35)','light'],++ };++ const THEMES = (function () {++ const out = {}, base = THEME_ROWS.vibe;++ for (const id in THEME_ROWS) {++ const row = THEME_ROWS[id], vars = {};++ for (let i = 0; i < THEME_KEYS.length; i++) { const v = row[i] || base[i]; if (v) vars[THEME_KEYS[i]] = v; }++ out[id] = vars;++ }++ return out;++ })();++ const SEASONS = [++ { id: 'halloween', name: 'Halloween', theme: 'halloween', light: 'hayride', from: '10-01', to: '10-31' },++ { id: 'winter', name: 'Winter', theme: 'frost', light: 'snowfall', from: '12-01', to: '12-31' },++ ];++ const PAIRS = { 'vibe':'cloud','violet-hour':'daylight','midnight':'dawn','forest':'meadow','neon-tokyo':'tokyo-day','dracula':'alucard','monokai':'monokai-light','ocean-deep':'shallows','sunset':'peach','arctic':'snow','ember':'ash','vapor':'vapor-day','slate':'lavender','matrix':'greenbar','nord':'nord-light','gruvbox-dark':'gruvbox-light','abyss':'bright','copper':'sand','sakura-night':'rose','hacker':'paper','daylight':'violet-hour','paper':'hacker','cloud':'vibe','meadow':'forest','peach':'sunset','lavender':'slate','gruvbox-light':'gruvbox-dark','snow':'arctic','sand':'copper','rose':'sakura-night','solarized-dark':'solarized-light','solarized-light':'solarized-dark','catppuccin-mocha':'catppuccin-latte','catppuccin-latte':'catppuccin-mocha','high-contrast':'high-contrast-light','halloween':'hayride','frost':'snowfall','dawn':'midnight','tokyo-day':'neon-tokyo','alucard':'dracula','monokai-light':'monokai','shallows':'ocean-deep','ash':'ember','vapor-day':'vapor','greenbar':'matrix','nord-light':'nord','bright':'abyss','high-contrast-light':'high-contrast','hayride':'halloween','snowfall':'frost' };++ const MODES = { 'vibe':'dark','violet-hour':'dark','midnight':'dark','forest':'dark','neon-tokyo':'dark','dracula':'dark','monokai':'dark','ocean-deep':'dark','sunset':'dark','arctic':'dark','ember':'dark','vapor':'dark','slate':'dark','matrix':'dark','nord':'dark','gruvbox-dark':'dark','abyss':'dark','copper':'dark','sakura-night':'dark','hacker':'dark','daylight':'light','paper':'light','cloud':'light','meadow':'light','peach':'light','lavender':'light','gruvbox-light':'light','snow':'light','sand':'light','rose':'light','solarized-dark':'dark','solarized-light':'light','catppuccin-mocha':'dark','catppuccin-latte':'light','high-contrast':'dark','halloween':'dark','frost':'dark','dawn':'light','tokyo-day':'light','alucard':'light','monokai-light':'light','shallows':'light','ash':'light','vapor-day':'light','greenbar':'light','nord-light':'light','bright':'light','high-contrast-light':'light','hayride':'light','snowfall':'light' };++ // THEMES:END++++ // ── Seasons: in its dates a season's theme stands in for the default theme unless display `seasonal` is off (a picked++ // theme stays). <html data-ov-season>, window 'ov:season'; the mark wears it on any theme (data-ov-season-mark).++ const DEFAULT_ID = 'vibe';++ function seasonNow(date) {++ const d = date || new Date();++ const md = String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');++ for (let i = 0; i < SEASONS.length; i++) {++ const s = SEASONS[i];++ if (s.from <= s.to ? (md >= s.from && md <= s.to) : (md >= s.from || md <= s.to)) return s;++ }++ return null;++ }++ let _season = null;++ let _shown = { id: DEFAULT_ID, vars: null, custom: false };++ let _trying = null; // { id, name, vars } while a theme is tried on this tab (?ov-try=, 4.41.0)++ /** Paint the theme `themeId` (its variables `vars`), or the season's theme in its place. → the id painted. */++ // ── Light, dark and Auto: every built-in has two halves (PAIRS); display `scheme` 'auto' paints the half the device++ // asks for (a theme without one gives way to the default's half), 'manual' the picked theme as it is.++ let _mq = null;++ function deviceMode() {++ try { _mq = _mq || window.matchMedia('(prefers-color-scheme: dark)'); return _mq.matches ? 'dark' : 'light'; } catch (e) { return 'dark'; }++ }++ function modeOf(id, vars) { return MODES[id] || ((vars && vars['--color-scheme']) === 'light' ? 'light' : 'dark'); }++ function otherHalf(id, mode) { return PAIRS[id] && MODES[PAIRS[id]] === mode ? PAIRS[id] : (mode === 'light' ? PAIRS[DEFAULT_ID] : DEFAULT_ID); }++ function halfFor(themeId, vars, custom) {++ if (readDisplay().scheme !== 'auto') return { id: themeId, vars: vars, custom: custom };++ const want = deviceMode();++ if (modeOf(themeId, vars) === want) return { id: themeId, vars: vars, custom: custom };++ const other = otherHalf(themeId, want);++ return { id: other, vars: THEMES[other], custom: false };++ }++ // The default theme in either mode: Vibe, and its light half. A season stands in for both, in the same mode.++ function isDefault(id) { return id === DEFAULT_ID || id === PAIRS[DEFAULT_ID]; }++++ function show(themeId, vars, custom) {++ _shown = { id: themeId, vars: vars, custom: !!custom };++ // A theme being tried (?ov-try=) wins every repaint; _shown keeps the real one for Stop.++ if (_trying && _trying.vars) return paintTry();++ const half = halfFor(themeId, vars, !!custom);++ const s = isDefault(half.id) && !half.custom && readDisplay().seasonal !== 'off' ? seasonNow() : null;++ const seasonTheme = s ? (modeOf(half.id) === 'light' && s.light ? s.light : s.theme) : null;++ const seasonal = seasonTheme && THEMES[seasonTheme] ? s : null;++ const painted = seasonal ? seasonTheme : half.id;++ applyVars(seasonal ? THEMES[seasonTheme] : half.vars);++ const changed = (_season && _season.id) !== (seasonal && seasonal.id);++ _season = seasonal;++ try {++ const el = document.documentElement;++ if (seasonal) el.setAttribute('data-ov-season', seasonal.id); else el.removeAttribute('data-ov-season');++ const marked = readDisplay().seasonal !== 'off' ? seasonNow() : null;++ if (marked) el.setAttribute('data-ov-season-mark', marked.id); else el.removeAttribute('data-ov-season-mark');++ el.setAttribute('data-theme', painted);++ } catch { /* no DOM */ }++ if (changed) { try { window.dispatchEvent(new CustomEvent('ov:season', { detail: seasonal })); } catch { /* */ } }++ return painted;++ }++++ /** The Light / Dark / Auto switch's state: 'auto', else the mode of the theme on screen. */++ function getScheme() { return readDisplay().scheme === 'auto' ? 'auto' : modeOf(_shown.id, _shown.vars); }++ /** Light or Dark saves the picked theme's other half (Auto off); Auto follows the device. → the new state. */++ function setScheme(mode) {++ if (mode === 'auto') { setDisplay({ scheme: 'auto' }); return 'auto'; }++ if (mode !== 'light' && mode !== 'dark') return getScheme();++ const cur = _shown;++ if (readDisplay().scheme === 'auto') setDisplay({ scheme: 'manual' }, { noRepaint: true });++ if (modeOf(cur.id, cur.vars) !== mode) save(otherHalf(cur.id, mode), null, { keepSeasonal: true });++ else show(cur.id, cur.vars || THEMES[cur.id] || THEMES[DEFAULT_ID], cur.custom);++ return mode;++ }++++ function getCookie(name) {++ const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));++ return match ? decodeURIComponent(match[1]) : null;++ }++++ function setCookie(name, value, days) {++ const maxAge = days * 24 * 60 * 60;++ const encoded = encodeURIComponent(value);++ const host = (typeof location !== 'undefined' && location.hostname) || '';++ // Domain=.openvibe.tools only on openvibe.tools and its subdomains (CONTRACTS.md); host-only everywhere else.++ if (host && (host.endsWith('.openvibe.tools') || host === 'openvibe.tools')) {++ document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};domain=.openvibe.tools;SameSite=Lax;Secure`;++ } else {++ const secure = (typeof location !== 'undefined' && location.protocol === 'https:') ? ';Secure' : '';++ document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};SameSite=Lax${secure}`;++ }++ }++++ /**++ * Apply a theme's CSS variables to document.documentElement.++ * If themeId is not found in the built-in map, does nothing.++ */++ function applyById(themeId) {++ const vars = THEMES[themeId];++ if (!vars) return false;++ applyVars(vars);++ return true;++ }++++ // Every token any built-in sets: applying a theme clears the ones it does not set (no inherited --success).++ const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();++ /** Readable text on a colour: near-black or white, whichever contrasts more (WCAG relative luminance). */++ function onColor(c) {++ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());++ if (!m) return null;++ const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];++ const lin = (i) => { const v = parseInt(h.slice(i, i + 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };++ const L = 0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4);++ return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? '#0b0d10' : '#ffffff';++ }++ function applyVars(vars) {++ if (!vars || typeof vars !== 'object') return;++ // A theme without its own text-on-accent colours (custom and older server themes) gets readable ones derived.++ if (vars['--accent'] && !vars['--on-accent']) {++ const on = onColor(vars['--accent']);++ if (on) vars = Object.assign({}, vars, { '--on-accent': on, '--on-accent-strong': vars['--on-accent-strong'] || onColor(vars['--accent-strong'] || vars['--accent']) || on });++ }++ const el = document.documentElement;++ for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);++ for (const [prop, val] of Object.entries(vars)) {++ if (prop.startsWith('--')) el.style.setProperty(prop, val);++ }++ // Native controls, scrollbars and form fields follow the theme's light/dark mode.++ const scheme = vars['--color-scheme'];++ if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }++ // The canvas follows the variables (value as fallback), so a page that sets --bg-primary later repaints it too.++ if (vars['--bg-primary']) el.style.background = `var(--bg-primary, ${vars['--bg-primary']})`;++ if (vars['--text-primary']) el.style.color = `var(--text-primary, ${vars['--text-primary']})`;++ syncBrowserUi();++ }++++ // ── A correction fades in ─────────────────────────────────++ // A theme that changes under someone (the account's answer, another tab) crossfades for 0.4 s instead of jumping;++ // first paint and their own picks stay instant. A colour fade moves nothing, so Calm keeps it; only Off stops it.++ let _fadeTimer = 0;++ function fadeNext() {++ if (typeof document === 'undefined' || !document.head || motionOf() === 'off') return;++ const el = document.documentElement;++ if (!document.getElementById('ov-theme-fade-css')) {++ const st = document.createElement('style'); st.id = 'ov-theme-fade-css';++ st.textContent = 'html.ov-theme-fade,html.ov-theme-fade *,html.ov-theme-fade *::before,html.ov-theme-fade *::after{transition:background-color .4s ease,color .4s ease,border-color .4s ease,fill .4s ease,stroke .4s ease!important}';++ document.head.appendChild(st);++ }++ el.classList.add('ov-theme-fade');++ clearTimeout(_fadeTimer); _fadeTimer = setTimeout(function () { el.classList.remove('ov-theme-fade'); }, 600);++ }++++ // ── The browser's UI follows the theme on every apply: theme-color, the favicon (<link rel="icon" data-ov-icon>),++ // and window 'ov:theme' { accent, accentLight, bg, mode }.++ // FAVICON:BEGIN — generated by scripts/build-theme-loader.js from mark.js and app-icon.js; do not edit by hand++ const FAV_DEFS = '<mask id="ovh" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><path d="M70.59 68.5L53.31 31.5M70.59 68.5L88.41 31.5" fill="none" stroke="#000" stroke-width="31" stroke-linecap="round"/></mask><linearGradient id="ovgo" gradientUnits="userSpaceOnUse" x1="3.09" y1="23" x2="57.09" y2="77"><stop offset="0" stop-color="#87a6ff"/><stop offset="1" stop-color="#4268d6"/></linearGradient><linearGradient id="ovgv" gradientUnits="userSpaceOnUse" x1="0" y1="31.5" x2="0" y2="68.5"><stop offset="0" stop-color="#ae8df9"/><stop offset="1" stop-color="#784fd4"/></linearGradient>';++ const FAV_BODY = '<circle cx="30.09" cy="50" r="18" fill="none" stroke="url(#ovgo)" stroke-width="18" mask="url(#ovh)"/><path d="M70.59 68.5L53.31 31.5M70.59 68.5L88.41 31.5" fill="none" stroke="url(#ovgv)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>';++ const FAV_LIGHT = { cx: 30.09, cy: 50, r: 5.58 };++ const FAV_SITES = { network: ['#60a5fa', ''], live: ['#60a5fa', '#ef4444'], tools: ['#38bdf8', ''], community: ['#34d399', ''], games: ['#a78bfa', '#facc15'], media: ['#fbbf24', ''], space: ['#818cf8', ''], ai: ['#22d3ee', ''] };++ // FAVICON:END++ const COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|hsla?\([\d\s.,%/deg]+\))$/i;++ function readColor(name, fallback) {++ try { const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return COLOR_RE.test(v) ? v : fallback; } catch { return fallback; }++ }++ /** The tab icon: app-icon.js's favicon() (the Pair, heavy form, the site's glow and light) on the theme's ground. */++ function faviconSvg(site, bg) {++ const s = FAV_SITES[site] || FAV_SITES.network, a = s[0];++ const light = s[1] ? '<circle cx="' + FAV_LIGHT.cx + '" cy="' + FAV_LIGHT.cy + '" r="' + FAV_LIGHT.r + '" fill="' + s[1] + '"/>' : '';++ return 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -32 64 64"><defs>' + FAV_DEFS++ + '\n<radialGradient id="glow"><stop offset="0" stop-color="' + a + '" stop-opacity=".3"/><stop offset="1" stop-color="' + a + '" stop-opacity="0"/></radialGradient></defs>'++ + '<rect x="-32" y="-32" width="64" height="64" rx="15" fill="' + bg + '"/><g transform="scale(0.58) translate(-50 -50)"><ellipse cx="50" cy="50" rx="52" ry="40" fill="url(#glow)"/>'++ + FAV_BODY + light + '</g></svg>');++ }++ let _browserUiKey = '';++ function syncBrowserUi() {++ if (typeof document === 'undefined' || !document.head) return;++ const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));++ const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';++ const key = [accent, light, bg, mode].join('|'); if (key === _browserUiKey) return; _browserUiKey = key;++ try {++ let meta = document.querySelector('meta[name="theme-color"]');++ if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }++ meta.setAttribute('content', bg);++ const link = document.querySelector('link[rel~="icon"][data-ov-icon]');++ if (link) { link.type = 'image/svg+xml'; link.href = faviconSvg(link.getAttribute('data-ov-icon'), readColor('--bg-primary', '#0a0f1c')); }++ } catch { /* cosmetic */ }++ try { window.dispatchEvent(new CustomEvent('ov:theme', { detail: { accent: accent, accentLight: light, bg: bg, mode: mode } })); } catch { /* */ }++ }++++ /** The theme from localStorage 'ov_theme' (id and variables), else the 'ov_theme_id' cookie, else the default. */++ function resolveAndApply() {++ // 1. Try localStorage (same-origin cache with full variables)++ try {++ const raw = localStorage.getItem(STORAGE_KEY);++ if (raw) {++ const saved = JSON.parse(raw);++ if (saved && saved.variables && typeof saved.variables === 'object') {++ return show(saved.id || saved.slug || DEFAULT_ID, saved.variables, saved.custom);++ }++ if (saved && (saved.id || saved.slug)) {++ const id = saved.id || saved.slug;++ if (THEMES[id]) return show(id, THEMES[id]);++ }++ }++ } catch { /* corrupt localStorage */ }++++ // 2. Try cross-domain cookie++ const cookieId = getCookie(COOKIE_NAME);++ if (cookieId && THEMES[cookieId]) {++ // Cache the full variable set so the next load paints from localStorage instantly.++ cacheLocally(cookieId, THEMES[cookieId]);++ return show(cookieId, THEMES[cookieId]);++ }++++ // 3. Default — vibe (already the CSS fallback values, but apply explicitly), or the season's theme++ return show(DEFAULT_ID, THEMES[DEFAULT_ID]);++ }++++ function authToken() {++ return getCookie('ov_token') ||++ (typeof localStorage !== 'undefined' && (localStorage.getItem('ov_token') || localStorage.getItem('token'))) ||++ null;++ }++++ /** Cache the theme's FULL variables (custom ones too) and the date it was chosen, so the next load paints at once. */++ function cacheLocally(themeId, vars, custom, at) {++ try {++ localStorage.setItem(STORAGE_KEY, JSON.stringify({++ id: themeId, slug: themeId, variables: vars, at: at || Date.now(), ...(custom ? { custom: true } : {}),++ }));++ } catch { /* quota */ }++ setCookie(COOKIE_NAME, themeId, 365);++ }++++ /** Save a theme here and to the account (opts.custom: variable overrides; opts.sync === false: this device only). */++ function save(themeId, variables, opts) {++ const custom = (opts && opts.custom) || null;++ const base = variables || THEMES[themeId] || {};++ const vars = custom ? Object.assign({}, base, custom) : base;++ if (!vars || !Object.keys(vars).length) return;++ // Picking the default theme while a season's stands in for it means "my normal theme": seasonal themes go off.++ if (isDefault(themeId) && !custom && _season && !(opts && opts.keepSeasonal)) setDisplay({ seasonal: 'off' }, { noRepaint: true });++ cacheLocally(themeId, vars, !!custom);++ show(themeId, vars, !!custom); // saving a theme shows it: every token, the canvas and the browser UI follow at once++ if (opts && opts.sync === false) return;++ if (typeof fetch === 'undefined') return;++ const token = authToken();++ const headers = { 'Content-Type': 'application/json' };++ if (token) headers['Authorization'] = 'Bearer ' + token;++ fetch(_config.apiBase + '/api/themes/me', {++ method: 'PUT', headers: headers, credentials: 'include', keepalive: true,++ body: JSON.stringify({ theme_id: themeId, custom_variables: custom, at: themeAt() }),++ }).catch(function () { /* offline / signed out — local cache still holds */ });++ }++++ /** When the theme in this browser was chosen (ms), or 0 for a copy older than 4.64.0. */++ function themeAt() {++ try { const t = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); return t && Number(t.at) > 0 ? Number(t.at) : 0; } catch { return 0; }++ }++++ /**++ * Get the current theme ID (from cookie or localStorage).++ */++ function getCurrent() {++ try {++ const raw = localStorage.getItem(STORAGE_KEY);++ if (raw) {++ const saved = JSON.parse(raw);++ if (saved && (saved.id || saved.slug)) return saved.id || saved.slug;++ }++ } catch {}++ return getCookie(COOKIE_NAME) || 'vibe';++ }++++ // ── Display settings, one record for every OpenVibe site: display.get() → DISPLAY_OPTS' values; display.set(patch)++ // applies here and in other tabs and saves to the account. <html data-ov-*> and --ov-text-scale before first paint.++ const DISPLAY_KEY = 'ov_display', DISPLAY_OPTS = { motion: ['auto', 'full', 'reduced', 'off'], text: ['100', '112', '125'], seasonal: ['on', 'off'], scheme: ['manual', 'auto'], contrast: ['normal', 'more'], font: ['default', 'readable'], colors: ['default', 'cb-safe'], oled: ['off', 'on'], icons: ['auto', 'line', 'solid'], window: ['glass', 'solid', 'outline'] };++ // Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's++ // readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them++ // below any site's own value (4.86.0).++ // A control nobody styled wears the theme, not the browser's grey (4.186.0): only one with no class, only its face,++ // at no specificity, so any rule a site wrote still wins and no layout moves. Its shape is the kit's job.++ const BARE = ':where(:not([class]))', FACE = '{font-family:inherit;border:1px solid var(--border-light,var(--border,color-mix(in srgb,currentColor 30%,transparent)));border-radius:10px;color:var(--text-primary,inherit);background:';++ const BARE_CSS = ':where(button,input:is([type=submit],[type=button],[type=reset]))' + BARE + FACE + 'var(--bg-hover,var(--bg-card,color-mix(in srgb,currentColor 8%,transparent)))}'++ + ':where(input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset],[type=hidden]),select,textarea)' + BARE + FACE + 'var(--bg-input,var(--bg-primary,transparent))}'++ + ':where(input:is([type=checkbox],[type=radio],[type=range]),progress)' + BARE + '{accent-color:var(--accent,auto)}';++ const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';++ // Comfort (4.45.0): one <html> attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).++ const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'++ + 'html[data-ov-contrast=more] :is(p,dd,td) a:not([class]){text-decoration-line:underline;text-underline-offset:2px}html[data-ov-contrast=more] :focus-visible{outline:3px solid var(--accent-strong,var(--accent))!important;outline-offset:2px}'++ + 'html[data-ov-font=readable]{--ov-font:"Atkinson Hyperlegible",Verdana,Tahoma,"Segoe UI",system-ui,sans-serif!important}html[data-ov-font=readable] body{font-family:var(--ov-font)!important;letter-spacing:.02em;word-spacing:.06em}html[data-ov-font=readable] :is(p,li,dd){line-height:1.7}'++ + 'html[data-ov-colors=cb-safe]{--success:#56b4e9!important;--success-text:#7cc6ef!important;--warning:#f0e442!important;--warning-text:#f0e442!important;--danger:#e8743b!important;--danger-text:#f0985f!important}'++ + 'html[data-ov-colors=cb-safe][data-theme-mode=light]{--success:#0072b2!important;--success-text:#005a8c!important;--warning:#e69f00!important;--warning-text:#8a6100!important;--danger:#d55e00!important;--danger-text:#a84400!important}'++ + 'html[data-ov-oled=on][data-theme-mode=dark]{--bg-primary:#000!important;--bg-secondary:#050608!important;--bg-tertiary:#0b0d12!important;--bg-card:#0a0c10!important;--bg-input:#0b0d12!important;--bg-hover:#12151c!important}';++ const COMFORT_ATTRS = { contrast: ['data-ov-contrast', 'normal'], font: ['data-ov-font', 'default'], colors: ['data-ov-colors', 'default'], oled: ['data-ov-oled', 'off'], icons: ['data-ov-icons', 'auto'], window: ['data-ov-window', 'glass'] };++ // ── Motion (4.183.0; the owner, 2026-11-10: "still have full motion but actually just reduce it instead of getting++ // rid of it"). auto follows the device, full plays everything, reduced is Calm (every effect, smaller and slower),++ // off alone stops motion. <html data-ov-motion=full|reduced|off> is the one signal; --ov-move is what a moving++ // thing multiplies its distance by (display.move() in script); under Calm an endless loop runs at CALM_RATE.++ const CALM_MOVE = 0.35, CALM_RATE = 0.6;++ let _rm = null;++ function deviceCalm() { try { _rm = _rm || window.matchMedia('(prefers-reduced-motion: reduce)'); return !!_rm.matches; } catch (e) { return false; } }++ function motionOf(d) { const m = (d || readDisplay()).motion; return m === 'off' ? 'off' : m === 'reduced' ? 'calm' : m === 'full' ? 'full' : deviceCalm() ? 'calm' : 'full'; }++ function moveOf() { const m = motionOf(); return m === 'off' ? 0 : m === 'calm' ? CALM_MOVE : 1; }++ const MOTION_CSS = ':where(html){--ov-move:1}html[data-ov-motion=reduced]{--ov-move:' + CALM_MOVE + ';scroll-behavior:auto!important}html[data-ov-motion=off]{--ov-move:0}'++ + 'html[data-ov-motion=off] *:not([data-fx-demo] *),html[data-ov-motion=off] *:not([data-fx-demo] *)::before,html[data-ov-motion=off] *:not([data-fx-demo] *)::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}'++ + 'html[data-ov-motion=off]::view-transition-group(*),html[data-ov-motion=off]::view-transition-old(*),html[data-ov-motion=off]::view-transition-new(*){animation-duration:.001ms!important}'++ + 'html[data-ov-motion=off] .ov-icon .ovi-comet{display:none}';++ const _paced = typeof WeakSet === 'function' ? new WeakSet() : null;++ function paceLoops(scope) {++ if (!_paced || typeof document === 'undefined') return;++ try {++ const calm = document.documentElement.getAttribute('data-ov-motion') === 'reduced';++ const list = scope && scope.getAnimations ? scope.getAnimations({ subtree: true }) : document.getAnimations();++ for (let i = 0; i < list.length; i++) {++ const a = list[i], fx = a.effect, t = fx && fx.getComputedTiming ? fx.getComputedTiming() : null;++ if (!t || t.iterations !== Infinity) continue;++ const el = fx.target, slow = calm && !(el && el.closest && el.closest('[data-fx-demo]'));++ if (slow && !_paced.has(a)) { _paced.add(a); a.updatePlaybackRate(a.playbackRate * CALM_RATE); }++ else if (!slow && _paced.has(a)) { _paced.delete(a); a.updatePlaybackRate(a.playbackRate / CALM_RATE); }++ }++ } catch (e) { /* no Web Animations here: loops keep their own speed */ }++ }++ if (typeof document !== 'undefined' && document.addEventListener) {++ document.addEventListener('animationstart', function (e) { if (document.documentElement.getAttribute('data-ov-motion') === 'reduced') paceLoops(e.target); }, true);++ }++ function readDisplay() {++ const out = { motion: 'auto', text: '100', seasonal: 'on', scheme: 'manual', contrast: 'normal', font: 'default', colors: 'default', oled: 'off', icons: 'auto', window: 'glass' };++ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null') || {}; for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(d[k])) >= 0) out[k] = String(d[k]); } catch (e) { /* */ }++ return out;++ }++ function applyDisplay(d) {++ if (typeof document === 'undefined') return;++ const el = document.documentElement;++ const m = motionOf(d), attr = m === 'off' ? 'off' : m === 'calm' ? 'reduced' : d.motion === 'full' ? 'full' : '';++ if (attr) el.setAttribute('data-ov-motion', attr); else el.removeAttribute('data-ov-motion');++ if (d.text === '100') { el.style.removeProperty('--ov-text-scale'); el.removeAttribute('data-ov-text'); } else { el.style.setProperty('--ov-text-scale', String(parseInt(d.text, 10) / 100)); el.setAttribute('data-ov-text', d.text); }++ for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }++ if (!document.getElementById('ov-display-css') && document.head) {++ const st = document.createElement('style'); st.id = 'ov-display-css';++ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + BARE_CSS + ALIAS_CSS + COMFORT_CSS;++ document.head.appendChild(st);++ }++ paceLoops();++ try { window.dispatchEvent(new CustomEvent('ov:display', { detail: d })); } catch (e) { /* */ }++ }++ /** When the display settings in this browser were last changed (ms), or 0 for a copy older than 4.64.0. */++ function displayAt() {++ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null'); return d && Number(d.at) > 0 ? Number(d.at) : 0; } catch (e) { return 0; }++ }++ function setDisplay(patch, opts) {++ const d = readDisplay();++ const was = d.seasonal + '|' + d.scheme;++ for (const k in DISPLAY_OPTS) if (patch && DISPLAY_OPTS[k].indexOf(String(patch[k])) >= 0) d[k] = String(patch[k]);++ // A change made here is dated now; one adopted from the account or another site keeps its own date.++ const at = opts && Number(opts.at) > 0 ? Number(opts.at) : (opts && opts.local ? displayAt() : Date.now());++ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: at }))); } catch (e) { /* */ }++ applyDisplay(d);++ if (d.seasonal + '|' + d.scheme !== was && !(opts && opts.noRepaint)) show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom);++ if (!(opts && opts.local) && typeof fetch !== 'undefined') {++ const token = authToken(); const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;++ if (token || /(?:^|; )ov_sso_hint=account/.test(document.cookie)) fetch(_config.apiBase + '/api/themes/me/display', { method: 'PUT', headers: headers, credentials: 'include', keepalive: true, body: JSON.stringify(Object.assign({}, d, { at: at })) }).catch(function () {});++ }++ return d;++ }++++ // ── Try a theme (4.41.0): ?ov-try=<catalog name> paints it for this tab without saving (?ov-try=off ends it); a bar++ // offers Use it (saves to the account) and Stop. Only a name is read from the URL, never colours.++ const TRY_KEY = 'ov_try';++ const TRY_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;++ const tryName = function (id) { return String(id).split('-').map(function (w) { return w ? w[0].toUpperCase() + w.slice(1) : w; }).join(' '); };++ function paintTry() {++ applyVars(_trying.vars);++ try {++ const el = document.documentElement;++ el.setAttribute('data-theme', _trying.id);++ el.setAttribute('data-ov-trying', _trying.id);++ el.removeAttribute('data-ov-season');++ } catch { /* no DOM */ }++ syncBrowserUi();++ return _trying.id;++ }++ function readTry() {++ let id = '';++ try {++ const p = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('ov-try') : null;++ if (p === 'off') { sessionStorage.removeItem(TRY_KEY); return null; }++ if (p && TRY_ID.test(p)) sessionStorage.setItem(TRY_KEY, p);++ id = sessionStorage.getItem(TRY_KEY) || '';++ } catch { id = ''; }++ return TRY_ID.test(id) ? id : null;++ }++ function startTry(id, name) {++ if (!id || !TRY_ID.test(id)) return;++ try { sessionStorage.setItem(TRY_KEY, id); } catch { /* private mode: this page only */ }++ const begin = function (vars, label) {++ if (!vars || !Object.keys(vars).length) { stopTry(false); return; }++ _trying = { id: id, name: label || tryName(id), vars: vars };++ fadeNext();++ paintTry();++ whenReady(tryBar);++ };++ if (THEMES[id]) return begin(THEMES[id], name);++ if (typeof fetch === 'undefined') return;++ // A gallery theme comes from the catalog, once the page has run init({ apiBase }) (the script tag after this one).++ whenReady(function () {++ fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(id))++ .then(function (r) { return r.ok ? r.json() : null; })++ .then(function (d) { begin(d && d.theme && d.theme.variables, d && d.theme && d.theme.name); })++ .catch(function () { stopTry(false); });++ });++ }++ function stopTry(repaint) {++ _trying = null;++ try { sessionStorage.removeItem(TRY_KEY); } catch { /* */ }++ try {++ document.documentElement.removeAttribute('data-ov-trying');++ const bar = document.getElementById('ov-try-bar');++ if (bar) bar.remove();++ const u = new URL(location.href);++ if (u.searchParams.has('ov-try')) { u.searchParams.delete('ov-try'); history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); }++ } catch { /* no DOM */ }++ if (repaint !== false) { fadeNext(); resolveAndApply(); }++ }++ function whenReady(fn) {++ if (typeof document === 'undefined') return;++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn();++ }++ function tryBar() {++ if (!_trying || document.getElementById('ov-try-bar') || !document.body) return;++ const t = _trying;++ const bar = document.createElement('div');++ bar.id = 'ov-try-bar';++ bar.setAttribute('role', 'status');++ bar.style.cssText = 'position:fixed;left:0;right:0;margin:0 auto;width:max-content;bottom:calc(var(--ovnav-appbar-h,0px) + 12px);z-index:2147483000;display:flex;align-items:center;gap:6px;max-width:calc(100vw - 24px);box-sizing:border-box;padding:4px 4px 4px 16px;border-radius:999px;border:1px solid var(--border-light,var(--border,#2c3d5c));background:var(--bg-card,#131c2e);color:var(--text-primary,#e6edf7);box-shadow:0 10px 30px rgba(0,0,0,.35);font:600 14px/1.3 var(--ov-font,system-ui,sans-serif)';++ const label = document.createElement('span');++ label.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0';++ label.textContent = 'Trying ' + t.name;++ const btn = function (text, strong) {++ const b = document.createElement('button');++ b.type = 'button';++ b.textContent = text;++ b.style.cssText = 'flex:none;min-height:44px;padding:0 16px;border-radius:999px;cursor:pointer;font:inherit;' + (strong++ ? 'border:0;background:var(--accent-strong,var(--accent,#3b82f6));color:var(--on-accent-strong,#fff)'++ : 'border:1px solid var(--border-light,var(--border,#2c3d5c));background:transparent;color:inherit');++ return b;++ };++ const use = btn('Use it', true);++ const stop = btn('Stop', false);++ use.addEventListener('click', function () {++ stopTry(false);++ save(t.id, t.vars);++ const done = document.createElement('div');++ done.setAttribute('role', 'status');++ done.style.cssText = bar.style.cssText + ';padding:12px 18px';++ done.textContent = t.name + ' is your theme now, on every OpenVibe site.';++ document.body.appendChild(done);++ setTimeout(function () { done.remove(); }, 3500);++ });++ stop.addEventListener('click', function () { stopTry(true); });++ bar.append(label, use, stop);++ document.body.appendChild(bar);++ }++++ // ── The look travels (4.64.0): each domain keeps its own copy, so a click to another OpenVibe site carries ?ov_look=++ // (what this page paints, the display and theme with their dates). Added at click time, read only after an OpenVibe++ // page, only colours pass; the next site keeps it when newer and drops it from the address before first paint.++ const OV_HOST = /(^|\.)(openvibe\.[a-z]{2,12}|openre\.stream)$/i;++ const PAINT_KEY = 'ov_paint';++ const PAINT_ATTRS = ['data-theme', 'data-theme-mode', 'data-ov-season', 'data-ov-season-mark', 'data-ov-motion', 'data-ov-text', 'data-ov-contrast', 'data-ov-font', 'data-ov-colors', 'data-ov-oled', 'data-ov-icons', 'data-ov-window'];++ const SAFE_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([0-9.,\s%]+\)|[0-9.,\s]+|light|dark|(-?[0-9.]+(px)?\s+){2,4}rgba?\([0-9.,\s%]+\))$/i;++ const SAFE_ATTR = /^[a-z0-9-]{1,40}$/;++ function seasonUntil(s) {++ if (!s) return 0;++ const d = new Date(), y = d.getFullYear(), to = s.to.split('-');++ const end = new Date(y + (s.from > s.to && s.from <= String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') ? 1 : 0), Number(to[0]) - 1, Number(to[1]) + 1);++ return end.getTime();++ }++ function paintRecord() {++ const el = document.documentElement, a = {};++ for (let i = 0; i < PAINT_ATTRS.length; i++) { const v = el.getAttribute(PAINT_ATTRS[i]); if (v) a[PAINT_ATTRS[i]] = v; }++ return { k: THEME_KEYS.map(function (k) { return el.style.getPropertyValue(k).trim(); }), a: a, ts: el.style.getPropertyValue('--ov-text-scale').trim(), u: seasonUntil(_season), at: Date.now() };++ }++ let _paintTimer = 0;++ function rememberPaint() {++ if (_paintTimer || typeof setTimeout === 'undefined') return;++ _paintTimer = setTimeout(function () { _paintTimer = 0; if (_trying) return; try { localStorage.setItem(PAINT_KEY, JSON.stringify(paintRecord())); } catch (e) { /* */ } }, 0);++ }++ function lookToken() {++ const rec = { v: 1, d: readDisplay(), da: displayAt(), t: _shown.id, c: _shown.custom ? 1 : 0, ta: themeAt(), p: paintRecord() };++ return btoa(JSON.stringify(rec)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');++ }++ function readLook() {++ try {++ if (typeof location === 'undefined' || !/[?&]ov_look=/.test(location.search)) return null;++ const u = new URL(location.href), raw = u.searchParams.get('ov_look') || '';++ u.searchParams.delete('ov_look');++ try { history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); } catch (e) { /* */ }++ const from = document.referrer ? new URL(document.referrer).hostname : '';++ if (!OV_HOST.test(from) || raw.length > 4000) return null;++ const L = JSON.parse(atob(raw.replace(/-/g, '+').replace(/_/g, '/')));++ return L && L.v === 1 ? L : null;++ } catch (e) { return null; }++ }++ function adoptLook(L) {++ if (!L) return;++ const now = Date.now(), clamp = function (n) { n = Number(n) || 0; return n > now + 60000 ? now : n; };++ const da = clamp(L.da), ta = clamp(L.ta);++ if (L.d && typeof L.d === 'object' && da > displayAt()) {++ const d = readDisplay();++ for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(L.d[k])) >= 0) d[k] = String(L.d[k]);++ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: da }))); } catch (e) { /* */ }++ }++ const id = String(L.t || '');++ if (TRY_ID.test(id) && ta > themeAt()) {++ if (!L.c && THEMES[id]) cacheLocally(id, THEMES[id], false, ta);++ else if (L.c && L.p && Array.isArray(L.p.k)) {++ const vars = {};++ for (let i = 0; i < THEME_KEYS.length; i++) { const v = String(L.p.k[i] || ''); if (v && SAFE_VALUE.test(v)) vars[THEME_KEYS[i]] = v; }++ if (Object.keys(vars).length > 8) cacheLocally(id, vars, true, ta);++ }++ }++ }++ function decorateLink(e) {++ const a = e.target && e.target.closest && e.target.closest('a[href]');++ if (!a || a.hasAttribute('download')) return;++ let u; try { u = new URL(a.href, location.href); } catch (err) { return; }++ if (!/^https?:$/.test(u.protocol) || u.hostname === location.hostname || !OV_HOST.test(u.hostname)) return;++ try { u.searchParams.set('ov_look', lookToken()); a.href = u.toString(); } catch (err) { /* */ }++ }++ const _arrived = readLook();++ adoptLook(_arrived);++ applyDisplay(readDisplay());++ if (typeof document !== 'undefined' && document.addEventListener) {++ ['pointerdown', 'keydown', 'click'].forEach(function (ev) { document.addEventListener(ev, function (e) { if (ev !== 'keydown' || e.key === 'Enter') decorateLink(e); }, true); });++ window.addEventListener('ov:theme', rememberPaint);++ window.addEventListener('ov:display', rememberPaint);++ }++++ const activeId = resolveAndApply();++ rememberPaint();++ startTry(readTry());++ // The icon link may be parsed after this script: repaint once the head is complete.++ if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _browserUiKey = ''; syncBrowserUi(); });++ // A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.++ if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { fadeNext(); resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) { fadeNext(); applyDisplay(readDisplay()); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } });++++ if (typeof window !== 'undefined' && window.matchMedia) {++ try {++ const mq = window.matchMedia('(prefers-color-scheme: dark)');++ const onDevice = function () { if (readDisplay().scheme === 'auto') { fadeNext(); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } };++ if (mq.addEventListener) mq.addEventListener('change', onDevice); else if (mq.addListener) mq.addListener(onDevice);++ } catch (e) { /* no media queries */ }++ }++++ if (typeof window !== 'undefined' && window.matchMedia) {++ try {++ const rmq = window.matchMedia('(prefers-reduced-motion: reduce)');++ const onMotion = function () { if (readDisplay().motion === 'auto') applyDisplay(readDisplay()); };++ if (rmq.addEventListener) rmq.addEventListener('change', onMotion); else if (rmq.addListener) rmq.addListener(onMotion);++ } catch (e) { /* no media queries */ }++ }++++ // A theme not in the built-in map (community, custom) comes from the catalog, never falls back to the default.++ function fetchThemeVars(themeId) {++ return fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(themeId))++ .then(function (r) { return r.ok ? r.json() : null; })++ .then(function (d) { return (d && d.theme && d.theme.variables) || null; })++ .catch(function () { return null; });++ }++++ // ── The account's look from openvibe.network, at once (the cached look is already painted; newest copy wins). No++ // token gate: the ov_token cookie rides along (credentials: 'include'), for sites whose script cannot read it.++ let _lastSync = 0;++ /** This site's id from <html data-ov-service> (openvibe-shared/shell writes it), or '' (4.43.0). */++ function siteService() {++ try { const s = document.documentElement.getAttribute('data-ov-service') || ''; return /^[a-z][a-z0-9-]{1,31}$/.test(s) ? s : ''; } catch { return ''; }++ }++ function syncFromServer() {++ if (typeof fetch === 'undefined') return;++ _lastSync = Date.now();++ const token = authToken();++ const headers = token ? { 'Authorization': 'Bearer ' + token } : {};++ // A per-site look set in OpenVibe.Design ("Games in Neon Tokyo") answers for ?service=<this site> (4.43.0).++ const site = siteService();++ fetch(_config.apiBase + '/api/themes/me/active' + (site ? '?service=' + encodeURIComponent(site) : ''), { headers: headers, credentials: 'include' })++ .then(function (r) { return r.ok ? r.json() : null; })++ .then(function (data) {++ if (data && data.display) {++ const cur = readDisplay(); const next = {}; for (const k in DISPLAY_OPTS) next[k] = data.display[k] || cur[k];++ const serverAt = Number(data.display_at) || 0, localAt = displayAt();++ const takeServer = serverAt ? serverAt > localAt : !(localAt && Date.now() - localAt < 60000);++ if (takeServer && JSON.stringify(next) !== JSON.stringify(cur)) { fadeNext(); setDisplay(data.display, { local: true, at: serverAt || localAt }); }++ else if (serverAt && localAt > serverAt && JSON.stringify(next) !== JSON.stringify(cur)) setDisplay(cur, { at: localAt }); // newer here: the account catches up++ }++ if (!data || !data.theme_id) return;++ const serverThemeAt = Number(data.theme_at) || 0, localThemeAt = themeAt();++ if (serverThemeAt ? serverThemeAt < localThemeAt : (localThemeAt && Date.now() - localThemeAt < 60000)) return; // a newer pick here++ const serverId = data.theme_id;++ const custom = (data.custom_variables && typeof data.custom_variables === 'object'++ && Object.keys(data.custom_variables).length) ? data.custom_variables : null;++ const builtin = THEMES[serverId];++ const settle = function (base) {++ const vars = custom ? Object.assign({}, base || {}, custom) : (base || null);++ if (!vars || !Object.keys(vars).length) return;++ cacheLocally(serverId, vars, !!custom, serverThemeAt || localThemeAt || 1); // cacheLocally, not save() — no write-back loop++ if (_shown.id === serverId && !!_shown.custom === !!custom && JSON.stringify(_shown.vars) === JSON.stringify(vars)) return;++ fadeNext();++ show(serverId, vars, !!custom);++ };++ if (builtin) return settle(builtin);++ fetchThemeVars(serverId).then(settle);++ })++ .catch(function () { /* offline / signed out — keep the cached theme */ });++ }++ syncFromServer();++ // Asked again when the page comes back into view, and shortly after arriving from another site (a change saved there++ // just before the link may land after this page first asked).++ if (typeof document !== 'undefined' && document.addEventListener) {++ const again = function () { if (Date.now() - _lastSync > 10000) syncFromServer(); };++ document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') again(); });++ if (typeof window !== 'undefined' && window.addEventListener) window.addEventListener('pageshow', function (e) { if (e && e.persisted) again(); });++ let from = '';++ try { from = document.referrer ? new URL(document.referrer).hostname : ''; } catch (e) { from = ''; }++ if (from && typeof location !== 'undefined' && from !== location.hostname) setTimeout(syncFromServer, 2500);++ }++++ // ── Public API ───────────────────────────────────────────++ const OpenVibeThemeLoader = {++ THEMES: THEMES,++ display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS, motion: function () { return motionOf(); }, move: moveOf },++ apply: applyById,++ applyVars: applyVars,++ // Seasons: SEASONS, now() → the season today (or null), showing() → the season whose theme is on screen (or null)++ seasons: { SEASONS: SEASONS, now: seasonNow, showing: function () { return _season; } },++ // Light / Dark / Auto: get() → 'light'|'dark'|'auto', set(mode); pairOf(id) → its other half; modeOf(id)++ scheme: { get: getScheme, set: setScheme, device: deviceMode },++ pairOf: function (id) { return PAIRS[id] || null; },++ modeOf: function (id) { return MODES[id] || null; },++ onColor: onColor,++ save: save,++ // Try a theme on this tab without saving it (?ov-try=): start(id), stop(), current() → the id being tried, or null.++ try: { start: function (id) { startTry(String(id || '')); }, stop: function () { stopTry(true); }, current: function () { return _trying ? _trying.id : null; } },++ getCurrent: getCurrent,++ // This site's id (<html data-ov-service>), the key of its theme exception on the account (4.43.0).++ site: function () { return siteService(); },++ resolveAndApply: resolveAndApply,++ init: function (opts) {++ if (opts && typeof opts.apiBase === 'string' && opts.apiBase.trim()) {++ _config.apiBase = opts.apiBase.trim();++ }++ return OpenVibeThemeLoader;++ },++ };++++ if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeThemeLoader;++ else root.OpenVibeThemeLoader = OpenVibeThemeLoader;++++})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);@@@ -1,6 +1,6 @@{"name": "openvibe-extensions",-- "version": "0.9.1",++ "version": "0.9.2","private": true,"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.","scripts": {@@@ -4,7 +4,7 @@* The side panel in a real browser: headless Chrome (CDP over Node's own WebSocket, no dependencies) loads the* unpacked extension, opens panel.html with a stubbed Network/Chat/Codes and a live agent view, and checks what a* person would see: the Dock on its Agents tab, the agent's stream frame, the line and the meta, the four controls-- * (Pause/Resume, Curtain now, Mute/Unmute, End), and the Dock's own styles and nothing a CSP refused.++ * (Pause/Resume, Curtain now, Mute/Unmute, End, and the local Float), and the Dock's own styles and nothing a CSP refused.** node scripts/check-panel.js (CHROME=/path/to/chrome to choose the browser; OV_EXT_DIR=an unpacked build, default browser/)*@@@ -189,7 +189,7 @@ async function main() {watch: (() => { const w = q('a.ovda-watch'); return w ? { hidden: w.hidden, text: w.textContent.trim() } : null; })(),frame: (q('iframe.ovda-frame') || {}).src || null,tab: text(q('[role="tab"][aria-selected="true"]')),-- styles: !!document.getElementById('ovdk-styles') && !!document.getElementById('ovda-styles') && !!document.getElementById('ovac-styles'),++ styles: !!document.getElementById('ovdk-styles') && !!document.getElementById('ovda-styles'),lineDisplay: q('.ovda-line') ? getComputedStyle(q('.ovda-line')).display : null,};})()`);@@@ -229,12 +229,12 @@ async function main() {if (seen && seen.line !== 'Ship the Dock') failures.push(`the agent's line is "${seen.line}"`);if (seen && !/Paused by @ada/.test(seen.meta || '')) failures.push(`the meta does not say who paused it: "${seen.meta}"`);if (seen && !/2 watching/.test(seen.meta || '')) failures.push(`the meta does not say how many watch: "${seen.meta}"`);-- const wantActs = ['pause:Resume', 'curtain:Curtain now', 'mute:Unmute', 'end:End'];-- if (seen && JSON.stringify(seen.acts) !== JSON.stringify(wantActs)) failures.push(`the four controls are ${JSON.stringify(seen.acts)}`);-- if (seen && seen.actLinks.length !== 4) failures.push(`the read-only controls are not links: ${JSON.stringify(seen.actLinks)}`);++ const wantActs = ['pause:Resume', 'curtain:Curtain now', 'mute:Unmute', 'end:End', 'float:Float'];++ if (seen && JSON.stringify(seen.acts) !== JSON.stringify(wantActs)) failures.push(`the controls are ${JSON.stringify(seen.acts)}`);++ if (seen && seen.actLinks.length !== 4) failures.push(`the four server controls are not read-only links: ${JSON.stringify(seen.actLinks)}`);if (seen && seen.actLinks.some((href) => href !== 'https://openvibe.codes')) failures.push(`a control does not open Codes: ${JSON.stringify(seen.actLinks)}`);if (seen && seen.frame !== AGENT.embed_url) failures.push(`the stream frame is "${seen.frame}"`);-- if (seen && !seen.styles) failures.push("the Dock's own styles are missing");++ if (seen && !seen.styles) failures.push("the Dock's and agent view's own styles are missing");if (seen && seen.lineDisplay !== 'flex') failures.push(`the agent's styles did not apply (line display ${seen.lineDisplay})`);if (security.length) failures.push(`CSP refused: ${security.join(' | ')}`);if (!paused.some((u) => u.startsWith('https://openvibe.live/embed/agent/'))) failures.push('the panel never asked for the agent embed');
@@ -4,6 +4,18 @@ The OpenVibe Companion's releases. Each one is a `browser/manifest.json` version
@@ -4,6 +4,18 @@ The OpenVibe Companion's releases. Each one is a `browser/manifest.json` version
release](https://github.com/OpenVibers/OpenVibe.Extensions/releases); older entries are in the git
release](https://github.com/OpenVibers/OpenVibe.Extensions/releases); older entries are in the git
history.
history.
+## 0.9.2 — 2026-11-11
+
+- **The side panel's Dock is OpenVibe.Shared 4.212.0** (was 4.124.0), copied from the release with the
+ Companion's own two patches kept: the read-only Dock token renders the four server controls as links
+ to openvibe.codes, and a Live frame that refuses embedding falls back to the still and a **Watch on
+ openvibe.live** link (Firefox and an unlisted Chrome id never try the frame).
+- The newer Dock brings its **Code** and **Actor** tabs, the one window (`ov-window.js`), the composer,
+ the glyph set, the theme loader and the agent sheet/session, all bundled under `browser/shared/`
+ because the extension's CSP allows no remote script.
+- `AGENT_EMBED_EXTENSION_ORIGINS` on OpenVibe.Live is set to the extension's fixed Chrome id
+ (`chrome-extension://bjcjcfcecpkkmfndegkjekoipdmbgakd`), so the panel can frame the agent stream.
+
## 0.9.1 — 2026-11-07
## 0.9.1 — 2026-11-07
- **The side panel's agent view can read.** OpenVibe.Codes' two agent-view read routes now accept
- **The side panel's agent view can read.** OpenVibe.Codes' two agent-view read routes now accept
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
**Build and test:** `npm install && npm run pack` (Node 22 or newer) · **Tests:** `npm test`
**Build and test:** `npm install && npm run pack` (Node 22 or newer) · **Tests:** `npm test`
**Status:** alpha (roadmap Wave 18, client part). The extension builds, its tests pass, and headless
**Status:** alpha (roadmap Wave 18, client part). The extension builds, its tests pass, and headless
-Chrome loads the unpacked 0.9.0 build with no console or CSP errors (2026-11-07, Chrome 156 via
+Chrome loads the unpacked 0.9.2 build with no console or CSP errors (2026-11-07, Chrome 156 via
CDP `Extensions.loadUnpacked`; the side panel's Agents tab draws the Dock's agent view against stubbed first parties, see the credential note below). A development build of
CDP `Extensions.loadUnpacked`; the side panel's Agents tab draws the Dock's agent view against stubbed first parties, see the credential note below). A development build of
the coupon part looked up codes and sent a report against a local OpenVibe.Coupons. Mozilla's
the coupon part looked up codes and sent a report against a local OpenVibe.Coupons. Mozilla's
`web-ext lint` (what addons.mozilla.org runs on upload) passes with no errors, warnings or notices
`web-ext lint` (what addons.mozilla.org runs on upload) passes with no errors, warnings or notices
@@ -111,7 +111,7 @@ covered in Coupons' own tests: lookups are identical for every caller, and the A
@@ -111,7 +111,7 @@ covered in Coupons' own tests: lookups are identical for every caller, and the A
The **OpenVibe Dock** — the pill, the card with its Vibe, Agents, Chats and Activity tabs — sits in
The **OpenVibe Dock** — the pill, the card with its Vibe, Agents, Chats and Activity tabs — sits in
the browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the
the browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the
-released OpenVibe.Shared Dock 4.124.0, bundled under `browser/shared/` because the extension's CSP allows no
+released OpenVibe.Shared Dock 4.212.0, bundled under `browser/shared/` because the extension's CSP allows no
remote script, and it reads and writes the same state on the Network as the Dock on a site, so it
remote script, and it reads and writes the same state on the Network as the Dock on a site, so it
opens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent and
opens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent and
unread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel.
unread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel.
@@ -119,14 +119,15 @@ unread counts) and re-reads it every 30 s, so a change on the page reaches the o
@@ -119,14 +119,15 @@ unread counts) and re-reads it every 30 s, so a change on the page reaches the o
The Agents tab draws the person's own desk stream the same way a site's Dock does: the Live embed, the
The Agents tab draws the person's own desk stream the same way a site's Dock does: the Live embed, the
task line, and the owner's **Pause/Resume**, **Curtain now**, **Mute/Unmute** and **End** controls. The
task line, and the owner's **Pause/Resume**, **Curtain now**, **Mute/Unmute** and **End** controls. The
view is read with the person's token from OpenVibe.Codes (`GET /api/v1/me/agent-view`); the extension
view is read with the person's token from OpenVibe.Codes (`GET /api/v1/me/agent-view`); the extension
-allows Codes and Live's embed for exactly that. Codes' normal session check does not yet accept the
-Dock's read-only OAuth token (issuer `<network>/dock`), so a production side panel keeps the view
-hidden until Codes or the Network accepts that credential; `scripts/check-panel.js` proves the bundled
-Dock itself draws it. Live's embed also allows first-party site origins, not an extension origin yet.
+allows Codes and Live's embed for exactly that. Codes accepts the Dock's read-only OAuth token (issuer
+`<network>/dock`), so the panel shows the stream; its four server controls are links to openvibe.codes,
+where the person's own session does the same thing. The fixed Chrome id is in Live's
+`AGENT_EMBED_EXTENSION_ORIGINS`, and a browser that cannot frame the embed gets the still and a
+**Watch on openvibe.live** link instead. `scripts/check-panel.js` proves both in headless Chrome.
It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: the
It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: the
Network's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no
Network's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no
-secret), and the Dock gets a read-only token (`dock`, `activity`, `chat.read`) that refreshes itself.
+secret), and the Dock gets a read-only token (`dock`, `activity`, `chat.read`, `agent.view`) that refreshes itself.
The access token is kept in `storage.session`; the rotating refresh token in `storage.local`, on this
The access token is kept in `storage.session`; the rotating refresh token in `storage.local`, on this
device. Sign out revokes it at the Network, and the account page (`/my`) lists it under **Signed-in
device. Sign out revokes it at the Network, and the account page (`/my`) lists it under **Signed-in
apps**. The 0.7.0 pasted-token path is still in the settings page as a fallback until a later release
apps**. The 0.7.0 pasted-token path is still in the settings page as a fallback until a later release
@@ -139,9 +140,10 @@ browser/
@@ -139,9 +140,10 @@ browser/
manifest.json MV3; Chromium (minimum_chrome_version 114) and Firefox (gecko id, strict_min_version 128)
manifest.json MV3; Chromium (minimum_chrome_version 114) and Firefox (gecko id, strict_min_version 128)
panel.html / panel.js the Dock in the side panel: the Network's state, the same tabs as the Dock on a site
panel.html / panel.js the Dock in the side panel: the Network's state, the same tabs as the Dock on a site
panel.css the Dock fills the panel (no floating card, no pill)
panel.css the Dock fills the panel (no floating card, no pill)
- shared/ the released OpenVibe.Shared Dock 4.124.0, bundled: the CSP allows no remote script
- (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js,
- notification-ui.js, notification-live.js, ov-mark.js, kit.css)
+ shared/ the released OpenVibe.Shared Dock 4.212.0, bundled: the CSP allows no remote script
+ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js,
+ agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js,
+ brands.js, theme-loader.js, ov-mark.js, kit.css)
popup.html / popup.js DOM wiring only
popup.html / popup.js DOM wiring only
options.html / .js connect (save a checked token), disconnect, grant the API host permission
options.html / .js connect (save a checked token), disconnect, grant the API host permission
lib/config.js API_ORIGIN (Coupons), NETWORK_ORIGIN and CHAT_ORIGIN (the Dock), CODES_ORIGIN (its agent view); HANDOFF_ORIGINS (Rent, Watch, Food, Reviews)
lib/config.js API_ORIGIN (Coupons), NETWORK_ORIGIN and CHAT_ORIGIN (the Dock), CODES_ORIGIN (its agent view); HANDOFF_ORIGINS (Rent, Watch, Food, Reviews)
@@ -5,11 +5,11 @@
@@ -5,11 +5,11 @@
"wave": "W18",
"wave": "W18",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",
"adr": "OpenVibe.Contracts ADR-023 (first-party client surfaces)",
- "version": "0.9.0",
+ "version": "0.9.2",
"surfaces": {
"surfaces": {
"browser": {
"browser": {
"name": "OpenVibe Companion",
"name": "OpenVibe Companion",
- "version": "0.9.0",
+ "version": "0.9.2",
"manifest": 3,
"manifest": 3,
"targets": [
"targets": [
"chromium >= 114",
"chromium >= 114",
@@ -31,7 +31,7 @@
@@ -31,7 +31,7 @@
],
],
"credential": "optional cpx_ install token (coupons.lookup, coupons.report); the Dock one-click OAuth public client (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local); the 0.7.0 pasted OpenVibe token is still accepted as a fallback until a later release",
"credential": "optional cpx_ install token (coupons.lookup, coupons.report); the Dock one-click OAuth public client (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local); the 0.7.0 pasted OpenVibe token is still accepted as a fallback until a later release",
"publishedToStores": false,
"publishedToStores": false,
- "loadedInChrome": "yes: headless Chrome 156 loads the 0.9.0 build via CDP Extensions.loadUnpacked; the side panel's Agents tab draws the Dock's agent view against a stubbed Network/Chat/Codes (the Live stream frame, the task line and meta, and Pause/Resume, Curtain now, Mute/Unmute and End) with its styles applied and no CSP refusal (scripts/check-panel.js); lib/autoapply.js passes three fixture checkout pages in headless Chrome; the one-click Dock sign-in is covered by test/auth.test.js",
+ "loadedInChrome": "yes: headless Chrome 156 loads the 0.9.2 build via CDP Extensions.loadUnpacked; the side panel's Agents tab draws the Dock's agent view against a stubbed Network/Chat/Codes (the Live stream frame, the task line and meta, and Pause/Resume, Curtain now, Mute/Unmute and End) with its styles applied and no CSP refusal (scripts/check-panel.js); lib/autoapply.js passes three fixture checkout pages in headless Chrome; the one-click Dock sign-in is covered by test/auth.test.js",
"loadedInFirefox": "not yet loaded in a browser; Mozilla's web-ext lint (8.10.0, what AMO runs on upload) passes with 0 errors, 0 warnings and 0 notices, and runs in CI with warnings as errors",
"loadedInFirefox": "not yet loaded in a browser; Mozilla's web-ext lint (8.10.0, what AMO runs on upload) passes with 0 errors, 0 warnings and 0 notices, and runs in CI with warnings as errors",
"handoffs": [
"handoffs": [
"https://openvibe.rent/board (keep a listing)",
"https://openvibe.rent/board (keep a listing)",
@@ -44,7 +44,7 @@
@@ -44,7 +44,7 @@
"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion",
"storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion",
"features": [
"features": [
"OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",
"OpenVibe Dock one-click sign-in: public OAuth client companion (PKCE S256, no secret), read-only scopes dock/activity/chat.read, access token in storage.session, rotating refresh token in storage.local, Sign out revokes the family, the account page lists it under Signed-in apps",
- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.124.0 bundled under browser/shared/ (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js, notification-ui.js, notification-live.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes"
+ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.212.0 bundled under browser/shared/ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js, agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js, brands.js, theme-loader.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes"
],
],
"pending": [
"pending": [
"owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",
"owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; the Chrome id is fixed by browser/manifest.json key",
@@ -67,7 +67,7 @@
@@ -67,7 +67,7 @@
"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food",
"watches a page on OpenVibe.Watch, and compares delivery apps on OpenVibe.Food",
"saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)",
"saves a review site's stated rating to OpenVibe.Reviews (read only when pressed, only on those sites)",
"an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",
"an optional cpx_ install token, never an OpenVibe session; and the Dock one-click OAuth sign-in (scopes dock, activity, chat.read; access in storage.session, rotating refresh in storage.local; the 0.7.0 pasted token is still a fallback until a later release)",
- "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.124.0 bundled under browser/shared/ (dock.js, dock-vibe.js, dock-agent.js, dock-timer.js, action-center.js, notification-ui.js, notification-live.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes",
+ "the OpenVibe Dock in the side panel (Chrome side_panel, Firefox sidebar_action): the released OpenVibe.Shared Dock 4.212.0 bundled under browser/shared/ (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js, agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js, brands.js, theme-loader.js, ov-mark.js, kit.css), reading and writing the same Network state as the Dock on a site, with the same tabs (Vibe, Agents, Chats, Activity), and re-reading it every 30 s so the page, the pop-out and the panel agree; the Agents tab draws the person's agent view with Pause/Resume, Curtain now, Mute/Unmute and End, and approvals/sends are hidden with the read-only scopes",
"store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI",
"store-ready: reproducible Chrome and Firefox store zips (npm run pack:stores), listing and privacy answers in store/, Mozilla web-ext lint in CI",
"kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live",
"kiosk companion (Firefox new-tab page and history bridge for Live's /kiosk), moved from OpenVibe.Live",
"hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"
"hardware/: the Raspberry Pi streamer and controller scripts, moved from OpenVibe.Live"
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"manifest_version": 3,
"manifest_version": 3,
"name": "OpenVibe Companion",
"name": "OpenVibe Companion",
"short_name": "OpenVibe",
"short_name": "OpenVibe",
- "version": "0.9.1",
+ "version": "0.9.2",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqVJohdlaysL2HgSsj2Fsd8PxPvXJkZHjEvNyGSYNSEpZBoIeZd7Cm7xznGVikDBbe7WseaNLt+HheX4WhN94jU/FrS2016AqQG9NTHoymJR8XSrPPDyhc85EUpICOFwqfFsSOykzaRyHZVcrvoepYwntwFjWdt1ul4VozwkqKovZoKUgCn398aNvZ6AH/J0xb0xWMiOiL9CvUWN/jK4wv0ShnJevpmyBBHL273CkQvfMS3VUBvBjq3seMU8BAHkE5RI3c2y/MRdjwYGsCoMU6MPYwt46PQZ0S8SPnFFozJndkpoqc/ue9oXhd77b12GKlJ1BPhHgn8BSx4IAvjeL1wIDAQAB",
"description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; a mask in the email box of a sign-up form; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.",
"description": "Coupon codes for the shop you are on, tried in its coupon box when you ask; the same restaurant on every delivery app; a review site's rating saved to OpenVibe.Reviews; a mask in the email box of a sign-up form; and one click to keep a rental listing on your OpenVibe.Rent board or watch a page. The OpenVibe Dock sits in the side panel, on every site. Reads the tab only when you open it.",
"homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",
"homepage_url": "https://github.com/OpenVibers/OpenVibe.Extensions",
@@ -158,7 +158,6 @@
@@ -158,7 +158,6 @@
.ovac-group { display: flex; align-items: center; gap: 8px; margin: 10px 10px 4px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #8b8b96); }
.ovac-group { display: flex; align-items: center; gap: 8px; margin: 10px 10px 4px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #8b8b96); }
.ovac-group span { font-variant-numeric: tabular-nums; letter-spacing: 0; color: var(--text-secondary, #b6b6c0); }
.ovac-group span { font-variant-numeric: tabular-nums; letter-spacing: 0; color: var(--text-secondary, #b6b6c0); }
.ovac-group[data-bucket="waiting"] { color: color-mix(in srgb, var(--warning, #f5a524) 70%, var(--text-primary, #ececf1)); }
.ovac-group[data-bucket="waiting"] { color: color-mix(in srgb, var(--warning, #f5a524) 70%, var(--text-primary, #ececf1)); }
- @media (prefers-reduced-motion: reduce) { .ovac { animation: none; } .ovac-item[data-bucket="running"] .ovac-state::before { animation: none; } }
`;
`;
document.head.appendChild(st);
document.head.appendChild(st);
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,283 @@
@@ -0,0 +1,283 @@
+/* openvibe-shared/agent-session.css: the look of agent-session.js (4.32.0; it was a string inside the script before).
+ Theme tokens only, light or dark; OpenVibe.Design can restyle it. agent-session.js links it from beside itself when a
+ page has not; a page may link it in its <head> to paint styled from the first line. */
+.ovas { --ovas-b: var(--border, rgba(255,255,255,.1)); --ovas-s: var(--bg-secondary, #121826); --ovas-t: var(--bg-tertiary, rgba(255,255,255,.05)); --ovas-mut: var(--text-muted, #8b98ad); --ovas-sec: var(--text-secondary, #a8b3c4); --ovas-ok: color-mix(in srgb, #22c55e 70%, var(--text-primary, #e6edf7)); --ovas-bad: var(--live-red, #f87171); --ovas-add: color-mix(in srgb, #22c55e 18%, transparent); --ovas-del: color-mix(in srgb, #ef4444 18%, transparent); --ovas-code: color-mix(in srgb, var(--bg-primary, #0b1020) 72%, var(--ovas-s));
+ display: grid; gap: 12px; color: var(--text-primary, #e6edf7); font-family: inherit; min-width: 0; }
+.ovas-ic { width: 16px; height: 16px; flex: none; }
+.ovas-head { position: sticky; top: var(--ovnav-real-h, 0px); z-index: 2; display: grid; gap: 8px; padding: 12px 14px; border-radius: 16px; border: 1px solid var(--ovas-b); background: color-mix(in srgb, var(--ovas-s) 92%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
+.ovas-top { display: flex; align-items: center; gap: 10px; min-width: 0; }
+.ovas-pill { display: inline-flex; align-items: center; gap: 6px; flex: none; height: 26px; padding: 0 10px; border-radius: 999px; font-size: 12.5px; font-weight: 700; background: var(--ovas-t); color: var(--ovas-sec); }
+.ovas-pill[data-s="running"], .ovas-pill[data-s="placed"], .ovas-pill[data-s="verifying"] { color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
+.ovas-pill[data-s="succeeded"] { color: var(--ovas-ok); background: color-mix(in srgb, #22c55e 15%, transparent); }
+.ovas-pill[data-s="failed"], .ovas-pill[data-s="expired"] { color: var(--ovas-bad); background: color-mix(in srgb, #ef4444 15%, transparent); }
+.ovas-title { flex: 1; min-width: 0; font-weight: 700; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovas-meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 12.5px; color: var(--ovas-mut); }
+.ovas-meta b { color: var(--ovas-sec); font-weight: 650; font-variant-numeric: tabular-nums; }
+.ovas-agents { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
+.ovas-agent { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; font-size: 12px; font-weight: 650; border: 1px solid var(--ovas-b); color: var(--ovas-sec); }
+.ovas-agent.is-now { border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); color: var(--text-primary, #e6edf7); }
+.ovas-plan { display: grid; gap: 6px; padding-top: 2px; }
+.ovas-plan > summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 600; }
+.ovas-plan > summary::-webkit-details-marker { display: none; }
+.ovas-plan-now { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovas-plan-now::before { content: ''; display: inline-block; width: 8px; height: 8px; margin-right: 7px; border-radius: 50%; background: var(--accent, #3b82f6); vertical-align: 1px; }
+.ovas-plan-n { flex: none; font-size: 12px; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }
+.ovas-plan:not([open]) > ol { display: none; }
+.ovas-plan-bar { height: 4px; border-radius: 4px; background: var(--ovas-t); overflow: hidden; }
+.ovas-plan-bar i { display: block; height: 100%; background: var(--accent, #3b82f6); border-radius: inherit; transition: width .3s; }
+.ovas-plan ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 3px; font-size: 13px; }
+.ovas-plan li { display: flex; gap: 8px; align-items: baseline; color: var(--ovas-sec); }
+.ovas-plan li::before { content: ''; flex: none; width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--ovas-mut); transform: translateY(1px); }
+.ovas-plan li[data-s="in_progress"] { color: var(--text-primary, #e6edf7); font-weight: 600; }
+.ovas-plan li[data-s="in_progress"]::before { border-color: var(--accent, #3b82f6); background: var(--accent, #3b82f6); animation: ovas-beat 1.2s ease-in-out infinite; }
+.ovas-plan li[data-s="completed"] { color: var(--ovas-mut); text-decoration: line-through; }
+.ovas-plan li[data-s="completed"]::before { background: var(--ovas-ok); border-color: var(--ovas-ok); }
+.ovas-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
+.ovas-chip { height: 30px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: var(--ovas-sec); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; }
+.ovas-chip[aria-pressed="true"] { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, #fff); }
+.ovas-chip.ovas-follow { margin-left: auto; }
+.ovas-feed { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; min-width: 0; }
+.ovas-feed > li { min-width: 0; }
+.ovas-msg { padding: 2px 4px; font-size: 15px; line-height: 1.55; overflow-wrap: anywhere; }
+.ovas-msg p { margin: 0 0 8px; } .ovas-msg p:last-child { margin-bottom: 0; }
+.ovas-msg ul, .ovas-msg ol { margin: 4px 0 8px; padding-left: 22px; } .ovas-msg li { margin: 2px 0; }
+.ovas-msg code, .ovas-detail code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .88em; padding: 1px 5px; border-radius: 6px; background: var(--ovas-t); }
+.ovas-msg a { color: var(--accent-light, var(--accent, #60a5fa)); }
+.ovas-code, .ovas-out, .ovas-diff { margin: 0; padding: 10px 12px; border-radius: 12px; background: var(--ovas-code); border: 1px solid var(--ovas-b); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; overflow: auto; max-height: 50vh; white-space: pre; }
+.ovas-code code { padding: 0; background: none; }
+.ovas-group, .ovas-step { border: 1px solid var(--ovas-b); border-radius: 14px; background: var(--ovas-s); overflow: hidden; }
+.ovas-group > summary, .ovas-step > summary { display: flex; align-items: center; gap: 10px; min-height: 46px; padding: 6px 12px; cursor: pointer; list-style: none; }
+.ovas-group > summary::-webkit-details-marker, .ovas-step > summary::-webkit-details-marker { display: none; }
+.ovas-chev { width: 14px; height: 14px; flex: none; color: var(--ovas-mut); transition: transform .15s; }
+details[open] > summary .ovas-chev { transform: rotate(90deg); }
+.ovas-icons { display: inline-flex; flex: none; }
+.ovas-kind { display: grid; place-items: center; width: 28px; height: 28px; flex: none; border-radius: 9px; background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }
+.ovas-icons .ovas-kind { width: 24px; height: 24px; border: 2px solid var(--ovas-s); border-radius: 8px; }
+.ovas-icons .ovas-kind + .ovas-kind { margin-left: -7px; }
+.ovas-kind .ovas-ic { width: 14px; height: 14px; }
+.ovas-kind[data-k="edit"], .ovas-kind[data-k="write"] { color: #4ade80; background: color-mix(in srgb, #22c55e 14%, transparent); }
+.ovas-kind[data-k="web"] { color: #38bdf8; background: color-mix(in srgb, #0ea5e9 14%, transparent); }
+.ovas-kind[data-k="agent"] { color: #c084fc; background: color-mix(in srgb, #a855f7 16%, transparent); }
+.ovas-sum { flex: 1; min-width: 0; display: grid; gap: 1px; }
+.ovas-sum b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovas-sum small { font-size: 12px; color: var(--ovas-mut); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
+.ovas-delta { flex: none; font-size: 12px; font-weight: 700; font-variant-numeric: tabular-nums; }
+.ovas-delta .a { color: var(--ovas-ok); } .ovas-delta .r { color: var(--ovas-bad); margin-left: 4px; }
+.ovas-state { flex: none; display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }
+.ovas-state .ovas-ic { width: 15px; height: 15px; }
+.ovas-state[data-s="ok"] .ovas-ic { color: var(--ovas-ok); } .ovas-state[data-s="fail"] { color: var(--ovas-bad); }
+.ovas-spin { position: relative; display: inline-block; width: 4px; height: 4px; margin: 0 6px; border: 0; border-radius: 50%; background: var(--accent, #3b82f6); animation: ovas-hop 1s ease-in-out .15s infinite; }
+.ovas-spin::before, .ovas-spin::after { content: ''; position: absolute; top: 0; width: 4px; height: 4px; border-radius: 50%; background: inherit; animation: ovas-hop 1s ease-in-out infinite; }
+.ovas-spin::before { left: -6px; } .ovas-spin::after { left: 6px; animation-delay: .3s; }
+@keyframes ovas-hop { 0%, 80%, 100% { transform: translateY(0); opacity: .45; } 40% { transform: translateY(-3px); opacity: 1; } }
+.ovas-group > ol { list-style: none; margin: 0; padding: 0 8px 8px; display: grid; gap: 6px; }
+.ovas-group.is-one { border: 0; background: none; }
+.ovas-group.is-one > summary { display: none; }
+.ovas-group.is-one > ol { padding: 0; }
+.ovas-group.is-one .ovas-step { background: var(--ovas-s); border-color: var(--ovas-b); }
+.ovas-group .ovas-step { background: var(--ovas-t); border-color: transparent; }
+.ovas-detail { display: grid; gap: 10px; padding: 0 12px 12px; }
+.ovas-label { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--ovas-mut); margin-bottom: -4px; }
+.ovas-copy { display: inline-flex; align-items: center; gap: 4px; border: 0; background: none; color: var(--ovas-mut); font: inherit; font-size: 11.5px; font-weight: 650; letter-spacing: 0; text-transform: none; cursor: pointer; padding: 2px 4px; border-radius: 6px; }
+.ovas-copy:hover { color: var(--text-primary, #e6edf7); background: var(--ovas-t); }
+.ovas-copy .ovas-ic { width: 13px; height: 13px; }
+.ovas-out.is-fail { box-shadow: inset 3px 0 0 var(--ovas-bad); }
+.ovas-wait { font-size: 13px; color: var(--ovas-mut); display: flex; align-items: center; gap: 8px; }
+.ovas-diff span { display: block; padding: 0 6px; margin: 0 -6px; }
+.ovas-diff .a { background: var(--ovas-add); } .ovas-diff .r { background: var(--ovas-del); } .ovas-diff .h { color: var(--ovas-mut); }
+.ovas-sub { margin: 0 8px 8px; padding: 8px 0 0 12px; border-left: 2px solid color-mix(in srgb, #a855f7 45%, transparent); display: grid; gap: 8px; }
+.ovas-handoff { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--ovas-sec); }
+.ovas-handoff::before, .ovas-handoff::after { content: ''; flex: 1; height: 1px; background: var(--ovas-b); }
+.ovas-hand { display: grid; justify-items: center; gap: 4px; max-width: 82%; text-align: center; }
+.ovas-hand-row { display: inline-flex; align-items: center; gap: 6px; }
+.ovas-hand-row .ovas-ic { color: var(--ovas-mut); }
+.ovas-hand small { font-size: 12.5px; color: var(--ovas-mut); line-height: 1.4; }
+.ovas-note { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; padding: 9px 12px; border-radius: 12px; font-size: 13.5px; background: color-mix(in srgb, #f59e0b 12%, transparent); color: var(--text-primary, #e6edf7); }
+.ovas-note[data-level="error"] { background: color-mix(in srgb, #ef4444 13%, transparent); }
+.ovas-note[data-level="info"] { background: color-mix(in srgb, #22c55e 10%, transparent); }
+.ovas-note[data-level="info"] .ovas-ic { color: var(--ovas-ok); }
+.ovas-note .ovas-ic { color: #f59e0b; margin-top: 2px; } .ovas-note[data-level="error"] .ovas-ic { color: var(--ovas-bad); }
+.ovas-done { display: grid; gap: 6px; padding: 12px 14px; border-radius: 14px; border: 1px solid color-mix(in srgb, #22c55e 40%, transparent); background: color-mix(in srgb, #22c55e 8%, transparent); }
+.ovas-done[data-ok="0"] { border-color: color-mix(in srgb, #ef4444 40%, transparent); background: color-mix(in srgb, #ef4444 8%, transparent); }
+.ovas-done > b { display: flex; align-items: center; gap: 6px; font-size: 13px; }
+.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 0 4px; font-size: 12.5px; color: var(--ovas-mut); }
+.ovas-line::before { content: ''; flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--ovas-mut); opacity: .7; }
+.ovas-extra { display: grid; gap: 8px; padding: 8px 12px; border-radius: 12px; border: 1px solid var(--ovas-b); background: var(--ovas-s); font-size: 13.5px; color: var(--ovas-sec); }
+.ovas-empty { padding: 18px; text-align: center; color: var(--ovas-mut); font-size: 13.5px; border: 1px dashed var(--ovas-b); border-radius: 14px; }
+.ovas [hidden] { display: none !important; }
+@keyframes ovas-beat { 0%, 100% { transform: translateY(1px) scale(.8); opacity: .6; } 50% { transform: translateY(1px) scale(1); opacity: 1; } }
+@media (hover: hover) { .ovas-group > summary:hover, .ovas-step > summary:hover { background: var(--bg-hover, rgba(255,255,255,.04)); } }
+@media (max-width: 640px) {
+ .ovas-head { padding: 10px 12px; border-radius: 14px; } .ovas-msg { font-size: 14.5px; }
+ .ovas-code, .ovas-out, .ovas-diff { font-size: 12px; max-height: 45vh; }
+ .ovas-bar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; padding: 0 2px; } .ovas-bar::-webkit-scrollbar { display: none; }
+ .ovas-chip { flex: none; } .ovas-icons .ovas-kind + .ovas-kind { display: none; }
+}
+
+/* Files changed (4.32.0): every file the agents edited or wrote, with its +/−; a tap opens its step. */
+.ovas-files { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
+.ovas-file { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 46px; padding: 6px 12px; border-radius: 14px; border: 1px solid var(--ovas-b); background: var(--ovas-s); color: inherit; font: inherit; text-align: left; cursor: pointer; }
+.ovas-file:hover { background: var(--bg-hover, rgba(255,255,255,.04)); }
+.ovas-file:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+.ovas-file-n { flex: none; font-size: 12px; color: var(--ovas-mut); }
+/* A step's raw input, folded. */
+.ovas-raw > summary { display: flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; font-size: 11px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; color: var(--ovas-mut); }
+.ovas-raw > summary::-webkit-details-marker { display: none; }
+.ovas-raw > summary .ovas-chev { width: 12px; height: 12px; }
+.ovas-raw[open] > summary .ovas-chev { transform: rotate(90deg); }
+.ovas-raw .ovas-label { margin: 8px 0 0; }
+.ovas-raw pre { margin-top: 8px; }
+/* Cost: per agent in the head, per step on its row when the agent reports it. */
+.ovas-agent-cost { margin-left: 6px; font-weight: 600; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }
+.ovas-cost { flex: none; font-size: 12px; font-weight: 650; color: var(--ovas-mut); font-variant-numeric: tabular-nums; }
+/* The action bar (Approve / Continue / Stop), sticky at the bottom while it is needed. */
+.ovas-actions { position: sticky; bottom: max(8px, env(safe-area-inset-bottom, 0px)); z-index: 2; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; border-radius: 16px; border: 1px solid var(--ovas-b); background: color-mix(in srgb, var(--ovas-s) 94%, transparent); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: 0 10px 30px rgba(0,0,0,.25); }
+.ovas-actions p { flex: 1 1 260px; min-width: 0; margin: 0; font-size: 13px; line-height: 1.4; color: var(--ovas-sec); }
+.ovas-actions { justify-content: flex-end; }
+.ovas-act { min-height: 38px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: var(--text-primary, #e6edf7); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }
+.ovas-act.is-primary { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, var(--on-accent, #fff)); }
+.ovas-act.is-danger { color: var(--ovas-bad); border-color: color-mix(in srgb, var(--ovas-bad) 45%, var(--ovas-b)); }
+.ovas-act:disabled { opacity: .55; cursor: default; }
+.ovas-act:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+
+/* Attachments (4.50.0): the images and files a step read, wrote or produced. A group's header shows them while the
+ group is folded; a message's sit under its text. The URLs come from the product; only the page's own origin and
+ Media's load. */
+.ovas-group > summary, .ovas-step > summary { flex-wrap: wrap; row-gap: 8px; }
+.ovas-att-slot { flex-basis: 100%; order: 9; display: block; min-width: 0; }
+.ovas-att-slot:empty { display: none; }
+.ovas-att { display: grid; gap: 8px; width: 100%; min-width: 0; max-width: 100%; }
+.ovas-att-row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 8px; min-width: 0; }
+.ovas-att-img { box-sizing: border-box; display: block; width: 136px; flex: none; padding: 0; border: 1px solid var(--ovas-b); border-radius: 12px; overflow: hidden; background: var(--ovas-t); color: inherit; font: inherit; text-align: left; cursor: pointer; }
+.ovas-att-img img { display: block; width: 136px; height: 92px; object-fit: cover; background: var(--ovas-t); }
+.ovas-att-chip { box-sizing: border-box; display: inline-flex; align-items: center; gap: 7px; flex: none; max-width: 220px; min-height: 36px; padding: 0 12px; border-radius: 999px; border: 1px solid var(--ovas-b); background: var(--ovas-t); color: var(--ovas-sec); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; }
+.ovas-att-chip .ovas-ic { width: 15px; height: 15px; flex: none; color: var(--accent-light, var(--accent, #60a5fa)); }
+.ovas-att-chip[data-role="read"] .ovas-ic { color: #38bdf8; }
+.ovas-att-chip[data-role="wrote"] .ovas-ic { color: #4ade80; }
+.ovas-att-cap { display: block; min-width: 0; font-size: 11.5px; line-height: 1.3; color: var(--ovas-mut); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.ovas-att-img .ovas-att-cap { padding: 5px 7px; }
+.ovas-att-chip .ovas-att-cap { padding: 0; }
+.ovas-att:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+.ovas-att-img:hover, .ovas-att-chip:hover { background: var(--bg-hover, rgba(255,255,255,.06)); }
+/* The preview sheet: one attachment, opened by a tap. */
+.ovas-sheet { box-sizing: border-box; position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; padding: 16px; background: color-mix(in srgb, var(--bg-primary, #0b1020) 80%, transparent); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
+.ovas-sheet[hidden] { display: none; }
+.ovas-sheet-panel { box-sizing: border-box; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 10px; width: min(960px, 92vw); max-height: min(92vh, 920px); padding: 14px; border-radius: 18px; border: 1px solid var(--ovas-b); background: var(--ovas-s); color: var(--text-primary, #e6edf7); box-shadow: 0 24px 70px rgba(0,0,0,.45); }
+.ovas-sheet-bar { display: flex; align-items: center; gap: 10px; min-width: 0; }
+.ovas-sheet-bar b { flex: 1; min-width: 0; font-size: 14px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovas-sheet-x { width: 40px; height: 40px; flex: none; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: inherit; font: inherit; font-size: 15px; line-height: 1; cursor: pointer; }
+.ovas-sheet-x:focus-visible, .ovas-sheet-foot a:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
+.ovas-sheet-body { min-width: 0; min-height: 0; overflow: auto; display: grid; }
+.ovas-prev { min-width: 0; display: grid; }
+.ovas-prev-img { display: block; width: 100%; height: auto; max-height: 74vh; object-fit: contain; border-radius: 12px; background: var(--ovas-t); touch-action: none; }
+.ovas-prev-media { display: block; width: 100%; max-height: 74vh; border-radius: 12px; background: #000; }
+.ovas-prev-audio { width: 100%; }
+.ovas-prev-pdf { width: 100%; height: 72vh; border: 0; border-radius: 12px; background: #fff; }
+.ovas-prev-code { margin: 0; padding: 12px; border-radius: 12px; border: 1px solid var(--ovas-b); background: var(--ovas-code); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.5; overflow: auto; max-height: 70vh; white-space: pre; }
+.ovas-prev-text { min-width: 0; }
+.ovas-prev-text .ovas-msg { padding: 0; }
+.ovas-prev-note { margin: 0 0 10px; font-size: 13px; color: var(--ovas-mut); }
+.ovas-sheet-foot { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
+.ovas-sheet-foot a { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--ovas-b); background: transparent; color: inherit; text-decoration: none; font-size: 13px; font-weight: 700; }
+.ovas-sheet-foot .ovas-sheet-open { background: var(--accent-strong, var(--accent, #3472d8)); border-color: transparent; color: var(--on-accent-strong, #fff); }
+.ovas-csv { width: 100%; border-collapse: collapse; font-size: 12.5px; }
+.ovas-csv th, .ovas-csv td { border: 1px solid var(--ovas-b); padding: 5px 8px; text-align: left; white-space: nowrap; }
+.ovas-csv th { background: var(--ovas-t); font-weight: 700; }
+.ovas-tok-key { color: var(--accent-light, var(--accent, #60a5fa)); }
+.ovas-tok-str { color: var(--ovas-ok); }
+.ovas-tok-num { color: #fbbf24; }
+.ovas-tok-kw { color: #c084fc; }
+.ovas-tok-com { color: var(--ovas-mut); font-style: italic; }
+
+@media (max-width: 760px) {
+ /* A phone: the head does not stick, so it never floats over the steps it sits above. */
+ .ovas-head { position: static; }
+}
+@media (max-width: 640px) {
+ /* Phones: bigger tap targets, and the preview sheet takes the whole screen. */
+ .ovas-att-chip { min-height: 44px; padding: 0 14px; }
+ .ovas-att-chip .ovas-att-cap { font-size: 12px; }
+ .ovas-sheet { padding: 0; align-items: stretch; }
+ .ovas-sheet-panel { width: 100%; max-height: 100vh; height: 100vh; border-radius: 0; border: 0; padding: 12px; }
+ .ovas-sheet-x, .ovas-sheet-foot a { min-height: 44px; }
+ .ovas-prev-img { max-height: 68vh; }
+}
+
+/* ── Protocol 1.2 (4.205.0, ask 354): phases, thinking, sources, card mode ───────────────────────────────────── */
+.ovas-phase { display: flex; align-items: center; gap: 9px; margin: 6px 2px 0; font-size: 12.5px; letter-spacing: .02em; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }
+.ovas-phase b { font-weight: 750; color: var(--text-primary, #e6edf7); }
+.ovas-phase-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: var(--accent, #3b82f6); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); }
+.ovas-phase[data-s="running"] .ovas-phase-dot { animation: ovasBreathe 1.6s ease-in-out infinite; }
+.ovas-phase[data-s="done"] .ovas-phase-dot { background: var(--ovas-ok, #22c55e); box-shadow: none; }
+.ovas-phase::after { content: ""; flex: 1; height: 1px; background: var(--ovas-b, rgba(255,255,255,.1)); }
+@keyframes ovasBreathe { 50% { transform: scale(calc(1 - .25 * var(--ov-move, 1))); opacity: calc(1 - .5 * var(--ov-move, 1)); } }
+.ovas-think details { border-radius: 12px; }
+.ovas-think summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; font-size: 13px; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }
+.ovas-think summary::-webkit-details-marker { display: none; }
+.ovas-think-dots { width: 18px; height: 6px; flex: none; background: radial-gradient(circle, currentColor 2px, transparent 2.5px) 0 50% / 6px 6px repeat-x; animation: ovasDots 1.1s linear infinite; }
+.ovas-think.is-done .ovas-think-dots { animation: none; opacity: .5; }
+@keyframes ovasDots { 50% { opacity: .35; } }
+.ovas-think-body { margin: 6px 0 0 26px; padding: 8px 12px; border-left: 2px solid var(--ovas-b, rgba(255,255,255,.1)); font-size: 13px; line-height: 1.5; white-space: pre-wrap; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); max-height: 320px; overflow: auto; }
+.ovas-sources { border: 1px solid var(--ovas-b, rgba(255,255,255,.1)); border-radius: 14px; padding: 8px 12px; background: var(--ovas-t, rgba(255,255,255,.04)); }
+.ovas-sources > summary { display: flex; align-items: center; gap: 10px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 650; }
+.ovas-sources > summary::-webkit-details-marker { display: none; }
+.ovas-src-tiles { display: inline-flex; }
+.ovas-src-tile { display: inline-grid; place-items: center; width: 22px; height: 22px; margin-left: -6px; border-radius: 7px; font-size: 11px; font-weight: 800; color: #fff;
+ background: hsl(var(--h, 210) 55% 42%); box-shadow: 0 0 0 2px var(--ovas-s, #121826); }
+.ovas-src-tile:first-child { margin-left: 0; }
+.ovas-sources ol { display: grid; gap: 6px; margin: 10px 0 2px; padding: 0; list-style: none; max-height: 340px; overflow: auto; }
+.ovas-sources li { display: grid; gap: 1px; min-width: 0; }
+.ovas-sources li a { color: var(--accent-light, var(--accent, #60a5fa)); text-decoration: none; font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovas-sources li a:hover { text-decoration: underline; }
+.ovas-sources li small { color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); font-size: 11.5px; }
+.ovas--card { gap: 8px; }
+.ovas--card .ovas-head { position: static; padding: 10px 12px; }
+.ovas--card :is(.ovas-bar, .ovas-files) { display: none !important; }
+.ovas--card .ovas-feed > li:not(:nth-last-child(-n+3)) { display: none; }
+.ovas--card .ovas-feed > li .ovas-out { max-height: 120px; }
+.ovas-open { justify-self: start; display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 10px; font-size: 13px; font-weight: 700; text-decoration: none;
+ color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }
+html[data-ov-motion="off"] :is(.ovas-phase-dot, .ovas-think-dots) { animation: none !important; }
+/* 3. A card keeps its groups folded: the head says what they did. */
+.ovas--card .ovas-group:not(.is-one) > :not(summary) { display: none; }
+.ovas--card .ovas-group:not(.is-one) > summary .ovas-chev { display: none; }
+
+/* ── Compact runs (2026-11-11, the owner with the portal's run view open: "consider stuff like this … parse and expandable
+ and compact mode … smart"): a group is one line, its state first, its words, the step it is on in mono, the chevron
+ last; the steps inside are light rows; the head says what it is doing now; three dots while it works; a box to talk
+ to it. Every motion follows --ov-move. */
+.ovas-feed { gap: 6px; }
+.ovas-group > summary { min-height: 42px; padding: 4px 10px; gap: 9px; }
+.ovas-group:not(.is-one) > summary .ovas-state { order: -1; }
+.ovas-group:not(.is-one) > summary .ovas-icons { display: none; }
+.ovas-group:not(.is-one) > summary .ovas-chev { order: 99; transition: transform var(--ov-dur-2, .2s) var(--ov-ease-out, ease); }
+.ovas-group[open]:not(.is-one) > summary .ovas-chev { transform: rotate(90deg); }
+.ovas-group:not(.is-one) > summary .ovas-sum { display: flex; align-items: baseline; gap: 9px; }
+.ovas-group:not(.is-one) > summary .ovas-sum b { flex: none; max-width: 60%; }
+.ovas-group:not(.is-one) > summary .ovas-sum small { flex: 1 1 auto; min-width: 0; }
+.ovas-group > ol .ovas-step { border: 0; background: transparent; border-radius: 10px; }
+.ovas-group > ol .ovas-step > summary { min-height: 38px; padding: 3px 8px; }
+.ovas-group > ol .ovas-step > summary:hover { background: color-mix(in srgb, var(--text-primary, #fff) 5%, transparent); }
+.ovas-group > ol .ovas-kind { width: 24px; height: 24px; border-radius: 7px; }
+.ovas-meta [data-m="now"] { flex: 1 1 100%; min-width: 0; order: -1; }
+.ovas-now { display: flex; align-items: center; gap: 7px; min-width: 0; font: 12.5px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--text-primary, #e6edf7); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
+.ovas-now::before { content: ""; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #3b82f6); animation: ovasBreathe 1.6s ease-in-out infinite; }
+.ovas-working { display: flex; gap: 5px; padding: 4px 6px; }
+.ovas-working[hidden] { display: none; }
+.ovas-working i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #3b82f6); opacity: .35; animation: ovasWork 1.2s ease-in-out infinite; }
+.ovas-working i:nth-child(2) { animation-delay: .16s; } .ovas-working i:nth-child(3) { animation-delay: .32s; }
+@keyframes ovasWork { 40% { opacity: 1; transform: translateY(calc(-3px * var(--ov-move, 1))); } }
+.ovas-you { justify-self: end; max-width: 85%; padding: 8px 12px; border-radius: 14px 14px 4px 14px; background: color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
+.ovas-you:not(.is-sent):not(.is-failed) { opacity: .6; }
+.ovas-you.is-failed { outline: 1px solid var(--ovas-bad, #ef4444); }
+.ovas-say { position: sticky; bottom: 0; display: grid; gap: 4px; padding: 8px; border-radius: 16px; border: 1px solid var(--ovas-b, rgba(255,255,255,.1)); background: var(--ovas-s, #121826); }
+.ovas-say textarea { flex: 1; min-height: 40px; max-height: 160px; resize: none; padding: 9px 12px; border: 0; border-radius: 12px; background: var(--ovas-t, rgba(255,255,255,.05)); color: inherit; font: inherit; font-size: 15px; }
+.ovas-say textarea:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 0; }
+.ovas-say-send { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 12px; background: var(--accent-strong, var(--accent, #2563eb)); color: #fff; cursor: pointer; }
+.ovas-say.is-busy .ovas-say-send { opacity: .5; }
+html[data-ov-motion="off"] :is(.ovas-now, .ovas-working i)::before, html[data-ov-motion="off"] .ovas-working i { animation: none !important; }
+.ovas-you-files { display: block; margin-top: 4px; font-size: 12px; opacity: .8; }
+.ovas-say .ovt-c-row { display: flex; align-items: center; gap: 6px; }
+.ovas-say .ovt-c-tools { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
+.ovas-say .ovt-c-note { margin: 0; font-size: 12px; color: var(--ovas-mut, var(--text-secondary, #9fb0cf)); }
+.ovas-say .ovt-c-note:empty { display: none; }
new file mode 100644
new file mode 100644
@@ -0,0 +1,1056 @@
@@ -0,0 +1,1056 @@
+/**
+ * openvibe-shared/agent-session.js — a coding agent's session as people read it: what it said, the steps it took
+ * (commands, files read and changed, searches, sub-agents) grouped and folded, each step's command, diff and OUTPUT
+ * (live while it runs), hand-offs between agents, the plan it keeps, and a summary that stays at the top (4.24.0).
+ *
+ * The owner, 2026-10-28, showing the Claude app's session view ("Ran 3 commands", a Bash sheet saying "Output
+ * unavailable"): "OpenVibe.Codes should… actually show the output from the agent… more user friendly… handle that
+ * between all agents… in a smart way… way better than this idea."
+ *
+ * const view = OpenVibeAgentSession.mount(el, { title, status: 'running', startedAt, link })
+ * link(url) → boolean: which https addresses in the agents' text become links (default:
+ * all; a site whose agents read untrusted pages links only its own, e.g. a person's desk)
+ * view.push(event) one event: a Codes job line ({ t: 'tool', … }), any Claude Code stream-json event
+ * ({ type: 'assistant', … }) or an OpenVibe.Actor task event ({ kind: 'output', step, … },
+ * actor.task-event@1); every OpenVibe.Codes harness adapter (Claude Code, Codex, OpenCode,
+ * Command Code, Aider, DeepSeek, OpenVibe.AI, your own model) and every Actor agent speaks one
+ * view.pushAll(events)
+ * view.status('succeeded' | 'failed' | 'cancelled' | 'running' | 'queued' | …, label?)
+ * view.actions([{ id, label, kind: 'primary' | 'danger' | undefined }], note?)
+ * the sticky bar at the bottom (Approve, Continue, Stop…); opts.onAction(id) runs when one is
+ * pressed (a returned promise keeps the buttons disabled until it settles); [] hides it
+ * view.summary() → { agents, steps, commands, files, added, removed, problems, costUsd, durationMs }
+ * view.destroy()
+ * OpenVibeAgentSession.normalize(event) → [line] the stream-json → line mapping, for servers and other readers
+ *
+ * The lines (Codes' job protocol 1.1, additive to 1.0):
+ * { t: 'agent', agent, model } an agent started (a hand-off starts the next one)
+ * { t: 'text', text, parent? } what it said (light Markdown: paragraphs, lists, code, links)
+ * { t: 'tool', id, name, what, input?, parent? } a step began: Bash, Read, Edit, Write, Grep, Glob, LS,
+ * WebFetch, WebSearch, Task (a sub-agent), TodoWrite (the plan)
+ * { t: 'tool_progress', id, chunk } output while it runs (agents whose steps the harness runs)
+ * { t: 'tool_result', id, ok, output, cost_usd?, duration_ms? } how it ended, with its output (and its cost,
+ * when the agent reports one: shown on the step's row)
+ * { t: 'handoff', from, to?, reason } one agent stopped and the next took over
+ * { t: 'note', text, level? } a warning or an error along the way
+ * { t: 'step', text } the job's own progress (cloning, the branch, files changed, pushing)
+ * { t: 'usage', input_tokens, output_tokens }
+ * { t: 'done', ok, text, duration_ms, cost_usd, turns }
+ * A step or a message may carry the images and files the agent read, wrote or produced:
+ * attachments: [{ url, name, mime, size?, kind: 'image'|'video'|'audio'|'pdf'|'text'|'file', role: 'read'|'wrote'|'output' }]
+ * The product supplies the URLs; the component never builds one from a local path. Any line may carry attachments,
+ * and the component shows them for every line it draws. A line of a type it does not know (Codes' proof lines) is
+ * drawn as a minimal row: its label or summary, then its attachments. A step's attachments show in its
+ * group's header, visible while the group is folded; a message's show under its text. A tap opens a preview sheet: the
+ * image at full width (pinch to zoom), video and audio in their players, a PDF in a frame, text as code, Markdown, a CSV
+ * table or pretty JSON, plus Open and Download. An attachment loads only from the product's own origin or Media's,
+ * never a third-party address the model printed; SVG is only ever an <img>, and text is set as text, never as markup.
+ * A line from a sub-agent carries `parent`: the id of the Task step that started it, so it nests under that step.
+ * Any line may carry `agent` (the head then follows who is working); a step without an id gets one, and the next result
+ * of the same tool without an id is its result. { t: 'state', state } moves the head (Actor's task states).
+ *
+ * Everything is text: no line's content ever becomes markup (links are https only). The look is agent-session.css
+ * (4.32.0), linked from beside this script unless the page links it; it follows the page's theme tokens, light or dark.
+ * The Files tab lists every file edited or written with its +/−; each step keeps its raw input (folded); the head shows
+ * each agent's cost from its done line.
+ */
+(function (root) {
+ 'use strict';
+
+ // ── What each step is ─────────────────────────────────────
+ const KIND = {
+ Bash: 'command', bash: 'command', run_command: 'command', shell: 'command', exec: 'command',
+ Read: 'read', read_file: 'read', Grep: 'search', grep: 'search', Glob: 'search', LS: 'search', list_files: 'search', search: 'search',
+ Edit: 'edit', MultiEdit: 'edit', edit_file: 'edit', NotebookEdit: 'edit', Write: 'write', write_file: 'write', apply_patch: 'edit',
+ WebFetch: 'web', WebSearch: 'web', Task: 'agent', Agent: 'agent', TodoWrite: 'plan',
+ };
+ // A tool no table knows is judged by the words of its name (run_shell, readFile, web-fetch), never by letters inside
+ // a word ("Frobnicate" is not a cat).
+ function kindOf(name) {
+ if (KIND[name]) return KIND[name];
+ const w = new Set(String(name || '').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z]+/).filter(Boolean));
+ const any = (...xs) => xs.some((x) => w.has(x));
+ // The web first: web_search and web_fetch are looking things up, not searching files.
+ return any('web', 'fetch', 'http', 'browse', 'url', 'internet') ? 'web' : any('bash', 'shell', 'command', 'exec', 'run', 'terminal') ? 'command' : any('edit', 'patch', 'replace') ? 'edit'
+ : any('write', 'create') ? 'write' : any('read', 'cat', 'view', 'open') ? 'read' : any('grep', 'glob', 'find', 'search', 'list', 'ls') ? 'search' : 'tool';
+ }
+ const NOUN = { command: ['command', 'commands'], read: ['read', 'reads'], search: ['search', 'searches'], edit: ['edit', 'edits'], write: ['new file', 'new files'], web: ['lookup', 'lookups'], agent: ['helper', 'helpers'], plan: ['plan', 'plan'], tool: ['tool', 'tools'] };
+ const VERB = {
+ command: ['Ran a command', 'Ran %d commands'], read: ['Read a file', 'Read %d files'], search: ['Searched once', 'Searched %d times'],
+ edit: ['Edited a file', 'Edited %d files'], write: ['Wrote a file', 'Wrote %d files'], web: ['Looked something up', 'Looked up %d things'],
+ agent: ['Started a helper', 'Started %d helpers'], plan: ['Updated the plan', 'Updated the plan'], tool: ['Used a tool', 'Used %d tools'],
+ };
+ const ICON = {
+ command: '<path d="M4 6l5 5-5 5"/><path d="M11 17h9"/>',
+ read: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/>',
+ search: '<circle cx="11" cy="11" r="6"/><path d="m20 20-4.2-4.2"/>',
+ edit: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="m13 7 4 4"/>',
+ write: '<path d="M6 3h8l4 4v14H6z"/><path d="M12 11v6M9 14h6"/>',
+ web: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17M12 3.5c2.5 2.6 2.5 14.4 0 17M12 3.5c-2.5 2.6-2.5 14.4 0 17"/>',
+ agent: '<rect x="5" y="7" width="14" height="11" rx="3"/><path d="M12 3v4M9 12h.01M15 12h.01"/>',
+ plan: '<path d="M9 6h11M9 12h11M9 18h11"/><path d="m3 6 1.5 1.5L7 5M3 12l1.5 1.5L7 11M3 18l1.5 1.5L7 17"/>',
+ tool: '<path d="M14 7a4 4 0 0 0-5.6 5.1L4 16.5 7.5 20l4.4-4.4A4 4 0 0 0 17 10l-2.5 2.5-3-3z"/>',
+ ok: '<path d="m5 12.5 4.5 4.5L19 7.5"/>', fail: '<path d="M6 6l12 12M18 6 6 18"/>', copy: '<rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/>',
+ handoff: '<path d="M4 12h13"/><path d="m13 7 5 5-5 5"/>', warn: '<path d="M12 4 2.5 20h19z"/><path d="M12 10v4M12 17h.01"/>',
+ chevron: '<path d="m9 6 6 6-6 6"/>',
+ img: '<rect x="3" y="4.5" width="18" height="15" rx="2"/><circle cx="8.5" cy="9.5" r="1.6"/><path d="m4 17 4.5-4.5 3.5 3.5 3-3L20 17"/>',
+ video: '<rect x="3" y="5" width="13" height="14" rx="2"/><path d="m16 10 5-3v10l-5-3z"/>',
+ audio: '<path d="M9 17V6l10-2v11"/><circle cx="6.5" cy="17.5" r="2.5"/><circle cx="16.5" cy="15.5" r="2.5"/>',
+ doc: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/><path d="M9 12.5h6M9 16h6"/>',
+ code: '<path d="m9 8-5 4 5 4M15 8l5 4-5 4"/>',
+ file: '<path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/>',
+ };
+ const svg = (name, cls) => `<svg class="${cls || 'ovas-ic'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICON[name] || ICON.tool}</svg>`;
+ const AGENT_NAME = { claude: 'Claude Code', 'claude-code': 'Claude Code', codex: 'Codex', opencode: 'OpenCode', 'command-code': 'Command Code', cmd: 'Command Code', aider: 'Aider', deepseek: 'DeepSeek', 'openvibe-ai': 'OpenVibe.AI', ai: 'OpenVibe.AI', own: 'Your model' };
+ const agentName = (a) => AGENT_NAME[String(a || '').toLowerCase()] || String(a || 'Agent');
+
+ // ── stream-json → lines (the same mapping as harness/job.js) ─
+ const MAX_TEXT = 6000;
+ const cut = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? `${t.slice(0, n - 1)}…` : t; };
+ function resultText(content) {
+ if (typeof content === 'string') return content;
+ if (Array.isArray(content)) return content.map((c) => (c && typeof c.text === 'string' ? c.text : '')).join('\n');
+ return content == null ? '' : String(content);
+ }
+ // OpenVibe.Actor's task events (actor.task-event@1): { kind: 'state' | 'end', state } and { kind: 'output', step, agent,
+ // text, tool, input, is_error }, step one of plan, text, handoff, check, tool_call, tool_result.
+ function fromActor(e) {
+ const agent = e.agent ? { agent: String(e.agent) } : {};
+ if (e.kind === 'state' || e.kind === 'end') return e.state ? [{ t: 'state', state: String(e.state), ...agent }] : [];
+ if (e.kind !== 'output') return [];
+ const text = cut(e.text, MAX_TEXT);
+ const att = e.attachments ? { attachments: e.attachments } : {};
+ if (e.step === 'text') return [e.is_error ? { t: 'note', level: 'error', text, ...agent } : { t: 'text', text, ...att, ...agent }];
+ if (e.step === 'plan') return [{ t: 'step', text, ...agent }];
+ if (e.step === 'handoff') return [{ t: 'handoff', from: null, to: e.agent || null, reason: text, ...agent }];
+ if (e.step === 'check') return [{ t: 'note', level: /^checked/i.test(text) ? 'info' : 'warning', text, ...agent }];
+ if (e.step === 'tool_call') return [{ t: 'tool', name: String(e.tool || 'tool'), what: typeof e.input === 'string' ? cut(e.input, 300) : '', input: e.input && typeof e.input === 'object' ? e.input : { description: typeof e.input === 'string' ? cut(e.input, 300) : '' }, ...att, ...agent }];
+ if (e.step === 'tool_result') return [{ t: 'tool_result', name: String(e.tool || 'tool'), ok: !e.is_error, output: cut(e.text, 8000), ...att, ...agent }];
+ return text ? [{ t: 'text', text, ...agent }] : [];
+ }
+
+ // ── AG-UI 1.0 (protocol 1.2, ask 354, 2026-11-11): every OpenVibe agent run streams AG-UI events (docs.ag-ui.com,
+ // spec 1.0 of 2026-11-07) with OpenVibe's own words in metadata.ov (title, kind, sources, …), so another tool's agent
+ // renders here and ours in theirs. Deltas (text, tool arguments, thinking) carry their ids; the view joins them.
+ const pick = (e, ...k) => { for (const n of k) if (e[n] != null) return e[n]; return null; };
+ function fromAgui(e) {
+ const ov = (e.metadata && typeof e.metadata === 'object' && e.metadata.ov && typeof e.metadata.ov === 'object') ? e.metadata.ov : {};
+ const src = Array.isArray(ov.sources) && ov.sources.length ? [{ t: 'sources', items: ov.sources }] : [];
+ const id = (...k) => String(pick(e, ...k) || '');
+ switch (e.type) {
+ case 'RUN_STARTED': return [{ t: 'state', state: 'running' }];
+ case 'RUN_FINISHED': return [...src, { t: 'done', ok: true, text: cut(typeof e.result === 'string' ? e.result : '', MAX_TEXT) }];
+ case 'RUN_ERROR': return [{ t: 'done', ok: false, text: cut(String(e.message || 'The run failed.'), 2000) }];
+ case 'STEP_STARTED': return [{ t: 'phase', id: id('stepName', 'step_name'), title: String(ov.title || pick(e, 'stepName', 'step_name') || 'Step'), status: 'running' }];
+ case 'STEP_FINISHED': return [{ t: 'phase', id: id('stepName', 'step_name'), status: 'done' }];
+ case 'TEXT_MESSAGE_START': return [{ t: 'text_start', id: id('messageId', 'message_id') }];
+ case 'TEXT_MESSAGE_CONTENT': case 'TEXT_MESSAGE_CHUNK': return [{ t: 'text_delta', id: id('messageId', 'message_id'), delta: String(e.delta || '') }];
+ case 'TOOL_CALL_START': return [{ t: 'tool', id: id('toolCallId', 'tool_call_id'), name: String(pick(e, 'toolCallName', 'tool_call_name') || 'tool'), input: {}, title: ov.title ? cut(ov.title, 160) : '', kind: ov.kind || null }];
+ case 'TOOL_CALL_ARGS': case 'TOOL_CALL_CHUNK': return [{ t: 'tool_args', id: id('toolCallId', 'tool_call_id'), delta: String(e.delta || '') }];
+ case 'TOOL_CALL_RESULT': return [...src, { t: 'tool_result', id: id('toolCallId', 'tool_call_id'), ok: !ov.error, output: cut(typeof e.content === 'string' ? e.content : JSON.stringify(e.content || ''), 8000), duration_ms: Number(ov.duration_ms) || 0, cost_usd: Number(ov.cost_usd) || 0 }];
+ case 'REASONING_START': case 'REASONING_MESSAGE_START': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r' }];
+ case 'REASONING_MESSAGE_CONTENT': case 'REASONING_MESSAGE_CHUNK': return [{ t: 'thinking_delta', id: id('messageId', 'message_id') || 'r', delta: String(e.delta || '') }];
+ case 'REASONING_END': case 'REASONING_MESSAGE_END': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r', done: true }];
+ case 'REASONING_ENCRYPTED_VALUE': return [{ t: 'thinking', id: id('messageId', 'message_id') || 'r', hidden: true }];
+ case 'STATE_SNAPSHOT': {
+ const plan = e.snapshot && Array.isArray(e.snapshot.plan) ? e.snapshot.plan : null;
+ return plan ? [{ t: 'phases', items: plan.slice(0, 30).map((p) => ({ title: String(p.title || p.content || ''), status: String(p.status || 'pending') })) }] : [];
+ }
+ case 'ACTIVITY_SNAPSHOT': return e.content && e.content.title ? [{ t: 'activity', text: cut(String(e.content.title), 200) }] : [];
+ case 'CUSTOM': {
+ if (e.name === 'ov.sources' && Array.isArray(e.value)) return [{ t: 'sources', items: e.value }];
+ if (e.name === 'ov.handoff' && e.value) return [{ t: 'handoff', from: e.value.from, to: e.value.to || null, reason: cut(e.value.reason, 600) }];
+ if (e.name === 'ov.approval' && e.value) return [{ t: 'note', level: 'warning', text: cut(e.value.question || 'Waiting for your approval.', 600) }];
+ return [];
+ }
+ default: return src;
+ }
+ }
+ function normalize(e) {
+ if (!e || typeof e !== 'object') return [];
+ if (e.t) return [e];
+ if (typeof e.type === 'string' && /^[A-Z][A-Z_]+$/.test(e.type)) return fromAgui(e);
+ if (e.kind) return fromActor(e);
+ const parent = e.parent_tool_use_id || null;
+ if (e.type === 'system' && e.subtype === 'init') return [{ t: 'agent', agent: e.agent || 'claude', model: e.model || null }];
+ if (e.type === 'system' && e.subtype === 'handoff') return [{ t: 'handoff', from: e.from, to: e.to || null, reason: cut(e.reason, 600) }];
+ if (e.type === 'system' && (e.subtype === 'error' || e.subtype === 'warning')) return [{ t: 'note', level: e.subtype, text: cut(e.text, 600) }];
+ if (e.type === 'system' && e.subtype === 'tool_progress') return [{ t: 'tool_progress', id: String(e.tool_use_id || ''), chunk: String(e.text || '') }];
+ if (e.type === 'system' && e.subtype === 'usage' && e.usage) return [{ t: 'usage', input_tokens: e.usage.input_tokens || 0, output_tokens: e.usage.output_tokens || 0 }];
+ if (e.type === 'assistant' && e.message) {
+ const out = [];
+ for (const c of e.message.content || []) {
+ if (c.type === 'text' && c.text) out.push({ t: 'text', text: cut(c.text, MAX_TEXT), parent });
+ if (c.type === 'tool_use') out.push({ t: 'tool', id: String(c.id || ''), name: String(c.name || ''), input: c.input || {}, parent });
+ }
+ if (e.message.usage) out.push({ t: 'usage', input_tokens: e.message.usage.input_tokens || 0, output_tokens: e.message.usage.output_tokens || 0 });
+ return out;
+ }
+ if (e.type === 'user' && e.message && Array.isArray(e.message.content)) {
+ return e.message.content.filter((c) => c && c.type === 'tool_result').map((c) => ({ t: 'tool_result', id: String(c.tool_use_id || ''), ok: !c.is_error, output: cut(resultText(c.content), 8000) }));
+ }
+ if (e.type === 'result') return [{ t: 'done', ok: !e.is_error, text: cut(e.result, MAX_TEXT), duration_ms: e.duration_ms || 0, cost_usd: e.total_cost_usd || 0, turns: e.num_turns || 0 }];
+ return [];
+ }
+
+ // ── Small helpers ─────────────────────────────────────────
+ const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+ const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
+ const plural = (k, n) => (n === 1 ? VERB[k][0] : VERB[k][1].replace('%d', n));
+ function dur(ms) {
+ const s = Math.max(0, Math.round(ms / 1000));
+ if (s < 60) return `${s} s`;
+ const m = Math.floor(s / 60);
+ return m < 60 ? `${m} min ${s % 60 ? `${s % 60} s` : ''}`.trim() : `${Math.floor(m / 60)} h ${m % 60} min`;
+ }
+ const base = (p) => String(p || '').split('/').filter(Boolean).pop() || String(p || '');
+
+ // ── Attachments: the images and files a step read, wrote or produced (4.50.0) ──────────
+ // The shape a step or a message carries:
+ // attachments: [{ url, name, mime, size?, kind: 'image'|'video'|'audio'|'pdf'|'text'|'file', role: 'read'|'wrote'|'output' }]
+ // The product supplies the URLs; the component never builds one from a local path.
+ const ATT_KINDS = new Set(['image', 'video', 'audio', 'pdf', 'text', 'file']);
+ const ATT_ROLES = new Set(['read', 'wrote', 'output']);
+ const ATT_ROLE_LABEL = { read: 'read', wrote: 'wrote', output: 'produced' };
+ const ATT_ICON = { image: 'img', video: 'video', audio: 'audio', pdf: 'doc', text: 'code', file: 'file' };
+ const ATT_EXT = {
+ image: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'bmp', 'ico'],
+ video: ['mp4', 'webm', 'mov', 'm4v', 'ogv'],
+ audio: ['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac'],
+ pdf: ['pdf'],
+ text: ['md', 'markdown', 'txt', 'json', 'jsonl', 'csv', 'tsv', 'log', 'yaml', 'yml', 'html', 'css', 'js', 'mjs', 'cjs',
+ 'ts', 'tsx', 'jsx', 'py', 'go', 'rs', 'sh', 'sql', 'xml', 'toml', 'ini', 'env', 'conf'],
+ };
+ const attExt = (name) => (String(name || '').split('?')[0].split('#')[0].split('.').pop() || '').toLowerCase();
+ /** What kind of thing an attachment is: what it says, or what its name ends in. */
+ function attKind(mime, name, kind) {
+ if (ATT_KINDS.has(kind)) return kind;
+ const m = String(mime || '').toLowerCase().split(';')[0].trim();
+ if (m.startsWith('image/')) return 'image';
+ if (m.startsWith('video/')) return 'video';
+ if (m.startsWith('audio/')) return 'audio';
+ if (m === 'application/pdf') return 'pdf';
+ if (m.startsWith('text/')) return 'text';
+ if (m === 'application/json' || m === 'application/xml' || m === 'application/x-yaml') return 'text';
+ const ext = attExt(name);
+ for (const k of Object.keys(ATT_EXT)) if (ATT_EXT[k].includes(ext)) return k;
+ return 'file';
+ }
+ /** The attachments of a step or a message, in the shape the component draws them. */
+ function normAttachments(list) {
+ const out = [], seen = new Set();
+ for (const a of Array.isArray(list) ? list : []) {
+ if (!a || typeof a !== 'object') continue;
+ const url = String(a.url || '').trim();
+ if (!url || seen.has(url)) continue;
+ seen.add(url);
+ const name = String(a.name || '').trim() || base(String(url).split('?')[0]) || 'file';
+ out.push({ url, name, mime: String(a.mime || ''), size: Number(a.size) || 0, kind: attKind(a.mime, name, a.kind), role: ATT_ROLES.has(a.role) ? a.role : 'output' });
+ }
+ return out;
+ }
+ /** The page's own origin, or Media's: an attachment loads from there and nowhere else. */
+ function attAllowed(url, extra) {
+ const raw = String(url || '').trim();
+ if (!raw) return false;
+ if (/^(blob:|data:image\/(png|jpe?g|gif|webp);)/i.test(raw)) return true; // the page made it itself
+ if (raw.startsWith('/') && !raw.startsWith('//')) return true; // the product's own origin
+ const here = (typeof location !== 'undefined' && location && location.hostname) ? String(location.hostname).toLowerCase() : '';
+ let host = '';
+ try { host = new URL(raw, here ? 'https://' + here : 'https://openvibe.network/').hostname.toLowerCase(); } catch { return false; }
+ if (here && host === here) return true;
+ if (host === 'openvibe.media' || host.endsWith('.openvibe.media')) return true;
+ for (const o of Array.isArray(extra) ? extra : []) {
+ const h = String(o || '').toLowerCase().replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
+ if (h && (host === h || host.endsWith('.' + h))) return true;
+ }
+ return false;
+ }
+ /** The tiles of a line: the images in one row, the other files as chips in a row under them. */
+ function attStrip(list, onOpen) {
+ const strip = el('div', 'ovas-att');
+ const imgs = el('div', 'ovas-att-row');
+ const chips = el('div', 'ovas-att-row');
+ for (const a of list) {
+ const b = el('button', a.kind === 'image' ? 'ovas-att-img' : 'ovas-att-chip');
+ b.type = 'button';
+ b.dataset.kind = a.kind;
+ b.dataset.role = a.role;
+ const role = ATT_ROLE_LABEL[a.role] || 'produced';
+ b.title = role + ': ' + a.name;
+ b.setAttribute('aria-label', 'Open ' + a.name + ' (' + role + ')');
+ if (a.kind === 'image') {
+ const img = el('img');
+ img.loading = 'lazy'; img.decoding = 'async'; img.alt = a.name; img.src = a.url;
+ img.addEventListener('error', () => { img.remove(); b.hidden = true; });
+ b.appendChild(img);
+ imgs.appendChild(b);
+ } else {
+ b.insertAdjacentHTML('beforeend', svg(ATT_ICON[a.kind] || 'file'));
+ chips.appendChild(b);
+ }
+ b.appendChild(el('span', 'ovas-att-cap', a.name));
+ b.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); onOpen(a); });
+ }
+ if (imgs.childNodes.length) strip.appendChild(imgs);
+ if (chips.childNodes.length) strip.appendChild(chips);
+ return strip;
+ }
+
+ /** A text file as the preview shows it: a CSV as a table, JSON pretty-printed, Markdown read, code highlighted. */
+ function attText(name, text, linkOk) {
+ const ext = attExt(name);
+ let body = String(text == null ? '' : text);
+ if (ext === 'json' || ext === 'jsonl') { try { body = JSON.stringify(JSON.parse(body), null, 2); } catch { /* not one document */ } }
+ if (ext === 'csv' || ext === 'tsv') { const table = attCsv(body, ext === 'tsv' ? '\t' : ','); if (table) return table; }
+ if (ext === 'md' || ext === 'markdown') {
+ const frag = markdown(body, linkOk);
+ frag.querySelectorAll('pre code').forEach((code) => { const hi = attHighlight(code.textContent, ''); code.textContent = ''; code.appendChild(hi); });
+ return frag;
+ }
+ const pre = el('pre', 'ovas-prev-code');
+ const code = el('code');
+ code.appendChild(attHighlight(body, ext === 'json' ? 'json' : ''));
+ pre.appendChild(code);
+ return pre;
+ }
+ /** Pinch to zoom on a phone, double tap to double, so a screenshot can be read. */
+ function attZoom(img) {
+ let scale = 1, start = null;
+ const apply = () => { img.style.transform = scale > 1 ? 'scale(' + scale + ')' : ''; };
+ img.addEventListener('touchstart', (e) => {
+ if (e.touches.length === 2) { const a = e.touches[0], b = e.touches[1]; start = { d: Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY), s: scale }; }
+ }, { passive: true });
+ img.addEventListener('touchmove', (e) => {
+ if (!start || e.touches.length !== 2) return;
+ e.preventDefault();
+ const a = e.touches[0], b = e.touches[1];
+ const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
+ scale = Math.min(4, Math.max(1, start.s * (d / (start.d || 1)))); apply();
+ }, { passive: false });
+ img.addEventListener('touchend', () => { start = null; });
+ img.addEventListener('dblclick', () => { scale = scale > 1 ? 1 : 2; apply(); });
+ img.addEventListener('wheel', (e) => { if (!e.ctrlKey) return; e.preventDefault(); scale = Math.min(4, Math.max(1, scale - e.deltaY / 200)); apply(); }, { passive: false });
+ }
+ /** A small highlighter for the preview: strings, numbers, keywords and comments, as spans (never markup). */
+ function attHighlight(text, lang) {
+ const frag = document.createDocumentFragment();
+ const json = lang === 'json';
+ const re = json
+ ? /("(?:[^"\\]|\\.)*")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g
+ : /(\/\/[^\n]*|#[^\n]*|--[^\n]*)|("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|\b(const|let|var|function|return|if|else|for|while|do|import|export|from|default|class|extends|new|await|async|try|catch|finally|throw|def|then|null|true|false|None|True|False)\b|(\b\d+(?:\.\d+)?\b)/g;
+ const push = (s, cls) => { if (!s) return; frag.appendChild(cls ? el('span', cls, s) : document.createTextNode(s)); };
+ let at = 0, m;
+ while ((m = re.exec(text))) {
+ push(text.slice(at, m.index));
+ if (json) {
+ if (m[1] != null) { push(m[1], m[2] != null ? 'ovas-tok-key' : 'ovas-tok-str'); if (m[2] != null) push(m[2], ''); }
+ else if (m[3] != null) push(m[3], 'ovas-tok-kw');
+ else push(m[4], 'ovas-tok-num');
+ } else if (m[1] != null) push(m[1], 'ovas-tok-com');
+ else if (m[2] != null) push(m[2], 'ovas-tok-str');
+ else if (m[3] != null) push(m[3], 'ovas-tok-kw');
+ else push(m[4], 'ovas-tok-num');
+ at = m.index + m[0].length;
+ }
+ push(text.slice(at));
+ return frag;
+ }
+ /** A CSV as a table (quoted cells kept whole). */
+ function attCsv(text, delim) {
+ const rows = String(text || '').split(/\r?\n/).filter((l) => l.trim() !== '');
+ if (!rows.length) return null;
+ const split = (line) => {
+ const out = []; let cur = '', q = false;
+ for (let i = 0; i < line.length; i++) {
+ const ch = line[i];
+ if (q) { if (ch === '"') { if (line[i + 1] === '"') { cur += '"'; i++; } else q = false; } else cur += ch; }
+ else if (ch === '"') q = true;
+ else if (ch === delim) { out.push(cur.trim()); cur = ''; }
+ else cur += ch;
+ }
+ out.push(cur.trim());
+ return out;
+ };
+ const head = split(rows[0]);
+ const table = el('table', 'ovas-csv');
+ const thead = el('thead'), tr = el('tr');
+ head.forEach((h) => tr.appendChild(el('th', '', h || ' ')));
+ thead.appendChild(tr); table.appendChild(thead);
+ const tb = el('tbody');
+ for (const line of rows.slice(1, 200)) {
+ const r = el('tr'), cells = split(line);
+ for (let i = 0; i < head.length; i++) r.appendChild(el('td', '', cells[i] == null ? '' : cells[i]));
+ tb.appendChild(r);
+ }
+ table.appendChild(tb);
+ return table;
+ }
+
+ /** Light Markdown, text-safe: paragraphs, bullet and numbered lists, `code`, ```blocks```, **bold**, https links (only
+ * those `linkOk(url)` allows; the others stay text). */
+ function markdown(text, linkOk) {
+ const frag = document.createDocumentFragment();
+ const inline = (s, into) => {
+ const re = /(`[^`]+`|\*\*[^*]+\*\*|https:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/g;
+ let at = 0, m;
+ while ((m = re.exec(s))) {
+ if (m.index > at) into.appendChild(document.createTextNode(s.slice(at, m.index)));
+ const tok = m[0];
+ if (tok[0] === '`') into.appendChild(el('code', '', tok.slice(1, -1)));
+ else if (tok.startsWith('**')) into.appendChild(el('strong', '', tok.slice(2, -2)));
+ else if (linkOk && !linkOk(tok)) into.appendChild(document.createTextNode(tok));
+ else { const a = el('a', '', tok); a.href = tok; a.target = '_blank'; a.rel = 'noopener noreferrer'; into.appendChild(a); }
+ at = m.index + tok.length;
+ }
+ if (at < s.length) into.appendChild(document.createTextNode(s.slice(at)));
+ };
+ const blocks = String(text || '').replace(/\r/g, '').split(/\n```/);
+ String(text || '').replace(/\r/g, '').split(/(```[\s\S]*?(?:```|$))/).forEach((part) => {
+ if (!part) return;
+ if (part.startsWith('```')) { const pre = el('pre', 'ovas-code'); pre.appendChild(el('code', '', part.replace(/^```[^\n]*\n?/, '').replace(/```$/, ''))); frag.appendChild(pre); return; }
+ part.split(/\n{2,}/).forEach((para) => {
+ const lines = para.split('\n').filter((l) => l.trim());
+ if (!lines.length) return;
+ if (lines.every((l) => /^\s*([-*•]|\d+[.)])\s+/.test(l))) {
+ const ol = /^\s*\d/.test(lines[0]);
+ const list = el(ol ? 'ol' : 'ul');
+ lines.forEach((l) => { const li = el('li'); inline(l.replace(/^\s*([-*•]|\d+[.)])\s+/, ''), li); list.appendChild(li); });
+ frag.appendChild(list);
+ } else { const p = el('p'); lines.forEach((l, i) => { if (i) p.appendChild(el('br')); inline(l.replace(/^#{1,6}\s+/, ''), p); }); frag.appendChild(p); }
+ });
+ });
+ void blocks;
+ return frag;
+ }
+
+ /** A line diff of two texts (LCS for small inputs, both sides whole beyond): [['=', line] | ['-', line] | ['+', line]]. */
+ function diffLines(a, b) {
+ const x = String(a || '').split('\n'), y = String(b || '').split('\n');
+ if (x.length * y.length > 90000) return x.map((l) => ['-', l]).concat(y.map((l) => ['+', l]));
+ const n = x.length, m = y.length, L = Array.from({ length: n + 1 }, () => new Uint16Array(m + 1));
+ for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) L[i][j] = x[i] === y[j] ? L[i + 1][j + 1] + 1 : Math.max(L[i + 1][j], L[i][j + 1]);
+ const out = []; let i = 0, j = 0;
+ while (i < n && j < m) { if (x[i] === y[j]) { out.push(['=', x[i]]); i++; j++; } else if (L[i + 1][j] >= L[i][j + 1]) out.push(['-', x[i++]]); else out.push(['+', y[j++]]); }
+ while (i < n) out.push(['-', x[i++]]);
+ while (j < m) out.push(['+', y[j++]]);
+ return out;
+ }
+
+ /** What a step shows on its row and inside: title, the thing it touched, a diff or content, the counts. */
+ function describe(step) {
+ const i = step.input || {};
+ const k = step.kind;
+ const out = { title: '', sub: '', command: '', diff: null, preview: '', added: 0, removed: 0 };
+ if (k === 'command') { out.command = String(i.command || i.cmd || step.what || ''); out.title = String(i.description || '') || out.command.split('\n')[0]; out.sub = i.description ? out.command.split('\n')[0] : ''; }
+ else if (k === 'read') { const p = i.file_path || i.path || step.what; out.title = `Read ${base(p)}`; out.sub = String(p || ''); }
+ else if (k === 'search') { out.title = i.pattern ? `Searched for ${i.pattern}` : `Listed ${base(i.path || step.what) || 'files'}`; out.sub = String(i.path || i.glob || step.what || ''); }
+ else if (k === 'edit' || k === 'write') {
+ const p = i.file_path || i.path || i.notebook_path || step.what;
+ out.title = `${k === 'write' ? 'Wrote' : 'Edited'} ${base(p) || 'a file'}`; out.sub = String(p || '');
+ const edits = Array.isArray(i.edits) ? i.edits : (i.old_string != null || i.new_string != null) ? [{ old_string: i.old_string, new_string: i.new_string }] : [];
+ if (edits.length) { out.diff = []; edits.forEach((e, n) => { if (n) out.diff.push(['@', '…']); out.diff.push(...diffLines(e.old_string, e.new_string)); }); }
+ else if (typeof i.content === 'string') { out.preview = i.content; out.diff = i.content.split('\n').map((l) => ['+', l]); }
+ if (out.diff) out.diff.forEach(([op]) => { if (op === '+') out.added++; else if (op === '-') out.removed++; });
+ } else if (k === 'web') {
+ const asked = i.query || (!i.url && !/^https?:\/\//.test(String(i.description || step.what || '')) ? i.description || step.what : '');
+ const url = i.url || (/^https?:\/\//.test(String(i.description || step.what || '')) ? i.description || step.what : '');
+ out.title = asked ? `Looked up ${asked}` : `Opened ${String(url).replace(/^https?:\/\//, '').slice(0, 60)}`; out.sub = asked ? '' : String(url);
+ }
+ else if (k === 'agent') { out.title = String(i.description || 'A helper agent'); out.sub = i.subagent_type ? `${i.subagent_type} helper` : 'helper'; out.preview = String(i.prompt || ''); }
+ else if (k === 'plan') { out.title = 'Updated the plan'; }
+ else { out.title = String(step.name || 'Tool'); out.sub = String(step.what || ''); }
+ if (!out.title) out.title = String(step.what || step.name || 'Step');
+ if (step.title) out.title = String(step.title); // the agent's own words for it (AG-UI metadata.ov.title)
+ return out;
+ }
+
+ // ── Styles: agent-session.css, from beside this script (theme tokens; light and dark) ─────────────────
+ const SELF_SRC = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
+ function styles() {
+ if (typeof document === 'undefined' || document.getElementById('openvibe-agent-session-styles') || document.querySelector('link[href*="agent-session.css"]')) return;
+ const m = /^(https?:\/\/[^?#]*\/)agent-session\.js(?:[?#]|$)/.exec(SELF_SRC);
+ const link = document.createElement('link');
+ link.id = 'openvibe-agent-session-styles';
+ link.rel = 'stylesheet';
+ link.href = `${m ? m[1] : 'https://openvibe.network/shared/'}agent-session.css`;
+ document.head.appendChild(link);
+ }
+
+ // ── The view ──────────────────────────────────────────────
+ function mount(host, opts = {}) {
+ styles();
+ const linkOk = typeof opts.link === 'function' ? (u) => { try { return !!opts.link(u); } catch { return false; } } : null;
+ // Attachments load from the product's own origin or Media's; a product that serves files from another host
+ // says so: mount(el, { origins: ['files.openvibe.codes'] }).
+ const origins = Array.isArray(opts.origins) ? opts.origins : (opts.origins ? [opts.origins] : []);
+ const safeAtts = (list) => normAttachments(list).filter((a) => attAllowed(a.url, origins));
+ const LIVE = ['running', 'placed', 'queued', 'verifying'];
+ // The clock runs from startedAt, or from now for a run that is live as it mounts; a finished run replayed without
+ // a start time shows the time only once its done line says it.
+ const clock = Boolean(opts.startedAt) || LIVE.includes(opts.status || 'running');
+ const S = { phases: [], thinks: new Map(), msgs: new Map(), sources: new Map(), activity: '', ownPlan: false, status: opts.status || 'running', label: opts.label || '', startedAt: opts.startedAt || Date.now(), endedAt: null, agents: [], steps: new Map(), plan: null, tokens: { in: 0, out: 0 }, cost: 0, agentCost: new Map(), durationMs: 0, problems: 0, filter: 'all', follow: opts.follow !== false, lastGroup: null };
+ host.textContent = '';
+ const box = el('div', 'ovas');
+ box.innerHTML = `<div class="ovas-head"><div class="ovas-top"><span class="ovas-pill" data-s="${esc(S.status)}"></span><span class="ovas-title"></span></div><div class="ovas-meta"><span class="ovas-agents"></span><span data-m="now" hidden></span><span data-m="time"></span><span data-m="steps"></span><span data-m="files"></span><span data-m="cost"></span></div><details class="ovas-plan" hidden><summary><span class="ovas-plan-now"></span><span class="ovas-plan-n"></span></summary><div class="ovas-plan-bar"><i style="width:0"></i></div><ol></ol></details></div>`
+ + '<div class="ovas-bar" role="toolbar" aria-label="Show"></div><ol class="ovas-files" hidden></ol><ol class="ovas-feed" aria-live="polite"></ol><div class="ovas-actions" hidden></div>';
+ host.appendChild(box);
+ const head = box.querySelector('.ovas-head'), feed = box.querySelector('.ovas-feed'), bar = box.querySelector('.ovas-bar');
+ const filesEl = box.querySelector('.ovas-files'), actBar = box.querySelector('.ovas-actions');
+ // The sources strip (protocol 1.2) under the feed; card mode (opts.mode 'card': a chat message, the Dock, a feed card)
+ // keeps the head, the plan row, the newest three lines and the sources, with a way to the whole run (opts.href).
+ const srcBox = el('details', 'ovas-sources'); srcBox.hidden = true;
+ srcBox.innerHTML = '<summary><span class="ovas-src-tiles"></span><span class="ovas-src-n"></span></summary><ol></ol>';
+ box.insertBefore(srcBox, actBar);
+ if (opts.mode === 'card') {
+ box.classList.add('ovas--card');
+ if (typeof opts.href === 'string' && /^(https:\/\/|\/)/.test(opts.href)) { const more = el('a', 'ovas-open', 'Open the whole run'); more.href = opts.href; box.insertBefore(more, actBar); }
+ }
+ head.querySelector('.ovas-title').textContent = opts.title || 'Agent session';
+ const FILTERS = [['all', 'Everything'], ['said', 'What it said'], ['steps', 'Steps'], ['files', 'Files'], ['problems', 'Problems']];
+ FILTERS.forEach(([id, label]) => { const b = el('button', 'ovas-chip', label); b.type = 'button'; b.dataset.f = id; b.setAttribute('aria-pressed', String(id === 'all')); bar.appendChild(b); });
+ const follow = el('button', 'ovas-chip ovas-follow', 'Follow'); follow.type = 'button'; follow.setAttribute('aria-pressed', String(S.follow)); follow.title = 'Keep the newest step in view'; bar.appendChild(follow);
+ bar.addEventListener('click', (e) => {
+ const b = e.target.closest('button'); if (!b) return;
+ if (b === follow) { S.follow = !S.follow; follow.setAttribute('aria-pressed', String(S.follow)); return; }
+ S.filter = b.dataset.f; bar.querySelectorAll('[data-f]').forEach((x) => x.setAttribute('aria-pressed', String(x === b))); applyFilter();
+ });
+ box.addEventListener('click', (e) => {
+ const c = e.target.closest('.ovas-copy'); if (!c) return;
+ e.preventDefault(); e.stopPropagation();
+ const pre = c.parentElement && c.parentElement.nextElementSibling;
+ const text = pre ? pre.textContent : '';
+ try { navigator.clipboard.writeText(text); c.lastChild.textContent = 'Copied'; setTimeout(() => { c.lastChild.textContent = 'Copy'; }, 1400); } catch { /* no clipboard */ }
+ });
+ const empty = el('li', 'ovas-empty', opts.empty || 'Waiting for the agent to start…'); feed.appendChild(empty);
+ // The run is still working: three dots after the newest line (gone when it ends).
+ const working = el('div', 'ovas-working'); working.setAttribute('aria-hidden', 'true'); working.innerHTML = '<i></i><i></i><i></i>';
+ working.hidden = !LIVE.includes(S.status); box.insertBefore(working, box.querySelector('.ovas-actions'));
+
+ // ── The preview sheet: one attachment, opened by a tap ──────────────────────────
+ const sheet = el('div', 'ovas-sheet');
+ sheet.hidden = true;
+ sheet.innerHTML = '<div class="ovas-sheet-panel" role="dialog" aria-modal="true" aria-label="Preview"><div class="ovas-sheet-bar"><b class="ovas-sheet-name"></b><button type="button" class="ovas-sheet-x" aria-label="Close the preview">✕</button></div><div class="ovas-sheet-body"></div><div class="ovas-sheet-foot"><a class="ovas-sheet-open" target="_blank" rel="noopener noreferrer">Open</a><a class="ovas-sheet-dl" download>Download</a></div></div>';
+ host.appendChild(sheet);
+ let sheetBack = null;
+ function closeSheet() {
+ if (sheet.hidden) return;
+ sheet.hidden = true;
+ sheet.querySelector('.ovas-sheet-body').textContent = '';
+ if (sheetBack && sheetBack.focus) sheetBack.focus();
+ sheetBack = null;
+ }
+ function openSheet(a) {
+ const body = sheet.querySelector('.ovas-sheet-body');
+ body.textContent = '';
+ sheet.querySelector('.ovas-sheet-name').textContent = a.name;
+ const open = sheet.querySelector('.ovas-sheet-open'); open.href = a.url; open.setAttribute('aria-label', 'Open ' + a.name);
+ const dl = sheet.querySelector('.ovas-sheet-dl'); dl.href = a.url; dl.setAttribute('download', a.name);
+ const wrap = el('div', 'ovas-prev');
+ if (a.kind === 'image') {
+ const img = el('img', 'ovas-prev-img'); img.alt = a.name; img.src = a.url; wrap.appendChild(img); attZoom(img);
+ } else if (a.kind === 'video') {
+ const v = el('video', 'ovas-prev-media'); v.controls = true; v.playsInline = true; v.preload = 'metadata'; v.src = a.url; wrap.appendChild(v);
+ } else if (a.kind === 'audio') {
+ const au = el('audio', 'ovas-prev-audio'); au.controls = true; au.preload = 'metadata'; au.src = a.url; wrap.appendChild(au);
+ } else if (a.kind === 'pdf') {
+ const f = el('iframe', 'ovas-prev-pdf'); f.src = a.url; f.title = a.name; wrap.appendChild(f);
+ } else if (a.kind === 'text') {
+ const holder = el('div', 'ovas-prev-text');
+ holder.appendChild(el('p', 'ovas-prev-note', 'Loading…'));
+ wrap.appendChild(holder);
+ fetch(a.url, { credentials: 'same-origin' })
+ .then((r) => (r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status))))
+ .then((t) => { holder.textContent = ''; holder.appendChild(attText(a.name, t, linkOk)); })
+ .catch((err) => { holder.textContent = 'Could not open it: ' + err.message; });
+ } else {
+ wrap.appendChild(el('p', 'ovas-prev-note', 'There is no preview for this file. Open it or download it.'));
+ }
+ body.appendChild(wrap);
+ sheetBack = (typeof document !== 'undefined' && document.activeElement) || null;
+ sheet.hidden = false;
+ const x = sheet.querySelector('.ovas-sheet-x'); if (x && x.focus) x.focus();
+ }
+ sheet.querySelector('.ovas-sheet-x').addEventListener('click', closeSheet);
+ sheet.addEventListener('click', (e) => { if (e.target === sheet) closeSheet(); });
+ document.addEventListener('keydown', (e) => {
+ if (sheet.hidden) return;
+ if (e.key === 'Escape') { e.preventDefault(); closeSheet(); return; }
+ if (e.key !== 'Tab') return;
+ const f = [...sheet.querySelectorAll('button, a[href], iframe')].filter((n) => !n.disabled && n.offsetParent !== null);
+ if (!f.length) return;
+ const first = f[0], last = f[f.length - 1];
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
+ });
+
+ function applyFilter() {
+ const files = S.filter === 'files';
+ feed.hidden = files; filesEl.hidden = !files;
+ if (files) { paintFiles(); return; }
+ feed.querySelectorAll(':scope > li').forEach((li) => {
+ const k = li.dataset.k || '';
+ li.hidden = S.filter === 'all' ? false : S.filter === 'said' ? !(k === 'msg' || k === 'done' || k === 'handoff') : S.filter === 'steps' ? !(k === 'steps' || k === 'handoff') : !(li.dataset.p === '1');
+ });
+ }
+ // The Files tab: one row per file, newest change last; a tap shows everything and opens that file's last step.
+ function changedFiles() {
+ const by = new Map();
+ for (const st of S.steps.values()) {
+ if (st.hidden || (st.kind !== 'edit' && st.kind !== 'write') || !st.info) continue;
+ const p = st.info.sub || st.info.title;
+ const f = by.get(p) || { path: p, added: 0, removed: 0, n: 0, last: null, wrote: false };
+ f.added += st.info.added || 0; f.removed += st.info.removed || 0; f.n++; f.last = st; f.wrote = f.wrote || st.kind === 'write';
+ by.set(p, f);
+ }
+ return [...by.values()];
+ }
+ function paintFiles() {
+ filesEl.textContent = '';
+ const list = changedFiles();
+ if (!list.length) { filesEl.appendChild(el('li', 'ovas-empty', 'No files changed yet.')); return; }
+ for (const f of list) {
+ const li = el('li');
+ const b = el('button', 'ovas-file'); b.type = 'button'; b.dataset.step = f.last.id;
+ b.innerHTML = `<span class="ovas-kind" data-k="${f.wrote ? 'write' : 'edit'}">${svg(f.wrote ? 'write' : 'edit')}</span><span class="ovas-sum"><b></b><small></small></span><span class="ovas-delta"><span class="a">+${f.added}</span><span class="r">−${f.removed}</span></span><span class="ovas-file-n"></span>`;
+ b.querySelector('b').textContent = base(f.path) || f.path;
+ b.querySelector('small').textContent = f.path;
+ b.querySelector('.ovas-file-n').textContent = f.n > 1 ? `${f.n} changes` : '';
+ li.appendChild(b); filesEl.appendChild(li);
+ }
+ }
+ filesEl.addEventListener('click', (e) => {
+ const b = e.target.closest('.ovas-file'); if (!b) return;
+ const st = S.steps.get(b.dataset.step); if (!st || !st.el) return;
+ S.filter = 'all'; bar.querySelectorAll('[data-f]').forEach((x) => x.setAttribute('aria-pressed', String(x.dataset.f === 'all'))); applyFilter();
+ for (let p = st.el; p; p = p.parentElement) if (p.tagName === 'DETAILS') p.open = true;
+ if (st.el.scrollIntoView) st.el.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ });
+
+ // The action bar: what the person can do now (the page decides; this only draws and reports the press).
+ function paintActions(list, note) {
+ actBar.textContent = '';
+ const acts = (Array.isArray(list) ? list : []).filter((a) => a && a.id && a.label).slice(0, 4);
+ actBar.hidden = !acts.length;
+ if (!acts.length) return;
+ if (note) actBar.appendChild(el('p', '', note));
+ for (const a of acts) { const b = el('button', `ovas-act${a.kind === 'primary' ? ' is-primary' : a.kind === 'danger' ? ' is-danger' : ''}`, a.label); b.type = 'button'; b.dataset.act = a.id; actBar.appendChild(b); }
+ }
+ actBar.addEventListener('click', async (e) => {
+ const b = e.target.closest('.ovas-act'); if (!b || typeof opts.onAction !== 'function') return;
+ const all = [...actBar.querySelectorAll('.ovas-act')]; all.forEach((x) => { x.disabled = true; });
+ try { await opts.onAction(b.dataset.act); } catch { /* the page says what went wrong */ } finally { all.forEach((x) => { x.disabled = false; }); }
+ });
+ if (Array.isArray(opts.actions)) paintActions(opts.actions, opts.actionsNote);
+
+ const keep = () => { if (S.follow && S.status === 'running') { const last = feed.lastElementChild; if (last && last.scrollIntoView) last.scrollIntoView({ block: 'end', behavior: 'smooth' }); } };
+
+ // A step's element: <details> with the summary row and, once opened, its detail.
+ function stepEl(step) {
+ const d = el('details', 'ovas-step');
+ d.dataset.id = step.id;
+ const sum = el('summary');
+ const info = describe(step); step.info = info;
+ sum.innerHTML = `${svg('chevron', 'ovas-chev')}<span class="ovas-kind" data-k="${esc(step.kind)}">${svg(step.kind)}</span><span class="ovas-sum"><b></b><small></small></span>${info.added || info.removed ? `<span class="ovas-delta"><span class="a">+${info.added}</span><span class="r">−${info.removed}</span></span>` : ''}<span class="ovas-state" data-s="run"><span class="ovas-spin" aria-label="Running"></span><span data-t></span></span><span class="ovas-att-slot"></span>`;
+ sum.querySelector('b').textContent = info.title;
+ sum.querySelector('small').textContent = info.sub;
+ if (!info.sub) sum.querySelector('small').hidden = true;
+ d.appendChild(sum);
+ const detail = el('div', 'ovas-detail'); d.appendChild(detail);
+ const section = (label, node, copy) => { const l = el('div', 'ovas-label'); l.appendChild(el('span', '', label)); if (copy) { const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); } detail.append(l, node); };
+ if (step.kind === 'command' && info.command) { const pre = el('pre', 'ovas-code'); pre.textContent = info.command; section('Command', pre, true); }
+ if (info.diff) { const pre = el('pre', 'ovas-diff'); info.diff.slice(0, 600).forEach(([op, line]) => { const s = el('span', op === '+' ? 'a' : op === '-' ? 'r' : op === '@' ? 'h' : '', `${op === '=' ? ' ' : op === '@' ? '' : op} ${line}`); pre.appendChild(s); }); section(step.kind === 'write' ? 'New file' : 'Change', pre, false); }
+ if (step.kind === 'agent' && info.preview) { const p = el('div', 'ovas-msg'); p.appendChild(markdown(info.preview.slice(0, 1600), linkOk)); section('Asked to', p, false); }
+ if (step.kind !== 'plan' && step.input && typeof step.input === 'object' && Object.keys(step.input).length) {
+ const raw = el('details', 'ovas-raw');
+ raw.innerHTML = `<summary>${svg('chevron', 'ovas-chev')}<span>Input</span></summary>`;
+ const l = el('div', 'ovas-label'); l.appendChild(el('span', '', 'What the agent sent')); const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c);
+ let text = ''; try { text = JSON.stringify(step.input, null, 2); } catch { text = String(step.input); }
+ const pre = el('pre', 'ovas-code'); pre.textContent = text.length > 8000 ? `${text.slice(0, 8000)}\n…` : text;
+ raw.append(l, pre); detail.appendChild(raw);
+ }
+ if (step.kind === 'agent') { const sub = el('div', 'ovas-sub'); step.subEl = sub; d.appendChild(sub); d.open = true; }
+ if (step.kind !== 'agent' && step.kind !== 'plan') {
+ const pre = el('pre', 'ovas-out'); pre.hidden = true; step.outEl = pre;
+ const wait = el('div', 'ovas-wait'); wait.innerHTML = '<span class="ovas-spin"></span><span>Running — the output shows here as it arrives</span>'; step.waitEl = wait;
+ const l = el('div', 'ovas-label'); l.appendChild(el('span', '', 'Output')); const c = el('button', 'ovas-copy'); c.type = 'button'; c.innerHTML = `${svg('copy')}<span>Copy</span>`; l.appendChild(c); step.outLabel = l;
+ detail.append(l, pre, wait);
+ }
+ step.el = d; step.stateEl = sum.querySelector('.ovas-state');
+ return d;
+ }
+ function setStepState(step) {
+ const st = step.stateEl; if (!st) return;
+ const t = step.ended ? dur(step.ended - step.started) : dur(Date.now() - step.started);
+ if (step.done) {
+ st.dataset.s = step.ok ? 'ok' : 'fail';
+ const c = step.cost > 0 ? `$${step.cost < 0.01 ? step.cost.toFixed(4).replace(/0+$/, '') : step.cost.toFixed(2)}` : '';
+ st.innerHTML = `${c ? `<span class="ovas-cost">${esc(c)}</span>` : ''}${svg(step.ok ? 'ok' : 'fail')}<span>${esc(step.ended - step.started > 1500 ? t : '')}</span>`;
+ } else { const tt = st.querySelector('[data-t]'); if (tt && Date.now() - step.started > 2000) tt.textContent = t; }
+ }
+ // The group a new step goes in: the last item of its feed when that is a group of steps, else a new one.
+ function groupFor(parentFeed) {
+ const last = parentFeed.lastElementChild;
+ if (last && last.classList.contains('ovas-group-li')) return last;
+ const li = el('li', 'ovas-group-li'); li.dataset.k = 'steps';
+ const g = el('details', 'ovas-group');
+ g.innerHTML = `<summary>${svg('chevron', 'ovas-chev')}<span class="ovas-icons"></span><span class="ovas-sum"><b></b><small></small></span><span class="ovas-delta" hidden><span class="a"></span><span class="r"></span></span><span class="ovas-state"></span><span class="ovas-att-slot"></span></summary><ol></ol>`;
+ li.appendChild(g); parentFeed.appendChild(li);
+ li.steps = [];
+ return li;
+ }
+ function paintGroup(li) {
+ const steps = li.steps, g = li.firstChild, count = {};
+ steps.forEach((s) => { count[s.kind] = (count[s.kind] || 0) + 1; });
+ const order = ['command', 'edit', 'write', 'read', 'search', 'web', 'agent', 'plan', 'tool'];
+ const kinds = order.filter((k) => count[k]);
+ g.querySelector('.ovas-icons').innerHTML = kinds.slice(0, 3).map((k) => `<span class="ovas-kind" data-k="${k}">${svg(k)}</span>`).join('');
+ // A few steps read as what was done ("Ran 2 commands"); more as counts ("2 commands · 1 edit · 3 reads").
+ // Steps the agent named itself (AG-UI metadata.ov.title) read as its words: the newest title and how many there are.
+ const titled = steps.filter((s2) => s2.title);
+ // Rows under one shared title say what each one did instead of the title again: its address, query or file.
+ const sharedTitle = titled.length > 1 && titled.every((s2) => s2.title === titled[0].title);
+ for (const s2 of steps) {
+ const b2 = s2.el && s2.el.querySelector(':scope>summary .ovas-sum b');
+ if (!b2 || !s2.info) continue;
+ // An address reads as its page and its site ("OpenVibe.Codes · github.com"), anything else as it is.
+ let own = String(s2.info.sub || '');
+ try { if (/^https?:\/\//.test(own)) { const u = new URL(own); const leaf = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || ''); own = leaf ? `${leaf} · ${u.hostname.replace(/^www\./, '')}` : u.hostname.replace(/^www\./, ''); } } catch { /* as it is */ }
+ b2.textContent = sharedTitle && own ? own : s2.info.title;
+ }
+ g.querySelector('.ovas-sum b').textContent = titled.length
+ ? `${titled[titled.length - 1].title}${steps.length > 1 ? ` · ${steps.length}` : ''}`
+ : kinds.length === 1 ? plural(kinds[0], count[kinds[0]]) : kinds.map((k) => `${count[k]} ${NOUN[k][count[k] === 1 ? 0 : 1]}`).join(' · ');
+ const running = steps.find((s) => !s.done) || steps[steps.length - 1];
+ const small = g.querySelector('.ovas-sum small');
+ small.textContent = running ? (running.info.sub || running.info.title) : '';
+ const added = steps.reduce((n, s) => n + (s.info.added || 0), 0), removed = steps.reduce((n, s) => n + (s.info.removed || 0), 0);
+ const delta = g.querySelector('.ovas-delta'); delta.hidden = !(added || removed); delta.firstChild.textContent = `+${added}`; delta.lastChild.textContent = `−${removed}`;
+ const st = g.querySelector('.ovas-state'), fails = steps.filter((s) => s.done && !s.ok).length, live = steps.some((s) => !s.done);
+ st.dataset.s = live ? 'run' : fails ? 'fail' : 'ok';
+ st.innerHTML = live ? '<span class="ovas-spin" aria-label="Running"></span>' : fails ? `${svg('fail')}<span>${fails} failed</span>` : svg('ok');
+ if (fails) li.dataset.p = '1';
+ // One step alone reads as itself, not as a group of one.
+ g.classList.toggle('is-one', steps.length === 1);
+ if (steps.length === 1) g.open = true;
+ // The group's attachments, in its header row, so they show while the group is folded.
+ const atts = [], seenAtt = new Set();
+ for (const s of steps) for (const a of (s.attachments || [])) { if (!seenAtt.has(a.url)) { seenAtt.add(a.url); atts.push(a); } }
+ const slot = g.querySelector('.ovas-att-slot');
+ slot.textContent = '';
+ // A group of one hides its header, so there the step's own row carries the attachments instead.
+ if (atts.length && steps.length > 1) slot.appendChild(attStrip(atts, openSheet));
+ // A group of one shows the step's own row; there the attachments belong to the step.
+ for (const s of steps) {
+ const ss = s.el && s.el.querySelector(':scope>summary .ovas-att-slot');
+ if (!ss) continue;
+ ss.textContent = '';
+ if (steps.length === 1 && s.attachments && s.attachments.length) ss.appendChild(attStrip(s.attachments, openSheet));
+ }
+ }
+ function feedFor(parent) { const p = parent && S.steps.get(parent); return p && p.subEl ? p.subEl : feed; }
+ function appendLi(target, li) { if (target === feed) { if (empty.parentNode) empty.remove(); feed.appendChild(li); } else target.appendChild(li); }
+
+ const pending = new Map(); // tool name → ids of its steps still waiting for a result (events without ids)
+ // Any line may carry attachments, and a line of a type this component does not know still shows: a minimal
+ // row with its label or summary, then its attachments (Codes' proof lines, for one).
+ const KNOWN = new Set(['agent', 'text', 'tool', 'tool_progress', 'tool_result', 'handoff', 'note', 'step', 'usage', 'done', 'state',
+ 'phase', 'phases', 'activity', 'thinking', 'thinking_delta', 'text_start', 'text_delta', 'tool_args', 'sources']);
+ function extraLine(x, target) {
+ const li = el('li', 'ovas-extra');
+ li.dataset.k = 'steps'; // it is part of the work, so the Steps filter keeps it
+ const label = String(x.label || x.kind || '');
+ const summary = String(x.summary || x.text || '');
+ const text = label && summary && label !== summary ? label + ' — ' + summary : (summary || label);
+ if (text) li.appendChild(el('span', '', text));
+ const atts = safeAtts(x.attachments);
+ if (atts.length) li.appendChild(attStrip(atts, openSheet));
+ appendLi(target, li);
+ }
+ function onLine(x) {
+ const target = feedFor(x.parent);
+ if (!KNOWN.has(x.t)) { extraLine(x, target); return; }
+ // Who is working: a line that names a different agent than the last moves the head along.
+ // A done line reports an agent's cost and time; it never moves the head.
+ if (x.agent && x.t !== 'agent' && x.t !== 'handoff' && x.t !== 'done' && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.agent)) { S.agents.push({ agent: x.agent }); paintHead(); }
+ if (x.t === 'tool' && !x.id) { x = { ...x, id: `a${S.steps.size + 1}` }; if (!pending.has(x.name)) pending.set(x.name, []); pending.get(x.name).push(x.id); }
+ if (x.t === 'tool_result' && !x.id) { const q = pending.get(x.name); x = { ...x, id: q && q.length ? q.shift() : '' }; }
+ if (x.t === 'state') { status(x.state); return; }
+ if (x.t === 'agent') {
+ const last = S.agents[S.agents.length - 1];
+ if (last && last.agent === x.agent) { if (x.model) last.model = x.model; } else S.agents.push({ agent: x.agent, model: x.model });
+ paintHead(); return;
+ }
+ if (x.t === 'text') {
+ const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; li.appendChild(markdown(x.text, linkOk));
+ const atts = safeAtts(x.attachments);
+ if (atts.length) li.appendChild(attStrip(atts, openSheet));
+ appendLi(target, li); return;
+ }
+ if (x.t === 'tool') {
+ const kind = x.kind && VERB[x.kind] ? x.kind : kindOf(x.name);
+ const step = { id: x.id || `s${S.steps.size}`, name: x.name, kind, input: x.input || {}, what: x.what || '', title: x.title || '', started: Date.now(), done: false, ok: null, out: '', attachments: safeAtts(x.attachments) };
+ if (kind === 'plan') { if (Array.isArray(step.input.todos)) { S.ownPlan = true; S.plan = step.input.todos; paintPlan(); } step.hidden = true; S.steps.set(step.id, step); return; }
+ S.steps.set(step.id, step);
+ const g = groupFor(target === feed ? feed : target);
+ if (target === feed && empty.parentNode) empty.remove();
+ g.steps.push(step);
+ g.firstChild.querySelector('ol').appendChild(stepEl(step));
+ paintGroup(g); step.group = g; paintHead(); return;
+ }
+ if (x.t === 'phase' || x.t === 'phases' || x.t === 'activity') { onPhase(x, target); return; }
+ if (x.t === 'thinking' || x.t === 'thinking_delta') { onThinking(x, target); return; }
+ if (x.t === 'text_start' || x.t === 'text_delta') {
+ let m = S.msgs.get(x.id);
+ if (!m) { const li = el('li', 'ovas-msg'); li.dataset.k = 'msg'; m = { li, text: '' }; S.msgs.set(x.id, m); appendLi(target, li); }
+ if (x.t === 'text_delta') { m.text = (m.text + x.delta).slice(0, MAX_TEXT); m.li.textContent = ''; m.li.appendChild(markdown(m.text, linkOk)); }
+ return;
+ }
+ if (x.t === 'tool_args') {
+ const step = S.steps.get(String(x.id)); if (!step) return;
+ step.argText = (step.argText || '') + x.delta;
+ try { step.input = JSON.parse(step.argText); step.info = describe(step); if (step.group) paintGroup(step.group); } catch { /* not whole yet */ }
+ return;
+ }
+ if (x.t === 'sources') { addSources(x.items); return; }
+ if (x.t === 'tool_progress') {
+ const step = S.steps.get(String(x.id)); if (!step || !step.outEl) return;
+ step.out = (step.out + x.chunk).slice(-20000); step.outEl.textContent = step.out; step.outEl.hidden = false; step.waitEl.hidden = true;
+ step.outEl.scrollTop = step.outEl.scrollHeight; return;
+ }
+ if (x.t === 'tool_result') {
+ const step = S.steps.get(String(x.id)); if (!step || step.hidden) return;
+ // A result may carry the files the step produced; the group's header shows them.
+ const extra = safeAtts(x.attachments);
+ if (extra.length) {
+ const have = step.attachments || (step.attachments = []);
+ for (const a of extra) if (!have.some((b) => b.url === a.url)) have.push(a);
+ }
+ step.done = true; step.ok = x.ok !== false; step.ended = x.duration_ms > 0 ? step.started + Number(x.duration_ms) : Date.now();
+ if (Number(x.cost_usd) > 0) step.cost = Number(x.cost_usd);
+ if (step.outEl) {
+ const out = String(x.output == null ? '' : x.output) || step.out || '';
+ step.waitEl.hidden = true;
+ if (out.trim()) { step.outEl.textContent = out; step.outEl.hidden = false; step.outEl.classList.toggle('is-fail', !step.ok); }
+ else { step.outEl.hidden = true; step.outLabel.hidden = true; }
+ }
+ if (!step.ok) { S.problems++; if (step.el) step.el.open = true; }
+ setStepState(step); if (step.group) paintGroup(step.group); paintHead(); return;
+ }
+ if (x.t === 'handoff') {
+ const li = el('li', 'ovas-handoff'); li.dataset.k = 'handoff'; li.dataset.p = '1';
+ const s = el('div', 'ovas-hand');
+ const row = el('div', 'ovas-hand-row');
+ const from = x.from || (S.agents.length ? S.agents[S.agents.length - 1].agent : '');
+ if (x.to && (!S.agents.length || S.agents[S.agents.length - 1].agent !== x.to)) S.agents.push({ agent: x.to });
+ row.appendChild(el('span', 'ovas-agent', agentName(from))); row.insertAdjacentHTML('beforeend', svg('handoff')); row.appendChild(el('span', 'ovas-agent is-now', x.to ? agentName(x.to) : 'the next agent'));
+ s.appendChild(row); if (x.reason) s.appendChild(el('small', '', x.reason));
+ const handAtts = safeAtts(x.attachments);
+ if (handAtts.length) s.appendChild(attStrip(handAtts, openSheet));
+ li.appendChild(s); appendLi(feed, li); S.problems++; paintHead(); return;
+ }
+ if (x.t === 'note') {
+ const li = el('li', 'ovas-note'); li.dataset.k = 'note'; li.dataset.level = x.level === 'warning' || x.level === 'info' ? x.level : 'error';
+ if (li.dataset.level !== 'info') li.dataset.p = '1';
+ li.innerHTML = svg(li.dataset.level === 'info' ? 'ok' : 'warn'); li.appendChild(el('span', '', x.text));
+ const noteAtts = safeAtts(x.attachments);
+ if (noteAtts.length) li.appendChild(attStrip(noteAtts, openSheet));
+ appendLi(target, li); if (li.dataset.level !== 'info') S.problems++; paintHead(); return;
+ }
+ if (x.t === 'step') {
+ const li = el('li', 'ovas-line'); li.dataset.k = 'line'; li.appendChild(el('span', '', x.text));
+ const lineAtts = safeAtts(x.attachments);
+ if (lineAtts.length) li.appendChild(attStrip(lineAtts, openSheet));
+ appendLi(feed, li); return;
+ }
+ if (x.t === 'usage') { S.tokens.in += Number(x.input_tokens) || 0; S.tokens.out += Number(x.output_tokens) || 0; return; }
+ if (x.t === 'done') {
+ if (x.parent) return;
+ S.cost += Number(x.cost_usd) || 0; S.durationMs += Number(x.duration_ms) || 0;
+ const who = x.agent || (S.agents[S.agents.length - 1] || {}).agent;
+ if (who && Number(x.cost_usd) > 0) S.agentCost.set(who, (S.agentCost.get(who) || 0) + Number(x.cost_usd));
+ const li = el('li', 'ovas-done'); li.dataset.k = 'done'; li.dataset.ok = x.ok ? '1' : '0';
+ const b = el('b'); b.innerHTML = svg(x.ok ? 'ok' : 'fail'); b.appendChild(document.createTextNode(`${agentName((S.agents[S.agents.length - 1] || {}).agent)} ${x.ok ? 'finished' : 'stopped'}${x.duration_ms ? ` in ${dur(x.duration_ms)}` : ''}`));
+ li.appendChild(b);
+ if (x.text) { const m = el('div', 'ovas-msg'); m.appendChild(markdown(x.text, linkOk)); li.appendChild(m); }
+ const doneAtts = safeAtts(x.attachments);
+ if (doneAtts.length) li.appendChild(attStrip(doneAtts, openSheet));
+ if (!x.ok) li.dataset.p = '1';
+ appendLi(feed, li); paintHead();
+ }
+ }
+ // Phases: named parts of the run (AG-UI steps). The plan row in the head shows where the run is ("2 of 5"), and a
+ // phase that starts is a divider in the feed. A plan the agent keeps itself (TodoWrite) wins over phases.
+ function onPhase(x, target) {
+ if (x.t === 'activity') { S.activity = x.text; paintPlan(); return; }
+ if (x.t === 'phases') { S.phases = x.items.map((p, i) => ({ id: `p${i}`, title: p.title, status: p.status })); }
+ else {
+ const same = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
+ let p = S.phases.find((q) => q.id === x.id) || S.phases.find((q) => same(q.title, x.title || x.id));
+ if (!p) { p = { id: x.id, title: x.title || x.id, status: 'running' }; S.phases.push(p); }
+ if (x.title) p.title = x.title;
+ p.status = x.status;
+ if (x.status === 'running' && !p.li) {
+ const li = el('li', 'ovas-phase'); li.dataset.k = 'phase';
+ li.appendChild(el('span', 'ovas-phase-dot')); li.appendChild(el('b', '', p.title));
+ p.li = li; appendLi(target === feed ? feed : target, li);
+ }
+ if (p.li) { p.li.dataset.s = p.status; p.li.querySelector('b').textContent = p.title; }
+ }
+ if (!S.ownPlan) {
+ S.plan = S.phases.map((p) => ({ content: p.title, status: /done|complete/.test(p.status) ? 'completed' : /run|progress/.test(p.status) ? 'in_progress' : 'pending' }));
+ paintPlan();
+ }
+ }
+ // Thinking: folded to one line, "Thinking…" while it streams and "Thought for 1m 20s" after; its words open on a tap.
+ function onThinking(x, target) {
+ let th = S.thinks.get(x.id);
+ if (!th) {
+ const li = el('li', 'ovas-think'); li.dataset.k = 'msg';
+ const d = el('details'); const sm = el('summary'); sm.appendChild(el('span', 'ovas-think-dots'));
+ const label = el('span', '', 'Thinking…'); sm.appendChild(label); d.appendChild(sm);
+ const body = el('div', 'ovas-think-body'); d.appendChild(body); li.appendChild(d);
+ th = { li, label, body, text: '', started: Date.now() }; S.thinks.set(x.id, th); appendLi(target, li);
+ }
+ if (x.t === 'thinking_delta') { th.text = (th.text + x.delta).slice(-20000); th.body.textContent = th.text; }
+ if (x.hidden) th.body.textContent = 'This model keeps its thinking to itself.';
+ if (x.done || x.hidden) { th.li.classList.add('is-done'); th.label.textContent = `Thought for ${dur(x.duration_ms || Date.now() - th.started)}`; }
+ }
+ // Sources: every page the run read, once each, in one strip under the feed: the sites as letter tiles and a count;
+ // opened, the titles as links (https only, a new tab).
+ function addSources(items) {
+ for (const it of (items || []).slice(0, 200)) {
+ const url = it && String(it.url || '');
+ if (!/^https:\/\//.test(url) || S.sources.has(url)) continue;
+ let site = String(it.site || ''); try { if (!site) site = new URL(url).hostname.replace(/^www\./, ''); } catch { continue; }
+ S.sources.set(url, { url, site, title: cut(String(it.title || site), 160) });
+ }
+ paintSources();
+ }
+ function paintSources() {
+ const list = [...S.sources.values()];
+ srcBox.hidden = !list.length; if (!list.length) return;
+ const tiles = srcBox.querySelector('.ovas-src-tiles'); tiles.textContent = '';
+ const sites = [...new Set(list.map((s) => s.site))];
+ sites.slice(0, 5).forEach((site) => { const t = el('span', 'ovas-src-tile', site.charAt(0).toUpperCase()); t.title = site; let h = 0; for (const c of site) h = (h * 31 + c.charCodeAt(0)) % 360; t.style.setProperty('--h', String(h)); tiles.appendChild(t); });
+ srcBox.querySelector('.ovas-src-n').textContent = `${list.length} source${list.length === 1 ? '' : 's'}${sites.length > 1 ? ` from ${sites.length} sites` : ''}`;
+ const ol = srcBox.querySelector('ol'); ol.textContent = '';
+ list.forEach((s) => { const li = el('li'); const a = el('a', '', s.title); a.href = s.url; a.target = '_blank'; a.rel = 'noopener noreferrer'; li.appendChild(a); li.appendChild(el('small', '', s.site)); ol.appendChild(li); });
+ }
+ function push(event) { normalize(event).forEach((x) => { try { onLine(x); } catch { /* one odd line never stops the view */ } }); applyFilter(); keep(); }
+
+ function paintPlan() {
+ const box2 = head.querySelector('.ovas-plan'); const todos = (S.plan || []).filter((t) => t && (t.content || t.activeForm));
+ if (!todos.length && S.activity) { box2.hidden = false; box2.querySelector('.ovas-plan-now').textContent = S.activity; box2.querySelector('.ovas-plan-n').textContent = ''; box2.querySelector('i').style.width = '0%'; return; }
+ box2.hidden = !todos.length; if (!todos.length) return;
+ const doneN = todos.filter((t) => t.status === 'completed').length;
+ box2.querySelector('i').style.width = `${Math.round((doneN / todos.length) * 100)}%`;
+ const now = todos.find((t) => t.status === 'in_progress') || todos.find((t) => t.status !== 'completed');
+ box2.querySelector('.ovas-plan-now').textContent = now ? (now.status === 'in_progress' && now.activeForm ? now.activeForm : now.content || now.activeForm) : 'Plan done';
+ box2.querySelector('.ovas-plan-n').textContent = `${doneN} of ${todos.length}`;
+ const ol = box2.querySelector('ol'); ol.textContent = '';
+ todos.slice(0, 12).forEach((t) => { const li = el('li', '', t.status === 'in_progress' && t.activeForm ? t.activeForm : t.content || t.activeForm); li.dataset.s = t.status || 'pending'; ol.appendChild(li); });
+ }
+ function summary() {
+ const steps = [...S.steps.values()].filter((s) => !s.hidden);
+ const files = new Set(steps.filter((s) => s.kind === 'edit' || s.kind === 'write').map((s) => s.info && s.info.sub).filter(Boolean));
+ return {
+ agents: S.agents.map((a) => agentName(a.agent)), steps: steps.length, commands: steps.filter((s) => s.kind === 'command').length, files: files.size,
+ added: steps.reduce((n, s) => n + ((s.info && s.info.added) || 0), 0), removed: steps.reduce((n, s) => n + ((s.info && s.info.removed) || 0), 0),
+ problems: S.problems, costUsd: Math.round(S.cost * 1e4) / 1e4, costByAgent: Object.fromEntries([...S.agentCost].map(([k, v]) => [agentName(k), Math.round(v * 1e4) / 1e4])), durationMs: S.durationMs || (clock ? (S.endedAt || Date.now()) - S.startedAt : 0), tokens: { ...S.tokens },
+ };
+ }
+ const LABEL = { queued: 'Waiting', placed: 'Starting', running: 'Running', verifying: 'Checking the answer', succeeded: 'Finished', failed: 'Failed', cancelled: 'Cancelled', expired: 'Expired' };
+ function paintHead() {
+ const sm = summary();
+ const pill = head.querySelector('.ovas-pill');
+ pill.dataset.s = S.status;
+ pill.innerHTML = `${S.status === 'running' || S.status === 'placed' || S.status === 'verifying' ? '<span class="ovas-spin"></span>' : S.status === 'succeeded' ? svg('ok') : S.status === 'failed' || S.status === 'expired' ? svg('fail') : ''}<span></span>`;
+ pill.lastChild.textContent = S.label || LABEL[S.status] || S.status;
+ const ag = head.querySelector('.ovas-agents'); ag.textContent = '';
+ const shown = new Set();
+ S.agents.forEach((a, i) => {
+ if (i) ag.appendChild(el('span', '', '→'));
+ const c = el('span', `ovas-agent${i === S.agents.length - 1 ? ' is-now' : ''}`, agentName(a.agent)); if (a.model) c.title = a.model;
+ // An agent's cost once, on its last chip (an agent may come back after a hand-off).
+ const cost = S.agentCost.get(a.agent);
+ if (cost && !shown.has(a.agent) && !S.agents.slice(i + 1).some((b) => b.agent === a.agent)) { shown.add(a.agent); c.appendChild(el('span', 'ovas-agent-cost', `$${cost < 0.01 ? cost.toFixed(4).replace(/0+$/, '') : cost.toFixed(2)}`)); }
+ ag.appendChild(c);
+ });
+ const fc = bar.querySelector('[data-f="files"]'); if (fc) fc.textContent = sm.files ? `Files ${sm.files}` : 'Files';
+ const m = (k, html) => { const n = head.querySelector(`[data-m="${k}"]`); n.innerHTML = html; n.hidden = !html; };
+ // What it is doing now (the portal's sticky line, 2026-11-11): the newest step that has not ended.
+ const nowStep = LIVE.includes(S.status) ? [...S.steps.values()].reverse().find((st) => !st.done && !st.hidden) : null;
+ m('now', nowStep && nowStep.info ? `<span class="ovas-now">${esc(String(nowStep.info.command || nowStep.info.title || '').split('\n')[0].slice(0, 90))}</span>` : '');
+ m('time', sm.durationMs ? `<b>${esc(dur(sm.durationMs))}</b>` : '');
+ m('steps', sm.steps ? `<b>${sm.steps}</b> step${sm.steps === 1 ? '' : 's'}${sm.commands ? ` · <b>${sm.commands}</b> command${sm.commands === 1 ? '' : 's'}` : ''}` : '');
+ m('files', sm.files ? `<b>${sm.files}</b> file${sm.files === 1 ? '' : 's'} <span style="color:var(--ovas-ok)">+${sm.added}</span> <span style="color:var(--ovas-bad)">−${sm.removed}</span>` : '');
+ m('cost', sm.costUsd ? `<b>$${sm.costUsd.toFixed(sm.costUsd < 1 ? 3 : 2)}</b>` : '');
+ }
+ function status(s, label) {
+ S.status = s; S.label = label || '';
+ working.hidden = !LIVE.includes(s);
+ if (!LIVE.includes(s)) { S.endedAt = S.endedAt || Date.now(); empty.textContent = 'Nothing was recorded for this run.'; }
+ paintHead();
+ }
+ // Talking to the run while it works (opts.onSend(text, { files, uploads }) → a promise; the product sends it to its
+ // agent: Codes' job message, Actor's task input, the portal's session). The box is the one composer on every site
+ // (owner, 2026-11-11: "uses our custom modular composer across all sites"): files by picker, paste and drop, "/"
+ // commands and the person's own, drafts, voice and their settings; loaded from beside this file when the page has
+ // not loaded it. opts.composer: false keeps a plain box.
+ if (typeof opts.onSend === 'function') {
+ // The one composer's markup (thread.js composer()): the box, then a row with its tools and Send, then its note,
+ // so ov-composer.js puts its + menu (files), "/" commands, voice and settings where every site has them.
+ const form = el('form', 'ovas-say ovt-composer'); form.setAttribute('data-ovt-composer', '');
+ const ta = el('textarea'); ta.name = 'text'; ta.rows = 1; ta.maxLength = 4000; ta.placeholder = opts.sayPlaceholder || 'Message the agent'; ta.setAttribute('aria-label', 'Message the agent'); ta.setAttribute('data-ovt-text', '');
+ const row = el('div', 'ovt-c-row'); const toolsEl = el('div', 'ovt-c-tools');
+ const send = el('button', 'ovas-say-send ovt-send'); send.type = 'submit'; send.setAttribute('aria-label', 'Send'); send.setAttribute('data-ovt-send', ''); send.innerHTML = svg('handoff');
+ const noteEl = el('p', 'ovt-c-note'); noteEl.setAttribute('data-ovt-note', ''); noteEl.setAttribute('role', 'status');
+ row.appendChild(toolsEl); row.appendChild(send);
+ // A file input makes the box one that takes files (the composer's + menu, paste and drop); the files go to onSend.
+ if (opts.files !== false) {
+ // The shared "+" (thread.js attachHtml): a label holding the file input, first in the tools row.
+ const plus = el('label', 'ovt-btn ovt-attach'); plus.title = 'Attach files';
+ plus.innerHTML = '<svg class="ovt-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span class="ovt-sr">Attach files</span>';
+ const fi = el('input'); fi.type = 'file'; fi.name = 'files'; fi.multiple = true; fi.hidden = true; fi.setAttribute('data-ovt-files', ''); if (opts.accept) fi.accept = String(opts.accept);
+ plus.appendChild(fi); toolsEl.insertBefore(plus, toolsEl.firstChild);
+ }
+ form.appendChild(ta); form.appendChild(row); form.appendChild(noteEl);
+ box.appendChild(form);
+ ta.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit ? form.requestSubmit() : form.dispatchEvent(new Event('submit', { cancelable: true })); } });
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const ovc = form.__ovc;
+ const files = ovc ? ovc.files() : [], uploads = ovc && ovc.uploads ? ovc.uploads() : [];
+ const text = ta.value.trim(); if ((!text && !files.length) || form.classList.contains('is-busy')) return;
+ form.classList.add('is-busy');
+ const li = el('li', 'ovas-you'); li.dataset.k = 'msg'; li.appendChild(el('span', '', text));
+ if (files.length) li.appendChild(el('small', 'ovas-you-files', files.map((f) => f.name || 'a file').join(' · ')));
+ appendLi(feed, li); keep();
+ try { await opts.onSend(text, { files, uploads }); if (ovc && ovc.clear) ovc.clear(); else ta.value = ''; li.classList.add('is-sent'); }
+ catch (err) { li.classList.add('is-failed'); li.title = (err && err.message) || 'Not sent'; }
+ finally { form.classList.remove('is-busy'); }
+ });
+ if (opts.composer !== false) {
+ form.id = form.id || `ovas-say-${Math.random().toString(36).slice(2, 8)}`;
+ const join = () => { try { if (root.OpenVibeComposer && form.isConnected) root.OpenVibeComposer.enhance(form); } catch { /* a plain box still sends */ } };
+ if (root.OpenVibeComposer) join();
+ else {
+ const m = /^(https?:\/\/[^?#]*\/)agent-session(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(SELF_SRC);
+ if (m) {
+ let sc = document.getElementById('ov-composer-script');
+ if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = m[1] + 'ov-composer.js'; document.head.appendChild(sc); }
+ sc.addEventListener('load', join, { once: true });
+ }
+ }
+ }
+ }
+ const tick = setInterval(() => { if (S.status === 'running' || S.status === 'placed') { S.steps.forEach((st) => { if (!st.done) setStepState(st); }); paintHead(); } }, 1000);
+ paintHead();
+ return { push, pushAll: (list) => (list || []).forEach(push), status, summary, actions: (list, note) => paintActions(list, note), files: () => changedFiles().map((f) => ({ path: f.path, added: f.added, removed: f.removed, changes: f.n })), destroy() { clearInterval(tick); host.textContent = ''; } };
+ }
+
+ const api = { mount, normalize, diffLines, kindOf };
+ root.OpenVibeAgentSession = api;
+ if (typeof module === 'object' && module.exports) module.exports = api;
+})(typeof globalThis !== 'undefined' ? globalThis : this);
new file mode 100644
new file mode 100644
@@ -0,0 +1,236 @@
@@ -0,0 +1,236 @@
+// ═══════════════════════════════════════════════════════════════
+// OpenVibe — an agent's sheet (openvibe-shared 4.211.0; Opus, 2026-11-11)
+//
+// One window (ov-window.js: a panel beside what opened it on a computer, a sheet on a phone) that says everything about
+// one agent: who it is and its state, what it is doing now, what it asks you, the work it did, when it runs, what it
+// remembers and its computer. The data is the agent's sheet (Contracts actor.agent-sheet@1 / codes.agent-sheet@1),
+// read through Network's same-origin relay with the person's token:
+// OVAgentSheet.open({ kind: 'actor' | 'codes', id, name, token, frameBase, base, from, openHref }) → { close() }
+// The parts are tabs (a segmented row; arrow keys move between them); a part with nothing in it says so in its own
+// words (asks_empty) rather than disappearing. While it reads, the window shows the mark's wait; a failed read says so
+// with Retry. Everything an agent or a person wrote is set as text, never markup.
+// ═══════════════════════════════════════════════════════════════
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OVAgentSheet) return;
+
+ const CSS = `.ovas { display: grid; gap: 12px; padding: 4px 2px 6px; }
+.ovas-who { display: flex; align-items: center; gap: 12px; min-width: 0; }
+.ovas-face { flex: none; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; overflow: hidden; font-weight: 800; font-size: 17px;
+ color: #fff; background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in srgb, var(--accent, #3b82f6) 45%, #7c3aed)); box-shadow: 0 0 0 3px var(--ovas-ring, transparent); }
+.ovas-face img { width: 100%; height: 100%; object-fit: cover; }
+.ovas[data-state="working"] { --ovas-ring: color-mix(in srgb, #22c55e 55%, transparent); }
+.ovas[data-state="paused"] { --ovas-ring: color-mix(in srgb, #f59e0b 55%, transparent); }
+.ovas-name { display: grid; gap: 2px; min-width: 0; flex: 1; }
+.ovas-name b { font-size: 16px; overflow-wrap: anywhere; }
+.ovas-name small { color: var(--text-secondary, #a8b3c4); font-size: 12.5px; overflow-wrap: anywhere; }
+.ovas-state { display: inline-flex; align-items: center; gap: 6px; padding: 2px 9px; border-radius: 999px; font-size: 12px; font-weight: 650; width: fit-content;
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); }
+.ovas-state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--ovas-dot, #94a3b8); }
+.ovas[data-state="working"] .ovas-state { --ovas-dot: #22c55e; }
+.ovas[data-state="paused"] .ovas-state { --ovas-dot: #f59e0b; }
+.ovas-open { flex: none; display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border-radius: 10px; color: var(--accent-light, #60a5fa);
+ font-size: 13px; font-weight: 650; text-decoration: none; }
+.ovas-open:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
+.ovas-tabs { display: flex; gap: 4px; padding: 4px; border-radius: 999px; overflow-x: auto; scrollbar-width: none; background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
+.ovas-tabs::-webkit-scrollbar { display: none; }
+@media (max-width: 520px) { .ovas-tabs { -webkit-mask-image: linear-gradient(90deg, #000 85%, transparent); mask-image: linear-gradient(90deg, #000 85%, transparent); } }
+.ovas-tab { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 34px; padding: 0 13px; border: 0; border-radius: 999px; background: none;
+ color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; cursor: pointer; }
+.ovas-tab[aria-selected="true"] { background: var(--bg-primary, #0b1220); color: var(--text-primary, #e6edf7); font-weight: 650; box-shadow: 0 1px 3px rgba(0, 0, 0, .25); }
+.ovas-tab em { font-style: normal; font-size: 11px; font-weight: 700; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, #f59e0b 22%, transparent); color: #fbbf24; }
+.ovas-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
+.ovas-part { display: grid; align-content: start; gap: 8px; min-height: 90px; }
+.ovas-part[hidden] { display: none; }
+.ovas-empty { margin: 0; padding: 18px 8px; text-align: center; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }
+.ovas-card { display: grid; gap: 4px; padding: 11px 13px; border-radius: 14px; border: 1px solid var(--border, rgba(255, 255, 255, .1));
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 3%, transparent); min-width: 0; }
+.ovas-card b { font-size: 14px; font-weight: 650; overflow-wrap: anywhere; }
+.ovas-card p { margin: 0; color: var(--text-secondary, #a8b3c4); font-size: 13px; overflow-wrap: anywhere; }
+.ovas-line { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; color: var(--text-muted, #7d8aa0); font-size: 12px; font-variant-numeric: tabular-nums; }
+.ovas-chip { display: inline-flex; align-items: center; padding: 1px 8px; border-radius: 999px; font-size: 11.5px; font-weight: 650;
+ background: color-mix(in srgb, var(--text-primary, #e6edf7) 8%, transparent); color: var(--text-primary, #e6edf7); }
+.ovas-chip.is-ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }
+.ovas-chip.is-bad { background: color-mix(in srgb, #ef4444 18%, transparent); color: #f87171; }
+.ovas-chip.is-run { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, #60a5fa); }
+.ovas-card.is-now { border-color: color-mix(in srgb, #22c55e 45%, transparent); }
+.ovas-card.is-ask { border-color: color-mix(in srgb, #f59e0b 50%, transparent); background: color-mix(in srgb, #f59e0b 7%, transparent); }
+.ovas-go { justify-self: start; display: inline-flex; align-items: center; min-height: 40px; padding: 0 16px; border-radius: 12px; text-decoration: none; font-weight: 650; font-size: 14px;
+ color: #fff; background: var(--accent, #3b82f6); }
+.ovas-wait { display: grid; justify-items: center; gap: 10px; padding: 26px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13.5px; }
+.ovas-retry { min-height: 40px; padding: 0 16px; border-radius: 12px; border: 1px solid var(--border, rgba(255, 255, 255, .14)); background: none; color: inherit; font: inherit; cursor: pointer; }
+@media (pointer: coarse) { .ovas-tab { min-height: 44px; } .ovas-open { min-height: 44px; } }`;
+
+ function styles(d) {
+ if (d.getElementById('ovas-css')) return;
+ const s = d.createElement('style');
+ s.id = 'ovas-css';
+ s.textContent = CSS;
+ d.head.appendChild(s);
+ }
+ const money = (n) => (Number(n) > 0 ? `$${Number(n).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: Number(n) < 0.1 ? 4 : 2 })}` : '$0');
+ function when(iso) {
+ const t = Date.parse(iso || '');
+ if (!Number.isFinite(t)) return '';
+ const s = (t - Date.now()) / 1000;
+ const a = Math.abs(s);
+ const unit = a < 3600 ? [Math.max(1, Math.round(a / 60)), 'min'] : a < 86400 ? [Math.round(a / 3600), 'h'] : [Math.round(a / 86400), a < 172800 ? 'day' : 'days'];
+ if (a < 60) return s > 0 ? 'in a moment' : 'just now';
+ return s > 0 ? `in ${unit[0]} ${unit[1]}` : `${unit[0]} ${unit[1]} ago`;
+ }
+ const tone = (state) => (/^(done|succeeded|ok|passed|completed)$/i.test(state) ? 'is-ok' : /^(failed|error|refused|stopped)$/i.test(state) ? 'is-bad' : /^(running|working|started)$/i.test(state) ? 'is-run' : '');
+ const STATE_WORD = { working: 'Working', idle: 'Resting', paused: 'Paused', ready: 'Ready' };
+
+ function open(opts = {}) {
+ const W = root.OVWindow;
+ if (!W) throw new Error('ov-window.js is not loaded');
+ const d = document;
+ styles(d);
+ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = String(text); return n; };
+ const kind = opts.kind === 'codes' ? 'codes' : 'actor';
+ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const aid = encodeURIComponent(String(opts.id || ''));
+ // Through Network's relay on a site; `base` reaches the service itself (its own site, or a test).
+ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';
+ const url = direct ? (kind === 'codes' ? `${direct}/api/v1/me/agents/${aid}/sheet` : `${direct}/api/v1/me/agents/${aid}?view=sheet`)
+ : `${frameBase}/${kind === 'codes' ? 'codes-agents' : 'agents'}/${aid}/sheet`;
+ const body = h('div', 'ovas');
+ // The window's title names what it is; the agent's own name leads the sheet under it (said once).
+ const win = W.open({ id: `agent-sheet-${kind}`, title: kind === 'codes' ? 'Coding agent' : 'Your Actor', body, kind: 'panel', size: 'md', from: opts.from || null,
+ onClose: () => { gone = true; } });
+ let gone = false;
+
+ function wait() {
+ const box = h('div', 'ovas-wait');
+ const mark = h('span', 'ov-mark');
+ mark.setAttribute('data-state', 'busy'); mark.setAttribute('data-size', '34'); mark.setAttribute('aria-hidden', 'true');
+ box.append(mark, h('span', '', 'Reading your agent…'));
+ body.replaceChildren(box);
+ }
+ function failed(text) {
+ const box = h('div', 'ovas-wait');
+ const retry = h('button', 'ovas-retry', 'Retry'); retry.type = 'button';
+ retry.addEventListener('click', load);
+ box.append(h('span', '', text), retry);
+ body.replaceChildren(box);
+ }
+
+ function card(cls, title, line, more) {
+ const c = h('div', `ovas-card${cls ? ` ${cls}` : ''}`);
+ c.append(h('b', '', title));
+ if (more) c.append(h('p', '', more));
+ if (line && line.childNodes.length) c.append(line);
+ return c;
+ }
+ function workCard(w, isNow) {
+ const line = h('div', 'ovas-line');
+ if (w.state) line.append(h('span', `ovas-chip ${tone(w.state)}`, w.state));
+ if (Number.isFinite(Number(w.cost_usd))) line.append(h('span', '', money(w.cost_usd)));
+ if (w.updated_at || w.started_at || w.at) line.append(h('span', '', when(w.updated_at || w.started_at || w.at)));
+ const result = typeof w.result === 'string' ? w.result : w.result && (w.result.summary || w.result.text) ? (w.result.summary || w.result.text) : '';
+ return card(isNow ? 'is-now' : '', w.task || 'A task', line, result ? String(result).slice(0, 400) : '');
+ }
+
+ function render(sheet) {
+ const a = sheet.agent || {};
+ body.dataset.state = a.state || '';
+ const who = h('div', 'ovas-who');
+ const face = h('span', 'ovas-face');
+ if (a.face && /^https:\/\//.test(a.face)) { const img = h('img'); img.src = a.face; img.alt = ''; face.append(img); } else face.textContent = String(a.name || opts.name || 'A').trim().charAt(0).toUpperCase();
+ const name = h('span', 'ovas-name');
+ name.append(h('b', '', a.name || opts.name || 'Your agent'), h('span', 'ovas-state', STATE_WORD[a.state] || a.state || 'Ready'));
+ if (a.instructions) name.append(h('small', '', String(a.instructions).slice(0, 160)));
+ else if (a.kind) name.append(h('small', '', a.kind));
+ who.append(face, name);
+ if (opts.openHref) { const go = h('a', 'ovas-open', 'Open ↗'); go.href = opts.openHref; go.target = '_blank'; go.rel = 'noopener'; who.append(go); }
+
+ const asks = Array.isArray(sheet.asks) ? sheet.asks : [];
+ const work = Array.isArray(sheet.work) ? sheet.work : [];
+ const parts = [
+ ['now', 'Now', () => (sheet.now ? [workCard(sheet.now, true)] : [h('p', 'ovas-empty', a.state === 'paused' ? 'Paused. Nothing runs until you resume it.' : 'Nothing running right now.')])],
+ ['asks', 'Asks', () => (asks.length ? asks.map((x) => {
+ const line = h('div', 'ovas-line');
+ if (x.state) line.append(h('span', 'ovas-chip', x.state));
+ if (x.touch && typeof x.touch === 'string') line.append(h('span', '', x.touch));
+ return card('is-ask', x.what || 'A request', line, x.why || '');
+ }) : [h('p', 'ovas-empty', sheet.asks_empty || 'Nothing to approve.')]), asks.length],
+ ['work', 'Work', () => (work.length ? work.map((w) => workCard(w, false)) : [h('p', 'ovas-empty', 'No work yet.')])],
+ ['when', 'Schedule', () => {
+ const rows = (Array.isArray(sheet.schedules) ? sheet.schedules : []).filter((s) => s && s.schedule !== 'off');
+ if (!rows.length) return [h('p', 'ovas-empty', 'It runs when you ask, on no schedule.')];
+ return rows.map((s) => {
+ const line = h('div', 'ovas-line');
+ if (s.next_run_at) line.append(h('span', '', `Next ${when(s.next_run_at)}`));
+ if (s.last_run_at) line.append(h('span', '', `Last ${when(s.last_run_at)}`));
+ if (s.last_result) line.append(h('span', `ovas-chip ${tone(s.last_result)}`, s.last_result));
+ return card('', String(s.schedule || 'On a schedule'), line, '');
+ });
+ }],
+ ['memory', 'Memory', () => {
+ const rows = Array.isArray(sheet.memory) ? sheet.memory : [];
+ return rows.length ? rows.slice(0, 30).map((m) => { const line = h('div', 'ovas-line'); if (m.source) line.append(h('span', '', m.source)); return card('', String(m.text || '').slice(0, 300), line, ''); }) : [h('p', 'ovas-empty', 'Nothing remembered yet.')];
+ }],
+ ['computer', 'Computer', () => {
+ const c = sheet.computer;
+ if (!c) return [h('p', 'ovas-empty', 'No computer is attached to this agent.')];
+ const out = [card('', c.name || 'Its computer', null, c.state || c.status || '')];
+ const href = [c.url, c.open].find((u) => typeof u === 'string' && /^https:\/\//.test(u));
+ if (href) { const go = h('a', 'ovas-go', 'Watch its computer'); go.href = href; go.target = '_blank'; go.rel = 'noopener'; out.push(go); }
+ return out;
+ }],
+ ];
+ const tabs = h('div', 'ovas-tabs');
+ tabs.setAttribute('role', 'tablist');
+ tabs.setAttribute('aria-label', 'About this agent');
+ const panes = [];
+ const first = asks.length ? 'asks' : 'now';
+ parts.forEach(([id, label, build, count]) => {
+ const tab = h('button', 'ovas-tab', label);
+ tab.type = 'button';
+ tab.setAttribute('role', 'tab');
+ tab.id = `ovas-t-${id}`;
+ tab.setAttribute('aria-controls', `ovas-p-${id}`);
+ if (count) tab.append(h('em', '', count));
+ const pane = h('div', 'ovas-part');
+ pane.id = `ovas-p-${id}`;
+ pane.setAttribute('role', 'tabpanel');
+ pane.setAttribute('aria-labelledby', tab.id);
+ pane.append(...build());
+ const on = id === first;
+ tab.setAttribute('aria-selected', on ? 'true' : 'false');
+ tab.tabIndex = on ? 0 : -1;
+ pane.hidden = !on;
+ tab.addEventListener('click', () => select(id));
+ tabs.append(tab);
+ panes.push([id, tab, pane]);
+ });
+ function select(id) {
+ panes.forEach(([pid, tab, pane]) => { const on = pid === id; tab.setAttribute('aria-selected', on ? 'true' : 'false'); tab.tabIndex = on ? 0 : -1; pane.hidden = !on; });
+ }
+ tabs.addEventListener('keydown', (e) => {
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
+ const i = panes.findIndex(([, tab]) => tab.getAttribute('aria-selected') === 'true');
+ const next = panes[(i + (e.key === 'ArrowRight' ? 1 : panes.length - 1)) % panes.length];
+ select(next[0]); next[1].focus(); e.preventDefault();
+ });
+ body.replaceChildren(who, tabs, ...panes.map(([, , pane]) => pane));
+ }
+
+ async function load() {
+ wait();
+ try {
+ const t = typeof opts.token === 'function' ? await opts.token() : opts.token;
+ if (!t) { failed('Sign in to see your agent.'); return; }
+ const r = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });
+ const b = await r.json().catch(() => null);
+ if (gone) return;
+ if (!r.ok || !b) { failed(r.status === 404 ? 'This agent is not there any more.' : 'Its sheet could not be read just now.'); return; }
+ render(b.sheet || b);
+ } catch { if (!gone) failed('Its sheet could not be read just now.'); }
+ }
+ load();
+ return { close: () => win.close() };
+ }
+
+ root.OVAgentSheet = { open };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+/* Generated by scripts/build-ai-marks-client.js from ai-marks-data.js. */
+(function (root) {
+ 'use strict';
+ if (!root || root.OpenVibeAIMarks) return;
+ const DATA = {"OpenAI":"<path d=\"M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z\"></path>","Anthropic":"<path d=\"M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z\"></path>","Google":"<path d=\"M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z\"></path>","Gemma":"<path d=\"M12.34 5.953a8.233 8.233 0 01-.247-1.125V3.72a8.25 8.25 0 015.562 2.232H12.34zm-.69 0c.113-.373.199-.755.257-1.145V3.72a8.25 8.25 0 00-5.562 2.232h5.304zm-5.433.187h5.373a7.98 7.98 0 01-.267.696 8.41 8.41 0 01-1.76 2.65L6.216 6.14zm-.264-.187H2.977v.187h2.915a8.436 8.436 0 00-2.357 5.767H0v.186h3.535a8.436 8.436 0 002.357 5.767H2.977v.186h2.976v2.977h.187v-2.915a8.436 8.436 0 005.767 2.357V24h.186v-3.535a8.436 8.436 0 005.767-2.357v2.915h.186v-2.977h2.977v-.186h-2.915a8.436 8.436 0 002.357-5.767H24v-.186h-3.535a8.436 8.436 0 00-2.357-5.767h2.915v-.187h-2.977V2.977h-.186v2.915a8.436 8.436 0 00-5.767-2.357V0h-.186v3.535A8.436 8.436 0 006.14 5.892V2.977h-.187v2.976zm6.14 14.326a8.25 8.25 0 005.562-2.233H12.34c-.108.367-.19.743-.247 1.126v1.107zm-.186-1.087a8.015 8.015 0 00-.258-1.146H6.345a8.25 8.25 0 005.562 2.233v-1.087zm-8.186-7.285h1.107a8.23 8.23 0 001.125-.247V6.345a8.25 8.25 0 00-2.232 5.562zm1.087.186H3.72a8.25 8.25 0 002.232 5.562v-5.304a8.012 8.012 0 00-1.145-.258zm15.47-.186a8.25 8.25 0 00-2.232-5.562v5.315c.367.108.743.19 1.126.247h1.107zm-1.086.186c-.39.058-.772.144-1.146.258v5.304a8.25 8.25 0 002.233-5.562h-1.087zm-1.332 5.69V12.41a7.97 7.97 0 00-.696.267 8.409 8.409 0 00-2.65 1.76l3.346 3.346zm0-6.18v-5.45l-.012-.013h-5.451c.076.235.162.468.26.696a8.698 8.698 0 001.819 2.688 8.698 8.698 0 002.688 1.82c.228.097.46.183.696.259zM6.14 17.848V12.41c.235.078.468.167.696.267a8.403 8.403 0 012.688 1.799 8.404 8.404 0 011.799 2.688c.1.228.19.46.267.696H6.152l-.012-.012zm0-6.245V6.326l3.29 3.29a8.716 8.716 0 01-2.594 1.728 8.14 8.14 0 01-.696.259zm6.257 6.257h5.277l-3.29-3.29a8.716 8.716 0 00-1.728 2.594 8.135 8.135 0 00-.259.696zm-2.347-7.81a9.435 9.435 0 01-2.88 1.96 9.14 9.14 0 012.88 1.94 9.14 9.14 0 011.94 2.88 9.435 9.435 0 011.96-2.88 9.14 9.14 0 012.88-1.94 9.435 9.435 0 01-2.88-1.96 9.434 9.434 0 01-1.96-2.88 9.14 9.14 0 01-1.94 2.88z\"></path>","Meta":"<path d=\"M6.897 4c1.915 0 3.516.932 5.43 3.376l.282-.373c.19-.246.383-.484.58-.71l.313-.35C14.588 4.788 15.792 4 17.225 4c1.273 0 2.469.557 3.491 1.516l.218.213c1.73 1.765 2.917 4.71 3.053 8.026l.011.392.002.25c0 1.501-.28 2.759-.818 3.7l-.14.23-.108.153c-.301.42-.664.758-1.086 1.009l-.265.142-.087.04a3.493 3.493 0 01-.302.118 4.117 4.117 0 01-1.33.208c-.524 0-.996-.067-1.438-.215-.614-.204-1.163-.56-1.726-1.116l-.227-.235c-.753-.812-1.534-1.976-2.493-3.586l-1.43-2.41-.544-.895-1.766 3.13-.343.592C7.597 19.156 6.227 20 4.356 20c-1.21 0-2.205-.42-2.936-1.182l-.168-.184c-.484-.573-.837-1.311-1.043-2.189l-.067-.32a8.69 8.69 0 01-.136-1.288L0 14.468c.002-.745.06-1.49.174-2.23l.1-.573c.298-1.53.828-2.958 1.536-4.157l.209-.34c1.177-1.83 2.789-3.053 4.615-3.16L6.897 4zm-.033 2.615l-.201.01c-.83.083-1.606.673-2.252 1.577l-.138.199-.01.018c-.67 1.017-1.185 2.378-1.456 3.845l-.004.022a12.591 12.591 0 00-.207 2.254l.002.188c.004.18.017.36.04.54l.043.291c.092.503.257.908.486 1.208l.117.137c.303.323.698.492 1.17.492 1.1 0 1.796-.676 3.696-3.641l2.175-3.4.454-.701-.139-.198C9.11 7.3 8.084 6.616 6.864 6.616zm10.196-.552l-.176.007c-.635.048-1.223.359-1.82.933l-.196.198c-.439.462-.887 1.064-1.367 1.807l.266.398c.18.274.362.56.55.858l.293.475 1.396 2.335.695 1.114c.583.926 1.03 1.6 1.408 2.082l.213.262c.282.326.529.54.777.673l.102.05c.227.1.457.138.718.138.176.002.35-.023.518-.073.338-.104.61-.32.813-.637l.095-.163.077-.162c.194-.459.29-1.06.29-1.785l-.006-.449c-.08-2.871-.938-5.372-2.2-6.798l-.176-.189c-.67-.683-1.444-1.074-2.27-1.074z\"></path>","DeepSeek":"<path d=\"M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z\"></path>","Qwen":"<path d=\"M19.713 2.825h-5.907c0-.251-.066-.503-.196-.728L12.819.728A1.46 1.46 0 0 0 11.559 0h-1.58c-.52 0-1 .275-1.26.728l-2.42 4.19a1.45 1.45 0 0 0 0 1.453l.791 1.37c.26.448.74.727 1.26.727h11.36c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451l-.79-1.37a1.46 1.46 0 0 0-1.26-.728zM.195 9.905l2.951 5.115a1.46 1.46 0 0 0-.531.531l-.791 1.37a1.45 1.45 0 0 0 0 1.452l.79 1.37c.26.448.74.727 1.26.727h4.84c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451L5.086 7.083a1.46 1.46 0 0 0-1.26-.728h-1.58c-.52 0-1 .276-1.26.728l-.79 1.37a1.45 1.45 0 0 0 0 1.452m15.888 13.367 2.951-5.115c.217.125.469.196.728.196h1.582c.52 0 1-.275 1.26-.728l.79-1.37a1.45 1.45 0 0 0 0-1.451l-2.42-4.191a1.46 1.46 0 0 0-1.259-.728h-1.582c-.52 0-1 .276-1.259.728l-5.678 9.838a1.45 1.45 0 0 0 0 1.452l.79 1.37c.26.448.74.727 1.26.727h1.582c.52 0 1-.275 1.26-.728z\"></path>","Mistral":"<path clip-rule=\"evenodd\" d=\"M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z\"></path>","Kimi":"<path d=\"M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z\"></path><path d=\"M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z\"></path>","GLM":"<path d=\"M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z\"></path>","MiniMax":"<path d=\"M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z\"></path>","Microsoft":"<path d=\"M11.49 2H2v9.492h9.492V2h-.002z\"></path><path d=\"M22 2h-9.492v9.492H22V2z\"></path><path d=\"M11.49 12.508H2V22h9.492v-9.492h-.002z\"></path><path d=\"M22 12.508h-9.492V22H22v-9.492z\"></path>","xAI":"<path d=\"M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815\"></path>","NVIDIA":"<path d=\"M10.212 8.976V7.62c.127-.01.256-.017.388-.021 3.596-.117 5.957 3.184 5.957 3.184s-2.548 3.647-5.282 3.647a3.227 3.227 0 01-1.063-.175v-4.109c1.4.174 1.681.812 2.523 2.258l1.873-1.627a4.905 4.905 0 00-3.67-1.846 6.594 6.594 0 00-.729.044m0-4.476v2.025c.13-.01.259-.019.388-.024 5.002-.174 8.261 4.226 8.261 4.226s-3.743 4.69-7.643 4.69c-.338 0-.675-.031-1.007-.092v1.25c.278.038.558.057.838.057 3.629 0 6.253-1.91 8.794-4.169.421.347 2.146 1.193 2.501 1.564-2.416 2.083-8.048 3.763-11.24 3.763-.308 0-.603-.02-.894-.048V19.5H24v-15H10.21zm0 9.756v1.068c-3.356-.616-4.287-4.21-4.287-4.21a7.173 7.173 0 014.287-2.138v1.172h-.005a3.182 3.182 0 00-2.502 1.178s.615 2.276 2.507 2.931m-5.961-3.3c1.436-1.935 3.604-3.148 5.961-3.336V6.523C5.81 6.887 2 10.723 2 10.723s2.158 6.427 8.21 7.015v-1.166C5.77 16 4.25 10.958 4.25 10.958h-.002z\"></path>","Cohere":"<path clip-rule=\"evenodd\" d=\"M8.128 14.099c.592 0 1.77-.033 3.398-.703 1.897-.781 5.672-2.2 8.395-3.656 1.905-1.018 2.74-2.366 2.74-4.18A4.56 4.56 0 0018.1 1H7.549A6.55 6.55 0 001 7.55c0 3.617 2.745 6.549 7.128 6.549z\"></path><path clip-rule=\"evenodd\" d=\"M9.912 18.61a4.387 4.387 0 012.705-4.052l3.323-1.38c3.361-1.394 7.06 1.076 7.06 4.715a5.104 5.104 0 01-5.105 5.104l-3.597-.001a4.386 4.386 0 01-4.386-4.387z\"></path><path d=\"M4.776 14.962A3.775 3.775 0 001 18.738v.489a3.776 3.776 0 007.551 0v-.49a3.775 3.775 0 00-3.775-3.775z\"></path>","Perplexity":"<path d=\"M19.785 0v7.272H22.5V17.62h-2.935V24l-7.037-6.194v6.145h-1.091v-6.152L4.392 24v-6.465H1.5V7.188h2.884V0l7.053 6.494V.19h1.09v6.49L19.786 0zm-7.257 9.044v7.319l5.946 5.234V14.44l-5.946-5.397zm-1.099-.08l-5.946 5.398v7.235l5.946-5.234V8.965zm8.136 7.58h1.844V8.349H13.46l6.105 5.54v2.655zm-8.982-8.28H2.59v8.195h1.8v-2.576l6.192-5.62zM5.475 2.476v4.71h5.115l-5.115-4.71zm13.219 0l-5.115 4.71h5.115v-4.71z\"></path>"};
+ const ALIAS = { openai: 'OpenAI', anthropic: 'Anthropic', google: 'Google', gemma: 'Gemma', meta: 'Meta', deepseek: 'DeepSeek', qwen: 'Qwen', mistral: 'Mistral', kimi: 'Kimi', glm: 'GLM', minimax: 'MiniMax', microsoft: 'Microsoft', xai: 'xAI', nvidia: 'NVIDIA', cohere: 'Cohere', perplexity: 'Perplexity' };
+ const mark = (maker, model) => {
+ const text = String(maker || model || '').toLowerCase();
+ const key = Object.keys(ALIAS).find((name) => text.includes(name)) || (/gpt|\bo[134]\b/.test(text) ? 'openai' : /claude/.test(text) ? 'anthropic' : /gemini/.test(text) ? 'google' : /grok/.test(text) ? 'xai' : null);
+ if (!key || !DATA[ALIAS[key]]) return null;
+ const tile = document.createElement('span'); tile.className = 'ovdk-run-maker'; tile.setAttribute('role', 'img'); tile.setAttribute('aria-label', ALIAS[key]);
+ tile.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true">' + DATA[ALIAS[key]] + '</svg>';
+ return tile;
+ };
+ root.OpenVibeAIMarks = { mark };
+})(typeof window !== 'undefined' ? window : null);
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+'use strict';
+/* Generated by scripts/build-brands.js from simple-icons 16.36.0 (CC0-1.0, https://simpleicons.org). Do not edit. */
+/**
+ * openvibe-shared/brands — the brand logos OpenVibe shows, in place of Font Awesome's fa-brands (plan L54 K4):
+ * 49 brands on a 24-unit grid, filled, each with its own colour. Brand names are their owners' marks; Simple Icons'
+ * drawings are public domain.
+ *
+ * brands.get('youtube') → { title, hex, path } or null
+ * brands.fromFa('fa-youtube') → 'youtube' (or null)
+ * brands.svg('youtube', { size, title, color })
+ */
+(function (root, factory) {
+ const api = factory();
+ if (typeof module === 'object' && module.exports) module.exports = api;
+ else root.OVBrands = api;
+})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
+ const BRANDS = {"android":{"title":"Android","hex":"#3ddc84","path":"M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z"},"apple":{"title":"Apple","hex":"#000000","path":"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"},"bitcoin":{"title":"Bitcoin","hex":"#f7931a","path":"M23.638 14.904c-1.602 6.43-8.113 10.34-14.542 8.736C2.67 22.05-1.244 15.525.362 9.105 1.962 2.67 8.475-1.243 14.9.358c6.43 1.605 10.342 8.115 8.738 14.548v-.002zm-6.35-4.613c.24-1.59-.974-2.45-2.64-3.03l.54-2.153-1.315-.33-.525 2.107c-.345-.087-.705-.167-1.064-.25l.526-2.127-1.32-.33-.54 2.165c-.285-.067-.565-.132-.84-.2l-1.815-.45-.35 1.407s.975.225.955.236c.535.136.63.486.615.766l-1.477 5.92c-.075.166-.24.406-.614.314.015.02-.96-.24-.96-.24l-.66 1.51 1.71.426.93.242-.54 2.19 1.32.327.54-2.17c.36.1.705.19 1.05.273l-.51 2.154 1.32.33.545-2.19c2.24.427 3.93.257 4.64-1.774.57-1.637-.03-2.58-1.217-3.196.854-.193 1.5-.76 1.68-1.93h.01zm-3.01 4.22c-.404 1.64-3.157.75-4.05.53l.72-2.9c.896.23 3.757.67 3.33 2.37zm.41-4.24c-.37 1.49-2.662.735-3.405.55l.654-2.64c.744.18 3.137.524 2.75 2.084v.006z"},"bluesky":{"title":"Bluesky","hex":"#1185fe","path":"M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"},"brave":{"title":"Brave","hex":"#fb542b","path":"M15.68 0l2.096 2.38s1.84-.512 2.709.358c.868.87 1.584 1.638 1.584 1.638l-.562 1.381.715 2.047s-2.104 7.98-2.35 8.955c-.486 1.919-.818 2.66-2.198 3.633-1.38.972-3.884 2.66-4.293 2.916-.409.256-.92.692-1.38.692-.46 0-.97-.436-1.38-.692a185.796 185.796 0 01-4.293-2.916c-1.38-.973-1.712-1.714-2.197-3.633-.247-.975-2.351-8.955-2.351-8.955l.715-2.047-.562-1.381s.716-.768 1.585-1.638c.868-.87 2.708-.358 2.708-.358L8.321 0h7.36zm-3.679 14.936c-.14 0-1.038.317-1.758.69-.72.373-1.242.637-1.409.742-.167.104-.065.301.087.409.152.107 2.194 1.69 2.393 1.866.198.175.489.464.687.464.198 0 .49-.29.688-.464.198-.175 2.24-1.759 2.392-1.866.152-.108.254-.305.087-.41-.167-.104-.689-.368-1.41-.741-.72-.373-1.617-.69-1.757-.69zm0-11.278s-.409.001-1.022.206-1.278.46-1.584.46c-.307 0-2.581-.434-2.581-.434S4.119 7.152 4.119 7.849c0 .697.339.881.68 1.243l2.02 2.149c.192.203.59.511.356 1.066-.235.555-.58 1.26-.196 1.977.384.716 1.042 1.194 1.464 1.115.421-.08 1.412-.598 1.776-.834.364-.237 1.518-1.19 1.518-1.554 0-.365-1.193-1.02-1.413-1.168-.22-.15-1.226-.725-1.247-.95-.02-.227-.012-.293.284-.851.297-.559.831-1.304.742-1.8-.089-.495-.95-.753-1.565-.986-.615-.232-1.799-.671-1.947-.74-.148-.068-.11-.133.339-.175.448-.043 1.719-.212 2.292-.052.573.16 1.552.403 1.632.532.079.13.149.134.067.579-.081.445-.5 2.581-.541 2.96-.04.38-.12.63.288.724.409.094 1.097.256 1.333.256s.924-.162 1.333-.256c.408-.093.329-.344.288-.723-.04-.38-.46-2.516-.541-2.961-.082-.445-.012-.45.067-.579.08-.129 1.059-.372 1.632-.532.573-.16 1.845.009 2.292.052.449.042.487.107.339.175-.148.069-1.332.508-1.947.74-.615.233-1.476.49-1.565.986-.09.496.445 1.241.742 1.8.297.558.304.624.284.85-.02.226-1.026.802-1.247.95-.22.15-1.413.804-1.413 1.169 0 .364 1.154 1.317 1.518 1.554.364.236 1.355.755 1.776.834.422.079 1.08-.4 1.464-1.115.384-.716.039-1.422-.195-1.977-.235-.555.163-.863.355-1.066l2.02-2.149c.341-.362.68-.546.68-1.243 0-.697-2.695-3.96-2.695-3.96s-2.274.436-2.58.436c-.307 0-.972-.256-1.585-.461-.613-.205-1.022-.206-1.022-.206z"},"cloudflare":{"title":"Cloudflare","hex":"#f38020","path":"M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"},"discord":{"title":"Discord","hex":"#5865f2","path":"M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"},"docker":{"title":"Docker","hex":"#2496ed","path":"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"},"ethereum":{"title":"Ethereum","hex":"#3c3c3d","path":"M11.944 17.97L4.58 13.62 11.943 24l7.37-10.38-7.372 4.35h.003zM12.056 0L4.69 12.223l7.365 4.354 7.365-4.35L12.056 0z"},"facebook":{"title":"Facebook","hex":"#0866ff","path":"M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"},"firefoxbrowser":{"title":"Firefox Browser","hex":"#ff7139","path":"M8.824 7.287c.008 0 .004 0 0 0zm-2.8-1.4c.006 0 .003 0 0 0zm16.754 2.161c-.505-1.215-1.53-2.528-2.333-2.943.654 1.283 1.033 2.57 1.177 3.53l.002.02c-1.314-3.278-3.544-4.6-5.366-7.477-.091-.147-.184-.292-.273-.446a3.545 3.545 0 01-.13-.24 2.118 2.118 0 01-.172-.46.03.03 0 00-.027-.03.038.038 0 00-.021 0l-.006.001a.037.037 0 00-.01.005L15.624 0c-2.585 1.515-3.657 4.168-3.932 5.856a6.197 6.197 0 00-2.305.587.297.297 0 00-.147.37c.057.162.24.24.396.17a5.622 5.622 0 012.008-.523l.067-.005a5.847 5.847 0 011.957.222l.095.03a5.816 5.816 0 01.616.228c.08.036.16.073.238.112l.107.055a5.835 5.835 0 01.368.211 5.953 5.953 0 012.034 2.104c-.62-.437-1.733-.868-2.803-.681 4.183 2.09 3.06 9.292-2.737 9.02a5.164 5.164 0 01-1.513-.292 4.42 4.42 0 01-.538-.232c-1.42-.735-2.593-2.121-2.74-3.806 0 0 .537-2 3.845-2 .357 0 1.38-.998 1.398-1.287-.005-.095-2.029-.9-2.817-1.677-.422-.416-.622-.616-.8-.767a3.47 3.47 0 00-.301-.227 5.388 5.388 0 01-.032-2.842c-1.195.544-2.124 1.403-2.8 2.163h-.006c-.46-.584-.428-2.51-.402-2.913-.006-.025-.343.176-.389.206-.406.29-.787.616-1.136.974-.397.403-.76.839-1.085 1.303a9.816 9.816 0 00-1.562 3.52c-.003.013-.11.487-.19 1.073-.013.09-.026.181-.037.272a7.8 7.8 0 00-.069.667l-.002.034-.023.387-.001.06C.386 18.795 5.593 24 12.016 24c5.752 0 10.527-4.176 11.463-9.661.02-.149.035-.298.052-.448.232-1.994-.025-4.09-.753-5.844z"},"github":{"title":"GitHub","hex":"#181717","path":"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"},"gitlab":{"title":"GitLab","hex":"#fc6d26","path":"m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"},"google":{"title":"Google","hex":"#4285f4","path":"M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"},"googlechrome":{"title":"Google Chrome","hex":"#4285f4","path":"M12 0C8.21 0 4.831 1.757 2.632 4.501l3.953 6.848A5.454 5.454 0 0 1 12 6.545h10.691A12 12 0 0 0 12 0zM1.931 5.47A11.943 11.943 0 0 0 0 12c0 6.012 4.42 10.991 10.189 11.864l3.953-6.847a5.45 5.45 0 0 1-6.865-2.29zm13.342 2.166a5.446 5.446 0 0 1 1.45 7.09l.002.001h-.002l-5.344 9.257c.206.01.413.016.621.016 6.627 0 12-5.373 12-12 0-1.54-.29-3.011-.818-4.364zM12 16.364a4.364 4.364 0 1 1 0-8.728 4.364 4.364 0 0 1 0 8.728Z"},"instagram":{"title":"Instagram","hex":"#ff0069","path":"M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"},"kick":{"title":"Kick","hex":"#53fc19","path":"M1.333 0h8v5.333H12V2.667h2.667V0h8v8H20v2.667h-2.667v2.666H20V16h2.667v8h-8v-2.667H12v-2.666H9.333V24h-8Z"},"kickstarter":{"title":"Kickstarter","hex":"#05ce78","path":"M7.168 0c-3.2 0-5.797 2.579-5.797 5.758v12.484C1.371 21.42 3.968 24 7.168 24c1.981 0 3.716-.978 4.768-2.479l.794.79c2.26 2.245 5.943 2.245 8.203 0a5.724 5.724 0 001.696-4.075 5.724 5.724 0 00-1.696-4.074l-2.182-2.168 2.182-2.156a5.724 5.724 0 001.696-4.074 5.724 5.724 0 00-1.696-4.074c-2.26-2.246-5.942-2.246-8.203 0l-.794.789A5.797 5.797 0 007.168 0Z"},"line":{"title":"LINE","hex":"#00c300","path":"M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.104.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"},"linux":{"title":"Linux","hex":"#fcc624","path":"M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489a.424.424 0 00-.11.135c-.26.268-.45.6-.663.839-.199.199-.485.267-.797.4-.313.136-.658.269-.864.68-.09.189-.136.394-.132.602 0 .199.027.4.055.536.058.399.116.728.04.97-.249.68-.28 1.145-.106 1.484.174.334.535.47.94.601.81.2 1.91.135 2.774.6.926.466 1.866.67 2.616.47.526-.116.97-.464 1.208-.946.587-.003 1.23-.269 2.26-.334.699-.058 1.574.267 2.577.2.025.134.063.198.114.333l.003.003c.391.778 1.113 1.132 1.884 1.071.771-.06 1.592-.536 2.257-1.306.631-.765 1.683-1.084 2.378-1.503.348-.199.629-.469.649-.853.023-.4-.2-.811-.714-1.376v-.097l-.003-.003c-.17-.2-.25-.535-.338-.926-.085-.401-.182-.786-.492-1.046h-.003c-.059-.054-.123-.067-.188-.135a.357.357 0 00-.19-.064c.431-1.278.264-2.55-.173-3.694-.533-1.41-1.465-2.638-2.175-3.483-.796-1.005-1.576-1.957-1.56-3.368.026-2.152.236-6.133-3.544-6.139zm.529 3.405h.013c.213 0 .396.062.584.198.19.135.33.332.438.533.105.259.158.459.166.724 0-.02.006-.04.006-.06v.105a.086.086 0 01-.004-.021l-.004-.024a1.807 1.807 0 01-.15.706.953.953 0 01-.213.335.71.71 0 00-.088-.042c-.104-.045-.198-.064-.284-.133a1.312 1.312 0 00-.22-.066c.05-.06.146-.133.183-.198.053-.128.082-.264.088-.402v-.02a1.21 1.21 0 00-.061-.4c-.045-.134-.101-.2-.183-.333-.084-.066-.167-.132-.267-.132h-.016c-.093 0-.176.03-.262.132a.8.8 0 00-.205.334 1.18 1.18 0 00-.09.4v.019c.002.089.008.179.02.267-.193-.067-.438-.135-.607-.202a1.635 1.635 0 01-.018-.2v-.02a1.772 1.772 0 01.15-.768c.082-.22.232-.406.43-.533a.985.985 0 01.594-.2zm-2.962.059h.036c.142 0 .27.048.399.135.146.129.264.288.344.465.09.199.14.4.153.667v.004c.007.134.006.2-.002.266v.08c-.03.007-.056.018-.083.024-.152.055-.274.135-.393.2.012-.09.013-.18.003-.267v-.015c-.012-.133-.04-.2-.082-.333a.613.613 0 00-.166-.267.248.248 0 00-.183-.064h-.021c-.071.006-.13.04-.186.132a.552.552 0 00-.12.27.944.944 0 00-.023.33v.015c.012.135.037.2.08.334.046.134.098.2.166.268.01.009.02.018.034.024-.07.057-.117.07-.176.136a.304.304 0 01-.131.068 2.62 2.62 0 01-.275-.402 1.772 1.772 0 01-.155-.667 1.759 1.759 0 01.08-.668 1.43 1.43 0 01.283-.535c.128-.133.26-.2.418-.2zm1.37 1.706c.332 0 .733.065 1.216.399.293.2.523.269 1.052.468h.003c.255.136.405.266.478.399v-.131a.571.571 0 01.016.47c-.123.31-.516.643-1.063.842v.002c-.268.135-.501.333-.775.465-.276.135-.588.292-1.012.267a1.139 1.139 0 01-.448-.067 3.566 3.566 0 01-.322-.198c-.195-.135-.363-.332-.612-.465v-.005h-.005c-.4-.246-.616-.512-.686-.71-.07-.268-.005-.47.193-.6.224-.135.38-.271.483-.336.104-.074.143-.102.176-.131h.002v-.003c.169-.202.436-.47.839-.601.139-.036.294-.065.466-.065zm2.8 2.142c.358 1.417 1.196 3.475 1.735 4.473.286.534.855 1.659 1.102 3.024.156-.005.33.018.513.064.646-1.671-.546-3.467-1.089-3.966-.22-.2-.232-.335-.123-.335.59.534 1.365 1.572 1.646 2.757.13.535.16 1.104.021 1.67.067.028.135.06.205.067 1.032.534 1.413.938 1.23 1.537v-.043c-.06-.003-.12 0-.18 0h-.016c.151-.467-.182-.825-1.065-1.224-.915-.4-1.646-.336-1.77.465-.008.043-.013.066-.018.135-.068.023-.139.053-.209.064-.43.268-.662.669-.793 1.187-.13.533-.17 1.156-.205 1.869v.003c-.02.334-.17.838-.319 1.35-1.5 1.072-3.58 1.538-5.348.334a2.645 2.645 0 00-.402-.533 1.45 1.45 0 00-.275-.333c.182 0 .338-.03.465-.067a.615.615 0 00.314-.334c.108-.267 0-.697-.345-1.163-.345-.467-.931-.995-1.788-1.521-.63-.4-.986-.87-1.15-1.396-.165-.534-.143-1.085-.015-1.645.245-1.07.873-2.11 1.274-2.763.107-.065.037.135-.408.974-.396.751-1.14 2.497-.122 3.854a8.123 8.123 0 01.647-2.876c.564-1.278 1.743-3.504 1.836-5.268.048.036.217.135.289.202.218.133.38.333.59.465.21.201.477.335.876.335.039.003.075.006.11.006.412 0 .73-.134.997-.268.29-.134.52-.334.74-.4h.005c.467-.135.835-.402 1.044-.7zm2.185 8.958c.037.6.343 1.245.882 1.377.588.134 1.434-.333 1.791-.765l.211-.01c.315-.007.577.01.847.268l.003.003c.208.199.305.53.391.876.085.4.154.78.409 1.066.486.527.645.906.636 1.14l.003-.007v.018l-.003-.012c-.015.262-.185.396-.498.595-.63.401-1.746.712-2.457 1.57-.618.737-1.37 1.14-2.036 1.191-.664.053-1.237-.2-1.574-.898l-.005-.003c-.21-.4-.12-1.025.056-1.69.176-.668.428-1.344.463-1.897.037-.714.076-1.335.195-1.814.12-.465.308-.797.641-.984l.045-.022zm-10.814.049h.01c.053 0 .105.005.157.014.376.055.706.333 1.023.752l.91 1.664.003.003c.243.533.754 1.064 1.189 1.637.434.598.77 1.131.729 1.57v.006c-.057.744-.48 1.148-1.125 1.294-.645.135-1.52.002-2.395-.464-.968-.536-2.118-.469-2.857-.602-.369-.066-.61-.2-.723-.4-.11-.2-.113-.602.123-1.23v-.004l.002-.003c.117-.334.03-.752-.027-1.118-.055-.401-.083-.71.043-.94.16-.334.396-.4.69-.533.294-.135.64-.202.915-.47h.002v-.002c.256-.268.445-.601.668-.838.19-.201.38-.336.663-.336zm7.159-9.074c-.435.201-.945.535-1.488.535-.542 0-.97-.267-1.28-.466-.154-.134-.28-.268-.373-.335-.164-.134-.144-.333-.074-.333.109.016.129.134.199.2.096.066.215.2.36.333.292.2.68.467 1.167.467.485 0 1.053-.267 1.398-.466.195-.135.445-.334.648-.467.156-.136.149-.267.279-.267.128.016.034.134-.147.332a8.097 8.097 0 01-.69.468zm-1.082-1.583V5.64c-.006-.02.013-.042.029-.05.074-.043.18-.027.26.004.063 0 .16.067.15.135-.006.049-.085.066-.135.066-.055 0-.092-.043-.141-.068-.052-.018-.146-.008-.163-.065zm-.551 0c-.02.058-.113.049-.166.066-.047.025-.086.068-.14.068-.05 0-.13-.02-.136-.068-.01-.066.088-.133.15-.133.08-.031.184-.047.259-.005.019.009.036.03.03.05v.02h.003z"},"mastodon":{"title":"Mastodon","hex":"#6364ff","path":"M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"},"matrix":{"title":"Matrix","hex":"#000000","path":"M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"},"medium":{"title":"Medium","hex":"#000000","path":"M4.21 0A4.201 4.201 0 0 0 0 4.21v15.58A4.201 4.201 0 0 0 4.21 24h15.58A4.201 4.201 0 0 0 24 19.79v-1.093c-.137.013-.278.02-.422.02-2.577 0-4.027-2.146-4.09-4.832a7.592 7.592 0 0 1 .022-.708c.093-1.186.475-2.241 1.105-3.022a3.885 3.885 0 0 1 1.395-1.1c.468-.237 1.127-.367 1.664-.367h.023c.101 0 .202.004.303.01V4.211A4.201 4.201 0 0 0 19.79 0Zm.198 5.583h4.165l3.588 8.435 3.59-8.435h3.864v.146l-.019.004c-.705.16-1.063.397-1.063 1.254h-.003l.003 10.274c.06.676.424.885 1.063 1.03l.02.004v.145h-4.923v-.145l.019-.005c.639-.144.994-.353 1.054-1.03V7.267l-4.745 11.15h-.261L6.15 7.569v9.445c0 .857.358 1.094 1.063 1.253l.02.004v.147H4.405v-.147l.019-.004c.705-.16 1.065-.397 1.065-1.253V6.987c0-.857-.358-1.094-1.064-1.254l-.018-.004zm19.25 3.668c-1.086.023-1.733 1.323-1.813 3.124H24V9.298a1.378 1.378 0 0 0-.342-.047Zm-1.862 3.632c-.1 1.756.86 3.239 2.204 3.634v-3.634z"},"nodedotjs":{"title":"Node.js","hex":"#5fa04e","path":"M11.998,24c-0.321,0-0.641-0.084-0.922-0.247l-2.936-1.737c-0.438-0.245-0.224-0.332-0.08-0.383 c0.585-0.203,0.703-0.25,1.328-0.604c0.065-0.037,0.151-0.023,0.218,0.017l2.256,1.339c0.082,0.045,0.197,0.045,0.272,0l8.795-5.076 c0.082-0.047,0.134-0.141,0.134-0.238V6.921c0-0.099-0.053-0.192-0.137-0.242l-8.791-5.072c-0.081-0.047-0.189-0.047-0.271,0 L3.075,6.68C2.99,6.729,2.936,6.825,2.936,6.921v10.15c0,0.097,0.054,0.189,0.139,0.235l2.409,1.392 c1.307,0.654,2.108-0.116,2.108-0.89V7.787c0-0.142,0.114-0.253,0.256-0.253h1.115c0.139,0,0.255,0.112,0.255,0.253v10.021 c0,1.745-0.95,2.745-2.604,2.745c-0.508,0-0.909,0-2.026-0.551L2.28,18.675c-0.57-0.329-0.922-0.945-0.922-1.604V6.921 c0-0.659,0.353-1.275,0.922-1.603l8.795-5.082c0.557-0.315,1.296-0.315,1.848,0l8.794,5.082c0.57,0.329,0.924,0.944,0.924,1.603 v10.15c0,0.659-0.354,1.273-0.924,1.604l-8.794,5.078C12.643,23.916,12.324,24,11.998,24z M19.099,13.993 c0-1.9-1.284-2.406-3.987-2.763c-2.731-0.361-3.009-0.548-3.009-1.187c0-0.528,0.235-1.233,2.258-1.233 c1.807,0,2.473,0.389,2.747,1.607c0.024,0.115,0.129,0.199,0.247,0.199h1.141c0.071,0,0.138-0.031,0.186-0.081 c0.048-0.054,0.074-0.123,0.067-0.196c-0.177-2.098-1.571-3.076-4.388-3.076c-2.508,0-4.004,1.058-4.004,2.833 c0,1.925,1.488,2.457,3.895,2.695c2.88,0.282,3.103,0.703,3.103,1.269c0,0.983-0.789,1.402-2.642,1.402 c-2.327,0-2.839-0.584-3.011-1.742c-0.02-0.124-0.126-0.215-0.253-0.215h-1.137c-0.141,0-0.254,0.112-0.254,0.253 c0,1.482,0.806,3.248,4.655,3.248C17.501,17.007,19.099,15.91,19.099,13.993z"},"npm":{"title":"npm","hex":"#cb3837","path":"M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"},"patreon":{"title":"Patreon","hex":"#000000","path":"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46 3.765.047 4.326-4.804 6.068-7.141 1.24-1.662 2.836-2.132 4.801-2.618 3.376-.836 5.678-3.501 5.673-7.031Z"},"paypal":{"title":"PayPal","hex":"#002991","path":"M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"},"pinterest":{"title":"Pinterest","hex":"#bd081c","path":"M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z"},"python":{"title":"Python","hex":"#3776ab","path":"M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01h8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z"},"raspberrypi":{"title":"Raspberry Pi","hex":"#a22846","path":"m19.8955 10.8961-.1726-.3028c.0068-2.1746-1.0022-3.061-2.1788-3.7348.356-.0938.7237-.1711.8245-.6182.6118-.1566.7397-.4398.8011-.7398.16-.1066.6955-.4061.6394-.9211.2998-.2069.4669-.4725.3819-.8487.3222-.3515.407-.6419.2702-.9096.3868-.4805.2152-.7295.05-.9817.2897-.5254.0341-1.0887-.7758-.9944-.3221-.4733-1.0244-.3659-1.133-.3637-.1215-.1519-.2819-.2821-.7755-.219-.3197-.2851-.6771-.2364-1.0458-.0964-.4378-.3403-.7275-.0675-1.0584.0356-.53-.1706-.6513.0631-.9117.1583-.5781-.1203-.7538.1416-1.0309.4182l-.3224-.0063c-.8719.5061-1.305 1.5366-1.4585 2.0664-.1536-.5299-.5858-1.5604-1.4575-2.0664l-.3223.0063C9.942.5014 9.7663.2394 9.1883.3597 8.9279.2646 8.807.0309 8.2766.2015c-.2172-.0677-.417-.2084-.6522-.2012l.0004.0002C7.5017.0041 7.369.049 7.2185.166c-.3688-.1401-.7262-.1887-1.0459.0964-.4936-.0631-.654.0671-.7756.219C5.2887.4791 4.5862.3717 4.264.845c-.8096-.0943-1.0655.4691-.7756.9944-.1653.2521-.3366.5013.05.9819-.1367.2677-.0519.5581.2703.9096-.085.3763.0822.6418.3819.8487-.0561.515.4795.8144.6394.9211.0614.3001.1894.5832.8011.7398.1008.4472.4685.5244.8245.6183-1.1766.6737-2.1856 1.56-2.1788 3.7348l-.1724.3028c-1.3491.8082-2.5629 3.4056-.6648 5.5167.124.6609.3319 1.1355.5171 1.6609.2769 2.117 2.0841 3.1082 2.5608 3.2255.6984.524 1.4423 1.0212 2.449 1.3696.949.964 1.977 1.3314 3.0107 1.3308.0152 0 .0306.0002.0457 0 1.0337.0006 2.0618-.3668 3.0107-1.3308 1.0067-.3483 1.7506-.8456 2.4491-1.3696.4766-.1173 2.2838-1.1085 2.5607-3.2255.1851-.5253.3931-1 .517-1.6609 1.8981-2.1113.6843-4.7089-.6649-5.517zm-1.0386-.3715c-.0704.8759-4.6354-3.0504-3.8472-3.1808 2.1391-.3558 3.9191.896 3.8472 3.1808zm-2.0155 4.3649c-1.1481.7409-2.8025.2626-3.6953-1.0681-.8928-1.3306-.6858-3.0101.4623-3.7509 1.1481-.7409 2.8025-.2627 3.6953 1.068.8927 1.3307.6858 3.0101-.4623 3.751zM13.6591 1.3721c.0396.1967.0843.321.1354.3577.2537-.272.4611-.5506.7878-.8123.0011.1537-.0776.3205.1169.4425.1752-.2356.4119-.4459.7263-.6244-.1514.2611-.026.3404.0554.4486.24-.2059.4681-.4144.9109-.5759-.121.1474-.2902.2914-.1108.4607.2473-.1544.496-.3086 1.0833-.4183-.1323.1475-.4059.295-.2401.4426.3104-.1186.6539-.2047 1.034-.2546-.182.1496-.3337.2963-.1846.4122.3323-.1022.7899-.2398 1.2372-.1212l-.2832.2849c-.0314.0382.6623.0297 1.1202.0364-.167.2321-.3375.4562-.437.8548.0454.0459.2723.0204.4862 0-.2194.4618-.6004.5783-.6893.776.134.1015.32.075.5232.006-.158.3254-.4892.5484-.7509.8123.0662.047.1818.075.4555.0425-.2418.257-.5339.492-.8802.7032.0614.0708.2722.0681.4678.0727-.3136.3069-.7173.466-1.0955.6668.1885.1288.3234.0988.4678.097-.2676.2198-.7225.3342-1.1448.4668.0803.1249.1607.1589.3324.194-.447.2473-1.0873.1343-1.2679.2607.0435.1243.1665.2053.3139.2728-.7197.0418-2.6879-.0262-3.0652-1.5156.7367-.8094 2.0813-1.7593 4.394-2.934-1.7994.6022-3.4229 1.405-4.7817 2.5096-1.5978-.7436-.4965-2.6197.283-3.3645zm-1.6126 5.3718c1.1329-.0123 2.5356.8325 2.53 1.6286-.005.7027-.9851 1.2715-2.5213 1.2607-1.5043-.0177-2.5172-.7148-2.5137-1.3957.003-.5603 1.2282-1.5263 2.505-1.4936zm-5.7646-.6006c.1717-.0351.252-.0692.3323-.194-.4223-.1327-.8772-.247-1.1448-.4668.1444.0018.2792.0318.4678-.097-.3783-.2008-.782-.3599-1.0956-.6668.1955-.0048.4064-.002.4677-.0728-.3462-.2113-.6383-.4463-.8801-.7033.2738.0325.3893.0045.4555-.0425-.2617-.264-.593-.487-.7509-.8123.2032.069.3892.0954.5232-.006-.089-.1977-.47-.3142-.6894-.776.214.0204.4409.0459.4863 0-.0994-.3985-.2698-.6226-.4369-.8547.4579-.0067 1.1516.0018 1.1202-.0364l-.2831-.2849c.4472-.1186.9049.019 1.2371.1213.1492-.1159-.0026-.2626-.1847-.4123.3801.05.7236.1361 1.034.2547.1659-.1476-.1076-.2951-.24-.4426.5872.1097.8361.2639 1.0833.4183.1794-.1694.0103-.3133-.1108-.4607.4428.1615.6709.37.911.5759.0814-.1082.2068-.1875.0554-.4486.3143.1785.5511.3888.7263.6244.1945-.122.1159-.2888.1169-.4426.3267.2618.534.5404.7879.8124.0511-.0366.0959-.161.1354-.3577.7794.7448 1.8807 2.6208.2831 3.3646-1.3589-1.1039-2.9817-1.9064-4.78-2.5086 2.3115 1.174 3.6556 2.1239 4.392 2.9328-.3773 1.4895-2.3455 1.5575-3.0651 1.5157.1473-.0676.2703-.1485.3139-.2728-.1806-.1264-.8209-.0134-1.2679-.2607zm2.8175 1.1334c.7881.1304-3.7769 4.0567-3.8472 3.1809-.0719-2.2846 1.7079-3.5367 3.8472-3.1809zm-4.847 8.7567c-1.1094-.8789-1.4668-3.4529.5901-4.6097 1.2394-.3273.4184 5.051-.5901 4.6097zm4.2656 4.5989c-.6257.3719-2.1452.2187-3.2252-1.3095-.7283-1.2823-.6345-2.5872-.123-2.9705.7648-.4589 1.9464.1609 2.8559 1.2003.7923.9405 1.1536 2.5927.4923 3.0797zm-1.2415-5.6086c-1.1481-.7409-1.3551-2.4203-.4623-3.7511.8928-1.3307 2.5472-1.8089 3.6952-1.068 1.1481.7409 1.3551 2.4203.4623 3.7509-.8926 1.3308-2.5471 1.809-3.6952 1.0682zm4.7948 8.2279c-1.3763.0584-2.7258-1.1105-2.7081-1.5157-.0206-.594 1.6758-1.0578 2.782-1.0306 1.1131-.0479 2.6068.3531 2.6097.8851.0184.5166-1.3547 1.6838-2.6836 1.6612zm2.7584-5.8578c.0081 1.3899-1.226 2.5225-2.7562 2.5299-1.5302.0073-2.7773-1.1135-2.7854-2.5033v-.0265c-.008-1.3899 1.2259-2.5226 2.7562-2.5299 1.5302-.0073 2.7773 1.1134 2.7853 2.5033a.7794.7794 0 0 1 .0001.0265zm3.855 2.0029c-1.186 1.6208-2.7916 1.684-3.3896 1.2325-.6255-.5811-.148-2.3854.7094-3.3747v-.0003c.9812-1.0912 2.0302-1.8037 2.7609-1.2469.4919.4828.7805 2.3008-.0807 3.3894zm1.0724-3.4301c-1.0086.4413-1.8298-4.9372-.5901-4.61 2.0568 1.1569 1.6994 3.731.5901 4.61zm-.0256-8.3279h.2985v-.5304h.2986c.1502 0 .2053.0624.2262.2052.0152.1088.0113.2395.0477.3253h.2984c-.0533-.0763-.0515-.2358-.0571-.3213-.0097-.1373-.0513-.2796-.1977-.3176v-.0037c.1502-.061.2149-.1807.2149-.341 0-.2048-.1539-.3738-.3974-.3738h-.732v1.3573zm.2985-1.1255h.3269c.1333 0 .2054.0573.2054.188 0 .1369-.0721.1942-.2054.1942H20.03v-.3822zm-1.0337.4633c0 .7009.5682 1.2694 1.2695 1.2694s1.2695-.5684 1.2695-1.2694c0-.7013-.5683-1.2697-1.2695-1.2697-.7013 0-1.2695.5684-1.2695 1.2697zm2.3275 0c0 .5845-.4737 1.058-1.058 1.058s-1.058-.4735-1.058-1.058c0-.5849.4737-1.058 1.058-1.058s1.058.4731 1.058 1.058z"},"reddit":{"title":"Reddit","hex":"#ff4500","path":"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"},"signal":{"title":"Signal","hex":"#3b45fd","path":"M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125"},"snapchat":{"title":"Snapchat","hex":"#fffc00","path":"M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"},"soundcloud":{"title":"SoundCloud","hex":"#ff5500","path":"M23.999 14.165c-.052 1.796-1.612 3.169-3.4 3.169h-8.18a.68.68 0 0 1-.675-.683V7.862a.747.747 0 0 1 .452-.724s.75-.513 2.333-.513a5.364 5.364 0 0 1 2.763.755 5.433 5.433 0 0 1 2.57 3.54c.282-.08.574-.121.868-.12.884 0 1.73.358 2.347.992s.948 1.49.922 2.373ZM10.721 8.421c.247 2.98.427 5.697 0 8.672a.264.264 0 0 1-.53 0c-.395-2.946-.22-5.718 0-8.672a.264.264 0 0 1 .53 0ZM9.072 9.448c.285 2.659.37 4.986-.006 7.655a.277.277 0 0 1-.55 0c-.331-2.63-.256-5.02 0-7.655a.277.277 0 0 1 .556 0Zm-1.663-.257c.27 2.726.39 5.171 0 7.904a.266.266 0 0 1-.532 0c-.38-2.69-.257-5.21 0-7.904a.266.266 0 0 1 .532 0Zm-1.647.77a26.108 26.108 0 0 1-.008 7.147.272.272 0 0 1-.542 0 27.955 27.955 0 0 1 0-7.147.275.275 0 0 1 .55 0Zm-1.67 1.769c.421 1.865.228 3.5-.029 5.388a.257.257 0 0 1-.514 0c-.21-1.858-.398-3.549 0-5.389a.272.272 0 0 1 .543 0Zm-1.655-.273c.388 1.897.26 3.508-.01 5.412-.026.28-.514.283-.54 0-.244-1.878-.347-3.54-.01-5.412a.283.283 0 0 1 .56 0Zm-1.668.911c.4 1.268.257 2.292-.026 3.572a.257.257 0 0 1-.514 0c-.241-1.262-.354-2.312-.023-3.572a.283.283 0 0 1 .563 0Z"},"spotify":{"title":"Spotify","hex":"#1ed760","path":"M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"},"steam":{"title":"Steam","hex":"#000000","path":"M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"},"stripe":{"title":"Stripe","hex":"#635bff","path":"M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z"},"telegram":{"title":"Telegram","hex":"#26a5e4","path":"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"},"threads":{"title":"Threads","hex":"#000000","path":"M18.263 11.097c-.03-3.486-1.92-5.586-5.111-5.586-2.13 0-3.922.963-4.863 2.499l2.062 1.438c.535-.843 1.272-1.543 2.628-1.543 1.528 0 2.318.85 2.544 2.431a15 15 0 0 0-2.236-.173c-4.125 0-6.068 1.867-6.068 4.336s1.943 3.99 4.804 3.99c3.139 0 5.013-2.115 5.781-4.735.798.361 1.348 1.204 1.348 2.47 0 3.387-3.907 5.232-7.22 5.232-4.885 0-8.077-3.207-8.077-8.424 0-6.392 4.223-10.487 9.9-10.487 3.808 0 5.69 1.671 6.97 3.914l2.108-1.475C21.44 2.078 18.331 0 13.663 0 6.227 0 1.168 5.277 1.168 12.934c0 7 4.953 11.066 10.856 11.066 4.878 0 9.809-2.846 9.809-7.716 0-2.545-1.46-4.231-3.569-5.187m-6.33 4.855c-1.077 0-2.026-.512-2.026-1.453 0-1.483 1.822-1.934 3.606-1.934.678 0 1.34.045 1.927.173-.422 1.927-1.671 3.215-3.508 3.214Z"},"tiktok":{"title":"TikTok","hex":"#000000","path":"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"},"tumblr":{"title":"Tumblr","hex":"#36465d","path":"M14.563 24c-5.093 0-7.031-3.756-7.031-6.411V9.747H5.116V6.648c3.63-1.313 4.512-4.596 4.71-6.469C9.84.051 9.941 0 9.999 0h3.517v6.114h4.801v3.633h-4.82v7.47c.016 1.001.375 2.371 2.207 2.371h.09c.631-.02 1.486-.205 1.936-.419l1.156 3.425c-.436.636-2.4 1.374-4.156 1.404h-.178l.011.002z"},"twitch":{"title":"Twitch","hex":"#9146ff","path":"M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"},"ubuntu":{"title":"Ubuntu","hex":"#e95420","path":"M17.61.455a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zM12.92.8C8.923.777 5.137 2.941 3.148 6.451a4.5 4.5 0 0 1 .26-.007 4.92 4.92 0 0 1 2.585.737A8.316 8.316 0 0 1 12.688 3.6 4.944 4.944 0 0 1 13.723.834 11.008 11.008 0 0 0 12.92.8zm9.226 4.994a4.915 4.915 0 0 1-1.918 2.246 8.36 8.36 0 0 1-.273 8.303 4.89 4.89 0 0 1 1.632 2.54 11.156 11.156 0 0 0 .559-13.089zM3.41 7.932A3.41 3.41 0 0 0 0 11.342a3.41 3.41 0 0 0 3.41 3.409 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41zm2.027 7.866a4.908 4.908 0 0 1-2.915.358 11.1 11.1 0 0 0 7.991 6.698 11.234 11.234 0 0 0 2.422.249 4.879 4.879 0 0 1-.999-2.85 8.484 8.484 0 0 1-.836-.136 8.304 8.304 0 0 1-5.663-4.32zm11.405.928a3.41 3.41 0 0 0-3.41 3.41 3.41 3.41 0 0 0 3.41 3.41 3.41 3.41 0 0 0 3.41-3.41 3.41 3.41 0 0 0-3.41-3.41z"},"vimeo":{"title":"Vimeo","hex":"#1ab7ea","path":"M23.9765 6.4168c-.105 2.338-1.739 5.5429-4.894 9.6088-3.2679 4.247-6.0258 6.3699-8.2898 6.3699-1.409 0-2.578-1.294-3.553-3.881l-1.9179-7.1138c-.719-2.584-1.488-3.878-2.312-3.878-.179 0-.806.378-1.8809 1.132l-1.129-1.457a315.06 315.06 0 003.501-3.1279c1.579-1.368 2.765-2.085 3.5539-2.159 1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.5069.5389 2.45 1.1309 3.674 1.7759 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.8679 3.434-5.7568 6.7619-5.6368 2.4729.06 3.6279 1.664 3.4929 4.7969z"},"whatsapp":{"title":"WhatsApp","hex":"#25d366","path":"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"},"wikipedia":{"title":"Wikipedia","hex":"#000000","path":"M12.09 13.119c-.936 1.932-2.217 4.548-2.853 5.728-.616 1.074-1.127.931-1.532.029-1.406-3.321-4.293-9.144-5.651-12.409-.251-.601-.441-.987-.619-1.139-.181-.15-.554-.24-1.122-.271C.103 5.033 0 4.982 0 4.898v-.455l.052-.045c.924-.005 5.401 0 5.401 0l.051.045v.434c0 .119-.075.176-.225.176l-.564.031c-.485.029-.727.164-.727.436 0 .135.053.33.166.601 1.082 2.646 4.818 10.521 4.818 10.521l.136.046 2.411-4.81-.482-1.067-1.658-3.264s-.318-.654-.428-.872c-.728-1.443-.712-1.518-1.447-1.617-.207-.023-.313-.05-.313-.149v-.468l.06-.045h4.292l.113.037v.451c0 .105-.076.15-.227.15l-.308.047c-.792.061-.661.381-.136 1.422l1.582 3.252 1.758-3.504c.293-.64.233-.801.111-.947-.07-.084-.305-.22-.812-.24l-.201-.021c-.052 0-.098-.015-.145-.051-.045-.031-.067-.076-.067-.129v-.427l.061-.045c1.247-.008 4.043 0 4.043 0l.059.045v.436c0 .121-.059.178-.193.178-.646.03-.782.095-1.023.439-.12.186-.375.589-.646 1.039l-2.301 4.273-.065.135 2.792 5.712.17.048 4.396-10.438c.154-.422.129-.722-.064-.895-.197-.172-.346-.273-.857-.295l-.42-.016c-.061 0-.105-.014-.152-.045-.043-.029-.072-.075-.072-.119v-.436l.059-.045h4.961l.041.045v.437c0 .119-.074.18-.209.18-.648.03-1.127.18-1.443.421-.314.255-.557.616-.736 1.067 0 0-4.043 9.258-5.426 12.339-.525 1.007-1.053.917-1.503-.031-.571-1.171-1.773-3.786-2.646-5.71l.053-.036z"},"x":{"title":"X","hex":"#000000","path":"M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"},"ycombinator":{"title":"Y Combinator","hex":"#f0652f","path":"M0 24V0h24v24H0zM6.951 5.896l4.112 7.708v5.064h1.583v-4.972l4.148-7.799h-1.749l-2.457 4.875c-.372.745-.688 1.434-.688 1.434s-.297-.708-.651-1.434L8.831 5.896h-1.88z"},"youtube":{"title":"YouTube","hex":"#ff0000","path":"M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"}};
+ const FA = {"youtube":"youtube","square-youtube":"youtube","twitch":"twitch","discord":"discord","github":"github","square-github":"github","x-twitter":"x","square-x-twitter":"x","twitter":"x","kick":"kick","kickstarter-k":"kick","kickstarter":"kickstarter","google":"google","tiktok":"tiktok","threads":"threads","spotify":"spotify","soundcloud":"soundcloud","reddit":"reddit","reddit-alien":"reddit","tumblr":"tumblr","hacker-news":"ycombinator","line":"line","paypal":"paypal","patreon":"patreon","mastodon":"mastodon","linux":"linux","instagram":"instagram","facebook":"facebook","facebook-f":"facebook","cloudflare":"cloudflare","brave":"brave","bluesky":"bluesky","bitcoin":"bitcoin","apple":"apple","telegram":"telegram","whatsapp":"whatsapp","steam":"steam","snapchat":"snapchat","pinterest":"pinterest","vimeo":"vimeo","vimeo-v":"vimeo","npm":"npm","node":"nodedotjs","node-js":"nodedotjs","docker":"docker","python":"python","android":"android","chrome":"googlechrome","firefox":"firefoxbrowser","firefox-browser":"firefoxbrowser","gitlab":"gitlab","ubuntu":"ubuntu","raspberry":"raspberrypi","raspberry-pi":"raspberrypi","stripe":"stripe","cc-stripe":"stripe","signal":"signal","matrix":"matrix","medium":"medium","wikipedia":"wikipedia","wikipedia-w":"wikipedia","ethereum":"ethereum"};
+ const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
+ const get = (slug) => (has(BRANDS, slug) ? BRANDS[slug] : null);
+ const fromFa = (name) => { const n = String(name || '').replace(/^fa-/, ''); return has(FA, n) ? FA[n] : null; };
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
+ function svg(slug, { size = 24, title = '', color = 'currentColor' } = {}) {
+ const b = get(slug);
+ if (!b) return '';
+ const label = title ? ` role="img" aria-label="${esc(title)}"><title>${esc(title)}</title>` : ' aria-hidden="true">';
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${Number(size) || 24}" height="${Number(size) || 24}" fill="${color === 'brand' ? b.hex : esc(color)}"${label}<path d="${b.path}"/></svg>`;
+ }
+ return { BRANDS, FA, get, fromFa, svg, SLUGS: Object.keys(BRANDS) };
+});
new file mode 100644
new file mode 100644
@@ -0,0 +1,381 @@
@@ -0,0 +1,381 @@
+// ═══════════════════════════════════════════════════════════════
+// OpenVibe — the Dock's Actor block (Actor v3 plan, stage A3; redrawn 2026-11-10)
+//
+// Your Actor at the top of the Dock's Agents tab, on every OpenVibe site. dock.js loads this file the first time that
+// tab opens:
+// OpenVibeDockActor.mount(container, { token, base, active }) → { refresh(), destroy() }
+//
+// It reads OpenVibe.Actor's GET /api/v1/me/actor with the person's Network token (scope actor.me.read); the answers,
+// pause, resume and tell are POSTs with actor.me.write. On a site every call goes through Network's same-origin relay
+// (/api/frame/actor…) because every site's connect-src allows openvibe.network and refuses openvibe.actor; `base`
+// reaches Actor itself in a test, and `frameBase` names the relay. Nothing is asked without a token.
+//
+// The look (the owner, 2026-11-10: "more intuitive, minimalist, elegant"): one row and one line to type in.
+// - Who: the face with a ring in the colour of its state, the name, and one quiet line (the state, today's spend
+// against the cap). The row is the way to the Actor's page; there is no separate "Open".
+// - One action beside it, the one that fits: "Resume" in the accent when it is paused, a quiet pause when it runs.
+// - What it does, only when there is something to say: the step it is on (a soft pulse), what it looks at, the
+// steps before it, and what comes next, as a short line of dots. A paused Actor shows none of this: the row says it.
+// - What it asks you, with its own buttons, in the warning tone (a lean and a glow, never a bar).
+// - One field to tell it something, with send inside the field.
+// A person with no Actor sees nothing here. Everything people or the Actor wrote is set as text, never markup. The
+// block is built once: a refresh repaints what changed and never touches the line being typed. Every control is 44 px.
+// ═══════════════════════════════════════════════════════════════
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockActor) return;
+
+ const POLL_MS = 15000;
+ const ACTOR_REV = { 'ov-window.js': 'd3d8f9e3e6d2', 'agent-sheet.js': '8d150490ad64' };
+ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';
+ const STATE = { working: 'Working', resting: 'Resting', paused: 'Paused', waiting: 'Waiting for you' };
+ // The lines Actor sends as "the current step" when there is none: the state already says them.
+ const NOT_A_STEP = /^(paused|resting\b.*)\.?$/i;
+ const ICON = {
+ play: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8.5 5.6v12.8a.9.9 0 0 0 1.37.77l10.3-6.4a.9.9 0 0 0 0-1.54l-10.3-6.4a.9.9 0 0 0-1.37.77z" fill="currentColor"/></svg>',
+ pause: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6.5" y="5" width="4" height="14" rx="1.3" fill="currentColor"/><rect x="13.5" y="5" width="4" height="14" rx="1.3" fill="currentColor"/></svg>',
+ go: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 6.5 15 12l-5.5 5.5"/></svg>',
+ send: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5.5M6 11l6-6 6 6"/></svg>',
+ ask: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.2 9.3a2.9 2.9 0 1 1 4.4 2.5c-.9.6-1.6 1.2-1.6 2.3"/><path d="M12 17.6v.1"/></svg>',
+ past: '<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12a8 8 0 1 0 2.6-5.9"/><path d="M4 5v3.6h3.6"/><path d="M12 8.2V12l2.6 1.7"/></svg>',
+ };
+
+ function styles(doc) {
+ const d = doc || document;
+ if (!d || d.getElementById('ovdac-styles')) return;
+ const st = d.createElement('style');
+ st.id = 'ovdac-styles';
+ st.textContent = `
+ .ovdac { display: none; flex: none; padding: 12px 12px 8px; border-bottom: 1px solid var(--border, rgba(255,255,255,.08));
+ --ovdac-ok: var(--success, #22c55e); --ovdac-warn: var(--warning, #f5a524); --ovdac-ink: var(--text-primary, #ececf1); }
+ .ovdac.on { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
+ .ovdac [hidden] { display: none !important; }
+ .ovdac svg { width: 18px; height: 18px; flex: none; }
+ .ovdac .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+
+ .ovdac-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
+ .ovdac-id { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 2px 8px 2px 4px; margin-left: -4px; border-radius: 14px;
+ color: inherit; text-decoration: none; transition: background .15s ease; }
+ .ovdac-id:hover { background: color-mix(in srgb, var(--ovdac-ink) 5%, transparent); }
+ .ovdac-id:focus-visible, .ovdac-go:focus-visible, .ovdac-btn:focus-visible, .ovdac-link:focus-visible, .ovdac-send:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 2px; }
+ .ovdac-face { position: relative; flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
+ background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font-weight: 800; font-size: 16px; transition: filter .2s ease, opacity .2s ease; }
+ .ovdac-face img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
+ .ovdac-face::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid transparent; transition: border-color .2s ease; }
+ .ovdac[data-state="working"] .ovdac-face::after { border-color: var(--ovdac-ok); animation: ovdac-ring 2.4s ease-in-out infinite; }
+ .ovdac[data-state="waiting"] .ovdac-face::after { border-color: var(--ovdac-warn); }
+ .ovdac[data-state="paused"] .ovdac-face { filter: saturate(.5); opacity: .78; }
+ .ovdac-who { min-width: 0; display: grid; gap: 1px; }
+ .ovdac-name { display: flex; align-items: center; gap: 1px; min-width: 0; font-weight: 750; font-size: 15px; line-height: 1.25; color: var(--ovdac-ink); }
+ .ovdac-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .ovdac-name svg { width: 16px; height: 16px; color: var(--text-muted, #8b8b96); transition: transform .18s ease, color .18s ease; }
+ .ovdac-id:hover .ovdac-name svg { transform: translateX(2px); color: var(--text-secondary, #b6b6c0); }
+ .ovdac-line { display: flex; gap: 6px; min-width: 0; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); white-space: nowrap; }
+ .ovdac-state { flex: none; font-weight: 650; color: var(--text-secondary, #b6b6c0); }
+ .ovdac[data-state="working"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-ok) 72%, var(--ovdac-ink)); }
+ .ovdac[data-state="waiting"] .ovdac-state { color: color-mix(in srgb, var(--ovdac-warn) 76%, var(--ovdac-ink)); }
+ .ovdac-spend { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
+ .ovdac-spend::before { content: '·'; margin-right: 6px; }
+
+ .ovdac-go { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 44px; height: 44px; padding: 0; border-radius: 999px;
+ border: 1px solid var(--border, rgba(255,255,255,.14)); background: transparent; color: var(--text-secondary, #b6b6c0);
+ font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, color .15s ease, transform .12s ease, box-shadow .2s ease; }
+ .ovdac-go:hover { color: var(--ovdac-ink); background: color-mix(in srgb, var(--ovdac-ink) 7%, transparent); }
+ .ovdac-go.is-main { padding: 0 18px 0 13px; border-color: transparent; background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff);
+ box-shadow: 0 8px 20px -10px var(--accent, #3b82f6); }
+ .ovdac-go.is-main:hover { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); filter: brightness(1.08); }
+ .ovdac-go:active, .ovdac-btn:active, .ovdac-send:active { transform: scale(.96); }
+ .ovdac .is-busy { opacity: .6; pointer-events: none; animation: ovdac-pulse 1.1s ease-in-out infinite; }
+
+ .ovdac-rail { list-style: none; margin: 0; padding: 2px 4px 2px 6px; display: grid; gap: 9px; }
+ .ovdac-rail li { position: relative; padding-left: 22px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }
+ .ovdac-rail li::before { content: ''; position: absolute; left: 1px; top: .36em; width: 6px; height: 6px; border-radius: 50%; border: 1.5px solid var(--text-muted, #8b8b96); }
+ .ovdac-rail li:not(:last-child)::after { content: ''; position: absolute; left: 5px; top: calc(.36em + 13px); bottom: -8px; width: 1px;
+ background: color-mix(in srgb, var(--ovdac-ink) 14%, transparent); }
+ .ovdac-rail .is-live { font-size: 13.5px; font-weight: 650; color: var(--ovdac-ink); }
+ .ovdac-rail .is-live::before { border-color: var(--ovdac-ok); background: var(--ovdac-ok); box-shadow: 0 0 0 4px color-mix(in srgb, var(--ovdac-ok) 20%, transparent); animation: ovdac-pulse 1.8s ease-in-out infinite; }
+ .ovdac-rail .is-next { color: var(--text-muted, #8b8b96); }
+ .ovdac-rail .is-next::before { border-style: dashed; }
+ .ovdac-rail b { font-weight: 650; }
+ .ovdac-look { display: block; margin-top: 2px; font-size: 12px; font-weight: 400; color: var(--text-muted, #8b8b96); }
+
+ .ovdac-ask { display: grid; gap: 10px; padding: 12px 14px 10px; border-radius: 16px;
+ background: linear-gradient(135deg, color-mix(in srgb, var(--ovdac-warn) 15%, transparent), color-mix(in srgb, var(--ovdac-warn) 5%, transparent) 62%);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ovdac-warn) 22%, transparent), 0 12px 28px -20px var(--ovdac-warn); }
+ .ovdac-ask-k { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; color: color-mix(in srgb, var(--ovdac-warn) 74%, var(--ovdac-ink)); }
+ .ovdac-ask-k i { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: color-mix(in srgb, var(--ovdac-warn) 24%, transparent); }
+ .ovdac-ask-k svg { width: 16px; height: 16px; }
+ .ovdac-ask p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ovdac-ink); overflow-wrap: anywhere; }
+ .ovdac-ask .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
+ .ovdac-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 16px; border-radius: 999px;
+ border: 1px solid color-mix(in srgb, var(--ovdac-ink) 18%, transparent); background: transparent; color: var(--ovdac-ink);
+ font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s ease, transform .12s ease; }
+ .ovdac-btn:hover { background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); }
+ .ovdac-btn.is-yes { background: var(--accent-strong, var(--accent, #2563eb)); border-color: transparent; color: var(--on-accent-strong, #fff); }
+ .ovdac-btn.is-yes:hover { background: var(--accent-strong, var(--accent, #2563eb)); filter: brightness(1.08); }
+ .ovdac-btn.is-quiet { padding: 0 10px; border-color: transparent; color: var(--text-secondary, #b6b6c0); font-weight: 650; }
+ .ovdac-more { display: inline-flex; align-items: center; min-height: 44px; margin: -8px 0 -8px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); text-decoration: underline; text-underline-offset: 3px; }
+
+ .ovdac-tell { position: relative; display: flex; }
+ .ovdac-tell input { flex: 1; min-width: 0; height: 52px; box-sizing: border-box; padding: 0 58px 0 18px; border-radius: 26px;
+ border: 1px solid var(--border, rgba(255,255,255,.14)); background: var(--bg-input, color-mix(in srgb, var(--ovdac-ink) 4%, transparent));
+ color: var(--ovdac-ink); font: inherit; font-size: 14.5px; outline: none; transition: border-color .15s ease, box-shadow .15s ease; }
+ .ovdac-tell input::placeholder { color: var(--text-muted, #8b8b96); }
+ .ovdac-tell input:focus { border-color: color-mix(in srgb, var(--accent, #3b82f6) 62%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }
+ .ovdac-send { position: absolute; right: 4px; top: 4px; display: grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; border-radius: 50%;
+ background: color-mix(in srgb, var(--ovdac-ink) 8%, transparent); color: var(--text-muted, #8b8b96); cursor: pointer;
+ transition: background .18s ease, color .18s ease, transform .12s ease, box-shadow .2s ease; }
+ .ovdac-tell.has-text .ovdac-send { background: var(--accent-strong, var(--accent, #2563eb)); color: var(--on-accent-strong, #fff); box-shadow: 0 6px 16px -8px var(--accent, #3b82f6); }
+
+ .ovdac-foot { display: flex; align-items: center; gap: 10px; min-width: 0; margin-top: -6px; }
+ .ovdac-link { flex: none; display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 0 8px; margin-left: -8px; border: 0; border-radius: 10px;
+ background: none; color: var(--text-muted, #8b8b96); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; transition: color .15s ease; }
+ .ovdac-link:hover, .ovdac-link[aria-expanded="true"] { color: var(--ovdac-ink); }
+ .ovdac-link svg { width: 15px; height: 15px; }
+ .ovdac-note { flex: 1; min-width: 0; text-align: right; font-size: 12.5px; line-height: 1.35; color: var(--text-muted, #8b8b96); overflow-wrap: anywhere; }
+ .ovdac-note.is-ok { color: color-mix(in srgb, var(--ovdac-ok) 70%, var(--ovdac-ink)); }
+ .ovdac-note.is-bad { color: color-mix(in srgb, var(--danger, #ef4444) 74%, var(--ovdac-ink)); }
+ .ovdac-hist { list-style: none; margin: -4px 0 4px; padding: 0; display: grid; gap: 6px; font-size: 12.5px; line-height: 1.4; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }
+ .ovdac-hist b { margin-right: 6px; font-weight: 650; color: var(--text-muted, #8b8b96); }
+
+ @keyframes ovdac-pulse { 50% { opacity: .45; } }
+ @keyframes ovdac-ring { 50% { opacity: .35; transform: scale(1.06); } }
+ @media (pointer: coarse) { .ovdac-tell input { font-size: 16px; } }
+ html[data-ov-motion=off] .ovdac *, html[data-ov-motion=off] .ovdac *::before, html[data-ov-motion=off] .ovdac *::after { animation: none !important; transition: none !important; }
+ `;
+ (d.head || d.documentElement).appendChild(st);
+ }
+
+ const money = (n) => { const v = Number(n) || 0; return v < 0.01 && v > 0 ? `$${v.toFixed(3)}` : `$${v.toFixed(2)}`; };
+ /** "in 40 min", "in 3 h", "tomorrow": when the next goal is due; '' when unknown or already due. */
+ function due(at, now = Date.now()) {
+ const t = Date.parse(at || '');
+ if (!Number.isFinite(t)) return '';
+ const min = Math.round((t - now) / 60000);
+ if (min < 1) return '';
+ if (min < 60) return `in ${min} min`;
+ if (min < 24 * 60) return `in ${Math.round(min / 60)} h`;
+ return min < 48 * 60 ? 'tomorrow' : `in ${Math.round(min / 1440)} days`;
+ }
+
+ function mount(container, opts = {}) {
+ const d = container.ownerDocument || document;
+ styles(d);
+ const o = { ...opts };
+ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
+ const icon = (name) => { const t = d.createElement('template'); t.innerHTML = ICON[name]; return t.content.firstChild; };
+ const tip = (n, text) => { if (root.OVTip) n.setAttribute('data-ov-tip', text); else n.title = text; };
+ let gone = false, timer = 0, busy = false, shown = false, last = '', actor = null, noteTimer = 0;
+ container.replaceChildren();
+ container.classList.add('ovdac');
+ // Network's frame relay on a site; an explicit base reaches Actor itself (a test).
+ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';
+ const frameBase = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const endpoint = (path) => (direct ? direct + '/api/v1/me/actor' + path : frameBase + '/actor' + path);
+ // The way to the Actor's page is a navigation, never a fetch: Actor's own site, wherever the API is.
+ const openBase = direct || 'https://openvibe.actor';
+
+ async function api(path, init = {}) {
+ const t = typeof o.token === 'function' ? await o.token() : o.token;
+ if (!t) throw Object.assign(new Error('Sign in to use your Actor'), { status: 401 });
+ const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), authorization: `Bearer ${t}` };
+ const r = await fetch(endpoint(path), { method: init.method || 'GET', headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });
+ const b = await r.json().catch(() => ({}));
+ if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
+ return b;
+ }
+
+ function stateOf(a) {
+ if (Array.isArray(a.asks) && a.asks.length) return 'waiting';
+ return STATE[a.status] ? a.status : 'paused';
+ }
+
+ // ── The parts, built once ────────────────────────────────────────────────────────────────────────────────
+ const head = h('div', 'ovdac-head');
+ const id = h('a', 'ovdac-id'); id.href = openBase + '/you'; id.target = '_blank'; id.rel = 'noopener';
+ function loadScript(file, ready) {
+ if (ready()) return Promise.resolve(true);
+ return new Promise((resolve) => {
+ const sid = `ovdac-${file}`;
+ let script = d.getElementById(sid);
+ if (!script) { script = d.createElement('script'); script.id = sid; script.src = SCRIPT_BASE + file + (ACTOR_REV[file] ? `?v=${ACTOR_REV[file]}` : ''); script.async = true; d.head.appendChild(script); }
+ script.addEventListener('load', () => resolve(!!ready()), { once: true });
+ script.addEventListener('error', () => resolve(false), { once: true });
+ });
+ }
+ // The row opens its sheet (agent-sheet.js in the one window: now, asks, work, schedule, memory, computer); the
+ // Actor's page is the sheet's "Open". A modified click, or a sheet that cannot load, goes to the page as before.
+ id.addEventListener('click', async (e) => {
+ if (!actor || !(actor.agent_id || actor.id) || e.metaKey || e.ctrlKey || e.shiftKey || e.button) return;
+ e.preventDefault();
+ const ok = (await Promise.all([loadScript('ov-window.js', () => !!root.OVWindow), loadScript('agent-sheet.js', () => !!root.OVAgentSheet)])).every(Boolean);
+ if (!ok) { d.defaultView.open(id.href, '_blank', 'noopener'); return; }
+ root.OVAgentSheet.open({ kind: 'actor', id: actor.agent_id || actor.id, name: actor.name || 'Your Actor', token: o.token,
+ frameBase: o.frameBase, base: direct || null, from: id, openHref: openBase + '/you' });
+ });
+ const face = h('span', 'ovdac-face');
+ const who = h('span', 'ovdac-who');
+ const name = h('span', 'ovdac-name'); const nameText = h('span'); name.append(nameText, icon('go'));
+ const line = h('span', 'ovdac-line'); const stateText = h('span', 'ovdac-state'); const spend = h('span', 'ovdac-spend'); line.append(stateText, spend);
+ who.append(name, line); id.append(face, who);
+ const go = h('button', 'ovdac-go'); go.type = 'button';
+ go.addEventListener('click', () => {
+ if (!actor) return;
+ const paused = actor.status === 'paused';
+ act(paused ? '/resume' : '/pause', {}, go, paused ? 'Resumed.' : 'Paused.');
+ });
+ head.append(id, go);
+
+ const rail = h('ul', 'ovdac-rail'); rail.hidden = true;
+ const askBox = h('div', 'ovdac-ask'); askBox.hidden = true;
+
+ const form = h('form', 'ovdac-tell');
+ const input = h('input'); input.type = 'text'; input.maxLength = 2000; input.autocomplete = 'off'; input.enterKeyHint = 'send';
+ input.placeholder = 'Tell your Actor something'; input.setAttribute('aria-label', 'Tell your Actor something');
+ const send = h('button', 'ovdac-send'); send.type = 'submit'; send.setAttribute('aria-label', 'Send'); send.append(icon('send'));
+ input.addEventListener('input', () => form.classList.toggle('has-text', !!input.value.trim()));
+ form.append(input, send);
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const text = input.value.trim();
+ if (!text) { input.focus(); return; }
+ const paused = actor && actor.status === 'paused';
+ act('/tell', { text }, send, paused ? 'Sent. Your Actor reads it once you resume it.' : 'Sent. Your Actor reads it at its next step.')
+ .then((ok) => { if (ok) { input.value = ''; form.classList.remove('has-text'); } });
+ });
+
+ const foot = h('div', 'ovdac-foot');
+ const past = h('button', 'ovdac-link'); past.type = 'button'; past.setAttribute('aria-expanded', 'false'); past.append(icon('past'), d.createTextNode('Recent history'));
+ const note = h('div', 'ovdac-note'); note.setAttribute('role', 'status'); note.setAttribute('aria-live', 'polite');
+ const hist = h('ul', 'ovdac-hist'); hist.hidden = true;
+ foot.append(past, note);
+ // The button that opens the list closes it.
+ past.addEventListener('click', async () => {
+ if (!hist.hidden) { hist.hidden = true; past.setAttribute('aria-expanded', 'false'); return; }
+ past.disabled = true; past.classList.add('is-busy');
+ const one = (text) => { hist.replaceChildren(h('li', '', text)); };
+ try {
+ const t = typeof o.token === 'function' ? await o.token() : o.token;
+ if (!t) throw new Error('Sign in to read history.');
+ const url = String(o.networkBase || 'https://openvibe.network').replace(/\/+$/, '') + '/api/v1/history/agents/recent?limit=10';
+ const response = await fetch(url, { headers: { accept: 'application/json', authorization: `Bearer ${t}` }, credentials: 'omit' });
+ const result = await response.json().catch(() => ({}));
+ if (response.status === 403) one('Your agent history setting is off. Turn it on in My settings to allow this read.');
+ else if (!response.ok) one('Recent history is unavailable.');
+ else {
+ const items = (result.items || []).slice(0, 10);
+ if (!items.length) one('No recent history.');
+ else hist.replaceChildren(...items.map((x) => { const li = h('li'); li.append(h('b', '', String(x.type || '')), d.createTextNode(String(x.title || ''))); return li; }));
+ }
+ } catch { one('Recent history is unavailable.'); }
+ finally { past.disabled = false; past.classList.remove('is-busy'); }
+ hist.hidden = false; past.setAttribute('aria-expanded', 'true');
+ });
+
+ /** One line under the field: what just happened. A good word goes by itself after a moment; a problem stays. */
+ function say(text, tone) {
+ clearTimeout(noteTimer);
+ note.textContent = text || '';
+ note.className = 'ovdac-note' + (tone ? ` is-${tone}` : '');
+ if (text && tone === 'ok') noteTimer = setTimeout(() => { note.textContent = ''; note.className = 'ovdac-note'; }, 6000);
+ }
+
+ // ── A refresh repaints the row, the steps and the ask; the field and the history stay as they are ────────
+ function paint(a) {
+ if (!a || (a.enabled === false && !a.id)) {
+ actor = null; shown = false; last = '';
+ container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state');
+ return;
+ }
+ actor = a;
+ if (!shown) { container.replaceChildren(head, rail, askBox, form, foot, hist); container.classList.add('on'); shown = true; }
+ const sig = JSON.stringify([a.name, a.status, a.avatar, a.brain, a.caps, a.now, a.asks]);
+ if (sig === last) return;
+ last = sig;
+ const state = stateOf(a), now = a.now || {};
+ container.setAttribute('data-state', state);
+
+ // Who, and the one line about it.
+ const label = String(a.name || 'Your Actor');
+ const pic = a.avatar && typeof a.avatar.url === 'string' && /^https:\/\//.test(a.avatar.url) ? a.avatar.url : '';
+ if (pic) { const img = h('img'); img.alt = ''; img.src = pic; face.replaceChildren(img); } else face.replaceChildren(d.createTextNode(label.trim().charAt(0).toUpperCase() || 'A'));
+ nameText.textContent = label;
+ id.setAttribute('aria-label', `${label}, ${STATE[state]}. Open your Actor's page`);
+ stateText.textContent = STATE[state];
+ const cap = a.caps && Number(a.caps.usd) > 0 ? ` of ${money(a.caps.usd)}` : '';
+ spend.textContent = `${money(now.cost_today)}${cap} today`;
+ tip(spend, `Model: ${a.brain && a.brain !== 'auto' ? a.brain : 'Auto'}`);
+
+ // The one action that fits.
+ const paused = a.status === 'paused';
+ go.className = 'ovdac-go' + (paused ? ' is-main' : '');
+ go.replaceChildren(icon(paused ? 'play' : 'pause'), h('span', paused ? '' : 'sr', paused ? 'Resume' : 'Pause'));
+ tip(go, paused ? 'Your Actor does nothing until you resume it' : 'Pause your Actor');
+
+ // What it does: the step it is on, the steps before it, what comes next. Nothing when there is nothing to say.
+ const asks = Array.isArray(a.asks) ? a.asks : [];
+ const ask = asks[0];
+ const step = String(now.current_step || '').trim();
+ const live = a.status === 'working' && step && !NOT_A_STEP.test(step) && !(ask && step === String(ask.text || '').trim()) ? step : '';
+ const items = [];
+ if (live) {
+ const li = h('li', 'is-live'); li.append(d.createTextNode(live));
+ const looking = now.looking_at && (now.looking_at.title || now.looking_at);
+ if (looking) li.append(h('span', 'ovdac-look', `Looking at ${String(looking)}`));
+ items.push(li);
+ }
+ const before = (Array.isArray(now.last_three_steps) ? now.last_three_steps : []).map((x) => String((x && x.text) || x || '').trim()).filter((t) => t && t !== live).slice(0, 3);
+ if (!paused) before.forEach((t) => items.push(h('li', '', t)));
+ if (!paused && now.next && (now.next.text || typeof now.next === 'string')) {
+ const li = h('li', 'is-next'); const when = due(now.next.at);
+ li.append(h('b', '', 'Next: '), d.createTextNode(String(now.next.text || now.next) + (when ? `, ${when}` : '')));
+ items.push(li);
+ }
+ rail.replaceChildren(...items); rail.hidden = !items.length;
+
+ // The first thing it asks, with its own buttons.
+ if (ask) {
+ const k = h('div', 'ovdac-ask-k'); const chip = h('i'); chip.append(icon('ask')); k.append(chip, d.createTextNode(`${label} asks`));
+ const row = h('div', 'row');
+ const options = Array.isArray(ask.options) && ask.options.length ? ask.options : [{ id: 'yes', label: 'Yes' }];
+ options.slice(0, 3).forEach((op, i) => {
+ const b = h('button', `ovdac-btn${i === 0 ? ' is-yes' : ''}`, String(op.label || op)); b.type = 'button';
+ b.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/answer`, { option: op.id || op }, b, 'Answered.'));
+ row.append(b);
+ });
+ const no = h('button', 'ovdac-btn is-quiet', 'Not now'); no.type = 'button';
+ no.addEventListener('click', () => act(`/asks/${encodeURIComponent(ask.id)}/decline`, {}, no));
+ row.append(no);
+ askBox.replaceChildren(k, h('p', '', String(ask.text || '')), row);
+ if (asks.length > 1) { const more = h('a', 'ovdac-more', `${asks.length - 1} more on your Actor's page`); more.href = openBase + '/you'; more.target = '_blank'; more.rel = 'noopener'; askBox.append(more); }
+ } else askBox.replaceChildren();
+ askBox.hidden = !ask;
+ }
+
+ async function act(path, body, btn, done) {
+ if (busy) return false;
+ busy = true; if (btn) { btn.disabled = true; btn.classList.add('is-busy'); }
+ say('');
+ try {
+ await api(path, { method: 'POST', json: true, body });
+ last = '';
+ await refresh();
+ if (done) say(done, 'ok');
+ return true;
+ } catch (err) { say(`That did not go through: ${err.message}`, 'bad'); return false; }
+ finally { busy = false; if (btn) { btn.disabled = false; btn.classList.remove('is-busy'); } }
+ }
+
+ async function refresh() {
+ if (gone) return;
+ if (o.active && !o.active()) return;
+ try { const data = await api(''); if (!gone) paint(data); }
+ catch (err) { if (!gone && (err.status === 401 || err.status === 403 || err.status === 404)) paint(null); }
+ }
+ refresh();
+ timer = setInterval(refresh, POLL_MS);
+ return { refresh, destroy() { gone = true; clearInterval(timer); clearTimeout(noteTimer); container.replaceChildren(); container.classList.remove('on'); container.removeAttribute('data-state'); } };
+ }
+
+ root.OpenVibeDockActor = { mount };
+})(typeof window !== 'undefined' ? window : globalThis);
@@ -1,19 +1,22 @@
@@ -1,19 +1,22 @@
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
-// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0
+// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0; Float 4.154.0
//
//
// The person's own agent desk stream at the top of the Dock's Agents tab. dock.js loads this file the first time that
// The person's own agent desk stream at the top of the Dock's Agents tab. dock.js loads this file the first time that
// tab opens:
// tab opens:
// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }
// OpenVibeDockAgent.mount(container, { token, base, active }) → { refresh(), destroy() }
//
//
-// `base` is OpenVibe.Codes' API (GET /api/v1/me/agent-view, the same POST for pause, Curtain now, mute and End, and
-// the frame). Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the
+// The read is OpenVibe.Codes' GET /api/v1/me/agent-view (the same POST for pause, Curtain now, mute and End, and the
+// frame). It asks Network's same-origin relay (/api/frame/agent-view…) because every site's connect-src allows
+// openvibe.network and refuses openvibe.codes; `frameBase` overrides the relay, and `base` reaches Codes itself in a
+// test or the extension (an explicit base wins). Nothing is asked when there is no token: a signed-out page can never
+// read the view. Agent Streams S5 adds the four controls: a moderator cannot resume the owner's pause on Live, and the
// Dock is the owner's own view, so it may always resume.
// Dock is the owner's own view, so it may always resume.
// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —
// Codes mints the view ticket server-side with the person's own token — an iframe on another site has no Live cookie —
// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the
// so the frame it answers is already Live's /embed/agent/<ags_id>?vt=wvt_…: the picture, the "AI agent" label, the
// curtain state and the viewer count. The embed renews its own ticket while it plays.
// curtain state and the viewer count. The embed renews its own ticket while it plays.
//
//
// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.
// Nothing live: the last frame through the same API (an <img> cannot carry a token), with the task and the worked time.
-// There is no step line yet: steps are Agent Streams S2, and this view shows none until Live serves them.
+// Live's S2 current step comes through Codes when available; an older stream simply has no step line.
// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.
// Everything is set as text, never markup; the frame's address is the one Codes answered, as an https Live address.
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
(function (root) {
(function (root) {
@@ -55,6 +58,7 @@
@@ -55,6 +58,7 @@
sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',
sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',
stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',
stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',
open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',
open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',
+ float: '<rect x="3.5" y="5" width="17" height="14" rx="2.5"/><rect x="11.5" y="11" width="7" height="6" rx="1.2" fill="currentColor" stroke="none"/>',
};
};
// The style goes into the document the view lives in: the page, or the Dock's own window when it is popped out.
// The style goes into the document the view lives in: the page, or the Dock's own window when it is popped out.
@@ -69,6 +73,7 @@
@@ -69,6 +73,7 @@
.ovda-stage { position: relative; overflow: hidden; aspect-ratio: 16 / 9; border-radius: 14px; background: #0a0c11; border: 1px solid var(--border, rgba(255,255,255,.1)); }
.ovda-stage { position: relative; overflow: hidden; aspect-ratio: 16 / 9; border-radius: 14px; background: #0a0c11; border: 1px solid var(--border, rgba(255,255,255,.1)); }
.ovda-frame, .ovda-still { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; object-fit: contain; background: #0a0c11; }
.ovda-frame, .ovda-still { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; object-fit: contain; background: #0a0c11; }
.ovda-still { object-fit: cover; opacity: .92; }
.ovda-still { object-fit: cover; opacity: .92; }
+ .ovda-stage[hidden] { display: none; }
.ovda-wait { position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-muted, #8b8b96); font-size: 12.5px; text-align: center; padding: 12px; }
.ovda-wait { position: absolute; inset: 0; display: grid; place-items: center; color: var(--text-muted, #8b8b96); font-size: 12.5px; text-align: center; padding: 12px; }
.ovda-line { display: flex; align-items: center; gap: 6px; min-height: 34px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }
.ovda-line { display: flex; align-items: center; gap: 6px; min-height: 34px; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }
.ovda-line b { color: var(--text-primary, #ececf1); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovda-line b { color: var(--text-primary, #ececf1); font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -87,9 +92,14 @@
@@ -87,9 +92,14 @@
.ovda-acts button:disabled { opacity: .5; cursor: default; }
.ovda-acts button:disabled { opacity: .5; cursor: default; }
.ovda-acts svg { width: 14px; height: 14px; }
.ovda-acts svg { width: 14px; height: 14px; }
.ovda-acts button[data-act="end"] { color: color-mix(in srgb, var(--live-red, #e5484d) 65%, var(--text-primary, #ececf1)); }
.ovda-acts button[data-act="end"] { color: color-mix(in srgb, var(--live-red, #e5484d) 65%, var(--text-primary, #ececf1)); }
+ /* A session that ended is two lines of words with its one way out beside them, not a button on a row of its own. */
+ .ovda.is-ended .ovda-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px; align-items: center; padding: 2px 4px 6px; }
+ .ovda.is-ended .ovda-line, .ovda.is-ended .ovda-meta { grid-column: 1; }
+ .ovda.is-ended .ovda-acts { grid-column: 2; grid-row: 1 / span 2; margin: 0; }
+ .ovda.is-ended .ovda-acts:empty { display: none; }
+ @media (pointer: coarse) { .ovda-acts button, .ovda-acts a { min-height: 44px; min-width: 44px; justify-content: center; } }
.ovda svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ovda svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
@keyframes ovda-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes ovda-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
- @media (prefers-reduced-motion: reduce) { .ovda-line .dot.live { animation: none; } }
`;
`;
(d.head || d.documentElement).appendChild(st);
(d.head || d.documentElement).appendChild(st);
}
}
@@ -105,30 +115,35 @@
@@ -105,30 +115,35 @@
const o = { ...opts };
const o = { ...opts };
const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const codesBase = String(o.base || CODES).replace(/\/+$/, '');
const codesBase = String(o.base || CODES).replace(/\/+$/, '');
+ // Network's frame relay on a site; an explicit base reaches Codes itself (a test, or the extension's panel).
+ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';
+ const frameBase = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const endpoint = (path) => (direct ? direct + path : frameBase + path.replace(/^\/api\/v1\/me\/agent-view/, '/agent-view'));
let gone = false, timer = 0, busy = false, agent = null, frameUrl = null, lastFrame = '';
let gone = false, timer = 0, busy = false, agent = null, frameUrl = null, lastFrame = '';
let readOnly = false, framedFor = '', frameRefused = '', frameTimer = 0;
let readOnly = false, framedFor = '', frameRefused = '', frameTimer = 0;
container.innerHTML = '';
container.innerHTML = '';
container.classList.add('ovda');
container.classList.add('ovda');
const stage = h('div', 'ovda-stage');
const stage = h('div', 'ovda-stage');
const line = h('div', 'ovda-line');
const line = h('div', 'ovda-line');
+ const step = h('div', 'ovda-meta ovda-step');
const meta = h('div', 'ovda-meta');
const meta = h('div', 'ovda-meta');
const watch = h('a', 'ovda-watch', 'Watch on openvibe.live');
const watch = h('a', 'ovda-watch', 'Watch on openvibe.live');
watch.target = '_blank';
watch.target = '_blank';
watch.rel = 'noopener';
watch.rel = 'noopener';
watch.hidden = true;
watch.hidden = true;
const acts = h('div', 'ovda-acts');
const acts = h('div', 'ovda-acts');
- const head = h('div');
- head.append(line, meta, watch, acts);
+ const head = h('div', 'ovda-head');
+ head.append(line, meta, step, watch, acts);
const body = h('div');
const body = h('div');
body.append(stage, head);
body.append(stage, head);
container.append(body);
container.append(body);
async function api(path, init = {}) {
async function api(path, init = {}) {
- const base = String(o.base || CODES).replace(/\/+$/, '');
const t = typeof o.token === 'function' ? await o.token() : o.token;
const t = typeof o.token === 'function' ? await o.token() : o.token;
readOnly = readOnlyToken(t);
readOnly = readOnlyToken(t);
- const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };
- const r = await fetch(base + path, { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });
+ if (!t) throw Object.assign(new Error('Sign in to see your agent'), { status: 401 });
+ const headers = { accept: 'application/json', ...(init.json ? { 'content-type': 'application/json' } : {}), authorization: `Bearer ${t}` };
+ const r = await fetch(endpoint(path), { ...init, headers, credentials: 'omit', body: init.json ? JSON.stringify(init.body || {}) : undefined });
const b = await r.json().catch(() => ({}));
const b = await r.json().catch(() => ({}));
if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
if (!r.ok) throw Object.assign(new Error(b.detail || `HTTP ${r.status}`), { status: r.status });
return b;
return b;
@@ -138,8 +153,10 @@
@@ -138,8 +153,10 @@
function still(url) {
function still(url) {
if (!url || frameUrl === url) return;
if (!url || frameUrl === url) return;
frameUrl = url;
frameUrl = url;
- Promise.resolve(typeof o.token === 'function' ? o.token() : o.token).then((t) => fetch(url, { headers: t ? { authorization: `Bearer ${t}` } : {}, credentials: 'omit' }))
- .then((r) => (r.ok ? r.blob() : null))
+ Promise.resolve(typeof o.token === 'function' ? o.token() : o.token).then((t) => (t
+ ? fetch(endpoint('/api/v1/me/agent-view/frame'), { headers: { authorization: `Bearer ${t}`, accept: 'image/jpeg, image/webp' }, credentials: 'omit' })
+ : null))
+ .then((r) => (r && r.ok ? r.blob() : null))
.then((blob) => {
.then((blob) => {
if (!blob || gone || frameUrl !== url) return;
if (!blob || gone || frameUrl !== url) return;
const next = URL.createObjectURL(blob);
const next = URL.createObjectURL(blob);
@@ -164,6 +181,7 @@
@@ -164,6 +181,7 @@
const live = agent.state === 'live';
const live = agent.state === 'live';
const ended = agent.state === 'ended';
const ended = agent.state === 'ended';
+ container.classList.toggle('is-ended', ended);
// The picture: the live embed when there is one and it may be framed, else the still and a plain link.
// The picture: the live embed when there is one and it may be framed, else the still and a plain link.
// A refused frame looks loaded to its parent (the browser shows its own error page), so the embed
// A refused frame looks loaded to its parent (the browser shows its own error page), so the embed
// announces itself (Live's agent-embed.js); no hello in time means frame-ancestors kept it out, and the
// announces itself (Live's agent-embed.js); no hello in time means frame-ancestors kept it out, and the
@@ -192,14 +210,17 @@
@@ -192,14 +210,17 @@
if (agent.last_frame_url) {
if (agent.last_frame_url) {
if (!stage.querySelector('img.ovda-still')) frameUrl = null;
if (!stage.querySelector('img.ovda-still')) frameUrl = null;
still(agent.last_frame_url);
still(agent.last_frame_url);
- } else if (!stage.querySelector('img.ovda-still') && !stage.querySelector('.ovda-wait')) {
- stage.append(h('div', 'ovda-wait', ended ? 'Nothing to show' : 'Waiting for the desk'));
+ } else if (!ended && !stage.querySelector('img.ovda-still') && !stage.querySelector('.ovda-wait')) {
+ stage.append(h('div', 'ovda-wait', 'Waiting for the desk'));
}
}
// The plain way to the stream, when this browser cannot frame it. It is a real link, not a control.
// The plain way to the stream, when this browser cannot frame it. It is a real link, not a control.
const page = typeof agent.page_url === 'string' && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;
const page = typeof agent.page_url === 'string' && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;
if (page) watch.href = page;
if (page) watch.href = page;
watch.hidden = !blocked || !page;
watch.hidden = !blocked || !page;
}
}
+ // A session that ended and left no picture has no stage: a black box saying "Nothing to show" told nobody
+ // anything (the owner, 2026-11-10). The line under it still says what it was and how long it worked.
+ stage.hidden = ended && !embed && !agent.last_frame_url && !stage.querySelector('img');
// The line: what it is doing, one row.
// The line: what it is doing, one row.
line.innerHTML = '';
line.innerHTML = '';
@@ -211,6 +232,11 @@
@@ -211,6 +232,11 @@
else title.textContent = text;
else title.textContent = text;
line.append(title);
line.append(title);
+ const current = agent.current_step;
+ step.textContent = current && typeof current.title === 'string' && current.title.trim()
+ ? `Step: ${current.title} · ${current.status || 'started'}` : '';
+ step.hidden = !step.textContent;
+
// The meta: the state, the viewers, the curtain, the worked time.
// The meta: the state, the viewers, the curtain, the worked time.
meta.textContent = '';
meta.textContent = '';
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
const bits = [live ? 'Live' : ended ? 'Ended' : 'Waiting for the desk'];
@@ -246,6 +272,16 @@
@@ -246,6 +272,16 @@
if (live && !(agent.curtain && agent.curtain.up)) acts.append(control('curtain', `${svg(G.eye)}Curtain now`, 'Hide the screen now; your desk lifts it when it is clear'));
if (live && !(agent.curtain && agent.curtain.up)) acts.append(control('curtain', `${svg(G.eye)}Curtain now`, 'Hide the screen now; your desk lifts it when it is clear'));
if (!ended) acts.append(control('mute', `${svg(agent.voice_muted ? G.sound : G.mute)}${agent.voice_muted ? 'Unmute' : 'Mute'}`, agent.voice_muted ? 'Let the agent speak again' : 'Mute the agent\'s voice'));
if (!ended) acts.append(control('mute', `${svg(agent.voice_muted ? G.sound : G.mute)}${agent.voice_muted ? 'Unmute' : 'Mute'}`, agent.voice_muted ? 'Let the agent speak again' : 'Mute the agent\'s voice'));
if (!ended) acts.append(control('end', `${svg(G.stop)}End`, 'End the agent stream'));
if (!ended) acts.append(control('end', `${svg(G.stop)}End`, 'End the agent stream'));
+ // 4.154.0: Float puts the agent's screen in a floating card (overlay.js) that stays on screen as the person
+ // moves around this site and on to other OpenVibe sites, so the Dock can close while they watch.
+ if (embed && !ended) {
+ const b = h('button', null);
+ b.type = 'button';
+ b.dataset.act = 'float';
+ b.innerHTML = `${svg(G.float)}Float`;
+ b.title = 'Keep watching in a small window that follows you';
+ acts.append(b);
+ }
if (agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url)) {
if (agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url)) {
const a = h('a');
const a = h('a');
a.href = agent.page_url;
a.href = agent.page_url;
@@ -325,8 +361,19 @@
@@ -325,8 +361,19 @@
else if (b.dataset.act === 'curtain') control('curtain', b);
else if (b.dataset.act === 'curtain') control('curtain', b);
else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });
else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });
else if (b.dataset.act === 'end') control('end', b);
else if (b.dataset.act === 'end') control('end', b);
+ else if (b.dataset.act === 'float') floatIt(b);
});
});
+ function floatIt(btn) {
+ const embed = agent && typeof agent.embed_url === 'string' && EMBED.test(agent.embed_url) ? agent.embed_url : null;
+ const nav = root.OpenVibeNavbar;
+ if (!embed || !nav || typeof nav.overlay !== 'function') { btn.title = 'Floating is not available on this page.'; return; }
+ const pageUrl = agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;
+ nav.overlay({ id: 'agent', src: embed, title: (agent.task && agent.task.title) || agent.title || 'Your agent', live: agent.state === 'live', href: pageUrl || undefined })
+ .then((card) => { if (card && root.OpenVibeDock && typeof root.OpenVibeDock.close === 'function') root.OpenVibeDock.close(); })
+ .catch(() => { btn.title = 'That did not work. Try again.'; });
+ }
+
refresh();
refresh();
return { refresh, destroy() { gone = true; clearTimeout(timer); clearTimeout(frameTimer); root.removeEventListener('message', onFrameHello); if (lastFrame) URL.revokeObjectURL(lastFrame); container.innerHTML = ''; container.classList.remove('ovda'); } };
return { refresh, destroy() { gone = true; clearTimeout(timer); clearTimeout(frameTimer); root.removeEventListener('message', onFrameHello); if (lastFrame) URL.revokeObjectURL(lastFrame); container.innerHTML = ''; container.classList.remove('ovda'); } };
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
+/**
+ * openvibe-shared/dock-card.js: a known OpenVibe link's card under a message in the Dock (4.147.0).
+ *
+ * OpenVibe.Chat resolves the card when a message is sent (its server/chat/link-cards.js) and the DM API carries it on
+ * the message as `card`: { kind, url, title, line, note?, image, action: { label, href } | null }, for a video on
+ * OpenVibe.Video ("Watch together"), a party invite ("Join the party"), a shared voice or a skill on OpenVibe.Actor.
+ * dock.js loads this file the first time an open conversation shows a message with a card, so the Dock that every
+ * signed-in page carries stays small.
+ *
+ * OpenVibeDockCard(card, h) → the card's node, or null when it is not a card this can draw
+ *
+ * Every field is data: set as text, cut to size, a link only when it is https, a picture only from openvibe.media.
+ * The card is compact (a 72 px picture beside the words) because the Dock is narrow; the message stays the thing said.
+ */
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockCard) return;
+
+ const CSS = `
+.ovdk-card { display: flex; align-items: center; gap: 10px; box-sizing: border-box; max-width: min(100%, 300px); margin-top: 4px; padding: 8px 10px 8px 8px; border-radius: 14px; border: 1px solid var(--border, rgba(255,255,255,.1)); background: var(--bg-primary, #12141b); text-align: left; }
+.ovdk-card-pic { flex: none; width: 72px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); }
+.ovdk-card-body { min-width: 0; display: grid; justify-items: start; gap: 2px; }
+.ovdk-card-t { font-size: 13.5px; font-weight: 700; line-height: 1.3; color: var(--text-primary, #ececf1); text-decoration: none; overflow-wrap: anywhere; }
+a.ovdk-card-t:hover { text-decoration: underline; }
+.ovdk-card-l { font-size: 12.5px; line-height: 1.35; color: var(--text-secondary, #b6b6c0); overflow-wrap: anywhere; }
+.ovdk-card-go { display: inline-flex; align-items: center; min-height: 32px; margin-top: 4px; padding: 0 12px; border-radius: 999px; font-size: 13px; font-weight: 700; text-decoration: none; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }
+.ovdk-card-go:hover { filter: brightness(1.08); }
+@media (pointer: coarse) { .ovdk-card-go { min-height: 44px; padding: 0 16px; } }
+`;
+
+ const https = (u) => (typeof u === 'string' && /^https:\/\/[^\s"'<>]+$/.test(u) ? u : null);
+ const cut = (v, n) => Array.from(String(v)).slice(0, n).join('');
+
+ function styles() {
+ if (document.getElementById('ovdk-card-styles')) return;
+ const st = document.createElement('style');
+ st.id = 'ovdk-card-styles';
+ st.textContent = CSS;
+ document.head.appendChild(st);
+ }
+
+ function card(c, h) {
+ if (!c || typeof c !== 'object' || typeof c.title !== 'string' || !c.title || typeof h !== 'function') return null;
+ styles();
+ const url = https(c.url);
+ const box = h('div', 'ovdk-card');
+ if (c.kind) box.dataset.kind = cut(c.kind, 20);
+ const pic = typeof c.image === 'string' && /^https:\/\/openvibe\.media\/[^\s"'<>]+$/.test(c.image) ? c.image : null;
+ if (pic) {
+ const img = h('img', 'ovdk-card-pic');
+ img.src = pic; img.alt = ''; img.loading = 'lazy'; img.decoding = 'async';
+ box.append(img);
+ }
+ const body = h('div', 'ovdk-card-body');
+ const title = h(url ? 'a' : 'b', 'ovdk-card-t', cut(c.title, 120));
+ if (url) { title.href = url; title.target = '_blank'; title.rel = 'noopener nofollow'; }
+ body.append(title);
+ if (typeof c.line === 'string' && c.line) body.append(h('span', 'ovdk-card-l', cut(c.line, 200)));
+ if (typeof c.note === 'string' && c.note) body.append(h('span', 'ovdk-card-l', cut(c.note, 160)));
+ const go = c.action && typeof c.action === 'object' && https(c.action.href) && typeof c.action.label === 'string' && c.action.label ? c.action : null;
+ if (go) {
+ const a = h('a', 'ovdk-card-go', cut(go.label, 40));
+ a.href = https(go.href); a.target = '_blank'; a.rel = 'noopener nofollow';
+ body.append(a);
+ }
+ box.append(body);
+ return box;
+ }
+
+ root.OpenVibeDockCard = card;
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+/* Edit events for an open Dock DM. Loaded when the conversation opens. */
+(function (root) {
+ 'use strict';
+ root.OpenVibeDockChatEdits = function ({ id, list, messages, error, tokenOf, chatCall, chatBase, active, load, h }) {
+ let socket = null, timer = null, stopped = false;
+ function apply(m) {
+ const old = messages.find((x) => Number(x.id) === Number(m && m.id));
+ const li = old && list.querySelector(`[data-id="${Number(m.id)}"]`);
+ if (!li || !m.edited_at || (old.edited_at && old.edited_at >= m.edited_at)) return;
+ Object.assign(old, { message: m.message, card: m.card, edited_at: m.edited_at });
+ li.querySelector('.ovdk-text').textContent = String(m.message || '');
+ let mark = li.querySelector('.ovdk-edited');
+ if (!mark) { mark = h('small', 'ovdk-edited'); li.append(mark); }
+ mark.textContent = `edited · ${String(m.edited_at).slice(11, 16)}`;
+ const card = li.querySelector('.ovdk-card'); if (card) card.remove();
+ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { if (ok && active() && li.isConnected) { const c = root.OpenVibeDockCard(m.card, h); if (c) li.append(c); } });
+ }
+ async function connect() {
+ if (stopped || !active() || !root.WebSocket) return;
+ const token = await tokenOf();
+ if (!token || stopped || !active()) return;
+ const ws = new root.WebSocket(chatBase().replace(/^http/, 'ws') + '/ws/chat');
+ socket = ws;
+ ws.onopen = () => ws.send(JSON.stringify({ type: 'join', token }));
+ ws.onmessage = (event) => { let frame; try { frame = JSON.parse(event.data); } catch { return; } if (frame.type === 'dm_edited' && String(frame.conversation_id) === id) apply(frame.message); };
+ ws.onclose = () => { if (socket === ws) socket = null; if (!stopped && active()) timer = setTimeout(connect, 3000); };
+ }
+ list.addEventListener('click', async (event) => {
+ if (!event.target.classList.contains('ovdk-edit')) return;
+ const li = event.target.closest('[data-id]');
+ const old = li && messages.find((m) => Number(m.id) === Number(li.dataset.id));
+ if (!old) return;
+ const body = root.prompt('Edit message', old.message);
+ if (body == null || body.trim() === old.message.trim()) return;
+ try { const result = await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages/${encodeURIComponent(old.id)}`, { method: 'PATCH', body: JSON.stringify({ message: body }) }); apply(result.message); }
+ catch (e) { error.textContent = e.message || 'Could not edit'; error.hidden = false; }
+ });
+ connect().catch(() => {});
+ return { stop() { stopped = true; clearTimeout(timer); if (socket) { socket.onclose = null; socket.close(); socket = null; } } };
+ };
+})(typeof window === 'undefined' ? globalThis : window);
new file mode 100644
new file mode 100644
@@ -0,0 +1,303 @@
@@ -0,0 +1,303 @@
+// ═══════════════════════════════════════════════════════════════
+// OpenVibe — the Code part (openvibe-shared 4.201.0). Ask 350, the owner: openvibe.codes "should be better interconnected
+// and shared … so that you can do chat, codes, and agents all in the same place and in one area". The menu's Vibe place
+// is Chat · Code · Agents, and the Dock has the same Code tab. dock.js loads this file the first time it opens:
+// OpenVibeDockCode.mount(container, { token, frameBase, base, codes, active, take }) → { refresh(), focus(), destroy() }
+// take() → the words another part handed over (Vibe's "Build it in Code"), which go in the box.
+//
+// One box to start a coding task, the way openvibe.codes starts one (Create: a new website, game or app; OpenVibe:
+// a change to OpenVibe itself; My code: a repository of yours), and your latest tasks under it, running ones first,
+// each with its state, its repository, how long ago, and Stop while it runs. The reads go through Network's same-origin
+// relay (/api/frame/codes-tasks…), because every site's connect-src allows openvibe.network and refuses openvibe.codes;
+// `base` reaches Codes itself in a test. While a task runs the list is read again every 8 seconds, only while the part
+// is on screen. When Codes cannot be reached, the box still works: it opens openvibe.codes with what you wrote in it.
+// Everything is set as text, never markup; a task's address is used only when it is on openvibe.codes.
+// ═══════════════════════════════════════════════════════════════
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockCode) return;
+
+ const CODES = 'https://openvibe.codes';
+ const SCRIPT_BASE = document.currentScript && document.currentScript.src ? new URL('.', document.currentScript.src).href : '/shared/';
+ const POLL_MS = 8000;
+ const MODES = [['create', 'Create', 'A new website, game or app'], ['openvibe', 'OpenVibe', 'A change to OpenVibe itself'], ['repo', 'My code', 'A repository of yours']];
+ const STATE = { queued: 'Waiting', running: 'Running', 'needs-you': 'Needs you', done: 'Done', failed: 'Failed', stopped: 'Stopped' };
+ const PLACEHOLDER = { create: 'A goal bar for my sub goal', openvibe: 'Make the menu remember my last tab', repo: 'Fix the login redirect and add a test' };
+ const svg = (d) => `<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;
+ const G = {
+ send: 'M5 12h13M13 6l6 6-6 6',
+ stop: 'M12 3a9 9 0 1 1 0 18 9 9 0 0 1 0-18zM9.5 9.5h5v5h-5z',
+ open: 'M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10',
+ code: 'M8 7l-5 5 5 5M16 7l5 5-5 5',
+ };
+
+ function styles(d) {
+ if (d.getElementById('ovdc-styles')) return;
+ const st = d.createElement('style');
+ st.id = 'ovdc-styles';
+ st.textContent = `
+ .ovdc { display: flex; flex-direction: column; gap: 12px; padding: 10px 10px 14px; }
+ .ovdc-box { display: grid; gap: 8px; padding: 10px 10px 8px 12px; border-radius: 18px; border: 1px solid var(--border, rgba(255,255,255,.1)); background: var(--bg-card, rgba(255,255,255,.03)); }
+ .ovdc-box:focus-within { border-color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--border, rgba(255,255,255,.1))); }
+ .ovdc-text { display: block; width: 100%; box-sizing: border-box; min-height: 44px; max-height: 30vh; padding: 2px 0; border: 0; outline: 0; resize: none; background: none; color: var(--text-primary, #ececf1); font: inherit; font-size: 15.5px; line-height: 1.45; }
+ .ovdc-repo { width: 100%; box-sizing: border-box; height: 36px; padding: 0 10px; border-radius: 10px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-input, rgba(0,0,0,.2)); color: inherit; font: inherit; font-size: 14px; }
+ .ovdc-repo[hidden] { display: none; }
+ .ovdc-row { display: flex; align-items: center; gap: 6px; }
+ .ovdc-modes { flex: 1; min-width: 0; display: flex; gap: 4px; overflow-x: auto; scrollbar-width: none; }
+ .ovdc-mode { flex: none; height: 32px; padding: 0 11px; border-radius: 999px; border: 1px solid transparent; background: none; color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; }
+ .ovdc-mode:hover { color: var(--text-primary, #ececf1); background: var(--bg-hover, rgba(255,255,255,.06)); }
+ .ovdc-mode[aria-checked="true"] { color: var(--accent-light, #93c5fd); border-color: color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }
+ .ovdc-go { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 13px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); cursor: pointer; }
+ .ovdc-go:disabled { opacity: .45; cursor: default; }
+ .ovdc-note { margin: 0 2px; font-size: 12.5px; color: var(--text-muted, #8b8b96); }
+ .ovdc-note:empty { display: none; }
+ .ovdc-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 4px; }
+ .ovdc-head b { font-size: 13px; letter-spacing: .02em; color: var(--text-secondary, #a8b3c4); }
+ .ovdc-all { display: inline-flex; align-items: center; gap: 5px; min-height: 32px; font-size: 13px; font-weight: 600; color: var(--accent-light, #93c5fd); text-decoration: none; }
+ .ovdc-all svg { width: 15px; height: 15px; }
+ .ovdc-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
+ .ovdc-task { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; border-radius: 12px; }
+ .ovdc-task:hover { background: var(--bg-hover, rgba(255,255,255,.05)); }
+ .ovdc-a { min-width: 0; display: grid; gap: 3px; padding: 9px 4px 9px 10px; color: inherit; text-decoration: none; }
+ .ovdc-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14.5px; font-weight: 600; color: var(--text-primary, #ececf1); }
+ .ovdc-m { display: flex; align-items: center; gap: 7px; min-width: 0; font-size: 12.5px; color: var(--text-muted, #8b8b96); white-space: nowrap; overflow: hidden; }
+ .ovdc-s { flex: none; display: inline-flex; align-items: center; gap: 5px; font-weight: 650; }
+ .ovdc-s::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
+ .ovdc-s[data-s="running"], .ovdc-s[data-s="queued"] { color: var(--accent-light, #93c5fd); }
+ .ovdc-s[data-s="running"]::before { animation: ovdc-beat 1.2s ease-in-out infinite; }
+ .ovdc-s[data-s="needs-you"] { color: #fbbf24; }
+ .ovdc-s[data-s="done"] { color: #4ade80; }
+ .ovdc-s[data-s="failed"] { color: var(--live-red, #f87171); }
+ .ovdc-s[data-s="stopped"] { color: var(--text-muted, #8b8b96); }
+ .ovdc-stop { display: grid; place-items: center; width: 36px; height: 36px; margin-right: 4px; border: 0; border-radius: 10px; background: none; color: var(--text-secondary, #a8b3c4); cursor: pointer; }
+ .ovdc-stop:hover, .ovdc-stop:focus-visible { background: color-mix(in srgb, var(--live-red, #ef4444) 14%, transparent); color: var(--live-red, #f87171); }
+ .ovdc-stop svg { width: 19px; height: 19px; }
+ .ovdc-none { padding: 18px 12px; text-align: center; color: var(--text-muted, #8b8b96); font-size: 13.5px; line-height: 1.45; }
+ .ovdc-none a { color: var(--accent-light, #93c5fd); }
+ @keyframes ovdc-beat { 50% { opacity: .35; } }
+ html[data-ov-motion=off] .ovdc-s::before { animation: none; }`;
+ d.head.appendChild(st);
+ }
+
+ const ago = (t) => {
+ const s = Math.max(0, (Date.now() - new Date(t).getTime()) / 1000);
+ if (!isFinite(s)) return '';
+ return s < 60 ? 'just now' : s < 3600 ? `${Math.floor(s / 60)} min ago` : s < 86400 ? `${Math.floor(s / 3600)} h ago` : `${Math.floor(s / 86400)} d ago`;
+ };
+ const onCodes = (u) => { try { const x = new URL(u); return x.protocol === 'https:' && (x.hostname === 'openvibe.codes' || x.hostname.endsWith('.openvibe.codes')) ? x.href : null; } catch { return null; } };
+ const repoName = (r) => String(r || '').replace(/^https?:\/\/[^/]+\//, '').replace(/\.git$/, '');
+
+ function mount(container, opts = {}) {
+ const d = container.ownerDocument || document;
+ styles(d);
+ const o = { ...opts };
+ const tok = () => (typeof o.token === 'function' ? o.token() : o.token) || null;
+ const codes = String(o.codes || CODES).replace(/\/+$/, '');
+ const direct = o.base ? String(o.base).replace(/\/+$/, '') : '';
+ const relay = String(o.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const url = (what, id) => (direct
+ ? direct + ({ list: '/api/v1/me/tasks?limit=8', start: '/api/v1/tasks', stop: `/api/v1/tasks/${encodeURIComponent(id)}/stop` })[what]
+ : relay + ({ list: '/codes-tasks?limit=8', start: '/codes-tasks', stop: `/codes-tasks/${encodeURIComponent(id)}/stop` })[what]);
+ const h = (tag, cls, text) => { const n = d.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
+ let gone = false, timer = 0, mode = 'create', tasks = null, reachable = true;
+
+ function loadScript(file, ready) {
+ if (ready()) return Promise.resolve(true);
+ return new Promise((resolve) => {
+ const id = `ovdc-${file}`;
+ let script = d.getElementById(id);
+ if (!script) { script = d.createElement('script'); script.id = id; script.src = SCRIPT_BASE + file; script.async = true; d.head.appendChild(script); }
+ script.addEventListener('load', () => resolve(!!ready()), { once: true });
+ script.addEventListener('error', () => resolve(false), { once: true });
+ });
+ }
+ function runUrl(id) {
+ return (direct ? direct + '/api/v1/runs/' : relay + '/codes-runs/') + encodeURIComponent(id);
+ }
+ async function openRun(task, from) {
+ const href = onCodes(task.url);
+ if (!href || !tok()) return;
+ const loaded = await Promise.all([
+ loadScript('ov-window.js', () => !!root.OVWindow),
+ loadScript('agent-session.js', () => !!root.OpenVibeAgentSession)
+ ]);
+ if (loaded.some((ok) => !ok)) { d.defaultView.location.assign(href); return; }
+ const body = h('div');
+ let stopped = false, controller = null, view = null;
+ const win = root.OVWindow.open({ id: `codes-run-${task.id}`, title: task.title || 'Coding task', body, kind: 'dialog', size: 'md', from,
+ onClose: () => { stopped = true; if (controller) controller.abort(); if (view) view.destroy(); } });
+ body.appendChild(h('p', 'ovdc-none', 'Loading the run…'));
+ const headers = () => ({ Authorization: `Bearer ${tok()}`, Accept: 'application/json' });
+ try {
+ const res = await fetch(runUrl(task.id), { headers: headers(), credentials: 'omit' });
+ if (!res.ok) throw new Error('The run could not be read.');
+ const run = await res.json();
+ if (stopped) return;
+ body.replaceChildren();
+ view = root.OpenVibeAgentSession.mount(body, { title: run.title || task.title || 'Coding task',
+ status: run.status || task.status || 'running', mode: 'card', href });
+ let last = 0;
+ while (!stopped) {
+ controller = new AbortController();
+ const events = await fetch(runUrl(task.id) + '/events', { credentials: 'omit',
+ headers: { ...headers(), Accept: 'text/event-stream', 'Last-Event-ID': String(last) }, signal: controller.signal });
+ if (!events.ok || !events.body) break;
+ const reader = events.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '', terminal = false;
+ while (!stopped) {
+ const part = await reader.read();
+ if (part.done) break;
+ buffer = (buffer + decoder.decode(part.value, { stream: true })).replace(/\r\n/g, '\n');
+ let cut;
+ while ((cut = buffer.indexOf('\n\n')) !== -1) {
+ const frame = buffer.slice(0, cut); buffer = buffer.slice(cut + 2);
+ const id = /^id: *(\d+)$/m.exec(frame), data = frame.split('\n').filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');
+ if (id) last = Number(id[1]);
+ if (!data) continue;
+ try { const event = JSON.parse(data); view.push(event); if (event.type === 'RUN_FINISHED' || event.type === 'RUN_ERROR') terminal = true; } catch { /* skip one malformed event */ }
+ }
+ }
+ if (terminal || !/running|queued|placed/.test(run.status || '')) break;
+ await new Promise((resolve) => setTimeout(resolve, 1500));
+ }
+ } catch (err) {
+ if (!stopped) { body.replaceChildren(); const a = h('a', 'ovk-btn ovk-btn--quiet', 'Open on Codes'); a.href = href; body.appendChild(a); }
+ }
+ return win;
+ }
+
+ container.innerHTML = '';
+ container.classList.add('ovdc');
+ const box = h('form', 'ovdc-box');
+ box.setAttribute('aria-label', 'Start a coding task');
+ const text = h('textarea', 'ovdc-text');
+ text.rows = 2; text.maxLength = 16000; text.placeholder = PLACEHOLDER.create;
+ text.setAttribute('aria-label', 'What should we make?');
+ const repo = h('input', 'ovdc-repo');
+ repo.placeholder = 'Repository: owner/name or its address'; repo.setAttribute('aria-label', 'Repository'); repo.hidden = true; repo.maxLength = 300;
+ const row = h('div', 'ovdc-row');
+ const modes = h('div', 'ovdc-modes');
+ modes.setAttribute('role', 'radiogroup'); modes.setAttribute('aria-label', 'Where it works');
+ for (const [id, label, hint] of MODES) {
+ const b = h('button', 'ovdc-mode', label);
+ b.type = 'button'; b.dataset.mode = id; b.title = hint;
+ b.setAttribute('role', 'radio'); b.setAttribute('aria-checked', String(id === mode));
+ modes.appendChild(b);
+ }
+ const go = h('button', 'ovdc-go'); go.type = 'submit'; go.innerHTML = svg(G.send); go.setAttribute('aria-label', 'Start'); go.disabled = true;
+ row.append(modes, go);
+ const note = h('p', 'ovdc-note'); note.setAttribute('role', 'status');
+ box.append(text, repo, row, note);
+ const head = h('div', 'ovdc-head');
+ const all = h('a', 'ovdc-all'); all.href = `${codes}/`; all.innerHTML = `<span>Open Codes</span>${svg(G.open)}`;
+ head.append(h('b', null, 'Your tasks'), all);
+ const list = h('ul', 'ovdc-list');
+ container.append(box, head, list);
+
+ const ready = () => { go.disabled = !text.value.trim() || (mode === 'repo' && !repo.value.trim()); };
+ text.addEventListener('input', () => { text.style.height = 'auto'; text.style.height = Math.min(text.scrollHeight, d.defaultView.innerHeight * 0.3) + 'px'; ready(); });
+ repo.addEventListener('input', ready);
+ text.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && !go.disabled) { e.preventDefault(); box.requestSubmit ? box.requestSubmit() : start(); } });
+ modes.addEventListener('click', (e) => {
+ const b = e.target.closest && e.target.closest('[data-mode]'); if (!b) return;
+ mode = b.dataset.mode;
+ modes.querySelectorAll('[data-mode]').forEach((x) => x.setAttribute('aria-checked', String(x === b)));
+ text.placeholder = PLACEHOLDER[mode];
+ repo.hidden = mode !== 'repo';
+ ready();
+ (mode === 'repo' && !repo.value ? repo : text).focus();
+ });
+
+ const fallback = () => `${codes}/?${new URLSearchParams({ prompt: text.value.trim(), mode, ...(mode === 'repo' ? { repo: repo.value.trim() } : {}) })}`;
+ async function call(what, id, body) {
+ const t = tok(); if (!t) throw new Error('signed out');
+ const r = await fetch(url(what, id), { method: body || what === 'stop' ? 'POST' : 'GET', headers: { authorization: `Bearer ${t}`, accept: 'application/json', ...(body ? { 'content-type': 'application/json' } : {}) }, body: body ? JSON.stringify(body) : undefined, credentials: 'omit' });
+ if (!r.ok) { const e = new Error(`HTTP ${r.status}`); e.status = r.status; try { e.body = await r.json(); } catch { /* */ } throw e; }
+ return r.json().catch(() => ({}));
+ }
+ async function start() {
+ if (go.disabled) return;
+ if (!reachable) { d.defaultView.location.assign(fallback()); return; }
+ go.disabled = true; note.textContent = 'Starting…';
+ try {
+ const r = await call('start', null, { text: text.value.trim(), mode, ...(mode === 'repo' ? { repo: repo.value.trim() } : {}) });
+ text.value = ''; text.dispatchEvent(new Event('input'));
+ note.textContent = '';
+ const there = onCodes(r && r.url);
+ if (there) { const a = h('a', null, 'Follow it on openvibe.codes'); a.href = there; note.textContent = 'Started. '; note.appendChild(a); }
+ read();
+ } catch (e) {
+ ready();
+ if (e.status === 404 || e.status === 502 || e.status === 503 || e.message === 'Failed to fetch') { d.defaultView.location.assign(fallback()); return; }
+ note.textContent = (e.body && (e.body.message || e.body.error)) || 'It did not start. Try again in a moment.';
+ }
+ }
+ box.addEventListener('submit', (e) => { e.preventDefault(); start(); });
+
+ function paint() {
+ list.replaceChildren();
+ if (!tok()) { list.appendChild(h('li', 'ovdc-none', 'Sign in to start coding tasks and follow them here.')); return; }
+ if (!reachable) {
+ const li = h('li', 'ovdc-none');
+ li.append('Your tasks are on ');
+ const a = h('a', null, 'openvibe.codes'); a.href = `${codes}/`; li.append(a, '. Starting one here opens it there.');
+ list.appendChild(li); return;
+ }
+ if (!tasks) return;
+ if (!tasks.length) { list.appendChild(h('li', 'ovdc-none', 'Nothing yet. Say what to make, and an agent builds it while you watch.')); return; }
+ for (const t of tasks) {
+ const li = h('li', 'ovdc-task');
+ const there = onCodes(t.url);
+ const a = h(there ? 'a' : 'div', 'ovdc-a');
+ if (there) { a.href = there; a.addEventListener('click', (event) => {
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+ event.preventDefault(); openRun(t, a);
+ }); }
+ a.appendChild(h('span', 'ovdc-t', t.title || 'A coding task'));
+ const m = h('span', 'ovdc-m');
+ const s = h('span', 'ovdc-s', STATE[t.status] || t.status || ''); s.dataset.s = t.status || '';
+ m.appendChild(s);
+ const bits = [t.mode === 'repo' && t.repo ? repoName(t.repo) : t.mode === 'openvibe' ? 'OpenVibe' : null, t.steps ? `${t.steps} steps` : null, ago(t.updated_at || t.started_at)].filter(Boolean);
+ if (bits.length) m.appendChild(h('span', null, bits.join(' · ')));
+ a.appendChild(m);
+ li.appendChild(a);
+ if (t.status === 'running' || t.status === 'queued' || t.status === 'needs-you') {
+ const x = h('button', 'ovdc-stop'); x.type = 'button'; x.innerHTML = svg(G.stop); x.setAttribute('aria-label', `Stop: ${t.title || 'this task'}`); x.title = 'Stop';
+ x.addEventListener('click', async () => { x.disabled = true; try { await call('stop', t.id); } catch { /* the next read says */ } read(); });
+ li.appendChild(x);
+ }
+ list.appendChild(li);
+ }
+ }
+ async function read() {
+ clearTimeout(timer);
+ if (gone || !tok()) { paint(); return; }
+ try {
+ const j = await call('list');
+ reachable = true;
+ tasks = (Array.isArray(j) ? j : j.tasks || []).slice(0, 8);
+ tasks.sort((a, b) => (/running|queued|needs-you/.test(b.status) ? 1 : 0) - (/running|queued|needs-you/.test(a.status) ? 1 : 0));
+ } catch (e) {
+ if (e.status === 401 || e.status === 403) tasks = [];
+ else reachable = false;
+ }
+ if (gone) return;
+ paint();
+ if (reachable && tasks && tasks.some((t) => /running|queued/.test(t.status)) && (!o.active || o.active())) timer = setTimeout(read, POLL_MS);
+ }
+ // What another part handed over (Vibe's Build it in Code) goes in the box, ready to start.
+ const take = () => { const t = typeof o.take === 'function' ? o.take() : ''; if (!t) return; text.value = t; text.dispatchEvent(new Event('input')); text.focus(); note.textContent = 'From your chat with Vibe. Start it, or change it first.'; };
+ paint();
+ read();
+ take();
+ return {
+ refresh: () => { take(); read(); },
+ focus: () => text.focus(),
+ destroy() { gone = true; clearTimeout(timer); container.replaceChildren(); container.classList.remove('ovdc'); },
+ };
+ }
+
+ root.OpenVibeDockCode = { mount };
+ if (typeof module === 'object' && module.exports) module.exports = { mount };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
+// The Dock body inside a host sheet. Loaded only when mountInto is first used.
+(function (root) {
+ 'use strict';
+ function mount(host, first, bridge) {
+ if (!document.getElementById('ovdk-mount-styles')) {
+ const style = document.createElement('style');
+ style.id = 'ovdk-mount-styles';
+ style.textContent = `
+ .ovdk.ovdk-inline, .ovdk.ovdk-inline[data-corner] { position: relative !important; inset: auto !important; z-index: auto !important; width: 100% !important; height: 100% !important; max-width: 100% !important; max-height: none !important; min-height: 0; padding: 0 !important; border: 0; border-radius: 0; box-shadow: none !important; transform: none !important; animation: none !important; }
+ .ovdk-inline .ovdk-body { width: 100%; min-width: 0; border: 0; overflow: hidden; }
+ .ovdk-inline .ovdk-panel { min-width: 0; overflow-x: hidden; }
+ @media (max-width: 640px) { .ovdk-inline button, .ovdk-inline a, .ovdk-inline summary, .ovdk-inline input, .ovdk-inline textarea { min-height: 44px !important; max-height: none !important; box-sizing: border-box; } }
+ `;
+ document.head.append(style);
+ }
+ const wrap = document.createElement('section');
+ wrap.className = 'ovdk ovdk-inline';
+ wrap.setAttribute('aria-label', 'Dock panel');
+ host.replaceChildren(wrap);
+ bridge.start(wrap);
+ let gone = false;
+ const show = (tab) => { if (!gone) bridge.show(tab); };
+ show(first);
+ return { show, destroy() { if (gone) return; gone = true; bridge.end(); host.replaceChildren(); } };
+ }
+ root.OpenVibeDockMount = { mount };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,135 @@
@@ -0,0 +1,135 @@
+/** The Dock's content-aware run list. All content comes from Network's action feed and Actor's skill catalogue. */
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockRuns) return;
+ const CSS = `
+.ovdk-overview{display:grid;gap:8px;padding:4px 10px 12px}.ovdk-tiles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
+.ovdk-tile{min-width:0;display:grid;align-content:start;gap:2px;padding:10px 11px;border-radius:14px;background:color-mix(in srgb,var(--text-primary,#ececf1) 5%,transparent)}
+.ovdk-tile small{font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted,#8b8b96)}
+.ovdk-tile b{font-size:22px;font-weight:800;line-height:1.1;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.ovdk-tile b em{font-style:normal;font-size:12px;font-weight:650;color:var(--text-secondary,#a8a8b2);letter-spacing:0;margin-left:4px}
+.ovdk-tile span{white-space:pre-wrap;font-variant-numeric:tabular-nums;font-size:11.5px;color:var(--text-secondary,#a8a8b2);line-height:1.35;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+.ovdk-tile .ovdk-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin:0 3px 1px 0;background:var(--ok,#3fb27f)}.ovdk-tile .ovdk-dot.is-bad{background:var(--live-red,#e5484d)}
+.ovdk-tile-next b{font-size:15px;line-height:1.25;letter-spacing:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.ovdk-tile-next span{color:var(--accent-light,var(--accent,#60a5fa));font-weight:700}
+.ovdk-overview h3{font-size:11px;font-weight:750;letter-spacing:.05em;text-transform:uppercase;color:var(--text-muted,#8b8b96);margin:8px 2px 0}.ovdk-skills{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
+.ovdk-skills button{min-height:44px;padding:9px 11px;border-radius:12px;border:0;background:color-mix(in srgb,var(--accent,#3b82f6) 10%,transparent);color:inherit;text-align:left;cursor:pointer;font:inherit;font-size:12.5px;font-weight:650}
+.ovdk-skills button:hover{background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent)}.ovdk-overview .ovdk-runs{padding:0}.ovdk-run-filters{display:flex;gap:4px;overflow-x:auto;padding:8px 10px;scrollbar-width:none}.ovdk-run-filters::-webkit-scrollbar{display:none}
+.ovdk-run-filters button{min-height:36px;padding:0 11px;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:99px;background:transparent;color:inherit;cursor:pointer;white-space:nowrap;font:inherit;font-size:12px}
+.ovdk-run-filters button[aria-pressed=true]{background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}
+.ovdk-runs{list-style:none;margin:0;padding:0 6px 10px;display:grid;gap:2px}.ovdk-run{display:grid;grid-template-columns:36px minmax(0,1fr) auto;gap:11px;padding:10px 8px;border-radius:14px;min-width:0}
+.ovdk-run-more{position:relative;align-self:start}.ovdk-run-more summary{list-style:none;width:36px;height:36px;display:grid;place-items:center;border-radius:50%;cursor:pointer;color:var(--text-muted,#8b8b96)}
+.ovdk-run-more summary::-webkit-details-marker{display:none}.ovdk-run-more summary svg{width:18px;height:18px;fill:currentColor}
+@media (hover:hover){.ovdk-run-more summary:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 8%,transparent);color:var(--text-primary,#ececf1)}}
+.ovdk-run-more menu{position:absolute;right:0;top:38px;z-index:3;min-width:210px;padding:4px;margin:0;list-style:none;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:12px;background:var(--bg-primary,#19191f);box-shadow:0 10px 28px rgba(0,0,0,.28)}
+.ovdk-run-more menu a,.ovdk-run-more menu button{display:flex;align-items:center;width:100%;min-height:40px;padding:0 12px;border:0;border-radius:9px;background:transparent;color:inherit;font:inherit;font-size:13px;text-decoration:none;cursor:pointer;text-align:left}
+.ovdk-run-more menu a:hover,.ovdk-run-more menu button:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}
+@media(max-width:640px){.ovdk-run-more summary{width:44px;height:44px}.ovdk-run-more menu{top:46px}}
+@media (hover:hover){.ovdk-run:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 4%,transparent)}}
+.ovdk-run-st{font-weight:750;margin-right:4px}.ovdk-run[data-state=running] .ovdk-run-st{color:var(--accent-light,var(--accent,#60a5fa))}.ovdk-run[data-state=waiting] .ovdk-run-st{color:#f0a73a}
+.ovdk-run[data-state=scheduled] .ovdk-run-st{color:var(--text-primary,#ececf1)}.ovdk-run[data-state=done] .ovdk-run-st{color:var(--ok,#3fb27f)}.ovdk-run[data-state=failed] .ovdk-run-st{color:var(--live-red,#e5484d)}
+.ovdk-run-ic{width:34px;height:34px;display:grid;place-items:center;border-radius:10px;background:color-mix(in srgb,var(--run-color,var(--accent,#3b82f6)) 15%,transparent);color:var(--run-color,var(--accent,#3b82f6))}
+.ovdk-run-sub{min-width:0;display:grid;gap:3px}.ovdk-run-sub>b,.ovdk-run-title b{display:block;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.ovdk-run-title{color:inherit;text-decoration:none;min-width:0}.ovdk-run-title:hover b{text-decoration:underline;text-underline-offset:2px}.ovdk-run-line{font-size:12.5px;color:var(--text-secondary,#a8a8b2);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
+.ovdk-run-model{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--text-secondary,#a8a8b2)}.ovdk-run-maker{display:inline-grid;place-items:center;width:20px;height:20px;border-radius:6px;background:color-mix(in srgb,var(--text-primary,#ececf1) 8%,transparent)}.ovdk-run-maker svg{width:14px;height:14px;fill:currentColor}
+.ovdk-run-bar{height:3px;margin:2px 0;background:color-mix(in srgb,var(--accent,#3b82f6) 18%,transparent);border-radius:9px;overflow:hidden}.ovdk-run-bar:after{content:'';display:block;width:40%;height:100%;border-radius:9px;background:var(--accent,#3b82f6);animation:ovdk-run-go 1.6s cubic-bezier(.45,0,.2,1) infinite}
+@keyframes ovdk-run-go{from{transform:translateX(-100%)}to{transform:translateX(250%)}}html[data-ov-motion=off] .ovdk-run-bar:after{width:45%}
+.ovdk-run-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:3px}.ovdk-run-actions a,.ovdk-run-actions button,.ovdk-run-actions summary{min-height:32px;display:inline-flex;align-items:center;padding:0 12px;border-radius:999px;color:inherit;background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent);border:0;cursor:pointer;font:inherit;font-size:12.5px;font-weight:650;text-decoration:none;list-style:none}
+.ovdk-run-actions summary::-webkit-details-marker{display:none}.ovdk-run-actions .primary{background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}
+@media (hover:hover){.ovdk-run-actions a:hover,.ovdk-run-actions button:not(.primary):hover,.ovdk-run-actions summary:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 12%,transparent)}}
+.ovdk-run-actions details{position:relative}.ovdk-run-actions details menu{position:absolute;right:0;z-index:2;min-width:180px;padding:4px;margin:0;list-style:none;border:1px solid var(--border,rgba(127,127,127,.2));border-radius:10px;background:var(--bg-primary,#19191f);box-shadow:0 8px 24px rgba(0,0,0,.2)}
+.ovdk-run-repeat{display:block;width:100%;min-height:36px;padding:0 10px;text-align:left;border:0;border-radius:10px;background:transparent;color:var(--accent-light,var(--accent,#60a5fa));font:inherit;font-size:12.5px;font-weight:650;cursor:pointer}
+@media(max-width:640px){.ovdk-run-filters button,.ovdk-run-actions a,.ovdk-run-actions button,.ovdk-run-actions summary,.ovdk-run-repeat{min-height:44px}.ovdk-skills button{min-height:52px}}
+`;
+ const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = String(text); return e; };
+ const ago = (at) => { const n = Date.parse(at || ''); if (!Number.isFinite(n)) return ''; const m = Math.max(0, Math.round((Date.now() - n) / 60000)); return m < 1 ? 'now' : m < 60 ? `${m} min` : m < 1440 ? `${Math.floor(m / 60)} h` : `${Math.floor(m / 1440)} d`; };
+ const until = (at) => { const m = Math.max(0, Math.ceil((Date.parse(at || '') - Date.now()) / 60000)); return m < 1 ? 'now' : m < 60 ? `in ${m} min` : m < 1440 ? `in ${Math.ceil(m / 60)} h` : `in ${Math.ceil(m / 1440)} d`; };
+ const cash = (n) => n == null ? '' : Number(n) < .01 ? `$${Number(n).toFixed(4)}` : `$${Number(n).toFixed(2)}`;
+ const iconOf = (it) => { const s = String(it.skill || it.subject && it.subject.kind || it.kind || ''); return /post|blog|write|changelog|draft|patch/.test(s) ? ['fa-pen','#d7974d'] : /wiki|article|research/.test(s) ? ['fa-book','#9875d8'] : /image|pic|meme/.test(s) ? ['fa-image','#d66bad'] : /chat|thread/.test(s) ? ['fa-comments','#61a9d7'] : /code|repo/.test(s) ? ['fa-code','#63b89c'] : /clip|stream|video/.test(s) ? ['fa-film','#dc866a'] : /quest/.test(s) ? ['fa-flag','#b9a74b'] : ['fa-sparkles','#8b92d9']; };
+ const dots = () => { const NS = 'http://www.w3.org/2000/svg'; const s = document.createElementNS(NS, 'svg'); s.setAttribute('viewBox', '0 0 24 24'); s.setAttribute('aria-hidden', 'true');
+ [5.5, 12, 18.5].forEach((x) => { const c = document.createElementNS(NS, 'circle'); c.setAttribute('cx', String(x)); c.setAttribute('cy', '12'); c.setAttribute('r', '1.7'); s.append(c); }); return s; };
+ const isUrl = (u) => /^https:\/\//.test(String(u || ''));
+ // Where a person picks the model a run uses, or adds their own key.
+ const MODEL_PAGE = 'https://openvibe.actor/chat/model';
+ function mount(panel, o) {
+ const st = o.styles && o.styles(); if (st && !st.textContent.includes('.ovdk-overview{')) st.textContent += CSS;
+ // A site's Dock passes no scopes (full behaviour); the extension passes its read-only set.
+ const canDecide = !Array.isArray(o.scopes) || o.scopes.includes('actions.decide');
+ let doc = null, busy = false, timer = 0, gone = false, filter = o.state().agent_filter || 'all';
+ const request = async (path, init) => o.call(path, init);
+ const all = () => doc ? ['waiting','running','scheduled','done','failed'].flatMap((b) => doc.buckets[b] || []) : [];
+ function row(it) {
+ const li = h('li', 'ovdk-run ovac-item'); li.dataset.state = it.bucket; li.dataset.bucket = it.bucket;
+ const [glyph, color] = iconOf(it); li.style.setProperty('--run-color', color);
+ const ic = h('span', 'ovdk-run-ic'); const i = h('i', `fa-solid ${glyph}`); i.setAttribute('aria-hidden', 'true'); ic.append(i);
+ const body = h('div', 'ovdk-run-sub');
+ const title = it.subject && it.subject.title || it.title || 'Agent run';
+ const url = it.subject && it.subject.url || it.href;
+ if (isUrl(url)) { const t = h('a', 'ovdk-run-title'); t.href = url; t.target = '_blank'; t.rel = 'noopener'; t.append(h('b', '', title)); body.append(t); } else body.append(h('b', '', title));
+ const error = it.run_error; let st = 'Done', line = '';
+ if (it.bucket === 'running') { st = 'Running'; line = it.step || it.summary || 'Working'; }
+ else if (it.bucket === 'waiting') { st = 'Waiting'; line = it.summary || 'Your answer is needed'; }
+ else if (it.bucket === 'scheduled') { st = 'Scheduled'; line = it.next_run_at ? until(it.next_run_at) : 'Time pending'; }
+ else if (it.bucket === 'failed') { st = 'Failed'; line = error ? `${error.words} ${error.next}` : 'This run could not finish. Open it for details.'; }
+ else line = [it.summary, cash(it.cost_usd), it.duration_ms != null ? `${Math.round(it.duration_ms / 1000)}s` : ''].filter(Boolean).join(' · ');
+ const ln = h('span', 'ovdk-run-line'); ln.append(h('span', 'ovdk-run-st', st), document.createTextNode(line)); body.append(ln);
+ if (it.model) { const model = h('span', 'ovdk-run-model'); const mark = root.OpenVibeAIMarks && root.OpenVibeAIMarks.mark(it.maker, it.model); if (mark) model.append(mark); model.append(String(it.model)); body.append(model); }
+ if (it.bucket === 'running') body.append(h('span', 'ovdk-run-bar'));
+ const acts = h('div', 'ovdk-run-actions'); let more = null;
+ if (canDecide && it.bucket === 'waiting' && (it.actions || []).includes('approve')) for (const action of ['approve','decline']) {
+ const b = h('button', action === 'approve' ? 'primary' : '', action === 'approve' ? 'Approve' : 'Decline'); b.type = 'button'; b.dataset.do = action;
+ b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/${action}`, { method: 'POST', body: '{}' }); b.textContent = action === 'approve' ? 'Approved' : 'Declined'; } catch { b.disabled = false; b.textContent = 'Try again'; } }); acts.append(b);
+ }
+
+ // The error's own action says what the person can do about it: pick a model, add a key, or wait.
+ const what = error && error.action;
+ if (it.bucket === 'failed' && (what === 'pick_model' || what === 'add_key')) { const a = h('a', '', what === 'pick_model' ? 'Pick a model' : 'Add a key'); a.href = MODEL_PAGE; a.target = '_blank'; a.rel = 'noopener'; acts.append(a); }
+ if (canDecide && it.bucket === 'running' && (it.actions || []).includes('cancel')) { const b = h('button', '', 'Stop'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/cancel`, { method: 'POST', body: '{}' }); refresh(); } catch { b.disabled = false; } }); acts.append(b); }
+ if (canDecide && it.bucket === 'failed' && (it.actions || []).includes('retry')) { const b = h('button', '', 'Retry'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/${encodeURIComponent(it.service)}/${encodeURIComponent(it.id)}/retry`, { method: 'POST', body: '{}' }); refresh(); } catch { b.disabled = false; } }); acts.append(b); }
+ if (canDecide && it.service === 'actor' && (it.kind === 'agent_task') && ['done', 'failed'].includes(it.bucket)) { const menu = h('details', 'ovdk-run-more'); const summary = h('summary'); summary.setAttribute('aria-label', 'More'); summary.title = 'More'; summary.append(dots()); menu.append(summary); const list = h('menu');
+ const choose = h('li'); const a = h('a', '', 'Run again with another model'); a.href = MODEL_PAGE; a.target = '_blank'; a.rel = 'noopener'; choose.append(a); list.append(choose);
+ const rerun = h('li'); const b = h('button', '', 'Run again'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await request(`/api/v1/me/actions/actor/${encodeURIComponent(it.id)}/retry`, { method: 'POST', body: '{}' }); menu.open = false; refresh(); } catch { b.disabled = false; b.textContent = 'Try again'; } }); rerun.append(b); list.append(rerun); menu.append(list); more = menu; }
+ if (acts.childNodes.length) body.append(acts); li.append(ic, body); if (more) li.append(more); if (root.OVGlyphs) root.OVGlyphs.swap(li); return li;
+ }
+ function grouped(items) { const map = new Map(); for (const it of items) { const day = String(it.updated_at || it.created_at || '').slice(0,10); const key = [it.skill || it.kind, it.subject && it.subject.kind || '', it.bucket, day].join('|'); if (!map.has(key)) map.set(key, []); map.get(key).push(it); } return [...map.values()]; }
+ function rows(items, target) { const list = h('ul', 'ovdk-runs'); grouped(items).forEach((group) => { if (group.length === 1) list.append(row(group[0])); else { const li = row(group[0]); const b = h('button', 'ovdk-run-repeat', `×${group.length} similar runs · Show all`); const details = h('ul', 'ovdk-runs'); details.hidden = true; group.slice(1).forEach((it) => details.append(row(it))); b.addEventListener('click', () => { details.hidden = !details.hidden; b.textContent = details.hidden ? `×${group.length} similar runs · Show all` : 'Hide repeats'; }); li.querySelector('.ovdk-run-sub').append(b, details); list.append(li); } }); target.append(list); }
+ async function skills(box) {
+ try {
+ // Through Network (every site's connect-src reaches openvibe.network and refuses openvibe.actor).
+ const data = await request('/api/v1/me/actions/skills');
+ const available = (data.listings || []).filter((x) => x && x.skill && x.scan && x.scan.state === 'passed' && x.curator && x.curator.state === 'active').slice(0, 6);
+ if (gone || !available.length) return;
+ box.append(h('h3', '', 'Things agents can do for you'));
+ const grid = h('div', 'ovdk-skills');
+ available.forEach((s) => {
+ const b = h('button', '', s.skill.name); b.type = 'button'; b.setAttribute('aria-label', `Start ${s.skill.name}`);
+ b.addEventListener('click', async () => {
+ b.disabled = true;
+ try {
+ const out = await request(`/api/v1/me/actions/skills/${encodeURIComponent(s.id)}/run`, { method: 'POST', body: JSON.stringify({ input: {}, choice: { mode: 'balanced' } }) });
+ if (out.run && out.run.id) root.location.href = `${o.actorBase}/tasks/${encodeURIComponent(out.run.id)}`;
+ else refresh();
+ } catch { b.disabled = false; b.textContent = `${s.skill.name} · Try again`; }
+ });
+ grid.append(b);
+ });
+ box.append(grid);
+ } catch { /* catalogue unavailable */ }
+ }
+ function overview() { const box = h('section', 'ovdk-overview'); const tiles = h('div', 'ovdk-tiles'); const now = Date.now(); const items = all(); const next = (doc.buckets.scheduled || []).find((x) => x.next_run_at); const periods = [[1,'Today'],[7,'This week']];
+ const first = h('div', 'ovdk-tile ovdk-tile-next'); first.append(h('small', '', 'Next'), h('b', '', next ? next.title : 'Nothing scheduled')); if (next) first.append(h('span', '', until(next.next_run_at))); tiles.append(first);
+ periods.forEach(([days,label]) => { const recent = items.filter((x) => now - Date.parse(x.updated_at || x.created_at) < days * 86400000); const done = recent.filter((x) => x.bucket === 'done').length, failed = recent.filter((x) => x.bucket === 'failed').length; const cost = recent.reduce((n, x) => n + (Number(x.cost_usd) || 0), 0);
+ const tile = h('div', 'ovdk-tile'); const n = h('b', '', String(recent.length)); n.append(h('em', '', recent.length === 1 ? 'run' : 'runs')); const sub = h('span');
+ if (done) { sub.append(h('i', 'ovdk-dot'), document.createTextNode(`${done} `)); } if (failed) { sub.append(h('i', 'ovdk-dot is-bad'), document.createTextNode(`${failed} `)); } if (cost) sub.append(document.createTextNode(cash(cost))); if (!recent.length) sub.textContent = 'None yet';
+ tile.setAttribute('aria-label', `${label}: ${recent.length} runs, ${done} done, ${failed} failed${cost ? `, ${cash(cost)}` : ''}`); sub.title = `${done} done · ${failed} failed`;
+ tile.append(h('small', '', label), n, sub); tiles.append(tile); }); box.append(tiles);
+ const recent = [...(doc.buckets.done || []), ...(doc.buckets.failed || [])].sort((a,b) => String(b.updated_at).localeCompare(String(a.updated_at))).slice(0,3); if (recent.length) { box.append(h('h3', '', 'Last results')); rows(recent, box); }
+ skills(box); return box; }
+ function paint() { if (gone || !doc) return; panel.textContent = ''; const items = all(); const filters = h('div', 'ovdk-run-filters'); for (const [id,label] of [['all','All'],['running','Running'],['waiting','Waiting'],['scheduled','Scheduled'],['done','Done'],['failed','Failed']]) { const b = h('button', '', label); b.type='button'; b.setAttribute('aria-pressed', String(filter===id)); b.addEventListener('click', () => { filter=id; o.save({agent_filter:id}); paint(); }); filters.append(b); } panel.append(filters);
+ if (filter==='all' && !(doc.counts.running || doc.counts.waiting)) panel.append(overview());
+ else { const shown = filter==='all' ? items : (doc.buckets[filter] || []); if (shown.length) rows(shown,panel); else panel.append(h('p','ovdk-none','No runs in this view.')); }
+ const foot = o.foot || h('div','ovdk-foot'); foot.textContent=''; const allLink = h('a','','All runs'); allLink.href='https://openvibe.actor/tasks'; const give = h('a','','Give an agent a task'); give.href='https://openvibe.actor/skills'; foot.append(allLink,give); if (!o.foot) panel.append(foot); }
+ async function refresh() { if (gone || busy || !o.active()) return; busy=true; clearTimeout(timer); try { doc = await request('/api/v1/me/actions'); if (gone) return; if (doc && doc.buckets) { paint(); if (o.onCounts) o.onCounts(doc.counts || {}); } } catch { if (!gone) { panel.textContent=''; panel.append(h('div','ovdk-none','Your runs did not load. Open Agents again to retry.')); } } finally { busy=false; if (!gone && o.active() && doc && doc.counts && doc.counts.running) timer=setTimeout(refresh,15000); } }
+ refresh(); if (o.loadMarks) o.loadMarks().then((ok) => { if (!gone && ok && doc) paint(); });
+ return { refresh, destroy: () => { gone = true; clearTimeout(timer); } };
+ }
+ root.OpenVibeDockRuns = { mount };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
+/**
+ * openvibe-shared/dock-share.js: the Dock's "Send to…" (4.144.0). dock.js loads it the first time something is sent
+ * from a page (OpenVibeDock.share, the share sheet's OpenVibe Chat), so the Dock that every signed-in page carries
+ * stays small.
+ *
+ * OpenVibeDockShare.bar({ draft, h, chatCall, open, cancel, host }) → the bar, drawn above the person's conversations
+ * OpenVibeDockShare.ready({ draft, h, panel, form, box, fit }) the text in a conversation's box, unsent, said so
+ *
+ * The bar says what is being sent and lets the person write to someone new by their OpenVibe name: Chat's
+ * /api/dm/users/search finds them, POST /api/dm/conversations starts the conversation, both with the person's own
+ * token (dock.js's chatCall). Nothing is sent from here: the message waits in the conversation's box until the person
+ * presses Send there.
+ */
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockShare) return;
+
+ const CSS = `
+.ovdk-share { flex: none; display: grid; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border, rgba(255,255,255,.08)); background: color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent); }
+.ovdk-share-h { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.ovdk-share-h b { font-size: 14px; color: var(--text-primary, #ececf1); }
+.ovdk-share-x { min-height: 32px; padding: 0 10px; border: 0; border-radius: 8px; background: none; color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; }
+.ovdk-share-x:hover { color: var(--text-primary, #ececf1); }
+.ovdk-share-what { margin: 0; font-size: 13px; color: var(--text-secondary, #b6b6c0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ovdk-to { display: flex; gap: 8px; }
+.ovdk-to input { flex: 1; min-width: 0; box-sizing: border-box; min-height: 40px; padding: 0 12px; border-radius: 12px; border: 1px solid var(--border-light, var(--border, rgba(255,255,255,.14))); background: var(--bg-primary, #12141b); color: var(--text-primary, #ececf1); font: inherit; font-size: 14px; }
+.ovdk-to input:focus { outline: none; border-color: var(--accent, #3b82f6); }
+.ovdk-to-go { flex: none; min-height: 40px; padding: 0 14px; border: 0; border-radius: 12px; cursor: pointer; font: inherit; font-size: 14px; font-weight: 700; color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }
+.ovdk-to-go:disabled { opacity: .55; cursor: default; }
+.ovdk-share .ovdk-err { margin: 0; }
+.ovdk-ready { flex: none; margin: 6px 14px 0; font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }
+`;
+ function style(host) {
+ const st = (host && host.ownerDocument && host.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles');
+ if (st && !st.textContent.includes('.ovdk-share {')) st.textContent += CSS;
+ }
+
+ function bar(c) {
+ const { draft, h, chatCall, open, cancel, host } = c;
+ style(host);
+ let busy = false;
+ const el = h('div', 'ovdk-share');
+ const top = h('div', 'ovdk-share-h');
+ const x = h('button', 'ovdk-share-x', 'Cancel'); x.type = 'button';
+ x.addEventListener('click', () => cancel());
+ top.append(h('b', '', 'Send to…'), x);
+ const what = h('p', 'ovdk-share-what', draft.title || draft.text); what.title = draft.text;
+ const to = h('form', 'ovdk-to');
+ const name = h('input'); name.type = 'text'; name.maxLength = 40; name.autocomplete = 'off'; name.spellcheck = false;
+ name.placeholder = 'Someone new: their OpenVibe name'; name.setAttribute('aria-label', 'Their OpenVibe name');
+ const next = h('button', 'ovdk-to-go', 'Next'); next.type = 'submit';
+ const err = h('p', 'ovdk-err'); err.hidden = true;
+ to.append(name, next);
+ to.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const want = name.value.trim().replace(/^@/, '');
+ if (want.length < 2 || busy) return;
+ busy = true; next.disabled = true; err.hidden = true;
+ try {
+ const found = (await chatCall(`/api/dm/users/search?q=${encodeURIComponent(want)}`)).users || [];
+ const exact = found.find((u) => String(u.username || '').toLowerCase() === want.toLowerCase());
+ if (!exact) {
+ err.textContent = found.length ? `Nobody is called @${want}. Did you mean @${found[0].username}?` : `Nobody on OpenVibe is called @${want}.`;
+ err.hidden = false;
+ return;
+ }
+ const made = await chatCall('/api/dm/conversations', { method: 'POST', body: JSON.stringify({ user_ids: [exact.id] }) });
+ const id = (made.conversation && made.conversation.id) || made.id;
+ if (!id) throw new Error('no conversation');
+ await open(id);
+ } catch (z) {
+ err.textContent = z.status === 429 ? 'Too many new conversations just now. Try again in a little while.' : z.status === 403 ? (z.message || 'You can’t start a conversation with them.') : 'That did not work. Try again.';
+ err.hidden = false;
+ } finally { busy = false; next.disabled = false; }
+ });
+ el.append(top, what, to, err);
+ return el;
+ }
+
+ /** The conversation the person picked: the text in its box, and a line saying nothing is sent until they send it. */
+ function ready({ draft, h, panel, form, box, fit }) {
+ box.value = draft;
+ const line = h('p', 'ovdk-ready', 'Ready to send: change it if you like, then press Send.');
+ panel.insertBefore(line, form);
+ box.addEventListener('input', () => line.remove(), { once: true });
+ form.addEventListener('submit', () => line.remove(), { once: true });
+ setTimeout(fit, 0);
+ }
+
+ root.OpenVibeDockShare = { bar, ready };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,423 @@
@@ -0,0 +1,423 @@
+// ═══════════════════════════════════════════════════════════════
+// OpenVibe — the Dock's Agent panel (design 2026-11-07: one agent, one timeline behind the Dock), Shared 4.126.0
+//
+// OpenVibeDockTimeline.mount(container, { token, base, active, focus }) → { refresh(), destroy() }
+//
+// `token` is the person's own Network token: a string, or the function the navbar hands over (which
+// resolves it when it is needed). Nothing is asked without it: a signed-out page can never read the timeline.
+// OpenVibe.Actor's API is GET /api/v1/agent/timeline (the person's own steps, newest first, with the cursor and the
+// mode and needs-you filters), POST /api/v1/agent/approvals/:product/:id/decide (an approval is answered in the mode
+// that owns it) and POST /api/v1/agent/kill-switch (stop what is open and pause the agent). On a site every call goes
+// through Network's same-origin relay (/api/frame/agent…, /api/frame/tasks…) because every site's connect-src allows
+// openvibe.network and refuses openvibe.actor; `base` reaches Actor itself in a test, and `frameBase` names the relay.
+// `focus` is the id of the step to point at (the task id a site opened the Dock with): the entry that
+// carries it is marked and brought into view.
+//
+// The panel is what the person reads:
+// Now one card for what the agent is doing this moment: the mode's word, the step in plain words, how long,
+// and the mode's own controls (Stop, and Resume when it is paused). Nothing running: "Your agent is idle."
+// Needs you every approval or question from any mode, oldest first, with its buttons in place.
+// Timeline one entry per step, newest first: the mode, the title, where it ran and why, the outcome, the time
+// and the cost, and a link to the step's own page. A mode change is a line of its own.
+// Folding today's entries open; earlier days are counted folds ("Yesterday (14)"); a long run of one mode folds
+// into "12 more Code steps".
+// Filters a chip for each mode and for "Needs you", in the URL of the panel; a filter never hides without a
+// count.
+//
+// Everything is set as text, never as markup: a title, a reason or a link label never becomes HTML.
+// ═══════════════════════════════════════════════════════════════
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OpenVibeDockTimeline) return;
+
+ const POLL_MS = 15000;
+ const MODES = [
+ { id: 'talk', word: 'Talk' }, { id: 'act', word: 'Act' }, { id: 'run', word: 'Run' },
+ { id: 'code', word: 'Code' }, { id: 'browse', word: 'Browse' }, { id: 'watch', word: 'Watch' },
+ { id: 'stream', word: 'Stream' },
+ ];
+ const word = (mode) => (MODES.find((m) => m.id === mode) || { word: 'Agent' }).word;
+ const OUTCOME = { done: 'Done', failed: 'Failed', stopped: 'Stopped by you', running: 'Running', waiting: 'Waiting', needs_you: 'Needs you' };
+
+ function el(doc, tag, cls, text) {
+ const n = doc.createElement(tag);
+ if (cls) n.className = cls;
+ if (text !== undefined && text !== null) n.textContent = String(text);
+ return n;
+ }
+
+ function since(startedAt) {
+ const t = Date.parse(String(startedAt || ''));
+ if (!Number.isFinite(t)) return '';
+ const s = Math.max(0, Math.round((Date.now() - t) / 1000));
+ if (s < 60) return s + 's';
+ if (s < 3600) return Math.round(s / 60) + ' min';
+ return Math.round(s / 3600) + ' h';
+ }
+
+ function dayKey(iso) {
+ const d = new Date(String(iso || ''));
+ if (Number.isNaN(d.getTime())) return '';
+ return d.toISOString().slice(0, 10);
+ }
+
+ function styles(doc) {
+ if (!doc || doc.getElementById('ovdt-styles')) return;
+ const st = doc.createElement('style');
+ st.id = 'ovdt-styles';
+ st.textContent = [
+ // The site's own theme (Opus, 11-07): the Dock's tokens, so the panel is the navy of a dark site and the paper
+ // of a light one, never a grey card of its own. The site's font, a gutter that clears the panel's edge, one
+ // primary button per card, and a breathing dot only on what is really running.
+ '.ovdt{font:inherit;font-size:14px;line-height:1.45;color:var(--text-primary,#ececf1);padding:4px 12px 20px}',
+ '.ovdt h3{margin:14px 2px 8px;font-size:11px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;color:var(--text-muted,#93a0b4)}',
+ '.ovdt h3:first-child{margin-top:6px}',
+ '.ovdt-card{border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:12px 14px;margin:0 0 8px;background:color-mix(in srgb,var(--text-primary,#ececf1) 4%,transparent)}',
+ '.ovdt-needs .ovdt-card{border-color:color-mix(in srgb,var(--warning,#f5a524) 38%,var(--border,rgba(255,255,255,.1)));background:color-mix(in srgb,var(--warning,#f5a524) 7%,transparent)}',
+ '.ovdt-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}',
+ '.ovdt-needs .ovdt-card>.ovdt-mode{margin-bottom:2px;font-size:11.5px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;color:color-mix(in srgb,var(--warning,#f5a524) 70%,var(--text-primary,#ececf1))}',
+ '.ovdt-mode{font-weight:750}',
+ '.ovdt-title{flex:1 1 100%;min-width:0;overflow-wrap:anywhere;font-size:14.5px;font-weight:550}',
+ '.ovdt-meta{color:var(--text-muted,#93a0b4);font-size:12.5px;overflow-wrap:anywhere}',
+ '.ovdt-outcome{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:11px;font-weight:750;letter-spacing:.04em;text-transform:uppercase;color:var(--text-secondary,#a8b3c4);background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}',
+ '.ovdt-outcome[data-s="running"]{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));background:color-mix(in srgb,var(--accent,#3b82f6) 16%,transparent)}',
+ '.ovdt-outcome[data-s="running"]::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor;animation:ovdt-breathe 1.8s ease-in-out infinite}',
+ '.ovdt-outcome[data-s="failed"]{color:var(--danger-light,#f87171);background:color-mix(in srgb,var(--danger,#ef4444) 14%,transparent)}',
+ '@keyframes ovdt-breathe{50%{opacity:.35;transform:scale(.8)}}',
+ '.ovdt-buttons{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}',
+ '.ovdt-btn{min-height:44px;min-width:44px;padding:0 16px;border-radius:12px;border:1px solid var(--border,rgba(255,255,255,.14));background:transparent;color:inherit;font:inherit;font-weight:650;cursor:pointer}',
+ '.ovdt-btn.is-primary{border-color:transparent;background:var(--accent,#3b82f6);color:var(--on-accent,#fff)}',
+ '.ovdt-btn.is-stop{border-color:color-mix(in srgb,var(--danger,#ef4444) 45%,transparent);color:var(--danger-light,#f87171)}',
+ '.ovdt-btn:disabled{opacity:.55;cursor:default}',
+ '.ovdt-btn:focus-visible,.ovdt-chip:focus-visible,.ovdt-fold:focus-visible{outline:2px solid var(--accent-light,var(--accent,#3b82f6));outline-offset:1px}',
+ '@media (hover:hover){.ovdt-btn:not(.is-primary):hover,.ovdt-chip:hover{background:color-mix(in srgb,var(--text-primary,#ececf1) 7%,transparent)}.ovdt-btn.is-primary:hover{filter:brightness(1.08)}}',
+ '.ovdt-chip{min-height:44px;padding:0 14px;border-radius:999px;border:1px solid var(--border,rgba(255,255,255,.12));background:transparent;color:var(--text-secondary,#a8b3c4);font:inherit;font-size:13.5px;font-weight:650;cursor:pointer;white-space:nowrap;flex:none}',
+ '.ovdt-chip[aria-pressed="true"]{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));border-color:color-mix(in srgb,var(--accent,#3b82f6) 55%,transparent);background:color-mix(in srgb,var(--accent,#3b82f6) 14%,transparent)}',
+ '.ovdt-chips{display:flex;gap:8px;overflow-x:auto;margin:4px -12px 0;padding:6px 12px 10px;scrollbar-width:none;-webkit-overflow-scrolling:touch}',
+ '.ovdt-chips::-webkit-scrollbar{display:none}',
+ '.ovdt-fold{width:100%;text-align:left;min-height:44px;margin:0 0 8px;padding:0 14px;border-radius:12px;border:1px dashed var(--border,rgba(255,255,255,.16));background:transparent;color:var(--text-secondary,#a8b3c4);font:inherit;font-weight:650;cursor:pointer}',
+ '.ovdt-card.ovdt-focus{border-color:color-mix(in srgb,var(--accent,#3b82f6) 55%,transparent)}',
+ '.ovdt-empty{color:var(--text-muted,#93a0b4);padding:4px 2px 6px}',
+ '.ovdt-card.ovdt-empty{padding:12px 14px}',
+ '.ovdt-link{color:color-mix(in srgb,var(--accent,#3b82f6) 72%,var(--text-primary,#ececf1));font-weight:650;text-decoration:none}',
+ '@media (max-width:640px){.ovdt-link{display:inline-flex;align-items:center;min-height:44px;padding:0 4px}}',
+ '',
+ ].join('');
+ (doc.head || doc.documentElement).appendChild(st);
+ }
+
+ function mount(container, opts = {}) {
+ const doc = container.ownerDocument || document;
+ styles(doc);
+ // Network's frame relay on a site; an explicit base reaches Actor itself (a test).
+ const direct = opts.base ? String(opts.base).replace(/\/+$/, '') : '';
+ const frameBase = String(opts.frameBase || 'https://openvibe.network/api/frame').replace(/\/+$/, '');
+ const endpoint = (path) => (direct ? direct + path : frameBase + path.replace(/^\/api\/v1/, ''));
+ const rawToken = opts.token;
+ const active = typeof opts.active === 'function' ? opts.active : () => true;
+ const focus = typeof opts.focus === 'function' ? opts.focus : () => opts.focus || null;
+ const filters = new Set();
+ let timer = null;
+ let destroyed = false;
+
+ // The navbar hands the Dock a function (the token may change when the person signs in again),
+ // so it is resolved when a call is made, never read once at mount.
+ async function bearer() {
+ const t = typeof rawToken === 'function' ? await rawToken() : rawToken;
+ return t ? 'Bearer ' + t : null;
+ }
+
+ const wrap = el(doc, 'div', 'ovdt');
+ const nowHead = el(doc, 'h3', null, 'Now');
+ const nowBox = el(doc, 'div', 'ovdt-now');
+ const needsHead = el(doc, 'h3', null, 'Needs you');
+ const needsBox = el(doc, 'div', 'ovdt-needs');
+ const chips = el(doc, 'div', 'ovdt-chips');
+ const list = el(doc, 'div', 'ovdt-list');
+ wrap.append(nowHead, nowBox, needsHead, needsBox, chips, list);
+ container.appendChild(wrap);
+
+ async function api(path, options = {}) {
+ const auth = await bearer();
+ if (!auth) throw new Error('Sign in to see your agent');
+ const res = await fetch(endpoint(path), Object.assign({ headers: { Accept: 'application/json', Authorization: auth } }, options));
+ const body = await res.json().catch(() => ({}));
+ if (!res.ok) throw new Error((body && (body.detail || body.title)) || ('HTTP ' + res.status));
+ return body;
+ }
+
+ function chip(id, label, count) {
+ const b = el(doc, 'button', 'ovdt-chip', label + (count === undefined || count === null ? '' : ' (' + count + ')'));
+ b.type = 'button';
+ b.setAttribute('aria-pressed', filters.has(id) ? 'true' : 'false');
+ b.addEventListener('click', () => {
+ if (filters.has(id)) filters.delete(id); else filters.add(id);
+ refresh();
+ });
+ return b;
+ }
+
+ /** The step's state as a small pill; data-s lets a running step breathe and a failed one show red. */
+ function outcome(status) {
+ const o = el(doc, 'span', 'ovdt-outcome', OUTCOME[status] || status);
+ o.setAttribute('data-s', String(status || '').toLowerCase());
+ return o;
+ }
+
+ function renderNow(now) {
+ nowBox.textContent = '';
+ if (!now) {
+ nowBox.appendChild(el(doc, 'div', 'ovdt-card ovdt-empty', 'Your agent is idle. Ask it something in Talk.'));
+ return;
+ }
+ const card = el(doc, 'div', 'ovdt-card');
+ const row = el(doc, 'div', 'ovdt-row');
+ row.appendChild(el(doc, 'span', 'ovdt-mode', word(now.mode)));
+ row.appendChild(outcome(now.status));
+ row.appendChild(el(doc, 'span', 'ovdt-title', now.title));
+ row.appendChild(el(doc, 'span', 'ovdt-meta', since(now.started_at)));
+ card.appendChild(row);
+ if (now.placement && (now.placement.where || now.placement.why)) {
+ card.appendChild(el(doc, 'div', 'ovdt-meta', [now.placement.where, now.placement.why].filter(Boolean).join(': ')));
+ }
+ // The Computer this step works on (plan 2026-11-07): the same machine and cost the task record carries.
+ if (now.computer) card.appendChild(el(doc, 'div', 'ovdt-meta', [now.computer.machine, now.computer.cost_hint].filter(Boolean).join(' · ')));
+ const buttons = el(doc, 'div', 'ovdt-buttons');
+ // Take over / Give it back: one API on the task at Actor, the same hand-off the agent's own
+ // hand_to_person tool uses. Only a step that names its task (ref) and its Computer offers it.
+ if (now.computer && now.ref) {
+ const driving = now.computer.driving === true || /^You took over/.test(String(now.title || ''));
+ const take = el(doc, 'button', 'ovdt-btn is-primary', driving ? 'Give it back' : 'Take over');
+ take.type = 'button';
+ take.addEventListener('click', async () => {
+ take.disabled = true;
+ const auth = await bearer();
+ try {
+ await api('/api/v1/tasks/' + encodeURIComponent(now.ref) + (driving ? '/giveback' : '/takeover'), {
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) }, body: '{}',
+ });
+ } catch (err) { /* the panel refreshes either way */ }
+ refresh();
+ });
+ buttons.appendChild(take);
+ }
+ const stop = el(doc, 'button', 'ovdt-btn is-stop', 'Stop');
+ stop.type = 'button';
+ stop.addEventListener('click', async () => {
+ stop.disabled = true;
+ const auth = await bearer();
+ try { await api('/api/v1/agent/kill-switch', { method: 'POST', headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) }, body: JSON.stringify({ action: 'stop' }) }); } catch (err) { /* the panel refreshes either way */ }
+ refresh();
+ });
+ buttons.appendChild(stop);
+ card.appendChild(buttons);
+ nowBox.appendChild(card);
+ }
+
+ function renderNeeds(needs) {
+ needsBox.textContent = '';
+ if (!needs.length) {
+ needsBox.appendChild(el(doc, 'div', 'ovdt-empty', 'Nothing needs you.'));
+ return;
+ }
+ for (const item of needs) {
+ const card = el(doc, 'div', 'ovdt-card');
+ card.appendChild(el(doc, 'div', 'ovdt-mode', word(item.mode) + ' needs you'));
+ card.appendChild(el(doc, 'div', 'ovdt-title', item.title));
+ if (item.reason) card.appendChild(el(doc, 'div', 'ovdt-meta', item.reason));
+ const buttons = el(doc, 'div', 'ovdt-buttons');
+ for (const [label, decision] of [['Approve', 'approve'], ['Decline', 'decline']]) {
+ const b = el(doc, 'button', decision === 'approve' ? 'ovdt-btn is-primary' : 'ovdt-btn', label);
+ b.type = 'button';
+ b.addEventListener('click', async () => {
+ b.disabled = true;
+ const approval = item.approval || {};
+ const auth = await bearer();
+ try {
+ await api('/api/v1/agent/approvals/' + encodeURIComponent(approval.product || item.product) + '/' + encodeURIComponent(approval.id) + '/decide', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...(auth ? { Authorization: auth } : {}) },
+ body: JSON.stringify({ decision }),
+ });
+ } catch (err) { /* the panel refreshes either way */ }
+ refresh();
+ });
+ buttons.appendChild(b);
+ }
+ card.appendChild(buttons);
+ needsBox.appendChild(card);
+ }
+ }
+
+ function entryNode(entry) {
+ const card = el(doc, 'div', 'ovdt-card');
+ const row = el(doc, 'div', 'ovdt-row');
+ row.appendChild(el(doc, 'span', 'ovdt-mode', word(entry.mode)));
+ row.appendChild(outcome(entry.status));
+ row.appendChild(el(doc, 'span', 'ovdt-title', entry.title));
+ const when = new Date(entry.started_at || 0);
+ row.appendChild(el(doc, 'span', 'ovdt-meta', Number.isNaN(when.getTime()) ? '' : when.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })));
+ card.appendChild(row);
+ if (entry.transition) card.appendChild(el(doc, 'div', 'ovdt-meta', entry.transition));
+ else if (entry.placement && (entry.placement.where || entry.placement.why)) {
+ card.appendChild(el(doc, 'div', 'ovdt-meta', [entry.placement.where, entry.placement.why].filter(Boolean).join(': ')));
+ }
+ if (entry.computer) card.appendChild(el(doc, 'div', 'ovdt-meta', [entry.computer.machine, entry.computer.cost_hint].filter(Boolean).join(' · ')));
+ if (entry.reason) card.appendChild(el(doc, 'div', 'ovdt-meta', entry.reason));
+ if (entry.cost_usd !== null && entry.cost_usd !== undefined) card.appendChild(el(doc, 'div', 'ovdt-meta', '$' + Number(entry.cost_usd).toFixed(2)));
+ if (entry.link) {
+ const a = el(doc, 'a', 'ovdt-link', 'Open');
+ a.href = entry.link;
+ a.rel = 'noopener noreferrer';
+ a.target = '_blank';
+ card.appendChild(a);
+ }
+ // The step a site opened the Dock at (its task id): it is marked, so the person sees where they landed.
+ const wanted = focusId();
+ if (wanted && (entry.id === wanted || entry.ref === wanted)) card.classList.add('ovdt-focus');
+ return card;
+ }
+
+ /** The step to point at: the id (a task id) a site opened the Dock with, if it is still there. */
+ function focusId() {
+ try { return String(focus() || '').trim() || null; } catch (err) { return null; }
+ }
+
+ /** Bring the focused step into view (the Dock's panel is short; the step may be below the fold). */
+ function scrollToFocus() {
+ const wanted = focusId();
+ if (!wanted) return;
+ const node = list.querySelector('.ovdt-focus');
+ if (!node || !node.scrollIntoView) return;
+ try { node.scrollIntoView({ block: 'nearest' }); } catch (err) { node.scrollIntoView(); }
+ }
+
+ function renderList(entries) {
+ list.textContent = '';
+ if (!entries.length) {
+ list.appendChild(el(doc, 'div', 'ovdt-empty', 'Nothing yet. The agent\u2019s steps show up here as it works.'));
+ return;
+ }
+ // Today's entries are open; earlier days are counted folds.
+ const today = new Date().toISOString().slice(0, 10);
+ const groups = new Map();
+ for (const entry of entries) {
+ const key = dayKey(entry.started_at);
+ if (!groups.has(key)) groups.set(key, []);
+ groups.get(key).push(entry);
+ }
+ for (const [key, group] of groups) {
+ if (key === today) {
+ for (const entry of group) list.appendChild(entryNode(entry));
+ continue;
+ }
+ const fold = el(doc, 'button', 'ovdt-fold', labelOf(key, group.length));
+ fold.type = 'button';
+ fold.setAttribute('aria-expanded', 'false');
+ fold.addEventListener('click', () => {
+ const open = fold.getAttribute('aria-expanded') === 'true';
+ fold.setAttribute('aria-expanded', open ? 'false' : 'true');
+ const shown = fold.nextElementSibling;
+ if (shown && shown.classList.contains('ovdt-fold-body')) shown.remove();
+ if (!open) {
+ const box = el(doc, 'div', 'ovdt-fold-body');
+ for (const entry of group) box.appendChild(entryNode(entry));
+ fold.after(box);
+ }
+ });
+ list.appendChild(fold);
+ }
+ // A long run of one mode folds into "12 more Code steps".
+ let run = 0;
+ let runMode = null;
+ const runs = [];
+ for (const entry of entries) {
+ if (entry.mode === runMode) { run += 1; continue; }
+ if (run >= 3) runs.push({ mode: runMode, count: run });
+ runMode = entry.mode;
+ run = 1;
+ }
+ if (run >= 3) runs.push({ mode: runMode, count: run });
+ for (const r of runs) {
+ const more = el(doc, 'div', 'ovdt-meta', r.count + ' more ' + word(r.mode) + ' steps');
+ list.appendChild(more);
+ }
+ }
+
+ function labelOf(day, count) {
+ const today = new Date();
+ const d = new Date(day + 'T00:00:00Z');
+ const diff = Math.round((Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - d.getTime()) / 86400000);
+ if (diff === 1) return 'Yesterday (' + count + ')';
+ if (diff < 7) return 'This week (' + count + ')';
+ return day + ' (' + count + ')';
+ }
+
+ function renderChips(counts) {
+ chips.textContent = '';
+ for (const m of MODES) {
+ const n = counts && counts[m.id];
+ if (!n) continue;
+ chips.appendChild(chip(m.id, m.word, n));
+ }
+ if (counts && counts.needs_you) chips.appendChild(chip('needs_you', 'Needs you', counts.needs_you));
+ }
+
+ async function refresh() {
+ if (destroyed || !active()) return;
+ const mode = [...filters].find((f) => f !== 'needs_you');
+ const query = [];
+ if (mode) query.push('mode=' + encodeURIComponent(mode));
+ if (filters.has('needs_you')) query.push('needs_you=1');
+ const path = '/api/v1/agent/timeline' + (query.length ? '?' + query.join('&') : '');
+ let data;
+ try { data = await api(path); } catch (err) {
+ if (destroyed) return;
+ container.hidden = true;
+ return;
+ }
+ if (destroyed) return;
+ container.hidden = !data.now && !(data.needs_you || []).length && !(data.entries || []).length;
+ renderNow(data.now);
+ renderNeeds(data.needs_you || []);
+ renderChips(data.counts || {});
+ renderList(data.entries || []);
+ scrollToFocus();
+ writeUrl();
+ }
+
+ function writeUrl() {
+ try {
+ const url = new URL(doc.defaultView.location.href);
+ url.searchParams.delete('mode');
+ url.searchParams.delete('needs_you');
+ for (const f of filters) {
+ if (f === 'needs_you') url.searchParams.set('needs_you', '1');
+ else url.searchParams.set('mode', f);
+ }
+ doc.defaultView.history.replaceState(null, '', url.toString());
+ } catch (err) { /* the panel keeps its filters in memory */ }
+ }
+
+ function start() {
+ if (timer) return;
+ timer = setInterval(refresh, POLL_MS);
+ if (timer.unref) timer.unref();
+ }
+
+ refresh();
+ start();
+
+ return {
+ refresh,
+ destroy() {
+ destroyed = true;
+ if (timer) clearInterval(timer);
+ timer = null;
+ wrap.remove();
+ },
+ };
+ }
+
+ root.OpenVibeDockTimeline = { mount };
+}(typeof window !== 'undefined' ? window : globalThis));
@@ -36,7 +36,6 @@
@@ -36,7 +36,6 @@
.ovdk-tmenu .go { background: var(--accent, #3b82f6); border-color: transparent; color: var(--on-accent, #fff); }
.ovdk-tmenu .go { background: var(--accent, #3b82f6); border-color: transparent; color: var(--on-accent, #fff); }
.ovdk-pill[data-tone="timer"] .ovdk-t { font-variant-numeric: tabular-nums; }
.ovdk-pill[data-tone="timer"] .ovdk-t { font-variant-numeric: tabular-nums; }
.ovdk-pill[data-tone="timer-done"] { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); }
.ovdk-pill[data-tone="timer-done"] { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); }
-@media (prefers-reduced-motion: reduce) { .ovdk-timer-bar i { transition: none; } }
`;
`;
const pad = (n) => String(n).padStart(2, '0');
const pad = (n) => String(n).padStart(2, '0');
const left = (ms) => {
const left = (ms) => {
@@ -9,11 +9,23 @@
@@ -9,11 +9,23 @@
if (typeof document === 'undefined' || root.OpenVibeDockVibe) return;
if (typeof document === 'undefined' || root.OpenVibeDockVibe) return;
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
+ const PLUS = '<path d="M12 5.5v13M5.5 12h13"/>';
+ const OUT = '<path d="M13.5 5.5h5v5M18.5 5.5l-7.5 7.5M17 13.5v4a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 5 17.5v-9A1.5 1.5 0 0 1 6.5 7h4"/>';
const MIC = '<rect x="9" y="3.5" width="6" height="10.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5v3"/>';
const MIC = '<rect x="9" y="3.5" width="6" height="10.5" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5v3"/>';
const CSS = `
const CSS = `
+ .ovdk-ib.is-new { background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }
+ .ovdk-ib.is-new:hover, .ovdk-ib.is-new:focus-visible { background: color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); }
.ovdk-vibe-h .ovdk-i { color: var(--accent, #3b82f6); margin-left: 6px; }
.ovdk-vibe-h .ovdk-i { color: var(--accent, #3b82f6); margin-left: 6px; }
- .ovdk-away { margin-left: auto; font-size: 12.5px; font-weight: 650; color: var(--accent, #3b82f6); text-decoration: none; white-space: nowrap; }
- .ovdk-away:hover { text-decoration: underline; }
+ .ovdk-vibe-list { list-style: none; padding: 2px 6px 6px; margin: 0; display: grid; gap: 2px; }
+ .ovdk-vibe-list button { width: 100%; min-height: 56px; display: grid; grid-template-columns: 36px minmax(0,1fr) auto; grid-template-rows: auto auto; gap: 2px 12px; align-items: center; padding: 9px 10px; border: 0; border-radius: 14px; background: transparent; color: var(--text-primary,#ececf1); text-align: left; cursor: pointer; font: inherit; }
+ .ovdk-vibe-list .ovdk-vibe-site { grid-row: 1 / 3; width: 36px; height: 36px; border-radius: 11px; display: grid; place-items: center; background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }
+ .ovdk-vibe-list .ovdk-vibe-site svg { width: 19px; height: 19px; }
+ .ovdk-vibe-list b { font-size: 14px; } .ovdk-vibe-list span { font-size: 12.5px; color: var(--text-secondary, #b6b6c0); }
+ .ovdk-vibe-list small { justify-self: end; }
+ .ovdk-vibe-list button:hover, .ovdk-vibe-list button:focus-visible { background: var(--hover-bg,rgba(127,127,127,.12)); }
+ .ovdk-vibe-list b, .ovdk-vibe-list span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .ovdk-vibe-list small, .ovdk-vibe-list time { color: var(--text-muted,#8b8b96); font-size: 12px; }
+ .ovdk-vibe-back { min-width: 44px; min-height: 44px; display: grid; place-items: center; border: 0; background: transparent; color: inherit; cursor: pointer; }
.ovdk-msg.is-vibe .ovdk-text a { color: var(--accent, #3b82f6); text-decoration: underline; word-break: break-all; }
.ovdk-msg.is-vibe .ovdk-text a { color: var(--accent, #3b82f6); text-decoration: underline; word-break: break-all; }
.ovdk-typing .ovdk-text { display: inline-flex; gap: 4px; align-items: center; padding: 10px 12px; }
.ovdk-typing .ovdk-text { display: inline-flex; gap: 4px; align-items: center; padding: 10px 12px; }
.ovdk-typing i { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted, #8b8b96); animation: ovdk-hop 1s ease-in-out infinite; }
.ovdk-typing i { width: 6px; height: 6px; border-radius: 50%; background: var(--text-muted, #8b8b96); animation: ovdk-hop 1s ease-in-out infinite; }
@@ -23,7 +35,8 @@
@@ -23,7 +35,8 @@
.ovdk-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 8px; }
.ovdk-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; margin-top: 8px; }
.ovdk-chip { padding: 7px 12px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 12.5px; cursor: pointer; }
.ovdk-chip { padding: 7px 12px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 12.5px; cursor: pointer; }
.ovdk-chip:hover { border-color: var(--accent, #3b82f6); }
.ovdk-chip:hover { border-color: var(--accent, #3b82f6); }
- .ovdk-mic { width: 40px; height: 40px; flex: none; margin-left: -8px; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }
+ .ovdk-mic, .ovdk-attach { width: 44px; height: 44px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }
+ .ovdk-attach:hover { color: var(--text-primary,#ececf1); }
.ovdk-mic:hover { color: var(--text-primary, #ececf1); }
.ovdk-mic:hover { color: var(--text-primary, #ececf1); }
.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }
.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }
.ovdk-res { display: grid; gap: 6px; margin-top: 6px; max-width: 100%; }
.ovdk-res { display: grid; gap: 6px; margin-top: 6px; max-width: 100%; }
@@ -40,10 +53,28 @@
@@ -40,10 +53,28 @@
.ovdk-res-a button:disabled { opacity: .55; cursor: default; }
.ovdk-res-a button:disabled { opacity: .55; cursor: default; }
.ovdk-res-c.is-stopped { border-color: var(--border, rgba(255,255,255,.12)); background: transparent; }
.ovdk-res-c.is-stopped { border-color: var(--border, rgba(255,255,255,.12)); background: transparent; }
.ovdk-res-c.is-stopped .ovdk-i, .ovdk-res-c.is-stopped .ovdk-res-t { color: var(--text-muted, #8b8b96); }
.ovdk-res-c.is-stopped .ovdk-i, .ovdk-res-c.is-stopped .ovdk-res-t { color: var(--text-muted, #8b8b96); }
- @media (prefers-reduced-motion: reduce) { .ovdk-typing i { animation: none; } }
`;
`;
let ctx = null, G = {};
let ctx = null, G = {};
+ // 4.203.0 (ask 349, "a better composer system unified across all sites"): Vibe's box is the one composer too, for
+ // drafts, "/" commands and your own, the marks, what sends and your settings; its picture keeps its own upload.
+ const selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
+ const VIBE_REV = { 'ov-composer.js': '9b3472802903' };
+ function joinComposer(form) {
+ if (!form || form.ownerDocument !== document) return; // a popped-out Dock keeps its plain box
+ form.setAttribute('data-ovc-no-files', '');
+ const go = () => { try { if (root.OpenVibeComposer && form.isConnected) root.OpenVibeComposer.enhance(form); } catch (e) { console.error(e); } };
+ if (root.OpenVibeComposer) { go(); return; }
+ const m = /^(https?:\/\/[^?#]*\/)dock-vibe(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(selfSrc);
+ if (!m) return;
+ let sc = document.getElementById('ov-composer-script');
+ if (!sc) { sc = document.createElement('script'); sc.id = 'ov-composer-script'; sc.async = true; sc.src = m[1] + 'ov-composer.js' + (VIBE_REV['ov-composer.js'] ? '?v=' + VIBE_REV['ov-composer.js'] : ''); document.head.appendChild(sc); }
+ sc.addEventListener('load', go, { once: true });
+ }
const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };
const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };
+ /** A round icon button with its name as label and tooltip. */
+ const iconButton = (tag, cls, path, label) => { const b = h(tag, cls); if (tag === 'button') b.type = 'button'; b.setAttribute('aria-label', label); b.setAttribute('data-ov-tip', label); b.title = label; b.innerHTML = svg(path); return b; };
+ /** Open Vibe in OpenVibe.Chat, as an icon (the thread's own address when one is open). */
+ const awayLink = () => { const a = iconButton('a', 'ovdk-ib ovdk-away', OUT, 'Open in Chat'); a.href = 'https://openvibe.chat/vibe'; a.target = '_blank'; a.rel = 'noopener'; return a; };
// ── Vibe (plan L48 V2; the owner, 11-01: "access your agent or the AI… from across all websites… use a voice
// ── Vibe (plan L48 V2; the owner, 11-01: "access your agent or the AI… from across all websites… use a voice
// feature to tell it to set a reminder… have a conversation with your Vibe AI") ──
// feature to tell it to set a reminder… have a conversation with your Vibe AI") ──
@@ -54,7 +85,7 @@
@@ -54,7 +85,7 @@
// browser without it (Firefox) records instead, up to 30 s, and Chat's /api/vibe/listen turns that into text
// browser without it (Firefox) records instead, up to 30 s, and Chat's /api/vibe/listen turns that into text
// (OpenVibe.AI's speech-to-text on this network's own machines; the recording is never kept).
// (OpenVibe.AI's speech-to-text on this network's own machines; the recording is never kept).
// Everything is set as text; a link is a link only when it is https.
// Everything is set as text; a link is a link only when it is https.
- const vibe = { panel: null, chat: null, msgs: [], last: 0, timer: 0, busy: false, waitUntil: 0, rec: null, talk: null };
+ const vibe = { panel: null, chat: null, view: 'list', fresh: false, msgs: [], last: 0, timer: 0, busy: false, waitUntil: 0, rec: null, talk: null, generation: 0 };
const SR = root.SpeechRecognition || root.webkitSpeechRecognition || null;
const SR = root.SpeechRecognition || root.webkitSpeechRecognition || null;
const MR = !SR && root.MediaRecorder && root.navigator && root.navigator.mediaDevices && root.navigator.mediaDevices.getUserMedia ? root.MediaRecorder : null;
const MR = !SR && root.MediaRecorder && root.navigator && root.navigator.mediaDevices && root.navigator.mediaDevices.getUserMedia ? root.MediaRecorder : null;
const TALK = Boolean(SR || MR);
const TALK = Boolean(SR || MR);
@@ -115,15 +146,52 @@
@@ -115,15 +146,52 @@
if (m.at) li.title = new Date(m.at).toLocaleString();
if (m.at) li.title = new Date(m.at).toLocaleString();
return li;
return li;
}
}
+ const age = (iso) => { const n = Date.parse(iso || ''); if (!Number.isFinite(n)) return ''; const m = Math.max(0, Math.floor((Date.now() - n) / 60000)); return m < 1 ? 'now' : m < 60 ? `${m} min ago` : m < 1440 ? `${Math.floor(m / 60)} h ago` : `${Math.floor(m / 1440)} d ago`; };
+ async function openList() {
+ const generation = ++vibe.generation;
+ clearTimeout(vibe.timer); vibe.view = 'list'; vibe.chat = null;
+ const panel = vibe.panel; if (!panel) return;
+ panel.textContent = '';
+ // No heading (ask 335, the owner: "remove the text … new conversation … an icon plus button … open in Chat a
+ // little button icon"): one slim row of two icon buttons over your chats.
+ const head = h('div', 'ovdk-bar');
+ const newer = iconButton('button', 'ovdk-ib is-new ovdk-vibe-new', PLUS, 'New chat'); newer.addEventListener('click', () => { vibe.fresh = true; vibe.chat = null; ctx.save({ vibe: null }); openVibe(); });
+ head.append(newer, awayLink());
+ const scroll = h('div', 'ovdk-scroll'); const list = h('ul', 'ovdk-vibe-list');
+ scroll.append(list); panel.append(head, scroll);
+ try {
+ const out = await ctx.chatCall('/api/vibe/conversations');
+ if (generation !== vibe.generation || vibe.view !== 'list' || vibe.panel !== panel) return;
+ const conversations = Array.isArray(out.conversations) ? out.conversations : [];
+ if (!conversations.length) list.append(h('li', 'ovdk-none', 'Your chats with Vibe will appear here. Press + to start one.'));
+ conversations.forEach((c) => {
+ const li = h('li'), b = h('button'); b.type = 'button';
+ const title = h('b', '', c.name || 'Conversation'); const time = h('time', '', age(c.last_message_at));
+ const line = h('span', '', c.last_line || 'No messages yet');
+ // The site it began on, by name ("Blog", not openvibe.blog), beside the time.
+ const host = String(c.site || '').replace(/^https?:\/\//, '').replace(/\/.*$/, '');
+ const name = host ? (/^openvibe\.([a-z]+)$/.exec(host) || [])[1] : '';
+ const site = h('small', '', name ? name.charAt(0).toUpperCase() + name.slice(1) : host);
+ const mark = h('span', 'ovdk-vibe-site'); mark.append(icon(G.vibe));
+ b.append(mark, title, time, line, site);
+ b.setAttribute('aria-label', `${c.name || 'Conversation'}, ${age(c.last_message_at)}`);
+ b.addEventListener('click', () => { vibe.fresh = false; vibe.chat = { slug: c.slug }; ctx.save({ vibe: { slug: c.slug, at: new Date().toISOString() } }); openVibe(); });
+ li.append(b); list.append(li);
+ });
+ } catch (e) { if (generation === vibe.generation) list.append(h('li', 'ovdk-none', e && e.status === 401 ? 'Sign in to see your conversations.' : 'Conversations did not load. Open Vibe again to retry.')); }
+ }
async function openVibe() {
async function openVibe() {
+ const generation = ++vibe.generation;
clearTimeout(vibe.timer);
clearTimeout(vibe.timer);
+ vibe.view = 'conversation';
const panel = vibe.panel; if (!panel) return;
const panel = vibe.panel; if (!panel) return;
vibe.msgs = []; vibe.last = 0;
vibe.msgs = []; vibe.last = 0;
panel.textContent = '';
panel.textContent = '';
const head = h('div', 'ovdk-thread-h ovdk-vibe-h');
const head = h('div', 'ovdk-thread-h ovdk-vibe-h');
const title = h('b', '', 'Vibe');
const title = h('b', '', 'Vibe');
- const away = h('a', 'ovdk-away', 'Open in Chat'); away.href = 'https://openvibe.chat/vibe'; away.target = '_blank'; away.rel = 'noopener';
- head.append(icon(G.vibe), title, away);
+ const back = h('button', 'ovdk-vibe-back'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your chats'); back.textContent = '←'; back.addEventListener('click', () => { ctx.save({ vibe: null }); openList(); });
+ const away = awayLink();
+ head.append(back, icon(G.vibe), title, away);
const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');
const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');
const scroll = h('div', 'ovdk-scroll ovdk-thread'); scroll.append(list);
const scroll = h('div', 'ovdk-scroll ovdk-thread'); scroll.append(list);
const typing = h('li', 'ovdk-msg is-vibe ovdk-typing'); typing.innerHTML = '<span class="ovdk-text"><i></i><i></i><i></i></span>'; typing.setAttribute('aria-label', 'Vibe is writing');
const typing = h('li', 'ovdk-msg is-vibe ovdk-typing'); typing.innerHTML = '<span class="ovdk-text"><i></i><i></i><i></i></span>'; typing.setAttribute('aria-label', 'Vibe is writing');
@@ -131,6 +199,37 @@
@@ -131,6 +199,37 @@
const box = h('textarea'); box.rows = 1; box.maxLength = 8000; box.placeholder = TALK ? 'Ask Vibe, or hold the mic' : 'Ask Vibe'; box.setAttribute('aria-label', 'Message Vibe');
const box = h('textarea'); box.rows = 1; box.maxLength = 8000; box.placeholder = TALK ? 'Ask Vibe, or hold the mic' : 'Ask Vibe'; box.setAttribute('aria-label', 'Message Vibe');
const go = h('button', 'ovdk-go'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(icon(G.send));
const go = h('button', 'ovdk-go'); go.type = 'submit'; go.setAttribute('aria-label', 'Send'); go.append(icon(G.send));
const err = h('p', 'ovdk-err'); err.hidden = true;
const err = h('p', 'ovdk-err'); err.hidden = true;
+ const reason = (e) => e && e.status === 401 ? 'Your sign in expired. Sign in again to continue.'
+ : e && e.status === 429 ? 'Too many requests were sent. Wait a minute, then retry.'
+ : e && e.code === 'vibe.off' ? 'Vibe is unavailable right now. Retry when it returns.'
+ : e && e.status === 503 ? 'The AI service is unavailable right now. Retry shortly.'
+ : 'The request could not reach Vibe. Check your connection and retry.';
+ const showFailure = (e, retry) => { err.textContent = `Vibe did not answer: ${reason(e)} `; err.hidden = false;
+ if (retry) { const b = h('button', '', 'Retry'); b.type = 'button'; b.addEventListener('click', async () => { b.disabled = true; try { await retry(); } catch (x) { showFailure(x, retry); } finally { b.disabled = false; } }); err.append(b); }
+ };
+ const attach = h('button', 'ovdk-attach', '+'); attach.type = 'button'; attach.setAttribute('aria-label', 'Attach a picture'); attach.title = 'Attach a picture';
+ const file = h('input'); file.type = 'file'; file.accept = 'image/*'; file.hidden = true;
+ attach.addEventListener('click', () => file.click());
+ file.addEventListener('change', async () => {
+ const picture = file.files && file.files[0]; if (!picture) return;
+ attach.disabled = true; err.hidden = true;
+ try {
+ const path = vibe.chat ? `/api/chat/rooms/${encodeURIComponent(vibe.chat.slug)}/pictures` : '/api/chat/vibe/picture';
+ const out = await ctx.chatCall(path, { method: 'POST', body: picture, type: picture.type || 'image/jpeg' });
+ if (out.room && out.room.slug) { vibe.chat = { slug: out.room.slug }; vibe.fresh = false; ctx.save({ vibe: { slug: out.room.slug, at: new Date().toISOString() } }); }
+ openVibe();
+ } catch { err.textContent = 'That picture could not be attached. Try again.'; err.hidden = false; }
+ finally { attach.disabled = false; file.value = ''; }
+ });
+ // 350: Build it in Code. What you wrote, else Vibe's last answer, goes to the Code part as a new task's words.
+ const build = iconButton('button', 'ovdk-attach ovdk-build', 'M8 7l-5 5 5 5M16 7l5 5-5 5', 'Build it in Code');
+ build.addEventListener('click', () => {
+ const last = [...vibe.msgs].reverse().find((m) => m.from !== 'you');
+ const text = box.value.trim() || (last && last.text) || '';
+ if (!text) { box.focus(); return; }
+ build.dispatchEvent(new CustomEvent('ov-dock:part', { bubbles: true, detail: { part: 'code', text: text.slice(0, 16000) } }));
+ });
+ form.append(attach, file, build);
let mic = null;
let mic = null;
if (TALK) {
if (TALK) {
mic = h('button', 'ovdk-mic'); mic.type = 'button'; mic.append(icon(G.mic));
mic = h('button', 'ovdk-mic'); mic.type = 'button'; mic.append(icon(G.mic));
@@ -140,6 +239,8 @@
@@ -140,6 +239,8 @@
form.append(box, go);
form.append(box, go);
panel.append(head, scroll, err, form);
panel.append(head, scroll, err, form);
const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };
const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };
+ vibe.fill = (text) => { box.value = String(text || '').slice(0, 8000); fit(); try { box.focus(); box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } };
+ if (vibe.pendingAsk) { vibe.fill(vibe.pendingAsk); vibe.pendingAsk = null; }
box.addEventListener('input', fit);
box.addEventListener('input', fit);
box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });
box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });
@@ -148,8 +249,9 @@
@@ -148,8 +249,9 @@
vibe.busy = true; go.disabled = true; err.hidden = true;
vibe.busy = true; go.disabled = true; err.hidden = true;
try {
try {
let timezone = null; try { timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { /* */ }
let timezone = null; try { timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } catch { /* */ }
- const out = await ctx.chatCall('/api/vibe/ask', { method: 'POST', body: JSON.stringify({ message: text, slug: vibe.chat ? vibe.chat.slug : undefined, timezone }) });
- if (out.chat) { vibe.chat = out.chat; away.href = out.chat.url; }
+ const out = await ctx.chatCall('/api/vibe/ask', { method: 'POST', body: JSON.stringify({ message: text, slug: vibe.chat ? vibe.chat.slug : undefined, fresh: vibe.fresh, timezone }) });
+ if (generation !== vibe.generation) return;
+ if (out.chat) { vibe.chat = out.chat; vibe.fresh = false; away.href = out.chat.url; ctx.save({ vibe: { slug: out.chat.slug, at: new Date().toISOString() } }); }
box.value = ''; fit();
box.value = ''; fit();
const empty = list.querySelector('.ovdk-vibe-start'); if (empty) empty.remove();
const empty = list.querySelector('.ovdk-vibe-start'); if (empty) empty.remove();
if (out.message && !vibe.msgs.some((x) => x.id === out.message.id)) { list.append(vbubble(out.message, vibe.msgs[vibe.msgs.length - 1])); vibe.msgs.push(out.message); vibe.last = Math.max(vibe.last, out.message.id || 0); }
if (out.message && !vibe.msgs.some((x) => x.id === out.message.id)) { list.append(vbubble(out.message, vibe.msgs[vibe.msgs.length - 1])); vibe.msgs.push(out.message); vibe.last = Math.max(vibe.last, out.message.id || 0); }
@@ -158,22 +260,22 @@
@@ -158,22 +260,22 @@
scroll.scrollTop = scroll.scrollHeight;
scroll.scrollTop = scroll.scrollHeight;
clearTimeout(vibe.timer); vibe.timer = setTimeout(() => pull(), 1200);
clearTimeout(vibe.timer); vibe.timer = setTimeout(() => pull(), 1200);
} catch (x) {
} catch (x) {
- err.textContent = x.status === 429 ? 'That is a lot of questions at once: wait a minute, then ask again.' : x.status === 401 ? 'Sign in again to talk to Vibe.' : 'That did not reach Vibe. Try again.';
- err.hidden = false;
+ showFailure(x, () => send(text));
} finally { vibe.busy = false; go.disabled = false; box.focus(); }
} finally { vibe.busy = false; go.disabled = false; box.focus(); }
}
}
form.addEventListener('submit', (e) => { e.preventDefault(); send(box.value.trim()); });
form.addEventListener('submit', (e) => { e.preventDefault(); send(box.value.trim()); });
+ joinComposer(form);
async function pull(first) {
async function pull(first) {
let got;
let got;
try {
try {
- got = await ctx.chatCall(`/api/vibe/chat${vibe.chat ? `?slug=${encodeURIComponent(vibe.chat.slug)}${vibe.last ? `&after=${vibe.last}` : ''}` : ''}`);
+ got = vibe.fresh && !vibe.chat ? { chat: null, messages: [] } : await ctx.chatCall(`/api/vibe/chat${vibe.chat ? `?slug=${encodeURIComponent(vibe.chat.slug)}${vibe.last ? `&after=${vibe.last}` : ''}` : ''}`);
} catch (e) {
} catch (e) {
- if (first) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 401 ? 'Sign in to OpenVibe.Chat to talk to Vibe.' : 'Vibe did not answer just now. Try again in a moment.')); }
+ if (first && generation === vibe.generation) { list.textContent = ''; showFailure(e, () => pull(true)); }
return;
return;
}
}
- if (!ctx.active() || vibe.panel !== panel) return;
- if (got.chat) { vibe.chat = got.chat; away.href = got.chat.url; }
+ if (generation !== vibe.generation || !ctx.active() || vibe.panel !== panel) return;
+ if (got.chat) { vibe.chat = got.chat; away.href = got.chat.url; title.textContent = got.chat.name || 'Vibe'; }
if (first) list.textContent = '';
if (first) list.textContent = '';
const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;
const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;
let answered = false;
let answered = false;
@@ -184,7 +286,13 @@
@@ -184,7 +286,13 @@
if (m.from === 'vibe') answered = true;
if (m.from === 'vibe') answered = true;
if (!first && Array.isArray(m.results) && m.results.some((r) => r && r.kind === 'timer') && ctx.refresh) ctx.refresh();
if (!first && Array.isArray(m.results) && m.results.some((r) => r && r.kind === 'timer') && ctx.refresh) ctx.refresh();
});
});
- if (answered || Date.now() > vibe.waitUntil) { typing.remove(); vibe.waitUntil = 0; }
+ if (answered || Date.now() > vibe.waitUntil) {
+ typing.remove();
+ if (!answered && vibe.waitUntil && Date.now() > vibe.waitUntil) {
+ showFailure({ status: 503 }, vibe.chat ? async () => { await ctx.chatCall(`/api/chat/vibe/${encodeURIComponent(vibe.chat.slug)}/retry`, { method: 'POST', body: '{}' }); err.hidden = true; vibe.waitUntil = Date.now() + 90000; list.append(typing); pull(); } : null);
+ }
+ vibe.waitUntil = 0;
+ }
if (first && !vibe.msgs.length) {
if (first && !vibe.msgs.length) {
const start = h('li', 'ovdk-vibe-start');
const start = h('li', 'ovdk-vibe-start');
start.append(h('b', '', 'Ask Vibe anything'), h('span', '', TALK ? 'Type, or hold the mic and say it. It sets reminders, finds what is live, and answers from the whole network.' : 'It sets reminders, finds what is live, and answers from the whole network.'));
start.append(h('b', '', 'Ask Vibe anything'), h('span', '', TALK ? 'Type, or hold the mic and say it. It sets reminders, finds what is live, and answers from the whole network.' : 'It sets reminders, finds what is live, and answers from the whole network.'));
@@ -267,7 +375,9 @@
@@ -267,7 +375,9 @@
list.append(h('li', 'ovdk-wait', null));
list.append(h('li', 'ovdk-wait', null));
list.firstChild.innerHTML = '<span></span>';
list.firstChild.innerHTML = '<span></span>';
await pull(true);
await pull(true);
- setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);
+ // A touch screen does not get the keyboard on opening (11-09, the owner: it "immediately opens the keyboard… you
+ // can't see the chat input or what it did"): the answer and the box show first; a tap on the box types.
+ if (!(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);
}
}
function mount(panel, c) {
function mount(panel, c) {
@@ -277,11 +387,18 @@
@@ -277,11 +387,18 @@
if (st && !st.textContent.includes('.ovdk-vibe-start')) st.textContent += CSS;
if (st && !st.textContent.includes('.ovdk-vibe-start')) st.textContent += CSS;
else if (!st && !document.getElementById('ovdk-vibe-styles')) { const s = document.createElement('style'); s.id = 'ovdk-vibe-styles'; s.textContent = CSS; document.head.appendChild(s); }
else if (!st && !document.getElementById('ovdk-vibe-styles')) { const s = document.createElement('style'); s.id = 'ovdk-vibe-styles'; s.textContent = CSS; document.head.appendChild(s); }
vibe.panel = panel;
vibe.panel = panel;
- openVibe();
+ // The Chats tab's AI chats row asks for the list, not for the last conversation.
+ const onList = ctx.wantList ? ctx.wantList() : false;
+ const selected = ctx.state && ctx.state().vibe;
+ if (!onList && selected && selected.slug && Date.now() - Date.parse(selected.at) < 20 * 60000) { vibe.chat = { slug: selected.slug }; openVibe(); }
+ else openList();
return {
return {
- refresh: () => { vibe.panel = panel; return openVibe(); },
- pause: () => { clearTimeout(vibe.timer); if (vibe.rec) { try { vibe.rec.abort(); } catch { /* */ } } },
+ refresh: () => { vibe.panel = panel; const list = ctx.wantList ? ctx.wantList() : false; if (list) return openList(); return vibe.view === 'list' ? openList() : openVibe(); },
+ list: () => { vibe.panel = panel; return openList(); },
+ pause: () => { vibe.generation++; clearTimeout(vibe.timer); if (vibe.rec) { try { vibe.rec.abort(); } catch { /* */ } } },
talk: () => { if (vibe.talk) vibe.talk(); },
talk: () => { if (vibe.talk) vibe.talk(); },
+ // A page's "Ask your agent" (4.135.0): the question waits in the box, with its context, for the person to send.
+ ask: (text) => { if (vibe.view === 'list') { vibe.pendingAsk = text; vibe.fresh = true; openVibe(); } else if (vibe.fill) vibe.fill(text); else vibe.pendingAsk = text; },
};
};
}
}
root.OpenVibeDockVibe = { mount };
root.OpenVibeDockVibe = { mount };
@@ -1,70 +1,62 @@
@@ -1,70 +1,62 @@
-// ═══════════════════════════════════════════════════════════════
-// OpenVibe — the Dock (Dock plan 2026-10-30, K-D1): your agents and your activity at the edge of every OpenVibe page
-//
-// navbar.js loads this file from beside itself for a signed-in person, after the bell, and mounts it:
-// OpenVibeDock.mount({ token: () => jwt, apiBase, sibling }) the pill; mount(null) takes it away (signed out)
-// OpenVibeDock.open({ tab }) · close() · isOpen()
-//
-// The pill (the mark, a word and a dot) shows only while an agent works, one needs you, or the person pinned it, in the
-// corner the Dock sits in. It paints at once from this site's copy (localStorage ov-dock), then from the Network, and
-// re-reads every 30 s while something runs and when the page comes back. A 404 (Network without the route yet) keeps
-// { off } for a day, and navbar.js does not load this file until then.
-//
-// State: GET/PUT <apiBase>/api/v1/me/dock (network.dock-result@1, network.dock-state@1): open, tab, pinned and where it
-// sits for each device class, so it follows the person from site to site. A change shows at once (onChange paints the
-// pill and the site's copy) and reaches the Network a beat later; before Network has the route the copy stands.
-// Tabs: Agents (action-center.js mount(): what needs you, then what runs, approve or decline in place; K-D3: above it,
-// the person's own desk stream — dock-agent.js loads the first time the tab opens — with Curtain now, End and the still
-// when nothing is live), Chats (K-D2: the person's conversations and DMs from OpenVibe.Chat's own API,
-// GET /api/dm/conversations, opened in place, read and answered there) and Activity (notification-ui.js renderDock():
-// the bell's list).
-// Desktop: a floating card at the pill's corner. Drag its head to move it (it snaps to the nearest corner), drag the
-// free corner to resize (320-720 × 360-900 px). It is not modal: the page stays usable. Phones: a sheet from the
-// bottom over a scrim; drag the head down, ×, Escape or a tap outside closes it. Motion is transform and opacity only.
-// Pop out (K-D4): on desktop Chromium the panel moves into a window of its own (Document Picture-in-Picture) — the same
-// DOM, the same tabs, the page's own styles copied in; the page keeps the pill, which says where the Dock went.
-// ═══════════════════════════════════════════════════════════════
+// The Dock: navbar.js calls mount({ token, apiBase, sibling }) for the pill. open({ tab }) opens the floating card;
+// mountInto(host, { tab }) places its panel body inside another sheet. Both use the same state, counts and loaders.
(function (root) {
(function (root) {
'use strict';
'use strict';
if (typeof document === 'undefined' || root.OpenVibeDock) return;
if (typeof document === 'undefined' || root.OpenVibeDock) return;
- const W = [320, 720], H = [360, 900], DEF = { width: 400, height: 560 };
+ const W = [320, 720], H = [360, 900], DEF = { width: 420, height: 640 };
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
const CORNERS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
const svg = (p) => `<svg viewBox="0 0 24 24" aria-hidden="true">${p}</svg>`;
+ // Close is the OpenVibe menu's orb in its × form (11-09, the owner: "replace this close button with our own… the
+ // corner button… its close form… nice animations"): nine dots that fold into an × as the Dock opens.
+ // The close button is the menu button, already crossed (navbar.js, root.OpenVibeOrb): it forms as the sheet opens.
+ const ORBX = () => (root.OpenVibeOrb ? root.OpenVibeOrb.svg(22, true) : '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7l10 10M17 7L7 17" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>');
const G = {
const G = {
+ code: '<path d="M8 7l-5 5 5 5M16 7l5 5-5 5"/>',
spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',
spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',
bell: '<path d="M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 2h-14z"/><path d="M10 20.5a2.2 2.2 0 0 0 4 0"/>',
bell: '<path d="M6.5 16.5V11a5.5 5.5 0 0 1 11 0v5.5l1.5 2h-14z"/><path d="M10 20.5a2.2 2.2 0 0 0 4 0"/>',
- pin: '<path d="M14.5 3.5l6 6-3 1.5-3.5 3.5.5 4-1.5 1.5-4-4-4.5 4.5M9 15l-4-4 1.5-1.5 4 .5L14 6.5z"/>',
+ // An upright pushpin whose lines never cross (owner, 11-09: the old one had "a line going through it").
+ pin: '<path d="M7.5 3.5h9M10 3.5v5.3L6.5 13h11L14 8.8V3.5M12 13v7.5"/>',
close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',
close: '<path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/>',
open: '<path d="M13.5 5.5h5v5M18.5 5.5l-8 8M16.5 13.5v4a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h4"/>',
open: '<path d="M13.5 5.5h5v5M18.5 5.5l-8 8M16.5 13.5v4a1 1 0 0 1-1 1h-9a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h4"/>',
chat: '<path d="M5 6.5h14a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-7l-4 3v-3H5A1.5 1.5 0 0 1 3.5 16V8A1.5 1.5 0 0 1 5 6.5z"/>',
chat: '<path d="M5 6.5h14a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5h-7l-4 3v-3H5A1.5 1.5 0 0 1 3.5 16V8A1.5 1.5 0 0 1 5 6.5z"/>',
back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',
back: '<path d="M14.5 6.5L9 12l5.5 5.5"/>',
send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',
send: '<path d="M4.5 12h11M11 6.5l5.5 5.5-5.5 5.5"/>',
vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',
vibe: '<path d="M12 4.5a7.5 7.5 0 0 1 6.4 11.4L19.5 19.5l-3.6-1.1A7.5 7.5 0 1 1 12 4.5z"/><path d="M9 11.5h.01M12 11.5h.01M15 11.5h.01"/>',
+ // Three filled dots on the exact centre line (a stroked h.01 path drew them a hair off centre: the owner, 11-09).
+ more: '<g fill="currentColor" stroke="none"><circle cx="5.5" cy="12" r="1.7"/><circle cx="12" cy="12" r="1.7"/><circle cx="18.5" cy="12" r="1.7"/></g>',
clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',
clock: '<circle cx="12" cy="13" r="7.5"/><path d="M12 9.5V13l2.5 1.5M9.5 3h5"/>',
};
};
// Vibe first (L48 V2): the person's AI, on every site.
// Vibe first (L48 V2): the person's AI, on every site.
- const TABS = [['vibe', 'Vibe', G.vibe], ['agent', 'Agents', G.spark], ['chats', 'Chats', G.chat], ['activity', 'Activity', G.bell]];
+ const ROBOT = '<rect x="5" y="8" width="14" height="10" rx="3"/><path d="M12 8V4.5M9.5 13h.01M14.5 13h.01M3 12.5v2M21 12.5v2"/>'; // as in the menu's row
+ const TABS = [['vibe', 'Vibe', G.spark], ['code', 'Code', G.code], ['agent', 'Agents', ROBOT], ['chats', 'Chat', G.chat], ['activity', 'Activity', G.bell]];
// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a
// The tabs the Network keeps in the person's Dock state (network.dock-state@1; vibe since contracts 0.270.0); a
// newer one stays on this site's copy until the Network learns it.
// newer one stays on this site's copy until the Network learns it.
- const SAVED_TABS = ['vibe', 'agent', 'chats', 'activity'];
+ const SAVED_TABS = ['vibe', 'code', 'agent', 'chats', 'activity'];
const DEFAULT_STATE = { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } } };
const DEFAULT_STATE = { open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } } };
- let el = null, o = {}, doc = null, tab = 'activity', saveT = 0, pending = {}, lastFocus = null;
+ let el = null, bodyEl = null, inline = null, panelEpoch = 0, o = {}, doc = null, tab = 'activity', saveT = 0, pending = {}, lastFocus = null, dockSurface = null;
// Pop out (K-D4): the window the panel lives in while it is popped out, and whether it is in one.
// Pop out (K-D4): the window the panel lives in while it is popped out, and whether it is in one.
let pipWin = null, popped = false, pipClosing = false, popOk = true;
let pipWin = null, popped = false, pipClosing = false, popOk = true;
const mounts = {};
const mounts = {};
const phone = () => { try { return root.matchMedia('(max-width: 640px)').matches; } catch { return false; } };
const phone = () => { try { return root.matchMedia('(max-width: 640px)').matches; } catch { return false; } };
- const reduced = () => { try { return root.matchMedia('(prefers-reduced-motion: reduce)').matches || document.documentElement.getAttribute('data-ov-motion') === 'reduced'; } catch { return false; } };
+ // Motion off (html[data-ov-motion=off], theme-loader.js) is the only setting that skips an effect; Calm plays it.
+ const reduced = () => document.documentElement.getAttribute('data-ov-motion') === 'off';
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const state = () => (doc && doc.state) || DEFAULT_STATE;
const state = () => (doc && doc.state) || DEFAULT_STATE;
const counts = () => (doc && doc.counts) || {};
const counts = () => (doc && doc.counts) || {};
const kind = () => (phone() ? 'phone' : 'desktop');
const kind = () => (phone() ? 'phone' : 'desktop');
const place = () => ({ ...DEFAULT_STATE.place[kind()], ...((state().place || {})[kind()] || {}) });
const place = () => ({ ...DEFAULT_STATE.place[kind()], ...((state().place || {})[kind()] || {}) });
const sibling = (name) => (typeof o.sibling === 'function' ? o.sibling(name) : `https://openvibe.network/shared/${name}`);
const sibling = (name) => (typeof o.sibling === 'function' ? o.sibling(name) : `https://openvibe.network/shared/${name}`);
- // K-D3: the agent stream comes from OpenVibe.Codes' API (it mints the view ticket with the person's own token).
- const agentBase = () => String(o.agentBase || 'https://openvibe.codes').replace(/\/+$/, '');
+ // K-D3: the agent stream is OpenVibe.Codes' (it mints the view ticket with the person's own token) and the one
+ // timeline behind the Dock is OpenVibe.Actor's (design 2026-11-07). On a site both go through Network's frame
+ // relay: every site's connect-src allows openvibe.network and refuses openvibe.codes and openvibe.actor. A test
+ // or the extension passes agentBase/actorBase and reaches the product itself; actorBase also names the Actor's
+ // pages in the run list.
+ const frameBase = () => String(o.frameBase || (String(o.apiBase || 'https://openvibe.network').replace(/\/+$/, '') + '/api/frame')).replace(/\/+$/, '');
+ const directBase = (v) => (v ? String(v).replace(/\/+$/, '') : '');
+ const actorBase = () => String(o.actorBase || 'https://openvibe.actor').replace(/\/+$/, '');
async function call(path, init = {}) {
async function call(path, init = {}) {
const base = String(o.apiBase || 'https://openvibe.network').replace(/\/+$/, '');
const base = String(o.apiBase || 'https://openvibe.network').replace(/\/+$/, '');
@@ -96,12 +88,14 @@
@@ -96,12 +88,14 @@
} catch { /* offline, or no route yet: the copy stands and the next change tries again */ }
} catch { /* offline, or no route yet: the copy stands and the next change tries again */ }
}
}
+ // Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
+ const DOCK_REV = { 'dock-vibe.js': 'ee110f2a0c1d', 'dock-code.js': '7a768982eff3', 'dock-mount.js': 'fb07dccadb8c', 'dock-runs.js': '4368f039df7c', 'ai-marks-client.js': 'f4a880a9f206', 'action-center.js': 'e40346ec1f16', 'dock-timeline.js': 'bffaed38fccf', 'dock-actor.js': 'f9aea671c7ad', 'dock-agent.js': '8787080a04d3', 'dock-timer.js': '460d68aa8321', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016', 'dock-chat-edits.js': '21e125e3f1e9' };
function load(src, test) {
function load(src, test) {
return new Promise((ok) => {
return new Promise((ok) => {
if (test()) return ok(true);
if (test()) return ok(true);
const id = `ov-dock-${src.replace(/\W/g, '')}`;
const id = `ov-dock-${src.replace(/\W/g, '')}`;
let sc = document.getElementById(id);
let sc = document.getElementById(id);
- if (!sc) { sc = document.createElement('script'); sc.id = id; sc.src = sibling(src); sc.async = true; document.head.appendChild(sc); }
+ if (!sc) { sc = document.createElement('script'); sc.id = id; sc.src = sibling(src) + (DOCK_REV[src] ? `?v=${DOCK_REV[src]}` : ''); sc.async = true; document.head.appendChild(sc); }
sc.addEventListener('load', () => ok(test()));
sc.addEventListener('load', () => ok(test()));
sc.addEventListener('error', () => ok(false));
sc.addEventListener('error', () => ok(false));
});
});
@@ -112,7 +106,7 @@
@@ -112,7 +106,7 @@
const st = document.createElement('style');
const st = document.createElement('style');
st.id = 'ovdk-styles';
st.id = 'ovdk-styles';
st.textContent = `
st.textContent = `
- .ovdk-pill { --t: var(--accent, #3b82f6); --bg: var(--bg-elevated, var(--bg-secondary, #252530)); position: fixed; z-index: 9995; right: max(16px, env(safe-area-inset-right, 0px)); bottom: calc(max(16px, env(safe-area-inset-bottom, 0px)) + var(--ovnav-appbar-h, 0px));
+ .ovdk-pill { --t: var(--accent, #3b82f6); --bg: var(--bg-elevated, var(--bg-secondary, #252530)); position: fixed; z-index: var(--ovb-z-pill, 9995); right: max(16px, env(safe-area-inset-right, 0px)); bottom: max(calc(16px + var(--ovb-dock, calc(max(var(--ovnav-appbar-h, 0px), env(safe-area-inset-bottom, 0px)) + var(--ovb-mini, 0px)))), var(--ovb-composer, 0px)); translate: 0 calc(-1 * var(--ovb-lift, 0px));
display: inline-flex; align-items: center; gap: 9px; height: 46px; padding: 0 18px 0 9px; border-radius: 99px; border: 1px solid var(--border, #333340); background: var(--bg); color: var(--text-primary, #e0e0e0);
display: inline-flex; align-items: center; gap: 9px; height: 46px; padding: 0 18px 0 9px; border-radius: 99px; border: 1px solid var(--border, #333340); background: var(--bg); color: var(--text-primary, #e0e0e0);
font: inherit; font-size: 13.5px; font-weight: 700; letter-spacing: -.005em; box-shadow: 0 14px 34px -14px rgba(0,0,0,.6); cursor: pointer; -webkit-tap-highlight-color: transparent; animation: ovdk-in .3s cubic-bezier(.2,.9,.25,1.2); }
font: inherit; font-size: 13.5px; font-weight: 700; letter-spacing: -.005em; box-shadow: 0 14px 34px -14px rgba(0,0,0,.6); cursor: pointer; -webkit-tap-highlight-color: transparent; animation: ovdk-in .3s cubic-bezier(.2,.9,.25,1.2); }
.ovdk-pill[hidden] { display: none; }
.ovdk-pill[hidden] { display: none; }
@@ -130,9 +124,7 @@
@@ -130,9 +124,7 @@
.ovdk-pill:has(.ovdk-t:empty) { padding-right: 8px; }
.ovdk-pill:has(.ovdk-t:empty) { padding-right: 8px; }
@keyframes ovdk-in { from { opacity: 0; transform: translateY(10px) scale(.9); } }
@keyframes ovdk-in { from { opacity: 0; transform: translateY(10px) scale(.9); } }
@keyframes ovdk-ping { to { transform: scale(2.6); opacity: 0; } }
@keyframes ovdk-ping { to { transform: scale(2.6); opacity: 0; } }
- @media (prefers-reduced-motion: reduce) { .ovdk-pill, .ovdk-m::before { animation: none !important; } }
- html[data-ov-motion="reduced"] .ovdk-pill, html[data-ov-motion="reduced"] .ovdk-m::before { animation: none; }
- .ovdk { --ovdk-gap: 16px; position: fixed; z-index: 9996; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box;
+ .ovdk { --ovdk-gap: 16px; position: fixed; z-index: var(--ovb-z-dock, 9996); display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box;
right: max(var(--ovdk-gap), env(safe-area-inset-right, 0px)); bottom: calc(max(var(--ovdk-gap), env(safe-area-inset-bottom, 0px)) + var(--ovnav-appbar-h, 0px));
right: max(var(--ovdk-gap), env(safe-area-inset-right, 0px)); bottom: calc(max(var(--ovdk-gap), env(safe-area-inset-bottom, 0px)) + var(--ovnav-appbar-h, 0px));
width: ${DEF.width}px; height: ${DEF.height}px; max-width: calc(100vw - 32px); max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px);
width: ${DEF.width}px; height: ${DEF.height}px; max-width: calc(100vw - 32px); max-height: calc(100vh - 96px); max-height: calc(100dvh - 96px);
background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); color: var(--text-primary, #ececf1); border: 1px solid var(--border, rgba(255,255,255,.1));
background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); color: var(--text-primary, #ececf1); border: 1px solid var(--border, rgba(255,255,255,.1));
@@ -148,6 +140,8 @@
@@ -148,6 +140,8 @@
@keyframes ovdk-grow { from { opacity: 0; transform: scale(.82); } }
@keyframes ovdk-grow { from { opacity: 0; transform: scale(.82); } }
@keyframes ovdk-rise { from { transform: translateY(100%); } }
@keyframes ovdk-rise { from { transform: translateY(100%); } }
.ovdk svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ovdk svg { fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
+ /* The menu button's drawing (navbar.js) has its own fills: the line style above is not its. */
+ .ovdk svg.ovmb { fill: currentColor; stroke: none; stroke-width: 0; }
.ovdk-head { position: relative; flex: none; display: flex; align-items: center; gap: 8px; padding: 10px 8px 8px 10px; cursor: grab; touch-action: none; user-select: none; }
.ovdk-head { position: relative; flex: none; display: flex; align-items: center; gap: 8px; padding: 10px 8px 8px 10px; cursor: grab; touch-action: none; user-select: none; }
.ovdk-tabs { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #ececf1) 6%, transparent); min-width: 0; flex: 0 1 auto; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
.ovdk-tabs { display: flex; gap: 2px; padding: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #ececf1) 6%, transparent); min-width: 0; flex: 0 1 auto; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
.ovdk-tabs::-webkit-scrollbar { display: none; }
.ovdk-tabs::-webkit-scrollbar { display: none; }
@@ -156,7 +150,7 @@
@@ -156,7 +150,7 @@
color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }
color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13.5px; font-weight: 700; cursor: pointer; white-space: nowrap; transition: background .15s, color .15s; }
.ovdk-tabs button svg { width: 17px; height: 17px; flex: none; }
.ovdk-tabs button svg { width: 17px; height: 17px; flex: none; }
.ovdk-tabs button:hover { color: var(--text-primary, #ececf1); }
.ovdk-tabs button:hover { color: var(--text-primary, #ececf1); }
- .ovdk-tabs button[aria-selected="true"] { background: var(--bg-primary, #12141b); color: var(--text-primary, #ececf1); box-shadow: 0 1px 3px rgba(0,0,0,.25); }
+ .ovdk-tabs button[aria-selected="true"] { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, var(--accent, #8ab4ff)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 30%, transparent); }
.ovdk-tabs button[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
.ovdk-tabs button[aria-selected="true"] svg { color: var(--accent-light, var(--accent, #60a5fa)); }
.ovdk-tabs button:focus-visible, .ovdk-acts button:focus-visible, .ovdk-foot :focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 1px; }
.ovdk-tabs button:focus-visible, .ovdk-acts button:focus-visible, .ovdk-foot :focus-visible { outline: 2px solid var(--accent-light, var(--accent, #3b82f6)); outline-offset: 1px; }
.ovdk-tabs .n { min-width: 18px; padding: 1px 6px; box-sizing: border-box; border-radius: 999px; font-size: 11px; font-weight: 800; line-height: 1.35; text-align: center; font-variant-numeric: tabular-nums;
.ovdk-tabs .n { min-width: 18px; padding: 1px 6px; box-sizing: border-box; border-radius: 999px; font-size: 11px; font-weight: 800; line-height: 1.35; text-align: center; font-variant-numeric: tabular-nums;
@@ -166,9 +160,44 @@
@@ -166,9 +160,44 @@
.ovdk-acts { display: flex; gap: 2px; margin-left: auto; }
.ovdk-acts { display: flex; gap: 2px; margin-left: auto; }
.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }
.ovdk-acts button { width: 40px; height: 40px; border: 0; border-radius: 12px; background: transparent; color: var(--text-muted, #8b8b96); cursor: pointer; display: grid; place-items: center; }
.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }
.ovdk-acts button:hover { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); color: var(--text-primary, #ececf1); }
- .ovdk-acts button svg { width: 19px; height: 19px; }
- .ovdk-acts [data-act="pin"][aria-pressed="true"] { color: var(--accent-light, var(--accent, #60a5fa)); }
- .ovdk-acts [data-act="pin"][aria-pressed="true"] svg { fill: color-mix(in srgb, currentColor 22%, transparent); }
+ .ovdk-acts button svg { width: 19px; height: 19px; display: block; }
+ .ovdk-acts .ovdk-tomenu span { display: none; }
+ .ovdk-orbx svg { width: 22px !important; height: 22px !important; overflow: visible; }
+ .ovdk-orbx { --ovo-a: var(--accent-light, var(--accent, #60a5fa)); }
+ .ovdk.is-in .ovdk-orbx .ovmb :is(.ovmb-bar, .ovmb-app) { animation: ovmb-form .5s cubic-bezier(.2,1.3,.3,1) both; }
+ .ovdk-orbx:hover .ovmb, .ovdk-orbx:focus-visible .ovmb { transform: rotate(90deg); }
+ .ovdk-orbx .ovmb { transition: transform .35s cubic-bezier(.2,1.3,.3,1); }
+ .ovdk-orbx:active .ovmb { transform: rotate(90deg) scale(.8); }
+ /* A phone's sheet opens out of the navbar's orb as the menu's does (4.186.0; see CHANGELOG): one move, one close button. */
+ .ovdk.from-orb.is-in { animation: ovdk-reveal .38s cubic-bezier(.2,.9,.25,1) both; }
+ .ovdk.from-orb.is-out { animation: ovdk-fold .28s cubic-bezier(.5,0,.75,.2) both; pointer-events: none; }
+ @keyframes ovdk-reveal { from { clip-path: circle(28px at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } }
+ @keyframes ovdk-fold { from { clip-path: circle(150% at var(--ovdk-ox) var(--ovdk-oy)); } to { clip-path: circle(20px at var(--ovdk-ox) var(--ovdk-oy)); opacity: 0; } }
+ .ovdk.from-orb .ovdk-head { padding: 4px 14px 6px 8px; }
+ .from-orb .ovdk-acts .ovdk-orbx { position: relative; width: 40px; height: 40px; margin: 2px; border-radius: 50%; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, var(--bg-primary, #0b0f19));
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 35%, transparent), 0 6px 18px -6px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); transition: background-color .3s, box-shadow .3s; }
+ .from-orb .ovdk-acts .ovdk-orbx.is-from { background: transparent; box-shadow: none; }
+ .from-orb.is-in .ovdk-orbx::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--accent-light, #60a5fa); pointer-events: none; animation: ovdk-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }
+ @keyframes ovdk-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }
+ .from-orb .ovdk-orbx { color: var(--text-primary, #ececf1); }
+ .from-orb .ovdk-orbx svg { transition: transform .42s cubic-bezier(.2, 1.1, .3, 1); }
+ .ovdk-orbx.is-set, .ovdk-orbx.is-set * { transition: none !important; }
+ .from-orb .is-from svg { transform: translate(var(--orb-dx, 0), var(--orb-dy, 0)) scale(var(--orb-s, 1)); }
+ .ovnav-orb.is-dock { transform: translate(var(--dock-dx, 0), var(--dock-dy, 0)) scale(var(--dock-s, 1)) !important; color: var(--text-primary, #e6edf7) !important; }
+ .ovdk-more { position: absolute; z-index: 6; right: 8px; top: calc(100% - 4px); width: min(320px, calc(100% - 16px)); display: grid; gap: 2px; padding: 6px; border-radius: 16px;
+ border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-elevated, var(--bg-secondary, #1d1f27)); box-shadow: 0 18px 40px -12px rgba(0,0,0,.55); cursor: default; }
+ .ovdk-more[hidden] { display: none; }
+ .ovdk-more button { display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 8px 10px; border: 0; border-radius: 12px; background: transparent; color: var(--text-primary, #ececf1); font: inherit; text-align: left; cursor: pointer; }
+ .ovdk-more button[hidden] { display: none; }
+ .ovdk-more button:hover, .ovdk-more button:focus-visible { background: color-mix(in srgb, var(--text-primary, #ececf1) 7%, transparent); outline: none; }
+ .ovdk-more button > svg { flex: none; width: 20px; height: 20px; color: var(--text-muted, #8b8b96); }
+ .ovdk-more span { flex: 1; min-width: 0; display: grid; gap: 2px; }
+ .ovdk-more b { font-size: 14px; font-weight: 650; }
+ .ovdk-more small { font-size: 12px; color: var(--text-muted, #8b8b96); line-height: 1.35; }
+ .ovdk-sw { flex: none; position: relative; width: 38px; height: 22px; border-radius: 11px; background: var(--bg-tertiary, rgba(255,255,255,.14)); transition: background .15s; }
+ .ovdk-sw::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }
+ .ovdk-more [aria-checked="true"] .ovdk-sw { background: var(--accent-strong, var(--accent, #2563eb)); }
+ .ovdk-more [aria-checked="true"] .ovdk-sw::after { transform: translateX(16px); }
.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovdk-body { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; border-top: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ovdk-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.ovdk-panel[hidden] { display: none; }
.ovdk-panel[hidden] { display: none; }
@@ -186,7 +215,7 @@
@@ -186,7 +215,7 @@
.ovdk-none { padding: 40px 24px; text-align: center; color: var(--text-muted, #8b8b96); font-size: 13.5px; line-height: 1.45; }
.ovdk-none { padding: 40px 24px; text-align: center; color: var(--text-muted, #8b8b96); font-size: 13.5px; line-height: 1.45; }
.ovdk-size { position: absolute; z-index: 2; width: 22px; height: 22px; top: 0; left: 0; cursor: nwse-resize; touch-action: none; }
.ovdk-size { position: absolute; z-index: 2; width: 22px; height: 22px; top: 0; left: 0; cursor: nwse-resize; touch-action: none; }
.ovdk-size::after { content: ''; position: absolute; top: 9px; left: 9px; width: 7px; height: 7px; border-top: 2px solid var(--text-muted, #8b8b96); border-left: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 3px; opacity: 0; transition: opacity .15s; }
.ovdk-size::after { content: ''; position: absolute; top: 9px; left: 9px; width: 7px; height: 7px; border-top: 2px solid var(--text-muted, #8b8b96); border-left: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 3px; opacity: 0; transition: opacity .15s; }
- .ovdk:hover .ovdk-size::after, .ovdk-size:active::after { opacity: .75; }
+ .ovdk .ovdk-size::after { opacity: .75; }
.ovdk[data-corner="bottom-left"] .ovdk-size { left: auto; right: 0; cursor: nesw-resize; }
.ovdk[data-corner="bottom-left"] .ovdk-size { left: auto; right: 0; cursor: nesw-resize; }
.ovdk[data-corner="bottom-left"] .ovdk-size::after { left: auto; right: 9px; border-left: 0; border-right: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 0; border-top-right-radius: 3px; }
.ovdk[data-corner="bottom-left"] .ovdk-size::after { left: auto; right: 9px; border-left: 0; border-right: 2px solid var(--text-muted, #8b8b96); border-top-left-radius: 0; border-top-right-radius: 3px; }
.ovdk[data-corner="top-right"] .ovdk-size { top: auto; bottom: 0; cursor: nesw-resize; }
.ovdk[data-corner="top-right"] .ovdk-size { top: auto; bottom: 0; cursor: nesw-resize; }
@@ -195,10 +224,21 @@
@@ -195,10 +224,21 @@
.ovdk[data-corner="top-left"] .ovdk-size::after { top: auto; bottom: 9px; left: auto; right: 9px; border: 0; border-bottom: 2px solid var(--text-muted, #8b8b96); border-right: 2px solid var(--text-muted, #8b8b96); border-radius: 0 0 3px 0; }
.ovdk[data-corner="top-left"] .ovdk-size::after { top: auto; bottom: 9px; left: auto; right: 9px; border: 0; border-bottom: 2px solid var(--text-muted, #8b8b96); border-right: 2px solid var(--text-muted, #8b8b96); border-radius: 0 0 3px 0; }
@media (pointer: coarse) { .ovdk-acts button { width: 44px; height: 44px; } .ovdk-tabs button { height: 40px; } }
@media (pointer: coarse) { .ovdk-acts button { width: 44px; height: 44px; } .ovdk-tabs button { height: 40px; } }
.ovdk { container-type: inline-size; }
.ovdk { container-type: inline-size; }
- @container (max-width: 440px) { .ovdk-tabs button { padding: 0 8px; gap: 6px; font-size: 13px; } .ovdk-tabs button svg { display: none; } .ovdk-acts { gap: 0; } .ovdk-acts button { width: 34px; } }
+ @container (max-width: 440px) { .ovdk-tabs button { padding: 0 8px; gap: 6px; font-size: 13px; } .ovdk-acts { gap: 0; } .ovdk-acts button { width: 34px; } }
+ /* A narrow Dock (Opus, 11-07: "Activity" was cut off at the strip's edge): the open tab keeps its icon and name,
+ the others show their icon and count only, their names kept for screen readers. All four always fit. */
+ @container (max-width: 480px) {
+ .ovdk-tabs button:not([aria-selected="true"]) { padding: 0 10px; gap: 4px; }
+ .ovdk-tabs button:not([aria-selected="true"]) span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
+ }
.ovdk-muted { color: var(--text-muted, #8b8b96); }
.ovdk-muted { color: var(--text-muted, #8b8b96); }
.ovdk-i { display: inline-grid; place-items: center; }
.ovdk-i { display: inline-grid; place-items: center; }
.ovdk-i svg { width: 18px; height: 18px; }
.ovdk-i svg { width: 18px; height: 18px; }
+ .ovdk-bar { flex: none; display: flex; justify-content: flex-end; gap: 4px; padding: 4px 8px 0; }
+ .ovdk-ib { flex: none; width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 12px; background: transparent; color: var(--text-secondary, #b6b6c0); cursor: pointer; text-decoration: none; }
+ .ovdk-ib svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; }
+ .ovdk-ib:hover, .ovdk-ib:focus-visible { background: var(--hover-bg, rgba(127,127,127,.12)); color: var(--text-primary, #ececf1); }
+ .ovdk-thread-h .ovdk-ib { margin-left: auto; }
.ovdk-chats { list-style: none; margin: 0; padding: 6px; display: grid; gap: 2px; }
.ovdk-chats { list-style: none; margin: 0; padding: 6px; display: grid; gap: 2px; }
.ovdk-chat { display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 12px; width: 100%; min-height: 60px; padding: 8px 10px; border: 0; border-radius: 14px;
.ovdk-chat { display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; gap: 12px; width: 100%; min-height: 60px; padding: 8px 10px; border: 0; border-radius: 14px;
background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
@@ -207,6 +247,11 @@
@@ -207,6 +247,11 @@
.ovdk-av { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; overflow: hidden; font-weight: 800; font-size: 15px;
.ovdk-av { display: grid; place-items: center; width: 40px; height: 40px; border-radius: 50%; overflow: hidden; font-weight: 800; font-size: 15px;
color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
color: var(--accent-light, var(--accent, #60a5fa)); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
.ovdk-av img { width: 100%; height: 100%; object-fit: cover; }
.ovdk-av img { width: 100%; height: 100%; object-fit: cover; }
+ .ovdk-av-vibe { color: #fff; background: radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--accent, #3b82f6) 70%, #a78bfa), var(--accent-strong, var(--accent, #2563eb))); }
+ .ovdk-av-vibe svg { width: 22px; height: 22px; }
+ .ovdk-av-group { display: flex; align-items: center; overflow: visible; background: transparent; }
+ .ovdk-av-group .ovdk-av { width: 27px; height: 27px; margin-left: -12px; border: 2px solid var(--bg-elevated, var(--bg-secondary, #1d1f27)); font-size: 11px; }
+ .ovdk-av-group .ovdk-av:first-child { margin-left: 0; }
.ovdk-chat-b { display: grid; gap: 1px; min-width: 0; }
.ovdk-chat-b { display: grid; gap: 1px; min-width: 0; }
.ovdk-chat-b b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovdk-chat-b b { font-size: 14px; font-weight: 650; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovdk-chat-p { font-size: 12.5px; color: var(--text-secondary, #b6b6c0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovdk-chat-p { font-size: 12.5px; color: var(--text-secondary, #b6b6c0); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -230,24 +275,53 @@
@@ -230,24 +275,53 @@
border: 1px solid var(--border-light, var(--border, rgba(255,255,255,.14))); background: var(--bg-primary, #12141b); }
border: 1px solid var(--border-light, var(--border, rgba(255,255,255,.14))); background: var(--bg-primary, #12141b); }
.ovdk-send:focus-within { border-color: var(--accent, #3b82f6); }
.ovdk-send:focus-within { border-color: var(--accent, #3b82f6); }
.ovdk-send textarea { flex: 1; min-width: 0; box-sizing: border-box; min-height: 36px; max-height: 140px; padding: 8px 0; resize: none; border: 0; outline: none; background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 14px; line-height: 1.4; }
.ovdk-send textarea { flex: 1; min-width: 0; box-sizing: border-box; min-height: 36px; max-height: 140px; padding: 8px 0; resize: none; border: 0; outline: none; background: transparent; color: var(--text-primary, #ececf1); font: inherit; font-size: 14px; line-height: 1.4; }
- .ovdk-go { width: 40px; height: 40px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center;
+ .ovdk-go { width: 44px; height: 44px; flex: none; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center;
color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }
color: var(--on-accent-strong, var(--on-accent, #fff)); background: var(--accent-strong, var(--accent, #3472d8)); }
.ovdk-go:disabled { opacity: .55; cursor: default; }
.ovdk-go:disabled { opacity: .55; cursor: default; }
.ovdk-err { margin: 0 14px; font-size: 12.5px; color: color-mix(in srgb, var(--live-red, #e5484d) 60%, var(--text-primary, #ececf1)); }
.ovdk-err { margin: 0 14px; font-size: 12.5px; color: color-mix(in srgb, var(--live-red, #e5484d) 60%, var(--text-primary, #ececf1)); }
.ovdk-err[hidden] { display: none; }
.ovdk-err[hidden] { display: none; }
.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }
.ovdk-none b { display: block; margin-bottom: 4px; font-size: 15px; color: var(--text-primary, #ececf1); }
@media (max-width: 640px) {
@media (max-width: 640px) {
- .ovdk, .ovdk[data-corner] { left: 0; right: 0; top: auto; bottom: 0; width: auto !important; height: min(86vh, 720px) !important; height: min(86dvh, 720px) !important; max-width: none; max-height: none;
- border-radius: 24px 24px 0 0; border-bottom: 0; padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: 0 0 0 100vmax rgba(0,0,0,.5), 0 -12px 40px rgba(0,0,0,.35); transform-origin: 50% 100%; }
+ .ovdk, .ovdk[data-corner] { z-index: 10010; left: 0; right: 0; top: 0; bottom: var(--ov-kb, 0px); width: auto !important; height: auto !important; max-width: none; max-height: none;
+ border: 0; border-radius: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px); box-shadow: none; transform-origin: 50% 100%; }
.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }
.ovdk.is-in { animation-name: ovdk-rise; animation-duration: .3s; }
- .ovdk-head { padding-top: 22px; cursor: default; }
- .ovdk-head::before { content: ''; position: absolute; top: 8px; left: 50%; width: 40px; height: 4px; margin-left: -20px; border-radius: 2px; background: var(--border-light, var(--border, rgba(255,255,255,.22))); }
+ .ovdk-head { cursor: default; }
.ovdk-size { display: none; }
.ovdk-size { display: none; }
.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }
.ovdk-tabs { flex: 1; } .ovdk-tabs button { flex: 1; justify-content: center; }
+ /* A thumb, not a cursor: every control on the sheet is 44 px (design 2026-11-07 §1, Phone). */
+ .ovdk-tabs button { height: 44px; }
+ .ovdk-acts button { width: 44px; height: 44px; }
+ /* Two rows (4.150.0, the owner: the strip was "cut off", "Activity" read "Ac"): the tools sit beside the grab
+ handle; the four tabs share the full width below, each icon over its name, every one whole. */
+ .ovdk-head { flex-wrap: nowrap; gap: 4px; padding: 6px 6px 6px 8px; }
+ .ovdk-acts { flex: none; gap: 0; }
+ .ovdk-acts button[data-act="pop"] { display: none !important; }
+ .ovdk-tabs { order: 0; flex: 1 1 auto; min-width: 0; padding: 3px; border-radius: 16px; }
+ .ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { flex: 1 1 0; min-width: 0; height: 54px; padding: 4px 2px; flex-direction: column; justify-content: center; gap: 3px; border-radius: 12px; font-size: 11.5px; font-weight: 650; }
+ .ovdk-tabs button span, .ovdk-tabs button:not([aria-selected="true"]) span { position: static; width: auto; height: auto; overflow: hidden; clip: auto; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; }
+ .ovdk-tabs button svg { width: 19px; height: 19px; }
+ .ovdk-tabs .n { position: absolute; top: 4px; left: calc(50% + 6px); min-width: 17px; padding: 1px 5px; font-size: 10.5px; }
+ .ovdk-foot a, .ovdk-foot button { min-height: 44px; }
+ /* Two rows, as the menu's head (4.189.0): the way back to the menu, More and Close; under them the four tabs,
+ full width, in the look of the menu's row that leads here. */
+ .ovdk-head, .ovdk.from-orb .ovdk-head { flex-wrap: wrap; gap: 6px 4px; padding: 6px 12px 8px; }
+ .ovdk-acts { order: 0; flex: 1 0 100%; align-items: center; gap: 2px; }
+ .ovdk-acts .ovdk-tomenu { width: auto; margin-right: auto; padding: 0 14px 0 10px; gap: 8px; display: inline-flex; align-items: center; border-radius: 999px; font: inherit; font-size: 13.5px; font-weight: 700;
+ color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--text-primary, #e6edf7) 6%, transparent); }
+ .ovdk-acts .ovdk-tomenu span { display: inline; }
+ .ovdk-tabs { order: 1; flex: 1 0 100%; gap: 4px; padding: 0; border-radius: 0; background: transparent; overflow: visible; }
+ .ovdk-tabs button, .ovdk-tabs button:not([aria-selected="true"]) { height: 50px; gap: 2px; background: color-mix(in srgb, var(--accent, #3b82f6) 7%, transparent); }
+ .ovdk-tabs button[aria-selected="true"] { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); }
+ .ovdk-tabs button:not([aria-selected="true"]) svg { color: var(--accent-light, var(--accent, #60a5fa)); }
+ }
+ @media (min-width: 641px) and (max-width: 1023px) {
+ .ovdk, .ovdk[data-corner] { z-index: 10010; top: 0; bottom: 0; right: 0; left: auto; width: min(460px, calc(100vw - 48px)) !important; height: auto !important;
+ max-width: none; max-height: none; border-radius: 22px 0 0 22px; border-right: 0; padding-top: env(safe-area-inset-top, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);
+ box-shadow: 0 0 0 100vmax rgba(0,0,0,.4), -16px 0 40px rgba(0,0,0,.3); transform-origin: 100% 50%; }
+ .ovdk-size { display: none; }
+ .ovdk-head { cursor: default; }
}
}
html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }
html.ovdk-sheet-open, html.ovdk-sheet-open body { overflow: hidden; }
- @media (prefers-reduced-motion: reduce) { .ovdk.is-in, .ovdk-wait span { animation: none; } }
- html[data-ov-motion="reduced"] .ovdk.is-in { animation: none; }
// In a window of its own the panel is the window: full bleed, no card, no phone sheet.
// In a window of its own the panel is the window: full bleed, no card, no phone sheet.
.ovdk-pip .ovdk, .ovdk-pip .ovdk[data-corner] { left: 0 !important; top: 0 !important; right: 0 !important; bottom: 0 !important;
.ovdk-pip .ovdk, .ovdk-pip .ovdk[data-corner] { left: 0 !important; top: 0 !important; right: 0 !important; bottom: 0 !important;
width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important;
width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important;
@@ -270,15 +344,20 @@
@@ -270,15 +344,20 @@
<header class="ovdk-head">
<header class="ovdk-head">
<div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div>
<div class="ovdk-tabs" role="tablist" aria-label="Dock">${TABS.map(([id, label, g]) => `<button type="button" role="tab" id="ovdk-tab-${id}" aria-controls="ovdk-panel-${id}" data-tab="${id}" aria-selected="false" tabindex="-1">${svg(g)}<span>${label}</span><b class="n" data-n="${id}"></b></button>`).join('')}</div>
<div class="ovdk-acts">
<div class="ovdk-acts">
- <button type="button" data-act="timer" title="A timer" aria-label="Start a timer">${svg(G.clock)}</button>
- <button type="button" data-act="pop" hidden title="Open the Dock in a window of its own" aria-label="Open the Dock in a window of its own">${svg(G.open)}</button>
- <button type="button" data-act="pin" aria-pressed="false" title="Keep the Dock's button on every page" aria-label="Keep the Dock's button on every page">${svg(G.pin)}</button>
- <button type="button" data-act="close" title="Close" aria-label="Close the Dock">${svg(G.close)}</button>
+ <button type="button" data-act="menu" class="ovdk-tomenu" title="The OpenVibe menu" aria-label="The OpenVibe menu: every site, the feed, you">${root.OpenVibeOrb ? root.OpenVibeOrb.svg(20) : ''}<span>Menu</span></button>
+ <button type="button" data-act="more" aria-haspopup="true" aria-expanded="false" title="More" aria-label="More: a timer, the Dock's button">${svg(G.more)}</button>
+ <button type="button" data-act="close" class="ovdk-orbx" title="Close" aria-label="Close the Dock">${ORBX()}</button>
+ </div>
+ <div class="ovdk-more" role="menu" aria-label="More" hidden>
+ <button type="button" role="menuitem" data-act="timer">${svg(G.clock)}<span><b>Start a timer</b><small>A countdown or a focus round, on the Dock's button</small></span></button>
+ <button type="button" role="menuitemcheckbox" aria-checked="false" data-act="pin">${svg(G.pin)}<span><b>Keep the Dock's button on every page</b><small>When off, it shows only while something works or needs you</small></span><i class="ovdk-sw" aria-hidden="true"></i></button>
+ <button type="button" role="menuitem" data-act="pop" hidden>${svg(G.open)}<span><b>Open the Dock in a window of its own</b><small>Keep it beside whatever you are doing</small></span></button>
</div>
</div>
</header>
</header>
<div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div>
<div class="ovdk-body">${TABS.map(([id]) => `<div class="ovdk-panel" role="tabpanel" id="ovdk-panel-${id}" aria-labelledby="ovdk-tab-${id}" data-panel="${id}" hidden></div>`).join('')}</div>
<span class="ovdk-size" aria-hidden="true"></span>`;
<span class="ovdk-size" aria-hidden="true"></span>`;
document.body.appendChild(el);
document.body.appendChild(el);
+ bodyEl = el.querySelector('.ovdk-body');
const tabs = [...el.querySelectorAll('[role="tab"]')];
const tabs = [...el.querySelectorAll('[role="tab"]')];
tabs.forEach((b) => b.addEventListener('click', () => setTab(b.dataset.tab, true)));
tabs.forEach((b) => b.addEventListener('click', () => setTab(b.dataset.tab, true)));
@@ -288,14 +367,39 @@
@@ -288,14 +367,39 @@
if (j >= 0) { e.preventDefault(); setTab(tabs[j].dataset.tab, true); tabs[j].focus(); }
if (j >= 0) { e.preventDefault(); setTab(tabs[j].dataset.tab, true); tabs[j].focus(); }
});
});
el.querySelector('[data-act="close"]').addEventListener('click', () => close());
el.querySelector('[data-act="close"]').addEventListener('click', () => close());
+ // The way back to the menu: this sheet folds, the menu opens out of the same button.
+ { const m = el.querySelector('[data-act="menu"]'); if (m) { if (!root.OpenVibeNavbar || !root.OpenVibeOrb) m.hidden = true; m.addEventListener('click', () => { close(); setTimeout(() => { try { root.OpenVibeNavbar.openSwitcher(); } catch { /* no bar */ } }, 300); }); } }
el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });
el.querySelector('[data-act="pop"]').addEventListener('click', () => { if (popped) backToPage(); else popOut(); });
el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });
el.querySelector('[data-act="pin"]').addEventListener('click', () => { save({ pinned: !state().pinned }); paintPin(); });
- el.querySelector('[data-act="timer"]').addEventListener('click', (e) => { const a = e.currentTarget; withTimer((t) => t.menu(a)); });
+ el.querySelector('[data-act="timer"]').addEventListener('click', () => { const a = el.querySelector('[data-act="more"]'); closeMore(); withTimer((t) => t.menu(a)); });
+ const moreBtn = el.querySelector('[data-act="more"]'), moreMenu = el.querySelector('.ovdk-more');
+ function closeMore() { moreMenu.hidden = true; moreBtn.setAttribute('aria-expanded', 'false'); }
+ moreBtn.addEventListener('click', () => {
+ const open = moreMenu.hidden;
+ closeMore();
+ const tm = el.querySelector('.ovdk-tmenu'); if (tm) tm.remove();
+ if (open) { moreMenu.hidden = false; moreBtn.setAttribute('aria-expanded', 'true'); const first = moreMenu.querySelector('button:not([hidden])'); if (first) first.focus(); }
+ });
+ moreMenu.addEventListener('click', (e) => { const b = e.target.closest('button'); if (b && b.dataset.act !== 'pin') closeMore(); });
+ moreMenu.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); closeMore(); moreBtn.focus(); } });
+ // A pop-over (More, the timer) closes on a tap anywhere else, in the Dock or on the page (owner, 11-09: "clicked off
+ // something else, and it didn't actually close").
+ const awayFrom = (e) => {
+ const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
+ if (!moreMenu.hidden && !path.includes(moreMenu) && !path.includes(moreBtn)) closeMore();
+ const tm = el.querySelector('.ovdk-tmenu');
+ if (tm && !path.includes(tm) && !path.includes(moreMenu) && !path.includes(moreBtn)) tm.remove();
+ };
+ el.addEventListener('pointerdown', awayFrom, true);
+ document.addEventListener('pointerdown', (e) => { if (!el.contains(e.target)) awayFrom(e); }, true);
el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });
el.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); close(); } });
// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a
// A tap outside closes the phone sheet. The path is the one at dispatch: a tap that redraws its own panel (opening a
// chat) leaves its target detached, and must not read as outside.
// chat) leaves its target detached, and must not read as outside.
document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => {
if (!isOpen() || !phone()) return;
if (!isOpen() || !phone()) return;
+ // The tap that opened the sheet (a page's button, the share sheet's OpenVibe Chat, Ask your agent) is still on
+ // its way up when the sheet opens: an event made before the sheet opened is never "a tap outside" (4.145.1).
+ if (e.timeStamp && e.timeStamp <= openedAt) return;
const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
const path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
if (path.includes(el) || (pill && path.includes(pill)) || (e.target && e.target.isConnected === false)) return;
if (path.includes(el) || (pill && path.includes(pill)) || (e.target && e.target.isConnected === false)) return;
close();
close();
@@ -366,7 +470,7 @@
@@ -366,7 +470,7 @@
setTimeout(() => { el.style.transition = ''; }, 300);
setTimeout(() => { el.style.transition = ''; }, 300);
};
};
head.addEventListener('pointerdown', (e) => {
head.addEventListener('pointerdown', (e) => {
- if (e.button || e.target.closest('.ovdk-acts')) return;
+ if (e.button || e.target.closest('.ovdk-acts, .ovdk-more')) return;
s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };
s = { x: e.clientX, y: e.clientY, r: el.getBoundingClientRect(), dx: 0, dy: 0, id: e.pointerId, live: false };
root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
root.addEventListener('pointermove', move); root.addEventListener('pointerup', end); root.addEventListener('pointercancel', end);
});
});
@@ -410,62 +514,92 @@
@@ -410,62 +514,92 @@
n('agent', agent, c.waiting ? 'waiting' : '');
n('agent', agent, c.waiting ? 'waiting' : '');
n('activity', c.unread || 0);
n('activity', c.unread || 0);
n('chats', chats.unread || 0);
n('chats', chats.unread || 0);
- el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chats, ${chats.unread} unread` : 'Chats');
+ el.querySelector('#ovdk-tab-chats').setAttribute('aria-label', chats.unread ? `Chat, ${chats.unread} unread` : 'Chat');
el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));
el.querySelector('#ovdk-tab-agent').setAttribute('aria-label', [`Agents`, c.waiting ? `${c.waiting} need${c.waiting === 1 ? 's' : ''} you` : '', c.running ? `${c.running} working` : ''].filter(Boolean).join(', '));
el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');
el.querySelector('#ovdk-tab-activity').setAttribute('aria-label', c.unread ? `Activity, ${c.unread} unread` : 'Activity');
}
}
function paintPin() {
function paintPin() {
const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;
const b = el.querySelector('[data-act="pin"]'), on = !!state().pinned;
- b.setAttribute('aria-pressed', String(on));
- const t = on ? "The Dock's button stays on every page" : "Keep the Dock's button on every page";
- b.title = t; b.setAttribute('aria-label', t);
+ b.setAttribute('aria-checked', String(on));
+ b.title = on ? "On: the Dock's button stays on every page" : "Off: the Dock's button shows only while something works or needs you";
}
}
function setTab(id, remember) {
function setTab(id, remember) {
+ if (id === 'agents') id = 'agent';
+ if (inline && tab !== id) disposePanel();
tab = TABS.some(([t]) => t === id) ? id : 'activity';
tab = TABS.some(([t]) => t === id) ? id : 'activity';
if (tab !== 'chats') clearTimeout(chats.timer);
if (tab !== 'chats') clearTimeout(chats.timer);
if (tab !== 'vibe' && vibe.api) vibe.api.pause();
if (tab !== 'vibe' && vibe.api) vibe.api.pause();
el.querySelectorAll('[role="tab"]').forEach((b) => { const on = b.dataset.tab === tab; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
el.querySelectorAll('[role="tab"]').forEach((b) => { const on = b.dataset.tab === tab; b.setAttribute('aria-selected', String(on)); b.tabIndex = on ? 0 : -1; });
- el.querySelectorAll('[data-panel]').forEach((p) => { p.hidden = p.dataset.panel !== tab; });
+ bodyEl.querySelectorAll('[data-panel]').forEach((p) => { p.hidden = p.dataset.panel !== tab; });
if (remember && state().tab !== tab && SAVED_TABS.includes(tab)) save({ tab });
if (remember && state().tab !== tab && SAVED_TABS.includes(tab)) save({ tab });
fill(tab);
fill(tab);
+ if (inline && tab === 'chats') readUnread();
+ }
+ const panelActive = (id) => tab === id && (isOpen() || !!inline);
+ function disposePanel() {
+ panelEpoch++;
+ clearTimeout(chats.timer); clearTimeout(chats.unreadT);
+ if (vibe.api) { vibe.api.pause(); vibe.api = null; }
+ for (const k of Object.keys(mounts)) { if (mounts[k] && mounts[k].destroy) mounts[k].destroy(); delete mounts[k]; }
+ if (root.OpenVibeNotifications && root.OpenVibeNotifications.unmountDock && bodyEl) root.OpenVibeNotifications.unmountDock(bodyEl.querySelector('[data-panel="activity"]'));
+ chats.panel = null; chats.open = null; vibe.panel = null;
+ if (bodyEl) bodyEl.querySelectorAll('[data-panel]').forEach((p) => { p.replaceChildren(); p.hidden = true; });
}
}
async function fill(id) {
async function fill(id) {
- const panel = el.querySelector(`[data-panel="${id}"]`);
+ const epoch = panelEpoch;
+ const panel = bodyEl.querySelector(`[data-panel="${id}"]`);
if (id === 'vibe') {
if (id === 'vibe') {
vibe.panel = panel;
vibe.panel = panel;
if (vibe.api) return vibe.api.refresh();
if (vibe.api) return vibe.api.refresh();
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));
const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));
+ if (epoch !== panelEpoch || !panelActive(id)) return;
if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }
if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }
- vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });
+ vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => panelActive('vibe'), state, save, wantList: () => { const v = vibeOnList; vibeOnList = false; return v; }, styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });
return;
return;
}
}
- if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }
- if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); return; }
+ if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; if (chats.draft) return chatList(); return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }
+ // The Agent panel reads the step.
+ if (mounts[id]) { if (mounts[id].refresh) mounts[id].refresh(); if (mounts[id + 'Timeline']) mounts[id + 'Timeline'].refresh(); return; }
mounts[id] = { refresh: null };
mounts[id] = { refresh: null };
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
+ if (id === 'code') {
+ const ok = await load('dock-code.js', () => !!(root.OpenVibeDockCode && root.OpenVibeDockCode.mount));
+ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }
+ if (!ok) return failed(panel, id);
+ mounts[id] = root.OpenVibeDockCode.mount(panel, { token: o.token, frameBase: frameBase(), active: () => panelActive('code') });
+ return;
+ }
if (id === 'agent') {
if (id === 'agent') {
- const ok = await load('action-center.js', () => !!(root.OpenVibeActionCenter && root.OpenVibeActionCenter.mount));
+ const ok = await load('dock-runs.js', () => !!(root.OpenVibeDockRuns && root.OpenVibeDockRuns.mount));
+ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }
if (!ok) return failed(panel, id);
if (!ok) return failed(panel, id);
// K-D3: the person's own agent stream above the list; it hides itself when there is none.
// K-D3: the person's own agent stream above the list; it hides itself when there is none.
- panel.innerHTML = `<div class="ovdk-agent"></div><div class="ovdk-scroll"></div><div class="ovdk-foot"><button type="button" data-act="all">${svg(G.open)}Scheduled, done and failed</button><a href="https://openvibe.actor/">Give an agent a task →</a></div>`;
+ panel.innerHTML = '<div class="ovdk-actor"></div><div class="ovdk-timeline"></div><div class="ovdk-agent"></div><div class="ovdk-scroll"></div><div class="ovdk-runs-foot ovdk-foot"></div>';
+ // The Agent panel: Now, Needs you and the one timeline (design 2026-11-07). It is first in the tab, so on a
+ // phone Now and Needs you are on screen before anything else.
+ load('dock-timeline.js', () => !!(root.OpenVibeDockTimeline && root.OpenVibeDockTimeline.mount)).then((on) => {
+ if (on && epoch === panelEpoch && !mounts[id + 'Timeline'] && panel.isConnected && panelActive(id)) mounts[id + 'Timeline'] = root.OpenVibeDockTimeline.mount(panel.querySelector('.ovdk-timeline'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), active: () => panelActive('agent'), focus: () => o.task || null });
+ });
+ // Actor v3 (A3): your Actor first, on every site; it shows nothing for a person without one.
+ load('dock-actor.js', () => !!(root.OpenVibeDockActor && root.OpenVibeDockActor.mount)).then((on) => {
+ if (on && epoch === panelEpoch && !mounts[id + 'Actor'] && panel.isConnected && panelActive(id)) mounts[id + 'Actor'] = root.OpenVibeDockActor.mount(panel.querySelector('.ovdk-actor'), { token: o.token, base: directBase(o.actorBase), frameBase: frameBase(), networkBase: o.apiBase || 'https://openvibe.network', active: () => panelActive('agent') });
+ });
load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {
load('dock-agent.js', () => !!(root.OpenVibeDockAgent && root.OpenVibeDockAgent.mount)).then((on) => {
- if (on && !mounts[id + 'View'] && panel.isConnected) mounts[id + 'View'] = root.OpenVibeDockAgent.mount(panel.firstChild, { token: o.token, base: agentBase(), active: () => isOpen() && tab === 'agent' });
+ if (on && epoch === panelEpoch && !mounts[id + 'View'] && panel.isConnected && panelActive(id)) mounts[id + 'View'] = root.OpenVibeDockAgent.mount(panel.querySelector('.ovdk-agent'), { token: o.token, base: directBase(o.agentBase), frameBase: frameBase(), active: () => panelActive('agent') });
});
});
- const m = root.OpenVibeActionCenter.mount(panel.querySelector('.ovdk-scroll'), {
- token: o.token, apiBase: o.apiBase, scopes: o.scopes, buckets: ['waiting', 'running'],
+ const m = root.OpenVibeDockRuns.mount(panel.querySelector('.ovdk-scroll'), {
+ call, state, save, scopes: o.scopes, active: () => panelActive('agent'), actorBase: actorBase(), loadMarks: () => load('ai-marks-client.js', () => !!root.OpenVibeAIMarks),
+ styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'),
+ foot: panel.querySelector('.ovdk-runs-foot'),
onCounts: (c) => { doc = { ...(doc || {}), state: state(), counts: { ...counts(), running: c.running || 0, waiting: c.waiting || 0 } }; badges(); notify(); },
onCounts: (c) => { doc = { ...(doc || {}), state: state(), counts: { ...counts(), running: c.running || 0, waiting: c.waiting || 0 } }; badges(); notify(); },
});
});
mounts[id] = m;
mounts[id] = m;
- panel.querySelector('[data-act="all"]').addEventListener('click', (e) => {
- const anchor = e.currentTarget;
- close();
- try { root.OpenVibeActionCenter.open({ token: o.token, apiBase: o.apiBase, scopes: o.scopes, anchor: (pill && !pill.hidden) ? pill : anchor }); } catch { /* */ }
- });
return;
return;
}
}
const ok = await load('notification-ui.js', () => !!(root.OpenVibeNotifications && root.OpenVibeNotifications.renderDock));
const ok = await load('notification-ui.js', () => !!(root.OpenVibeNotifications && root.OpenVibeNotifications.renderDock));
+ if (epoch !== panelEpoch || !panelActive(id)) { delete mounts[id]; return; }
if (!ok) return failed(panel, id);
if (!ok) return failed(panel, id);
const N = root.OpenVibeNotifications;
const N = root.OpenVibeNotifications;
if (!N.__ovNavInit) { try { N.init({ token: o.token, apiBase: o.apiBase || 'https://openvibe.network', scopes: o.scopes }); N.__ovNavInit = true; } catch { /* */ } }
if (!N.__ovNavInit) { try { N.init({ token: o.token, apiBase: o.apiBase || 'https://openvibe.network', scopes: o.scopes }); N.__ovNavInit = true; } catch { /* */ } }
@@ -482,33 +616,36 @@
@@ -482,33 +616,36 @@
// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are
// The person's conversations and DMs, from OpenVibe.Chat's own API with their Network token (Chat checks they are
// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls
// in each one). Everything people wrote is set as text, never markup; only https pictures are shown. A thread polls
// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.
// for new messages every 5 s while it is open in the Dock, and is marked read as it is shown.
- const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, unread: 0, unreadT: 0, busy: false };
+ const chats = { panel: null, list: null, open: null, msgs: [], last: 0, timer: 0, edits: null, unread: 0, unreadT: 0, busy: false, draft: null, vibeSummary: null, filter: '' };
const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');
const chatBase = () => String(o.chatBase || 'https://openvibe.chat').replace(/\/+$/, '');
const me = () => String(o.me || '');
const me = () => String(o.me || '');
const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;
const scopes = () => Array.isArray(o.scopes) ? o.scopes : null;
const canWriteChat = () => !scopes() || scopes().includes('chat.write');
const canWriteChat = () => !scopes() || scopes().includes('chat.write');
const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const h = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };
const icon = (path) => { const s = h('span', 'ovdk-i'); s.innerHTML = svg(path); return s; };
+ const toChat = (href) => { const a = h('a', 'ovdk-ib ovdk-away'); a.href = href; a.target = '_blank'; a.rel = 'noopener'; a.title = 'Open in Chat'; a.setAttribute('aria-label', 'Open in Chat'); a.setAttribute('data-ov-tip', 'Open in Chat'); a.innerHTML = svg('<path d="M13.5 5.5h5v5M18.5 5.5l-7.5 7.5M17 13.5v4a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 5 17.5v-9A1.5 1.5 0 0 1 6.5 7h4"/>'); return a; };
async function chatCall(path, init = {}) {
async function chatCall(path, init = {}) {
const t = await tokenOf();
const t = await tokenOf();
const headers = { accept: 'application/json', ...(init.body ? { 'content-type': init.type || 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };
const headers = { accept: 'application/json', ...(init.body ? { 'content-type': init.type || 'application/json' } : {}), ...(t ? { authorization: `Bearer ${t}` } : {}) };
const { type: _type, ...rest } = init;
const { type: _type, ...rest } = init;
return fetch(chatBase() + path, { ...rest, headers, credentials: 'omit' }).then(async (r) => {
return fetch(chatBase() + path, { ...rest, headers, credentials: 'omit' }).then(async (r) => {
const b = await r.json().catch(() => ({}));
const b = await r.json().catch(() => ({}));
- if (!r.ok) { const e = new Error(b.error || `HTTP ${r.status}`); e.status = r.status; throw e; }
+ if (!r.ok) { const e = new Error(b.error || `HTTP ${r.status}`); e.status = r.status; e.code = b.code || null; throw e; }
return b;
return b;
});
});
}
}
function readUnread() {
function readUnread() {
clearTimeout(chats.unreadT);
clearTimeout(chats.unreadT);
chatCall('/api/dm/unread').then((b) => { chats.unread = Number(b && b.unread) || 0; badges(); }).catch(() => { /* Chat away: no count */ });
chatCall('/api/dm/unread').then((b) => { chats.unread = Number(b && b.unread) || 0; badges(); }).catch(() => { /* Chat away: no count */ });
- if (isOpen()) chats.unreadT = setTimeout(readUnread, 30000);
+ if (panelActive('chats')) chats.unreadT = setTimeout(readUnread, 30000);
}
}
const when = (iso) => {
const when = (iso) => {
const t = Date.parse(iso || ''); if (!Number.isFinite(t)) return '';
const t = Date.parse(iso || ''); if (!Number.isFinite(t)) return '';
const m = Math.max(0, Math.round((Date.now() - t) / 60000));
const m = Math.max(0, Math.round((Date.now() - t) / 60000));
return m < 1 ? 'now' : m < 60 ? `${m} min` : m < 1440 ? `${Math.floor(m / 60)} h` : `${Math.floor(m / 1440)} d`;
return m < 1 ? 'now' : m < 60 ? `${m} min` : m < 1440 ? `${Math.floor(m / 60)} h` : `${Math.floor(m / 1440)} d`;
};
};
+ // "answered 4 min ago", and "answered just now" when it was this minute.
+ const answered = (iso) => { const w = when(iso); return !w ? '' : w === 'now' ? 'answered just now' : `answered ${w} ago`; };
function avatar(p, group) {
function avatar(p, group) {
const a = h('span', 'ovdk-av');
const a = h('span', 'ovdk-av');
const url = p && /^https:\/\//.test(String(p.avatar_url || '')) ? p.avatar_url : '';
const url = p && /^https:\/\//.test(String(p.avatar_url || '')) ? p.avatar_url : '';
@@ -516,30 +653,54 @@
@@ -516,30 +653,54 @@
else a.textContent = group ? '#' : String((p && (p.display_name || p.username)) || '?').trim().charAt(0).toUpperCase() || '?';
else a.textContent = group ? '#' : String((p && (p.display_name || p.username)) || '?').trim().charAt(0).toUpperCase() || '?';
return a;
return a;
}
}
+ function groupAvatar(c) {
+ const people = others(c).slice(0, 3);
+ if (!people.length) return avatar({}, false);
+ const stack = h('span', 'ovdk-av ovdk-av-group');
+ people.forEach((p) => stack.append(avatar(p, false)));
+ return stack;
+ }
const others = (c) => (c.participants || []).filter((p) => p.username !== me());
const others = (c) => (c.participants || []).filter((p) => p.username !== me());
const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';
const titleOf = (c) => c.name || others(c).map((p) => p.display_name || p.username).join(', ') || 'Just you';
- function chatFoot(panel) {
- const foot = h('div', 'ovdk-foot');
- const a = h('a', '', 'Open Chat →'); a.href = `${chatBase()}/messages`; a.target = '_blank'; a.rel = 'noopener';
- foot.append(h('span', 'ovdk-muted', 'Your conversations and DMs'), a);
- panel.append(foot);
- }
async function chatList() {
async function chatList() {
clearTimeout(chats.timer);
clearTimeout(chats.timer);
+ if (chats.edits) { chats.edits.stop(); chats.edits = null; }
chats.open = null;
chats.open = null;
const panel = chats.panel; if (!panel) return;
const panel = chats.panel; if (!panel) return;
if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }
if (!chats.list) { panel.innerHTML = '<div class="ovdk-wait"><span></span></div>'; }
- try { chats.list = (await chatCall('/api/dm/conversations')).conversations || []; } catch (e) { panel.textContent = ''; panel.append(h('div', 'ovdk-none', e.status === 401 ? 'Sign in to Chat to see your conversations.' : 'Your chats did not load. Check your connection, then open them again.')); chatFoot(panel); return; }
- if (chats.open || tab !== 'chats') return;
+ const sharing = chats.draft && canWriteChat() ? load('dock-share.js', () => !!(root.OpenVibeDockShare && root.OpenVibeDockShare.bar)) : null;
+ try {
+ const [dm, ai] = await Promise.all([chatCall('/api/dm/conversations'), chatCall('/api/vibe/conversations').catch(() => null)]);
+ chats.list = dm.conversations || [];
+ chats.vibeSummary = ai && ai.count ? { kind: 'vibe', count: ai.count, latest: ai.conversations && ai.conversations[0], last_message_at: ai.conversations && ai.conversations[0] && ai.conversations[0].last_message_at } : null;
+ } catch (e) { if (panel !== chats.panel || !panelActive('chats')) return; panel.textContent = ''; panel.append(h('div', 'ovdk-none', e.status === 401 ? 'Sign in to Chat to see your conversations.' : 'Your chats did not load. Check your connection, then open them again.')); chatFoot(panel); return; }
+ if (sharing) await sharing;
+ if (panel !== chats.panel || !panelActive('chats') || chats.open) return;
panel.textContent = '';
panel.textContent = '';
+ if (chats.draft && canWriteChat() && root.OpenVibeDockShare) panel.append(root.OpenVibeDockShare.bar({ draft: chats.draft, h, chatCall, host: el, cancel: () => { chats.draft = null; chatList(); }, open: (id) => { chats.list = null; return openChat(String(id), true); } }));
+ // Open in Chat, as an icon over your conversations (ask 336).
+ const bar = h('div', 'ovdk-bar'); bar.append(toChat('https://openvibe.chat/messages')); panel.append(bar);
const scroll = h('div', 'ovdk-scroll');
const scroll = h('div', 'ovdk-scroll');
- if (!chats.list.length) {
+ if (!chats.list.length && !chats.vibeSummary) {
const none = h('div', 'ovdk-none');
const none = h('div', 'ovdk-none');
none.append(h('b', '', 'No conversations yet'), document.createTextNode('A message to someone on OpenVibe starts one, and it shows here on every site.'));
none.append(h('b', '', 'No conversations yet'), document.createTextNode('A message to someone on OpenVibe starts one, and it shows here on every site.'));
scroll.append(none);
scroll.append(none);
} else {
} else {
const ul = h('ul', 'ovdk-chats');
const ul = h('ul', 'ovdk-chats');
- chats.list.forEach((c) => {
+ [...chats.list.filter((c) => !chats.filter || titleOf(c).toLowerCase().includes(chats.filter)), ...(chats.vibeSummary ? [chats.vibeSummary] : [])].sort((a, b) => String(b.last_message_at || b.updated_at || '').localeCompare(String(a.last_message_at || a.updated_at || ''))).forEach((c) => {
+ if (c.kind === 'vibe') {
+ const li = h('li'), b = h('button', 'ovdk-chat'); b.type = 'button';
+ const body = h('span', 'ovdk-chat-b'); body.append(h('b', '', 'Vibe'), h('span', 'ovdk-chat-p', `${c.count} chat${c.count === 1 ? '' : 's'}${c.latest && c.latest.name ? ` · ${c.latest.name}` : ''}`));
+ const meta = h('span', 'ovdk-chat-m'); const at = c.latest && c.latest.last_answer_at;
+ const t = h('time', '', when(at || c.last_message_at)); if (at) t.title = answered(at); meta.append(t);
+ const av = h('span', 'ovdk-av ovdk-av-vibe'); av.append(icon(G.vibe));
+ b.append(av, body, meta); b.setAttribute('aria-label', 'Open Vibe');
+ b.addEventListener('click', () => {
+ if (!b.dispatchEvent(new CustomEvent('openvibe:open-vibe', { bubbles: true, cancelable: true }))) return;
+ vibeOnList = true; save({ vibe: null }); setTab('vibe', true);
+ });
+ li.append(b); ul.append(li); return;
+ }
const li = h('li'), b = h('button', 'ovdk-chat');
const li = h('li'), b = h('button', 'ovdk-chat');
b.type = 'button';
b.type = 'button';
const unread = Number(c.unread_count) || 0;
const unread = Number(c.unread_count) || 0;
@@ -549,7 +710,7 @@
@@ -549,7 +710,7 @@
const meta = h('span', 'ovdk-chat-m');
const meta = h('span', 'ovdk-chat-m');
meta.append(h('time', '', when(c.last_message_at || c.updated_at)));
meta.append(h('time', '', when(c.last_message_at || c.updated_at)));
if (unread) meta.append(h('b', 'ovdk-unread', unread > 99 ? '99+' : String(unread)));
if (unread) meta.append(h('b', 'ovdk-unread', unread > 99 ? '99+' : String(unread)));
- b.append(avatar(others(c)[0] || {}, !!c.is_group), body, meta);
+ b.append(c.is_group ? groupAvatar(c) : avatar(others(c)[0] || {}, false), body, meta);
b.setAttribute('aria-label', `${titleOf(c)}${unread ? `, ${unread} unread` : ''}`);
b.setAttribute('aria-label', `${titleOf(c)}${unread ? `, ${unread} unread` : ''}`);
if (unread) li.className = 'is-unread';
if (unread) li.className = 'is-unread';
b.addEventListener('click', () => openChat(String(c.id), true));
b.addEventListener('click', () => openChat(String(c.id), true));
@@ -558,20 +719,27 @@
@@ -558,20 +719,27 @@
scroll.append(ul);
scroll.append(ul);
}
}
panel.append(scroll);
panel.append(scroll);
- chatFoot(panel);
}
}
function bubble(m, prev) {
function bubble(m, prev) {
const mine = m.username === me();
const mine = m.username === me();
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ''}${prev && prev.username === m.username ? ' is-cont' : ''}`);
if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));
if (!mine && !(prev && prev.username === m.username)) li.append(h('span', 'ovdk-who', m.display_name || m.username || ''));
+ li.dataset.id = String(m.id);
li.append(h('span', 'ovdk-text', String(m.message || '')));
li.append(h('span', 'ovdk-text', String(m.message || '')));
+ if (m.edited_at) li.append(h('small', 'ovdk-edited', `edited · ${String(m.edited_at).slice(11, 16)}`));
+ if (mine && canWriteChat()) { const edit = h('button', 'ovk-btn ovk-btn--quiet ovdk-edit', 'Edit'); edit.type = 'button'; li.append(edit); }
+ // A known OpenVibe link's card (Chat's link cards): dock-card.js draws it, loaded the first time one shows.
+ if (m.card) load('dock-card.js', () => !!root.OpenVibeDockCard).then((ok) => { const c = ok && root.OpenVibeDockCard(m.card, h); if (c) li.append(c); });
li.title = new Date(m.created_at).toLocaleString();
li.title = new Date(m.created_at).toLocaleString();
return li;
return li;
}
}
async function openChat(id, remember) {
async function openChat(id, remember) {
clearTimeout(chats.timer);
clearTimeout(chats.timer);
+ if (chats.edits) { chats.edits.stop(); chats.edits = null; }
const panel = chats.panel; if (!panel) return;
const panel = chats.panel; if (!panel) return;
chats.open = String(id); chats.msgs = []; chats.last = 0;
chats.open = String(id); chats.msgs = []; chats.last = 0;
+ const draft = chats.draft && canWriteChat() ? chats.draft.text : '';
+ chats.draft = null;
if (remember) save({ chat: chats.open });
if (remember) save({ chat: chats.open });
const conv = (chats.list || []).find((c) => String(c.id) === chats.open);
const conv = (chats.list || []).find((c) => String(c.id) === chats.open);
panel.textContent = '';
panel.textContent = '';
@@ -579,7 +747,7 @@
@@ -579,7 +747,7 @@
const back = h('button', 'ovdk-back'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your chats'); back.append(icon(G.back));
const back = h('button', 'ovdk-back'); back.type = 'button'; back.setAttribute('aria-label', 'Back to your chats'); back.append(icon(G.back));
back.addEventListener('click', () => { save({ chat: null }); chatList(); });
back.addEventListener('click', () => { save({ chat: null }); chatList(); });
const title = h('b', '', conv ? titleOf(conv) : 'Conversation');
const title = h('b', '', conv ? titleOf(conv) : 'Conversation');
- head.append(back, title);
+ head.append(back, title, toChat('https://openvibe.chat/messages'));
// Opened from the Dock's state on another page: the list is not here yet, so the title comes with it.
// Opened from the Dock's state on another page: the list is not here yet, so the title comes with it.
if (!conv) chatCall('/api/dm/conversations').then((b) => { chats.list = b.conversations || []; const c = chats.list.find((x) => String(x.id) === String(id)); if (c && chats.open === String(id)) title.textContent = titleOf(c); }).catch(() => { /* the title stays */ });
if (!conv) chatCall('/api/dm/conversations').then((b) => { chats.list = b.conversations || []; const c = chats.list.find((x) => String(x.id) === String(id)); if (c && chats.open === String(id)) title.textContent = titleOf(c); }).catch(() => { /* the title stays */ });
const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');
const list = h('ol', 'ovdk-msgs'); list.setAttribute('aria-live', 'polite');
@@ -594,6 +762,7 @@
@@ -594,6 +762,7 @@
panel.append(form);
panel.append(form);
const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };
const fit = () => { box.style.height = 'auto'; box.style.height = `${Math.min(box.scrollHeight, 140)}px`; };
box.addEventListener('input', fit);
box.addEventListener('input', fit);
+ if (draft && root.OpenVibeDockShare) root.OpenVibeDockShare.ready({ draft, h, panel, form, box, fit });
box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });
box.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (form.requestSubmit) form.requestSubmit(); else form.dispatchEvent(new Event('submit', { cancelable: true })); } });
form.addEventListener('submit', async (e) => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
e.preventDefault();
@@ -615,17 +784,18 @@
@@ -615,17 +784,18 @@
list.append(h('li', 'ovdk-wait', null));
list.append(h('li', 'ovdk-wait', null));
list.firstChild.innerHTML = '<span></span>';
list.firstChild.innerHTML = '<span></span>';
await pull(list, scroll, true);
await pull(list, scroll, true);
- if (canWriteChat()) setTimeout(() => { try { box.focus({ preventScroll: true }); } catch { /* */ } }, 0);
+ load('dock-chat-edits.js', () => !!root.OpenVibeDockChatEdits).then((ok) => { if (ok && chats.open === String(id)) chats.edits = root.OpenVibeDockChatEdits({ id: String(id), list, messages: chats.msgs, error: err, tokenOf, chatCall, chatBase, active: () => chats.open === String(id) && panelActive('chats'), load, h }); });
+ if (canWriteChat() && !(typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches)) setTimeout(() => { try { box.focus({ preventScroll: true }); if (draft) box.setSelectionRange(box.value.length, box.value.length); } catch { /* */ } }, 0);
}
}
/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */
/** New messages of the open thread (all of them the first time), drawn in place; read once shown. */
async function pull(list, scroll, first) {
async function pull(list, scroll, first) {
const id = chats.open;
const id = chats.open;
let got = [];
let got = [];
try { got = (await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages?limit=50${chats.last ? `&after=${chats.last}` : ''}`)).messages || []; } catch (e) {
try { got = (await chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/messages?limit=50${chats.last ? `&after=${chats.last}` : ''}`)).messages || []; } catch (e) {
- if (first) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 403 ? 'This conversation is not yours.' : 'The messages did not load. Try again.')); }
+ if (first && panelActive('chats')) { list.textContent = ''; list.append(h('li', 'ovdk-none', e.status === 403 ? 'This conversation is not yours.' : 'The messages did not load. Try again.')); }
return;
return;
}
}
- if (id !== chats.open) return;
+ if (id !== chats.open || !panelActive('chats')) return;
if (first) list.textContent = '';
if (first) list.textContent = '';
const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;
const atEnd = scroll.scrollHeight - scroll.scrollTop - scroll.clientHeight < 40;
got.forEach((m) => { if (chats.msgs.some((x) => x.id === m.id)) return; list.append(bubble(m, chats.msgs[chats.msgs.length - 1])); chats.msgs.push(m); chats.last = Math.max(chats.last, Number(m.id) || 0); });
got.forEach((m) => { if (chats.msgs.some((x) => x.id === m.id)) return; list.append(bubble(m, chats.msgs[chats.msgs.length - 1])); chats.msgs.push(m); chats.last = Math.max(chats.last, Number(m.id) || 0); });
@@ -633,13 +803,15 @@
@@ -633,13 +803,15 @@
if (first || atEnd) scroll.scrollTop = scroll.scrollHeight;
if (first || atEnd) scroll.scrollTop = scroll.scrollHeight;
if (got.length || first) chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/read`, { method: 'POST', body: '{}' }).then(readUnread).catch(() => { /* */ });
if (got.length || first) chatCall(`/api/dm/conversations/${encodeURIComponent(id)}/read`, { method: 'POST', body: '{}' }).then(readUnread).catch(() => { /* */ });
clearTimeout(chats.timer);
clearTimeout(chats.timer);
- if (isOpen() && tab === 'chats' && chats.open === id) chats.timer = setTimeout(() => pull(list, scroll), 5000);
+ if (panelActive('chats') && chats.open === id) chats.timer = setTimeout(() => pull(list, scroll), 5000);
}
}
// ── Vibe (plan L48 V2): the person's Vibe, in dock-vibe.js, loaded the first time the tab opens (the Dock is on
// ── Vibe (plan L48 V2): the person's Vibe, in dock-vibe.js, loaded the first time the tab opens (the Dock is on
// every page for signed-in people; Vibe's conversation and mic are not needed until then). Alt+V talks to Vibe from
// every page for signed-in people; Vibe's conversation and mic are not needed until then). Alt+V talks to Vibe from
// any page: it opens the Dock on Vibe and starts listening (only where the browser can listen).
// any page: it opens the Dock on Vibe and starts listening (only where the browser can listen).
const vibe = { panel: null, api: null };
const vibe = { panel: null, api: null };
+ // The Chats tab's AI chats row opens the Vibe tab on its list, not on the last conversation.
+ let vibeOnList = false;
const canListen = () => !!(root.SpeechRecognition || root.webkitSpeechRecognition);
const canListen = () => !!(root.SpeechRecognition || root.webkitSpeechRecognition);
document.addEventListener('keydown', (e) => {
document.addEventListener('keydown', (e) => {
if (!e.altKey || e.ctrlKey || e.metaKey || String(e.key).toLowerCase() !== 'v' || !canListen() || !o || typeof o.token !== 'function') return;
if (!e.altKey || e.ctrlKey || e.metaKey || String(e.key).toLowerCase() !== 'v' || !canListen() || !o || typeof o.token !== 'function') return;
@@ -653,14 +825,14 @@
@@ -653,14 +825,14 @@
function withTimer(fn) {
function withTimer(fn) {
if (timerMod) return fn(timerMod);
if (timerMod) return fn(timerMod);
timerLoading = timerLoading || load('dock-timer.js', () => !!(root.OpenVibeDockTimer && root.OpenVibeDockTimer.attach)).then((ok) => {
timerLoading = timerLoading || load('dock-timer.js', () => !!(root.OpenVibeDockTimer && root.OpenVibeDockTimer.attach)).then((ok) => {
- if (ok && !timerMod) timerMod = root.OpenVibeDockTimer.attach({ state, save, host: () => el, repaint: () => paintPill() });
+ if (ok && !timerMod) timerMod = root.OpenVibeDockTimer.attach({ state, save, host: () => inline || el, repaint: () => paintPill() });
return timerMod;
return timerMod;
});
});
return timerLoading.then((m) => (m ? fn(m) : null));
return timerLoading.then((m) => (m ? fn(m) : null));
}
}
// ── The pill ─────────────────────────────────────────────
// ── The pill ─────────────────────────────────────────────
- let pill = null, readT = 0, bound = false;
+ let pill = null, readT = 0, bound = false, openedAt = 0;
function store(v) {
function store(v) {
try { if (v === undefined) return JSON.parse(localStorage.getItem('ov-dock') || 'null'); localStorage.setItem('ov-dock', JSON.stringify(v)); } catch { /* */ }
try { if (v === undefined) return JSON.parse(localStorage.getItem('ov-dock') || 'null'); localStorage.setItem('ov-dock', JSON.stringify(v)); } catch { /* */ }
return null;
return null;
@@ -702,7 +874,7 @@
@@ -702,7 +874,7 @@
const next = r.ok ? await r.json() : null;
const next = r.ok ? await r.json() : null;
if (!next || !next.state || !pill) return;
if (!next || !next.state || !pill) return;
update(next);
update(next);
- if (!isOpen() && next.state.open && !phone()) open();
+ if (!isOpen() && !inline && next.state.open && !phone()) open();
const c = next.counts || {};
const c = next.counts || {};
if (c.running || c.waiting || next.state.timer) readT = setTimeout(read, 30000);
if (c.running || c.waiting || next.state.timer) readT = setTimeout(read, 30000);
} catch { /* offline: the copy stands */ }
} catch { /* offline: the copy stands */ }
@@ -710,6 +882,7 @@
@@ -710,6 +882,7 @@
/** The pill for a signed-in person (navbar.js), or none (null: signed out, or the site turned the Dock off). */
/** The pill for a signed-in person (navbar.js), or none (null: signed out, or the site turned the Dock off). */
function mount(opts) {
function mount(opts) {
if (!opts) {
if (!opts) {
+ if (mountHandle) mountHandle.destroy();
clearTimeout(readT);
clearTimeout(readT);
if (pill) { pill.remove(); pill = null; }
if (pill) { pill.remove(); pill = null; }
if (isOpen()) close();
if (isOpen()) close();
@@ -747,7 +920,7 @@
@@ -747,7 +920,7 @@
if (!b) return;
if (!b) return;
b.hidden = !pip();
b.hidden = !pip();
const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';
const t = popped ? 'Bring the Dock back to the page' : 'Open the Dock in a window of its own';
- b.title = t; b.setAttribute('aria-label', t);
+ b.title = t; const name = b.querySelector('b'); if (name) name.textContent = t;
}
}
/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */
/** The window gets the Dock's styles, the theme's colours and the page's own type: the Dock looks the same there. */
@@ -809,8 +982,54 @@
@@ -809,8 +982,54 @@
}
}
// ── Open and close ───────────────────────────────────────
// ── Open and close ───────────────────────────────────────
+ // Ask your agent (4.135.0): Vibe opens with the question in its box, unsent.
+ function ask({ text = '' } = {}, n = 0) { if (!n) open({ tab: 'vibe' }); if (vibe.api && vibe.api.ask) vibe.api.ask(text); else if (n < 40) setTimeout(() => ask({ text }, n + 1), 100); }
function isOpen() { return !!(el && !el.hidden); }
function isOpen() { return !!(el && !el.hidden); }
+ // A phone's sheet and the navbar's orb are one button (.from-orb).
+ let orbLink = null, closing = 0;
+ function navOrb() {
+ if (!phone()) return null;
+ const list = document.querySelectorAll('.openvibe-navbar .ovnav-orb');
+ for (let i = 0; i < list.length; i++) { const r = list[i].getBoundingClientRect(); if (r.width && r.left > root.innerWidth / 2 && r.top < 120 && r.bottom > 0) return list[i]; }
+ return null;
+ }
+ function fromOrb() {
+ unlinkOrb();
+ const nav = navOrb(), mine = el.querySelector('.ovdk-orbx'), mineSvg = mine && mine.querySelector('svg');
+ if (!nav || !mineSvg || reduced() || popped) return;
+ el.classList.add('from-orb'); // first: it sets the header's layout, and the close orb is measured where it will rest
+ const a = (nav.querySelector('svg') || nav).getBoundingClientRect(), b = mineSvg.getBoundingClientRect(), box = el.getBoundingClientRect();
+ if (!a.width || !b.width) { el.classList.remove('from-orb'); return; }
+ const ax = a.left + a.width / 2, ay = a.top + a.height / 2, bx = b.left + b.width / 2, by = b.top + b.height / 2, s = a.width / b.width;
+ el.style.setProperty('--ovdk-ox', `${Math.round(ax - box.left)}px`); el.style.setProperty('--ovdk-oy', `${Math.round(ay - box.top)}px`);
+ mine.style.setProperty('--orb-dx', `${(ax - bx).toFixed(1)}px`); mine.style.setProperty('--orb-dy', `${(ay - by).toFixed(1)}px`); mine.style.setProperty('--orb-s', s.toFixed(3));
+ nav.style.setProperty('--dock-dx', `${(bx - ax).toFixed(1)}px`); nav.style.setProperty('--dock-dy', `${(by - ay).toFixed(1)}px`); nav.style.setProperty('--dock-s', (1 / s).toFixed(3));
+ mine.classList.add('is-set', 'is-from');
+ orbLink = { nav, mine };
+ void mine.offsetWidth; // the starting pose is put on, not animated into
+ mine.classList.remove('is-set');
+ void mine.offsetWidth;
+ nav.style.transition = 'transform .42s cubic-bezier(.2, 1.1, .3, 1), color .3s';
+ root.requestAnimationFrame(() => { if (!orbLink || orbLink.mine !== mine) return; mine.classList.remove('is-from'); nav.classList.add('is-dock'); });
+ }
+ function unlinkOrb() {
+ if (closing) { clearTimeout(closing); closing = 0; }
+ if (el) el.classList.remove('from-orb', 'is-out');
+ if (!orbLink) return;
+ orbLink.nav.classList.remove('is-dock'); orbLink.nav.style.transition = ''; orbLink.mine.classList.remove('is-from', 'is-set');
+ orbLink = null;
+ }
+ function share({ text = '', title = '' } = {}) {
+ const t = String(text || '').trim().slice(0, 4000);
+ if (!t) return;
+ chats.draft = { text: t, title: String(title || '').trim().slice(0, 140) }; chats.open = null;
+ // A bar left from an earlier share would show the old words until the list reloads: it goes now.
+ const stale = chats.panel && chats.panel.querySelector('.ovdk-share'); if (stale) stale.remove();
+ open({ tab: 'chats' });
+ }
function open(opts = {}) {
function open(opts = {}) {
+ if (mountHandle) mountHandle.destroy();
+ openedAt = typeof performance !== 'undefined' && performance.now ? performance.now() : 0;
o = { ...o, ...opts };
o = { ...o, ...opts };
if (opts.doc) doc = opts.doc;
if (opts.doc) doc = opts.doc;
if (!el) build();
if (!el) build();
@@ -822,8 +1041,18 @@
@@ -822,8 +1041,18 @@
paintPin();
paintPin();
paintPop();
paintPop();
el.hidden = false;
el.hidden = false;
- el.classList.remove('is-in'); void el.offsetWidth; el.classList.add('is-in');
- if (phone()) { el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open'); } else el.removeAttribute('aria-modal');
+ el.classList.remove('is-in');
+ fromOrb();
+ void el.offsetWidth; el.classList.add('is-in');
+ // On a computer it grows out of its pill; a phone's sheet opens out of the navbar's orb (fromOrb), or rises.
+ if (!phone() && root.OpenVibeNavbar && root.OpenVibeNavbar.pop) {
+ const pill = document.getElementById('ovdk-pill');
+ root.OpenVibeNavbar.pop(el, pill?.offsetWidth ? pill : null);
+ }
+ if (phone()) {
+ el.setAttribute('aria-modal', 'true'); document.documentElement.classList.add('ovdk-sheet-open');
+ if (root.OVSurfaces && !dockSurface) { dockSurface = root.OVSurfaces.sheet({ el, body: el.querySelector('.ovdk-body'), close }); dockSurface.open(); }
+ } else { el.removeAttribute('aria-modal'); if (dockSurface) { dockSurface.destroy(); dockSurface = null; } }
setTab(want, false);
setTab(want, false);
readUnread();
readUnread();
if (!state().open || (state().tab !== tab && SAVED_TABS.includes(tab))) save({ open: true, ...(SAVED_TABS.includes(tab) ? { tab } : {}) }); else notify();
if (!state().open || (state().tab !== tab && SAVED_TABS.includes(tab))) save({ open: true, ...(SAVED_TABS.includes(tab) ? { tab } : {}) }); else notify();
@@ -831,16 +1060,29 @@
@@ -831,16 +1060,29 @@
}
}
function close() {
function close() {
if (!isOpen()) return;
if (!isOpen()) return;
+ // Fold back into the navbar's orb first, then go.
+ if (orbLink && !closing && !popped && !reduced() && el.classList.contains('from-orb')) {
+ el.classList.remove('is-in'); el.classList.add('is-out');
+ orbLink.mine.classList.add('is-from'); orbLink.nav.classList.remove('is-dock');
+ closing = setTimeout(() => { closing = 0; shut(); }, 280);
+ return;
+ }
+ shut();
+ }
+ function shut(keepState = false) {
+ if (!isOpen()) return;
+ unlinkOrb();
const wasPopped = popped;
const wasPopped = popped;
if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }
if (wasPopped) { popped = false; const w = pipWin; pipWin = null; pipClosing = true; try { if (w) w.close(); } catch { /* already gone */ } pipClosing = false; if (el.parentNode !== document.body) document.body.appendChild(el); }
const inside = el.contains(document.activeElement);
const inside = el.contains(document.activeElement);
el.hidden = true;
el.hidden = true;
+ if (dockSurface) { dockSurface.destroy(); dockSurface = null; }
el.style.transform = '';
el.style.transform = '';
el.removeAttribute('aria-modal');
el.removeAttribute('aria-modal');
document.documentElement.classList.remove('ovdk-sheet-open');
document.documentElement.classList.remove('ovdk-sheet-open');
clear(0);
clear(0);
clearTimeout(chats.timer); clearTimeout(chats.unreadT);
clearTimeout(chats.timer); clearTimeout(chats.unreadT);
- save({ open: false });
+ if (!keepState) save({ open: false });
if (inside) {
if (inside) {
const back = pill && !pill.hidden ? pill : (lastFocus && lastFocus.isConnected ? lastFocus : null);
const back = pill && !pill.hidden ? pill : (lastFocus && lastFocus.isConnected ? lastFocus : null);
try { if (back) back.focus({ preventScroll: true }); } catch { /* */ }
try { if (back) back.focus({ preventScroll: true }); } catch { /* */ }
@@ -859,5 +1101,29 @@
@@ -859,5 +1101,29 @@
if (isOpen()) layout();
if (isOpen()) layout();
}
}
- root.OpenVibeDock = { mount, open, close, isOpen, update, _state: () => ({ doc, tab, pending }) };
+ let mountHandle = null;
+ /** Place the same panel body under another sheet's head. The helper and its layout load on first use. */
+ function mountInto(host, { tab: first = 'activity' } = {}) {
+ if (!(host instanceof Element)) throw new TypeError('mountInto needs a host element');
+ if (mountHandle) mountHandle.destroy();
+ if (isOpen()) { clearTimeout(closing); closing = 0; shut(true); }
+ let api = null, gone = false, wanted = first;
+ const handle = {
+ show(id) { if (gone) return; wanted = id; if (api) api.show(id); },
+ destroy() { if (gone) return; gone = true; if (api) api.destroy(); if (mountHandle === handle) mountHandle = null; },
+ };
+ mountHandle = handle;
+ load('dock-mount.js', () => !!root.OpenVibeDockMount).then((ok) => {
+ if (ok && !gone) api = root.OpenVibeDockMount.mount(host, wanted, {
+ start(wrap) { if (!el) build(); inline = wrap; wrap.append(bodyEl); },
+ show(id) { setTab(id, false); badges(); },
+ end() { disposePanel(); inline = null; if (el) el.insertBefore(bodyEl, el.querySelector('.ovdk-size')); paintPill(); },
+ });
+ });
+ return handle;
+ }
+
+ root.OpenVibeDock = { mount, mountInto, open, close, isOpen, update, ask, share,
+ filterChats: (v) => { chats.filter = v.toLowerCase(); chatList(); },
+ more: a => el.querySelector(`[data-act="${a}"]`).click(), canSend: () => canWriteChat(), _state: () => ({ doc, tab, pending }) };
})(typeof window !== 'undefined' ? window : globalThis);
})(typeof window !== 'undefined' ? window : globalThis);
@@ -51,7 +51,7 @@
@@ -51,7 +51,7 @@
--ovk-radius-sm: var(--ov-radius-1);
--ovk-radius-sm: var(--ov-radius-1);
--ovk-tap: 44px;
--ovk-tap: 44px;
--ovk-top: var(--ovnav-real-h, 52px); /* the navbar's height: sticky things stop under it */
--ovk-top: var(--ovnav-real-h, 52px); /* the navbar's height: sticky things stop under it */
- --ovk-bottom: var(--ovnav-appbar-h, 0px); /* the app bar's height on phones (navbar.js sets it) */
+ --ovk-bottom: var(--ovb-dock, var(--ovnav-appbar-h, 0px)); /* the app bar (and a small player above it) on phones, measured by bottom.js */
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
--ovk-font: var(--ov-font, 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
}
}
@@ -63,9 +63,16 @@
@@ -63,9 +63,16 @@
:is([data-theme=neon-tokyo], [data-theme=vapor], [data-theme=matrix], [data-theme=hacker], [data-theme=high-contrast], [data-theme=dracula], [data-theme=monokai], [data-theme=abyss]) {
:is([data-theme=neon-tokyo], [data-theme=vapor], [data-theme=matrix], [data-theme=hacker], [data-theme=high-contrast], [data-theme=dracula], [data-theme=monokai], [data-theme=abyss]) {
--ov-dur-1: .09s; --ov-dur-2: .15s; --ov-dur-3: .24s; --ov-dur-4: .42s; --ov-ease-spring: cubic-bezier(.3, 1.8, .6, 1);
--ov-dur-1: .09s; --ov-dur-2: .15s; --ov-dur-3: .24s; --ov-dur-4: .42s; --ov-ease-spring: cubic-bezier(.3, 1.8, .6, 1);
}
}
-html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; }
+/* Motion (4.183.0; the owner, 2026-11-10: "it should still have full motion but actually just reduce it instead of
+ getting rid of it"). Calm keeps every effect and makes it smaller and slower: the calm feel's lengths, no
+ overshoot, and --ov-move, the factor a moving thing multiplies its distance by (calc(12px * var(--ov-move, 1))).
+ Only Off stops motion. theme-loader.js writes data-ov-motion (reduced for Calm, the device's own setting counted
+ in; off; full); the media rule serves a page the loader has not reached. */
+:where(html) { --ov-move: 1; }
+html[data-ov-motion=reduced] { --ov-move: .35; --ov-dur-1: .15s; --ov-dur-2: .25s; --ov-dur-3: .4s; --ov-dur-4: .62s; --ov-ease-spring: var(--ov-ease-out); }
+@media (prefers-reduced-motion: reduce) { html:not([data-ov-motion]) { --ov-move: .35; --ov-ease-spring: var(--ov-ease-out); } }
/* :root:root outranks ov-fx.js's own :root and theme feels, which load later. */
/* :root:root outranks ov-fx.js's own :root and theme feels, which load later. */
-@media (prefers-reduced-motion: reduce) { :root:root { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; } }
+:root:root[data-ov-motion=off] { --ov-move: 0; --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; --ov-dur-4: 0s; }
/* Shape personalities (plan L45: "Paper is quiet and square, Neon glows and bounces"). A quiet look has square corners
/* Shape personalities (plan L45: "Paper is quiet and square, Neon glows and bounces"). A quiet look has square corners
* and lies flat, with a hairline where a shadow would be; a loud one is round and its raised things glow in its accent.
* and lies flat, with a hairline where a shadow would be; a loud one is round and its raised things glow in its accent.
* The ids are Network's theme catalog (as ov-fx.js's motion feels); any other look keeps the defaults above. */
* The ids are Network's theme catalog (as ov-fx.js's motion feels); any other look keeps the defaults above. */
@@ -111,6 +118,83 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
@@ -111,6 +118,83 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
.ovk-icon-btn[hidden] { display: none; }
.ovk-icon-btn[hidden] { display: none; }
.ovk-icon-btn.is-done { color: var(--ovk-ok-text); }
.ovk-icon-btn.is-done { color: var(--ovk-ok-text); }
.ovk-icon-btn--danger:hover { color: var(--ovk-bad-text); }
.ovk-icon-btn--danger:hover { color: var(--ovk-bad-text); }
+
+/* ── Buttons that show what they do and what is happening (4.204.0) ───────────────────────────────────────────────
+ The owner, 2026-11-11: "improve our buttons across the site and add better animations and effects for them, and
+ different things we can use them for, and little indicators or statuses … idle animation … more important buttons …
+ more intuitive and more modular … shared across everything through openvibe.design". docs/kit.md#buttons.
+ Every motion is scaled by --ov-move (Calm makes it smaller and slower, Off removes it; the durations follow). */
+.ovk-btn, .ovk-icon-btn { position: relative; -webkit-tap-highlight-color: transparent;
+ transition: transform var(--ov-dur-1, .12s) var(--ov-ease-out, ease), box-shadow var(--ov-dur-2, .2s) var(--ov-ease-out, ease),
+ background-color var(--ov-dur-1, .12s), border-color var(--ov-dur-1, .12s), color var(--ov-dur-1, .12s), filter var(--ov-dur-1, .12s); }
+.ovk-btn:hover:not([disabled]):not([aria-disabled="true"]) { transform: translateY(calc(-1px * var(--ov-move, 1))); box-shadow: 0 8px 18px -12px rgba(0, 0, 0, .6); }
+.ovk-btn:active:not([disabled]), .ovk-icon-btn:active:not([disabled]) { transform: scale(calc(1 - .03 * var(--ov-move, 1))); box-shadow: none; transition-duration: .06s; }
+.ovk-btn .ovk-i, .ovk-icon-btn .ovk-i { transition: transform var(--ov-dur-2, .2s) var(--ov-ease-spring, var(--ov-ease-out, ease)); }
+/* The icon moves the way the button does (data-nudge): forward slides on, add turns, open lifts, down drops, refresh turns. */
+.ovk-btn[data-nudge="forward"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="forward"]:hover .ovk-i { transform: translateX(calc(3px * var(--ov-move, 1))); }
+.ovk-btn[data-nudge="back"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="back"]:hover .ovk-i { transform: translateX(calc(-3px * var(--ov-move, 1))); }
+.ovk-btn[data-nudge="add"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="add"]:hover .ovk-i { transform: rotate(calc(90deg * var(--ov-move, 1))); }
+.ovk-btn[data-nudge="open"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="open"]:hover .ovk-i { transform: translate(calc(2px * var(--ov-move, 1)), calc(-2px * var(--ov-move, 1))); }
+.ovk-btn[data-nudge="down"]:hover .ovk-i:last-child, .ovk-icon-btn[data-nudge="down"]:hover .ovk-i { transform: translateY(calc(2px * var(--ov-move, 1))); }
+.ovk-btn[data-nudge="refresh"]:hover .ovk-i:first-child, .ovk-icon-btn[data-nudge="refresh"]:hover .ovk-i { transform: rotate(calc(180deg * var(--ov-move, 1))); }
+
+/* Importance: hero (the one thing a page is for), soft (a tinted second choice), live (on air now). */
+.ovk-btn--hero { min-height: 52px; padding: 12px 24px; font-size: 1.02rem; color: var(--ovk-on-fill); border: 0;
+ background: linear-gradient(135deg, var(--ovk-accent), var(--ovk-fill)); box-shadow: 0 10px 26px -14px var(--ovk-fill); }
+.ovk-btn--hero:hover:not([disabled]) { box-shadow: 0 14px 30px -12px var(--ovk-fill); filter: brightness(1.06); }
+.ovk-btn--soft { color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 14%, transparent); border-color: color-mix(in srgb, var(--ovk-accent) 22%, transparent); }
+.ovk-btn--soft:hover { background: color-mix(in srgb, var(--ovk-accent) 20%, transparent); }
+.ovk-btn--live { color: #fff; background: var(--ovk-live); }
+.ovk-btn--live::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: #fff; flex: none; animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
+@keyframes ovkBreathe { 50% { opacity: calc(1 - .65 * var(--ov-move, 1)); transform: scale(calc(1 - .2 * var(--ov-move, 1))); } }
+
+/* A toggle says it is on (aria-pressed): filled tint and a check-weight border. */
+.ovk-btn[aria-pressed="true"], .ovk-icon-btn[aria-pressed="true"] { color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 16%, transparent); border-color: color-mix(in srgb, var(--ovk-accent) 45%, transparent); }
+
+/* The one next step (data-attention): a soft ring goes out from it now and then, until it is pressed. */
+.ovk-btn[data-attention]:not([disabled])::after, .ovk-icon-btn[data-attention]:not([disabled])::after {
+ content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none;
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--ovk-accent) 55%, transparent); animation: ovkAttention 3.2s var(--ov-ease-out, ease-out) infinite; }
+@keyframes ovkAttention { 0%, 55% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--ovk-accent) 55%, transparent); } 85%, 100% { box-shadow: 0 0 0 calc(10px * var(--ov-move, 1)) transparent; } }
+
+/* A count (data-badge="3") and a status dot (data-dot="ok|warn|bad|live") at the top right. */
+.ovk-btn[data-badge]:not([data-badge=""])::before, .ovk-icon-btn[data-badge]:not([data-badge=""])::before {
+ content: attr(data-badge); position: absolute; top: -6px; right: -6px; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box;
+ border-radius: 999px; background: var(--ovk-bad); color: #fff; font-size: 11px; font-weight: 800; line-height: 18px; text-align: center;
+ box-shadow: 0 0 0 2px var(--ovk-bg, var(--bg-primary, #0b0f19)); animation: ovkPop var(--ov-dur-2, .2s) var(--ov-ease-spring, ease-out); }
+.ovk-btn--live[data-badge]::before { animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
+.ovk-btn[data-dot], .ovk-icon-btn[data-dot] { --ovk-dot: var(--ovk-muted); }
+.ovk-btn[data-dot="ok"], .ovk-icon-btn[data-dot="ok"] { --ovk-dot: var(--ovk-ok); }
+.ovk-btn[data-dot="warn"], .ovk-icon-btn[data-dot="warn"] { --ovk-dot: var(--warning, #f59e0b); }
+.ovk-btn[data-dot="bad"], .ovk-icon-btn[data-dot="bad"], .ovk-btn[data-dot="live"], .ovk-icon-btn[data-dot="live"] { --ovk-dot: var(--ovk-bad); }
+.ovk-btn[data-dot]:not([data-badge])::before, .ovk-icon-btn[data-dot]:not([data-badge])::before {
+ content: ""; position: absolute; top: 4px; right: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--ovk-dot);
+ box-shadow: 0 0 0 2px var(--ovk-bg, var(--bg-primary, #0b0f19)); }
+.ovk-btn[data-dot="live"]:not([data-badge])::before, .ovk-icon-btn[data-dot="live"]:not([data-badge])::before { animation: ovkBreathe 1.6s var(--ov-ease-in-out, ease-in-out) infinite; }
+@keyframes ovkPop { from { transform: scale(calc(1 - .6 * var(--ov-move, 1))); opacity: 0; } }
+
+/* Progress inside the button (style="--ovk-progress: .4"): an upload, a countdown, a hold. */
+.ovk-btn[style*="--ovk-progress"] { background-image: linear-gradient(90deg, color-mix(in srgb, currentColor 18%, transparent) calc(var(--ovk-progress, 0) * 100%), transparent 0); }
+
+/* What is happening (data-state, set by OpenVibeKitWeb.busy): busy keeps the words and adds the mark's wait; done shows
+ a check for a moment; error shakes once and turns red. */
+.ovk-btn[data-state="busy"] { cursor: progress; pointer-events: none; }
+.ovk-btn[data-state="busy"] > :not(.ovk-btn-wait) { opacity: .62; }
+.ovk-btn-wait { display: inline-flex; gap: 3px; align-items: center; }
+.ovk-btn-wait i { width: 5px; height: 5px; border-radius: 50%; background: currentColor; animation: ovkWait 1.1s var(--ov-ease-in-out, ease-in-out) infinite; }
+.ovk-btn-wait i:nth-child(2) { animation-delay: .15s; } .ovk-btn-wait i:nth-child(3) { animation-delay: .3s; }
+@keyframes ovkWait { 0%, 80%, 100% { opacity: .25; transform: translateY(0); } 40% { opacity: 1; transform: translateY(calc(-3px * var(--ov-move, 1))); } }
+.ovk-btn[data-state="done"] { color: #fff; background: var(--ovk-ok); border-color: transparent; }
+.ovk-btn[data-state="done"]::after { content: ""; flex: none; width: 16px; height: 16px; background: currentColor; animation: ovkPop var(--ov-dur-2, .2s) var(--ov-ease-spring, ease-out);
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12.5 4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat; }
+.ovk-btn[data-state="error"] { color: var(--ovk-bad-text); border-color: var(--ovk-bad); background: color-mix(in srgb, var(--ovk-bad) 10%, transparent); animation: ovkShake .36s var(--ov-ease-out, ease-out); }
+@keyframes ovkShake { 25% { transform: translateX(calc(-4px * var(--ov-move, 1))); } 50% { transform: translateX(calc(4px * var(--ov-move, 1))); } 75% { transform: translateX(calc(-2px * var(--ov-move, 1))); } }
+
+/* Hold to confirm (data-hold="1200" on a destructive button): the fill runs while it is held; letting go early cancels. */
+.ovk-btn[data-hold] { background-image: linear-gradient(90deg, color-mix(in srgb, var(--ovk-bad) 26%, transparent) var(--ovk-hold, 0%), transparent 0); }
+.ovk-btn[data-hold].is-holding { transition: none; }
+@media (hover: none) { .ovk-btn:hover:not([disabled]):not([aria-disabled="true"]) { transform: none; box-shadow: none; } }
.ovk-icon-btn .ovk-i { width: 20px; height: 20px; }
.ovk-icon-btn .ovk-i { width: 20px; height: 20px; }
@media (pointer: coarse) { .ovk-icon-btn { width: var(--ovk-tap); height: var(--ovk-tap); } }
@media (pointer: coarse) { .ovk-icon-btn { width: var(--ovk-tap); height: var(--ovk-tap); } }
.ovk-badge { display: inline-flex; align-items: center; flex: none; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--ovk-line); font-size: .75rem; font-weight: 800; letter-spacing: .03em; text-transform: uppercase; color: var(--ovk-muted); white-space: nowrap; }
.ovk-badge { display: inline-flex; align-items: center; flex: none; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--ovk-line); font-size: .75rem; font-weight: 800; letter-spacing: .03em; text-transform: uppercase; color: var(--ovk-muted); white-space: nowrap; }
@@ -128,10 +212,16 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
@@ -128,10 +212,16 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
.ovk-head-row { display: flex; align-items: center; gap: 6px; min-height: 48px; padding-bottom: 6px; }
.ovk-head-row { display: flex; align-items: center; gap: 6px; min-height: 48px; padding-bottom: 6px; }
.ovk-head-row > .ovk-back { margin-left: -8px; }
.ovk-head-row > .ovk-back { margin-left: -8px; }
.ovk-head-id { flex: 1; min-width: 0; }
.ovk-head-id { flex: 1; min-width: 0; }
-.ovk-head-id.ovk-head-id h1, .ovk-head-id.ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+/* The title and its line wrap to two lines before anything is cut (openvibe.design names-are-never-cut, 2026-11-11: the
+ Arena's "One prompt, two models, side b…" lost its point on a phone). */
+.ovk-head-id.ovk-head-id h1, .ovk-head-id.ovk-head-id h2 { margin: 0; color: var(--ovk-text); font-family: var(--ovk-font); font-size: clamp(1.15rem, 4.2vw, 1.5rem); font-weight: 800; letter-spacing: -.01em; line-height: 1.2; overflow: hidden; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
+.ovk-sub { margin: 2px 0 0; color: var(--ovk-muted); font-size: .86rem; line-height: 1.35; overflow: hidden; overflow-wrap: anywhere; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; }
.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }
.ovk-head-tools { display: flex; align-items: center; gap: 2px; margin-right: -8px; }
.ovk-head-tools form { margin: 0; }
.ovk-head-tools form { margin: 0; }
+.ovk-heading-icon { display: grid; place-items: center; flex: 0 0 40px; width: 40px; height: 40px; border-radius: var(--ovk-radius-sm); color: var(--ovk-accent); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); }
+.ovk-heading-icon > .ovk-i { width: 22px; height: 22px; }
+.ovk-heading-icon.ovk-i { box-sizing: border-box; width: 40px; height: 40px; padding: 9px; vertical-align: middle; }
+.ovk-titled.ovk-titled { display: flex; align-items: center; gap: 10px; white-space: normal; overflow-wrap: anywhere; }
.ovk-tabs { position: relative; display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.ovk-tabs { position: relative; display: flex; gap: 4px; margin: 0 -16px; padding: 0 16px 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.ovk-tabs::-webkit-scrollbar { display: none; }
.ovk-tabs::-webkit-scrollbar { display: none; }
.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }
.ovk-tabs a { display: inline-flex; align-items: center; gap: 6px; flex: none; min-height: 36px; padding: 6px 14px; border-radius: 999px; color: var(--ovk-muted); font-size: .9rem; font-weight: 650; text-decoration: none; white-space: nowrap; }
@@ -173,6 +263,12 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
@@ -173,6 +263,12 @@ html[data-ov-motion=reduced] { --ov-dur-1: 0s; --ov-dur-2: 0s; --ov-dur-3: 0s; -
/* ── Sections (settings groups, a page's parts) and folds ── */
/* ── Sections (settings groups, a page's parts) and folds ── */
.ovk-section { margin: 0 0 12px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
.ovk-section { margin: 0 0 12px; border: 1px solid var(--ovk-line); border-radius: var(--ovk-radius); background: var(--ovk-panel); overflow: hidden; color: var(--ovk-text); }
.ovk-section.ovk-section > h2 { display: flex; align-items: center; gap: 6px; margin: 0; padding: 12px 14px 6px; color: var(--ovk-muted); font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.ovk-section.ovk-section > h2 { display: flex; align-items: center; gap: 6px; margin: 0; padding: 12px 14px 6px; color: var(--ovk-muted); font-family: inherit; font-size: .74rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
+.ovk-section-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; padding: 14px; }
+.ovk-section-head.ovk-section-head > h2 { flex: 1 1 160px; min-width: 0; margin: 0; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--ovk-text); font-family: var(--ovk-font); font-size: 1rem; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
+.ovk-section-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-left: auto; max-width: 100%; }
+.ovk-section-actions .ovk-btn { min-height: 44px; white-space: normal; text-align: center; }
+.ovk-section-actions form { margin: 0; }
+.ovk-section > .ovk-empty { margin: 0 14px 14px; }
.ovk-count { color: var(--ovk-text); font-weight: 800; letter-spacing: 0; }
.ovk-count { color: var(--ovk-text); font-weight: 800; letter-spacing: 0; }
.ovk-note { margin: 0; padding: 0 14px 10px; color: var(--ovk-muted); font-size: .86rem; }
.ovk-note { margin: 0; padding: 0 14px 10px; color: var(--ovk-muted); font-size: .86rem; }
.ovk-section > .ovk-form, .ovk-section > form:not(.ovk-find):not(.ovk-inline) { padding: 4px 14px 14px; }
.ovk-section > .ovk-form, .ovk-section > form:not(.ovk-find):not(.ovk-inline) { padding: 4px 14px 14px; }
@@ -205,6 +301,12 @@ div.ovk-set-row { cursor: default; }
@@ -205,6 +301,12 @@ div.ovk-set-row { cursor: default; }
.ovk-set-row select { flex: none; max-width: 52%; margin: 0; padding: 7px 8px; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .9rem; }
.ovk-set-row select { flex: none; max-width: 52%; margin: 0; padding: 7px 8px; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: .9rem; }
.ovk-set-range input[type=range] { flex: 0 1 45%; min-width: 120px; margin: 0; accent-color: var(--ovk-fill); }
.ovk-set-range input[type=range] { flex: 0 1 45%; min-width: 120px; margin: 0; accent-color: var(--ovk-fill); }
.ovk-set-chips { flex-wrap: wrap; }
.ovk-set-chips { flex-wrap: wrap; }
+/* A switch row (4.164.0): the icon tile, the title and its one line, then the switch; the whole 56 px row is the
+ target, and the switch is the native checkbox (role="switch") drawn as one, so it works without JavaScript. */
+.ovk-set-switch { min-height: 56px; }
+.ovk-set-ic { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: var(--ov-radius-1); background: color-mix(in srgb, var(--ovk-accent) 12%, transparent); color: var(--ovk-accent); }
+.ovk-set-ic .ovk-i { width: 19px; height: 19px; }
+.ovk-set-row .ovk-switch { margin-left: auto; }
.ovk-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative; width: 44px; height: 26px; margin: 0; padding: 0; border-radius: 999px; border: 0; background: var(--ovk-line); cursor: pointer; transition: background .15s; }
.ovk-switch { appearance: none; -webkit-appearance: none; flex: none; position: relative; width: 44px; height: 26px; margin: 0; padding: 0; border-radius: 999px; border: 0; background: var(--ovk-line); cursor: pointer; transition: background .15s; }
.ovk-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: var(--ov-elev-1); transition: transform .15s; }
.ovk-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: var(--ov-elev-1); transition: transform .15s; }
.ovk-switch:checked { background: var(--ovk-fill); }
.ovk-switch:checked { background: var(--ovk-fill); }
@@ -215,10 +317,17 @@ div.ovk-set-row { cursor: default; }
@@ -215,10 +317,17 @@ div.ovk-set-row { cursor: default; }
.ovk-seg span { display: block; padding: 6px 12px; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
.ovk-seg span { display: block; padding: 6px 12px; border-radius: max(0px, calc(var(--ov-radius-1) - 2px)); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
.ovk-seg input:checked + span { background: var(--ovk-fill); color: var(--ovk-on-fill); }
.ovk-seg input:checked + span { background: var(--ovk-fill); color: var(--ovk-on-fill); }
.ovk-seg input:focus-visible + span, .ovk-chip input:focus-visible + span { outline: 2px solid var(--ovk-accent); outline-offset: 1px; }
.ovk-seg input:focus-visible + span, .ovk-chip input:focus-visible + span { outline: 2px solid var(--ovk-accent); outline-offset: 1px; }
+/* A segmented control (4.164.0): three or four equal choices in one 44 px control; the icons sit before the words. */
+.ovk-seg--fill { display: flex; flex: 1 1 auto; flex-wrap: nowrap; width: 100%; min-width: 0; }
+.ovk-seg--fill .ovk-seg-item { flex: 1 1 0; min-width: 0; }
+.ovk-seg--fill span { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 38px; box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.ovk-seg--fill .ovk-i { flex: none; width: 16px; height: 16px; }
+/* Straight in a section (no label row around it), it keeps the rows' inset instead of running to the card's edges. */
+.ovk-section > .ovk-seg--fill { width: auto; margin: 10px 12px 6px; }
.ovk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ovk-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.ovk-chip span { display: inline-flex; align-items: center; min-height: 32px; box-sizing: border-box; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--ovk-line); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
.ovk-chip span { display: inline-flex; align-items: center; min-height: 32px; box-sizing: border-box; padding: 5px 12px; border-radius: 999px; border: 1px solid var(--ovk-line); color: var(--ovk-muted); font-size: .86rem; font-weight: 650; cursor: pointer; }
.ovk-chip input:checked + span { border-color: color-mix(in srgb, var(--ovk-accent) 55%, transparent); background: color-mix(in srgb, var(--ovk-accent) 14%, transparent); color: var(--ovk-text); }
.ovk-chip input:checked + span { border-color: color-mix(in srgb, var(--ovk-accent) 55%, transparent); background: color-mix(in srgb, var(--ovk-accent) 14%, transparent); color: var(--ovk-text); }
-.ovk-save { position: sticky; bottom: var(--ovk-bottom); z-index: 30; margin: 0; padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px)); background: linear-gradient(transparent, var(--ovk-bg) 35%); }
+.ovk-save { position: sticky; bottom: var(--ovk-bottom); translate: 0 calc(-1 * var(--ovb-lift, 0px)); z-index: 30; margin: 0; padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px)); background: linear-gradient(transparent, var(--ovk-bg) 35%); }
.ovk-save .ovk-btn { width: 100%; }
.ovk-save .ovk-btn { width: 100%; }
.ovk-form { display: grid; gap: 12px; margin: 0; }
.ovk-form { display: grid; gap: 12px; margin: 0; }
.ovk-form label { display: grid; gap: 5px; color: var(--ovk-muted); font-size: .9rem; }
.ovk-form label { display: grid; gap: 5px; color: var(--ovk-muted); font-size: .9rem; }
@@ -242,7 +351,7 @@ div.ovk-set-row { cursor: default; }
@@ -242,7 +351,7 @@ div.ovk-set-row { cursor: default; }
.ovk-go { display: inline-grid; place-items: center; flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; background: var(--ovk-fill); color: var(--ovk-on-fill); cursor: pointer; }
.ovk-go { display: inline-grid; place-items: center; flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; background: var(--ovk-fill); color: var(--ovk-on-fill); cursor: pointer; }
.ovk-go .ovk-i { width: 18px; height: 18px; stroke-width: 2.4; }
.ovk-go .ovk-i { width: 18px; height: 18px; stroke-width: 2.4; }
.ovk-go--send { width: var(--ovk-tap); height: var(--ovk-tap); }
.ovk-go--send { width: var(--ovk-tap); height: var(--ovk-tap); }
-.ovk-dock { position: sticky; bottom: var(--ovk-bottom); z-index: 30; display: flex; align-items: flex-end; gap: 6px; margin: 8px -16px 0; padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); background: color-mix(in srgb, var(--ovk-bg) 92%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
+.ovk-dock { position: sticky; bottom: var(--ovk-bottom); translate: 0 calc(-1 * var(--ovb-lift, 0px)); z-index: 30; display: flex; align-items: flex-end; gap: 6px; margin: 8px -16px 0; padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px)); background: color-mix(in srgb, var(--ovk-bg) 92%, transparent); -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px); }
.ovk-compose { flex: 1; min-width: 0; display: flex; align-items: flex-end; gap: 8px; margin: 0; }
.ovk-compose { flex: 1; min-width: 0; display: flex; align-items: flex-end; gap: 8px; margin: 0; }
.ovk-compose textarea { flex: 1; min-width: 0; min-height: var(--ovk-tap); max-height: 40vh; box-sizing: border-box; margin: 0; padding: 10px 16px; border-radius: 22px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; line-height: 1.4; resize: none; field-sizing: content; }
.ovk-compose textarea { flex: 1; min-width: 0; min-height: var(--ovk-tap); max-height: 40vh; box-sizing: border-box; margin: 0; padding: 10px 16px; border-radius: 22px; border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font: inherit; font-size: 1rem; line-height: 1.4; resize: none; field-sizing: content; }
.ovk-compose textarea:focus-visible { outline: none; border-color: var(--ovk-accent); }
.ovk-compose textarea:focus-visible { outline: none; border-color: var(--ovk-accent); }
@@ -275,14 +384,18 @@ div.ovk-set-row { cursor: default; }
@@ -275,14 +384,18 @@ div.ovk-set-row { cursor: default; }
.ovk-empty > .ovk-i { width: 28px; height: 28px; }
.ovk-empty > .ovk-i { width: 28px; height: 28px; }
.ovk-empty.ovk-empty p { margin: 0; max-width: 46ch; }
.ovk-empty.ovk-empty p { margin: 0; max-width: 46ch; }
.ovk-empty .ovk-empty-title { color: var(--ovk-text); font-weight: 750; }
.ovk-empty .ovk-empty-title { color: var(--ovk-text); font-weight: 750; }
-.ovk-notice { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 12px; padding: 10px 14px; border-radius: calc(var(--ov-radius-1) + 2px); border: 1px solid var(--ovk-line); background: var(--ovk-panel); color: var(--ovk-text); font-size: .92rem; }
-.ovk-notice > .ovk-i { margin-top: 2px; }
-.ovk-notice--ok { border-color: color-mix(in srgb, var(--ovk-ok) 45%, transparent); }
-.ovk-notice--ok > .ovk-i { color: var(--ovk-ok-text); }
-.ovk-notice--error { border-color: color-mix(in srgb, var(--ovk-bad) 50%, transparent); }
-.ovk-notice--error > .ovk-i { color: var(--ovk-bad-text); }
-.ovk-notice--warn > .ovk-i { color: var(--ovk-warn-text); }
-.ovk-notice--info > .ovk-i { color: var(--ovk-accent); }
+/* A tone is a lean and a glow, never a coloured bar (4.196.0; the owner, 2026-11-10: "instead of just like this red and
+ crappy border … shadow, glow … something nice. Also add some icons"). A notice leans toward its tone from the side its
+ icon is on, carries a hairline tinted with it and a soft glow in it; its icon sits in a round chip of the tone. The same
+ three properties are a class of their own (.ovk-tone, with data-tone) for a card, a toast or a row that must say
+ "good", "careful" or "wrong" without a stripe. */
+.ovk-notice, .ovk-tone { --ovk-tone: var(--ovk-accent); --ovk-tone-text: var(--ovk-accent); border: 1px solid color-mix(in srgb, var(--ovk-tone) 32%, var(--ovk-line)); background: linear-gradient(100deg, color-mix(in srgb, var(--ovk-tone) 13%, var(--ovk-panel)) 0, var(--ovk-panel) 58%); box-shadow: 0 12px 28px -20px var(--ovk-tone); }
+.ovk-notice--ok, .ovk-tone[data-tone="good"] { --ovk-tone: var(--ovk-ok); --ovk-tone-text: var(--ovk-ok-text); }
+.ovk-notice--error, .ovk-tone[data-tone="alert"] { --ovk-tone: var(--ovk-bad); --ovk-tone-text: var(--ovk-bad-text); }
+.ovk-notice--warn, .ovk-tone[data-tone="warning"] { --ovk-tone: var(--ovk-warn); --ovk-tone-text: var(--ovk-warn-text); }
+.ovk-notice { display: flex; align-items: flex-start; gap: 11px; margin: 0 0 12px; padding: 10px 14px 10px 10px; border-radius: calc(var(--ov-radius-1) + 4px); color: var(--ovk-text); font-size: .92rem; }
+.ovk-notice > .ovk-i { flex: none; box-sizing: border-box; width: 26px; height: 26px; margin: -2px 0; padding: 5px; border-radius: 50%; background: color-mix(in srgb, var(--ovk-tone) 20%, transparent); color: var(--ovk-tone-text); }
+.ovk-notice > span { align-self: center; min-width: 0; }
.ovk-snip { position: relative; }
.ovk-snip { position: relative; }
.ovk-snip > .ovk-icon-btn { position: absolute; top: 6px; right: 6px; z-index: 1; width: 34px; height: 34px; background: var(--ovk-panel); border-color: var(--ovk-line); }
.ovk-snip > .ovk-icon-btn { position: absolute; top: 6px; right: 6px; z-index: 1; width: 34px; height: 34px; background: var(--ovk-panel); border-color: var(--ovk-line); }
.ovk-snip > pre { max-height: 50vh; margin: 0 0 10px; padding: 12px 48px 12px 12px; overflow: auto; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font-size: .84rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.ovk-snip > pre { max-height: 50vh; margin: 0 0 10px; padding: 12px 48px 12px 12px; overflow: auto; border-radius: var(--ov-radius-1); border: 1px solid var(--ovk-line); background: var(--ovk-input); color: var(--ovk-text); font-size: .84rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
@@ -312,7 +425,6 @@ div.ovk-set-row { cursor: default; }
@@ -312,7 +425,6 @@ div.ovk-set-row { cursor: default; }
.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.ovk-split { grid-template-columns: minmax(0, 1fr) 320px; align-items: start; }
.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }
.ovk-split > .ovk-side { position: sticky; top: calc(var(--ovk-top) + 12px); }
}
}
-@media (prefers-reduced-motion: reduce) { .ovk-switch, .ovk-switch::after, .ovk-fold > summary > .ovk-i, .ovk-head, .ovk-btn { transition: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
@media print { .ovk-head, .ovk-dock, .ovk-save { position: static; background: none; -webkit-backdrop-filter: none; backdrop-filter: none; } }
/* Touch (4.46.0, the fleet tap audit): small buttons are 44px tall on a touch screen, the find bar's go button reaches
/* Touch (4.46.0, the fleet tap audit): small buttons are 44px tall on a touch screen, the find bar's go button reaches
@@ -370,12 +482,11 @@ div.ovk-set-row { cursor: default; }
@@ -370,12 +482,11 @@ div.ovk-set-row { cursor: default; }
.ovk-sidebar-foot { padding-top: 8px; border-top: 1px solid var(--ovk-line); }
.ovk-sidebar-foot { padding-top: 8px; border-top: 1px solid var(--ovk-line); }
@media (max-width: 760px) {
@media (max-width: 760px) {
.ovk-with-sidebar { grid-template-columns: minmax(0, 1fr); }
.ovk-with-sidebar { grid-template-columns: minmax(0, 1fr); }
- .ovk-sidebar { position: fixed; z-index: 10060; top: 0; left: 0; bottom: 0; height: auto; width: min(320px, 86vw); padding-top: max(12px, env(safe-area-inset-top, 0px)); padding-bottom: max(10px, env(safe-area-inset-bottom, 0px)); border-right: 0; translate: -100% 0; visibility: hidden; transition: translate .25s ease, visibility 0s linear .25s; box-shadow: 12px 0 32px rgba(0, 0, 0, .25); }
- .ovk-sidebar.is-open { translate: 0 0; visibility: visible; transition: translate .25s ease; }
+ .ovk-sidebar { position: fixed; z-index: 10060; top: 0; left: 0; bottom: 0; height: auto; width: min(320px, 86vw); padding-top: max(12px, env(safe-area-inset-top, 0px)); padding-bottom: max(10px, env(safe-area-inset-bottom, 0px)); border-right: 0; translate: calc(-100% * var(--ov-move, 1)) 0; opacity: 0; visibility: hidden; transition: translate .25s ease, opacity .2s ease, visibility 0s linear .25s; box-shadow: 12px 0 32px rgba(0, 0, 0, .25); }
+ .ovk-sidebar.is-open { translate: 0 0; opacity: 1; visibility: visible; transition: translate .25s ease, opacity .2s ease; }
.ovk-sidebar-close, .ovk-sidebar-open { display: inline-grid; }
.ovk-sidebar-close, .ovk-sidebar-open { display: inline-grid; }
.ovk-sidebar-scrim { position: fixed; inset: 0; z-index: 10050; background: rgba(0, 0, 0, .4); }
.ovk-sidebar-scrim { position: fixed; inset: 0; z-index: 10050; background: rgba(0, 0, 0, .4); }
}
}
-@media (prefers-reduced-motion: reduce) { .ovk-sidebar, .ovk-sidebar.is-open { transition: none; } .ovk-sidebar-dot.is-busy { animation: none; } }
/* ── Scroll areas (4.69.0; the owner, 2026-11-01: "you can't scroll down the page even when you're at the bottom of
/* ── Scroll areas (4.69.0; the owner, 2026-11-01: "you can't scroll down the page even when you're at the bottom of
the chat… this should be consistent across websites"). The network's rule: a scroll area inside a page that scrolls
the chat… this should be consistent across websites"). The network's rule: a scroll area inside a page that scrolls
@@ -412,16 +523,14 @@ div.ovk-set-row { cursor: default; }
@@ -412,16 +523,14 @@ div.ovk-set-row { cursor: default; }
.ovk-skel-dot { flex: none; width: 22px; height: 22px; border-radius: 50%; }
.ovk-skel-dot { flex: none; width: 22px; height: 22px; border-radius: 50%; }
.ovk-skel-stat { display: grid; gap: 6px; padding: 12px; border-radius: var(--ov-radius-2); border: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovk-skel-stat { display: grid; gap: 6px; padding: 12px; border-radius: var(--ov-radius-2); border: 1px solid var(--border, rgba(255,255,255,.08)); }
.ovk-skel-num { height: 22px; width: 48%; }
.ovk-skel-num { height: 22px; width: 48%; }
-@media (prefers-reduced-motion: reduce) { .ovk-skel { animation: none; } }
-html[data-ov-motion=reduced] .ovk-skel { animation: none; }
/* ── Moving between pages (4.70.0, plan L52) ── A server-rendered site's pages cross-fade while the navbar and the app bar
/* ── Moving between pages (4.70.0, plan L52) ── A server-rendered site's pages cross-fade while the navbar and the app bar
- stay put (cross-document view transitions; a browser without them just loads the page). Off under reduced motion. */
+ stay put (cross-document view transitions; a browser without them just loads the page). A cross-fade moves nothing,
+ so Calm keeps it; with motion off the loader gives it no length. */
@view-transition { navigation: auto; }
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .18s; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .18s; }
#navbar-mount, .openvibe-navbar { view-transition-name: ov-navbar; }
#navbar-mount, .openvibe-navbar { view-transition-name: ov-navbar; }
.ovnav-appbar { view-transition-name: ov-appbar; }
.ovnav-appbar { view-transition-name: ov-appbar; }
-@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }
/* Content labels (L1): quiet chips beside a title, the adult one in the warning colour. They say what a thing carries;
/* Content labels (L1): quiet chips beside a title, the adult one in the warning colour. They say what a thing carries;
each viewer's own choice (ov-labels.js) decides whether it shows, blurs or hides. */
each viewer's own choice (ov-labels.js) decides whether it shows, blurs or hides. */
@@ -429,3 +538,43 @@ html[data-ov-motion=reduced] .ovk-skel { animation: none; }
@@ -429,3 +538,43 @@ html[data-ov-motion=reduced] .ovk-skel { animation: none; }
.ovk-label { display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px; border-radius: 999px; font-size: .7rem; font-weight: 700; letter-spacing: .02em;
.ovk-label { display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px; border-radius: 999px; font-size: .7rem; font-weight: 700; letter-spacing: .02em;
color: var(--text-secondary, #a8b3c7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); border: 1px solid var(--border, rgba(255,255,255,.1)); }
color: var(--text-secondary, #a8b3c7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 7%, transparent); border: 1px solid var(--border, rgba(255,255,255,.1)); }
.ovk-label--adult { color: var(--warning-text, #fbbf24); border-color: color-mix(in srgb, var(--warning-text, #fbbf24) 45%, transparent); background: color-mix(in srgb, var(--warning-text, #fbbf24) 9%, transparent); }
.ovk-label--adult { color: var(--warning-text, #fbbf24); border-color: color-mix(in srgb, var(--warning-text, #fbbf24) 45%, transparent); background: color-mix(in srgb, var(--warning-text, #fbbf24) 9%, transparent); }
+
+/* "Ask your agent" (4.134.0): the chip that hands this page's question to your Vibe in the Dock. */
+.ovk-ask { border-color: color-mix(in srgb, var(--accent, #3b82f6) 45%, var(--border, #2a3345)); }
+.ovk-ask .ovk-i { color: var(--accent-light, var(--accent, #60a5fa)); }
+.ovk-ask:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
+
+/* A phone's drawer holds the page still: the page cannot scroll behind it, the drawer scrolls on its own and never hands
+ the scroll on to the page, and the scrim takes no scroll gestures (11-09). */
+@media (max-width: 760px) {
+ html.ovk-drawer-open, html.ovk-drawer-open body { overflow: hidden; overscroll-behavior: none; }
+ .ovk-sidebar { overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
+ .ovk-sidebar-scrim { touch-action: none; overscroll-behavior: contain; }
+}
+/* A page's head (kit.pageHead; designed 11-09, the owner: "less websitey… modern… an icon system"): a small named way
+ back, the page's icon tile and name with its actions, and one line about it, whole (one copy: readers and search see
+ the same words; nothing behind an "About this"). */
+.ovk-page-head { margin: 0 0 20px; min-width: 0; }
+.ovk-page-back { position: relative; display: inline-flex; align-items: center; gap: 2px; height: 32px; margin: 0 0 12px; padding: 0 12px 0 8px;
+ border-radius: 999px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); color: var(--text-secondary, #a8b3c4);
+ font-size: 13.5px; font-weight: 650; text-decoration: none; transition: background .15s, color .15s; }
+.ovk-page-back::after { content: ""; position: absolute; inset: -6px -4px; } /* 44 px under the finger */
+.ovk-page-back .ovk-i { width: 18px; height: 18px; }
+@media (hover: hover) { .ovk-page-back:hover { background: color-mix(in srgb, var(--text-primary, #e8ecf3) 11%, transparent); color: var(--text-primary, #e8ecf3); } }
+/* The icon and the actions sit on the title's FIRST line (centred on it), so a title that wraps to three lines (a home
+ page's hero, Space 11-09) keeps them beside its start instead of floating in the middle. --ovk-page-line is the
+ title's line (1.15 × its size, in rem and vw so it means the same on every child). */
+.ovk-page-head-row { --ovk-page-line: calc(1.15 * clamp(1.45rem, 2.4vw + .7rem, 2.2rem)); display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
+.ovk-page-head-row > .ovk-heading-icon, .ovk-page-head-row > .ovk-page-actions { margin-top: calc(var(--ovk-page-line) / 2 - 22px); }
+.ovk-page-head-row > .ovk-heading-icon { flex-basis: 44px; width: 44px; height: 44px; border-radius: 13px;
+ background: linear-gradient(150deg, color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent), color-mix(in srgb, var(--accent, #3b82f6) 9%, transparent));
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); }
+.ovk-page-head-row > .ovk-heading-icon > .ovk-i { width: 24px; height: 24px; }
+.ovk-page-head-row h1 { flex: 1 1 auto; min-width: 0; margin: 0; font-size: clamp(1.45rem, 2.4vw + .7rem, 2.2rem); line-height: 1.15; letter-spacing: -.015em; overflow-wrap: anywhere; }
+.ovk-page-actions { display: flex; align-items: center; gap: 6px; flex: none; margin-left: auto; }
+.ovk-page-actions .ovk-icon-btn { width: 44px; height: 44px; border-radius: 50%; }
+.ovk-page-lede { max-width: 68ch; margin: 10px 0 0; color: var(--text-secondary, #a8b3c4); font-size: 15.5px; line-height: 1.5; }
+.ovk-page-back:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 3px; }
+@media (max-width: 760px) { .ovk-page-head { margin-bottom: 16px; } .ovk-page-lede { margin-top: 8px; font-size: 14.5px; } }
+/* A phone hero's 1fr track shrinks around the head. */
+.sc-hero-main:has(>.ovk-page-head),.sc-room-main:has(>.ovk-page-head){min-width:0}
@@ -49,7 +49,10 @@
@@ -49,7 +49,10 @@
stream: 'Live', social: 'Social', chat: 'Chat', game: 'Games', economy: 'Money & deals',
stream: 'Live', social: 'Social', chat: 'Chat', game: 'Games', economy: 'Money & deals',
achievement: 'Achievements', service: 'Services', moderation: 'Moderation', system: 'System', admin: 'Announcements',
achievement: 'Achievements', service: 'Services', moderation: 'Moderation', system: 'System', admin: 'Announcements',
};
};
- const SERVICE_LABELS = { live: 'Live', tools: 'Tools', games: 'Games', media: 'Media', network: 'Network', host: 'Host', deals: 'Deals', coupons: 'Coupons', trade: 'Trade', chat: 'Chat' };
+ const SERVICE_LABELS = {
+ live: 'Live', tools: 'Tools', games: 'Games', media: 'Media', network: 'Network', host: 'Host', deals: 'Deals', coupons: 'Coupons', trade: 'Trade', chat: 'Chat',
+ actor: 'Agents', watch: 'Watch', connect: 'Connect', link: 'Link', community: 'Community', billing: 'Wallet', blog: 'Blog', design: 'Design', ai: 'AI', events: 'Events',
+ };
// Line glyphs (24×24, stroked like the open-loop mark): one per kind of notice.
// Line glyphs (24×24, stroked like the open-loop mark): one per kind of notice.
const GLYPH = {
const GLYPH = {
@@ -70,7 +73,12 @@
@@ -70,7 +73,12 @@
bellSolid: '<path d="M5.8 16.2V11a6.2 6.2 0 0 1 12.4 0v5.2l1.7 2.3H4.1ZM9.9 19.6h4.2a2.1 2.1 0 0 1-4.2 0Z"/>',
bellSolid: '<path d="M5.8 16.2V11a6.2 6.2 0 0 1 12.4 0v5.2l1.7 2.3H4.1ZM9.9 19.6h4.2a2.1 2.1 0 0 1-4.2 0Z"/>',
key: '<circle cx="8.2" cy="14.8" r="4"/><path d="M11.1 12l8.2-8.2M16.4 6.7l2.3 2.3M14.2 8.9l1.8 1.8"/>',
key: '<circle cx="8.2" cy="14.8" r="4"/><path d="M11.1 12l8.2-8.2M16.4 6.7l2.3 2.3M14.2 8.9l1.8 1.8"/>',
spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',
spark: '<path d="M12 3.5l1.9 5.1 5.1 1.9-5.1 1.9-1.9 5.1-1.9-5.1L5 10.5l5.1-1.9z"/><path d="M18.4 15.6l.6 1.6 1.6.6-1.6.6-.6 1.6-.6-1.6-1.6-.6 1.6-.6z"/>',
+ agent: '<rect x="5" y="8.2" width="14" height="10.8" rx="3.6"/><path d="M12 8.2V5.4M9.4 13.4h.01M14.6 13.4h.01M3 12.6v2.6M21 12.6v2.6"/><circle cx="12" cy="4.4" r="1"/>',
+ eye: '<path d="M2.8 12s3.4-6.2 9.2-6.2 9.2 6.2 9.2 6.2-3.4 6.2-9.2 6.2S2.8 12 2.8 12z"/><circle cx="12" cy="12" r="2.8"/>',
+ screen: '<rect x="3.5" y="4.5" width="17" height="11.5" rx="2.2"/><path d="M9 20h6M12 16v4"/>',
};
};
+ const SERVICE_GLYPH = { actor: 'agent', watch: 'eye', connect: 'screen', link: 'screen', billing: 'coin' };
+ const TYPE_GLYPH = { WELCOME: 'spark', AGENT_TASK_DONE: 'agent', AGENT_TASK_FAILED: 'agent', AGENT_NEEDS_YOU: 'agent' };
const CATEGORY_GLYPH = { stream: 'live', social: 'people', chat: 'chat', game: 'game', economy: 'coin', achievement: 'trophy', moderation: 'shield', system: 'gear', service: 'grid', admin: 'bell' };
const CATEGORY_GLYPH = { stream: 'live', social: 'people', chat: 'chat', game: 'game', economy: 'coin', achievement: 'trophy', moderation: 'shield', system: 'gear', service: 'grid', admin: 'bell' };
const TONE = {
const TONE = {
live: 'var(--live-red, #e5484d)', danger: 'var(--live-red, #e5484d)', warn: 'var(--warning, #f5a524)', ok: 'var(--success, #30a46c)',
live: 'var(--live-red, #e5484d)', danger: 'var(--live-red, #e5484d)', warn: 'var(--warning, #f5a524)', ok: 'var(--success, #30a46c)',
@@ -92,6 +100,7 @@
@@ -92,6 +100,7 @@
let _pollTimer = null;
let _pollTimer = null;
let _lastSeenAt = null; // newest created_at we have toasted / seen
let _lastSeenAt = null; // newest created_at we have toasted / seen
let _unreadCount = 0;
let _unreadCount = 0;
+ let _msgs = 0; // new messages (navbar.js setMessages): on the bell's badge with the notifications, never in the list's own count
let _toastContainer = null;
let _toastContainer = null;
let _panelEl = null;
let _panelEl = null;
let _bellEl = null;
let _bellEl = null;
@@ -196,6 +205,7 @@
@@ -196,6 +205,7 @@
.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }
.openvibe-notif-dock { display: flex; flex-direction: column; min-height: 0; flex: 1; color: var(--text-primary, #ececf1); }
.openvibe-notif-dock .openvibe-nv-head { display: none; }
.openvibe-notif-dock .openvibe-nv-head { display: none; }
.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }
.openvibe-notif-dock .openvibe-nv-tools { padding-top: 10px; }
+.openvibe-notif-dock.is-preview .openvibe-nv-tools, .openvibe-notif-dock.is-preview .openvibe-nv-foot, .openvibe-notif-dock.is-preview .openvibe-nv-head, .openvibe-notif-dock.is-preview .openvibe-nv-more { display: none; }
.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }
.openvibe-notif-dock .openvibe-nv-foot { gap: 12px; }
.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }
.openvibe-notif-dock .openvibe-nv-foot [data-role="status"] { margin-right: auto; }
.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }
.ovn-foot-read { display: inline-flex; align-items: center; gap: 6px; min-height: 32px; padding: 0; border: 0; background: none; font: inherit; font-weight: 700; color: var(--text-secondary, #b6b6c0); cursor: pointer; }
@@ -231,6 +241,7 @@
@@ -231,6 +241,7 @@
.openvibe-nv-unread svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.openvibe-nv-unread svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.openvibe-nv-tabs { display: flex; overflow-x: auto; scrollbar-width: none; margin: 0 -12px; padding: 0 6px; }
.openvibe-nv-tabs { display: flex; overflow-x: auto; scrollbar-width: none; margin: 0 -12px; padding: 0 6px; }
.openvibe-nv-tabs::-webkit-scrollbar { display: none; }
.openvibe-nv-tabs::-webkit-scrollbar { display: none; }
+ .openvibe-nv-tabs.is-more { -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent); }
.openvibe-nv-tabs button { flex: none; position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 0 10px; border: 0; background: transparent; color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }
.openvibe-nv-tabs button { flex: none; position: relative; display: inline-flex; align-items: center; gap: 6px; min-height: 40px; padding: 0 10px; border: 0; background: transparent; color: var(--text-secondary, #b6b6c0); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap; }
.openvibe-nv-tabs button:hover, .openvibe-nv-tabs button[aria-selected="true"] { color: var(--text-primary, #ececf1); }
.openvibe-nv-tabs button:hover, .openvibe-nv-tabs button[aria-selected="true"] { color: var(--text-primary, #ececf1); }
.openvibe-nv-tabs button[aria-selected="true"]::after { content: ''; position: absolute; left: 10px; right: 10px; bottom: 0; height: 2.5px; border-radius: 2px 2px 0 0; background: var(--accent, #3b82f6); }
.openvibe-nv-tabs button[aria-selected="true"]::after { content: ''; position: absolute; left: 10px; right: 10px; bottom: 0; height: 2.5px; border-radius: 2px 2px 0 0; background: var(--accent, #3b82f6); }
@@ -304,12 +315,7 @@
@@ -304,12 +315,7 @@
}
}
html.ovn-sheet-open, html.ovn-sheet-open body { overflow: hidden; }
html.ovn-sheet-open, html.ovn-sheet-open body { overflow: hidden; }
@media (max-height: 520px) { .openvibe-notif-item .msg { -webkit-line-clamp: 1; } }
@media (max-height: 520px) { .openvibe-notif-item .msg { -webkit-line-clamp: 1; } }
- @media (prefers-reduced-motion: reduce) {
- .openvibe-toast, .ovn-phone .openvibe-toast, .openvibe-notif-panel { animation: none; }
- .openvibe-toast.removing { animation: none; opacity: 0; }
- .openvibe-bell.ringing svg, .openvibe-bell .badge { animation: none; }
- .openvibe-toast-progress { display: none; }
- }
+ html[data-ov-motion=off] .openvibe-toast.removing{opacity: 0}html[data-ov-motion=off] .openvibe-toast-progress{display: none}
`;
`;
document.head.appendChild(style);
document.head.appendChild(style);
}
}
@@ -407,17 +413,54 @@
@@ -407,17 +413,54 @@
label: 'System', url: rich.runbook_url || urlOf(n), actionLabel: 'Open status',
label: 'System', url: rich.runbook_url || urlOf(n), actionLabel: 'Open status',
};
};
}
}
+ // JSON in a message (a watch sends a feed entry, cut short): its title and summary, never code.
+ function readableMessage(raw) {
+ const s = String(raw || '').trim();
+ if (!/^[{[]\s*"/.test(s)) return s;
+ const field = (name) => {
+ const m = new RegExp(`"${name}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)`).exec(s);
+ if (!m) return '';
+ try { return JSON.parse(`"${m[1].replace(/\\$/, '')}"`); } catch { return m[1]; }
+ };
+ const title = field('title') || field('name');
+ const summary = field('summary') || field('description');
+ return [title, summary].filter(Boolean).join(' — ').replace(/\s+/g, ' ').trim();
+ }
+ const cap = (s) => (s ? s.charAt(0).toUpperCase() + s.slice(1) : s);
+ // What kind of step it is, as a label (a noun, so it never reads as a button).
+ const ASK_CHIP = { approve: 'Approval', confirm: 'Approval', answer: 'Question', choose: 'Choice', pick: 'Choice', review: 'Review', 'sign in': 'Sign-in', 'log in': 'Sign-in' };
+ // An agent's notice: the task, not its wording ("Done: x" → x; "approve: step (task)" → step + chip).
+ function agentText(n, rich) {
+ const t = String(n.type || '');
+ let msg = String(n.message || '').trim();
+ const chips = [];
+ if (t === 'AGENT_TASK_DONE') msg = rich.task_title || msg.replace(/^Done:\s*/i, '');
+ else if (t === 'AGENT_TASK_FAILED') msg = rich.task_title ? `${rich.task_title}${rich.detail ? ` — ${rich.detail}` : ''}` : msg.replace(/^Not finished:\s*/i, '');
+ else if (t === 'AGENT_NEEDS_YOU') {
+ if (rich.step) msg = String(rich.step);
+ else if (/\)$/.test(msg) && msg.lastIndexOf(' (') > 0) msg = msg.slice(0, msg.lastIndexOf(' ('));
+ const m = /^(approve|answer|confirm|choose|pick|review|sign in|log in)\s*:\s*/i.exec(msg);
+ const k = (m ? m[1] : String(rich.kind || '')).toLowerCase();
+ if (m) msg = msg.slice(m[0].length);
+ if (ASK_CHIP[k]) chips.push(ASK_CHIP[k]);
+ }
+ return { message: cap(msg.trim()), chips };
+ }
function present(n) {
function present(n) {
const rich = richOf(n);
const rich = richOf(n);
if (isOpsAlert(n, rich)) return presentOps(n, rich);
if (isOpsAlert(n, rich)) return presentOps(n, rich);
const cat = n.category || 'system';
const cat = n.category || 'system';
const t = String(n.type || '');
const t = String(n.type || '');
- const glyph = /DEAL|COUPON|PRICE|TRADE/.test(t) ? 'tag' : /CONFIRM|APPROV|SECUR|SIGN/.test(t) ? 'key' : (CATEGORY_GLYPH[cat] || 'bell');
- const tone = n.priority === 'critical' ? 'danger' : (CATEGORY_TONE[cat] || 'accent');
- const chips = Array.isArray(rich.chips) ? rich.chips.filter(c => typeof c === 'string').slice(0, 4) : [];
+ const agent = /^AGENT_/.test(t);
+ const glyph = /DEAL|COUPON|PRICE|TRADE/.test(t) ? 'tag' : /CONFIRM|APPROV|SECUR|SIGN/.test(t) ? 'key'
+ : (TYPE_GLYPH[t] || SERVICE_GLYPH[n.service] || CATEGORY_GLYPH[cat] || 'bell');
+ const tone = n.priority === 'critical' ? 'danger'
+ : t === 'AGENT_TASK_DONE' ? 'ok' : t === 'AGENT_TASK_FAILED' ? 'warn' : (CATEGORY_TONE[cat] || 'accent');
+ const said = agent ? agentText(n, rich) : { message: readableMessage(n.message), chips: [] };
+ const chips = said.chips.concat(Array.isArray(rich.chips) ? rich.chips.filter(c => typeof c === 'string') : []).slice(0, 4);
return {
return {
- kind: 'plain', key: [t, n.title || '', n.service || '', cat].join('\u0001'), tone, glyph,
- title: String(n.title || ''), message: String(n.message || ''), chips, severity: null, command: null,
+ kind: 'plain', key: [t, n.title || '', n.service || '', cat, rich.task_id || ''].join('\u0001'), tone, glyph,
+ title: String(n.title || ''), message: said.message, chips, severity: null, command: null,
label: SERVICE_LABELS[n.service] || (n.service && n.service !== 'network' ? n.service : '') || CATEGORY_LABELS[cat] || '',
label: SERVICE_LABELS[n.service] || (n.service && n.service !== 'network' ? n.service : '') || CATEGORY_LABELS[cat] || '',
url: urlOf(n), actionLabel: null,
url: urlOf(n), actionLabel: null,
};
};
@@ -509,6 +552,9 @@
@@ -509,6 +552,9 @@
if (msg.type === 'unread') setBadge(msg.count, { quiet: true });
if (msg.type === 'unread') setBadge(msg.count, { quiet: true });
else if (msg.type === 'read') { for (const v of _views.values()) applyReadLocally(v, msg.ids || [], msg.all); }
else if (msg.type === 'read') { for (const v of _views.values()) applyReadLocally(v, msg.ids || [], msg.all); }
}
}
+ function announceCount() {
+ try { document.dispatchEvent(new CustomEvent('openvibe-notifications-count', { detail: { count: _unreadCount } })); } catch { /* */ }
+ }
// ── Sound ───────────────────────────────────────────────
// ── Sound ───────────────────────────────────────────────
function playSound(priority) {
function playSound(priority) {
@@ -784,33 +830,43 @@
@@ -784,33 +830,43 @@
}
}
// ── Bell / badge ────────────────────────────────────────
// ── Bell / badge ────────────────────────────────────────
+ let _bellAction = null;
function createBellEl() {
function createBellEl() {
const bell = document.createElement('div');
const bell = document.createElement('div');
bell.className = 'openvibe-bell';
bell.className = 'openvibe-bell';
bell.setAttribute('role', 'button'); bell.setAttribute('aria-label', 'Notifications'); bell.tabIndex = 0;
bell.setAttribute('role', 'button'); bell.setAttribute('aria-label', 'Notifications'); bell.tabIndex = 0;
bell.innerHTML = `<svg class="ovnav-ovg has-s" viewBox="0 0 24 24" aria-hidden="true">${GLYPH.bell}<g class="ovg-s">${GLYPH.bellSolid}</g></svg><span class="badge hidden">0</span>`;
bell.innerHTML = `<svg class="ovnav-ovg has-s" viewBox="0 0 24 24" aria-hidden="true">${GLYPH.bell}<g class="ovg-s">${GLYPH.bellSolid}</g></svg><span class="badge hidden">0</span>`;
- bell.addEventListener('click', () => togglePanel());
- bell.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePanel(); } });
+ // With the Dock on the page, its Activity tab is this list: the bell opens it there (4.150.0, the owner: the bell
+ // "doesn't actually open the notifications … you have to open the chat button and click this manually"); the
+ // panel opened under the Dock's sheet, so the tap seemed to do nothing.
+ const press = () => { if (_bellAction) { try { if (_bellAction() === true) return; } catch { /* the panel then */ } } togglePanel(); };
+ bell.addEventListener('click', press);
+ bell.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); press(); } });
_bellEl = bell;
_bellEl = bell;
return bell;
return bell;
}
}
+ // The bell's own number: notifications and new messages together, each named in its label.
+ function paintBell(ring) {
+ if (!_bellEl) return;
+ const badge = _bellEl.querySelector('.badge'), n = _unreadCount + _msgs;
+ if (n > 0) {
+ badge.textContent = n > 99 ? '99+' : String(n);
+ badge.classList.remove('hidden');
+ if (ring) { _bellEl.classList.add('ringing'); setTimeout(() => _bellEl.classList.remove('ringing'), 700); }
+ } else badge.classList.add('hidden');
+ const parts = [_unreadCount ? `${_unreadCount} unread` : '', _msgs ? `${_msgs} new ${_msgs === 1 ? 'message' : 'messages'}` : ''].filter(Boolean);
+ _bellEl.setAttribute('aria-label', parts.length ? `Notifications, ${parts.join(', ')}` : 'Notifications');
+ }
function setBadge(count, { quiet = false } = {}) {
function setBadge(count, { quiet = false } = {}) {
const prev = _unreadCount;
const prev = _unreadCount;
_unreadCount = Math.max(0, Number(count) || 0);
_unreadCount = Math.max(0, Number(count) || 0);
- if (_bellEl) {
- const badge = _bellEl.querySelector('.badge');
- if (_unreadCount > 0) {
- badge.textContent = _unreadCount > 99 ? '99+' : String(_unreadCount);
- badge.classList.remove('hidden');
- if (!quiet && _unreadCount > prev) { _bellEl.classList.add('ringing'); setTimeout(() => _bellEl.classList.remove('ringing'), 700); }
- } else badge.classList.add('hidden');
- _bellEl.setAttribute('aria-label', _unreadCount ? `Notifications, ${_unreadCount} unread` : 'Notifications');
- }
+ paintBell(!quiet && _unreadCount > prev);
for (const v of _views.values()) {
for (const v of _views.values()) {
const c = v.root.querySelector('.openvibe-nv-head .count');
const c = v.root.querySelector('.openvibe-nv-head .count');
if (c) { c.textContent = _unreadCount > 99 ? '99+' : String(_unreadCount); c.classList.toggle('zero', _unreadCount === 0); }
if (c) { c.textContent = _unreadCount > 99 ? '99+' : String(_unreadCount); c.classList.toggle('zero', _unreadCount === 0); }
}
}
if (!quiet) broadcast({ type: 'unread', count: _unreadCount });
if (!quiet) broadcast({ type: 'unread', count: _unreadCount });
+ announceCount();
if (_unreadCount !== prev && listShowing()) loadTabCounts();
if (_unreadCount !== prev && listShowing()) loadTabCounts();
}
}
@@ -872,6 +928,14 @@
@@ -872,6 +928,14 @@
};
};
tabs.forEach((btn) => btn.addEventListener('click', () => selectTab(btn)));
tabs.forEach((btn) => btn.addEventListener('click', () => selectTab(btn)));
const tabRow = root.querySelector('.openvibe-nv-tabs');
const tabRow = root.querySelector('.openvibe-nv-tabs');
+ // More filters past the edge: a fade says the row scrolls (4.150.0; "System" was cut off with no sign of more).
+ if (tabRow && !tabRow.__more) {
+ tabRow.__more = true;
+ const more = () => tabRow.classList.toggle('is-more', tabRow.scrollWidth - tabRow.clientWidth - tabRow.scrollLeft > 4);
+ tabRow.addEventListener('scroll', more, { passive: true });
+ if (typeof ResizeObserver === 'function') new ResizeObserver(more).observe(tabRow);
+ setTimeout(more, 0);
+ }
const fade = () => { tabRow.classList.toggle('is-more', tabRow.scrollLeft + tabRow.clientWidth < tabRow.scrollWidth - 4); };
const fade = () => { tabRow.classList.toggle('is-more', tabRow.scrollLeft + tabRow.clientWidth < tabRow.scrollWidth - 4); };
tabRow.addEventListener('scroll', fade, { passive: true });
tabRow.addEventListener('scroll', fade, { passive: true });
v.fade = fade;
v.fade = fade;
@@ -924,6 +988,7 @@
@@ -924,6 +988,7 @@
}, true);
}, true);
attachRowSwipe(v, list);
attachRowSwipe(v, list);
refreshPushButton(v);
refreshPushButton(v);
+ v.destroy = () => { clearTimeout(qTimer); v.seq++; };
_views.set(root, v);
_views.set(root, v);
paintTabCounts();
paintTabCounts();
return v;
return v;
@@ -1038,7 +1103,7 @@
@@ -1038,7 +1103,7 @@
}
}
async function fetchPage(v, offset) {
async function fetchPage(v, offset) {
- const params = new URLSearchParams({ limit: String(PAGE_SIZE), offset: String(offset) });
+ const params = new URLSearchParams({ limit: String(v.pageSize || PAGE_SIZE), offset: String(offset) });
if (v.unread) params.set('unread_only', '1');
if (v.unread) params.set('unread_only', '1');
if (v.tab && v.tab !== 'all') params.set('tab', v.tab);
if (v.tab && v.tab !== 'all') params.set('tab', v.tab);
if (v.q) params.set('q', v.q);
if (v.q) params.set('q', v.q);
@@ -1056,6 +1121,7 @@
@@ -1056,6 +1121,7 @@
if (seq !== v.seq) return;
if (seq !== v.seq) return;
v.items = data.notifications || []; v.items.forEach(n => v.ids.add(String(n.id)));
v.items = data.notifications || []; v.items.forEach(n => v.ids.add(String(n.id)));
v.hasMore = !!data.has_more; v.total = data.total ?? null; v.offset = v.items.length;
v.hasMore = !!data.has_more; v.total = data.total ?? null; v.offset = v.items.length;
+ if (typeof v.onCount === 'function') { try { v.onCount(v.total); } catch { /* */ } }
renderItems(v);
renderItems(v);
} catch (e) {
} catch (e) {
if (seq !== v.seq) return;
if (seq !== v.seq) return;
@@ -1190,6 +1256,8 @@
@@ -1190,6 +1256,8 @@
}
}
function stopPolling() { if (_pollTimer) clearInterval(_pollTimer); _pollTimer = null; }
function stopPolling() { if (_pollTimer) clearInterval(_pollTimer); _pollTimer = null; }
+ // notification-live.js's content hash (scripts/sibling-revs.js writes it), so a release gives it a new URL.
+ const NOTIFY_REV = { 'notification-live.js': 'cd2d64cc77d0' };
// ── Realtime (notification-live.js) ─────────────────────
// ── Realtime (notification-live.js) ─────────────────────
function liveOpen() { return !!(_live && _live.state().state === 'open'); }
function liveOpen() { return !!(_live && _live.state().state === 'open'); }
/** One re-read for a burst of events (a go-live fan-out, a replay after a reconnect); a gap re-reads open lists too. */
/** One re-read for a burst of events (a go-live fan-out, a replay after a reconnect); a gap re-reads open lists too. */
@@ -1207,7 +1275,7 @@
@@ -1207,7 +1275,7 @@
function liveSrc() {
function liveSrc() {
if (_config.liveSrc) return _config.liveSrc;
if (_config.liveSrc) return _config.liveSrc;
const m = /^(https?:\/\/[^?#]*\/)notification-ui\.js(?:[?#]|$)/.exec(SELF_SRC);
const m = /^(https?:\/\/[^?#]*\/)notification-ui\.js(?:[?#]|$)/.exec(SELF_SRC);
- return `${m ? m[1] : 'https://openvibe.network/shared/'}notification-live.js`;
+ return `${m ? m[1] : 'https://openvibe.network/shared/'}notification-live.js${NOTIFY_REV['notification-live.js'] ? `?v=${NOTIFY_REV['notification-live.js']}` : ''}`;
}
}
let _liveLoading = null;
let _liveLoading = null;
function loadLive() {
function loadLive() {
@@ -1386,6 +1454,12 @@
@@ -1386,6 +1454,12 @@
_toastContainer?.remove(); _panelEl?.remove();
_toastContainer?.remove(); _panelEl?.remove();
_toastContainer = null; if (_panelEl) _views.delete(_panelEl); _panelEl = null; _queue.length = 0;
_toastContainer = null; if (_panelEl) _views.delete(_panelEl); _panelEl = null; _queue.length = 0;
},
},
+ /** What a tap on the bell does instead of its own panel: fn() returns true when it handled it (navbar.js: the Dock). */
+ setBellAction(fn) { _bellAction = typeof fn === 'function' ? fn : null; },
+ /** New messages, counted on the bell with the notifications (navbar.js): 0 takes them off. */
+ setMessages(count) { const prev = _msgs; _msgs = Math.max(0, Number(count) || 0); paintBell(_msgs > prev); },
+ /** How many notifications are unread (not counting messages). */
+ unread() { return _unreadCount; },
/** Create and inject the bell icon. Returns the DOM element. */
/** Create and inject the bell icon. Returns the DOM element. */
createBell(container) {
createBell(container) {
injectStyles();
injectStyles();
@@ -1416,6 +1490,23 @@
@@ -1416,6 +1490,23 @@
reload(v);
reload(v);
return v;
return v;
},
},
+ /** The You tab's preview (4.211.0): the newest three, no tools; Show all opens the whole list in place. */
+ renderMenuNotifications(container, onCount) {
+ injectStyles();
+ if (!container) return null;
+ container.classList.add('openvibe-notif-dock', 'is-preview');
+ const v = buildView(container, { mode: 'dock' });
+ v.pageSize = 3; v.onCount = onCount;
+ reload(v);
+ return { showAll() { container.classList.remove('is-preview'); v.pageSize = PAGE_SIZE; reload(v); } };
+ },
+ unmountDock(container) {
+ const v = _views.get(container);
+ if (!v || v.mode !== 'dock') return;
+ if (v.destroy) v.destroy();
+ _views.delete(container);
+ container.classList.remove('openvibe-notif-dock');
+ },
/** Programmatically show a toast (real-time events). Marks nothing on the server. */
/** Programmatically show a toast (real-time events). Marks nothing on the server. */
push(notification) {
push(notification) {
showToast(notification);
showToast(notification);
new file mode 100644
new file mode 100644
@@ -0,0 +1,605 @@
@@ -0,0 +1,605 @@
+/**
+ * ov-composer.js (openvibe-shared 4.201.0): one composer on every site. Ask 349, the owner: "how come on this composer
+ * there's no file attaching system and all the other features … a better composer system unified across all sites …
+ * moddable and fully customizable". ov-thread.js loads it for each thread.composer() form, and a site's own box joins
+ * with OpenVibeComposer.enhance(form). The form's markup and its submit stay the site's; this adds:
+ * - files: the + menu, a paste, a drop; each one a chip above the box (a picture's thumbnail, the name and size, ×).
+ * A long paste becomes a text file, which you can put back in the box. Files go with the form (multipart); where a
+ * mod gives upload(file, onProgress), they go ahead of it, the chip shows the progress, and the form carries the ids;
+ * - "/" for commands (the site's, its mods', your own snippets) and "@" for mentions (the mods' sources);
+ * - voice, where the browser can listen; a draft kept per box until it is sent; Ctrl/⌘+B, I, E for bold, italic and
+ * code; the count near the box's limit;
+ * - your settings (what sends, voice, compact, your snippets), from "/settings" or the slash list's last row.
+ *
+ * Mods (a site, or a script on it): OpenVibeComposer.mod({
+ * id, match(form) → bool, which boxes it joins (all when absent)
+ * tools: [{ id, label, path (24-unit path data), run(ctx) }],
+ * commands: [{ name, hint, insert } | { name, hint, run(ctx) }],
+ * mentions: { search(q) → Promise<[{ label, handle, sub, avatar, insert }]> },
+ * upload(file, onProgress) → Promise<{ id }>, files go to the site's store before the send
+ * beforeSend(ctx) → false to hold the send })
+ * ctx: { form, text, value, set(v), insert(s), files(), add(files), note(s), send() }.
+ * A box its page sends by script (a chat, an agent's run): form.__ovc.files() → the files (File objects), uploads(): the
+ * ids a mod's upload gave, clear() after the send (the text, the chips, the draft); a form 'reset' clears the same.
+ * A site without script says the same in data: data-ovt-commands='[{"name","hint","insert"}]', data-ovc-max-files,
+ * data-ovc-max-mb, data-ovc-no-draft.
+ */
+(function (root) {
+ 'use strict';
+ if (root.OpenVibeComposer || typeof document === 'undefined') return;
+
+ // 4.203.0: the composer carries its own look (it was in thread.css), so any box on any page gets it: the Dock's
+ // Vibe and Code boxes included. Injected once, before the first box is joined.
+ const CSS = `.ovt-composer { position: relative; display: grid; gap: 6px; max-width: 780px; margin: 0 auto; padding: 10px 12px; border: 1px solid var(--border, rgba(148,163,184,.18)); border-radius: 18px; background: var(--bg-card, #131c2e); color: var(--text-primary, #e6edf7); }
+.ovt-composer textarea { width: 100%; min-height: 48px; box-sizing: border-box; padding: 6px 2px; border: 0; outline: 0; resize: vertical; background: transparent; color: inherit; font: inherit; }
+.ovt-c-row { display: flex; align-items: center; gap: 8px; }
+.ovt-c-tools { flex: 1; display: flex; align-items: center; gap: 6px; }
+.ovt-send { min-height: 40px; padding: 0 14px; border: 0; border-radius: 12px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font: inherit; font-weight: 650; cursor: pointer; }
+.ovt-send:disabled { opacity: .5; }
+.ovt-composer .ovc-file.ovc-image { height: auto; min-height: 100px; flex-wrap: wrap; overflow: visible; }
+.ovt-composer .ovc-image input { flex: 1 0 100%; min-width: 0; min-height: 36px; box-sizing: border-box; border: 1px solid var(--border, rgba(148,163,184,.18)); border-radius: 8px; background: var(--bg-primary, #0b1220); color: inherit; font: inherit; }
+.ovc-files { display: flex; gap: 8px; overflow-x: auto; padding: 2px 0 8px; margin: 0 0 2px -4px; scrollbar-width: thin; }
+.ovc-files[hidden] { display: none; }
+.ovc-file { position: relative; flex: none; display: flex; align-items: center; gap: 9px; max-width: 230px; height: 52px; padding: 5px 30px 5px 5px; box-sizing: border-box; border-radius: 14px;
+ border: 1px solid var(--ovt-line, var(--border, rgba(148, 163, 184, .18))); background: color-mix(in srgb, var(--text-primary, #fff) 4%, transparent); overflow: hidden; animation: ovc-in .2s cubic-bezier(.2, .9, .3, 1) both; }
+.ovc-file.is-bad { border-color: color-mix(in srgb, var(--live-red, #ef4444) 70%, transparent); }
+.ovc-file.is-up::after { content: ""; position: absolute; left: 0; bottom: 0; height: 3px; width: calc(var(--p, 0) * 100%); background: var(--ovt-accent, var(--accent, #3b82f6)); transition: width .2s; }
+.ovc-file-th { flex: none; width: 40px; height: 40px; border-radius: 10px; object-fit: cover; background: #0b1220; }
+.ovc-file-ic { display: grid; place-items: center; background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 16%, transparent); color: var(--ovt-accent, var(--accent, #3b82f6)); font-size: 11px; font-weight: 750; letter-spacing: .02em; }
+.ovc-file-t { min-width: 0; display: grid; line-height: 1.25; }
+.ovc-file-n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 600; color: var(--text-primary, #e6edf7); }
+.ovc-file-s { font-size: 12px; color: var(--text-muted, #7386a3); white-space: nowrap; }
+.ovc-file.is-bad .ovc-file-s { color: var(--live-red, #ef4444); }
+.ovc-file-x, .ovc-file-b { position: absolute; top: 4px; right: 4px; display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 8px; background: none; color: var(--text-secondary, #b4c0d4); cursor: pointer; }
+.ovc-file-b { top: auto; bottom: 4px; }
+.ovc-file-x:hover, .ovc-file-b:hover, .ovc-file-x:focus-visible, .ovc-file-b:focus-visible { background: var(--bg-hover, color-mix(in srgb, var(--text-primary, #fff) 10%, transparent)); color: var(--text-primary, #e6edf7); }
+.ovc-file-x svg, .ovc-file-b svg { width: 15px; height: 15px; }
+.ovc-dropping { border-color: var(--ovt-accent, var(--accent, #3b82f6)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 30%, transparent), 0 8px 30px rgba(0, 0, 0, .25); }
+.ovc-dropping::before { content: "Drop to attach"; position: absolute; inset: 0; z-index: 5; display: grid; place-items: center; border-radius: inherit; background: color-mix(in srgb, var(--bg-card, #131c2e) 86%, var(--ovt-accent, var(--accent, #3b82f6))); color: var(--text-primary, #e6edf7); font-weight: 650; pointer-events: none; }
+.ovc-pop { position: absolute; z-index: 40; left: 6px; right: 6px; bottom: calc(100% + 8px); max-height: min(320px, 50vh); overflow: auto; padding: 6px; border-radius: 16px; border: 1px solid var(--ovt-line, var(--border, rgba(148, 163, 184, .18)));
+ background: var(--bg-card, #131c2e); box-shadow: 0 18px 50px rgba(0, 0, 0, .35); animation: ovc-in .16s cubic-bezier(.2, .9, .3, 1) both; }
+.ovt.is-start .ovc-pop { bottom: auto; top: calc(100% + 8px); }
+.ovc-pop[hidden] { display: none; }
+.ovc-pop-i { display: grid; grid-template-columns: 30px 1fr auto; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 6px 10px; box-sizing: border-box; border: 0; border-radius: 11px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }
+.ovc-pop-i[aria-selected="true"], .ovc-pop-i:hover { background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 14%, transparent); }
+.ovc-pop-ic, .ovc-pop-av { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--ovt-accent, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 12%, transparent); object-fit: cover; }
+.ovc-pop-av { border-radius: 50%; }
+.ovc-pop-ic svg { width: 17px; height: 17px; }
+.ovc-pop-t { min-width: 0; display: grid; line-height: 1.3; }
+.ovc-pop-t b { font-weight: 650; font-size: 14px; color: var(--text-primary, #e6edf7); }
+.ovc-pop-t span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-muted, #7386a3); }
+.ovc-pop-tag { font-size: 11px; font-weight: 650; padding: 2px 7px; border-radius: 999px; color: var(--ovt-accent, var(--accent, #3b82f6)); background: color-mix(in srgb, var(--ovt-accent, var(--accent, #3b82f6)) 12%, transparent); }
+.ovc-mic[aria-pressed="true"] { color: var(--live-red, #ef4444); background: color-mix(in srgb, var(--live-red, #ef4444) 14%, transparent); animation: ovc-listen 1.4s ease-in-out infinite; }
+.ovc-count { flex: none; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text-muted, #7386a3); }
+.ovc-count.is-low { color: var(--live-red, #ef4444); }
+.ovc-count[hidden] { display: none; }
+.ovt-composer.ovc-compact { padding: 6px 8px 6px 12px; border-radius: 18px; }
+.ovt-composer.ovc-compact .ovt-c-row { margin-top: 0; }
+.ovt-composer.ovc-compact .ovt-btn, .ovt-composer.ovc-compact .ovt-send { width: 36px; min-width: 36px; height: 36px; }
+@keyframes ovc-in { from { opacity: 0; transform: translateY(4px) scale(.98); } }
+@keyframes ovc-listen { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--live-red, #ef4444) 18%, transparent); } }
+html[data-ov-motion=off] .ovc-file, html[data-ov-motion=off] .ovc-pop, html[data-ov-motion=off] .ovc-mic { animation: none; }
+.ovc-set { width: min(460px, calc(100vw - 24px)); max-height: min(640px, calc(100vh - 32px)); padding: 0; border: 1px solid var(--ovt-line, rgba(148, 163, 184, .2)); border-radius: 20px; background: var(--bg-card, #131c2e); color: var(--text-primary, #e6edf7); box-shadow: 0 30px 80px rgba(0, 0, 0, .5); }
+.ovc-set::backdrop { background: rgba(2, 6, 14, .55); }
+.ovc-set.ovc-set-in { width: auto; max-height: none; border: 0; border-radius: 0; background: none; box-shadow: none; }
+.ovc-set-in .ovc-set-f { padding: 0; }
+.ovc-set-f { display: grid; gap: 14px; padding: 6px 18px 18px; }
+.ovc-set-h { display: flex; align-items: center; justify-content: space-between; margin: 0 -8px 0 0; font-size: 17px; }
+.ovc-set-g { margin: 0; padding: 0; border: 0; display: grid; gap: 6px; }
+.ovc-set-g legend { padding: 0 0 6px; font-weight: 650; font-size: 14px; }
+.ovc-set label { display: flex; align-items: center; gap: 10px; min-height: 40px; font-size: 14.5px; cursor: pointer; }
+.ovc-set small, .ovc-set-p { color: var(--text-muted, #7386a3); font-size: 12.5px; margin: 0; }
+.ovc-set input[type=radio], .ovc-set input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent, #3b82f6); }
+.ovc-set-l { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
+.ovc-set-l li { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 4px 4px 4px 10px; border-radius: 10px; background: color-mix(in srgb, var(--text-primary, #fff) 4%, transparent); }
+.ovc-set-l span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary, #b4c0d4); font-size: 13px; }
+.ovc-set-add { display: grid; grid-template-columns: 110px 1fr auto; gap: 8px; align-items: start; }
+.ovc-set-add input, .ovc-set-add textarea { box-sizing: border-box; width: 100%; padding: 9px 10px; border-radius: 10px; border: 1px solid var(--ovt-line, rgba(148, 163, 184, .25)); background: var(--bg-input, rgba(2, 6, 14, .35)); color: inherit; font: inherit; font-size: 14px; resize: vertical; }
+.ovc-set-btn { height: 40px; padding: 0 14px; border: 0; border-radius: 12px; background: var(--accent, #3b82f6); color: var(--on-accent, #fff); font: inherit; font-weight: 650; cursor: pointer; }
+@media (max-width: 480px) { .ovc-set-add { grid-template-columns: 1fr; } .ovc-file { max-width: 200px; } }
+/* 4.205.0: the composer's own tool buttons and row on a page without thread.css (an agent's run, a card, any box), so it
+ is the same composer everywhere. Where thread.css is loaded its rules are the same. */
+.ovc .ovt-btn { flex: none; display: grid; place-items: center; width: 40px; height: 40px; border-radius: 12px; border: 0; background: none; color: var(--text-secondary, #b4c0d4); cursor: pointer; text-decoration: none; }
+.ovc .ovt-btn:hover, .ovc .ovt-btn:focus-visible { background: var(--bg-hover, color-mix(in srgb, var(--text-primary, #fff) 6%, transparent)); color: var(--text-primary, #e6edf7); }
+.ovc .ovt-btn .ovt-ic, .ovc .ovt-btn svg { width: 22px; height: 22px; }
+.ovc .ovt-c-row { display: flex; align-items: center; gap: 6px; }
+.ovc .ovt-c-tools { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
+.ovc .ovt-sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
+`;
+ function styles() { if (document.getElementById('ovc-styles')) return; const st = document.createElement('style'); st.id = 'ovc-styles'; st.textContent = CSS; document.head.appendChild(st); }
+ const mods = [];
+ // 4.202.0: your settings open in the one window (ov-window.js: a sheet on a phone, swiped down to close), loaded
+ // from beside this file; without it, a plain <dialog>.
+ const COMPOSER_REV = { 'ov-window.js': 'd3d8f9e3e6d2' };
+ const selfSrc = (document.currentScript && document.currentScript.src) || '';
+ function needWindow() {
+ return new Promise((ok) => {
+ if (root.OVWindow) return ok(root.OVWindow);
+ const m = /^(https?:\/\/[^?#]*\/)ov-composer(?:\.[0-9a-f]{6,})?\.js(?:[?#]|$)/.exec(selfSrc);
+ if (!m) return ok(null);
+ const sc = document.createElement('script');
+ sc.src = m[1] + 'ov-window.js' + (COMPOSER_REV['ov-window.js'] ? '?v=' + COMPOSER_REV['ov-window.js'] : '');
+ sc.onload = () => ok(root.OVWindow || null); sc.onerror = () => ok(null);
+ document.head.appendChild(sc);
+ });
+ }
+ const live = new Set();
+ const PREFS_KEY = 'ov:composer';
+ const DEFAULTS = { send_key: 'enter', voice: true, density: 'comfy', snippets: [] };
+ const PATH_OK = /^[MmLlHhVvCcSsQqTtAaZz0-9 ,.\-]{1,2000}$/;
+ const Speech = root.SpeechRecognition || root.webkitSpeechRecognition || null;
+ const mac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || '');
+ const IC = {
+ mic: 'M12 3a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V6a3 3 0 0 1 3-3zM5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21',
+ x: 'M7 7l10 10M17 7L7 17',
+ file: 'M7 3h7l5 5v12a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zM14 3v5h5',
+ slash: 'M15 4L9 20',
+ at: 'M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-3.5 7.1',
+ gear: 'M12 9a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM12 2.8v2.4M12 18.8v2.4M2.8 12h2.4M18.8 12h2.4M5.5 5.5l1.7 1.7M16.8 16.8l1.7 1.7M5.5 18.5l1.7-1.7M16.8 7.2l1.7-1.7',
+ back: 'M9 14l-5-5 5-5M4 9h11a5 5 0 0 1 0 10h-3',
+ };
+ const svg = (d, cls) => `<svg${cls ? ` class="${cls}"` : ''} viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"/></svg>`;
+ const el = (tag, cls, html) => { const n = document.createElement(tag); if (cls) n.className = cls; if (html != null) n.innerHTML = html; return n; };
+ const store = {
+ get(k) { try { return JSON.parse(root.localStorage.getItem(k) || 'null'); } catch { return null; } },
+ set(k, v) { try { if (v == null) root.localStorage.removeItem(k); else root.localStorage.setItem(k, JSON.stringify(v)); } catch { /* private mode */ } },
+ };
+ const size = (n) => (n < 1024 ? `${n} B` : n < 1048576 ? `${Math.round(n / 1024)} KB` : `${(n / 1048576).toFixed(n < 10485760 ? 1 : 0)} MB`);
+
+ // ── Your settings: this browser's copy, and your account's when the Network keeps them (the same on every site).
+ let prefs = Object.assign({}, DEFAULTS, store.get(PREFS_KEY) || {});
+ let synced = false, rev = null;
+ const token = () => { try { const n = root.OpenVibeNavbar; return n && typeof n.token === 'function' ? n.token() : null; } catch { return null; } };
+ const NET = 'https://openvibe.network/api/v1/me/composer';
+ async function pull() {
+ const t = token(); if (!t) return;
+ try {
+ const r = await fetch(NET, { headers: { authorization: `Bearer ${t}`, accept: 'application/json' }, credentials: 'omit' });
+ if (!r.ok) return;
+ const j = await r.json();
+ synced = true; rev = j.rev != null ? j.rev : null;
+ prefs = Object.assign({}, DEFAULTS, j.prefs || j);
+ store.set(PREFS_KEY, prefs);
+ live.forEach((c) => c.refresh());
+ } catch { /* local only */ }
+ }
+ async function push() {
+ const t = token(); if (!t || !synced) return;
+ try {
+ const r = await fetch(NET, { method: 'PUT', headers: { authorization: `Bearer ${t}`, 'content-type': 'application/json', accept: 'application/json' }, credentials: 'omit', body: JSON.stringify({ prefs, rev }) });
+ if (r.status === 409) { await pull(); return; }
+ if (r.ok) { const j = await r.json().catch(() => ({})); if (j.rev != null) rev = j.rev; }
+ } catch { /* kept locally */ }
+ }
+ function setPrefs(next) {
+ prefs = Object.assign({}, DEFAULTS, prefs, next);
+ prefs.snippets = (prefs.snippets || []).filter((s) => s && /^[\w-]{1,30}$/.test(s.trigger || '') && s.text).slice(0, 50);
+ store.set(PREFS_KEY, prefs);
+ live.forEach((c) => c.refresh());
+ push();
+ }
+ root.addEventListener('storage', (e) => { if (e.key === PREFS_KEY) { prefs = Object.assign({}, DEFAULTS, store.get(PREFS_KEY) || {}); live.forEach((c) => c.refresh()); } });
+
+ // ── One box.
+ function enhance(form) {
+ if (!form || form.__ovc) return form && form.__ovc;
+ styles();
+ const text = form.querySelector('[data-ovt-text]') || form.querySelector('textarea');
+ if (!text) return null;
+ const row = form.querySelector('.ovt-c-row');
+ const tools = form.querySelector('.ovt-c-tools');
+ const note = form.querySelector('[data-ovt-note]');
+ const send = form.querySelector('[data-ovt-send]') || form.querySelector('[type="submit"]');
+ const inputs = () => [...form.querySelectorAll('input[type="file"]')];
+ const mine = () => mods.filter((m) => !m.match || m.match(form));
+ const uploader = () => { const m = mine().find((x) => typeof x.upload === 'function'); return m ? m.upload : null; };
+ // data-ovc-no-files: the box handles its own files (the Dock's Vibe box uploads a picture as it is picked).
+ const noFiles = form.hasAttribute('data-ovc-no-files');
+ const canFile = () => !noFiles && !!(inputs().length || uploader());
+ const maxFiles = Number(form.getAttribute('data-ovc-max-files')) || 10;
+ const maxBytes = (Number(form.getAttribute('data-ovc-max-mb')) || 25) * 1048576;
+ const files = [];
+ let quiet = '';
+ const wasRequired = !!text.required; // a box that was not required stays so (the Dock's Vibe box)
+
+ const say = (s) => { if (!note) return; if (!quiet) quiet = note.textContent; note.textContent = s; clearTimeout(say.t); say.t = setTimeout(() => { note.textContent = quiet; quiet = ''; }, 5000); };
+ const fire = () => text.dispatchEvent(new Event('input', { bubbles: true }));
+ const ctx = {
+ form, text,
+ get value() { return text.value; },
+ set(v) { text.value = v; fire(); },
+ insert(s) { const a = text.selectionStart, b = text.selectionEnd; text.setRangeText(s, a, b, 'end'); fire(); text.focus(); },
+ files: () => files.map((f) => f.file),
+ add: (list) => add(list),
+ note: say,
+ send: () => { if (form.requestSubmit) form.requestSubmit(send || undefined); else form.submit(); },
+ };
+
+ // Files: a strip of chips above the box.
+ const strip = el('div', 'ovc-files');
+ strip.hidden = true;
+ strip.setAttribute('aria-label', 'Attached files');
+ const firstLabel = form.querySelector(`label[for="${text.id}"]`);
+ form.insertBefore(strip, firstLabel || text);
+ const accepts = (file) => {
+ const acc = (inputs()[0] && inputs()[0].getAttribute('accept')) || '';
+ if (!acc) return true;
+ const name = (file.name || '').toLowerCase(), type = (file.type || '').toLowerCase();
+ return acc.split(',').map((x) => x.trim().toLowerCase()).some((a) => !a || (a.startsWith('.') ? name.endsWith(a) : a.endsWith('/*') ? type.startsWith(a.slice(0, -1)) : type === a));
+ };
+ function syncInputs() {
+ if (noFiles) return;
+ if (uploader()) { inputs().forEach((i) => { i.value = ''; }); return; }
+ const [first, ...rest] = inputs();
+ if (!first || typeof DataTransfer === 'undefined') return;
+ const dt = new DataTransfer();
+ files.forEach((f) => dt.items.add(f.file));
+ first.files = dt.files;
+ rest.forEach((i) => { i.value = ''; });
+ }
+ function paintFile(f) {
+ const kind = (f.file.type || '').split('/')[0];
+ const chip = f.chip || el('div', 'ovc-file');
+ f.chip = chip;
+ chip.classList.toggle('is-bad', !!f.error);
+ chip.classList.toggle('is-up', f.progress != null && f.progress < 1 && !f.error);
+ chip.style.setProperty('--p', String(f.progress == null ? 1 : f.progress));
+ const ext = ((f.file.name || '').split('.').pop() || 'file').slice(0, 4).toUpperCase();
+ const thumb = f.url && kind === 'image' ? `<img class="ovc-file-th" src="${f.url}" alt="">` : f.url && kind === 'video' ? `<video class="ovc-file-th" src="${f.url}" muted preload="metadata"></video>` : `<span class="ovc-file-th ovc-file-ic">${f.pasted ? svg(IC.file) : ext}</span>`;
+ chip.innerHTML = `${thumb}<span class="ovc-file-t"><span class="ovc-file-n"></span><span class="ovc-file-s"></span></span>`
+ + (f.pasted ? `<button type="button" class="ovc-file-b" data-ovc-back aria-label="Put the text back in the box" title="Put it back in the box">${svg(IC.back)}</button>` : '')
+ + `<button type="button" class="ovc-file-x" data-ovc-x aria-label="Remove">${svg(IC.x)}</button>`;
+ chip.querySelector('.ovc-file-n').textContent = f.pasted ? 'Pasted text' : f.file.name || 'File';
+ chip.querySelector('.ovc-file-s').textContent = f.error || (f.pasted ? `${f.lines} lines · ${size(f.file.size)}` : f.progress != null && f.progress < 1 ? `${Math.round(f.progress * 100)}%` : size(f.file.size));
+ chip.querySelector('[data-ovc-x]').setAttribute('aria-label', `Remove ${f.pasted ? 'the pasted text' : f.file.name || 'the file'}`);
+ if (!chip.isConnected) strip.appendChild(chip);
+ }
+ function remove(f) {
+ const i = files.indexOf(f); if (i < 0) return;
+ files.splice(i, 1);
+ if (f.url) URL.revokeObjectURL(f.url);
+ if (f.hidden) f.hidden.remove();
+ form.dispatchEvent(new CustomEvent('ovc:file-removed', { detail: { file: f.file, chip: f.chip } }));
+ f.chip.remove();
+ strip.hidden = !files.length;
+ syncInputs();
+ syncSend();
+ }
+ function add(list, pasted) {
+ const up = uploader();
+ for (const file of [...(list || [])]) {
+ if (files.length >= maxFiles) { say(`Up to ${maxFiles} files at a time.`); break; }
+ if (!accepts(file)) { say(`${file.name || 'That file'}: this box does not take that kind of file.`); continue; }
+ if (file.size > maxBytes) { say(`${file.name || 'That file'} is ${size(file.size)}: the limit is ${size(maxBytes)}.`); continue; }
+ const kind = (file.type || '').split('/')[0];
+ const f = { file, url: kind === 'image' || kind === 'video' ? URL.createObjectURL(file) : null, pasted: !!pasted, lines: pasted ? pasted.lines : 0, text: pasted ? pasted.text : null };
+ files.push(f);
+ paintFile(f);
+ form.dispatchEvent(new CustomEvent('ovc:file-added', { detail: { file, chip: f.chip } }));
+ if (up) {
+ f.progress = 0; paintFile(f);
+ Promise.resolve(up(file, (p) => { f.progress = Math.max(0, Math.min(0.99, Number(p) || 0)); paintFile(f); syncSend(); }))
+ .then((r) => {
+ if (!files.includes(f)) return;
+ f.progress = 1; f.id = r && r.id;
+ if (f.id) { f.hidden = el('input'); f.hidden.type = 'hidden'; f.hidden.name = form.getAttribute('data-ovc-ids') || 'attachments'; f.hidden.value = f.id; form.appendChild(f.hidden); }
+ paintFile(f); syncSend();
+ }, (e) => { f.error = (e && e.message) || 'Did not upload'; paintFile(f); syncSend(); });
+ }
+ }
+ strip.hidden = !files.length;
+ syncInputs();
+ syncSend();
+ }
+ strip.addEventListener('click', (e) => {
+ const chip = e.target.closest && e.target.closest('.ovc-file'); if (!chip) return;
+ const f = files.find((x) => x.chip === chip); if (!f) return;
+ if (e.target.closest('[data-ovc-x]')) { remove(f); text.focus(); }
+ else if (e.target.closest('[data-ovc-back]')) { const t = f.text; remove(f); ctx.insert(t); }
+ });
+ form.addEventListener('change', (e) => {
+ const i = e.target; if (noFiles || !i || i.type !== 'file' || i.__ovcSync) return;
+ const picked = [...(i.files || [])];
+ i.__ovcSync = true; i.value = ''; i.__ovcSync = false;
+ add(picked);
+ });
+ text.addEventListener('paste', (e) => {
+ const cd = e.clipboardData; if (!cd || !canFile()) return;
+ const got = [...(cd.files || [])];
+ if (got.length) { e.preventDefault(); add(got); return; }
+ const t = cd.getData('text/plain') || '';
+ const lines = t.split('\n').length;
+ if (t.length > 6000 || lines > 120) {
+ // Only where the box takes a text file (or a mod uploads): a box for pictures keeps the paste as text.
+ const asFile = new File([t], 'pasted-text.txt', { type: 'text/plain' });
+ if (!uploader() && !accepts(asFile)) return;
+ e.preventDefault();
+ add([asFile], { lines, text: t });
+ say('The long paste went in as a file: ↩ on it puts it back in the box.');
+ }
+ });
+ const zone = form.closest('[data-ovt]') || form;
+ let depth = 0;
+ const hasFiles = (e) => e.dataTransfer && [...(e.dataTransfer.types || [])].includes('Files');
+ zone.addEventListener('dragenter', (e) => { if (!hasFiles(e) || !canFile()) return; depth++; form.classList.add('ovc-dropping'); });
+ zone.addEventListener('dragleave', () => { if (depth && --depth === 0) form.classList.remove('ovc-dropping'); });
+ zone.addEventListener('dragover', (e) => { if (hasFiles(e) && canFile()) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; } });
+ zone.addEventListener('drop', (e) => { if (!hasFiles(e) || !canFile()) return; e.preventDefault(); depth = 0; form.classList.remove('ovc-dropping'); add(e.dataTransfer.files); text.focus(); });
+
+ // Send waits for words or a file, and for uploads to finish.
+ const busy = () => files.some((f) => f.progress != null && f.progress < 1 && !f.error);
+ function syncSend() {
+ if (!send || form.classList.contains('is-busy')) return;
+ send.disabled = busy() || (!text.value.trim() && !files.length);
+ if (wasRequired && text.required && files.length) text.required = false;
+ else if (wasRequired && !files.length && !text.required && !form.classList.contains('is-busy')) text.required = true;
+ }
+ text.addEventListener('input', syncSend);
+
+ // The slash list and the mention list: one popover above the box.
+ const pop = el('div', 'ovc-pop');
+ pop.setAttribute('role', 'listbox');
+ pop.id = `ovc-pop-${Math.random().toString(36).slice(2, 8)}`;
+ pop.hidden = true;
+ form.appendChild(pop);
+ text.setAttribute('aria-controls', pop.id);
+ let items = [], at = 0, trig = null, seq = 0;
+ function siteCommands() {
+ let list = [];
+ try { list = JSON.parse(form.getAttribute('data-ovt-commands') || '[]'); } catch { /* none */ }
+ return list.filter((c) => c && /^[\w-]{1,30}$/.test(c.name || ''));
+ }
+ function commands() {
+ const own = (prefs.snippets || []).map((s) => ({ name: s.trigger, hint: s.label || s.text.slice(0, 60), insert: s.text, mine: true }));
+ const fromMods = mine().flatMap((m) => m.commands || []);
+ return [...siteCommands(), ...fromMods, ...own, { name: 'settings', hint: 'Your composer: what sends, voice, your own commands', run: () => openSettings() }];
+ }
+ function caretWord() {
+ const upto = text.value.slice(0, text.selectionStart);
+ let m = upto.match(/(?:^|\n)\/([\w-]{0,30})$/);
+ if (m) return { kind: 'slash', q: m[1], from: upto.length - m[1].length - 1 };
+ m = upto.match(/(?:^|[\s(])@([\w.-]{0,30})$/);
+ if (m && mine().some((x) => x.mentions && typeof x.mentions.search === 'function')) return { kind: 'at', q: m[1], from: upto.length - m[1].length - 1 };
+ return null;
+ }
+ function close() { pop.hidden = true; items = []; trig = null; text.removeAttribute('aria-activedescendant'); text.setAttribute('aria-expanded', 'false'); }
+ function paintPop() {
+ if (!items.length) { close(); return; }
+ pop.innerHTML = '';
+ items.forEach((it, i) => {
+ const b = el('button', 'ovc-pop-i');
+ b.type = 'button'; b.id = `${pop.id}-${i}`;
+ b.setAttribute('role', 'option');
+ b.setAttribute('aria-selected', i === at ? 'true' : 'false');
+ const lead = it.avatar ? el('img', 'ovc-pop-av') : el('span', 'ovc-pop-ic', svg(trig && trig.kind === 'at' ? IC.at : it.name === 'settings' ? IC.gear : IC.slash));
+ if (it.avatar) { lead.src = it.avatar; lead.alt = ''; }
+ const t = el('span', 'ovc-pop-t');
+ const n = el('b'); n.textContent = trig && trig.kind === 'at' ? it.label : `/${it.name}`;
+ const h = el('span'); h.textContent = trig && trig.kind === 'at' ? (it.sub || (it.handle ? `@${it.handle}` : '')) : it.hint || '';
+ t.append(n, h);
+ b.append(lead, t);
+ if (it.mine) b.appendChild(el('span', 'ovc-pop-tag', 'Yours'));
+ b.addEventListener('mousedown', (e) => e.preventDefault());
+ b.addEventListener('click', () => choose(i));
+ pop.appendChild(b);
+ });
+ pop.hidden = false;
+ text.setAttribute('aria-expanded', 'true');
+ text.setAttribute('aria-activedescendant', `${pop.id}-${at}`);
+ const cur = pop.children[at]; if (cur && cur.scrollIntoView) cur.scrollIntoView({ block: 'nearest' });
+ }
+ async function look() {
+ const w = caretWord();
+ if (!w) { close(); return; }
+ trig = w; const mineSeq = ++seq;
+ if (w.kind === 'slash') {
+ const q = w.q.toLowerCase();
+ items = commands().filter((c) => !q || c.name.toLowerCase().startsWith(q) || (c.hint || '').toLowerCase().includes(q)).slice(0, 12);
+ at = 0; paintPop(); return;
+ }
+ const srcs = mine().filter((m) => m.mentions && typeof m.mentions.search === 'function');
+ const lists = await Promise.all(srcs.map((m) => Promise.resolve(m.mentions.search(w.q)).catch(() => [])));
+ if (mineSeq !== seq) return;
+ items = lists.flat().filter((x) => x && x.label).slice(0, 8);
+ at = 0; paintPop();
+ }
+ function choose(i) {
+ const it = items[i]; if (!it || !trig) return;
+ const end = text.selectionStart;
+ const from = trig.from, kind = trig.kind;
+ close();
+ if (kind === 'at') { text.setRangeText(it.insert || `@${it.handle || it.label} `, from, end, 'end'); fire(); text.focus(); return; }
+ text.setRangeText('', from, end, 'end');
+ if (typeof it.run === 'function') { fire(); try { it.run(ctx); } catch (e) { console.error(e); } return; }
+ text.setRangeText(String(it.insert || ''), from, from, 'end'); fire(); text.focus();
+ }
+ let lookT = 0;
+ text.addEventListener('input', () => { clearTimeout(lookT); lookT = setTimeout(() => { lookT = 0; look(); }, trig && trig.kind === 'at' ? 140 : 0); });
+ text.addEventListener('blur', () => setTimeout(() => { if (document.activeElement !== text) close(); }, 120));
+ text.setAttribute('aria-autocomplete', 'list');
+
+ // Keys, before the thread's own: the list's arrows and Enter, what sends, and the marks.
+ form.addEventListener('keydown', (e) => {
+ if (e.target !== text) return;
+ // Enter right after the last letter: the slash list is read now, so "/settings" + Enter picks it.
+ if (e.key === 'Enter' && lookT) { clearTimeout(lookT); lookT = 0; const w = caretWord(); if (w && w.kind === 'slash') look(); }
+ if (!pop.hidden && items.length) {
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); e.stopPropagation(); at = (at + (e.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length; paintPop(); return; }
+ if ((e.key === 'Enter' && !e.shiftKey) || e.key === 'Tab') { e.preventDefault(); e.stopPropagation(); choose(at); return; }
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); return; }
+ }
+ const mod = mac ? e.metaKey : e.ctrlKey;
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
+ if (mod) { e.preventDefault(); e.stopPropagation(); if (!send || !send.disabled) ctx.send(); return; }
+ if (prefs.send_key === 'mod-enter') { e.stopPropagation(); return; }
+ if (busy()) { e.preventDefault(); e.stopPropagation(); say('A file is still uploading.'); return; }
+ if (!text.value.trim() && files.length && (root.matchMedia ? root.matchMedia('(pointer: fine)').matches : true)) { e.preventDefault(); e.stopPropagation(); ctx.send(); return; }
+ }
+ const wrap = { b: '**', i: '_', e: '`' }[mod && !e.altKey && !e.shiftKey ? e.key.toLowerCase() : ''];
+ if (wrap) {
+ e.preventDefault(); e.stopPropagation();
+ const a = text.selectionStart, z = text.selectionEnd, sel = text.value.slice(a, z);
+ text.setRangeText(wrap + sel + wrap, a, z, 'select');
+ if (!sel) text.setSelectionRange(a + wrap.length, a + wrap.length);
+ else text.setSelectionRange(a + wrap.length, z + wrap.length);
+ fire();
+ }
+ }, true);
+
+ // The send: the mods may hold it; files still uploading hold it; the draft goes.
+ const draftKey = form.hasAttribute('data-ovc-no-draft') ? null : `ov:draft:${location.pathname}#${form.id || form.getAttribute('action') || ''}`;
+ form.addEventListener('submit', (e) => {
+ if (busy()) { e.preventDefault(); e.stopImmediatePropagation(); say('A file is still uploading.'); return; }
+ for (const m of mine()) {
+ if (typeof m.beforeSend !== 'function') continue;
+ let ok; try { ok = m.beforeSend(ctx); } catch (err) { console.error(err); }
+ if (ok === false) { e.preventDefault(); e.stopImmediatePropagation(); return; }
+ }
+ if (draftKey) store.set(draftKey, null);
+ close();
+ }, true);
+ if (draftKey) {
+ const d = store.get(draftKey);
+ if (d && d.t && !text.value && Date.now() - (d.at || 0) < 7 * 864e5) { text.value = d.t; fire(); }
+ let dt = 0;
+ text.addEventListener('input', () => { clearTimeout(dt); dt = setTimeout(() => store.set(draftKey, text.value.trim() ? { t: text.value, at: Date.now() } : null), 400); });
+ }
+
+ // The count, near the box's limit.
+ let count = null;
+ if (text.maxLength > 0 && row) {
+ count = el('span', 'ovc-count'); count.setAttribute('aria-live', 'polite');
+ row.insertBefore(count, send && send.parentNode === row ? send : null);
+ // The count shows only near the limit: the last 15% of a box, at most 200 and at least 20 characters (a 120-character
+ // search box said "120 left" while empty, 2026-11-11).
+ const paintCount = () => { const left = text.maxLength - text.value.length; count.hidden = left > Math.min(200, Math.max(20, Math.round(text.maxLength * 0.15))); count.textContent = `${left} left`; count.classList.toggle('is-low', left < 20); };
+ text.addEventListener('input', paintCount); paintCount();
+ }
+
+ // Voice: the browser listens, the words go in at the caret as they come.
+ let mic = null, rec = null;
+ function micButton() {
+ if (!Speech || !tools) return;
+ if (!prefs.voice) { if (mic) { mic.remove(); mic = null; } return; }
+ if (mic) return;
+ mic = el('button', 'ovt-btn ovc-mic', svg(IC.mic));
+ mic.type = 'button'; mic.setAttribute('aria-label', 'Speak'); mic.title = 'Speak'; mic.setAttribute('aria-pressed', 'false');
+ mic.addEventListener('click', () => {
+ if (rec) { rec.stop(); return; }
+ rec = new Speech();
+ rec.lang = document.documentElement.lang || navigator.language || 'en-US';
+ rec.interimResults = true; rec.continuous = true;
+ const a = text.selectionStart, z = text.selectionEnd, head = text.value.slice(0, a), tail = text.value.slice(z);
+ const pad = head && !/\s$/.test(head) ? ' ' : '';
+ rec.onresult = (ev) => { let s = ''; for (let i = 0; i < ev.results.length; i++) s += ev.results[i][0].transcript; text.value = head + pad + s + tail; text.setSelectionRange((head + pad + s).length, (head + pad + s).length); fire(); };
+ rec.onend = () => { rec = null; mic.setAttribute('aria-pressed', 'false'); mic.title = 'Speak'; text.focus(); };
+ rec.onerror = (ev) => { if (ev.error === 'not-allowed') say('The microphone is blocked for this site.'); };
+ try { rec.start(); mic.setAttribute('aria-pressed', 'true'); mic.title = 'Stop listening'; } catch { rec = null; }
+ });
+ tools.appendChild(mic);
+ }
+
+ // The mods' tools: buttons in the row, in the order the mods came.
+ let modTools = [];
+ function paintTools() {
+ modTools.forEach((b) => b.remove()); modTools = [];
+ if (!tools) return;
+ for (const t of mine().flatMap((m) => m.tools || [])) {
+ if (!t || !t.label || typeof t.run !== 'function') continue;
+ const b = el('button', 'ovt-btn ovc-tool', svg(PATH_OK.test(t.path || '') ? t.path : IC.slash));
+ b.type = 'button'; b.setAttribute('aria-label', t.label); b.title = t.label;
+ b.addEventListener('click', () => { try { t.run(ctx); } catch (e) { console.error(e); } });
+ tools.insertBefore(b, mic && mic.parentNode === tools ? mic : null);
+ modTools.push(b);
+ }
+ }
+
+ const api = {
+ form, ctx,
+ refresh() { form.classList.toggle('ovc-compact', prefs.density === 'compact'); micButton(); paintTools(); syncSend(); },
+ add, files: () => files.map((f) => f.file), clearFiles: () => { [...files].forEach(remove); },
+ uploads: () => files.map((f) => f.id).filter(Boolean),
+ clear() { files.slice().forEach(remove); text.value = ''; if (draftKey) store.set(draftKey, null); fire(); },
+ destroy() { live.delete(api); pop.remove(); strip.remove(); if (mic) mic.remove(); modTools.forEach((b) => b.remove()); form.__ovc = null; },
+ };
+ form.__ovc = api;
+ form.addEventListener('reset', () => setTimeout(() => api.clear(), 0));
+ live.add(api);
+ form.classList.add('ovc');
+ api.refresh();
+ return api;
+ }
+
+ // ── Your settings, in a sheet: what sends, voice, compact, your own commands.
+ let sheet = null;
+ function openSettings(from) {
+ const opener = from || document.activeElement;
+ needWindow().then((W) => settingsIn(W, opener));
+ }
+ function settingsIn(W, opener) {
+ let win = null;
+ if (!W && !sheet) {
+ sheet = el('dialog', 'ovc-set');
+ sheet.setAttribute('aria-label', 'Your composer');
+ sheet.addEventListener('click', (e) => { if (e.target === sheet) sheet.close(); });
+ document.body.appendChild(sheet);
+ }
+ const k = mac ? '⌘' : 'Ctrl';
+ const host = W ? el('div', 'ovc-set ovc-set-in') : sheet;
+ host.innerHTML = `<form method="dialog" class="ovc-set-f">${W ? '' : `<header class="ovc-set-h"><b>Your composer</b><button class="ovt-btn" value="done" aria-label="Done">${svg(IC.x)}</button></header>`}
+<fieldset class="ovc-set-g"><legend>Send with</legend><label><input type="radio" name="send_key" value="enter"> Enter <small>(Shift+Enter for a new line)</small></label><label><input type="radio" name="send_key" value="mod-enter"> ${k}+Enter <small>(Enter for a new line)</small></label></fieldset>
+<label class="ovc-set-c"><input type="checkbox" name="voice"> A microphone button${Speech ? '' : ' <small>(this browser cannot listen)</small>'}</label>
+<label class="ovc-set-c"><input type="checkbox" name="compact"> A compact box</label>
+<fieldset class="ovc-set-g"><legend>Your commands</legend><p class="ovc-set-p">Type / and the word, and its text goes in. Up to 50.</p><ul class="ovc-set-l"></ul>
+<div class="ovc-set-add"><input name="trigger" placeholder="word" maxlength="30" pattern="[A-Za-z0-9_-]+" aria-label="The word after /"><textarea name="snippet" rows="2" maxlength="4000" placeholder="The text it puts in" aria-label="The text it puts in"></textarea><button type="button" class="ovc-set-btn" data-ovc-add>Add</button></div></fieldset>
+<p class="ovc-set-p">${synced ? 'Saved to your account: the same on every OpenVibe site.' : 'Saved in this browser.'}</p></form>`;
+ const f = host.querySelector('form');
+ f.send_key.value = prefs.send_key;
+ f.voice.checked = !!prefs.voice; f.voice.disabled = !Speech;
+ f.compact.checked = prefs.density === 'compact';
+ const list = host.querySelector('.ovc-set-l');
+ const paintList = () => {
+ list.innerHTML = '';
+ (prefs.snippets || []).forEach((s, i) => {
+ const li = el('li');
+ const w = el('b'); w.textContent = `/${s.trigger}`;
+ const t = el('span'); t.textContent = s.text.slice(0, 80);
+ const x = el('button', 'ovt-btn', svg(IC.x)); x.type = 'button'; x.setAttribute('aria-label', `Remove /${s.trigger}`);
+ x.addEventListener('click', () => { const next = prefs.snippets.slice(); next.splice(i, 1); setPrefs({ snippets: next }); paintList(); });
+ li.append(w, t, x); list.appendChild(li);
+ });
+ };
+ paintList();
+ f.addEventListener('change', (e) => {
+ if (e.target.name === 'send_key') setPrefs({ send_key: f.send_key.value });
+ else if (e.target.name === 'voice') setPrefs({ voice: f.voice.checked });
+ else if (e.target.name === 'compact') setPrefs({ density: f.compact.checked ? 'compact' : 'comfy' });
+ });
+ host.querySelector('[data-ovc-add]').addEventListener('click', () => {
+ const w = f.trigger.value.trim().replace(/^\//, ''), t = f.snippet.value;
+ if (!/^[\w-]{1,30}$/.test(w)) { f.trigger.focus(); return; }
+ if (!t.trim()) { f.snippet.focus(); return; }
+ setPrefs({ snippets: (prefs.snippets || []).filter((s) => s.trigger !== w).concat([{ trigger: w, text: t }]) });
+ f.trigger.value = ''; f.snippet.value = '';
+ paintList(); f.trigger.focus();
+ });
+ f.addEventListener('submit', (e) => e.preventDefault());
+ if (W) win = W.open({ id: 'ov-composer-settings', title: 'Your composer', body: host, kind: 'dialog', size: 'sm', from: opener, focus: 'input[name=send_key]:checked' });
+ else { if (typeof sheet.showModal === 'function') sheet.showModal(); else sheet.setAttribute('open', ''); const first = f.querySelector('input[name=send_key]:checked'); if (first) first.focus(); }
+ return win;
+ }
+
+ function mod(m) {
+ if (!m || !m.id) return;
+ const i = mods.findIndex((x) => x.id === m.id);
+ if (i >= 0) mods[i] = m; else mods.push(m);
+ live.forEach((c) => c.refresh());
+ }
+ function scan(scope) { (scope || document).querySelectorAll('[data-ovt-composer], [data-ovc]').forEach((f) => enhance(f)); }
+
+ root.OpenVibeComposer = { enhance, mod, scan, settings: openSettings, get prefs() { return Object.assign({}, prefs); }, setPrefs };
+ if (typeof module === 'object' && module.exports) module.exports = { enhance, mod, scan };
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => scan(), { once: true }); else scan();
+ document.addEventListener('ov:boost:load', () => { live.forEach((c) => { if (!c.form.isConnected) c.destroy(); }); scan(); });
+ pull();
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,815 @@
@@ -0,0 +1,815 @@
+/**
+ * ov-float.js (openvibe-shared 4.208.0): the move engine behind a picture-in-picture, a panel window or any box that
+ * floats over a page. It is Live's PiP move (public/js/stream-pip.js, 2026-11-11: "move it anywhere … mobile and
+ * desktop … more solid feeling and more responsive") lifted out whole, so every floating thing behaves the same:
+ *
+ * const f = OVFloat.attach(el, { frame, key, defaults, aspect, minPx, onChange });
+ *
+ * - Docked in `frame` (an element) or floating over the page (position: fixed). It follows the finger or the mouse
+ * with a transform each animation frame and lands with a spring (FLIP); a flick carries on where it was thrown.
+ * - Dropped over its frame it docks; anywhere else it floats. Near an edge it snaps; thrown past a side it tucks into
+ * that side, leaving a tab with its picture still showing. Two fingers pinch it, the corner sizes it, a double tap
+ * toggles small and large, and with focus the arrows move it, + and − size it, T tucks, F pops it out of its frame
+ * (or back in) and Home puts it back where it started.
+ * - Its place is kept as fractions of its frame: in this browser under `key`, and in the person's account when the
+ * page has GET/PATCH /api/me/layout (the `pip` map Live keeps). `OVFloat.store` is the account plug:
+ * { get(key), put(key, value), load?() }; assign your own to keep it somewhere else.
+ * - `f.geom()`, `f.set(patch, { save })`, `f.tuck()`, `f.untuck()`, `f.toggleMode()`, `f.toggleSize()`, `f.reset()`,
+ * `f.paint()`, `f.settle()`, `f.save()`, `f.relayout()`, `f.destroy()`. `OVFloat.layout()` lists every attached box,
+ * and each save dispatches `ov:float` ({ key, geom, el }).
+ *
+ * The CSS (injected once) is behaviour, not looks: positioning, the lift, the tuck's translate, the tab and the resize
+ * grip. A component adds its own colours, border, shadow and children. `aspect: null`, `size: false` and
+ * `resize: false` let a panel window keep its own height and width while it gets the move, the tuck and the place.
+ */
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OVFloat) return;
+
+ const BASE = 16 / 9; // the shape `w` is measured for
+ const MIN_W = 0.08, MAX_W = 0.60;
+ const MAX_H = 0.85; // of the frame's height, whatever the shape
+ const MIN_PX = 150; // the smallest 16:9 width on a phone-sized frame
+ const SNAP = 0.04; // within this fraction of an edge, a drop snaps to it
+ const EDGE_PX = 12; // the margin it snaps to
+ const TAB_PX = 30; // what stays on screen of a tucked box
+ const DRAG_PX = 4; // a press that moves less is a tap
+ const BIG_W = 0.5; // a double tap's large size
+ const PROJECT_MS = 120; // how far a flick carries
+ const TAP_MS = 300;
+ const ACTIVE_MS = 3200;
+ const LAND_MS = 300;
+ const DEFAULT_IGNORE = 'a, button, input, textarea, select, [contenteditable], .ov-float-resize, [data-ov-float-resize], [data-ov-float-no-drag]';
+
+ const CSS = `:where(.ov-float) { position: absolute; box-sizing: border-box; touch-action: none; -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent; }
+.ov-float.is-floating { position: fixed; z-index: 900; }
+.ov-float.is-lifted { position: fixed; z-index: 905; will-change: transform; transition: none; }
+.ov-float.is-dragging { cursor: grabbing; }
+.ov-float.is-tucked-left { translate: calc(-100% + var(--ov-float-tab, ${TAB_PX}px)) 0; }
+.ov-float.is-tucked-right { translate: calc(100% - var(--ov-float-tab, ${TAB_PX}px)) 0; }
+.ov-float.is-tucked .ov-float-tab { display: grid; }
+.ov-float-tab { position: absolute; top: 0; bottom: 0; z-index: 4; width: var(--ov-float-tab, ${TAB_PX}px); display: none; place-items: center; padding: 0; border: 0; cursor: pointer;
+ color: #fff; font: 600 15px/1 system-ui, sans-serif; background: rgba(10, 12, 18, .55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); }
+.ov-float.is-tucked-left .ov-float-tab { right: 0; }
+.ov-float.is-tucked-right .ov-float-tab { left: 0; background: linear-gradient(270deg, rgba(0, 0, 0, .55), rgba(0, 0, 0, .25)); }
+.ov-float-tab-l, .ov-float-tab-r { display: none; }
+.ov-float.is-tucked-left .ov-float-tab-r, .ov-float.is-tucked-right .ov-float-tab-l { display: block; }
+.ov-float-resize { position: absolute; right: 0; bottom: 0; z-index: 3; width: 22px; height: 22px; cursor: nwse-resize; touch-action: none; opacity: 0;
+ background: linear-gradient(135deg, transparent 52%, rgba(255, 255, 255, .72) 52%, rgba(255, 255, 255, .72) 60%, transparent 60%, transparent 70%, rgba(255, 255, 255, .72) 70%, rgba(255, 255, 255, .72) 78%, transparent 78%); }
+.ov-float:hover .ov-float-resize, .ov-float:focus-within .ov-float-resize, .ov-float.is-resizing .ov-float-resize { opacity: 1; }
+@media (pointer: coarse) { .ov-float-resize { width: 30px; height: 30px; opacity: .85; } }
+html[data-ov-motion="off"] .ov-float { transition: none !important; }
+html[data-ov-motion="off"] .ov-float-tab, html[data-ov-motion="off"] .ov-float-resize { transition: none !important; }
+@media (prefers-reduced-motion: reduce) {
+ html:not([data-ov-motion="on"]) .ov-float { transition: none !important; }
+}`;
+
+ const instances = new Set();
+ let uid = 0;
+
+ function clamp(v, lo, hi, dflt) {
+ const n = Number(v);
+ if (!Number.isFinite(n)) return dflt;
+ return Math.min(hi, Math.max(lo, n));
+ }
+
+ function viewport(inst) {
+ const de = document.documentElement;
+ // A floating element's percentage place is measured on the document's client box, not on innerWidth when a
+ // scrollbar takes 15 px of it. Live's PiP keeps the window size it always used; a free panel asks for `client`.
+ const client = inst && inst.options.viewport === 'client' && de;
+ const w = (client ? de.clientWidth : root.innerWidth) || 0;
+ const h = (client ? de.clientHeight : root.innerHeight) || 0;
+ return { left: 0, top: 0, width: w, height: h, right: w, bottom: h };
+ }
+
+ function rectOf(el) {
+ return el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : viewport();
+ }
+
+ /** The box an absolutely positioned child measures its percentages on: the frame's padding box, not its border. */
+ function contentRect(el) {
+ const r = rectOf(el);
+ if (!el || !el.clientWidth || !el.clientHeight) return r;
+ const left = r.left + (el.clientLeft || 0), top = r.top + (el.clientTop || 0);
+ return { left, top, width: el.clientWidth, height: el.clientHeight, right: left + el.clientWidth, bottom: top + el.clientHeight };
+ }
+
+ function reduced() {
+ const m = document.documentElement && document.documentElement.getAttribute('data-ov-motion');
+ if (m === 'off') return true;
+ try { return m !== 'on' && root.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return false; }
+ }
+
+ function inFullscreen(inst) {
+ const fs = typeof document !== 'undefined' && document.fullscreenElement;
+ return !!(fs && inst && inst.frame && (fs === inst.frame || fs.contains(inst.frame)));
+ }
+
+ function frameRect(inst) {
+ if (inst.geom.mode === 'float' && !inFullscreen(inst)) return viewport(inst);
+ return inst.frame ? contentRect(inst.frame) : viewport(inst);
+ }
+
+ /* ── geometry (pure; the tests call these) ─────────────────────────────────────────────────────────────── */
+
+ /**
+ * The box as fractions of its frame: width, height and the place it may go, for the source's own shape.
+ * aspect: null keeps the element's own height (a panel window); `elHeight` is its measured height.
+ */
+ function boxOf(geom, rect, opts) {
+ const o = opts || {};
+ const a = o.aspect === null ? null : (Number(o.aspect) > 0 ? Number(o.aspect) : BASE);
+ const minW = o.minW == null ? MIN_W : o.minW;
+ const maxW = o.maxW == null ? MAX_W : o.maxW;
+ const maxH = o.maxH == null ? MAX_H : o.maxH;
+ const rw = (rect && rect.width) || 0, rh = (rect && rect.height) || 0;
+ const rl = (rect && rect.left) || 0, rt = (rect && rect.top) || 0;
+ let w = clamp(geom && geom.w, minW, maxW, Math.min(0.25, maxW));
+ if (a == null) {
+ return { a: null, wf: w, hf: rh && o.elHeight ? o.elHeight / rh : 0, rw, rh, rl, rt };
+ }
+ // On a phone-sized frame a quarter of the width is a postage stamp: never smaller than a 150 px wide 16:9 box
+ // (the saved size is unchanged, so a desktop shows it as saved).
+ const minPx = o.minPx == null ? MIN_PX : o.minPx;
+ if (rw && rw < (o.phoneFramePx == null ? 640 : o.phoneFramePx)) w = Math.max(w, Math.min(maxW, minPx / rw));
+ let wf = w * Math.sqrt(a / BASE); // the same area as a 16:9 box of width w
+ if (rw && rh && (wf * rw) / a > maxH * rh) wf = (maxH * rh * a) / rw;
+ wf = Math.min(wf, 1);
+ const hf = rw && rh ? (wf * rw) / a / rh : wf / a;
+ return { a, wf, hf, rw, rh, rl, rt };
+ }
+
+ /** After a drop: keep it inside, and snap it to an edge it was dropped close to. */
+ function settleGeom(geom, box, opts) {
+ const o = opts || {};
+ const edge = o.edgePx == null ? EDGE_PX : o.edgePx;
+ const snap = o.snap == null ? SNAP : o.snap;
+ const mx = box.rw ? edge / box.rw : 0, my = box.rh ? edge / box.rh : 0;
+ const maxX = Math.max(0, 1 - box.wf), maxY = Math.max(0, 1 - (box.hf || 0));
+ let x = clamp(geom.x, 0, maxX, 0), y = clamp(geom.y, 0, maxY, 0);
+ if (x < snap) x = Math.min(mx, maxX); else if (x > maxX - snap) x = Math.max(0, maxX - mx);
+ if (y < snap) y = Math.min(my, maxY); else if (y > maxY - snap) y = Math.max(0, maxY - my);
+ geom.x = x; geom.y = y;
+ return geom;
+ }
+
+ /** The last 90 ms of a drag: the speed it was let go at, and where a flick carries it. */
+ function flick(hist, from, opts) {
+ const o = opts || {};
+ const h = Array.isArray(hist) && hist.length ? hist : [{ x: 0, y: 0, t: 0 }];
+ const a = h[0], z = h[h.length - 1];
+ const dt = Math.max(16, (z.t || 0) - (a.t || 0));
+ const vx = ((z.x || 0) - (a.x || 0)) / dt, vy = ((z.y || 0) - (a.y || 0)) / dt;
+ const fast = Math.hypot(vx, vy) > (o.fastAt == null ? 0.6 : o.fastAt);
+ return { vx, vy, fast, left: from.left + (fast ? vx * PROJECT_MS : 0), top: from.top + (fast ? vy * PROJECT_MS : 0) };
+ }
+
+ /** Past a side of its frame by more than a third of itself (or thrown at it): tucked into that side. */
+ function tuckFor(point, from, frame, speed, opts) {
+ const o = opts || {};
+ const edge = o.edgePx == null ? EDGE_PX : o.edgePx;
+ const out = from.width / 3;
+ if (point.left < frame.left - out || (speed.fast && speed.vx < -1.2 && point.left < frame.left + edge)) return 'left';
+ if (point.left + from.width > frame.right + out || (speed.fast && speed.vx > 1.2 && point.left + from.width > frame.right - edge)) return 'right';
+ return null;
+ }
+
+ function overFrame(cx, cy, frame) {
+ return !!frame && cx >= frame.left && cx <= frame.right && cy >= frame.top && cy <= frame.bottom;
+ }
+
+ /** Draw a geometry: the percentages, the shape, and the state classes the component styles. */
+ function paint(el, geom, box, opts) {
+ const o = opts || {};
+ const tuck = geom.tuck;
+ const floating = !!o.floating;
+ const classes = el.classList;
+ classes.toggle('is-floating', floating);
+ classes.toggle('is-tucked', !!tuck);
+ classes.toggle('is-tucked-left', tuck === 'left');
+ classes.toggle('is-tucked-right', tuck === 'right');
+ classes.toggle('is-portrait', !!box.a && box.a < 1);
+ // Shown inside its frame even when a turn of the phone or a smaller window would push it out; the saved place
+ // is not changed, so it comes back when there is room again. A tucked one sits at its side (CSS `translate`
+ // leaves only its tab on screen; `transform` stays free for the landing).
+ const left = tuck === 'left' ? 0 : tuck === 'right' ? Math.max(0, 1 - box.wf) : Math.min(Math.max(0, geom.x), Math.max(0, 1 - box.wf));
+ el.style.left = `${left * 100}%`;
+ el.style.top = `${Math.min(Math.max(0, geom.y), Math.max(0, 1 - (box.hf || 0))) * 100}%`;
+ if (o.size !== false) {
+ el.style.width = `${box.wf * 100}%`;
+ if (box.a) el.style.aspectRatio = String(box.a);
+ }
+ }
+
+ /* ── the account plug (GET/PATCH /api/me/layout, the shape Live's stream-pip.js keeps) ───────────────── */
+
+ function makeDefaultStore() {
+ const state = { data: null, loading: null, pending: {}, timer: 0, off: false };
+ function load() {
+ if (state.off || typeof fetch !== 'function') return Promise.resolve(null);
+ if (!state.loading) {
+ state.loading = fetch('/api/me/layout', { credentials: 'same-origin', headers: { accept: 'application/json' } })
+ .then((r) => {
+ if (r.status === 400 || r.status === 401 || r.status === 403 || r.status === 404) { state.off = true; return null; }
+ return r.ok ? r.json() : null;
+ })
+ .then((d) => { state.data = d && typeof d === 'object' ? d : {}; return state.data; })
+ .catch(() => null);
+ }
+ return state.loading;
+ }
+ function get(key) {
+ const pip = state.data && state.data.pip;
+ return pip && pip[key] ? pip[key] : null;
+ }
+ function put(key, value) {
+ if (state.off || typeof fetch !== 'function') return;
+ state.pending[key] = value;
+ if (state.data) { state.data.pip = state.data.pip || {}; state.data.pip[key] = value; }
+ clearTimeout(state.timer);
+ state.timer = setTimeout(() => {
+ const pip = state.pending; state.pending = {};
+ fetch('/api/me/layout', {
+ method: 'PATCH', credentials: 'same-origin',
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
+ body: JSON.stringify({ pip }),
+ }).then((r) => { if (r.status === 400 || r.status === 401 || r.status === 403 || r.status === 404) state.off = true; })
+ .catch(() => { /* the next move tries again */ });
+ }, 1500);
+ }
+ return { load, get, put, _state: state };
+ }
+
+ function normalizeDefaults(raw, hasFrame) {
+ const d = raw && typeof raw === 'object' ? raw : {};
+ return {
+ x: clamp(d.x, 0, 1, 0.72), y: clamp(d.y, 0, 1, 0.70), w: clamp(d.w, MIN_W, MAX_W, 0.25),
+ mode: hasFrame ? (d.mode === 'float' ? 'float' : 'dock') : 'float',
+ tuck: d.tuck === 'left' || d.tuck === 'right' ? d.tuck : null,
+ muted: typeof d.muted === 'boolean' ? d.muted : undefined,
+ at: 0,
+ };
+ }
+
+ function normalize(raw, fallback, opts) {
+ const o = opts || {};
+ if (!raw || typeof raw !== 'object') return Object.assign({}, fallback);
+ const g = Object.assign({}, fallback);
+ g.x = clamp(raw.x, 0, 1, fallback.x);
+ g.y = clamp(raw.y, 0, 1, fallback.y);
+ g.w = clamp(raw.w, o.minW == null ? MIN_W : o.minW, o.maxW == null ? MAX_W : o.maxW, fallback.w);
+ g.mode = o.hasFrame === false ? 'float' : (raw.mode === 'float' ? 'float' : raw.mode === 'dock' ? 'dock' : fallback.mode);
+ g.tuck = raw.tuck === 'left' || raw.tuck === 'right' ? raw.tuck : null;
+ if (typeof raw.muted === 'boolean') g.muted = raw.muted;
+ g.at = Number(raw.at) || 0;
+ return g;
+ }
+
+ function readSaved(key, legacyKeys) {
+ const keys = [key].concat(Array.isArray(legacyKeys) ? legacyKeys : []);
+ for (const k of keys) {
+ try {
+ const raw = root.localStorage && root.localStorage.getItem(k);
+ if (!raw) continue;
+ const v = JSON.parse(raw);
+ if (v && typeof v === 'object') return v;
+ } catch { /* private mode, or a key from another shape */ }
+ }
+ return null;
+ }
+
+ /* ── the instance ──────────────────────────────────────────────────────────────────────────────────────── */
+
+ function box(inst) {
+ return boxOf(inst.geom, frameRect(inst), {
+ aspect: inst.aspect, minPx: inst.minPx, minW: inst.minW, maxW: inst.maxW,
+ elHeight: inst.el.offsetHeight || 0,
+ });
+ }
+
+ function paintNow(inst) {
+ const b = box(inst);
+ paint(inst.el, inst.geom, b, {
+ floating: inst.geom.mode === 'float' && !inFullscreen(inst),
+ size: inst.options.size !== false,
+ });
+ return b;
+ }
+
+ function settleInst(inst) {
+ return settleGeom(inst.geom, box(inst));
+ }
+
+ function placeIn(inst) {
+ const fs = document.fullscreenElement;
+ let want;
+ if (inst.geom.mode === 'float' && !inFullscreen(inst)) want = document.body;
+ else if (inst.frame) want = fs && !fs.contains(inst.frame) ? fs : inst.frame;
+ else want = document.body;
+ if (want && inst.el.parentNode !== want) {
+ const v = inst.el.querySelector('video');
+ want.appendChild(inst.el);
+ // A media element moved in one task keeps playing; play() covers a browser that paused it.
+ if (v && v.paused) v.play().catch(() => {});
+ }
+ }
+
+ function save(inst) {
+ if (inst.destroyed) return;
+ // A drop clears the lifted inline left/top before it saves; draw the final place first so an onChange that
+ // reads the element's rect (a window's absolute place, for one) sees where it landed, not its static position.
+ if (!inst.el.classList.contains('is-lifted')) paintNow(inst);
+ inst.geom.at = Date.now();
+ try { if (root.localStorage) root.localStorage.setItem(inst.key, JSON.stringify(inst.geom)); } catch { /* private mode */ }
+ if (inst.store && typeof inst.store.put === 'function') {
+ try { inst.store.put(inst.accountKey, Object.assign({}, inst.geom)); } catch { /* the page's own store */ }
+ }
+ if (typeof inst.options.onChange === 'function') {
+ try { inst.options.onChange(Object.assign({}, inst.geom), inst.api); } catch (err) { console.error(err); }
+ }
+ try {
+ root.dispatchEvent(new root.CustomEvent('ov:float', { detail: { key: inst.key, geom: Object.assign({}, inst.geom), el: inst.el } }));
+ } catch { /* an environment without CustomEvent */ }
+ }
+
+ /** Move it to its place with a spring from where it was (`from`: its rect on screen before). */
+ function land(inst, from) {
+ placeIn(inst);
+ const el = inst.el;
+ el.style.transition = 'none';
+ el.style.transform = '';
+ paintNow(inst);
+ if (!from || reduced()) { el.style.transition = ''; return; }
+ const to = el.getBoundingClientRect();
+ if (!to.width) { el.style.transition = ''; return; }
+ const dx = from.left - to.left, dy = from.top - to.top, s = from.width / to.width;
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1 && Math.abs(s - 1) < 0.01) { el.style.transition = ''; return; }
+ el.style.transformOrigin = '0 0';
+ el.style.transform = `translate3d(${dx}px, ${dy}px, 0) scale(${s})`;
+ void el.offsetWidth;
+ el.style.transition = `transform ${LAND_MS}ms var(--ov-ease-spring, cubic-bezier(.2, 1.25, .3, 1))`;
+ el.style.transform = '';
+ clearTimeout(inst.landTimer);
+ inst.landTimer = setTimeout(() => {
+ if (!inst.destroyed) { el.style.transition = ''; el.style.transformOrigin = ''; }
+ }, LAND_MS + 40);
+ }
+
+ /** Switch frames keeping its size and place on screen (`rect`: where it is now). */
+ function toMode(inst, mode, rect) {
+ if (!inst.frame) mode = 'float';
+ inst.geom.mode = mode;
+ const b0 = box(inst);
+ if (!b0.rw || !b0.rh) return;
+ const a = inst.aspect || BASE;
+ const k = Math.sqrt(a / BASE) || 1;
+ if (inst.options.size !== false) inst.geom.w = clamp(rect.width / b0.rw / k, inst.minW, inst.maxW, inst.geom.w);
+ const b = box(inst);
+ inst.geom.x = (rect.left - b.rl) / b.rw;
+ inst.geom.y = (rect.top - b.rt) / b.rh;
+ settleInst(inst);
+ }
+
+ function tuck(inst, side) {
+ const from = inst.el.getBoundingClientRect();
+ if (side === null) { inst.geom.tuck = null; }
+ else {
+ const b = box(inst);
+ inst.geom.tuck = side || (inst.geom.x + b.wf / 2 < 0.5 ? 'left' : 'right');
+ }
+ save(inst);
+ land(inst, from);
+ }
+
+ function untuck(inst) {
+ const from = inst.el.getBoundingClientRect();
+ const b = box(inst);
+ inst.geom.tuck = null;
+ inst.geom.x = from.left + from.width / 2 < (b.rl + b.rw / 2)
+ ? (b.rw ? EDGE_PX / b.rw : 0)
+ : Math.max(0, 1 - b.wf - (b.rw ? EDGE_PX / b.rw : 0));
+ settleInst(inst);
+ save(inst);
+ land(inst, from);
+ }
+
+ function toggleMode(inst) {
+ if (!inst.frame) return false;
+ const from = inst.el.getBoundingClientRect();
+ toMode(inst, inst.geom.mode === 'float' ? 'dock' : 'float', from);
+ save(inst);
+ land(inst, from);
+ return true;
+ }
+
+ function toggleSize(inst) {
+ const from = inst.el.getBoundingClientRect();
+ const big = inst.options.bigW == null ? BIG_W : inst.options.bigW;
+ inst.geom.w = inst.geom.w < big - 0.01 ? big : clamp(inst.defaults.w, inst.minW, inst.maxW, inst.defaults.w);
+ inst.geom.tuck = null;
+ settleInst(inst);
+ save(inst);
+ land(inst, from);
+ }
+
+ function reset(inst) {
+ const from = inst.el.getBoundingClientRect();
+ const d = inst.defaults;
+ Object.assign(inst.geom, { x: d.x, y: d.y, w: d.w, mode: inst.frame ? d.mode : 'float', tuck: null });
+ settleInst(inst);
+ save(inst);
+ land(inst, from);
+ }
+
+ function relayout(inst) {
+ if (inst.destroyed || inst.el.classList.contains('is-lifted')) return;
+ placeIn(inst);
+ paintNow(inst);
+ }
+
+ function syncAccount(inst) {
+ const store = inst.store;
+ if (!store || typeof store.get !== 'function') return;
+ const apply = () => {
+ if (inst.destroyed || inst.touched) return;
+ const remote = normalize(store.get(inst.accountKey), inst.defaults, { hasFrame: !!inst.frame, minW: inst.minW, maxW: inst.maxW });
+ if ((remote.at || 0) > (inst.geom.at || 0)) {
+ inst.geom = remote;
+ placeIn(inst);
+ paintNow(inst);
+ }
+ };
+ if (typeof store.load === 'function') {
+ let p;
+ try { p = store.load(); } catch { p = null; }
+ if (p && typeof p.then === 'function') p.then(apply).catch(() => { /* no account on this page */ });
+ else apply();
+ } else apply();
+ }
+
+ /* ── gestures ──────────────────────────────────────────────────────────────────────────────────────────── */
+
+ function wireMove(inst) {
+ const el = inst.el;
+ const handle = inst.options.handle && inst.options.handle.addEventListener ? inst.options.handle : el;
+ const ignore = inst.options.ignore || DEFAULT_IGNORE;
+ const hintClass = inst.options.hintClass || 'ov-float-dock-hint';
+ const pts = new Map(); // pointerId → { x, y }
+ let drag = null, pinch = null, raf = 0, lastTap = 0, activeTimer = 0;
+ const gap = () => { const [a, b] = [...pts.values()]; return Math.hypot(a.x - b.x, a.y - b.y) || 1; };
+ const hint = (on) => { if (inst.frame && inst.frame.classList) inst.frame.classList.toggle(hintClass, !!on); };
+ const paint = () => {
+ raf = 0;
+ if (!drag || !drag.lifted) return;
+ el.style.transform = `translate3d(${drag.dx}px, ${drag.dy}px, 0)`;
+ const c = drag.r;
+ const p = inst.frame ? contentRect(inst.frame) : null;
+ if (p) hint(overFrame(c.left + drag.dx + c.width / 2, c.top + drag.dy + c.height / 2, p));
+ };
+ const lift = () => {
+ drag.lifted = true;
+ // Over the page from the first move, at the same place on screen, so it can go anywhere.
+ const r = drag.r;
+ const layer = document.fullscreenElement || document.body;
+ if (el.parentNode !== layer) layer.appendChild(el);
+ // Moving the element in the DOM releases any pointer capture it had; take the pointers it is following back.
+ for (const id of pts.keys()) { try { el.setPointerCapture(id); } catch { /* a pointer the browser no longer tracks */ } }
+ el.classList.add('is-dragging', 'is-lifted');
+ el.style.transition = 'none';
+ el.style.left = `${r.left}px`; el.style.top = `${r.top}px`; el.style.width = `${r.width}px`;
+ const v = el.querySelector('video');
+ if (v && v.paused) v.play().catch(() => {});
+ if (typeof inst.options.onLift === 'function') { try { inst.options.onLift(inst.api); } catch { /* */ } }
+ };
+ const showActive = () => {
+ el.classList.add('is-active');
+ clearTimeout(activeTimer);
+ activeTimer = setTimeout(() => el.classList.remove('is-active'), ACTIVE_MS);
+ };
+ const onDown = (e) => {
+ if (e.button > 0) return;
+ if (e.target.closest && e.target.closest(ignore)) return;
+ if (typeof inst.options.canDrag === 'function' && !inst.options.canDrag(e)) return;
+ inst.touched = true;
+ pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
+ try { el.setPointerCapture(e.pointerId); } catch { /* a pointer the browser no longer tracks */ }
+ e.preventDefault();
+ if (pts.size === 2) {
+ if (drag && drag.lifted) el.style.transform = '';
+ drag = null;
+ pinch = { d: gap(), w: inst.geom.w };
+ el.classList.add('is-resizing');
+ return;
+ }
+ if (pts.size > 2) return;
+ drag = {
+ sx: e.clientX, sy: e.clientY, dx: 0, dy: 0, r: el.getBoundingClientRect(), lifted: false,
+ hist: [{ x: e.clientX, y: e.clientY, t: performance.now() }],
+ };
+ };
+ const onMove = (e) => {
+ if (pts.has(e.pointerId)) pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
+ if (pinch && pts.size === 2) {
+ inst.geom.w = clamp(pinch.w * (gap() / pinch.d), inst.minW, inst.maxW, pinch.w);
+ paintNow(inst);
+ return;
+ }
+ if (!drag) return;
+ const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
+ if (!drag.lifted && Math.hypot(dx, dy) < DRAG_PX) return;
+ if (!drag.lifted) lift();
+ // Anywhere on screen; most of it may go past a side (that is how it tucks), never past the top or bottom.
+ const vp = viewport(inst);
+ const r = drag.r, vw = vp.width, vh = vp.height, tab = inst.options.tabPx == null ? TAB_PX : inst.options.tabPx;
+ drag.dx = Math.min(Math.max(dx, -r.left - r.width + tab), vw - r.left - tab);
+ drag.dy = Math.min(Math.max(dy, -r.top), vh - r.top - r.height);
+ const now = performance.now();
+ drag.hist.push({ x: e.clientX, y: e.clientY, t: now });
+ while (drag.hist.length > 2 && now - drag.hist[0].t > 90) drag.hist.shift();
+ if (!raf) raf = requestAnimationFrame(paint);
+ };
+ const onUp = (e) => {
+ pts.delete(e.pointerId);
+ try { el.releasePointerCapture(e.pointerId); } catch { /* */ }
+ if (pinch) {
+ if (pts.size < 2) {
+ pinch = null;
+ el.classList.remove('is-resizing');
+ // A pinch that began after the lift put the box over the page; put it back in its frame for its mode.
+ if (el.classList.contains('is-lifted')) {
+ el.classList.remove('is-dragging', 'is-lifted');
+ el.style.left = ''; el.style.top = ''; el.style.width = ''; el.style.transform = '';
+ placeIn(inst);
+ }
+ settleInst(inst);
+ paintNow(inst);
+ save(inst);
+ }
+ return;
+ }
+ if (!drag) return;
+ const d = drag; drag = null;
+ if (raf) { cancelAnimationFrame(raf); raf = 0; }
+ hint(false);
+ if (!d.lifted) {
+ // A tap: the bar for a moment (a touch screen has no hover); a second within 300 ms toggles the size.
+ const now = performance.now();
+ if (inst.options.doubleTap !== false && now - lastTap < TAP_MS) { lastTap = 0; toggleSize(inst); }
+ else {
+ lastTap = now;
+ if (e.pointerType !== 'mouse' && inst.options.activeBar !== false) showActive();
+ }
+ if (inst.geom.tuck && e.type === 'pointerup') untuck(inst);
+ return;
+ }
+ el.classList.remove('is-dragging', 'is-lifted');
+ // Where it was let go, and where a flick carries it (the last 90 ms, 120 ms on).
+ const fl = flick(d.hist, { left: d.r.left + d.dx, top: d.r.top + d.dy });
+ const from = { left: d.r.left + d.dx, top: d.r.top + d.dy, width: d.r.width, height: d.r.height };
+ const vp = viewport(inst);
+ const vhei = vp.height;
+ const top = Math.min(Math.max(0, fl.top), vhei - from.height);
+ const p = inst.frame ? contentRect(inst.frame) : null;
+ const fs = inFullscreen(inst);
+ const overPlayer = !!p && (fs || overFrame(fl.left + from.width / 2, top + from.height / 2, p));
+ const frame = overPlayer ? p : vp;
+ const tuckSide = tuckFor({ left: fl.left }, from, frame, fl);
+ el.style.transform = '';
+ el.style.left = ''; el.style.top = ''; el.style.width = '';
+ toMode(inst, overPlayer ? 'dock' : 'float', {
+ left: Math.min(Math.max(frame.left, fl.left), frame.right - from.width),
+ top,
+ width: from.width,
+ height: from.height,
+ });
+ inst.geom.tuck = tuckSide;
+ save(inst);
+ land(inst, from);
+ };
+ handle.addEventListener('pointerdown', onDown);
+ el.addEventListener('pointermove', onMove);
+ el.addEventListener('pointerup', onUp);
+ el.addEventListener('pointercancel', onUp);
+ inst.cleanups.push(() => {
+ handle.removeEventListener('pointerdown', onDown);
+ el.removeEventListener('pointermove', onMove);
+ el.removeEventListener('pointerup', onUp);
+ el.removeEventListener('pointercancel', onUp);
+ clearTimeout(activeTimer);
+ hint(false);
+ });
+ }
+
+ function wireKeys(inst) {
+ const el = inst.el;
+ const onKey = (e) => {
+ if (e.target !== el || e.altKey || e.ctrlKey || e.metaKey) return;
+ const step = e.shiftKey ? 0.1 : 0.02;
+ const k = e.key;
+ let moved = true;
+ if (k === 'ArrowLeft') inst.geom.x -= step;
+ else if (k === 'ArrowRight') inst.geom.x += step;
+ else if (k === 'ArrowUp') inst.geom.y -= step;
+ else if (k === 'ArrowDown') inst.geom.y += step;
+ else if (k === '+' || k === '=') inst.geom.w = clamp(inst.geom.w + 0.03, inst.minW, inst.maxW, inst.geom.w);
+ else if (k === '-' || k === '_') inst.geom.w = clamp(inst.geom.w - 0.03, inst.minW, inst.maxW, inst.geom.w);
+ else if (k === 'Home') { e.preventDefault(); reset(inst); return; }
+ else if (k === 't' || k === 'T' || k === 'h' || k === 'H') { e.preventDefault(); inst.geom.tuck ? untuck(inst) : tuck(inst); return; }
+ else if (k === 'f' || k === 'F') { e.preventDefault(); toggleMode(inst); return; }
+ else return;
+ e.preventDefault();
+ if (!moved) return;
+ const b = box(inst);
+ inst.geom.tuck = null;
+ inst.geom.x = Math.min(Math.max(0, inst.geom.x), Math.max(0, 1 - b.wf));
+ inst.geom.y = Math.min(Math.max(0, inst.geom.y), Math.max(0, 1 - b.hf));
+ paintNow(inst);
+ save(inst);
+ };
+ el.addEventListener('keydown', onKey);
+ inst.cleanups.push(() => el.removeEventListener('keydown', onKey));
+ }
+
+ function ensureTab(inst) {
+ let tab = inst.el.querySelector('[data-ov-float-tab]');
+ if (!tab) {
+ tab = document.createElement('button');
+ tab.type = 'button';
+ tab.className = 'ov-float-tab';
+ tab.dataset.ovFloatTab = '';
+ tab.setAttribute('aria-label', 'Bring it back');
+ tab.innerHTML = '<span class="ov-float-tab-l" aria-hidden="true">‹</span><span class="ov-float-tab-r" aria-hidden="true">›</span>';
+ inst.el.append(tab);
+ inst.ownedTab = tab;
+ }
+ const onClick = (e) => { e.preventDefault(); e.stopPropagation(); untuck(inst); };
+ tab.addEventListener('click', onClick);
+ inst.cleanups.push(() => tab.removeEventListener('click', onClick));
+ }
+
+ function ensureResize(inst) {
+ let handle = inst.el.querySelector('[data-ov-float-resize]');
+ if (!handle) {
+ handle = document.createElement('span');
+ handle.className = 'ov-float-resize';
+ handle.dataset.ovFloatResize = '';
+ handle.setAttribute('aria-hidden', 'true');
+ inst.el.append(handle);
+ inst.ownedResize = handle;
+ }
+ let start = null;
+ const onDown = (e) => {
+ if (e.button > 0) return;
+ const b = box(inst);
+ start = { px: e.clientX, py: e.clientY, w: inst.geom.w, rw: b.rw };
+ inst.touched = true;
+ inst.el.classList.add('is-resizing');
+ try { handle.setPointerCapture(e.pointerId); } catch { /* */ }
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ const onMove = (e) => {
+ if (!start || !start.rw) return;
+ // The handle's diagonal pull is the box's own width; `w` is the 16:9 width of the same area.
+ const k = inst.aspect ? Math.sqrt(inst.aspect / BASE) || 1 : 1;
+ const pull = inst.aspect ? ((e.clientX - start.px) + (e.clientY - start.py) * inst.aspect) / 2 : (e.clientX - start.px);
+ inst.geom.w = clamp(start.w + pull / start.rw / k, inst.minW, inst.maxW, start.w);
+ const b = box(inst);
+ inst.geom.x = Math.min(inst.geom.x, Math.max(0, 1 - b.wf));
+ inst.geom.y = Math.min(inst.geom.y, Math.max(0, 1 - b.hf));
+ paintNow(inst);
+ };
+ const end = (e) => {
+ if (!start) return;
+ start = null;
+ inst.el.classList.remove('is-resizing');
+ try { handle.releasePointerCapture(e.pointerId); } catch { /* */ }
+ settleInst(inst);
+ paintNow(inst);
+ save(inst);
+ };
+ handle.addEventListener('pointerdown', onDown);
+ handle.addEventListener('pointermove', onMove);
+ handle.addEventListener('pointerup', end);
+ handle.addEventListener('pointercancel', end);
+ inst.cleanups.push(() => {
+ handle.removeEventListener('pointerdown', onDown);
+ handle.removeEventListener('pointermove', onMove);
+ handle.removeEventListener('pointerup', end);
+ handle.removeEventListener('pointercancel', end);
+ });
+ }
+
+ function wireWindow(inst) {
+ const onResize = () => relayout(inst);
+ root.addEventListener('resize', onResize);
+ document.addEventListener('fullscreenchange', onResize);
+ let ro = null;
+ if (typeof ResizeObserver === 'function') {
+ ro = new ResizeObserver(onResize);
+ if (inst.frame) ro.observe(inst.frame);
+ ro.observe(inst.el);
+ }
+ inst.cleanups.push(() => {
+ root.removeEventListener('resize', onResize);
+ document.removeEventListener('fullscreenchange', onResize);
+ try { ro && ro.disconnect(); } catch { /* */ }
+ });
+ }
+
+ /* ── public API ───────────────────────────────────────────────────────────────────────────────────────── */
+
+ function attach(el, options) {
+ if (!el || typeof el !== 'object' || !el.classList) throw new TypeError('OVFloat.attach(el, options): el is required');
+ const o = options || {};
+ const frame = o.frame || null;
+ const key = String(o.key || `ov-float-${++uid}`);
+ const minW = clamp(o.minW == null ? MIN_W : o.minW, 0, 1, MIN_W);
+ const maxW = clamp(o.maxW == null ? MAX_W : o.maxW, minW, 1, MAX_W);
+ const defaults = normalizeDefaults(o.defaults, !!frame);
+ const inst = {
+ el, frame, key,
+ accountKey: o.accountKey == null ? key : String(o.accountKey),
+ options: o, defaults, minW, maxW,
+ aspect: o.aspect === null ? null : (typeof o.aspect === 'function' ? Number(o.aspect()) || BASE : (Number(o.aspect) > 0 ? Number(o.aspect) : BASE)),
+ minPx: Number(o.minPx) > 0 ? Number(o.minPx) : MIN_PX,
+ store: o.store === null ? null : (o.store || root.OVFloat.store || null),
+ geom: null, touched: false, destroyed: false, cleanups: [], landTimer: 0, ownedTab: null, ownedResize: null, api: null,
+ };
+ inst.geom = normalize(readSaved(key, o.legacyKeys), defaults, { hasFrame: !!frame, minW, maxW });
+ injectStyles();
+ el.classList.add('ov-float');
+ if (o.tab !== false) ensureTab(inst);
+ if (o.resize !== false) ensureResize(inst);
+ wireMove(inst);
+ wireKeys(inst);
+ wireWindow(inst);
+ placeIn(inst);
+ paintNow(inst);
+
+ inst.api = {
+ el, key,
+ geom: () => Object.assign({}, inst.geom),
+ set(patch, opts) {
+ Object.assign(inst.geom, patch || {});
+ if (opts && opts.settle) settleInst(inst);
+ placeIn(inst);
+ paintNow(inst);
+ if (opts && opts.save) save(inst);
+ return Object.assign({}, inst.geom);
+ },
+ setAspect(a) {
+ inst.aspect = a === null ? null : (Number(a) > 0 ? Number(a) : BASE);
+ paintNow(inst);
+ },
+ metrics: () => box(inst),
+ paint: () => paintNow(inst),
+ settle: () => { settleInst(inst); paintNow(inst); return Object.assign({}, inst.geom); },
+ save: () => save(inst),
+ tuck: (side) => tuck(inst, side),
+ untuck: () => untuck(inst),
+ toggleTuck: () => (inst.geom.tuck ? untuck(inst) : tuck(inst)),
+ toggleMode: () => toggleMode(inst),
+ toggleSize: () => toggleSize(inst),
+ reset: () => reset(inst),
+ relayout: () => relayout(inst),
+ destroy: () => destroy(inst),
+ };
+ instances.add(inst);
+ syncAccount(inst);
+ return inst.api;
+ }
+
+ function destroy(inst) {
+ if (inst.destroyed) return;
+ inst.destroyed = true;
+ instances.delete(inst);
+ clearTimeout(inst.landTimer);
+ for (const fn of inst.cleanups) { try { fn(); } catch { /* */ } }
+ inst.cleanups = [];
+ if (inst.ownedTab) { try { inst.ownedTab.remove(); } catch { /* */ } }
+ if (inst.ownedResize) { try { inst.ownedResize.remove(); } catch { /* */ } }
+ inst.el.classList.remove('ov-float', 'is-floating', 'is-lifted', 'is-dragging', 'is-tucked', 'is-tucked-left', 'is-tucked-right', 'is-resizing', 'is-active');
+ }
+
+ function injectStyles() {
+ if (document.getElementById('ov-float-styles')) return;
+ const st = document.createElement('style');
+ st.id = 'ov-float-styles';
+ st.textContent = CSS;
+ (document.head || document.documentElement).appendChild(st);
+ }
+
+ const defaultStore = makeDefaultStore();
+ root.OVFloat = {
+ attach,
+ store: defaultStore,
+ newStore: makeDefaultStore,
+ layout: () => [...instances].map((i) => ({ key: i.key, geom: Object.assign({}, i.geom), el: i.el })),
+ _test: {
+ boxOf, settle: settleGeom, flick, tuckFor, overFrame, paint, normalize,
+ BASE, MAX_H, MIN_W, MAX_W, EDGE_PX, SNAP, PROJECT_MS,
+ },
+ };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
+/* Generated by scripts/build-glyphs.js from glyphs.js: edit those, then run it. */
+/**
+ * openvibe-shared/ov-glyphs.js — the OpenVibe glyphs in place of Font Awesome (plan L32). A page that loads it keeps its
+ * markup (<i class="fa-solid fa-tower-broadcast">) and shows the network's own icon instead: the same hand as the mark
+ * (24-unit grid, 2-unit round strokes), in the text's colour and size. A name the glyphs do not have yet stays Font
+ * Awesome's. A brand logo (fa-brands) is drawn from brands.js (Simple Icons, CC0; 4.72.0), fetched from beside this
+ * file the first time a page shows one, filled in the text's colour with the brand's own as --ovg-brand (add
+ * ovg-brand-color to wear it). A spinner is three dots that take turns, never a spin.
+ *
+ * Font Awesome's features keep working without its stylesheet (4.62.0): sizes (fa-2xs … fa-2xl, fa-1x … fa-10x), fa-fw,
+ * fa-ul/fa-li, fa-border, fa-pull-*, fa-rotate-* and fa-flip-*, fa-stack, fa-layers with counters and text, the
+ * animations (fa-beat, fa-bounce, fa-fade, fa-beat-fade, fa-flip, fa-shake; fa-spin breathes), fa-thin and fa-light
+ * as thinner strokes, data-fa-transform (grow, shrink, up, down, left, right, rotate, flip-h, flip-v), and a title that
+ * names the icon to screen readers. Every variable Font Awesome documents (--fa-beat-scale …) is honoured. The glyphs'
+ * own motion: ovg-draw draws the lines in once, ovg-once plays an animation a single time (4.63.0).
+ *
+ * <script src="/shared/ov-glyphs.js" defer></script> swaps what is in the page, then anything added later
+ * OVGlyphs.swap(rootEl) swaps inside one element now
+ * OVGlyphs.fromFa('fa-eye') → 'eye' the glyph a Font Awesome name maps to, or null
+ *
+ * 272 glyphs for 449 Font Awesome names (openvibe.design/icons shows them).
+ */
+(function () {
+ 'use strict';
+ if (typeof window === 'undefined' || window.OVGlyphs) return;
+ const G = {"airplane":"<path d=\"M12 3.2c0.9 0 1.4 0.9 1.4 2.4V9.8l7 4.2V15.4l-7-2.4V17.2l1.9 1.4V20L12 19.1l-3.3 0.9v-1.4l1.9-1.4v-4.2L3.6 15.4v-1.4L10.6 9.8V5.6C10.6 4.2 11.1 3.2 12 3.2Z\"/>","alert":"<path d=\"M12 4.5L3.6 18.8h16.9Z\"/><path d=\"M12 10v3.7M12 16.5h0\"/>","alert-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5v5.5M12 16.5h.01\"/>","align-left":"<path d=\"M4 5.9h16.6M4 10h10.4M4 14.3h16.6M4 18.5h10.4\"/>","archive":"<rect x=\"3.5\" y=\"4.8\" width=\"17\" height=\"4.7\" rx=\"0.9\"/><path d=\"M5.4 9.5v9.4a0.9 0.9 0 0 0 0.9 0.9h11.3a0.9 0.9 0 0 0 0.9-0.9V9.5M10.1 13.3h3.8\"/>","arrow-down":"<path d=\"M12 3.2v16.7M5.7 13.7l6.3 6.3l6.3-6.3\"/>","arrow-left":"<path d=\"M20.8 12H4M10.3 5.7l-6.3 6.3l6.3 6.3\"/>","arrow-right":"<path d=\"M3.2 12h16.7M13.7 5.7l6.3 6.3l-6.3 6.3\"/>","arrow-up":"<path d=\"M12 20.8V4M5.7 10.3l6.3-6.3l6.3 6.3\"/>","arrows-h":"<path d=\"M3.8 12h16.5M7.6 8.1l-3.9 3.9l3.9 3.9M16.4 8.1l3.9 3.9l-3.9 3.9\"/>","at":"<circle cx=\"12\" cy=\"12\" r=\"3.4\"/><path d=\"M15.4 12v1.3a2.5 2.5 0 0 0 4.9 0V12a8.3 8.3 0 1 0-3.2 6.5\"/>","back":"<path d=\"M16.1 3.6l-8.4 8.4l8.4 8.4\"/>","ban":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m6 6 12 12\"/>","bell":"<path d=\"M6 14.6V9.5a6.1 6.1 0 0 1 12.2 0v5.1l1.5 2.1h-15.3ZM10 18.7a2.1 2.1 0 0 0 4 0\"/>","book":"<path d=\"M5 5.7a1.9 1.9 0 0 1 1.9-1.9h12.1v14.9H6.8a1.9 1.9 0 0 0-1.9 1.9Z\"/><path d=\"M5 18.7V5.7\"/>","book-open":"<path d=\"M12 6.3C10 4.8 7 4.3 3.5 4.9v13.2c3.5-0.6 6.5-0.1 8.5 1.4c2-1.5 5-2 8.5-1.4V4.9C17 4.3 14 4.8 12 6.3Z\"/><path d=\"M12 6.3v13.2\"/>","box":"<path d=\"M3.6 7.9L11.9 3.9l8.4 3.9v8.4L11.9 20.1l-8.4-3.9Z\"/><path d=\"M3.6 7.9L11.9 11.8l8.4-3.9M11.9 11.8v8.4M7.8 5.9l8.4 3.9\"/>","brain":"<path d=\"M9.2 4.2a2.8 2.8 0 0 0-2.8 2.8a2.8 2.8 0 0 0-1.9 4.8a2.8 2.8 0 0 0 1.9 4.8a2.8 2.8 0 0 0 5.8 1.1V5.8A1.9 1.9 0 0 0 9.2 4.2ZM14.9 4.2a2.8 2.8 0 0 1 2.8 2.8a2.8 2.8 0 0 1 1.9 4.8a2.8 2.8 0 0 1-1.9 4.8a2.8 2.8 0 0 1-5.8 1.1\"/>","branch":"<circle cx=\"6.7\" cy=\"5.6\" r=\"1.9\"/><circle cx=\"6.7\" cy=\"18.6\" r=\"1.9\"/><circle cx=\"17.9\" cy=\"7.5\" r=\"1.9\"/><path d=\"M6.7 7.5v9.3M17.9 9.3c0 5.1-6 3.7-10.2 7.9\"/>","bridge":"<path d=\"M3.5 16h17M7 19.5V5M17 19.5V5\"/><path d=\"M3.5 10c1.8 0 2.8-2.3 3.5-5c2.4 5.2 7.6 5.2 10 0c.7 2.7 1.7 5 3.5 5\"/><path d=\"M12 10.9V16\"/>","briefcase":"<rect x=\"3.5\" y=\"7.2\" width=\"17\" height=\"12.3\" rx=\"1.9\"/><path d=\"M9.2 7.2V5.7A1.4 1.4 0 0 1 10.6 4.3h2.8A1.4 1.4 0 0 1 14.8 5.7V7.2M3.5 12.3h7.1M13.4 12.3H20.5M10.6 11.4h2.8v2.4h-2.8Z\"/>","bug":"<rect x=\"7.2\" y=\"6.2\" width=\"9.6\" height=\"13.3\" rx=\"4.8\"/><path d=\"M9.3 6.5a2.7 2.7 0 0 1 5.3 0M12 10.9v8.5M7.2 12H3.5M20.5 12h-3.7M7.6 8.3L4.6 5.6M16.4 8.3L19.4 5.6M7.6 16.3L4.6 18.9M16.4 16.3L19.4 18.9\"/>","burst":"<path d=\"M11.9 3.7L13.6 7.8L17.8 6.2L16.2 10.4L20.3 12.1L16.2 13.8L17.8 18L13.6 16.4L11.9 20.4L10.2 16.4L6 18L7.6 13.8L3.6 12.1L7.6 10.4L6 6.2L10.2 7.8Z\"/>","busy":"<path d=\"M4.7 12h0M12.1 12h0M19.4 12h0\" stroke-width=\"4.2\"/>","calendar":"<rect x=\"4\" y=\"5.5\" width=\"16\" height=\"15\" rx=\"2\"/><path d=\"M4 10.5h16M9 3.5v4M15 3.5v4\"/>","camera":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><circle cx=\"12\" cy=\"13.3\" r=\"3.4\"/>","camera-flip":"<path d=\"M3.5 8h3.4l1.8-2.8h6.6L17.1 8h3.4v11.5h-17Z\"/><path d=\"M15 12.6a3.1 3.1 0 0 0-5.6-1.2M9 14.4a3.1 3.1 0 0 0 5.6 1.2M9.3 9.5v2h2M14.7 17.5v-2h-2\"/>","camera-off":"<path d=\"M3.7 7.9h3.3l1.7-2.7h6.4L16.9 7.9h3.3v11.2h-16.5Z\"/><circle cx=\"11.9\" cy=\"13\" r=\"3.3\"/><path d=\"M3.7 3.5l16.5 16.5\"/>","car":"<path d=\"M3.5 15.8v-4.5L6.2 5.8h11.6l2.7 5.5V15.8Z\"/><path d=\"M3.5 11.3h17M6.5 15.8v2.5M17.5 15.8v2.5M7.2 13.8h0M16.8 13.8h0\"/>","card":"<rect x=\"3.7\" y=\"5.8\" width=\"16.7\" height=\"12.3\" rx=\"1.8\"/><path d=\"M3.7 10.3h16.7M6.7 14.6h3.5\"/>","care":"<path d=\"M3.3 16.5h3.2l4.2 3.2h5.8a2.2 2.2 0 0 0 0-4.2H12.8M7.5 16.5l2.4-2.2h3.5\"/><path d=\"M16.6 10.6s-3.4-2.2-3.4-4.7a2.1 2.1 0 0 1 3.4-1.4a2.1 2.1 0 0 1 3.4 1.4c0 2.6-3.4 4.7-3.4 4.7Z\"/>","cart":"<path d=\"M3.5 4.1h1.9l2.2 10.2h10.2L20.3 6.9H6.5\"/><circle cx=\"9.1\" cy=\"18.6\" r=\"1.2\"/><circle cx=\"16.5\" cy=\"18.6\" r=\"1.2\"/>","cc":"<rect x=\"3.5\" y=\"5.4\" width=\"17\" height=\"13.2\" rx=\"2.4\"/><path d=\"M10.7 10.2a2.3 2.3 0 1 0 0 3.6M16.8 10.2a2.3 2.3 0 1 0 0 3.6\"/>","chart":"<path d=\"M3.9 3.4v16.7h16.7\"/><path d=\"M7.7 14.5l3.6-4.1l3.2 2.7l5.2-6.3\"/>","chat":"<path d=\"M3.8 4.3h16.8v11.7H8l-4.2 4.2Z\"/>","chat-off":"<path d=\"M4.2 5.1h15.7v10.9H8l-4 4Z\"/><path d=\"M3.7 3.7l16.7 16.7\"/>","check":"<path d=\"M3.7 12.1l5.4 5.4L20.4 6.1\"/>","check-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8 12.4 2.8 2.8L16 9.6\"/>","check-double":"<path d=\"M3.6 11.7l3.6 3.6L14.6 7.7M11.1 15.2l0.9 0.9L20.4 7.7\"/>","chevron":"<path d=\"M7.9 3.6l8.4 8.4l-8.4 8.4\"/>","chevrons-left":"<path d=\"M10.9 19.3l-7.3-7.3l7.3-7.3M20.4 19.3l-7.3-7.3l7.3-7.3\"/>","chevrons-right":"<path d=\"M3.6 19.3l7.3-7.3l-7.3-7.3M13.1 19.3l7.3-7.3l-7.3-7.3\"/>","child":"<circle cx=\"12\" cy=\"5.8\" r=\"2.1\"/><path d=\"M7.9 10.5L12 9.2l4.1 1.4M12 9.2v5.6M9.2 20.4l2.8-5.6l2.8 5.6\"/>","chip":"<rect x=\"7.3\" y=\"7.3\" width=\"9.4\" height=\"9.4\" rx=\"1.4\"/><path d=\"M10.1 3.5v3.8M13.9 3.5v3.8M10.1 16.7v3.8M13.9 16.7v3.8M3.5 10.1h3.8M3.5 13.9h3.8M16.7 10.1h3.8M16.7 13.9h3.8\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/>","city":"<path d=\"M3.5 19.7h17M5.4 19.7V8.3h4.7v11.3M10.1 19.7V3.6h5.7v16.1M15.8 19.7v-7.6h3.3v7.6M7.8 11.2h0M7.8 14.5h0M12.9 6.9h0M12.9 10.2h0M12.9 13.5h0\"/>","clapper":"<path d=\"M4 10.4h16.3v8.6a1 1 0 0 1-1 1h-14.4a1 1 0 0 1-1-1Z\"/><path d=\"M4 10.4L3.4 7.2l15.7-2.9l0.7 3.3ZM8.5 6.3l2.2 3.2M13.3 5.4l2.2 3.2\"/>","clipboard":"<rect x=\"5.7\" y=\"5\" width=\"12.6\" height=\"15.5\" rx=\"1.9\"/><path d=\"M9.1 5V3.5h5.8V5M9.1 10.8h5.8M9.1 14.7h3.9\"/>","clipboard-check":"<rect x=\"5.7\" y=\"4.9\" width=\"12.6\" height=\"15.5\" rx=\"1.9\"/><path d=\"M9.1 4.9V3.5h5.8V4.9M9.1 13.2l1.9 1.9l3.9-3.9\"/>","clock":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 7.5V12l3 2\"/>","close":"<path d=\"M3.7 3.7l16.7 16.7M20.4 3.7L3.7 20.4\"/>","cloud":"<path d=\"M7.8 17.6h8.9a3.4 3.4 0 0 0 0.5-7A5.2 5.2 0 0 0 6.9 10.1A3.7 3.7 0 0 0 7.8 17.6Z\"/>","cloud-off":"<path d=\"M7.5 17.6h9.5a3.8 3.8 0 0 0 0.5-7.5A5.7 5.7 0 0 0 6.4 9.6A4.1 4.1 0 0 0 7.5 17.6Z\"/><path d=\"M4 3.9l16.1 16.1\"/>","code":"<path d=\"M7.8 7.8l-4.2 4.2l4.2 4.2M16.2 7.8l4.2 4.2l-4.2 4.2M14.2 4.6l-4.2 14.8\"/>","coin":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M14.5 9.2c-.5-.8-1.4-1.2-2.5-1.2-1.5 0-2.5.8-2.5 2s1 1.6 2.5 2 2.5.8 2.5 2-1 2-2.5 2c-1.1 0-2-.4-2.5-1.2M12 6.5v11\"/>","columns":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"15\" rx=\"2\"/><path d=\"M12 4.5v15M3.5 9h17\"/>","commit":"<circle cx=\"12\" cy=\"12\" r=\"3.4\"/><path d=\"M3.5 12h5.1M15.4 12H20.5\"/>","compass":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m15.6 8.4-2.1 5.1-5.1 2.1 2.1-5.1Z\"/>","compress":"<path d=\"M8.8 3.5v5.3H3.5M15.2 3.5v5.3h5.3M8.8 20.5v-5.3H3.5M15.2 20.5v-5.3h5.3\"/>","computer":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"11.3\" rx=\"1.9\"/><path d=\"M8.2 19.7h7.6M12 15.9v3.8\"/>","contrast":"<circle cx=\"11.7\" cy=\"12\" r=\"8.4\"/><path d=\"M11.7 3.6v16.7M11.7 7.9h6.5M11.7 12h8.4M11.7 16.2h6.5\"/>","cookie":"<path d=\"M20.6 12.3a8.4 8.4 0 1 1-8.9-8.9a3 3 0 0 0 3.9 3.5a3 3 0 0 0 4.9 5.3Z\"/><path d=\"M8.8 9.4h0M12.2 13.8h0M8.3 15.3h0M15.7 15.3h0\"/>","copy":"<rect x=\"8\" y=\"8\" width=\"12.5\" height=\"12.5\" rx=\"2.1\"/><path d=\"M16.3 8V5.8a2.1 2.1 0 0 0-2.1-2.1H5.8a2.1 2.1 0 0 0-2.1 2.1v8.4a2.1 2.1 0 0 0 2.1 2.1h2.1\"/>","couch":"<path d=\"M5.4 10.1V7.2a1.9 1.9 0 0 1 1.9-1.9h9.4a1.9 1.9 0 0 1 1.9 1.9v2.8\"/><path d=\"M3.5 11.9a1.9 1.9 0 0 1 3.8 0v1.4h9.4V11.9a1.9 1.9 0 0 1 3.8 0v4.7H3.5ZM5.4 16.7v1.9M18.6 16.7v1.9\"/>","crown":"<path d=\"M3.5 8.3L7.8 12L11.9 5.5l4.2 6.5l4.2-3.7L18.4 18.1H5.4Z\"/>","cubes":"<path d=\"M12 3.2L8.1 5.2v4.4l3.9 2l3.9-2V5.2ZM7.6 11.6L3.6 13.6v4.4l3.9 2l3.9-2V13.6ZM16.4 11.6l-3.9 2v4.4l3.9 2l3.9-2V13.6Z\"/>","database":"<ellipse cx=\"12\" cy=\"6.4\" rx=\"6.6\" ry=\"2.8\"/><path d=\"M5.4 6.4v11.3c0 1.6 2.9 2.8 6.6 2.8s6.6-1.2 6.6-2.8V6.4M5.4 12c0 1.6 2.9 2.8 6.6 2.8s6.6-1.2 6.6-2.8\"/>","dice":"<rect x=\"3.7\" y=\"3.6\" width=\"16.7\" height=\"16.7\" rx=\"3\"/><path d=\"M8.6 8.6h0M15.5 8.6h0M12.1 12h0M8.6 15.4h0M15.5 15.4h0\" stroke-width=\"2.6\"/>","dish":"<path d=\"M3.8 9.2A10.9 10.9 0 0 0 14.8 20.2Z\"/><path d=\"M9 15l5.2-5.2M14.8 3.4a5.8 5.8 0 0 1 5.8 5.8M14.8 7.5a1.8 1.8 0 0 1 1.8 1.8\"/>","dollar":"<path d=\"M15.7 8C15 6.6 13.7 5.6 12 5.6C9.6 5.6 8.3 6.9 8.3 8.5s1.6 2.5 3.7 2.8s3.7 1.3 3.7 3.1s-1.5 3-3.7 3c-1.8 0-3.2-0.9-3.7-2.4M12 3.7v16.7\"/>","donate":"<circle cx=\"11.9\" cy=\"8.4\" r=\"5.2\"/><path d=\"M11.9 6v4.3M3.7 15.6h16.5V20H3.7ZM8.3 17.7h7.2\"/>","door":"<path d=\"M13.7 4.7L6.3 3.2v16.7l7.5-1.4ZM13.7 5.1h3.3a0.9 0.9 0 0 1 0.9 0.9v14M3.5 20h16.7M11 11.6h0\"/>","down":"<path d=\"M3.6 7.9l8.4 8.4l8.4-8.4\"/>","download":"<path d=\"M12 3.2v11.5M6.8 9.5l5.2 5.2l5.2-5.2M4.7 20h14.7\"/>","drive":"<rect x=\"3.5\" y=\"12.3\" width=\"17\" height=\"6.6\" rx=\"1.9\"/><path d=\"M3.9 12.3L6.8 4.7h10.4l2.8 7.6M16.7 15.6h0M13.8 15.6h0\"/>","drop":"<path d=\"M12 3.5s5.9 6.4 5.9 10.8a5.9 5.9 0 0 1-11.8 0c0-4.4 5.9-10.8 5.9-10.8Z\"/>","ear":"<path d=\"M6.2 9.5a5.7 5.7 0 1 1 10.8 2.4c-0.9 1.9-3.1 3-3.1 5.2a3.4 3.4 0 0 1-6.5 1.6\"/><path d=\"M9.7 9.9a2.4 2.4 0 1 1 3.8 1.5\"/>","enter":"<path d=\"M13.8 3.6h4.1a2.1 2.1 0 0 1 2.1 2.1v12.5a2.1 2.1 0 0 1-2.1 2.1h-4.1M9.5 7.9l4.1 4.1l-4.1 4.1M13.8 12H3.2\"/>","eraser":"<path d=\"M14.3 4.4l5.5 5.5l-9 9H6.6l-2.6-2.6a1.5 1.5 0 0 1 0-2.1Z\"/><path d=\"M9.3 9.4l5.5 5.5M10.8 18.9H20.3\"/>","expand":"<path d=\"M3.5 8.8V3.5h5.3M20.5 8.8V3.5h-5.3M3.5 15.2v5.3h5.3M20.5 15.2v5.3h-5.3\"/>","eye":"<path d=\"M3.5 12S6.6 6.2 12 6.2S20.5 12 20.5 12S17.4 17.8 12 17.8S3.5 12 3.5 12Z\"/><circle cx=\"12\" cy=\"12\" r=\"2.7\"/>","eye-off":"<path d=\"M3.7 12s3.2-6.1 8.3-6.1c1.5 0 2.7 0.5 4 1.1M20.5 12s-3.2 6.1-8.3 6.1c-1.5 0-2.7-0.5-4-1.1\"/><path d=\"M10.1 9.9a2.7 2.7 0 0 0 3.9 3.9M4.6 4.5l14.9 14.9\"/>","fast-forward":"<path d=\"M4 6.4v11.2L11.6 12ZM13.1 6.4v11.2L20.7 12Z\"/>","file":"<path d=\"M6.4 3.5h7.6l3.8 3.8v13.2H6.4Z\"/><path d=\"M13.9 3.5v3.8h3.8\"/>","file-code":"<path d=\"M13.9 3.5H7.4a1.9 1.9 0 0 0-1.9 1.9v13a1.9 1.9 0 0 0 1.9 1.9h9.3a1.9 1.9 0 0 0 1.9-1.9V8.2Z\"/><path d=\"M13.9 3.5v4.6h4.6M10.1 12.4L8.3 14.3l1.9 1.9M13.9 12.4l1.9 1.9l-1.9 1.9\"/>","film":"<rect x=\"3.5\" y=\"4.4\" width=\"17\" height=\"15.1\" rx=\"1.9\"/><path d=\"M7.7 4.4v15.1M16.3 4.4v15.1M3.5 9.2h4.3M3.5 14.8h4.3M16.3 9.2H20.5M16.3 14.8H20.5\"/>","filter":"<path d=\"M3.5 6h17M6.7 12.4h10.6M9.9 18.7h4.2\"/>","finish":"<path d=\"M5.2 20.7V4\"/><rect x=\"5.2\" y=\"4.6\" width=\"13.9\" height=\"8.6\" rx=\"1\"/><path d=\"M5.2 8.8h13.9M9.6 4.6v8.6M14.3 4.6v8.6\"/>","fire":"<path d=\"M12 20.5a6.8 6.8 0 0 0 6.8-6.8c0-4.3-2.9-6.8-4.2-10.1c-2 2.3-2.6 4.5-2.6 4.5S10.7 5.8 9.5 3.8C7 6.6 5.2 10 5.2 13.7a6.8 6.8 0 0 0 6.8 6.8Z\"/>","fish":"<path d=\"M8.1 12c1.9-3.1 4.3-4.6 7.2-4.6c2.5 0 4.3 1.9 5.2 4.6c-1 2.7-2.8 4.6-5.2 4.6c-2.9 0-5.2-1.6-7.2-4.6Z\"/><path d=\"M8.1 12L3.7 8.8v6.7Z\"/><path d=\"M16.5 10.7h0\"/>","fist":"<path d=\"M6.4 10.1V6.4a1.6 1.6 0 0 1 3.2 0V9.1M9.5 8.5v-3.2a1.6 1.6 0 0 1 3.2 0v3.2M12.6 8.5v-2.1a1.6 1.6 0 0 1 3.2 0V9.1M15.7 9.1V8a1.6 1.6 0 0 1 3.2 0v5.2a7.3 7.3 0 0 1-7.3 7.3H11.1a5.7 5.7 0 0 1-5.7-5.7V12.3A2.1 2.1 0 0 1 7.4 11.2h2.1a1.6 1.6 0 0 1 0 3.2H8\"/>","flag":"<path d=\"M6.7 20.7V4M6.7 4h10.8l-2 3.9l2 3.9H6.7\"/>","flask":"<path d=\"M9.5 3.2h4.9M10 3.2V9.2L4.9 17.7a1.5 1.5 0 0 0 1.3 2.3h11.6a1.5 1.5 0 0 0 1.3-2.3L14 9.2V3.2\"/><path d=\"M7.3 14.6h9.5\"/>","focus":"<circle cx=\"12\" cy=\"12\" r=\"2.8\"/><path d=\"M3.5 9V5.9A2.4 2.4 0 0 1 5.9 3.5H9M15 3.5h3.1A2.4 2.4 0 0 1 20.5 5.9V9M20.5 15v3.1a2.4 2.4 0 0 1-2.4 2.4H15M9 20.5H5.9A2.4 2.4 0 0 1 3.5 18.1V15\"/>","folder":"<path d=\"M3.8 7.3a1.9 1.9 0 0 1 1.9-1.9h3.6l1.9 1.9h7.4a1.9 1.9 0 0 1 1.9 1.9v7.4a1.9 1.9 0 0 1-1.9 1.9H5.8a1.9 1.9 0 0 1-1.9-1.9Z\"/>","folder-tree":"<path d=\"M4.1 3.3v13.2a1 1 0 0 0 1 1H9.7M4.1 8.3H9.7\"/><path d=\"M10.2 5.3h3.1l1 1.5h5.6V10.9h-9.7ZM10.2 14.5h3.1l1 1.5h5.6V20.1h-9.7Z\"/>","food":"<path d=\"M8.2 4v6.5M6.3 4v4.2a1.9 1.9 0 0 0 3.7 0V4M8.2 10.5v10.2M17.5 20.8V4c-1.9 1.1-3 3.3-3 6.5v2.8H17.5\"/>","game":"<rect x=\"3.7\" y=\"7.2\" width=\"16.7\" height=\"9.7\" rx=\"4.4\"/><path d=\"M7.6 10.7v2.7M6.4 12h2.7M15.1 11.6h0M17.4 12.9h0\"/>","gauge":"<path d=\"M3.6 16.9a8.4 8.4 0 1 1 16.7 0\"/><path d=\"M11.9 13.2l4.1-5.7\"/><circle cx=\"11.9\" cy=\"13.2\" r=\"1.4\"/>","gavel":"<path d=\"M13.7 4.1l5.1 5.1M10.5 7.4l5.1 5.1M12.1 5.8l-5.6 5.6l5 5l5.6-5.6M3.5 19.5l5.9-5.9M13.7 19.5h6.5\"/>","gear":"<circle cx=\"12\" cy=\"12.1\" r=\"2.3\"/><path d=\"M17.6 14.3a1.3 1.3 0 0 0 0.2 1.5l0.1 0.1a1.5 1.5 0 1 1-2.4 2.4l-0.1-0.1a1.3 1.3 0 0 0-1.5-0.2a1.3 1.3 0 0 0-0.8 1.3V18.8a1.5 1.5 0 1 1-3.4 0v-0.1a1.3 1.3 0 0 0-0.9-1.3a1.3 1.3 0 0 0-1.5 0.2l-0.1 0.1a1.5 1.5 0 1 1-2.4-2.4l0.1-0.1a1.3 1.3 0 0 0 0.2-1.5a1.3 1.3 0 0 0-1.3-0.8H5.4a1.5 1.5 0 1 1 0-3.4h0.1a1.3 1.3 0 0 0 1.3-0.9a1.3 1.3 0 0 0-0.2-1.5l-0.1-0.1a1.5 1.5 0 1 1 2.4-2.4l0.1 0.1a1.3 1.3 0 0 0 1.5 0.2H9.8a1.3 1.3 0 0 0 0.8-1.3V5.4a1.5 1.5 0 1 1 3.4 0v0.1a1.3 1.3 0 0 0 0.8 1.3a1.3 1.3 0 0 0 1.5-0.2l0.1-0.1a1.5 1.5 0 1 1 2.4 2.4l-0.1 0.1a1.3 1.3 0 0 0-0.2 1.5V9.8a1.3 1.3 0 0 0 1.3 0.8H18.7a1.5 1.5 0 1 1 0 3.4h-0.1a1.3 1.3 0 0 0-1.3 0.8Z\"/>","gem":"<path d=\"M6.8 4.9h10.3L20.2 9.5l-8.3 10.3L3.6 9.5Z\"/><path d=\"M3.6 9.5h16.7M9.6 4.9L8.1 9.5l3.8 10.3M14.2 4.9L15.7 9.5l-3.8 10.3\"/>","ghost":"<path d=\"M4.7 20V10.6a7.3 7.3 0 0 1 14.7 0v9.4l-2.4-1.9l-2.5 1.9l-2.4-1.9L9.6 20l-2.5-1.9Z\"/><path d=\"M9.4 10.1h0M14.6 10.1h0\"/>","gift":"<rect x=\"4.4\" y=\"8.8\" width=\"15.1\" height=\"10.4\" rx=\"0.9\"/><path d=\"M3.5 8.8h17M12 8.8v10.4M12 8.8c-1.4-2.8-4.7-3.8-4.7-1.4S12 8.8 12 8.8s4.7-0.9 4.7-3.3S13.4 6 12 8.8\"/>","globe":"<circle cx=\"11.9\" cy=\"11.9\" r=\"8.3\"/><path d=\"M3.6 11.9h16.7M11.9 3.6c2.7 2.7 2.7 13.9 0 16.7M11.9 3.6c-2.7 2.7-2.7 13.9 0 16.7\"/>","grid":"<rect x=\"3.5\" y=\"3.5\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"13.1\" y=\"3.5\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"3.5\" y=\"13.1\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/><rect x=\"13.1\" y=\"13.1\" width=\"7.4\" height=\"7.4\" rx=\"1.6\"/>","grip":"<path d=\"M8.3 4.5h0M15.8 4.5h0M8.3 12h0M15.8 12h0M8.3 19.5h0M15.8 19.5h0\" stroke-width=\"3.8\"/>","group":"<path d=\"M3.5 7.3V3.5h3.8M16.7 3.5h3.8v3.8M20.5 16.7v3.8h-3.8M7.3 20.5H3.5v-3.8\"/><rect x=\"7.3\" y=\"7.3\" width=\"5.7\" height=\"4.7\" rx=\"0.9\"/><rect x=\"11.1\" y=\"12\" width=\"5.7\" height=\"4.7\" rx=\"0.9\"/>","hammer":"<path d=\"M13.1 3.7L19.3 9.9l-2.7 2.7l-6.2-6.2Z\"/><path d=\"M13.6 9.4L5 18.1a1.3 1.3 0 0 0 1.9 1.9l8.6-8.6\"/>","hand":"<path d=\"M9.9 13.6V6.2a1.5 1.5 0 0 1 3 0V11.7M12.8 10.7V5.3a1.5 1.5 0 0 1 3 0V11.7M15.8 11.2V6.7a1.5 1.5 0 0 1 3 0v6.9c0 3.9-2.5 6.9-6.4 6.9c-2.6 0-4.1-1.3-5.6-3.5L4.9 14.2a1.5 1.5 0 0 1 2.4-1.8L9.9 15.6\"/>","handshake":"<path d=\"M3.6 9.9v5.3M20.3 9.9v5.3\"/><path d=\"M3.6 10.8h2.2l3.5-2.7l2.9 1.3L14.5 8.2l3.5 2.7h2.2\"/><path d=\"M5.7 10.8l6 6a1.2 1.2 0 0 0 1.7-1.6M13.4 15.3l0.5 0.5a1.2 1.2 0 0 0 1.7-1.6M15.6 14.2l0.3 0.3a1.2 1.2 0 0 0 1.7-1.6L18 10.8M17.5 12.8l-4-4l-2.3 2.1a1.3 1.3 0 0 1-1.9-1.9l1.9-2\"/>","hash":"<path d=\"M5 9h15M4 15h15M10 3.5 8 20.5M16 3.5l-2 17\"/>","heading":"<path d=\"M5.6 3.5v17M18.4 3.5v17M5.6 12h12.7M3.5 3.5h4.2M16.2 3.5h4.2M3.5 20.5h4.2M16.2 20.5h4.2\"/>","headphones":"<path d=\"M4.4 13.9v-2.8a7.5 7.5 0 0 1 14.9 0v2.8\"/><rect x=\"3.5\" y=\"13\" width=\"4.2\" height=\"6\" rx=\"1.4\"/><rect x=\"16.1\" y=\"13\" width=\"4.2\" height=\"6\" rx=\"1.4\"/>","heart":"<path d=\"M11.9 20.3s-8.1-5.1-8.1-11.6a4.6 4.6 0 0 1 8.1-3A4.6 4.6 0 0 1 20 9.2c0 6.5-8.1 11.6-8.1 11.6Z\"/>","heart-broken":"<path d=\"M11.9 20.3s-8.1-5.1-8.1-11.6a4.6 4.6 0 0 1 8.1-3A4.6 4.6 0 0 1 20 9.2c0 6.5-8.1 11.6-8.1 11.6Z\"/><path d=\"M11.9 5.8L10 9.8l3.5 2.3l-1.6 3.9\"/>","heart-pulse":"<path d=\"M19 12.3c-1.7 3.6-7 6.7-7 6.7s-5.3-3.1-7-6.7M4.3 9.7A4.3 4.3 0 0 1 12 7.4a4.3 4.3 0 0 1 7.7 2.2\"/><path d=\"M3.5 11.9h4l1.3-2.2l2.2 4.5l1.8-3.1h7.6\"/>","help":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M9.6 9.3a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.8M12 17h.01\"/>","history":"<path d=\"M3.8 12.1a8.3 8.3 0 1 0 2.5-5.9L3.8 8.5M3.8 4.2v4.2h4.2\"/><path d=\"M12 7.9v4.3l3.1 1.9\"/>","home":"<path d=\"M3.6 10.6L12 3.3l8.4 7.3v8.4a1 1 0 0 1-1 1h-4.2v-6.3h-6.3v6.3H4.7a1 1 0 0 1-1-1Z\"/>","hot-tub":"<circle cx=\"8.6\" cy=\"5.8\" r=\"1.7\"/><path d=\"M3.8 11.9h16.6v3.3a4.2 4.2 0 0 1-4.2 4.2h-8.4A4.2 4.2 0 0 1 3.8 15.2ZM6.8 11.9v-1.4A2.4 2.4 0 0 1 8.9 8.2H9.4M13.7 9.5c0.8-0.9-0.8-1.9 0-2.8M17.3 9.5c0.8-0.9-0.8-1.9 0-2.8\"/>","hourglass":"<path d=\"M6 3.5h12M6 20.5h12M7.5 3.5c0 4.5 4.5 5.5 4.5 8.5s-4.5 4-4.5 8.5M16.5 3.5c0 4.5-4.5 5.5-4.5 8.5s4.5 4 4.5 8.5\"/>","id":"<rect x=\"3.7\" y=\"5.5\" width=\"16.7\" height=\"13\" rx=\"1.9\"/><circle cx=\"9.2\" cy=\"11\" r=\"1.9\"/><path d=\"M6.4 15.6c0.5-1.4 1.6-2.2 2.8-2.2s2.4 0.8 2.8 2.2M14.8 10.1h2.8M14.8 13.4h2.8\"/>","idea":"<path d=\"M9.1 17.6h5.8M10.1 20.5h3.9M8.7 14.3A5.8 5.8 0 1 1 15.2 14.3c-0.7 0.6-1.4 1.5-1.4 2.3h-3.9c0-0.9-0.7-1.7-1.4-2.3Z\"/>","image":"<rect x=\"3.5\" y=\"5.4\" width=\"16.7\" height=\"13\" rx=\"2.8\"/><circle cx=\"9.1\" cy=\"10\" r=\"1.5\"/><path d=\"M20.3 15.6l-4.7-4.7l-7.4 7.4\"/>","inbox":"<path d=\"M3.5 12.7L5.9 4.8h12.1l2.4 7.9V17.8a0.9 0.9 0 0 1-0.9 0.9H4.4a0.9 0.9 0 0 1-0.9-0.9ZM3.5 12.7h4.6l1.4 2.4h4.6l1.4-2.4h4.6\"/>","infinity":"<path d=\"M12 12c-1.8-2.4-3.3-3.6-4.9-3.6a3.6 3.6 0 0 0 0 7.2c1.6 0 3.1-1.2 4.9-3.6Zm0 0c1.8 2.4 3.3 3.6 4.9 3.6a3.6 3.6 0 0 0 0-7.2c-1.6 0-3.1 1.2-4.9 3.6Z\"/>","info":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 11v5M12 8h.01\"/>","invite":"<circle cx=\"9.2\" cy=\"8.2\" r=\"2.9\"/><path d=\"M3.8 18.8c0-3 2.4-4.8 5.3-4.8s5.3 2 5.3 4.8M17.9 8.2v5.3M15.3 10.9h5.3\"/>","key":"<circle cx=\"8.1\" cy=\"15.9\" r=\"4.3\"/><path d=\"M11.1 12.9l9.4-9.4M16.8 7.3l3.2 3.2M14.6 9.4l2.2 2.2\"/>","keyboard":"<rect x=\"3.5\" y=\"6.6\" width=\"17\" height=\"10.7\" rx=\"1.8\"/><path d=\"M6.6 10.2h0M9.3 10.2h0M12 10.2h0M14.7 10.2h0M17.4 10.2h0M8.4 13.8h7.2\"/>","language":"<path d=\"M4.6 5.4h8.4M8.8 3.6v1.9M6.4 5.4c0.5 2.8 2.3 5.6 5.6 7M11.1 5.4c-0.6 3.2-2.6 6.3-6 7.9M12 20.3l3.7-8.4l3.7 8.4M13.3 17.5h4.8\"/>","laugh":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8 13.5h8a4 4 0 0 1-8 0ZM7.5 8.6l2.2 1.2-2.2 1.2M16.5 8.6l-2.2 1.2 2.2 1.2\"/>","layers":"<path d=\"M12 5.4l8.3 4.6l-8.3 4.6l-8.3-4.6Z\"/><path d=\"M3.8 14l8.3 4.6l8.3-4.6\"/>","leaf":"<path d=\"M3.8 20.1c0-9.5 5.6-15.2 16.7-16.7c-1 11.1-6.7 16.7-16.7 16.7ZM3.8 20.1l8.9-8.9\"/>","leave":"<path d=\"M12.7 3.5h4.2a2.1 2.1 0 0 1 2.1 2.1v12.7a2.1 2.1 0 0 1-2.1 2.1h-4.2M8.5 16.2l-4.2-4.2l4.2-4.2M4.2 12h10.6\"/>","lifebuoy":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"3.8\"/><path d=\"M6 6l3.4 3.4M14.6 14.7l3.4 3.4M18 6l-3.4 3.4M9.4 14.7l-3.4 3.4\"/>","link":"<path d=\"M9.9 14.1a4.3 4.3 0 0 0 6.1 0l3.2-3.2a4.3 4.3 0 0 0-6.1-6.1l-1.1 1.1M14.1 9.9a4.3 4.3 0 0 0-6.1 0l-3.2 3.2a4.3 4.3 0 0 0 6.1 6.1l1.1-1.1\"/>","link-off":"<path d=\"M10 13.8a4 4 0 0 0 5.6 0l2.9-2.9a4 4 0 0 0-5.6-5.6l-1 1M13.8 10a4 4 0 0 0-5.6 0l-2.9 2.9a4 4 0 0 0 5.6 5.6l1-1\"/><path d=\"M3.6 3.6l16.7 16.7\"/>","list":"<path d=\"M8.2 5.5h11.9M8.2 12h11.9M8.2 18.5h11.9M3.3 5.5h0M3.3 12h0M3.3 18.5h0\"/>","list-ordered":"<path d=\"M9.7 6.8h10.8M9.7 12.8h10.8M9.7 18.8h10.8M3.8 5.4L4.7 5v4.4M3.8 9.3h2M3.8 13.7a1 1 0 0 1 2 0.3c0 1.1-2 1.7-2 2.7h2.2\"/>","live":"<circle cx=\"12\" cy=\"11.8\" r=\"2\"/><path d=\"M8.6 8.4a4.9 4.9 0 0 0 0 7.1M15.4 8.4a4.9 4.9 0 0 1 0 7.1M6.3 6.1a8.1 8.1 0 0 0 0 11.9M17.7 6.1a8.1 8.1 0 0 1 0 11.9\"/>","lock":"<rect x=\"4.7\" y=\"10.7\" width=\"14.5\" height=\"9.4\" rx=\"2.1\"/><path d=\"M7.8 10.7V7.5a4 4 0 0 1 8.3 0v3.2\"/>","lungs":"<path d=\"M12 3.5v8M12 9.4l-3 2.1M12 9.4l3 2.1M8.4 6.5C5.6 6.5 3.5 10.4 3.5 15.2c0 2.8 1.1 4.8 3.2 4.8s2.7-2.1 2.7-4.8V8.3M15.6 6.5C18.4 6.5 20.5 10.4 20.5 15.2c0 2.8-1.1 4.8-3.2 4.8s-2.7-2.1-2.7-4.8V8.3\"/>","mail":"<rect x=\"3.5\" y=\"5.5\" width=\"17\" height=\"13.2\" rx=\"1.9\"/><path d=\"M4.4 7.4l7.6 5.7l7.6-5.7\"/>","map":"<path d=\"M9.1 4.6L3.5 6.5v13l5.6-1.9l5.6 1.9l5.6-1.9V4.6l-5.6 1.9Z\"/><path d=\"M9.1 4.6v13M14.7 6.5v13\"/>","mask":"<path d=\"M3.5 7c3 1.3 5.8 1.8 8.5 1.8S17.5 8.3 20.5 7c0 6-2.5 10-5 10-1.4 0-2-1.5-3.5-1.5S9.9 17 8.5 17c-2.5 0-5-4-5-10Z\"/><path d=\"M7.5 11.5h2M14.5 11.5h2\"/>","medical":"<rect x=\"3.5\" y=\"7.1\" width=\"17\" height=\"12.3\" rx=\"1.9\"/><path d=\"M9.2 7.1V5.2a0.9 0.9 0 0 1 0.9-0.9h3.8a0.9 0.9 0 0 1 0.9 0.9v1.9M12 10.4v5.7M9.2 13.3h5.7\"/>","megaphone":"<path d=\"M3.6 9.5v4a1 1 0 0 0 1 1H7.1l9 4.5v-15L7.1 8.5H4.5a1 1 0 0 0-1 1Z\"/><path d=\"M7.1 14.5l1.3 5h2.4L10.1 15.1M19.1 9a3.2 3.2 0 0 1 0 5\"/>","members":"<circle cx=\"9.2\" cy=\"7.9\" r=\"3\"/><path d=\"M3.6 19.1c0-3.1 2.6-5.1 5.6-5.1s5.6 2.1 5.6 5.1M15.7 5a3 3 0 0 1 0 5.9M17.6 14.3c1.8 0.6 2.8 2.3 2.8 4.8\"/>","menu":"<path d=\"M3.5 6.7h17M3.5 12h17M3.5 17.3h17\"/>","mic":"<rect x=\"9.2\" y=\"3.5\" width=\"5.7\" height=\"10.4\" rx=\"2.8\"/><path d=\"M5.4 11.1a6.6 6.6 0 0 0 13.2 0M12 17.7v2.8\"/>","mic-off":"<rect x=\"9.2\" y=\"3.6\" width=\"5.7\" height=\"10.4\" rx=\"2.8\"/><path d=\"M5.5 11.1a6.6 6.6 0 0 0 11 5M18.7 11.1c0 0.8-0.1 1.5-0.4 2.2M12.1 17.7v2.8M4.5 4.5l15.1 15.1\"/>","minus":"<path d=\"M3.5 12h17\"/>","minus-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8 12h8\"/>","money":"<rect x=\"3.5\" y=\"6.6\" width=\"17\" height=\"10.7\" rx=\"1.8\"/><circle cx=\"12\" cy=\"12\" r=\"2.2\"/><path d=\"M6.6 9.8v0M17.4 14.2v0\"/>","moon":"<path d=\"M20.5 14.6A8.6 8.6 0 1 1 9.4 3.5a6.9 6.9 0 0 0 11.2 11.2Z\"/>","more":"<circle cx=\"5.2\" cy=\"12\" r=\"1.6\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/><circle cx=\"18.8\" cy=\"12\" r=\"1.6\"/>","mountain":"<path d=\"M3.6 17.5L9.3 6l3.7 6.3L15.5 9.1L20.3 17.5Z\"/>","move":"<path d=\"M11.9 3.6v16.7M3.6 11.9h16.7M9 6.5l2.9-2.9l2.9 2.9M9 17.3l2.9 2.9l2.9-2.9M6.5 9l-2.9 2.9l2.9 2.9M17.3 9l2.9 2.9l-2.9 2.9\"/>","mug":"<path d=\"M4.8 9.1h11v7a4 4 0 0 1-4 4h-3a4 4 0 0 1-4-4Z\"/><path d=\"M15.8 10.5H17.3a2.5 2.5 0 0 1 0 5h-1.5M8.3 3.5c-0.8 1 0.8 2 0 3M11.8 3.5c-0.8 1 0.8 2 0 3\"/>","music":"<path d=\"M8.6 17.7V5.6l11.1-2.1v12.2\"/><circle cx=\"6\" cy=\"17.7\" r=\"2.6\"/><circle cx=\"17.2\" cy=\"15.7\" r=\"2.6\"/>","mute":"<path d=\"M3.9 9.4h3.6L12.2 5.8v12.4l-4.6-3.6H3.9Z\"/><path d=\"M16.3 9.9l4.1 4.1M20.5 9.9l-4.1 4.1\"/>","news":"<path d=\"M3.5 3.5h13.8v14.9a2.1 2.1 0 0 0 2.1 2.1H5.6a2.1 2.1 0 0 1-2.1-2.1Z\"/><path d=\"M17.3 7.7h3.2v10.6a2.1 2.1 0 0 1-2.1 2.1M7.2 7.7h6.4M7.2 11.5h6.4M7.2 15.2h3.7\"/>","nodes":"<circle cx=\"12\" cy=\"5.4\" r=\"2.1\"/><circle cx=\"5.8\" cy=\"17.9\" r=\"2.1\"/><circle cx=\"18.2\" cy=\"17.9\" r=\"2.1\"/><path d=\"M11 7.4L6.8 15.9M13 7.4l4.3 8.6M7.9 17.9h8.3\"/>","open":"<path d=\"M14 3.8h6.3v6.3M20.2 3.8l-9.4 9.4M18.2 14.2v4.2a2.1 2.1 0 0 1-2.1 2.1H5.6a2.1 2.1 0 0 1-2.1-2.1V7.9a2.1 2.1 0 0 1 2.1-2.1h4.2\"/>","palette":"<path d=\"M12.2 3.8a8.4 8.4 0 1 0 0 16.8c1.1 0 1.7-0.9 1.7-1.6c0-1.1-0.9-1.5-0.9-2.6s0.9-1.6 1.9-1.6H16.9a3.7 3.7 0 0 0 3.7-3.7C20.6 7.2 16.9 3.8 12.2 3.8Z\"/><circle cx=\"8\" cy=\"11.4\" r=\"1.1\"/><circle cx=\"10.3\" cy=\"7.9\" r=\"1.1\"/><circle cx=\"14.5\" cy=\"7.9\" r=\"1.1\"/>","parking":"<rect x=\"3.7\" y=\"3.7\" width=\"16.7\" height=\"16.7\" rx=\"3\"/><path d=\"M9.6 17V7.2h3.4a3 3 0 0 1 0 5.9H9.6\"/>","pause":"<path d=\"M7.1 3.5v17M16.9 3.5v17\"/>","paw":"<path d=\"M7.8 18.5c0-3 1.9-5.3 4.2-5.3s4.2 2.3 4.2 5.3c0 1.4-1.1 2.1-2.1 2.1c-0.8 0-1.4-0.5-2.1-0.5s-1.3 0.5-2.1 0.5c-1.1 0-2.1-0.7-2.1-2.1Z\"/><circle cx=\"5.4\" cy=\"10.6\" r=\"1.9\"/><circle cx=\"9.2\" cy=\"5.7\" r=\"1.9\"/><circle cx=\"14.8\" cy=\"5.7\" r=\"1.9\"/><circle cx=\"18.6\" cy=\"10.6\" r=\"1.9\"/>","pencil":"<path d=\"M11.9 19.6h8.4M16.1 4.2a2 2 0 0 1 2.8 2.8L7.2 18.7l-3.7 0.9l0.9-3.7Z\"/>","phone":"<path d=\"M5.7 3.4h3.9l2 4.9l-2.5 1.5a10.8 10.8 0 0 0 4.9 4.9L15.6 12.3l4.9 2v3.9a2 2 0 0 1-2 2A15.8 15.8 0 0 1 3.8 5.4a2 2 0 0 1 2-2\"/>","phone-off":"<path d=\"M5.6 4.1h3.7l1.9 4.7l-2.4 1.4a10.3 10.3 0 0 0 4.7 4.7L15.1 12.5l4.7 1.9v3.7a1.9 1.9 0 0 1-1.9 1.9A15.1 15.1 0 0 1 3.8 5.9a1.9 1.9 0 0 1 1.9-1.9\"/><path d=\"M4.3 3.6l16.1 16.1\"/>","piggy":"<path d=\"M3.5 11.4c0-3.2 3.3-5.7 7.7-5.7c1.3 0 2.4 0.2 3.4 0.6L17.4 4.7v3.3c0.8 0.7 1.4 1.6 1.6 2.5h1.4v3.4H18.8c-0.5 1-1.3 1.7-2.3 2.3V19.1h-2.9v-1.9c-0.8 0.2-1.7 0.3-2.5 0.3c-0.8 0-1.6-0.1-2.3-0.3v1.9h-2.9v-2.9C4.4 15.1 3.5 13.4 3.5 11.4Z\"/><path d=\"M14.9 10.1h0M8.7 8.1h2.9\"/>","pin":"<path d=\"M12 20.7s-6.2-5.4-6.2-10.5a6.2 6.2 0 0 1 12.4 0C18.2 15.2 12 20.7 12 20.7Z\"/><circle cx=\"12\" cy=\"10\" r=\"2.3\"/>","plane":"<path d=\"M20 4L3.5 10.8l6.6 3.1l3.1 6.6Z\"/><path d=\"M10.1 13.9l9.9-9.9\"/>","play":"<path d=\"M5.6 3.6v16.9l13.4-8.5Z\"/>","plug":"<path d=\"M9.2 4v4.6M14.8 4v4.6M6.9 8.7h10.2v2.8a5.1 5.1 0 0 1-10.2 0ZM12 16.6V20.7\"/>","plus":"<path d=\"M12 3.5v17M3.5 12h17\"/>","plus-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M12 8v8M8 12h8\"/>","print":"<path d=\"M7.1 8.9V3.5h9.9V8.9\"/><rect x=\"3.6\" y=\"8.9\" width=\"16.7\" height=\"7.9\" rx=\"2\"/><path d=\"M7.1 13.8h9.9v6.4H7.1ZM16.4 11.9h0\"/>","puzzle":"<path d=\"M3.8 7h4.2a2.6 2.6 0 1 1 4.8 0h4.2v4.2a2.6 2.6 0 1 1 0 4.8v4.2h-4.2a2.6 2.6 0 1 0-4.8 0H3.8v-4.2a2.6 2.6 0 1 0 0-4.8Z\"/>","quote":"<path d=\"M9.8 6.2C5.8 7.2 3.6 10 3.6 13.9v3.3h5.6v-5.6h-5.6M20.3 6.2c-3.9 1.1-6.1 3.9-6.1 7.8v3.3h5.6v-5.6h-5.6\"/>","radio":"<rect x=\"3.5\" y=\"8.3\" width=\"16.8\" height=\"11.3\" rx=\"1.9\"/><path d=\"M7.3 8.3L16.7 3.9\"/><circle cx=\"15.3\" cy=\"14\" r=\"2.4\"/><path d=\"M6.8 12.4h3.8M6.8 15.4h3.8\"/>","ranking":"<path d=\"M3.5 19.9h17M4.5 19.9V13.8h4.7M9.2 19.9V10h5.7v9.9M14.9 19.9V15.7h4.7v4.3\"/><path d=\"M12 3.4l0.9 1.7l1.9 0.3l-1.4 1.3l0.3 1.9L12 7.7l-1.7 0.9l0.3-1.9L9.3 5.4l1.9-0.3Z\"/>","record":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/>","recycle":"<path d=\"M7.8 18.2H5.7a1.2 1.2 0 0 1-1-1.8l2.3-3.9M10 5.5l0.9-1.5a1.2 1.2 0 0 1 2.1 0l2.3 3.9M17.7 12.4l1.6 2.7a1.2 1.2 0 0 1-1 1.8h-4.5\"/><path d=\"M11.1 16.5l1.8 1.8l-1.8 1.8M4.5 10.4l2.4-0.7l0.7 2.5M17.2 6.1l-0.7 2.5l-2.5-0.7\"/>","reply":"<path d=\"M9.2 5.8L4 11l5.1 5.1M4 11h10.3a6.2 6.2 0 0 1 6.2 6.2v1\"/>","restroom":"<circle cx=\"7.1\" cy=\"5.3\" r=\"1.7\"/><circle cx=\"16.4\" cy=\"5.3\" r=\"1.7\"/><path d=\"M11.7 3.6v16.7M5.6 20.3v-5.1H4.7V10.6A1.4 1.4 0 0 1 6.1 9.2h1.9a1.4 1.4 0 0 1 1.4 1.4v4.6H8.5V20.3M15.5 20.3v-3.7h-2.4l1.9-6.1a0.9 0.9 0 0 1 0.9-0.9h0.9a0.9 0.9 0 0 1 0.9 0.9l1.9 6.1H17.3v3.7\"/>","retry":"<path d=\"M3.7 12a8.2 8.2 0 0 1 14.1-5.9L20.2 8.4M20.2 3.8v4.6h-4.6M20.2 12a8.2 8.2 0 0 1-14.1 5.9L3.7 15.6M3.7 20.3v-4.6h4.6\"/>","rewind":"<path d=\"M20 6.4v11.2L12.4 12ZM10.9 6.4v11.2L3.3 12Z\"/>","road":"<path d=\"M8.2 3.5L4.4 20.5M15.8 3.5l3.8 17M12 4.4v2.4M12 10.6v2.8M12 17.2V19.6\"/>","robot":"<rect x=\"5.4\" y=\"8.3\" width=\"13.2\" height=\"10.4\" rx=\"2.8\"/><path d=\"M12 4.6v3.8M9.2 13.1h0M14.8 13.1h0M9.6 16.4h4.7M3.5 13.1v1.9M20.5 13.1v1.9\"/>","rocket":"<path d=\"M11.8 15.7L8.4 12.3C9.9 6.9 13.8 3.9 20.2 3.9C20.2 10.3 17.3 14.2 11.8 15.7ZM8.4 12.3L4.9 11.8L7.4 8.3h3.9M11.8 15.7l0.5 3.4l3.4-2.5v-3.9M6.4 17.7c-1.5 0-3 1.5-3 3c1.5 0 3-1.5 3-3Z\"/><circle cx=\"15.3\" cy=\"8.8\" r=\"1.5\"/>","route":"<circle cx=\"6\" cy=\"18\" r=\"2.4\"/><circle cx=\"18\" cy=\"6\" r=\"2.4\"/><path d=\"M8.4 18H16a3 3 0 0 0 0-6H8a3 3 0 0 1 0-6h7.6\"/>","rss":"<path d=\"M4 3.5a16.5 16.5 0 0 1 16.5 16.5M4 10.4A9.6 9.6 0 0 1 13.6 20.1\"/><path d=\"M4.6 19.4h0\" stroke-width=\"3.9\"/>","save":"<path d=\"M4.7 3.5h11.5l3.2 3.2v13.6H4.7Z\"/><path d=\"M7.8 3.5v5.2h7.3V3.5M7.8 20.2v-6.3h8.4v6.3\"/>","scale":"<path d=\"M12 4.3V19.5M8.5 19.5h6.9M5.5 7.4h13M12 4.3h0M3.7 13.4L5.9 7.8L8.1 13.4M15.9 13.4L18.1 7.8l2.2 5.6M3.7 13.4a2.2 2.2 0 0 0 4.3 0M15.9 13.4a2.2 2.2 0 0 0 4.3 0\"/>","scissors":"<circle cx=\"6\" cy=\"7\" r=\"2.6\"/><circle cx=\"6\" cy=\"17\" r=\"2.6\"/><path d=\"M8.2 8.6 20 18M8.2 15.4 20 6\"/>","scroll":"<path d=\"M8.3 3.8h10.1a1.9 1.9 0 0 1 1.9 1.9v1.5H17M8.3 3.8a1.9 1.9 0 0 0-1.9 1.9v11.7a2.9 2.9 0 0 1-2.9 2.9h10.7a2.9 2.9 0 0 0 2.9-2.9v-11.7a1.9 1.9 0 0 0-1.9-1.9M10.2 9.2h3.9M10.2 13h3.9\"/>","search":"<circle cx=\"10.6\" cy=\"10.6\" r=\"7\"/><path d=\"M20.4 20.4l-4.5-4.5\"/>","seedling":"<path d=\"M12 20.8v-9.8M12 11C12 6.7 9.3 4 4.4 4c0 4.3 2.7 7 7.6 7ZM12 13.7c0-4.3 2.7-7 7.6-7c0 4.3-2.7 7-7.6 7Z\"/>","server":"<rect x=\"3.5\" y=\"3.5\" width=\"17\" height=\"7.4\" rx=\"2.1\"/><rect x=\"3.5\" y=\"13.1\" width=\"17\" height=\"7.4\" rx=\"2.1\"/><path d=\"M7.8 7.2h0M7.8 16.8h0\"/>","shapes":"<circle cx=\"7.5\" cy=\"7.7\" r=\"3.4\"/><path d=\"M16.4 3.7l3.9 6.9h-7.9ZM13.9 14.1h5.9v5.9h-5.9ZM3.6 20l3.4-5.9l3.4 5.9Z\"/>","share":"<path d=\"M12 15.2V3.3M7.4 8.1L12 3.3L16.6 8.1M4.9 12.9v6.1a1 1 0 0 0 1 1h12.2a1 1 0 0 0 1-1v-6.1\"/>","shelter":"<path d=\"M3.7 10.5L12 4l8.3 6.5M5.9 9.2v10.4M18.1 9.2v10.4\"/><circle cx=\"12\" cy=\"11\" r=\"1.6\"/><path d=\"M9.8 19.6v-3.9a2.2 2.2 0 0 1 4.3 0V19.6\"/>","shield":"<path d=\"M12 3.7L5.5 6.6v5.6c0 4.1 2.8 7.1 6.5 8.4c3.7-1.3 6.5-4.2 6.5-8.4V6.6Z\"/>","shirt":"<path d=\"M8.2 3.8L3.7 6.6l1.8 3.8l1.8-0.9v11.2h9.3V9.4l1.8 0.9l1.8-3.8l-4.7-2.7c-0.5 1.4-1.8 2.3-3.8 2.3S8.8 5.1 8.2 3.8Z\"/>","signature":"<path d=\"M3.9 14c2.4 0 3.3-8.4 5.6-8.4c1.4 0 0.9 5.6 2.4 5.6c0.9 0 1.4-2.4 2.4-2.4s0.8 1.9 1.9 1.9c0.8 0 1.4-0.6 1.9-1.1M3.9 17.8h16.7\"/>","skip":"<path d=\"M4.8 3.5l10.8 8.4l-10.8 8.4Z\"/><path d=\"M19.7 3.5v16.7\"/>","skull":"<path d=\"M12 3.7a6.5 6.5 0 0 0-6.5 6.5c0 2.2 1 3.6 2.4 4.6v2.8h8.4v-2.8c1.3-1 2.4-2.6 2.4-4.6a6.5 6.5 0 0 0-6.5-6.5Z\"/><circle cx=\"9.6\" cy=\"10.7\" r=\"1.3\"/><circle cx=\"14.4\" cy=\"10.7\" r=\"1.3\"/><path d=\"M10.6 17.7v2.8M13.4 17.7v2.8\"/>","slash":"<path d=\"M3.6 3.6l16.7 16.7\"/>","sliders":"<path d=\"M3.5 6.8h10M18.7 6.8H20.3M3.5 17.2h3.6M12.4 17.2H20.3\"/><circle cx=\"16.2\" cy=\"6.8\" r=\"2.4\"/><circle cx=\"9.8\" cy=\"17.2\" r=\"2.4\"/>","smile":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"M8.5 14a4 4 0 0 0 7 0M9 9.5h.01M15 9.5h.01\"/>","snow":"<path d=\"M12 3.5v17M4.6 7.8l14.7 8.5M4.6 16.3l14.7-8.5M9.6 4.9L12 7.3l2.4-2.4M9.6 19.1L12 16.7l2.4 2.4\"/>","sort":"<path d=\"M3.5 5.8h9.2M3.5 12h6.2M3.5 18.2h3M17.5 4.8v14.4M14.3 16.2l3 3l3-3\"/>","sort-asc":"<path d=\"M3.4 5.4h3M3.4 11.6h6.3M3.4 17.9h9.3M17.5 4.3v14.6M14.3 16l3 3l3-3\"/>","spark":"<path d=\"M10.4 3.6l1.7 4.8L17 10.1l-4.8 1.7L10.4 16.7l-1.7-4.8L3.9 10.1l4.8-1.7ZM17 14.8l0.8 2.1L19.8 17.6l-2.1 0.8L17 20.3l-0.8-2.1L14.3 17.6l2.1-0.8Z\"/>","square":"<rect x=\"3.5\" y=\"3.5\" width=\"17\" height=\"17\" rx=\"2.7\"/>","star":"<path d=\"M12 4l2.6 5.3l5.7 0.6l-4.3 3.9l1.2 5.7L12 16.8l-5.2 2.9l1.2-5.7l-4.3-3.9l5.7-0.6Z\"/>","steps":"<path d=\"M9.7 5.7h11M9.7 11.8h11M9.7 17.8h11\"/><path d=\"M3.7 5.7l1.5 1.5L7.7 4.8M3.7 11.8l1.5 1.5L7.7 10.8M3.7 17.8l1.5 1.5L7.7 16.8\"/>","stop":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m8.5 8.5 7 7\"/>","store":"<path d=\"M3.7 8.8L5.3 3.7h13.4L20.3 8.8M3.7 8.8h16.5v1.6a2.8 2.8 0 0 1-5.4 0a2.8 2.8 0 0 1-5.6 0A2.8 2.8 0 0 1 3.7 10.4ZM4.8 12.9v7.2h14.5v-7.2M9.9 20.1v-4.1h4.1v4.1\"/>","storm":"<path d=\"M7.8 16a3.8 3.8 0 1 1 0.8-7.4A4.5 4.5 0 0 1 17.4 10.5a3.1 3.1 0 0 1-0.4 6.2\"/><path d=\"M12.9 12.2l-2.1 3.3h2.5L11.3 18.9\"/>","sun":"<circle cx=\"11.9\" cy=\"11.9\" r=\"3.4\"/><path d=\"M11.9 3.5v1.8M11.9 18.6v1.8M6 6l1.2 1.2M16.7 16.7l1.2 1.2M3.5 11.9h1.8M18.6 11.9h1.8M6 17.9l1.2-1.2M16.7 7.2l1.2-1.2\"/>","swap":"<path d=\"M3.5 7.8h14.9l-3.7-3.7M20.5 16.3H5.6l3.7 3.7\"/>","sword":"<path d=\"M20.3 3.7v3.7L10.4 17.3L6.7 13.6L16.7 3.7Z\"/><path d=\"M5.2 12L12 18.8M8.3 15.7L3.6 20.4\"/>","table":"<rect x=\"3.5\" y=\"4.3\" width=\"17\" height=\"15.1\" rx=\"1.9\"/><path d=\"M3.5 9.5h17M3.5 14.7h17M9.2 9.5V19.4M14.8 9.5V19.4\"/>","tack":"<path d=\"M9.3 4h5.6l-0.9 4.6l2.8 2.8H7.4l2.8-2.8ZM12 11.4v9.3\"/>","tag":"<path d=\"M3.8 11.2V3.8h7.4l9.3 9.3l-7.4 7.4Z\"/><circle cx=\"8\" cy=\"8\" r=\"1.2\"/>","target":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"12\" r=\"5\"/><circle cx=\"12\" cy=\"12\" r=\"1.5\"/>","tent":"<path d=\"M3.5 18.4h17M12 4.9L4.8 18.4M12 4.9l7.2 13.4M12 12.5L9.3 18.4M12 12.5l2.7 5.8\"/>","terminal":"<rect x=\"3.7\" y=\"4.6\" width=\"16.7\" height=\"14.9\" rx=\"1.9\"/><path d=\"M7.4 9.2l2.8 2.8l-2.8 2.8M12.9 14.8h3.7\"/>","text":"<path d=\"M5.2 20.3L10.9 3.6h2.1l5.7 16.7M7.4 14h9.2\"/>","thumbs-up":"<path d=\"M7.3 10.4v9.6H4.8a1 1 0 0 1-1-1v-7.6a1 1 0 0 1 1-1Z\"/><path d=\"M7.3 10.4L10.9 3.3a2.2 2.2 0 0 1 2.5 2.6L12.7 9.4H18.5a2 2 0 0 1 2 2.4l-1.3 6.6a2 2 0 0 1-2 1.6H7.3\"/>","ticket":"<path d=\"M3.6 7.4h16.7v2.8a1.9 1.9 0 0 0 0 3.7v2.8H3.6v-2.8a1.9 1.9 0 0 0 0-3.7Z\"/><path d=\"M13.8 7.4v9.3\"/>","timeline":"<path d=\"M3.5 12h0.9M8.2 12h1.9M13.9 12h1.9M19.6 12h0.9\"/><circle cx=\"6.3\" cy=\"12\" r=\"1.9\"/><circle cx=\"12\" cy=\"12\" r=\"1.9\"/><circle cx=\"17.7\" cy=\"12\" r=\"1.9\"/><path d=\"M6.3 10.1V5.9M12 13.9v4.3M17.7 10.1V5.9\"/>","toolbox":"<rect x=\"3.5\" y=\"8.1\" width=\"17\" height=\"11.3\" rx=\"1.9\"/><path d=\"M9.2 8.1V5.8A1.4 1.4 0 0 1 10.6 4.3h2.8A1.4 1.4 0 0 1 14.8 5.8V8.1M3.5 12.8h17M8.2 11.4v2.8M15.8 11.4v2.8\"/>","train":"<rect x=\"6.4\" y=\"3.8\" width=\"11.2\" height=\"12.1\" rx=\"2.8\"/><path d=\"M6.4 10.3h11.2M9.2 13.1h0M14.8 13.1h0M8.7 20.5l1.9-3.3M15.3 20.5l-1.9-3.3\"/>","trash":"<path d=\"M4.1 6.7h15.8M10 10.6v5.9M14 10.6v5.9M6.1 6.7l1 11.8a2 2 0 0 0 2 2h5.9a2 2 0 0 0 2-2l1-11.8M9 6.7V3.7h5.9v3\"/>","tree":"<path d=\"M12.1 3.6L7 10.6h2.8L5.6 16.6h13l-4.2-6h2.8ZM12.1 16.6v3.7\"/>","trend-down":"<path d=\"M3.2 7.2l5.6 5.6l3.8-3.8l7.4 7.4\"/><path d=\"M14.4 16.6h5.6v-5.6\"/>","trend-up":"<path d=\"M3.2 16.8L8.8 11.3l3.8 3.8l7.4-7.4\"/><path d=\"M14.4 7.4h5.6v5.6\"/>","trophy":"<path d=\"M7.9 4h8.4v5.2a4.1 4.1 0 0 1-8.4 0ZM7.9 6.1H4.7a3.2 3.2 0 0 0 3.2 4.1M16.2 6.1h3.2a3.2 3.2 0 0 1-3.2 4.1M12 13.4v4.1M7.9 20.8h8.4\"/>","tv":"<rect x=\"3.5\" y=\"7.8\" width=\"17\" height=\"11.6\" rx=\"1.8\"/><path d=\"M8.4 4.3l3.6 3.6l3.6-3.6\"/>","umbrella":"<path d=\"M3.8 12.5a8.4 8.4 0 0 1 16.6 0ZM12.1 12.5v6a1.9 1.9 0 0 1-3.7 0M12.1 4.1v0\"/>","undo":"<path d=\"M9.2 14.2L4 8.9l5.2-5.2\"/><path d=\"M4 8.9h11a5.8 5.8 0 0 1 0 11.5H11.3\"/>","up":"<path d=\"M3.7 16.1l8.3-8.3l8.3 8.3\"/>","upload":"<path d=\"M12 20.8V9.2M6.8 14.5l5.2-5.2l5.2 5.2M4.7 4h14.7\"/>","user":"<circle cx=\"12\" cy=\"7.4\" r=\"3.8\"/><path d=\"M4.3 20.5c0-3.9 3.4-6.6 7.7-6.6s7.7 2.6 7.7 6.6\"/>","user-check":"<circle cx=\"9.2\" cy=\"8.1\" r=\"2.8\"/><path d=\"M4 19c0-2.9 2.4-4.9 5.4-4.9s5.4 1.9 5.4 4.9M15.4 10.8l1.8 1.8l3.6-3.6\"/>","user-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><circle cx=\"12\" cy=\"10\" r=\"3\"/><path d=\"M6.6 18.3a6.5 6.5 0 0 1 10.8 0\"/>","user-lock":"<circle cx=\"9.8\" cy=\"7.3\" r=\"3.3\"/><path d=\"M3.5 18.8c0-3.4 2.8-5.7 6.2-5.7c1.2 0 2.2 0.3 3.1 0.7\"/><rect x=\"14.6\" y=\"14.5\" width=\"5.7\" height=\"4.8\" rx=\"1\"/><path d=\"M16 14.5v-1.2a1.5 1.5 0 0 1 2.9 0v1.2\"/>","user-minus":"<circle cx=\"10.1\" cy=\"8\" r=\"3.3\"/><path d=\"M4 19.3c0-3.3 2.7-5.7 6.1-5.7c1.1 0 2.2 0.3 3 0.7\"/><path d=\"M15.1 17h5.7\"/>","user-off":"<circle cx=\"12\" cy=\"8.2\" r=\"3.5\"/><path d=\"M5.1 20c0-3.6 3-5.9 6.9-5.9s6.9 2.3 6.9 5.9\"/><path d=\"M3.7 3.7l16.7 16.7\"/>","user-pen":"<circle cx=\"9.8\" cy=\"8\" r=\"3.3\"/><path d=\"M3.8 19.1c0-3.3 2.7-5.6 6-5.6c1.1 0 2.1 0.3 3 0.7\"/><path d=\"M19.1 12.1l1.4 1.4l-4.7 4.7l-1.9 0.5l0.5-1.9Z\"/>","user-tag":"<circle cx=\"10\" cy=\"7.6\" r=\"3.3\"/><path d=\"M3.8 18.9c0-3.3 2.8-5.6 6.1-5.6c1.1 0 2.2 0.3 3.1 0.7\"/><path d=\"M14.2 14.2h3.7l2.4 2.4l-2.4 2.4h-3.7Z\"/><path d=\"M16.1 16.5h0\"/>","video":"<rect x=\"3.4\" y=\"6.4\" width=\"12.3\" height=\"11.3\" rx=\"1.9\"/><path d=\"M15.6 10.2l4.7-2.8v9.4l-4.7-2.8\"/>","volume":"<path d=\"M3.5 9.5h3.7L11.8 5.8v12.6l-4.7-3.7H3.5Z\"/><path d=\"M15.5 9.5a3.7 3.7 0 0 1 0 5.2M18.1 6.9a7.3 7.3 0 0 1 0 10.5\"/>","walk":"<circle cx=\"12.3\" cy=\"5.1\" r=\"1.7\"/><path d=\"M8.8 20.3l2.4-5.5l1.9 2.2V20.3M11 14.8l0.9-5.2l-2.7 1.4l-1.4 2.7M11.9 9.6l1.8 2.7l2.7 0.9\"/>","wallet":"<path d=\"M3.5 6.8A2.7 2.7 0 0 1 6.2 4.2H18.4v4.2M3.5 6.8V18a2.1 2.1 0 0 0 2.1 2.1h14.9V8.4H6.2A2.7 2.7 0 0 1 3.5 6.8Z\"/><path d=\"M16.2 14.2h0\"/>","water":"<path d=\"M3.5 10.2c2.1 0 2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8s2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8M3.5 15.6c2.1 0 2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8s2.1-1.8 4.3-1.8s2.1 1.8 4.2 1.8\"/>","wave":"<path d=\"M3.7 16.2c2.1 0 2.1-1.7 4.2-1.7s2.1 1.7 4.2 1.7s2.1-1.7 4.2-1.7s2.1 1.7 4.2 1.7\"/><path d=\"M6.5 12.7A5.6 5.6 0 0 1 16.4 9.4c-1.9-0.4-3.5 0.3-4 2\"/>","weight":"<circle cx=\"12\" cy=\"5.2\" r=\"2\"/><path d=\"M7.1 8.7h9.9l3 11.3H4.1Z\"/>","wifi":"<path d=\"M3.7 9.4a12.4 12.4 0 0 1 16.7 0M6.4 12.5a8.4 8.4 0 0 1 11.5 0M9.1 15.6a4.4 4.4 0 0 1 6.2 0M12.1 18.6h0\"/>","wind":"<path d=\"M3.5 9.9h10.4a2.8 2.8 0 1 0-2.8-2.8M3.5 13.7h14.2a2.8 2.8 0 1 1-2.8 2.8M3.5 17.5h6.6\"/>","window":"<rect x=\"3.5\" y=\"4.5\" width=\"17\" height=\"15\" rx=\"2\"/><path d=\"M3.5 9h17\"/>","window-restore":"<rect x=\"3.5\" y=\"8.5\" width=\"12\" height=\"11\" rx=\"2\"/><path d=\"M8.5 8.5v-2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3\"/>","wizard":"<path d=\"M3.6 20h16.7M6.3 20L9.9 9.1L15.7 3.3L14.1 10.1L17.8 20\"/><path d=\"M8.4 15.4h7.3\"/>","wrench":"<path d=\"M14.6 3.6a5.2 5.2 0 0 0-4.6 7.3L3.7 17.3V20.3h3.1l6.4-6.4a5.2 5.2 0 0 0 7.3-4.6l-3.2 1.2l-2.5-2.5l1.2-3.2Z\"/>","x-circle":"<circle cx=\"12\" cy=\"12\" r=\"8.5\"/><path d=\"m9 9 6 6M15 9l-6 6\"/>","zap":"<path d=\"M13.4 3.5L5.9 12.9h5.7l-0.9 7.6l7.6-9.4h-5.7Z\"/>"};
+ const S = {"airplane":"<path d=\"M11.8 2.6c1.3 0 2 1.2 2 3V9.5l7.2 4.2v2.5L13.8 13.7v3.5l2 1.5v2.5L11.8 19.7l-3.7 1.2v-2.5l2-1.5v-3.5l-7.2 2.5v-2.5L9.8 9.5V5.3c0-1.8 0.7-3 2-3Z\"/>","alert":"<path fill-rule=\"evenodd\" d=\"M10.5 4.1a1.7 1.7 0 0 1 3 0l7.7 13.3a1.7 1.7 0 0 1-1.5 2.6H4.3a1.7 1.7 0 0 1-1.5-2.6ZM11 8.8h2v5.2h-2ZM12 15.2a1.2 1.2 0 1 1 0 2.4a1.2 1.2 0 0 1 0-2.4Z\"/>","alert-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.7 6.9h2.5v7.1h-2.5ZM12.9 15.3a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","archive":"<rect x=\"2.6\" y=\"3.4\" width=\"18.8\" height=\"5.4\" rx=\"1.2\"/><path fill-rule=\"evenodd\" d=\"M4.4 10h15.2v9.2a1.6 1.6 0 0 1-1.6 1.6H6a1.6 1.6 0 0 1-1.6-1.6ZM9.6 12.4v1.8h4.8v-1.8Z\"/>","bell":"<path d=\"M5.2 15.4V9.6a6.8 6.8 0 0 1 13.7 0v5.7l1.9 2.6H3.3ZM9.7 19.3h4.6a2.3 2.3 0 0 1-4.6 0Z\"/>","book":"<path d=\"M4.3 5.2A2.4 2.4 0 0 1 6.8 2.7h12.2v14.6H6.8a2.4 2.4 0 0 0-2.4 2.4Z\"/><path class=\"ln\" d=\"M7.2 20.2h11.2\"/>","book-open":"<path d=\"M10.9 5.8C8.8 4.4 6 3.8 2.5 4.5v14c3.5-0.6 6.3 0 8.4 1.5ZM13.1 5.8c2.1-1.5 5-2 8.4-1.4v14c-3.5-0.6-6.3 0-8.4 1.5Z\"/>","box":"<path d=\"M12 2.7l9.4 4.5l-9.4 4.5l-9.4-4.5ZM2.6 8.9l8.6 4.1v8.2l-8.6-4.3ZM21.4 8.9l-8.6 4.1v8.2l8.6-4.3Z\"/>","brain":"<path d=\"M8.9 3.8a3.2 3.2 0 0 0-3.2 3.1a3.1 3.1 0 0 0-2.2 5.3a3.1 3.1 0 0 0 2.2 5.2a3.2 3.2 0 0 0 5.7 1.3V5.6A2.3 2.3 0 0 0 8.9 3.8Z\"/><path d=\"M15.1 3.8a3.2 3.2 0 0 1 3.2 3.1a3.1 3.1 0 0 1 2.2 5.3a3.1 3.1 0 0 1-2.2 5.2a3.2 3.2 0 0 1-5.7 1.3V5.6A2.3 2.3 0 0 1 15.1 3.8Z\"/>","bridge":"<path d=\"M5.7 4.6h2.6v15.4H5.7ZM15.7 4.6h2.6v15.4h-2.6ZM3.4 14.6h17.2v2.8H3.4Z\"/><path class=\"ln\" d=\"M4 10c1.6 0 2.5-2.3 3-5c2.4 5.2 7.6 5.2 10 0c.5 2.7 1.4 5 3 5M12 10.9V15\"/>","briefcase":"<path d=\"M2.7 9.1a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.8H2.7ZM2.7 13.1h7.2v1.5h4.3v-1.5h7.2V17.9a2.5 2.5 0 0 1-2.5 2.5H5.2a2.5 2.5 0 0 1-2.5-2.5Z\"/><path class=\"ln\" d=\"M9.1 6.6V5.7A1.5 1.5 0 0 1 10.5 4.2h2.9A1.5 1.5 0 0 1 14.9 5.7v0.9\"/>","bug":"<path fill-rule=\"evenodd\" d=\"M12 7a4.8 4.8 0 0 1 4.8 4.8v3.4a4.8 4.8 0 0 1-9.6 0v-3.4A4.8 4.8 0 0 1 12 8Zm-0.7 4.4v7.4h1.4v-7.4Z\"/><path d=\"M9.4 5.8a2.6 2.6 0 0 1 5.2 0Z\"/><path class=\"ln\" d=\"M7.2 12.5H3.8M20.2 12.5h-3.4M7.6 9.4L5 7M16.4 9.4L19 7M7.6 16.6L5 19M16.4 16.6L19 19\"/>","burst":"<path d=\"M12.1 2.6l2 4.6l4.7-1.9l-1.9 4.7l4.6 2l-4.6 2l1.9 4.7l-4.7-1.9l-2 4.6l-2-4.6l-4.7 1.9l1.9-4.7L2.7 12l4.6-2l-1.9-4.7l4.7 1.9Z\"/>","calendar":"<path fill-rule=\"evenodd\" d=\"M4.1 8.1a2 2 0 0 1 2-2h11.8a2 2 0 0 1 2 2V19.5a2 2 0 0 1-2 2H6.1a2 2 0 0 1-2-2Zm2.2 3.9V19.3h11.4v-7.3Z\"/><path class=\"ln\" d=\"M9 3.9v3.5M15 3.9v3.5\"/>","camera":"<path fill-rule=\"evenodd\" d=\"M2.5 7.8a1.9 1.9 0 0 1 1.9-1.9h2.6L8.8 3.3h6.3l1.8 2.6h2.6A1.9 1.9 0 0 1 21.5 7.8v10.7a1.9 1.9 0 0 1-1.9 1.9H4.4A1.9 1.9 0 0 1 2.5 18.5Zm9.5 2a3.6 3.6 0 1 0 0 7.2a3.6 3.6 0 0 0 0-7.2Z\"/>","camera-flip":"<path fill-rule=\"evenodd\" d=\"M2.5 7.8a1.9 1.9 0 0 1 1.9-1.9h2.6L8.8 3.3h6.3l1.8 2.6h2.6A1.9 1.9 0 0 1 21.5 7.8v10.7a1.9 1.9 0 0 1-1.9 1.9H4.4A1.9 1.9 0 0 1 2.5 18.5ZM7.7 11.7A4.5 4.5 0 0 1 16.3 11.7L13.9 12.6A2 2 0 0 0 10.1 12.6ZM16.3 14.8A4.5 4.5 0 0 1 7.7 14.8L10.1 14A2 2 0 0 0 13.9 14Z\"/>","camera-off":"<path d=\"M8.5 4.6L9 3.8h6.1l1.7 2.5h2.5A1.8 1.8 0 0 1 21.2 8.1v9.1ZM3.8 7.3L16.7 20.2H4.8A1.8 1.8 0 0 1 3 18.4V8.1Z\"/><path class=\"ln\" d=\"M4 3.8l16.2 16.2\"/>","car":"<path fill-rule=\"evenodd\" d=\"M5.6 5.2h12.7l2.7 5.5H3ZM2.6 12.2h18.7v4.5H2.6ZM5.9 14.4a1.1 1.1 0 1 0 2.3 0a1.1 1.1 0 1 0-2.3 0ZM15.8 14.4a1.1 1.1 0 1 0 2.3 0a1.1 1.1 0 1 0-2.3 0ZM5 17.5h2.7v1.8H5ZM16.3 17.5h2.7v1.8h-2.7Z\"/>","card":"<path fill-rule=\"evenodd\" d=\"M2.6 7.4a2.4 2.4 0 0 1 2.4-2.4h13.9a2.4 2.4 0 0 1 2.4 2.4v9.3a2.4 2.4 0 0 1-2.4 2.4h-13.9a2.4 2.4 0 0 1-2.4-2.4ZM2.6 9.5v1.7h18.7V9.5ZM5.9 14.2v1.5h4.8v-1.5Z\"/>","care":"<path d=\"M17 11s-4.1-2.4-4.1-5.4a2.3 2.3 0 0 1 4.1-1.6a2.3 2.3 0 0 1 4.1 1.6c0 2.9-4.1 5.4-4.1 5.4Z\"/><path class=\"ln\" d=\"M3.5 16.2h3.2l4.3 3.2h5.9a2.1 2.1 0 0 0 0-4.3H13.3M7.9 16.2l2.3-2.1h3.6\"/>","cart":"<path d=\"M6.3 7h15l-2.5 8.3H8.1Z\"/><circle cx=\"9.5\" cy=\"19.4\" r=\"1.6\"/><circle cx=\"17\" cy=\"19.4\" r=\"1.6\"/><path class=\"ln\" d=\"M3.8 4.8H5.8l2.3 10.5\"/>","cc":"<path fill-rule=\"evenodd\" d=\"M5.6 4.6h12.7a3 3 0 0 1 3 3v8.8a3 3 0 0 1-3 3h-12.7a3 3 0 0 1-3-3v-8.8a3 3 0 0 1 3-3ZM10.8 9.5A3.2 3.2 0 1 0 10.8 14.5L9.6 13A1.3 1.3 0 1 1 9.6 11ZM17.4 9.5A3.2 3.2 0 1 0 17.4 14.5L16.2 13A1.3 1.3 0 1 1 16.2 11Z\"/>","chart":"<path class=\"ln\" d=\"M4 3.5v16.2h16.2\"/><path d=\"M7 14.9L11 10.3l3 2.5l5.2-6.2V17.7H7Z\"/>","chat":"<path d=\"M2.7 4.7A1.8 1.8 0 0 1 4.5 3h15.2a1.8 1.8 0 0 1 1.8 1.8v10.9a1.8 1.8 0 0 1-1.8 1.8H8.3L2.7 21.8Z\"/>","chat-off":"<path d=\"M8.1 4.6H19a1.6 1.6 0 0 1 1.6 1.6v9.5a1.6 1.6 0 0 1-0.4 1ZM3.4 7.3L12.9 16.8H8.3l-4.9 4.6Z\"/><path class=\"ln\" d=\"M4 4.2l16 16\"/>","check-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM17.3 8.9l1.6 1.6l-7 7l-4.1-4.1l1.6-1.6l2.6 2.6Z\"/>","child":"<circle cx=\"12\" cy=\"5.2\" r=\"2.6\"/><path class=\"ln\" d=\"M7.7 10L12 8.6l4.3 1.4M12 8.6v5.8M9.1 20.1l2.9-5.8l2.9 5.8\"/>","chip":"<path fill-rule=\"evenodd\" d=\"M8.6 7.2h6.8a1.4 1.4 0 0 1 1.4 1.4v6.8a1.4 1.4 0 0 1-1.4 1.4H8.6a1.4 1.4 0 0 1-1.4-1.4V8.6a1.4 1.4 0 0 1 1.4-1.4Zm1.7 3.1v3.4h3.4v-3.4Z\"/><path class=\"ln\" d=\"M10.2 4v3.2M13.8 4v3.2M10.2 16.8V20M13.8 16.8V20M4 10.2h3.2M4 13.8h3.2M16.8 10.2H20M16.8 13.8H20\"/>","circle":"<circle cx=\"12\" cy=\"12\" r=\"9.5\"/>","city":"<path fill-rule=\"evenodd\" d=\"M5.4 7.7h4.6v10.9H5.4Zm1.9 2.2v1.4h1.2v-1.4Zm0 2.9v1.4h1.2v-1.4Z\"/><path fill-rule=\"evenodd\" d=\"M10.7 3.4h5.1v15.2h-5.1Zm1.7 2.4v1.4h1.4V5.8Zm0 2.9v1.4h1.4V8.7Zm0 2.9v1.4h1.4v-1.4Z\"/><path d=\"M16.5 11.3h2.2v7.4h-2.2Z\"/><path class=\"ln\" d=\"M3.9 18.6h16.1\"/>","clapper":"<path fill-rule=\"evenodd\" d=\"M2.6 5.9L19.9 2.7l0.9 4.3L3.2 8.7ZM7 5.1L9.2 8.1l2.6-0.2L9.6 4.6ZM12.6 4l2.2 3.6l2.6-0.3L15.2 3.6Z\"/><path d=\"M3.1 9.9h18.2v9a1.6 1.6 0 0 1-1.6 1.6h-15a1.6 1.6 0 0 1-1.6-1.6Z\"/>","clipboard":"<path fill-rule=\"evenodd\" d=\"M7.5 4.2h9a2 2 0 0 1 2 2v13.1a2 2 0 0 1-2 2H7.5a2 2 0 0 1-2-2V6.3a2 2 0 0 1 2-2ZM9.1 9.9v1.6h5.8v-1.6Zm0 3.9v1.6h3.9v-1.6Z\"/><rect x=\"8.7\" y=\"2.5\" width=\"6.6\" height=\"3.7\" rx=\"1.2\"/>","clipboard-check":"<path fill-rule=\"evenodd\" d=\"M7.5 4.2h9a2 2 0 0 1 2 2v13.1a2 2 0 0 1-2 2H7.5a2 2 0 0 1-2-2V6.3a2 2 0 0 1 2-2ZM8 13.1L11 16.1l5-5l-1.6-1.6l-3.4 3.4l-1.5-1.5Z\"/><rect x=\"8.7\" y=\"2.5\" width=\"6.6\" height=\"3.7\" rx=\"1.2\"/>","clock":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.9 6.6h2.3v5l3.3 2l-1.1 2l-4.3-2.7Z\"/>","cloud":"<path d=\"M7.9 18.2h9.6a4.2 4.2 0 0 0 0.6-8.3A6.1 6.1 0 0 0 6.5 9.2A4.5 4.5 0 0 0 7.9 18.2Z\"/>","cloud-off":"<path d=\"M10.1 5.7A6 6 0 0 1 17.9 9.8a4.1 4.1 0 0 1 2.8 6.5ZM6.6 9.1L15.5 18H7.9A4.4 4.4 0 0 1 6.4 9.2Z\"/><path class=\"ln\" d=\"M5.1 4.2l15 15\"/>","coin":"<path fill-rule=\"evenodd\" d=\"M13 2.6a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM12 5.8h2.1v1.2c1.1 0.2 2.1 0.8 2.6 1.7l-1.7 1c-0.3-0.6-1-1-1.8-1c-0.9 0-1.5 0.4-1.5 1s0.5 0.8 1.8 1.1c1.8 0.4 3.5 1 3.5 3.1c0 1.4-1 2.5-2.8 2.8V18h-2.1v-1.3c-1.2-0.2-2.3-0.9-2.8-2l1.7-1c0.4 0.7 1.1 1.1 2.1 1.1c1 0 1.6-0.4 1.6-1s-0.6-0.8-2-1.1c-1.7-0.4-3.4-1-3.4-3c0-1.3 1-2.4 2.7-2.7Z\"/>","columns":"<path d=\"M2.6 6.3a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v1.5H2.6ZM2.6 9.9h8.3v10.4H5.3a2.7 2.7 0 0 1-2.7-2.7ZM13 9.9h8.3v7.7a2.7 2.7 0 0 1-2.7 2.7H13Z\"/>","compass":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM17.4 7.7L14.9 14.1L8.5 16.5L11 10.1Z\"/>","compress":"<path d=\"M9.7 9.7H2.7L9.7 2.7ZM14.3 9.7h7l-7-7ZM9.7 14.3H2.7l7 7ZM14.3 14.3h7l-7 7Z\"/>","computer":"<rect x=\"2.6\" y=\"3.4\" width=\"18.8\" height=\"12.8\" rx=\"2.2\"/><path class=\"ln\" d=\"M8 20h8M12 16.2v3.8\"/>","contrast":"<path fill-rule=\"evenodd\" d=\"M12.4 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 2.1a7.2 7.2 0 0 1 0 14.4Z\"/>","cookie":"<path fill-rule=\"evenodd\" d=\"M21.6 12.4a9.4 9.4 0 1 1-10-10a3.4 3.4 0 0 0 4.4 3.9a3.4 3.4 0 0 0 5.5 5.9ZM7 9.1a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM10.9 14a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM6.5 15.7a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0ZM14.8 15.7a1.3 1.3 0 1 0 2.7 0a1.3 1.3 0 1 0-2.7 0Z\"/>","copy":"<rect x=\"7.5\" y=\"7.5\" width=\"13.8\" height=\"13.8\" rx=\"2.5\"/><path class=\"ln\" d=\"M16.4 7.5V5.9a2.1 2.1 0 0 0-2.1-2.1H5.9a2.1 2.1 0 0 0-2.1 2.1v8.5a2.1 2.1 0 0 0 2.1 2.1h1.7\"/>","couch":"<path d=\"M7.1 4.2h9.7A2.5 2.5 0 0 1 19.4 6.7v1.9a3.1 3.1 0 0 0-3.1 3.1v0.1H7.7v-0.1A3.1 3.1 0 0 0 4.6 8.7V6.7A2.5 2.5 0 0 1 7.1 4.2ZM2.7 11.6a2.5 2.5 0 0 1 5.1 0v1.9h8.6V11.6a2.5 2.5 0 0 1 5.1 0v5.5H2.7Z\"/><path class=\"ln\" d=\"M5.2 17.1v1.9M18.8 17.1v1.9\"/>","crown":"<path d=\"M2.6 7.3L7.4 11.5L12 4.1L16.6 11.5l4.8-4.2l-2 11.7H4.6Z\"/>","cubes":"<path d=\"M12 2.3L7.8 4.4v4.8l4.2 2.1l4.2-2.1V4.4ZM6.8 12.1L2.7 14.2v4.8l4.2 2.1l4.2-2.1v-4.8ZM17.2 12.1l-4.2 2.1v4.8l4.2 2.1l4.2-2.1v-4.8Z\"/>","database":"<ellipse cx=\"12\" cy=\"5.7\" rx=\"7.9\" ry=\"3.2\"/><path d=\"M4.1 7.5a7.9 3.2 0 0 0 15.9 0v4.3a7.9 3.2 0 0 1-15.9 0ZM4.1 13.4a7.9 3.2 0 0 0 15.9 0v4.7a7.9 3.2 0 0 1-15.9 0Z\"/>","dice":"<path fill-rule=\"evenodd\" d=\"M6.2 2.7h11.5a3.6 3.6 0 0 1 3.6 3.6v11.5a3.6 3.6 0 0 1-3.6 3.6H6.2a3.6 3.6 0 0 1-3.6-3.6V6.2a3.6 3.6 0 0 1 3.6-3.6ZM6.2 8.2a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM13.8 8.2a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM10 12a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM6.2 15.8a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0ZM13.8 15.8a2 2 0 1 0 3.9 0a2 2 0 1 0-3.9 0Z\"/>","dish":"<path d=\"M2.7 9.7A11.7 11.7 0 0 0 14.4 21.4Z\"/><path class=\"ln\" d=\"M8.8 15.3l4.9-4.9M14.2 4a5.8 5.8 0 0 1 5.8 5.8M14.2 8.1a1.8 1.8 0 0 1 1.8 1.8\"/>","donate":"<path fill-rule=\"evenodd\" d=\"M12 2.2a5.9 5.9 0 1 0 0 11.8a5.9 5.9 0 0 0 0-11.8Zm-0.8 3.4h1.7v5.1h-1.7ZM2.9 15h18.2v6H2.9ZM7.8 17.1v1.6h8.5V17.1Z\"/>","door":"<path fill-rule=\"evenodd\" d=\"M14.3 4.5L6.2 3v17.1l8.2-1.5ZM10.2 11.5a1 1 0 1 0 2 0a1 1 0 1 0-2 0Z\"/><path class=\"ln\" d=\"M15.5 5.3h1.4a0.9 0.9 0 0 1 0.9 0.9v13.3M15.5 19.5H20M4 19.5h2.1\"/>","download":"<path d=\"M10.6 2.3h2.5v7h4.7L11.9 15.4l-5.9-5.9h4.7Z\"/><path class=\"ln\" d=\"M4.5 19.7h14.9\"/>","drive":"<path fill-rule=\"evenodd\" d=\"M5.3 11.4h13.5a2.7 2.7 0 0 1 2.7 2.7v3a2.7 2.7 0 0 1-2.7 2.7H5.3a2.7 2.7 0 0 1-2.7-2.7v-3a2.7 2.7 0 0 1 2.7-2.7ZM16.8 14.4a1.1 1.1 0 1 1 0 2.3a1.1 1.1 0 0 1 0-2.3ZM13.7 14.4a1.1 1.1 0 1 1 0 2.3a1.1 1.1 0 0 1 0-2.3Z\"/><path class=\"ln\" d=\"M4.1 11.9L6.8 4.7h10.4l2.8 7.2\"/>","drop":"<path d=\"M12 2.2s7 7.4 7 12.6a7 7 0 0 1-13.9 0C5 9.6 12 2.2 12 2.2Z\"/>","eraser":"<path d=\"M14 3.9l6.1 6.1-5.2 5.2-6.1-6.1ZM7.4 10l6.2 6.2-3.2 3.2H6.1l-2.9-2.9a1.6 1.6 0 0 1 0-2.3Z\"/><path class=\"ln\" d=\"M13 19.4h7\"/>","expand":"<path d=\"M2.7 2.7h7L2.7 9.7ZM21.3 2.7h-7l7 7ZM2.7 21.3h7l-7-7ZM21.3 21.3h-7l7-7Z\"/>","eye":"<path fill-rule=\"evenodd\" d=\"M12 5.3C6.4 5.3 2.6 12 2.6 12s3.9 6.7 9.4 6.7S21.5 12 21.5 12S17.6 5.3 12 5.3Zm0 3.3a3.3 3.3 0 1 1 0 6.7a3.3 3.3 0 0 1 0-6.7Z\"/><circle cx=\"12\" cy=\"12\" r=\"1.6\"/>","eye-off":"<path d=\"M9 5.8A10 10 0 0 1 21.4 12a10 10 0 0 1-2.5 3.8L15.3 12.2a3.3 3.3 0 0 0-3.5-3.5ZM15 18.2A10 10 0 0 1 2.6 12a10 10 0 0 1 2.5-3.8L8.7 11.8a3.3 3.3 0 0 0 3.5 3.5Z\"/><path class=\"ln\" d=\"M4.6 4.6l14.8 14.8\"/>","fast-forward":"<path d=\"M3 5.5v13.1L12 12ZM12.7 5.5v13.1L21.8 12Z\"/>","file":"<path d=\"M5.8 2.4h7.6v5.2h5V21.2H5.8ZM15 2.7l3.1 3.3h-3.1Z\"/>","file-code":"<path fill-rule=\"evenodd\" d=\"M5.8 2.4h7.6v5.2h5V21.2H5.8ZM9.8 11l1.5 1.4l-1.6 1.7l1.6 1.7l-1.5 1.4l-2.9-3.1ZM14.4 11l-1.5 1.4l1.6 1.7l-1.6 1.7l1.5 1.4l2.9-3.1Z\"/><path d=\"M15 2.7l3.1 3.3h-3.1Z\"/>","film":"<path fill-rule=\"evenodd\" d=\"M4.8 3.6h14.4a2.3 2.3 0 0 1 2.3 2.3v12.3a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 18.2V5.8A2.3 2.3 0 0 1 4.8 3.6Zm3.8 2v12.9h6.8V5.5ZM4.4 6v2h2.1V6Zm0 5v1.8h2.1v-1.8Zm0 4.9v2h2.1v-2Zm13.1-10v2h2.1V6Zm0 5v1.8h2.1v-1.8Zm0 4.9v2h2.1v-2Z\"/>","finish":"<path d=\"M6.5 4.2h3.9v4.6H6.5ZM14.8 4.2h4a0.9 0.9 0 0 1 0.9 0.9v3.7h-4.9ZM10.5 8.6h4.5v4.6H10.5Z\"/><path class=\"ln\" d=\"M6 20.3V4.1\"/>","fire":"<path fill-rule=\"evenodd\" d=\"M12.1 19.8a6.8 6.8 0 0 0 6.8-6.8c0-4.3-2.9-6.8-4.2-10.3c-2.1 2.3-2.6 4.6-2.6 4.6S10.8 5.1 9.6 3C7.1 5.9 5.2 9.2 5.2 12.9A6.8 6.8 0 0 0 12.1 21.4Zm0-2.8a2.8 2.8 0 0 0 2.8-2.8c0-1.6-1.2-2.7-1.7-4c-0.9 1-1.1 1.8-1.1 1.8s-0.5-0.9-1-1.6c-1 1.2-1.7 2.5-1.7 3.9a2.8 2.8 0 0 0 2.7 2.7Z\"/>","fish":"<path fill-rule=\"evenodd\" d=\"M7.6 12c2.1-3.6 4.8-5.4 7.9-5.4 2.9 0 5 2.2 6.1 5.4-1.1 3.2-3.2 5.4-6.1 5.4-3.1 0-5.8-1.8-7.9-5.4ZM15.7 10.6a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0Z\"/><path d=\"M8.4 12 2.9 7.4v9.2Z\"/>","flag":"<path class=\"ln\" d=\"M7.2 20.5V4.3\"/><path d=\"M7.8 4.1h10.9l-2 4.1l2 4.1H7.8Z\"/>","focus":"<circle cx=\"12\" cy=\"12\" r=\"3.6\"/><path class=\"ln\" d=\"M3.8 9V6.1A2.3 2.3 0 0 1 6.1 3.8H9M15 3.8h2.9A2.3 2.3 0 0 1 20.2 6.1V9M20.2 15v2.9a2.3 2.3 0 0 1-2.3 2.3H15M9 20.2H6.1A2.3 2.3 0 0 1 3.8 17.9V15\"/>","folder":"<path d=\"M2.7 6.4a2.1 2.1 0 0 1 2.1-2.1h4.4l2.1 2.1h8.1a2.1 2.1 0 0 1 2.1 2.1v8.7a2.1 2.1 0 0 1-2.1 2.1H4.8a2.1 2.1 0 0 1-2.1-2.1Z\"/>","folder-tree":"<path d=\"M9.6 5h4l1 1.5h6.4v5.3H9.6ZM9.6 14.3h4l1 1.5h6.4v5.3H9.6Z\"/><path class=\"ln\" d=\"M4.1 3.6v13.2a1 1 0 0 0 1 1H9.6M4.1 8.8H9.6\"/>","food":"<path d=\"M4.7 3.3h1.5v4.7h0.6V3.3h1.4v4.7h0.6V3.3h1.5v4.9A2.9 2.9 0 0 1 8.6 10.8V21.7H6.5V10.8a2.9 2.9 0 0 1-1.8-2.7ZM18.9 21.7h-2.3v-7.4h-3V10.6c0-3.9 1.7-6.6 5.2-7.7Z\"/>","game":"<path fill-rule=\"evenodd\" d=\"M7.6 6.3h8.8a5.8 5.8 0 0 1 0 11.5H7.6a5.8 5.8 0 0 1 0-11.5Zm-1.4 3.7v1.6H4.7v1.8h1.6v1.6H7.9v-1.6h1.6v-1.8H7.9v-1.6Zm9.2 1a1.2 1.2 0 1 0 0 2.3a1.2 1.2 0 0 0 0-2.3Zm2.5 1.6a1.2 1.2 0 1 0 0 2.3a1.2 1.2 0 0 0 0-2.3Z\"/>","gauge":"<path fill-rule=\"evenodd\" d=\"M3.7 18A9.4 9.4 0 1 1 19.4 19.6ZM15 8.6l-2.2 5.6l-2.1 0.3l-0.4-2.1Z\"/>","gavel":"<path d=\"M11.3 5.5L5.5 11.3l6.1 6.1l5.8-5.8Z\"/><path class=\"ln\" d=\"M13.7 4.2l5 5M3.8 19.1l5.7-5.7M13.7 19.1h6.3\"/>","gear":"<path fill-rule=\"evenodd\" d=\"M18.2 14.6a1.4 1.4 0 0 0 0.3 1.7l0.1 0.1a1.7 1.7 0 1 1-2.7 2.7l-0.1-0.1a1.4 1.4 0 0 0-1.7-0.3a1.4 1.4 0 0 0-0.9 1.4V19.6a1.7 1.7 0 1 1-3.8 0v-0.1a1.4 1.4 0 0 0-1-1.4a1.4 1.4 0 0 0-1.7 0.3l-0.1 0.1a1.7 1.7 0 1 1-2.7-2.7l0.1-0.1a1.4 1.4 0 0 0 0.3-1.7a1.4 1.4 0 0 0-1.4-0.9H4.6a1.7 1.7 0 1 1 0-3.8h0.1a1.4 1.4 0 0 0 1.4-1a1.4 1.4 0 0 0-0.3-1.7l-0.1-0.1a1.7 1.7 0 1 1 2.7-2.7l0.1 0.1a1.4 1.4 0 0 0 1.7 0.3H9.6a1.4 1.4 0 0 0 0.9-1.4V4.6a1.7 1.7 0 1 1 3.8 0v0.1a1.4 1.4 0 0 0 0.9 1.4a1.4 1.4 0 0 0 1.7-0.3l0.1-0.1a1.7 1.7 0 1 1 2.7 2.7l-0.1 0.1a1.4 1.4 0 0 0-0.3 1.7V9.6a1.4 1.4 0 0 0 1.4 0.9H19.6a1.7 1.7 0 1 1 0 3.8h-0.1a1.4 1.4 0 0 0-1.4 0.9ZM12.1 9.3a2.8 2.8 0 1 0 0 6.5a2.8 2.8 0 0 0 0-6.5Z\"/>","gem":"<path d=\"M6.4 3.9h11.2l3.8 5.3l-9.4 11.6L2.6 9.2Z\"/>","ghost":"<path fill-rule=\"evenodd\" d=\"M3.4 21.3V9.9a8.7 8.7 0 0 1 17.2 0v11.5l-2.9-2.1l-2.9 2.1l-2.9-2.1l-2.9 2.1l-2.9-2.1ZM8.8 7.8a1.6 1.6 0 1 0 0 3.1a1.6 1.6 0 0 0 0-3.1Zm6.2 0a1.6 1.6 0 1 0 0 3.1a1.6 1.6 0 0 0 0-3.1Z\"/>","gift":"<path d=\"M3.1 8.6h17.7v3.3H3.1ZM4.4 12.9h6.6v8.2H5.4a1 1 0 0 1-1-1ZM13 12.9h6.6v7.2a1 1 0 0 1-1 1H13Z\"/><path class=\"ln\" d=\"M12 8.6c-1.5-3.1-5.1-4.1-5.1-1.5S12 8.6 12 8.6s5.1-1 5.1-3.6s-3.6-1.5-5.1 3.6\"/>","globe":"<path d=\"M2.6 11.1A9.5 9.5 0 0 1 12 2.6a4.5 9.5 0 0 0-4.5 8.5ZM21.4 11.1A9.5 9.5 0 0 0 12 2.6a4.5 9.5 0 0 1 4.5 8.5ZM2.6 12.9A9.5 9.5 0 0 0 12 21.5a4.5 9.5 0 0 1-4.5-8.5ZM21.4 12.9A9.5 9.5 0 0 1 12 21.5a4.5 9.5 0 0 0 4.5-8.5ZM12 2.6a2.7 9.5 0 0 0-2.7 8.5h5.3A2.7 9.5 0 0 0 12 2.6ZM12 21.5a2.7 9.5 0 0 1-2.7-8.5h5.3A2.7 9.5 0 0 1 12 21.5Z\"/>","grid":"<rect x=\"2.7\" y=\"2.7\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"13.3\" y=\"2.7\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"2.7\" y=\"13.3\" width=\"8\" height=\"8\" rx=\"2.2\"/><rect x=\"13.3\" y=\"13.3\" width=\"8\" height=\"8\" rx=\"2.2\"/>","group":"<rect x=\"6.9\" y=\"6.8\" width=\"6.4\" height=\"4.8\" rx=\"1.1\"/><rect x=\"10.8\" y=\"12.5\" width=\"6.4\" height=\"4.6\" rx=\"1.1\"/><path class=\"ln\" d=\"M4 7.6V4h3.6M16.5 4h3.6v3.6M20 16.4v3.6h-3.6M7.6 20H4v-3.6\"/>","hammer":"<path d=\"M13.1 2.5l7.5 7.5l-3.7 3.7l-7.5-7.5ZM12.9 8.9L3.7 18.1a2.1 2.1 0 0 0 2.9 2.9l9.2-9.2Z\"/>","hand":"<path d=\"M9 14.3V4.9a1.4 1.4 0 0 1 2.8 0v5.2a0.5 0.5 0 0 0 1.1 0V3.8a1.4 1.4 0 0 1 2.8 0v6.4a0.5 0.5 0 0 0 1.1 0V5.7a1.4 1.4 0 0 1 2.8 0v7.7c0 4.5-2.8 7.7-7.3 7.7c-2.9 0-4.7-1.5-6.3-4l-1.8-2.9a1.7 1.7 0 0 1 2.8-2Z\"/>","headphones":"<path class=\"ln\" d=\"M4 14.5V12a8 8 0 0 1 16 0v2.5\"/><rect x=\"2.6\" y=\"13.4\" width=\"5.4\" height=\"7.6\" rx=\"1.8\"/><rect x=\"16\" y=\"13.4\" width=\"5.4\" height=\"7.6\" rx=\"1.8\"/>","heart":"<path d=\"M12 21.6s-9.3-5.7-9.3-13a5.3 5.3 0 0 1 9.3-3.6a5.3 5.3 0 0 1 9.3 3.6c0 7.4-9.3 13-9.3 13Z\"/>","heart-broken":"<path fill-rule=\"evenodd\" d=\"M12 21.6s-9.3-5.7-9.3-13a5.3 5.3 0 0 1 9.3-3.6a5.3 5.3 0 0 1 9.3 3.6c0 7.4-9.3 13-9.3 13ZM10.9 5.9l2.7 0.6l-1.4 2.7l3.6 2.5l-2.6 5.9l-2.5-1.1l1.5-3.7l-3.7-2.5Z\"/>","help":"<path fill-rule=\"evenodd\" d=\"M13 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM9.3 9.4A3.7 3.7 0 1 1 13.9 12.8v1.6h-2.1V10.9h1a1.6 1.6 0 1 0-1.6-1.6ZM13 15.8a1.3 1.3 0 1 1 0 2.6a1.3 1.3 0 0 1 0-2.6Z\"/>","history":"<path d=\"M5.8 5.8A9.1 9.1 0 1 1 3.2 12.2h3a6.1 6.1 0 1 0 1.8-4.3ZM2.5 2.7v7.2h7.2ZM11.2 8.1h2.2v3.7l2.7 1.6l-1.1 1.8l-3.8-2.3Z\"/>","home":"<path d=\"M2.7 10.4L12 2.3l9.3 8.1v9.5a1.1 1.1 0 0 1-1.1 1.1h-5.6v-6.9h-5.4v6.9H3.8a1.1 1.1 0 0 1-1.1-1.1Z\"/>","hot-tub":"<circle cx=\"8.2\" cy=\"5.4\" r=\"2.4\"/><path d=\"M2.6 10.9h18.8v4.2a5.1 5.1 0 0 1-5.1 5.1h-8.5a5.1 5.1 0 0 1-5.1-5.1Z\"/><path class=\"ln\" d=\"M6.3 10.9v-0.6A2.4 2.4 0 0 1 8.7 8H9.2M13.5 9.4c0.8-0.9-0.8-1.9 0-2.8M17.3 9.4c0.8-0.9-0.8-1.9 0-2.8\"/>","id":"<path fill-rule=\"evenodd\" d=\"M4.8 4.6h14.4a2.3 2.3 0 0 1 2.3 2.3v10.3a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 17.2V6.9A2.3 2.3 0 0 1 4.8 4.6Zm4.1 4.2a2.2 2.2 0 1 0 0 4.3a2.2 2.2 0 0 0 0-4.3Zm-3.2 7.7h6.4c-0.6-1.7-1.8-2.7-3.2-2.7s-2.6 0.9-3.2 2.7Zm8.8-7.2v1.5h4V9.4Zm0 3.6v1.5h4v-1.5Z\"/>","idea":"<path d=\"M8.3 14.9A6.5 6.5 0 1 1 15.7 14.9c-0.7 0.6-1.3 1.3-1.4 2.2H9.7c-0.1-0.9-0.7-1.6-1.4-2.2Z\"/><path class=\"ln\" d=\"M9.6 20.5h4.8\"/>","image":"<path fill-rule=\"evenodd\" d=\"M2.8 6.1a2.7 2.7 0 0 1 2.7-2.7h13.3A2.7 2.7 0 0 1 21.5 6.1v12.3a2.7 2.7 0 0 1-2.7 2.7H5.4A2.7 2.7 0 0 1 2.8 18.4Zm6.2 2.2a2 2 0 1 0 0 3.9a2 2 0 0 0 0-3.9Zm7.5 3.5l-8.3 7.8h10.7a1.5 1.5 0 0 0 1.5-1.5v-3.1Z\"/>","inbox":"<path fill-rule=\"evenodd\" d=\"M2.7 12.4L5.2 3.7h13.6l2.5 8.8V17.9a1.6 1.6 0 0 1-1.6 1.6H4.2A1.6 1.6 0 0 1 2.7 17.9ZM6.4 5.6L4.6 11.5h3.8l1.5 2.4h4.3l1.5-2.4h3.8L17.6 5.6Z\"/>","info":"<path fill-rule=\"evenodd\" d=\"M12.9 2.6a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM11.7 10.5h2.5V16.9h-2.5ZM12.9 6.4a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","invite":"<circle cx=\"8.8\" cy=\"7.7\" r=\"3.2\"/><path d=\"M2.9 19.2c0-3.4 2.7-5.8 5.8-5.8s5.8 2.4 5.8 5.8Z\"/><path class=\"ln\" d=\"M17.6 7.8v5.3M15 10.5h5.3\"/>","key":"<path fill-rule=\"evenodd\" d=\"M8.3 10.1a5.4 5.4 0 1 1 0 10.9a5.4 5.4 0 0 1 0-10.9Zm0 3.6a1.9 1.9 0 1 0 0 3.8a1.9 1.9 0 0 0 0-3.8Z\"/><path class=\"ln\" d=\"M11.7 12.2l8.7-8.7M16.7 7.2l3.1 3.1M14.4 9.5l2.1 2.1\"/>","keyboard":"<path fill-rule=\"evenodd\" d=\"M4.8 5.7h14.4A2.3 2.3 0 0 1 21.5 8v8a2.3 2.3 0 0 1-2.3 2.3H4.8A2.3 2.3 0 0 1 2.5 16V8a2.3 2.3 0 0 1 2.3-2.3ZM5.3 9.1h1.9v1.9H5.3ZM8.2 9.1h1.9v1.9H8.2ZM11 9.1h1.9v1.9h-1.9ZM13.9 9.1h1.9v1.9h-1.9ZM16.8 9.1h1.9v1.9h-1.9ZM7.8 13.3h8.4v1.7H7.8Z\"/>","laugh":"<path fill-rule=\"evenodd\" d=\"M13 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM7.9 8.1l3.5 1.7l-3.5 1.7l-0.7-1.4l0.7-0.4l-0.7-0.4ZM18 8.1l-3.5 1.7l3.5 1.7l0.7-1.4l-0.7-0.4l0.7-0.4ZM8.5 13.5h8.9a4.4 4.4 0 0 1-8.9 0Z\"/>","layers":"<path d=\"M12 4.3l8.6 5l-8.6 5l-8.6-5Z\"/><path class=\"ln\" d=\"M3.9 13.9l8.1 4.5l8.1-4.5\"/>","leaf":"<path fill-rule=\"evenodd\" d=\"M5.4 18.6c0-8.9 5.2-14.2 15.7-15.7c-1 10.4-6.3 15.7-15.7 15.7Zm3.1-2L16.8 7.1l-9.5 8.5Z\"/><path class=\"ln\" d=\"M3.7 20.3L6 18\"/>","lifebuoy":"<path fill-rule=\"evenodd\" d=\"M13.1 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 5.3a4.1 4.1 0 1 1 0 8.2a4.1 4.1 0 0 1 0-8.2ZM15.5 16.2L18.4 19.1L20.2 17.3L17.2 14.3ZM10.8 16.2L7.9 19.1L6 17.3L9 14.3ZM15.5 7.8L18.4 4.9L20.2 6.7L17.2 9.7ZM10.8 7.8L7.9 4.9L6 6.7L9 9.7Z\"/>","live":"<circle cx=\"12\" cy=\"12\" r=\"2.5\"/><path class=\"ln\" d=\"M8.5 8.5a4.9 4.9 0 0 0 0 6.9M15.5 8.5a4.9 4.9 0 0 1 0 6.9M6.2 6.2a8.2 8.2 0 0 0 0 11.5M17.8 6.2a8.2 8.2 0 0 1 0 11.5\"/>","lock":"<path d=\"M4.6 11.1a1.6 1.6 0 0 1 1.6-1.6h11.5a1.6 1.6 0 0 1 1.6 1.6v8.4a1.6 1.6 0 0 1-1.6 1.6H6.2A1.6 1.6 0 0 1 4.6 19.6Z\"/><path class=\"ln\" d=\"M7.9 9.4V6.8a4.1 4.1 0 0 1 8 0V9.4\"/>","mail":"<path d=\"M2.5 7.3v-0.4A2.3 2.3 0 0 1 4.8 4.6h14.4A2.3 2.3 0 0 1 21.5 6.9v0.4l-8.7 5.6a1.6 1.6 0 0 1-1.7 0ZM2.5 9.4v7.6A2.3 2.3 0 0 0 4.8 19.4h14.4a2.3 2.3 0 0 0 2.3-2.3V9.4l-8 5.2a2.7 2.7 0 0 1-3 0Z\"/>","map":"<path d=\"M2.6 6.1 8.3 4.2v14l-5.7 1.9ZM9.7 4.2l4.6 1.6v14l-4.6-1.6ZM15.7 5.8l5.7-1.9v14l-5.7 1.9Z\"/>","mask":"<path fill-rule=\"evenodd\" d=\"M2.7 6.1c3.3 1.4 6.4 2.1 9.4 2.1s6.1-0.6 9.4-2.1c0 7.1-2.9 11.7-5.8 11.7c-1.5 0-2.2-1.5-3.6-1.5s-2.1 1.5-3.6 1.5c-2.9 0-5.8-4.6-5.8-11.7ZM6.8 11.6a1.6 1.1 0 1 0 3.3 0a1.6 1.1 0 1 0-3.3 0ZM14 11.6a1.6 1.1 0 1 0 3.3 0a1.6 1.1 0 1 0-3.3 0Z\"/>","medical":"<path fill-rule=\"evenodd\" d=\"M5.3 6.3h13.5a2.7 2.7 0 0 1 2.7 2.7v8.7a2.7 2.7 0 0 1-2.7 2.7H5.3a2.7 2.7 0 0 1-2.7-2.7V9a2.7 2.7 0 0 1 2.7-2.7ZM10.9 10h2.3v2.2h2.2v2.3h-2.2V16.7h-2.3v-2.2H8.7v-2.3h2.2Z\"/><path class=\"ln\" d=\"M9.2 6.3V5.3a0.9 0.9 0 0 1 0.9-0.9h3.8a0.9 0.9 0 0 1 0.9 0.9v1\"/>","megaphone":"<path d=\"M2.7 9.1a1.1 1.1 0 0 1 1.1-1.1h2.7l9.7-4.8v16.1l-9.7-4.8H3.8A1.1 1.1 0 0 1 2.7 13.2Z\"/><path class=\"ln\" d=\"M6.6 14.5l1.2 5M18.8 8.5a3.4 3.4 0 0 1 0 5.4\"/>","members":"<circle cx=\"9.1\" cy=\"7.1\" r=\"3.5\"/><path d=\"M2.6 19.6c0-3.7 2.9-6.3 6.4-6.3s6.4 2.6 6.4 6.3Z\"/><circle cx=\"16.5\" cy=\"6.6\" r=\"2.8\"/><path d=\"M16.7 12.4c2.8 0.4 4.7 2.7 4.7 6.2V19.6h-4.3c0-2.8-1-5.1-2.7-6.5Z\"/>","mic":"<path d=\"M8.9 5.8a3.1 3.1 0 0 1 6.2 0V10.8a3.1 3.1 0 0 1-6.2 0Z\"/><path class=\"ln\" d=\"M5.9 10.8a6.1 6.1 0 0 0 12.3 0M12 16.9V20.2\"/>","mic-off":"<path d=\"M9 5.4A3.1 3.1 0 0 1 15.2 5.8V10.7a3.1 3.1 0 0 1-0.1 0.8Z\"/><path class=\"ln\" d=\"M5.4 10.7a6.6 6.6 0 0 0 11 5M18.7 10.7c0 0.8-0.1 1.5-0.4 2.2M12.1 17.3v2.8M4.5 4.1l15.1 15.1\"/>","minus-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM7.9 10.9v2.5h10.2v-2.5Z\"/>","money":"<path fill-rule=\"evenodd\" d=\"M4.8 5.7h14.4a2.1 2.1 0 0 1 2.1 2.1v8.4a2.1 2.1 0 0 1-2.1 2.1H4.8a2.1 2.1 0 0 1-2.1-2.1V7.8a2.1 2.1 0 0 1 2.1-2.1ZM12 8.9a3.1 3.1 0 1 0 0 6.3a3.1 3.1 0 0 0 0-6.3ZM6.3 8.7a1 1 0 1 0 0 2.1a1 1 0 0 0 0-2.1Zm11.4 4.6a1 1 0 1 0 0 2.1a1 1 0 0 0 0-2.1Z\"/><circle cx=\"12\" cy=\"12\" r=\"1.4\"/>","moon":"<path d=\"M21.7 14.6A9.7 9.7 0 1 1 9.4 2.3a8 8 0 0 0 12.3 12.3Z\"/>","more":"<circle cx=\"4.8\" cy=\"12\" r=\"2.2\"/><circle cx=\"12\" cy=\"12\" r=\"2.2\"/><circle cx=\"19.2\" cy=\"12\" r=\"2.2\"/>","mountain":"<path d=\"M2.8 18.1L9.2 5.3l4.1 7.1L16.1 8.9L21.5 18.1Z\"/>","mug":"<path d=\"M3.8 8.9h12.2v7.6a4.6 4.6 0 0 1-4.6 4.6h-3a4.6 4.6 0 0 1-4.6-4.6Z\"/><path class=\"ln\" d=\"M16 11H16.9a2.5 2.5 0 0 1 0 5h-0.9M7.9 3.7c-0.8 1 0.8 2 0 3M11.4 3.7c-0.8 1 0.8 2 0 3\"/>","music":"<path d=\"M7.6 5.4l13.3-2.5v3.5l-13.3 2.5Z\"/><path class=\"ln\" d=\"M8.8 17.9V6.5M19.7 15.6V4.2\"/><circle cx=\"5.5\" cy=\"17.9\" r=\"3.2\"/><circle cx=\"16.7\" cy=\"15.6\" r=\"3.2\"/>","mute":"<path d=\"M2.9 8.8h4l5.5-4.2v14.8l-5.5-4.2H2.9Z\"/><path class=\"ln\" d=\"M15.5 9.6l4.7 4.7M20.3 9.6l-4.7 4.7\"/>","news":"<path fill-rule=\"evenodd\" d=\"M2.8 2.6h14.2v15.2a2.2 2.2 0 0 0 2.2 2.2H5.1A2.2 2.2 0 0 1 2.8 18.9ZM6.6 6.6h6.8v1.7H6.6Zm0 3.7h6.8v1.7H6.6Zm0 3.7h4v1.7H6.6Z\"/><path class=\"ln\" d=\"M17.1 7.7H19.8V17.9a2.1 2.1 0 0 1-2.1 2.1\"/>","nodes":"<circle cx=\"12\" cy=\"5.2\" r=\"3\"/><circle cx=\"5.6\" cy=\"18\" r=\"3\"/><circle cx=\"18.4\" cy=\"18\" r=\"3\"/><path class=\"ln\" d=\"M10.9 7.6L6.8 15.5M13.1 7.6l4.1 7.9M8.4 18h7.1\"/>","palette":"<path fill-rule=\"evenodd\" d=\"M12.1 2.7a9.4 9.4 0 1 0 0 18.8c1.3 0 2-0.9 2-1.9c0-1.2-1-1.5-1-2.5s0.8-1.6 2-1.6H17.3a4.4 4.4 0 0 0 4.4-4.4C21.7 6.7 17.4 2.7 12.1 2.7ZM7.8 9.7a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm2.5-3.7a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Zm4.6 0a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3Z\"/>","parking":"<path fill-rule=\"evenodd\" d=\"M6.2 2.6h11.6a3.6 3.6 0 0 1 3.6 3.6v11.6a3.6 3.6 0 0 1-3.6 3.6H6.2a3.6 3.6 0 0 1-3.6-3.6V6.2a3.6 3.6 0 0 1 3.6-3.6ZM8.4 18V6H13a4 4 0 0 1 0 8h-2.4v4ZM10.6 8.2v3.6H13a1.8 1.8 0 0 0 0-3.6Z\"/>","pause":"<rect x=\"4.8\" y=\"2.6\" width=\"5.1\" height=\"18.8\" rx=\"1.5\"/><rect x=\"14.2\" y=\"2.6\" width=\"5.1\" height=\"18.8\" rx=\"1.5\"/>","paw":"<path d=\"M6.9 18.5c0-3.5 2.2-6.3 5.1-6.3s5.1 2.8 5.1 6.3c0 1.8-1.3 2.9-2.9 2.9c-0.9 0-1.5-0.6-2.2-0.6s-1.3 0.6-2.2 0.6c-1.6 0-2.9-1.1-2.9-2.9Z\"/><circle cx=\"5.1\" cy=\"10.2\" r=\"2.6\"/><circle cx=\"9.1\" cy=\"5.1\" r=\"2.6\"/><circle cx=\"14.9\" cy=\"5.1\" r=\"2.6\"/><circle cx=\"18.9\" cy=\"10.2\" r=\"2.6\"/>","pencil":"<path d=\"M15.6 3.1a2.2 2.2 0 0 1 3.2 3.2L6.9 18.1l-4.5 1.2l1.2-4.5Z\"/><path class=\"ln\" d=\"M11.8 19.7H20.1\"/>","phone":"<path d=\"M5.1 2.4h4.9l2.4 6l-2.7 1.6a10.7 10.7 0 0 0 4.2 4.2l1.6-2.7l6 2.4v4.9a2.3 2.3 0 0 1-2.3 2.3A17.3 17.3 0 0 1 2.8 4.6a2.3 2.3 0 0 1 2.3-2.3Z\"/>","piggy":"<path fill-rule=\"evenodd\" d=\"M2.6 11.4c0-3.6 3.7-6.5 8.7-6.5c1.4 0 2.7 0.2 3.9 0.7L18.3 3.8v3.7c0.9 0.8 1.5 1.7 1.7 2.8h1.5v3.8H20c-0.5 1.1-1.4 2-2.6 2.6V20.1h-3.3v-2.2c-0.9 0.2-1.8 0.3-2.8 0.3c-0.9 0-1.7-0.1-2.6-0.3v2.2h-3.3v-3.3C3.7 15.5 2.6 13.6 2.6 11.4ZM14.5 9.8a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0ZM8.3 6.8v1.5h3.7V6.8Z\"/>","pin":"<path fill-rule=\"evenodd\" d=\"M12 2.8a7.1 7.1 0 0 0-7.1 7.1c0 5.5 7.1 11.8 7.1 11.8s7.1-6.3 7.1-11.8A7.1 7.1 0 0 0 12 2.8Zm0 4.5a2.5 2.5 0 1 1 0 5.1a2.5 2.5 0 0 1 0-5.1Z\"/>","plane":"<path d=\"M21.2 2.9L2.4 10.7l6.9 3.2ZM21.2 2.9l-11 12l3.2 6.9Z\"/>","play":"<path d=\"M5.1 3.5v17a0.9 0.9 0 0 0 1.3 0.8l12.8-8.5a0.9 0.9 0 0 0 0-1.6L6.5 2.7a0.9 0.9 0 0 0-1.3 0.8Z\"/>","plug":"<path d=\"M6.5 8h10.9v3.3a5.4 5.4 0 0 1-10.9 0Z\"/><path class=\"ln\" d=\"M9.2 4.2v3.8M14.6 4.2v3.8M11.9 16.8v3.5\"/>","plus-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm-1.2 4.2h2.5v3.8H18v2.5h-3.8V17.1h-2.5v-3.8H7.9v-2.5h3.8Z\"/>","print":"<path d=\"M6.2 2.6h11.5v4.4H6.2ZM2.6 11a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v4.1a2.7 2.7 0 0 1-2.7 2.7h-0.3v-4.5H5.6v4.5h-0.3A2.7 2.7 0 0 1 2.6 15.1ZM7.1 14.7h9.9v6.7H7.1Z\"/>","puzzle":"<path d=\"M2.8 5.4h5a2.9 2.9 0 1 1 5.8 0h5v5a2.9 2.9 0 1 1 0 5.8v5h-5a2.9 2.9 0 1 0-5.8 0H2.8v-5a2.9 2.9 0 1 0 0-5.8Z\"/>","radio":"<path fill-rule=\"evenodd\" d=\"M5.2 7.8h13.6a2.5 2.5 0 0 1 2.5 2.5v7.8a2.5 2.5 0 0 1-2.5 2.5H5.2A2.5 2.5 0 0 1 2.6 18.1v-7.8A2.5 2.5 0 0 1 5.2 7.8ZM12.9 14.2a2.5 2.5 0 1 0 5.1 0a2.5 2.5 0 1 0-5.1 0ZM6.1 11.6v2h4.7v-2ZM6.1 15v2h4.7v-2Z\"/><path class=\"ln\" d=\"M7.1 7.8L16.9 4\"/>","ranking":"<path d=\"M3.5 13.7h4.9V9.9h7.1v5.7h4.9v5.4H3.5ZM12 2.2l1 2l2.2 0.3l-1.6 1.6l0.4 2.2L12 7.5l-2 1l0.4-2.2l-1.6-1.6l2.2-0.3Z\"/>","record":"<path fill-rule=\"evenodd\" d=\"M13.2 2.7a9.3 9.3 0 1 0 0 18.5a9.3 9.3 0 0 0 0-18.5Zm0 3.3a6 6 0 1 1 0 11.9a6 6 0 0 1 0-11.9Z\"/><circle cx=\"13.2\" cy=\"12\" r=\"3.7\"/>","restroom":"<circle cx=\"7.2\" cy=\"5.1\" r=\"1.8\"/><circle cx=\"16.2\" cy=\"5.1\" r=\"1.8\"/><path d=\"M6 9h1.8a1.5 1.5 0 0 1 1.5 1.5v4.7H8.6v5H5.5v-5H4.7v-4.7A1.5 1.5 0 0 1 6 9Z\"/><path d=\"M15.7 9.4h1.1a0.9 0.9 0 0 1 0.9 0.7l1.9 6.3H17.4v3.6h-2.3v-3.6h-2.1l1.9-6.3a0.9 0.9 0 0 1 0.9-0.7Z\"/><path class=\"ln\" d=\"M11.6 3.7v16.3\"/>","retry":"<path class=\"ln\" d=\"M4 12a8 8 0 0 1 13.4-5.9M20 12a8 8 0 0 1-13.4 5.9\"/><path d=\"M21.3 2.8v7h-7ZM2.7 21.2v-7h7Z\"/>","rewind":"<path d=\"M21 5.5v13.1L12 12ZM11.3 5.5v13.1L2.2 12Z\"/>","robot":"<path fill-rule=\"evenodd\" d=\"M8.5 8.7h6.9a3.1 3.1 0 0 1 3.1 3.1v4.3A3.1 3.1 0 0 1 15.5 19.3H8.5A3.1 3.1 0 0 1 5.4 16.2v-4.3A3.1 3.1 0 0 1 8.5 8.7Zm0.9 3.8a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4Zm5.2 0a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4ZM9.7 16v1.3h4.5v-1.3Z\"/><path class=\"ln\" d=\"M12 5.3v3.5M3.9 13.1v2.8M20.2 13.1v2.8\"/>","rocket":"<path fill-rule=\"evenodd\" d=\"M11.9 16.6L7.4 12.2C9.1 6.1 13.6 2.9 21.1 2.9c0 7.5-3.2 12-9.2 13.7ZM13.6 8.7a1.7 1.7 0 1 0 3.5 0a1.7 1.7 0 1 0-3.5 0Z\"/><path d=\"M3.7 12.2L6.8 7.5h3.5L6.6 12.6ZM11.4 17.4L11.9 20.3l4.6-3.3v-3.5ZM6.3 17.7c-2 0-3.9 2-3.9 3.9c2 0 3.9-2 3.9-3.9Z\"/>","save":"<path fill-rule=\"evenodd\" d=\"M4.4 2.7h12.4l3.5 3.5V20.8a0.7 0.7 0 0 1-0.7 0.7H4.4a0.7 0.7 0 0 1-0.7-0.7V3.4a0.7 0.7 0 0 1 0.7-0.7Zm3.4 1.4v4.9h7.4V4.1ZM7.2 13.6v6.4h9.6v-6.4Z\"/>","scissors":"<path fill-rule=\"evenodd\" d=\"M6.2 3.8a3.2 3.2 0 1 1 0 6.5a3.2 3.2 0 0 1 0-6.5Zm0 2a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4ZM6.2 13.7a3.2 3.2 0 1 1 0 6.5a3.2 3.2 0 0 1 0-6.5Zm0 2a1.2 1.2 0 1 0 0 2.4a1.2 1.2 0 0 0 0-2.4Z\"/><path class=\"ln\" d=\"M8.7 8.8L20.4 18.1M8.7 15.2L20.4 5.9\"/>","search":"<path fill-rule=\"evenodd\" d=\"M10.6 3a7.7 7.7 0 1 1 0 15.4a7.7 7.7 0 0 1 0-15.4Zm0 3.2a4.5 4.5 0 1 0 0 9.2a4.5 4.5 0 0 0 0-9.2Z\"/><path class=\"ln\" d=\"M16.3 16.4l3.8 3.8\"/>","seedling":"<path d=\"M12.4 10.8C12.4 6.1 9.5 2.9 4 2.9c0 4.9 3 7.8 8.4 7.8ZM11.6 13.4c0-4.7 3-7.8 8.4-7.8c0 4.9-3 7.8-8.4 7.8Z\"/><path class=\"ln\" d=\"M12 20.4V12.5\"/>","server":"<path fill-rule=\"evenodd\" d=\"M5.2 2.6h13.4a2.7 2.7 0 0 1 2.7 2.7v3.4a2.7 2.7 0 0 1-2.7 2.7H5.2A2.7 2.7 0 0 1 2.5 8.6V5.3A2.7 2.7 0 0 1 5.2 2.6Zm2.2 2.9a1.5 1.5 0 1 0 0 2.9a1.5 1.5 0 0 0 0-2.9ZM5.2 12.7h13.4a2.7 2.7 0 0 1 2.7 2.7v3.4a2.7 2.7 0 0 1-2.7 2.7H5.2A2.7 2.7 0 0 1 2.5 18.7v-3.4A2.7 2.7 0 0 1 5.2 12.7Zm2.2 2.9a1.5 1.5 0 1 0 0 2.9a1.5 1.5 0 0 0 0-2.9Z\"/>","shapes":"<circle cx=\"7.3\" cy=\"7.6\" r=\"4.3\"/><path d=\"M16.5 2.7l5 8.6h-9.8ZM13.4 13.7h7.4v7.4h-7.4ZM2.5 21.1L6.8 13.7l4.3 7.4Z\"/>","shelter":"<path fill-rule=\"evenodd\" d=\"M12 2.7L2.5 10.2l1.1 1.5l1-0.8v10h14.8V10.9l1 0.8l1.1-1.5ZM9.5 20.9v-4.8a2.5 2.5 0 0 1 4.9 0v4.8ZM12 9.1a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4Z\"/>","shield":"<path d=\"M12 2.6 4.6 5.8V12c0 4.6 3.1 8 7.4 9.5 4.3-1.5 7.4-4.9 7.4-9.5V5.8Z\"/>","shirt":"<path d=\"M8 2.7L2.5 5.9l2.5 4.9l1.5-0.8v11.2h11V10.1l1.5 0.8L21.5 5.9l-5.5-3.2c-0.6 1.5-1.9 2.5-4 2.5S8.6 4.2 8 2.7Z\"/>","skip":"<path d=\"M3.3 2.6v18.9L15.8 12Z\"/><rect x=\"17.8\" y=\"2.7\" width=\"3.2\" height=\"18.7\" rx=\"1.5\"/>","skull":"<path fill-rule=\"evenodd\" d=\"M12 2.6a7.9 7.9 0 0 0-7.9 7.9c0 2.4 1.1 4.2 2.5 5.4V19.5a2 2 0 0 0 2 2h6.8a2 2 0 0 0 2-2v-3.6c1.4-1.2 2.5-3 2.5-5.4A7.9 7.9 0 0 0 12 2.6ZM9.2 9.4a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4ZM14.8 9.4a1.7 1.7 0 1 1 0 3.4a1.7 1.7 0 0 1 0-3.4ZM12 13.3l1 1.8h-2ZM10.1 18.1h0.9v2.4h-0.9ZM13 18.1h0.9v2.4H13Z\"/>","sliders":"<path class=\"ln\" d=\"M4 7h8.8M19.2 7H20M4 17h2.8M13.2 17H20\"/><circle cx=\"16\" cy=\"7\" r=\"3.2\"/><circle cx=\"10\" cy=\"17\" r=\"3.2\"/>","smile":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM9.9 8.6a1.4 1.4 0 1 1 0 2.8a1.4 1.4 0 0 1 0-2.8ZM16 8.6a1.4 1.4 0 1 1 0 2.8a1.4 1.4 0 0 1 0-2.8ZM8.8 13.5h8.3a4.1 4.1 0 0 1-8.3 0Z\"/>","spark":"<path d=\"M10.7 2.6l2 5.6l5.6 2l-5.6 2L10.7 17.8l-2-5.6l-5.6-2l5.6-2ZM17.6 14.7l0.9 2.5l2.5 0.9l-2.5 0.9l-0.9 2.5l-0.9-2.5l-2.5-0.9l2.5-0.9Z\"/>","square":"<rect x=\"2.7\" y=\"2.7\" width=\"18.7\" height=\"18.7\" rx=\"3.3\"/>","star":"<path d=\"M12 2.8l2.9 6l6.6 0.8l-4.9 4.5l1.3 6.6L12 17.4l-5.9 3.2l1.3-6.6l-4.9-4.5l6.6-0.8Z\"/>","steps":"<path fill-rule=\"evenodd\" d=\"M2.8 4h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3ZM2.8 9.4h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3ZM2.8 15h4.9v4.9H2.8Zm1 2.5l0.8-0.9l0.9 0.9l1.4-1.4l0.9 0.9l-2.3 2.3Z\"/><path class=\"ln\" d=\"M10.6 6.4H20.3M10.6 11.9H20.3M10.6 17.4H20.3\"/>","store":"<path d=\"M4.4 3h15l2 6.2v1.4a3.2 3.2 0 0 1-6.3 0a3.2 3.2 0 0 1-6.3 0a3.2 3.2 0 0 1-6.3 0V9.2ZM4 14.9h15.9v5.6a1.1 1.1 0 0 1-1.1 1.1H14.1v-5.1h-4.4v5.1H5.1a1.1 1.1 0 0 1-1.1-1.1Z\"/>","sun":"<circle cx=\"12\" cy=\"12\" r=\"3.8\"/><path class=\"ln\" d=\"M12 3.9v1.3M12 18.7v1.3M6.3 6.2l0.9 0.9M16.8 16.8L17.8 17.8M4 12h1.3M18.7 12h1.3M6.3 17.8l0.9-0.9M16.8 7.2L17.8 6.2\"/>","sword":"<path d=\"M21.2 2.8v4.7l-10.8 10.8l-4.7-4.7L16.5 2.8Z\"/><path class=\"ln\" d=\"M5.2 11.9L12.1 18.8M8.4 15.6L3.7 20.3\"/>","table":"<path d=\"M2.7 6.2a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.3H2.7ZM2.7 10.5h5.3v9.9H5.2a2.5 2.5 0 0 1-2.5-2.5ZM9.8 10.5h4.5v9.9H9.8ZM16.1 10.5h5.3V17.9a2.5 2.5 0 0 1-2.5 2.5h-2.7Z\"/>","tack":"<path d=\"M8.5 3h7.2a0.8 0.8 0 0 1 0.8 1l-0.8 4l2.8 2.8a0.7 0.7 0 0 1-0.6 1.4H6.1a0.7 0.7 0 0 1-0.6-1.4l2.8-2.8l-0.8-4a0.8 0.8 0 0 1 0.8-1Z\"/><path class=\"ln\" d=\"M12 12.2V20.5\"/>","tag":"<path fill-rule=\"evenodd\" d=\"M2.9 3.4A1.5 1.5 0 0 1 4.4 2.9h7.2l10 10l-8.7 8.7L2.9 10.6Zm5 2.5a1.9 1.9 0 1 0 0 3.7a1.9 1.9 0 0 0 0-3.7Z\"/>","target":"<path fill-rule=\"evenodd\" d=\"M12 2.6a9.4 9.4 0 1 1 0 18.8a9.4 9.4 0 0 1 0-18.8Zm0 3a6.4 6.4 0 1 0 0 12.9a6.4 6.4 0 0 0 0-12.9Zm0 2.7a3.7 3.7 0 1 1 0 7.4a3.7 3.7 0 0 1 0-7.4Z\"/>","tent":"<path fill-rule=\"evenodd\" d=\"M12 4.2L19.3 17.4H4.7ZM12 11.6L9.5 17.4h5.1Z\"/><path class=\"ln\" d=\"M3.9 17.7h16.2\"/>","terminal":"<path fill-rule=\"evenodd\" d=\"M5 3.6h14a2.4 2.4 0 0 1 2.4 2.4v12a2.4 2.4 0 0 1-2.4 2.4H5A2.4 2.4 0 0 1 2.6 18V6A2.4 2.4 0 0 1 5 3.6Zm1.4 6 1.3-1.3 3.6 3.7-3.6 3.7-1.3-1.3 2.4-2.4Zm6.4 4.6h4.4v1.7h-4.4Z\"/>","text":"<path fill-rule=\"evenodd\" d=\"M10.2 2.4h3.7l6.8 18.7h-3.4l-1.6-4.7H8.3l-1.6 4.7H3.3ZM9.3 13.6h5.4L12 5.7Z\"/>","thumbs-up":"<path d=\"M6.5 10.1v10.3H3.6a1.1 1.1 0 0 1-1.1-1.1v-8a1.1 1.1 0 0 1 1.1-1.1ZM7.9 10.1l3.6-7.3a2.4 2.4 0 0 1 2.7 2.8l-0.6 3.5h5.6a2.2 2.2 0 0 1 2.2 2.6l-1.4 7a2.2 2.2 0 0 1-2.2 1.7H7.9Z\"/>","ticket":"<path fill-rule=\"evenodd\" d=\"M2.6 6.4h18.8v3.4a2.2 2.2 0 0 0 0 4.4v3.4H2.6v-3.4a2.2 2.2 0 0 0 0-4.4ZM13.3 8.4v1.8h1.4V8.4Zm0 2.7v1.8h1.4v-1.8Zm0 2.7v1.8h1.4v-1.8Z\"/>","toolbox":"<path d=\"M2.7 10.1a2.5 2.5 0 0 1 2.5-2.5h13.6a2.5 2.5 0 0 1 2.5 2.5v2.2H2.7ZM2.7 13.7h4.7v1.5h1.6v-1.5h6.2v1.5h1.6v-1.5h4.7V17.8a2.5 2.5 0 0 1-2.5 2.5H5.2A2.5 2.5 0 0 1 2.7 17.8Z\"/><path class=\"ln\" d=\"M9.1 7.5V5.7A1.5 1.5 0 0 1 10.5 4.2h2.9A1.5 1.5 0 0 1 14.9 5.7v1.9\"/>","train":"<path fill-rule=\"evenodd\" d=\"M9.1 2.7h5.8a2.9 2.9 0 0 1 2.9 2.9v6.8a2.9 2.9 0 0 1-2.9 2.9H9.1a2.9 2.9 0 0 1-2.9-2.9V5.6a2.9 2.9 0 0 1 2.9-2.9Zm-1.2 2.5v4.1h8.2V5.2ZM9.1 11.4a1.1 1.1 0 1 0 0 2.1A1.1 1.1 0 0 0 9.1 11.4Zm5.8 0a1.1 1.1 0 1 0 0 2.1a1.1 1.1 0 0 0 0-2.1Z\"/><path class=\"ln\" d=\"M8.6 20.2l1.9-3.4M15.4 20.2l-1.9-3.4\"/>","trash":"<path d=\"M5.8 7.6h12.4l-1 12a2 2 0 0 1-2 1.8H8.8a2 2 0 0 1-2-1.8Z\"/><path class=\"ln\" d=\"M3.8 6.6h16.4M9 6.6V4h6v2.6\"/>","tree":"<path d=\"M12 2.5L5.9 10.7h3L4.4 16.9h15.1l-4.5-6.2h3Z\"/><path class=\"ln\" d=\"M12 16.2v4\"/>","trophy":"<path d=\"M7.3 3h9.6V8.6a4.7 4.7 0 0 1-9.6 0Z\"/><path class=\"ln\" d=\"M7.3 5.6H5a3.1 3.1 0 0 0 3.1 4.1M16.7 5.6h2.3a3.1 3.1 0 0 1-3.1 4.1M12 13.7V16.9M8 20.4h8.1\"/>","tv":"<rect x=\"2.7\" y=\"7.4\" width=\"18.6\" height=\"13.3\" rx=\"2.3\"/><path class=\"ln\" d=\"M8.2 3.8l3.8 3.4l3.8-3.4\"/>","umbrella":"<path d=\"M2.6 12.7a9.4 9.4 0 0 1 18.8 0Z\"/><path class=\"ln\" d=\"M12 12.7v5.6a1.9 1.9 0 0 1-3.8 0\"/>","upload":"<path d=\"M10.6 21.7h2.5v-7h4.7L11.9 8.6l-5.9 5.9h4.7Z\"/><path class=\"ln\" d=\"M4.5 4.3h14.9\"/>","user":"<circle cx=\"12\" cy=\"6.7\" r=\"4.5\"/><path d=\"M3.5 21c0-4.7 3.8-7.8 8.5-7.8s8.5 3.2 8.5 7.8Z\"/>","user-check":"<circle cx=\"8.9\" cy=\"7.5\" r=\"3.2\"/><path d=\"M2.9 19.3c0-3.4 2.7-5.9 5.9-5.9s5.9 2.4 5.9 5.9Z\"/><path class=\"ln\" d=\"M14.8 10.6l1.9 1.9l3.8-3.8\"/>","user-circle":"<path fill-rule=\"evenodd\" d=\"M13 2.9a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7Zm0 3.8a3.3 3.3 0 1 1 0 6.5a3.3 3.3 0 0 1 0-6.5ZM7.2 17.9a6.7 6.7 0 0 1 11.5 0a8.1 8.1 0 0 1-11.5 0Z\"/>","user-lock":"<circle cx=\"9.9\" cy=\"7.2\" r=\"4.2\"/><path d=\"M2.7 20c0-4.2 3.2-7 7.2-7c1.2 0 2.3 0.3 3.2 0.8v6.2Z\"/><rect x=\"14.5\" y=\"14.5\" width=\"6.9\" height=\"5.7\" rx=\"1.3\"/><path class=\"ln\" d=\"M16.4 14.5v-1a1.5 1.5 0 0 1 3 0v1\"/>","user-minus":"<circle cx=\"9.6\" cy=\"7.6\" r=\"3.9\"/><path d=\"M2.8 19.7c0-3.9 3.1-6.6 6.8-6.6c1.2 0 2.2 0.3 3.1 0.8v5.9Z\"/><path class=\"ln\" d=\"M15.1 16.7h5.2\"/>","user-off":"<path d=\"M9.1 4.7A4.2 4.2 0 1 1 15 10.6ZM4.2 20.8c0-4.3 2.8-6.9 6.3-7.2L17.5 20.8Z\"/><path class=\"ln\" d=\"M4 3.5l16.2 16.2\"/>","user-pen":"<circle cx=\"9.8\" cy=\"7.6\" r=\"4\"/><path d=\"M2.9 19.9c0-4 3.1-6.7 6.9-6.7c1.2 0 2.2 0.3 3.1 0.8v5.9Z\"/><path d=\"M19.5 11.4l2.1 2.1l-5.2 5.2l-2.8 0.7l0.7-2.8Z\"/>","user-tag":"<circle cx=\"10\" cy=\"7.4\" r=\"4\"/><path d=\"M2.9 19.9c0-4 3.2-6.8 7-6.8c1.2 0 2.3 0.3 3.2 0.8v6Z\"/><path fill-rule=\"evenodd\" d=\"M14 13.7h4.5l3.1 3.1l-3.1 3.1h-4.5ZM15.2 16.8a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0-2.2 0Z\"/>","video":"<path d=\"M3 7.7a2 2 0 0 1 2-2H14.4a2 2 0 0 1 2 2v8.7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2ZM17.5 10.1l4.2-2.6v8.9l-4.2-2.6Z\"/>","volume":"<path d=\"M2.5 8.9h4l5.4-4.2v14.6l-5.4-4.2H2.5Z\"/><path class=\"ln\" d=\"M15.2 9.3a3.8 3.8 0 0 1 0 5.5M17.7 6.5a7.5 7.5 0 0 1 0 11\"/>","walk":"<circle cx=\"12.4\" cy=\"4.6\" r=\"2.2\"/><path class=\"ln\" d=\"M8.7 19.8l2.4-5.5l1.9 2.2V19.8M11.1 14.3l0.9-5.2l-2.8 1.5l-1.4 2.8M12 9.1l1.8 2.8l2.8 0.9\"/>","wallet":"<path fill-rule=\"evenodd\" d=\"M5.7 2.8H19v4.6h1.6a0.9 0.9 0 0 1 0.9 0.9v12.4H5.1a2.5 2.5 0 0 1-2.5-2.5V5.9a3.1 3.1 0 0 1 3.1-3.1Zm0 2.1a1 1 0 0 0 0 2.1h11.2V4.9ZM16.7 12.9a1.4 1.4 0 1 0 0 2.8a1.4 1.4 0 0 0 0-2.8Z\"/>","water":"<path d=\"M2.7 7.9c2.4 0 2.4-2 4.7-2s2.4 2 4.6 2s2.4-2 4.7-2s2.4 2 4.6 2v3.2c-2.3 0-2.3-2-4.6-2s-2.4 2-4.7 2s-2.3-2-4.6-2s-2.4 2-4.7 2ZM2.7 14.8c2.4 0 2.4-2 4.7-2s2.4 2 4.6 2s2.4-2 4.7-2s2.4 2 4.6 2v3.2c-2.3 0-2.3-2-4.6-2s-2.4 2-4.7 2s-2.3-2-4.6-2s-2.4 2-4.7 2Z\"/>","weight":"<circle cx=\"12\" cy=\"4.8\" r=\"2.6\"/><path d=\"M6.5 8.7h10.9l3.3 12.3H3.2Z\"/>","window":"<path d=\"M2.6 6.2a2.7 2.7 0 0 1 2.7-2.7h13.4a2.7 2.7 0 0 1 2.7 2.7v1.5H2.6ZM2.6 9.8h18.7v7.7a2.7 2.7 0 0 1-2.7 2.7h-13.4a2.7 2.7 0 0 1-2.7-2.7Z\"/>","window-restore":"<path d=\"M2.7 10.4a2.7 2.7 0 0 1 2.7-2.7h8.2a2.7 2.7 0 0 1 2.7 2.7v7.2a2.7 2.7 0 0 1-2.7 2.7h-8.2a2.7 2.7 0 0 1-2.7-2.7ZM7.9 6.4V6.3a2.7 2.7 0 0 1 2.7-2.7h8.2a2.7 2.7 0 0 1 2.7 2.7v7.2a2.7 2.7 0 0 1-2.7 2.7h-1.1v-5.8a4 4 0 0 0-4-4Z\"/>","wrench":"<path d=\"M14.9 2.5a6 6 0 0 0-5.5 8.3L2.7 17.5V21.3H6.5l6.7-6.7a6 6 0 0 0 8.3-5.5l-3.5 1.3l-2.9-2.9l1.3-3.5Z\"/>","x-circle":"<path fill-rule=\"evenodd\" d=\"M12.9 2.7a9.4 9.4 0 1 0 0 18.7a9.4 9.4 0 0 0 0-18.7ZM10.1 7.9L12.9 10.7l2.9-2.9l1.4 1.4l-2.9 2.9l2.9 2.9l-1.4 1.4l-2.9-2.9l-2.9 2.9l-1.4-1.4l2.9-2.9l-2.9-2.9Z\"/>","zap":"<path d=\"M13.9 2.5L5.2 13.3h6.1l-1.2 8.1l8.7-10.8h-6.1Z\"/>"};
+ const FA = {"address-card":"id","adjust":"contrast","align-left":"align-left","angle-down":"down","angle-right":"chevron","angles-left":"chevrons-left","angles-right":"chevrons-right","arrow-down":"arrow-down","arrow-down-short-wide":"sort-asc","arrow-down-wide-short":"sort","arrow-left":"arrow-left","arrow-right":"arrow-right","arrow-right-arrow-left":"swap","arrow-right-from-bracket":"leave","arrow-right-to-bracket":"enter","arrow-rotate-right":"retry","arrow-trend-down":"trend-down","arrow-trend-up":"trend-up","arrow-up":"arrow-up","arrow-up-from-bracket":"share","arrow-up-right-from-square":"open","arrows-left-right":"arrows-h","arrows-rotate":"retry","arrows-to-dot":"focus","arrows-up-down-left-right":"move","at":"at","backward":"rewind","backward-fast":"rewind","balance-scale":"scale","ban":"ban","bars":"menu","basket-shopping":"cart","bell":"bell","bolt":"zap","bolt-lightning":"zap","book":"book","book-open":"book-open","box":"box","box-archive":"archive","box-open":"box","brain":"brain","bridge":"bridge","briefcase":"briefcase","briefcase-medical":"medical","broadcast-tower":"live","bug":"bug","bullhorn":"megaphone","bullseye":"target","burst":"burst","calendar":"calendar","calendar-day":"calendar","calendar-days":"calendar","camera":"camera","camera-cctv":"camera","camera-retro":"camera","camera-rotate":"camera-flip","camera-slash":"camera-off","campground":"tent","car":"car","caret-down":"down","cart":"cart","cart-plus":"cart","cart-shopping":"cart","chart-area":"chart","chart-bar":"chart","chart-column":"chart","chart-line":"chart","chart-simple":"chart","check":"check","check-circle":"check-circle","check-double":"check-double","chevron-down":"down","chevron-left":"back","chevron-right":"chevron","chevron-up":"up","child":"child","circle":"circle","circle-check":"check-circle","circle-dollar-to-slot":"donate","circle-dot":"record","circle-exclamation":"alert-circle","circle-half-stroke":"contrast","circle-info":"info","circle-minus":"minus-circle","circle-nodes":"nodes","circle-notch":"busy","circle-play":"play","circle-plus":"plus-circle","circle-question":"help","circle-user":"user-circle","circle-xmark":"x-circle","city":"city","clapperboard":"clapper","clipboard":"clipboard","clipboard-check":"clipboard-check","clipboard-list":"clipboard","clock":"clock","clock-rotate-left":"history","clone":"copy","closed-captioning":"cc","cloud":"cloud","cloud-arrow-up":"upload","cloud-bolt":"storm","cloud-moon":"cloud","cloud-rain":"cloud","cloud-showers-heavy":"cloud","cloud-slash":"cloud-off","cloud-sun":"cloud","code":"code","code-branch":"branch","code-commit":"commit","code-fork":"branch","coin":"coin","coins":"coin","comment":"chat","comment-dots":"chat","comment-slash":"chat-off","comments":"chat","compass":"compass","compress":"compress","cookie":"cookie","cookie-bite":"cookie","copy":"copy","couch":"couch","credit-card":"card","crown":"crown","cube":"box","cubes":"cubes","cut":"scissors","database":"database","desktop":"computer","diagram-project":"nodes","dice":"dice","dice-five":"dice","dice-six":"dice","display":"computer","dollar-sign":"dollar","door-closed":"door","door-open":"door","down-left-and-up-right-to-center":"compress","download":"download","droplet":"drop","ear-listen":"ear","earth-americas":"globe","earth-europe":"globe","ellipsis":"more","ellipsis-vertical":"more","envelope":"mail","eraser":"eraser","exchange-alt":"swap","expand":"expand","external-link":"open","eye":"eye","eye-slash":"eye-off","face-grin":"laugh","face-grin-squint":"laugh","face-grin-squint-tears":"laugh","face-grin-stars":"smile","face-laugh":"laugh","face-smile":"smile","faucet":"drop","faucet-drip":"drop","file":"file","file-audio":"file","file-code":"file-code","file-image":"image","file-lines":"file","file-pdf":"file","file-pen":"pencil","film":"film","filter":"filter","fire":"fire","fish":"fish","fist-raised":"fist","flag":"flag","flag-checkered":"finish","flask":"flask","flask-vial":"flask","floppy-disk":"save","folder":"folder","folder-open":"folder","folder-tree":"folder-tree","font":"text","forward":"fast-forward","forward-fast":"fast-forward","forward-step":"skip","gamepad":"game","gauge":"gauge","gauge-high":"gauge","gauge-simple":"gauge","gauge-simple-high":"gauge","gauge-simple-low":"gauge","gavel":"gavel","gear":"gear","gears":"gear","gem":"gem","ghost":"ghost","gift":"gift","globe":"globe","grid":"grid","grip":"grip","grip-vertical":"grip","hammer":"hammer","hand":"hand","hand-fist":"fist","hand-holding-dollar":"coin","hand-holding-heart":"care","hand-point-right":"hand","hand-pointer":"hand","handshake":"handshake","handshake-angle":"handshake","hard-drive":"drive","hashtag":"hash","hat-wizard":"wizard","hdd":"drive","heading":"heading","headphones":"headphones","headphones-simple":"headphones","headset":"headphones","heart":"heart","heart-crack":"heart-broken","heart-pulse":"heart-pulse","heartbeat":"heart-pulse","hiking":"mountain","hot-tub":"hot-tub","hot-tub-person":"hot-tub","hourglass":"hourglass","hourglass-end":"hourglass","hourglass-half":"hourglass","hourglass-start":"hourglass","house":"home","house-chimney":"home","house-signal":"home","icicles":"snow","icons":"shapes","id-card":"id","image":"image","images":"image","inbox":"inbox","infinity":"infinity","info-circle":"info","key":"key","keyboard":"keyboard","kit-medical":"medical","language":"language","laptop":"computer","layer-group":"layers","leaf":"leaf","left-right":"arrows-h","life-ring":"lifebuoy","lightbulb":"idea","link":"link","link-slash":"link-off","list":"list","list-check":"steps","list-ol":"list-ordered","list-ul":"list","location-dot":"pin","location-pin":"pin","lock":"lock","lungs":"lungs","magnifying-glass":"search","map":"map","map-location-dot":"map","map-marker-alt":"pin","map-pin":"pin","mask":"mask","message":"chat","microchip":"chip","microphone":"mic","microphone-lines":"mic","microphone-slash":"mic-off","minus":"minus","minus-circle":"minus-circle","mobile-screen":"phone","mobile-screen-button":"phone","money-bill":"money","money-bill-1":"money","money-bill-transfer":"money","money-bill-wave":"money","money-check":"money","moon":"moon","mountain":"mountain","mountain-sun":"mountain","mug-hot":"mug","mug-saucer":"mug","music":"music","network-wired":"nodes","newspaper":"news","object-group":"group","paint-brush":"palette","paintbrush":"palette","palette":"palette","paper-plane":"plane","parking":"parking","paste":"clipboard","pause":"pause","paw":"paw","pen":"pencil","pen-nib":"pencil","pen-to-square":"pencil","pencil":"pencil","people-arrows":"members","people-group":"members","person-hiking":"mountain","person-shelter":"shelter","person-swimming":"water","person-walking":"walk","phone":"phone","phone-slash":"phone-off","photo-film":"film","piggy-bank":"piggy","plane":"airplane","plane-departure":"airplane","play":"play","plug":"plug","plug-circle-bolt":"plug","plug-circle-check":"plug","plug-circle-xmark":"plug","plus":"plus","plus-circle":"plus-circle","print":"print","puzzle":"puzzle","puzzle-piece":"puzzle","puzzle-piece-simple":"puzzle","question":"help","quote-left":"quote","quote-right":"quote","radio":"radio","ranking-star":"ranking","recycle":"recycle","reply":"reply","restroom":"restroom","right-from-bracket":"leave","right-left":"swap","right-to-bracket":"enter","road":"road","robot":"robot","rocket":"rocket","rotate":"retry","rotate-back":"undo","rotate-left":"retry","rotate-right":"retry","route":"route","rss":"rss","satellite-dish":"dish","save":"save","scale-balanced":"scale","scissors":"scissors","screwdriver-wrench":"wrench","scroll":"scroll","search":"search","seedling":"seedling","server":"server","shapes":"shapes","share-nodes":"nodes","shield":"shield","shield-halved":"shield","shirt":"shirt","signal":"wifi","signature":"signature","sitemap":"nodes","skull":"skull","skull-crossbones":"skull","slash":"slash","sliders":"sliders","sliders-h":"sliders","smog":"cloud","snowflake":"snow","sparkles":"spark","spinner":"busy","square":"square","square-parking":"parking","star":"star","star-of-life":"spark","step-forward":"skip","stop":"stop","store":"store","suitcase":"briefcase","suitcase-medical":"medical","sun":"sun","sword":"sword","table":"table","table-cells":"grid","table-columns":"columns","table-list":"table","tag":"tag","tags":"tag","television":"tv","terminal":"terminal","text-height":"text","th":"grid","thumbs-up":"thumbs-up","thumbtack":"tack","ticket":"ticket","timeline":"timeline","times":"close","toolbox":"toolbox","tower-broadcast":"live","tower-cell":"live","train":"train","trash":"trash","trash-can":"trash","tree":"tree","triangle-exclamation":"alert","trophy":"trophy","tv":"tv","umbrella":"umbrella","umbrella-beach":"umbrella","undo":"undo","unlock":"lock","up-right-and-down-left-from-center":"expand","up-right-from-square":"open","upload":"upload","user":"user","user-astronaut":"user","user-check":"user-check","user-circle":"user-circle","user-edit":"user-pen","user-group":"members","user-lock":"user-lock","user-minus":"user-minus","user-pen":"user-pen","user-plus":"invite","user-secret":"user","user-shield":"shield","user-slash":"user-off","user-tag":"user-tag","users":"members","utensils":"food","video":"video","video-slash":"video","volume-high":"volume","volume-low":"volume","volume-off":"mute","volume-xmark":"mute","wallet":"wallet","wand-magic":"spark","wand-magic-sparkles":"spark","water":"water","wave-square":"wave","weight-hanging":"weight","wifi":"wifi","wind":"wind","window-maximize":"window","window-restore":"window-restore","wrench":"wrench","xmark":"close","xmark-circle":"x-circle"};
+ const FB = {"youtube":"youtube","square-youtube":"youtube","twitch":"twitch","discord":"discord","github":"github","square-github":"github","x-twitter":"x","square-x-twitter":"x","twitter":"x","kick":"kick","kickstarter-k":"kick","kickstarter":"kickstarter","google":"google","tiktok":"tiktok","threads":"threads","spotify":"spotify","soundcloud":"soundcloud","reddit":"reddit","reddit-alien":"reddit","tumblr":"tumblr","hacker-news":"ycombinator","line":"line","paypal":"paypal","patreon":"patreon","mastodon":"mastodon","linux":"linux","instagram":"instagram","facebook":"facebook","facebook-f":"facebook","cloudflare":"cloudflare","brave":"brave","bluesky":"bluesky","bitcoin":"bitcoin","apple":"apple","telegram":"telegram","whatsapp":"whatsapp","steam":"steam","snapchat":"snapchat","pinterest":"pinterest","vimeo":"vimeo","vimeo-v":"vimeo","npm":"npm","node":"nodedotjs","node-js":"nodedotjs","docker":"docker","python":"python","android":"android","chrome":"googlechrome","firefox":"firefoxbrowser","firefox-browser":"firefoxbrowser","gitlab":"gitlab","ubuntu":"ubuntu","raspberry":"raspberrypi","raspberry-pi":"raspberrypi","stripe":"stripe","cc-stripe":"stripe","signal":"signal","matrix":"matrix","medium":"medium","wikipedia":"wikipedia","wikipedia-w":"wikipedia","ethereum":"ethereum"};
+ // Font Awesome's style, size, layout and animation classes: they say how, not what.
+ const MOD = /^fa-(solid|regular|light|thin|duotone|sharp|sharp-duotone|classic|brands|fw|lg|xl|xs|sm|2xs|2xl|[0-9]+x|spin|spin-pulse|spin-reverse|pulse|beat|beat-fade|bounce|fade|flip|shake|border|inverse|li|ul|stack|stack-1x|stack-2x|layers|layers-text|layers-counter|pull-left|pull-right|rotate-[0-9]+|rotate-by|flip-horizontal|flip-vertical|flip-both|swap-opacity|width-auto|width-fixed)$/;
+ const BUSY = '<circle cx="6" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="18" cy="12" r="1.8"/>';
+ const CSS = ".ovg::before,.ovg::after{content:none!important;display:none!important}.ovg{display:inline-block;width:1.15em;height:1.15em;line-height:1;vertical-align:-.18em;font-family:inherit!important;font-style:normal!important}.ovg>svg{display:block;width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:calc(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25));stroke-linecap:round;stroke-linejoin:round;overflow:visible}html[data-ov-icons=solid]{--ovg-pref:99999px}html[data-ov-icons=line]{--ovg-pref:0px}.ovg>svg.has-s{stroke-width:min(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25), max(0px, (1.15em - var(--ovg-pref, var(--ovg-solid-below, 18.4px))) * 999))}.ovg>svg>.ovg-s{fill:currentColor;stroke:none;clip-path:inset(max(0px, (1.15em - var(--ovg-pref, var(--ovg-solid-below, 18.4px))) * 999))}.ovg>svg>.ovg-s .ln{fill:none;stroke:currentColor;stroke-width:2.6px}.ovg>svg>.ovg-s,.ovg>svg>.ovg-s *{stroke-dasharray:none!important;animation:none!important}.ovg.ovg-solid>svg.has-s{stroke-width:0}.ovg.ovg-solid>svg>.ovg-s{clip-path:none}.ovg.ovg-line>svg.has-s{stroke-width:calc(var(--ovg-stroke,2) * 1px + max(0px, (18.4px - 1.15em) * .25))}.ovg.ovg-line>svg>.ovg-s{clip-path:inset(50%)}.ovg.ovg-thin{--ovg-stroke:1}.ovg.ovg-light{--ovg-stroke:1.5}.ovg.ovg-regular{--ovg-stroke:2}.ovg.ovg-bold{--ovg-stroke:2.5}.ovg.ovg-duotone>svg>.ovg-s{clip-path:none;opacity:.2;fill:var(--ovg-duotone-color,currentColor)}.ovg.ovg-duotone>svg.has-s{stroke-width:var(--ovg-stroke,2)}:where([aria-current=page],[aria-current=true],[aria-pressed=true],[aria-selected=true],.is-here)>.ovg:not(.ovg-line)>svg.has-s{stroke-width:0}:where([aria-current=page],[aria-current=true],[aria-pressed=true],[aria-selected=true],.is-here)>.ovg:not(.ovg-line)>svg>.ovg-s{clip-path:none}.ovg.ovg-brand>svg{fill:currentColor;stroke:none}.ovg.ovg-brand.ovg-brand-color{color:var(--ovg-brand)}.ovg.fa-thin{--ovg-stroke:1}.ovg.fa-light{--ovg-stroke:1.5}.ovg.fa-solid{--ovg-solid-below:99999px}.ovg.fa-duotone{--ovg-duotone-color:var(--fa-secondary-color,currentColor)}.ovg.fa-duotone>svg{stroke:var(--fa-primary-color,currentColor)}.ovg.fa-2xs{font-size:.625em}.ovg.fa-xs{font-size:.75em}.ovg.fa-sm{font-size:.875em}.ovg.fa-lg{font-size:1.25em}.ovg.fa-xl{font-size:1.5em}.ovg.fa-2xl{font-size:2em}.ovg.fa-1x{font-size:1em}.ovg.fa-2x{font-size:2em}.ovg.fa-3x{font-size:3em}.ovg.fa-4x{font-size:4em}.ovg.fa-5x{font-size:5em}.ovg.fa-6x{font-size:6em}.ovg.fa-7x{font-size:7em}.ovg.fa-8x{font-size:8em}.ovg.fa-9x{font-size:9em}.ovg.fa-10x{font-size:10em}.ovg.fa-fw{width:1.25em;text-align:center}.ovg.fa-border{box-sizing:content-box;border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);padding:var(--fa-border-padding,.2em .25em .15em)}.ovg.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.ovg.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.ovg.fa-rotate-90>svg{transform:rotate(90deg)}.ovg.fa-rotate-180>svg{transform:rotate(180deg)}.ovg.fa-rotate-270>svg{transform:rotate(270deg)}.ovg.fa-rotate-by>svg{transform:rotate(var(--fa-rotate-angle,0))}.ovg.fa-flip-horizontal>svg{transform:scale(-1,1)}.ovg.fa-flip-vertical>svg{transform:scale(1,-1)}.ovg.fa-flip-both>svg{transform:scale(-1,-1)}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{position:absolute;left:calc(var(--fa-li-width,2em)*-1);width:var(--fa-li-width,2em);text-align:center;line-height:inherit}.fa-stack{display:inline-block;position:relative;width:2.5em;height:2em;line-height:2em;vertical-align:middle}.fa-stack>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-stack>.ovg.fa-stack-2x{font-size:2em}.ovg.fa-inverse{color:var(--fa-inverse,#fff)}.fa-layers{display:inline-block;position:relative;width:1em;height:1em;text-align:center;vertical-align:-.125em}.fa-layers>.ovg{position:absolute;inset:0;margin:auto;width:1em;height:1em}.fa-layers-text{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:inline-block}.fa-layers-counter{position:absolute;top:var(--fa-top,0);right:var(--fa-right,0);display:inline-block;box-sizing:border-box;min-width:var(--fa-counter-min-width,1.5em);max-width:var(--fa-counter-max-width,5em);padding:var(--fa-counter-padding,.25em .5em);border-radius:var(--fa-counter-border-radius,1em);background:var(--fa-counter-background-color,#ff253a);color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transform:scale(var(--fa-counter-scale,.25));transform-origin:top right}.ovg.fa-beat{animation:ovgBeat var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-bounce{animation:ovgBounce var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-fade{animation:ovgFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-beat-fade{animation:ovgBeatFade var(--fa-animation-duration,1s) var(--fa-animation-timing,cubic-bezier(.4,0,.6,1)) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-flip{animation:ovgFlip var(--fa-animation-duration,1s) var(--fa-animation-timing,ease-in-out) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-shake{animation:ovgShake var(--fa-animation-duration,1s) var(--fa-animation-timing,linear) var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite) var(--fa-animation-direction,normal)}.ovg.fa-spin,.ovg.fa-pulse,.ovg.fa-spin-pulse{animation:ovgBreathe var(--fa-animation-duration,1.6s) ease-in-out var(--fa-animation-delay,0s) var(--fa-animation-iteration-count,infinite)!important}.ovg[data-ovg=busy].fa-spin,.ovg[data-ovg=busy].fa-pulse,.ovg[data-ovg=busy].fa-spin-pulse{animation:none!important}@keyframes ovgBeat{0%,90%{transform:scale(1)}45%{transform:scale(var(--fa-beat-scale,1.25))}}@keyframes ovgBounce{0%{transform:scale(1,1) translateY(0)}10%{transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{transform:scale(1,1) translateY(var(--fa-bounce-rebound,-.125em))}64%,100%{transform:scale(1,1) translateY(0)}}@keyframes ovgFade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes ovgBeatFade{0%,100%{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}50%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes ovgFlip{50%{transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes ovgShake{0%{transform:rotate(-15deg)}4%{transform:rotate(15deg)}8%,24%{transform:rotate(-18deg)}12%,28%{transform:rotate(18deg)}16%{transform:rotate(-22deg)}20%{transform:rotate(22deg)}32%{transform:rotate(-12deg)}36%{transform:rotate(12deg)}40%,100%{transform:rotate(0)}}@keyframes ovgBreathe{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.55;transform:scale(.9)}}.ovg.ovg-draw>svg>*{stroke-dasharray:1;stroke-dashoffset:1;animation:ovgDraw var(--ovg-draw-duration,.6s) cubic-bezier(.4,0,.2,1) var(--ovg-draw-delay,0s) forwards}.ovg.ovg-draw>svg>*:nth-child(2){animation-delay:calc(var(--ovg-draw-delay,0s) + .12s)}.ovg.ovg-draw>svg>*:nth-child(n+3){animation-delay:calc(var(--ovg-draw-delay,0s) + .24s)}@keyframes ovgDraw{to{stroke-dashoffset:0}}.ovg.ovg-once{--fa-animation-iteration-count:1}.ovg[data-ovg=busy]>svg{fill:currentColor;stroke:none}.ovg[data-ovg=busy] circle{animation:ovgHop 1s ease-in-out infinite;transform-box:fill-box;transform-origin:center}.ovg[data-ovg=busy] circle:nth-child(2){animation-delay:.15s}.ovg[data-ovg=busy] circle:nth-child(3){animation-delay:.3s}@keyframes ovgHop{0%,60%,100%{transform:translateY(0);opacity:.55}30%{transform:translateY(-3px);opacity:1}}html[data-ov-motion=off] .ovg,html[data-ov-motion=off] .ovg[data-ovg=busy] circle,html[data-ov-motion=off] .ovg.ovg-draw>svg>*{animation:none!important;stroke-dashoffset:0!important}";
+ const nameOf = (el) => { for (const c of el.classList) if (c.startsWith('fa-') && !MOD.test(c)) return c.slice(3); return null; };
+ const fromFa = (fa) => { const n = String(fa || '').replace(/^fa-/, ''); return Object.prototype.hasOwnProperty.call(FA, n) ? FA[n] : null; };
+ // data-fa-transform, in Font Awesome's units (a sixteenth of the icon).
+ const TF = /^(grow|shrink|up|down|left|right|rotate)-(-?[0-9.]+)$|^flip-(h|v)$/;
+ function transformOf(spec) {
+ let s = 1, x = 0, y = 0, r = 0, fx = 1, fy = 1, any = false;
+ for (const p of String(spec || '').toLowerCase().split(/\s+/)) {
+ const m = TF.exec(p);
+ if (!m) continue;
+ any = true;
+ if (m[3]) { if (m[3] === 'h') fx = -1; else fy = -1; continue; }
+ const n = Number(m[2]);
+ if (m[1] === 'grow') s += n / 16; else if (m[1] === 'shrink') s -= n / 16;
+ else if (m[1] === 'up') y -= n / 16; else if (m[1] === 'down') y += n / 16;
+ else if (m[1] === 'left') x -= n / 16; else if (m[1] === 'right') x += n / 16; else r += n;
+ }
+ return any ? 'translate(' + x + 'em,' + y + 'em) rotate(' + r + 'deg) scale(' + (fx * s) + ',' + (fy * s) + ')' : '';
+ }
+ // Brand logos (4.72.0): brands.js from beside this script, once, then every waiting element is drawn.
+ const SRC = (document.currentScript && document.currentScript.src) || '';
+ let brandState = 0; // 0 idle, 1 loading, 2 ready, 3 unavailable
+ const waiting = new Set();
+ const BRANDS_REV = '0da501c80e60';
+ // A logo far below the fold (a footer's social links) waits until it comes near the screen (4.138.0): most visits
+ // never get there, and brands.js is 26 KB. One that is in view, or hidden in a closed fold that opens, loads then.
+ let brandSeen = null;
+ function wantBrands(el) {
+ if (brandState || typeof IntersectionObserver !== 'function' || !el.getBoundingClientRect) { loadBrands(); return; }
+ const r = el.getBoundingClientRect();
+ if ((r.width || r.height) && r.bottom > -800 && r.top < (window.innerHeight || 0) + 800) { loadBrands(); return; }
+ brandSeen = brandSeen || new IntersectionObserver((es) => { if (es.some((e) => e.isIntersecting)) { brandSeen.disconnect(); brandSeen = null; loadBrands(); } }, { rootMargin: '800px 0px' });
+ brandSeen.observe(el);
+ }
+ function loadBrands() {
+ if (brandState) return;
+ if (window.OVBrands) { brandState = 2; return; }
+ brandState = 1;
+ // No backslashes here: this text is inside a template literal.
+ const base = SRC ? SRC.split(/[?#]/)[0] : '';
+ const dir = base && /ov-glyphs[.]js$/.test(base) ? base.slice(0, base.lastIndexOf('/') + 1) : 'https://openvibe.network/shared/';
+ const sc = document.createElement('script');
+ sc.src = dir + 'brands.js' + (BRANDS_REV ? '?v=' + BRANDS_REV : '');
+ sc.async = true;
+ sc.onload = () => { brandState = window.OVBrands ? 2 : 3; waiting.forEach((el) => drawBrand(el)); waiting.clear(); };
+ sc.onerror = () => { brandState = 3; waiting.clear(); };
+ document.head.appendChild(sc);
+ }
+ function drawBrand(el) {
+ let fa = null;
+ for (const c of el.classList) if (c.startsWith('fa-') && c !== 'fa-brands' && !MOD.test(c) && Object.prototype.hasOwnProperty.call(FB, c.slice(3))) { fa = c.slice(3); break; }
+ if (!fa) return;
+ const slug = FB[fa];
+ if (el.dataset.ovg === 'brand:' + slug) return;
+ if (!window.OVBrands) { if (brandState !== 3) { waiting.add(el); wantBrands(el); } return; }
+ const b = window.OVBrands.get(slug);
+ if (!b || (!el.dataset.ovg && el.textContent.trim())) return;
+ el.classList.add('ovg', 'ovg-brand');
+ el.dataset.ovg = 'brand:' + slug; el.dataset.ovgFa = fa;
+ // A near-black brand (GitHub, X, Apple…) would vanish on a dark page: it wears the text's colour instead.
+ const n = parseInt(b.hex.slice(1), 16), lum = (0.2126 * (n >> 16) + 0.7152 * ((n >> 8) & 255) + 0.0722 * (n & 255)) / 255;
+ el.style.setProperty('--ovg-brand', lum < 0.12 ? 'currentColor' : b.hex);
+ const title = el.getAttribute('title');
+ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }
+ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');
+ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="' + b.path + '"/></svg>';
+ }
+ function draw(el) {
+ if (el.classList.contains('fa-brands') || el.classList.contains('fab')) return drawBrand(el);
+ const fa = nameOf(el);
+ const g = fa && fromFa(fa);
+ if (!g) { if (el.dataset.ovg) { el.classList.remove('ovg'); delete el.dataset.ovg; delete el.dataset.ovgFa; delete el.dataset.ovgTf; el.textContent = ''; } return; }
+ const tf = el.getAttribute('data-fa-transform') || '';
+ if (el.dataset.ovgFa === fa && (el.dataset.ovgTf || '') === tf) { if (!el.classList.contains('ovg')) el.classList.add('ovg'); return; } // adding a class it has would observe itself forever
+ if (!el.dataset.ovg && el.textContent.trim()) return; // an icon element with text of its own is not an icon
+ el.classList.add('ovg');
+ el.dataset.ovg = g; el.dataset.ovgFa = fa;
+ if (tf) el.dataset.ovgTf = tf; else delete el.dataset.ovgTf;
+ const title = el.getAttribute('title');
+ if (title && !el.hasAttribute('aria-label')) { el.setAttribute('role', 'img'); el.setAttribute('aria-label', title); }
+ else if (!el.hasAttribute('aria-label') && !title) el.setAttribute('aria-hidden', 'true');
+ const t = transformOf(tf);
+ el.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"' + (S[g] ? ' class="has-s"' : '') + (t ? ' style="transform:' + t + '"' : '') + '>' + (g === 'busy' ? BUSY : G[g] + (S[g] ? '<g class="ovg-s">' + S[g] + '</g>' : '')) + '</svg>';
+ // ovg-draw draws each line in: every shape measures 1 along its length.
+ if (el.classList.contains('ovg-draw')) el.firstChild.childNodes.forEach((n) => { if (n.setAttribute) n.setAttribute('pathLength', '1'); });
+ }
+ const SEL = 'i[class*="fa-"],span[class*="fa-"]';
+ function swap(root) {
+ const r = root || document;
+ if (r.matches && r.matches(SEL)) draw(r);
+ if (r.querySelectorAll) r.querySelectorAll(SEL).forEach(draw);
+ }
+ function start() {
+ if (!document.getElementById('ov-glyphs-css')) { const s = document.createElement('style'); s.id = 'ov-glyphs-css'; s.textContent = CSS; document.head.appendChild(s); }
+ swap(document);
+ try {
+ new MutationObserver((records) => {
+ for (const r of records) {
+ if (r.type === 'attributes') { if (r.target.matches && r.target.matches(SEL)) draw(r.target); continue; }
+ r.addedNodes.forEach((n) => { if (n.nodeType === 1) swap(n); });
+ }
+ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-fa-transform'] });
+ } catch (e) { /* no observer: what was there is swapped */ }
+ }
+ window.OVGlyphs = { swap, fromFa };
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
+})();
@@ -26,7 +26,11 @@
@@ -26,7 +26,11 @@
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* - a page on its way (navbar): the ring empties and draws itself round again as the next page loads, paced by how
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* long this site's pages usually take in this browser (learned per site, boost moves and full loads apart); the
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
* V nods while it waits and hops when the page is here (window.OVMark.navStart/navEnd for single-page apps);
- * - busy: the V bobs and the O breathes, in turn; ok: the V becomes a tick, green, and hops (the tick only ever
+ * - busy (working): the O breaks into eight dots and three bright ones go round it, dot to dot, while the V bobs
+ * (the owner, 2026-11-10: "make the O turn into a bunch of little circles… a spinner that has little dots around
+ * it… that actually looks like it's loading"); think (your agent is thinking or writing): three dots rise in turn
+ * inside the O and the V's two strokes glimmer one after the other, the one sign used wherever tokens are on
+ * their way; ok: the V becomes a tick, green, and hops (the tick only ever
* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O
* means "done"); error: the V becomes "!", red, and the mark shakes; no: the V becomes "×", grey, and the O
* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and
* shakes its head; notify: a dot pops up by the V and the O bounces; live: the O is the record light, red, and
* ripples go out from it; sleep: the O closes like an eye and the V sinks.
* ripples go out from it; sleep: the O closes like an eye and the V sinks.
@@ -36,19 +40,25 @@
@@ -36,19 +40,25 @@
* visit, from the first time a mark is on screen in this tab; later pages of a multi-page site open with the mark at
* visit, from the first time a mark is on screen in this tab; later pages of a multi-page site open with the mark at
* rest), while it or its link is hovered or focused (and LINGER_MS after), and while it shows a state, a page load or
* rest), while it or its link is hovered or focused (and LINGER_MS after), and while it shows a state, a page load or
* progress. Otherwise its CSS animations are paused where they are. It never moves while the page is hidden, while it
* progress. Otherwise its CSS animations are paused where they are. It never moves while the page is hidden, while it
- * is off-screen (IntersectionObserver), with data-static, under prefers-reduced-motion or under the network's own
- * reduced-motion setting (html[data-ov-motion=reduced], theme-loader.js); its colours and shapes still change.
+ * is off-screen (IntersectionObserver), with data-static, or with motion off (html[data-ov-motion=off],
+ * theme-loader.js); its colours and shapes still change. Calm (html[data-ov-motion=reduced]: the person's choice, or
+ * the device asking for less motion) keeps the intro, the hover and every state, drops the linger after a hover and
+ * lets fewer leaves fall a shorter way.
*
*
* The season (theme-loader.js sets <html data-ov-season-mark="halloween|winter"> for anyone who has not turned seasonal
* The season (theme-loader.js sets <html data-ov-season-mark="halloween|winter"> for anyone who has not turned seasonal
* themes off, on any theme): the mark wears it, the O a pumpkin with its stem and leaf in October, caps of snow in
* themes off, on any theme): the mark wears it, the O a pumpkin with its stem and leaf in October, caps of snow in
* December. Now and then (every 4 to 8 minutes) a mark on screen flickers for a moment, and on the first visit of the
* December. Now and then (every 4 to 8 minutes) a mark on screen flickers for a moment, and on the first visit of the
- * day a few leaves (snowflakes in December) drift down from the first mark seen. Never under either reduced-motion setting.
+ * day a few leaves (snowflakes in December) drift down from the first mark seen: seven in full, three under Calm, none
+ * with motion off.
*/
*/
(function () {
(function () {
'use strict';
'use strict';
if (window.__ovMark) return; window.__ovMark = true;
if (window.__ovMark) return; window.__ovMark = true;
let n = 0;
let n = 0;
const f = (v) => +v.toFixed(2);
const f = (v) => +v.toFixed(2);
+ // The ground around the V where it crosses the O: the wide gap (the owner's pick, 2026-11-10, the Arena's P07:
+ // "more ground around the V where it crosses"); it was 7.
+ const HALO = 14;
// The Pair in a 100-unit box, centred on (50, 50) (openvibe-shared/mark: the same numbers; test/mark.test.js).
// The Pair in a 100-unit box, centred on (50, 50) (openvibe-shared/mark: the same numbers; test/mark.test.js).
function pair(r, sw, vsw, a = 0.86, t = 1.5, b = 2.16) {
function pair(r, sw, vsw, a = 0.86, t = 1.5, b = 2.16) {
const R = r + sw / 2, right = b * R + vsw / 2, cx = 50 - (right - R) / 2, cy = 50;
const R = r + sw / 2, right = b * R + vsw / 2, cx = 50 - (right - R) / 2, cy = 50;
@@ -66,7 +76,7 @@
@@ -66,7 +76,7 @@
err: [mx, A[1], mx, T[1] - vsw * 1.28, mx, T[1], mx, T[1] + 0.01],
err: [mx, A[1], mx, T[1] - vsw * 1.28, mx, T[1], mx, T[1] + 0.01],
};
};
for (const k in shapes) shapes[k] = shapes[k].map(f);
for (const k in shapes) shapes[k] = shapes[k].map(f);
- return { cx: f(cx), cy, r, sw, R: f(R), vsw, halo: vsw + 7, A, T, B, ring, box, wideBox, shapes };
+ return { cx: f(cx), cy, r, sw, R: f(R), vsw, halo: vsw + HALO, A, T, B, ring, box, wideBox, shapes };
}
}
const FORMS = { pair: pair(20, 13, 13), heavy: pair(18, 18, 17) };
const FORMS = { pair: pair(20, 13, 13), heavy: pair(18, 18, 17) };
const formOf = (v) => (v === 'heavy' || v === 'solid' ? 'heavy' : 'pair');
const formOf = (v) => (v === 'heavy' || v === 'solid' ? 'heavy' : 'pair');
@@ -131,7 +141,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@@ -131,7 +141,19 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}
.ov-mark[data-ovm-arrive] .og{animation:ovmPulse .5s ease-out 1}
/* The states. */
/* The states. */
.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}
.ov-mark[data-state=busy] .vg{animation:ovmBob .9s ease-in-out infinite}
-.ov-mark[data-state=busy] .og{animation:ovmBreathe .9s ease-in-out .45s infinite}
+/* Working: the O as eight dots (a dash of no length with a round cap is a dot), three bright ones hopping round. */
+.ov-mark[data-state=busy] :is(.o,.ot){stroke-linecap:round;stroke-width:var(--ovm-dot-w)}
+.ov-mark[data-state=busy] .ot{opacity:.3;stroke-dasharray:0 12.5}
+.ov-mark[data-state=busy] .o{stroke-dasharray:0 12.5 0 12.5 0 75;animation:ovmDots .96s steps(8,end) infinite}
+.ov-mark[data-state=busy] .og{animation:ovmSettle .4s cubic-bezier(.2,1.3,.3,1) 1}
+/* Thinking: three dots inside the O rise in turn, the ring quiets, the V's strokes glimmer one after the other. */
+.ov-mark .td{display:none;fill:var(--ovm-o);transform-box:fill-box;transform-origin:center}
+.ov-mark[data-state=think] .td{display:inline;animation:ovmThink 1.2s ease-in-out infinite}
+.ov-mark[data-state=think] .td2{animation-delay:.16s}
+.ov-mark[data-state=think] .td3{animation-delay:.32s}
+.ov-mark[data-state=think] .o{stroke-opacity:.5;transition:stroke-opacity .3s}
+.ov-mark[data-state=think] .va{animation:ovmSpeak 1.2s ease-in-out infinite}
+.ov-mark[data-state=think] .vb{animation:ovmSpeak 1.2s ease-in-out .6s infinite}
.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}
.ov-mark[data-state=ok]{--ovm-o:var(--success,#22c55e);--ovm-v:var(--success,#22c55e)}
.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}
.ov-mark[data-state=ok] .vg{animation:ovmHop .56s ease-out .14s 1 both}
.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}
.ov-mark[data-state=error]{--ovm-o:var(--danger,#ef4444);--ovm-v:var(--danger,#ef4444)}
@@ -168,6 +190,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@@ -168,6 +190,10 @@ html[data-ov-season-mark] .ov-mark[data-ovm-intro] .sl{animation:ovmLeaf .9s cub
@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}
@keyframes ovmHop{0%,100%{transform:translateY(0) scaleY(1)}35%{transform:translateY(-9px) scaleY(1.04)}70%{transform:translateY(0) scaleY(.96)}}
@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
@keyframes ovmBob{0%,100%{transform:translateY(0)}50%{transform:translateY(6px)}}
@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}
@keyframes ovmBreathe{0%,100%{transform:scale(1)}50%{transform:scale(.88)}}
+@keyframes ovmDots{to{stroke-dashoffset:-100}}
+@keyframes ovmSettle{from{transform:scale(.86)}}
+@keyframes ovmThink{0%,55%,100%{transform:translateY(0);opacity:.4}28%{transform:translateY(-2.6px);opacity:1}}
+@keyframes ovmSpeak{0%,100%{opacity:1}50%{opacity:.5}}
@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
@keyframes ovmPulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}
@keyframes ovmShake{0%,100%{transform:translateX(0)}15%{transform:translateX(-7px)}30%{transform:translateX(7px)}45%{transform:translateX(-6px)}60%{transform:translateX(6px)}80%{transform:translateX(-3px)}}
@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}
@keyframes ovmNo{0%,100%{transform:translateX(0)}20%{transform:translateX(-4px)}40%{transform:translateX(4px)}60%{transform:translateX(-3px)}80%{transform:translateX(3px)}}
@@ -186,8 +212,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@@ -186,8 +212,7 @@ html[data-ov-season-mark=winter] .ovm-fall i{width:6px;height:6px;border-radius:
@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmWiggle{0%,100%{transform:none}25%{transform:rotate(-18deg)}60%{transform:rotate(12deg)}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
@keyframes ovmTwinkle{0%,100%{opacity:1}50%{opacity:.3}}
@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
@keyframes ovmFall{0%{opacity:0;transform:none}12%{opacity:1}72%{opacity:1;transform:translate(var(--x),var(--y)) rotate(var(--r))}100%{opacity:0;transform:translate(var(--x),var(--y)) rotate(var(--r))}}
-@media (prefers-reduced-motion:reduce){.ov-mark *{animation:none!important}.ov-mark,.ov-mark *,.ov-mark::before{transition:none!important}.ovm-fall{display:none}}
-html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
+html[data-ov-motion=off] .ov-mark:not([data-fx-demo] *) *{animation-iteration-count:1!important}html[data-ov-motion=off] .ovm-fall{display:none}
.ov-busy{cursor:progress;pointer-events:none}
.ov-busy{cursor:progress;pointer-events:none}
.ov-busy-mark{display:inline-block;vertical-align:-.2em;margin-right:.45em;flex:none}
.ov-busy-mark{display:inline-block;vertical-align:-.2em;margin-right:.45em;flex:none}
`;
`;
@@ -206,7 +231,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -206,7 +231,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
<linearGradient id="${id}gg"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset=".5" stop-color="#fff" stop-opacity=".8"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient>
<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs>
<mask id="${id}s" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><g mask="url(#${id}h)"><g class="og" style="${oc}"><path d="${G.ring}" fill="none" stroke="#fff" stroke-width="${G.sw}"/></g></g><g class="vg" style="${vc}"><path class="ma" d="${seg(p, 0)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/><path class="mb" d="${seg(p, 4)}" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="${G.vsw}"/></g></mask></defs>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
<g class="m"><circle class="rp" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/><circle class="rp rp2" cx="${G.cx}" cy="${G.cy}" r="${G.R}"/>
-<g mask="url(#${id}h)"><g class="og" style="${oc}"><path class="ot" d="${G.ring}" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>
+<g mask="url(#${id}h)"><g class="og" style="${oc};--ovm-dot-w:${f(G.sw * 0.62)}"><path class="ot" d="${G.ring}" pathLength="100" stroke-width="${G.sw}"/><path class="o" d="${G.ring}" pathLength="100" stroke-width="${G.sw}" style="stroke:url(#${id}go)"/><circle class="cd" cx="${G.cx}" cy="${G.cy}" r="${f(ri * 0.62)}"/>${[-1, 0, 1].map((k, n) => `<circle class="td${n ? ` td${n + 1}` : ''}" cx="${f(G.cx + k * ri * 0.52)}" cy="${G.cy}" r="${f(ri * 0.21)}"/>`).join('')}
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
<g class="sw2"><path class="sc" d="M${c0[0]} ${c0[1]}A${capR} ${capR} 0 0 1 ${c1[0]} ${c1[1]}"/></g></g></g>
<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/>
<g class="vg" style="${vc}"><path class="va" d="${seg(p, 0)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/><path class="vb" d="${seg(p, 4)}" stroke-width="${G.vsw}" style="stroke:url(#${id}gv)"/>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
<g class="sw2"><circle class="sd" cx="${G.A[0]}" cy="${f(G.A[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/><circle class="sd" cx="${G.B[0]}" cy="${f(G.B[1] - G.vsw * 0.18)}" r="${f(G.vsw * 0.36)}"/></g></g>
@@ -251,21 +276,29 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -251,21 +276,29 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
if (!end || end > now + INTRO_MS) { end = now + INTRO_MS; try { sessionStorage.setItem(INTRO_KEY, String(end)); } catch { /* */ } }
if (!end || end > now + INTRO_MS) { end = now + INTRO_MS; try { sessionStorage.setItem(INTRO_KEY, String(end)); } catch { /* */ } }
return end;
return end;
}
}
- function reduced() {
- return !!(rm && rm.matches) || document.documentElement.getAttribute('data-ov-motion') === 'reduced';
+ // Inside [data-fx-demo] (ov-fx.js: a motion page's own container, set when the person asks) a mark plays in full.
+ // 'full' | 'calm' | 'off': <html data-ov-motion> is the signal (theme-loader.js, the device's setting counted in);
+ // a page without the loader asks the device.
+ function motion(el) {
+ if (el && el.closest && el.closest('[data-fx-demo]')) return 'full';
+ const m = document.documentElement.getAttribute('data-ov-motion');
+ if (m === 'off') return 'off';
+ return m === 'reduced' || (!m && rm && rm.matches) ? 'calm' : 'full';
}
}
+ const off = (el) => motion(el) === 'off';
function update(el) {
function update(el) {
const st = state.get(el); if (!st) return;
const st = state.get(el); if (!st) return;
const svgEl = el.querySelector('svg');
const svgEl = el.querySelector('svg');
if (!el.isConnected || !svgEl) { forget(el); return; }
if (!el.isConnected || !svgEl) { forget(el); return; }
const now = Date.now();
const now = Date.now();
- const may = !reduced() && !el.hasAttribute('data-static') && !document.hidden && st.onScreen === true;
- if (may && !st.introEnd) { st.introEnd = introDeadline(now); if (season()) setTimeout(() => fallOnce(el), 900); }
+ const may = !el.hasAttribute('data-static') && !document.hidden && st.onScreen === true;
+ if (may && !off(el) && !st.introEnd) { st.introEnd = introDeadline(now); if (season()) setTimeout(() => fallOnce(el), 900); }
const s = el.getAttribute('data-state');
const s = el.getAttribute('data-state');
// Anything the mark is showing keeps it moving: a state, a page on its way, a known progress.
// Anything the mark is showing keeps it moving: a state, a page on its way, a known progress.
const showing = (s && s !== 'idle') || el.hasAttribute('data-nav') || el.hasAttribute('data-progress');
const showing = (s && s !== 'idle') || el.hasAttribute('data-nav') || el.hasAttribute('data-progress');
const intro = now < st.introEnd;
const intro = now < st.introEnd;
- const run = may && (intro || st.hover || st.focus || now < st.lingerEnd || showing);
+ // Calm plays the intro and answers a hover or focus, and rests as soon as that ends (no linger).
+ const run = may && (showing || (!off(el) && (intro || st.hover || st.focus || (motion(el) === 'full' && now < st.lingerEnd))));
if (run !== el.hasAttribute('data-ovm-run')) { if (run) el.setAttribute('data-ovm-run', ''); else el.removeAttribute('data-ovm-run'); }
if (run !== el.hasAttribute('data-ovm-run')) { if (run) el.setAttribute('data-ovm-run', ''); else el.removeAttribute('data-ovm-run'); }
// The intro's pop and growth are on only inside the intro (paused with the rest while the mark cannot move).
// The intro's pop and growth are on only inside the intro (paused with the rest while the mark cannot move).
if ((may && intro) !== el.hasAttribute('data-ovm-intro') && (may || !intro)) { if (may && intro) el.setAttribute('data-ovm-intro', ''); else el.removeAttribute('data-ovm-intro'); }
if ((may && intro) !== el.hasAttribute('data-ovm-intro') && (may || !intro)) { if (may && intro) el.setAttribute('data-ovm-intro', ''); else el.removeAttribute('data-ovm-intro'); }
@@ -292,7 +325,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -292,7 +325,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
clearTimeout(flourishTimer); flourishTimer = 0;
clearTimeout(flourishTimer); flourishTimer = 0;
if (!season()) return;
if (!season()) return;
flourishTimer = setTimeout(() => {
flourishTimer = setTimeout(() => {
- if (season() && !reduced() && !document.hidden) {
+ if (season() && !off() && !document.hidden) {
document.querySelectorAll('.ov-mark[data-ov]:not([data-static])').forEach((el) => {
document.querySelectorAll('.ov-mark[data-ov]:not([data-static])').forEach((el) => {
const st = state.get(el); if (!st || st.onScreen === false) return;
const st = state.get(el); if (!st || st.onScreen === false) return;
el.setAttribute('data-ovm-flourish', ''); setTimeout(() => el.removeAttribute('data-ovm-flourish'), 1700);
el.setAttribute('data-ovm-flourish', ''); setTimeout(() => el.removeAttribute('data-ovm-flourish'), 1700);
@@ -303,7 +336,8 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -303,7 +336,8 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
}
}
const FALL_KEY = 'ov-season-fall';
const FALL_KEY = 'ov-season-fall';
function fallOnce(el) {
function fallOnce(el) {
- const id = season(); if (!id || reduced() || document.hidden || !el.isConnected) return;
+ const id = season(); if (!id || off() || document.hidden || !el.isConnected) return;
+ const calm = motion() === 'calm';
const day = id + ':' + new Date().toDateString();
const day = id + ':' + new Date().toDateString();
try { const ls = window.localStorage; if (ls.getItem(FALL_KEY) === day) return; ls.setItem(FALL_KEY, day); } catch { return; }
try { const ls = window.localStorage; if (ls.getItem(FALL_KEY) === day) return; ls.setItem(FALL_KEY, day); } catch { return; }
const r = el.getBoundingClientRect(); if (!r.width || !document.body) return;
const r = el.getBoundingClientRect(); if (!r.width || !document.body) return;
@@ -316,11 +350,11 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -316,11 +350,11 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
const rb = bar && bar.getBoundingClientRect();
const rb = bar && bar.getBoundingClientRect();
const floor = rb && rb.bottom > y0 + 8 ? rb.bottom - y0 - 4 : 0;
const floor = rb && rb.bottom > y0 + 8 ? rb.bottom - y0 - 4 : 0;
const colors = id === 'winter' ? ['#f4fbff', '#d8f3ff', '#ffffff'] : ['#ff7a1a', '#f5a524', '#d9481f', '#7fd04e'];
const colors = id === 'winter' ? ['#f4fbff', '#d8f3ff', '#ffffff'] : ['#ff7a1a', '#f5a524', '#d9481f', '#7fd04e'];
- for (let i = 0; i < 7; i++) {
+ for (let i = 0; i < (calm ? 3 : 7); i++) {
const s = document.createElement('i');
const s = document.createElement('i');
- const dx = Math.max(8 - x0, Math.min((window.innerWidth || 0) - 8 - x0, Math.round((Math.random() - 0.3) * 200)));
+ const dx = Math.max(8 - x0, Math.min((window.innerWidth || 0) - 8 - x0, Math.round((Math.random() - 0.3) * (calm ? 70 : 200))));
const dy = floor ? Math.round(floor - Math.random() * 3) : Math.round(36 + Math.random() * 28);
const dy = floor ? Math.round(floor - Math.random() * 3) : Math.round(36 + Math.random() * 28);
- s.style.cssText = `--x:${dx}px;--y:${dy}px;--r:${Math.round(Math.random() * 540 - 270)}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;
+ s.style.cssText = `--x:${dx}px;--y:${dy}px;--r:${Math.round((Math.random() * 540 - 270) * (calm ? 0.35 : 1))}deg;animation-delay:${(i * 0.18 + Math.random() * 0.2).toFixed(2)}s;animation-duration:${(2.4 + Math.random() * 1.2).toFixed(2)}s;background:${colors[i % colors.length]}`;
box.appendChild(s);
box.appendChild(s);
}
}
document.body.appendChild(box);
document.body.appendChild(box);
@@ -380,7 +414,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -380,7 +414,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
new MutationObserver((records) => {
new MutationObserver((records) => {
let added = false;
let added = false;
for (const r of records) {
for (const r of records) {
- if (r.type === 'attributes') { if (r.target === document.documentElement) { updateAll(); if (r.attributeName === 'data-ov-season-mark') scheduleFlourish(); } else if (state.has(r.target)) update(r.target); continue; }
+ if (r.type === 'attributes') { if (r.attributeName === 'data-fx-demo') { updateAll(); continue; } if (r.target === document.documentElement) { updateAll(); if (r.attributeName === 'data-ov-season-mark') scheduleFlourish(); } else if (state.has(r.target)) update(r.target); continue; }
if (r.addedNodes.length) added = true;
if (r.addedNodes.length) added = true;
r.removedNodes.forEach((node) => {
r.removedNodes.forEach((node) => {
if (node.nodeType !== 1) return;
if (node.nodeType !== 1) return;
@@ -389,7 +423,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -389,7 +423,7 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
});
});
}
}
if (added) mount();
if (added) mount();
- }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-state', 'data-static', 'data-nav', 'data-progress', 'data-ov-motion', 'data-ov-season-mark'] });
+ }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-state', 'data-static', 'data-nav', 'data-progress', 'data-ov-motion', 'data-ov-season-mark', 'data-fx-demo'] });
} catch { /* */ }
} catch { /* */ }
}
}
// ── A page on its way: the navbar's mark draws its ring round as the next page loads ──
// ── A page on its way: the navbar's mark draws its ring round as the next page loads ──
@@ -457,9 +491,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
@@ -457,9 +491,12 @@ html[data-ov-motion=reduced] .ov-mark *{animation:none!important}
window.addEventListener('pageshow', (e) => { if (e.persisted) { navStop(); clearNav(); } });
window.addEventListener('pageshow', (e) => { if (e.persisted) { navStop(); clearNav(); } });
// This page came from a click on the last one: learn how long it took, and finish the ring here.
// This page came from a click on the last one: learn how long it took, and finish the ring here.
let t = 0; try { t = Number(sessionStorage.getItem(NAV_FLAG)) || 0; sessionStorage.removeItem(NAV_FLAG); } catch { /* */ }
let t = 0; try { t = Number(sessionStorage.getItem(NAV_FLAG)) || 0; sessionStorage.removeItem(NAV_FLAG); } catch { /* */ }
+ // soft.js ends the ring itself at the arriving page's first paint (it starts at the press, not the click); the
+ // pace is still learned here, from the click's marker, before soft.js's ending takes over.
if (t && Date.now() - t < 20000) {
if (t && Date.now() - t < 20000) {
learn('page', Date.now() - t);
learn('page', Date.now() - t);
- const finish = () => { const marks = navMarks(); if (!marks.length) return false; setFill(marks, 0.9); setTimeout(() => navEnd(null), 60); return true; };
+ if (window.__ovSoftNavEnded) return;
+ const finish = () => { if (window.__ovSoftNavEnded) return true; const marks = navMarks(); if (!marks.length) return false; setFill(marks, 0.9); setTimeout(() => navEnd(null), 60); return true; };
let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();
let tries = 0; const again = () => { if (!finish() && ++tries <= 20) setTimeout(again, 100); }; again();
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,319 @@
@@ -0,0 +1,319 @@
+/**
+ * ov-window.js (openvibe-shared 4.202.0): one window for every little interface. Asks 304 and 318, the owner: "a system
+ * that's used for all the windows [in] harmony … people can make their own or … custom … everything … feels connected".
+ * One frame (a head with the title and ×, a body, a foot) in three kinds, the same motion, the same keys:
+ * dialog: centred on a computer; a sheet from the bottom on a phone
+ * sheet: a sheet from the bottom on every screen
+ * panel: floats beside what opened it on a computer, draggable by its head (where you leave it is kept per id);
+ * a sheet on a phone
+ * It grows out of the button that opened it (the navbar's pop motion), closes with ×, Escape, a press outside (a dialog
+ * or a sheet; a panel stays until closed, and the page under it stays usable) or a swipe down on a phone, keeps the focus inside while open, and gives it back to that button. Windows stack: Escape closes the
+ * top one. The look follows the person's theme and their window style, <html data-ov-window="glass|solid|outline">
+ * (OVWindow.setStyle), on every window at once.
+ *
+ * const w = OVWindow.open({ id, title, body: node | text, foot: node, kind: 'dialog', size: 'md', from: button,
+ * label, onClose, bare, width }); bare: the content draws its own head (the title is still
+ * read); width: px, for content that wants its own
+ * w.el, w.body, w.foot, w.setTitle(text), w.close()
+ * A panel can be resizable (4.204.0, the owner: a robot's controls "can be popped out … and resized"): { resizable: true,
+ * minWidth, minHeight, onResize({ width, height }) } adds a grip at its corner that a mouse, a finger or a pen drags and
+ * the arrow keys move (Shift for bigger steps, Enter for the first size); the size is kept with its place per id.
+ * A site's content goes in as nodes (or text, set as text): the window never parses markup it is given.
+ */
+(function (root) {
+ 'use strict';
+ if (typeof document === 'undefined' || root.OVWindow) return;
+ const HERE = document.currentScript && document.currentScript.src;
+ const WINDOW_REV = { 'ov-float.js': '5c8251ac3738' };
+
+ const STYLES = ['glass', 'solid', 'outline'];
+ const CSS = `.ovw-scrim { position: fixed; inset: 0; z-index: var(--ovw-z, 10040); background: rgba(3, 7, 15, .5); animation: ovw-fade .2s ease-out both; }
+.ovw-scrim.is-clear { background: transparent; }
+.ovw { position: fixed; z-index: var(--ovw-z, 10040); box-sizing: border-box; display: flex; flex-direction: column; max-height: min(86vh, 760px); color: var(--text-primary, #e6edf7);
+ font: 500 15px/1.45 var(--font, Inter, system-ui, -apple-system, 'Segoe UI', sans-serif); border-radius: 20px; overflow: hidden;
+ background: var(--ovw-bg); border: 1px solid var(--ovw-line); box-shadow: var(--ovw-shadow); -webkit-backdrop-filter: var(--ovw-blur); backdrop-filter: var(--ovw-blur);
+ transform-origin: var(--ov-pop-x, 50%) var(--ov-pop-y, 0); animation: ovw-in .28s cubic-bezier(.2, 1.2, .3, 1) both; }
+:root { --ovw-bg: color-mix(in srgb, var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))) 92%, transparent); --ovw-line: var(--border, rgba(255, 255, 255, .12));
+ --ovw-shadow: 0 28px 70px rgba(0, 0, 0, .45), 0 2px 8px rgba(0, 0, 0, .2); --ovw-blur: blur(16px) saturate(1.3); }
+html[data-ov-window="solid"] { --ovw-bg: var(--bg-elevated, var(--bg-secondary, var(--bg-primary, #111826))); --ovw-blur: none; }
+html[data-ov-window="outline"] { --ovw-bg: var(--bg-primary, #0b1220); --ovw-blur: none; --ovw-line: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); --ovw-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 20%, transparent), 0 18px 40px rgba(0, 0, 0, .35); }
+.ovw.is-out { animation: ovw-out .16s ease-in both; }
+.ovw-h { flex: none; display: flex; align-items: center; gap: 10px; min-height: 56px; padding: 6px 8px 6px 18px; border-bottom: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }
+.ovw-t { flex: 1; min-width: 0; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16.5px; font-weight: 700; letter-spacing: -.01em; }
+.ovw-x { flex: none; display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border: 0; border-radius: 12px; background: none; color: var(--text-secondary, #a8b3c4); cursor: pointer; }
+.ovw-x:hover, .ovw-x:focus-visible { background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); color: var(--text-primary, #e6edf7); }
+.ovw-x:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
+.ovw-b { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 16px 18px 18px; }
+.ovw-f { flex: none; display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding: 12px 18px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid color-mix(in srgb, var(--ovw-line) 70%, transparent); }
+.ovw-f:empty { display: none; }
+.ovw.is-dialog { left: 50%; top: 50%; width: min(var(--ovw-w), calc(100vw - 32px)); transform: translate(-50%, -50%); animation-name: ovw-in-c; }
+.ovw.is-dialog.is-out { animation-name: ovw-out-c; }
+.ovw.is-panel { width: min(var(--ovw-w), calc(100vw - 24px)); }
+.ovw.is-panel .ovw-h { cursor: grab; touch-action: none; }
+.ovw.is-panel.is-dragging { animation: none; transition: none; box-shadow: 0 34px 90px rgba(0, 0, 0, .55); }
+.ovw.is-panel.is-dragging .ovw-h { cursor: grabbing; }
+.ovw.is-sheet { left: 0; right: 0; bottom: 0; width: auto; max-height: 92vh; margin: 0 auto; max-width: 720px; border-radius: 22px 22px 0 0; border-bottom: 0; animation: ovw-up .32s cubic-bezier(.2, 1, .3, 1) both; }
+.ovw.is-sheet.is-out { animation: ovw-down .2s ease-in both; }
+.ovw.is-sheet .ovw-h { position: relative; padding-top: 16px; touch-action: none; }
+.ovw.is-sheet .ovw-h::before { content: ""; position: absolute; left: 50%; top: 6px; width: 38px; height: 4px; margin-left: -19px; border-radius: 4px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 28%, transparent); }
+.ovw.is-sheet.is-dragging { animation: none; }
+.ovw.is-resizable { max-height: none; }
+.ovw-grip { position: absolute; right: 2px; bottom: 2px; z-index: 2; width: 26px; height: 26px; padding: 0; border: 0; border-radius: 8px; background: none; cursor: nwse-resize; touch-action: none; color: var(--text-secondary, #a8b3c4); }
+.ovw-grip::before { content: ""; position: absolute; right: 6px; bottom: 6px; width: 10px; height: 10px; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; border-radius: 0 0 3px 0; opacity: .55; }
+.ovw-grip:hover::before, .ovw-grip:focus-visible::before, .ovw.is-resizing .ovw-grip::before { opacity: 1; color: var(--accent, #3b82f6); }
+.ovw-grip:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }
+.ovw.is-resizing { animation: none; transition: none; user-select: none; }
+.ovw { --ovw-w: 520px; }
+.ovw.is-sm { --ovw-w: 380px; }
+.ovw.is-lg { --ovw-w: 760px; }
+.ovw.is-full { --ovw-w: 1100px; }
+@keyframes ovw-in { from { opacity: 0; transform: translateY(calc(-6px * var(--ov-move, 1))) scale(.94); } }
+@keyframes ovw-out { to { opacity: 0; transform: scale(.96); } }
+@keyframes ovw-in-c { from { opacity: 0; transform: translate(-50%, calc(-50% + 10px * var(--ov-move, 1))) scale(.95); } }
+@keyframes ovw-out-c { to { opacity: 0; transform: translate(-50%, -50%) scale(.97); } }
+@keyframes ovw-up { from { transform: translateY(100%); } }
+@keyframes ovw-down { to { transform: translateY(100%); } }
+@keyframes ovw-fade { from { opacity: 0; } }
+html[data-ov-motion=off] .ovw, html[data-ov-motion=off] .ovw-scrim { animation: none !important; }
+html.ovw-locked { overflow: hidden; }
+/* bare: the content carries its own head (a card's banner); the title is for screen readers, × floats on top, and
+ the top strip is still the handle (drag a panel, swipe a sheet). */
+.ovw.is-bare .ovw-h { position: absolute; left: 0; right: 0; top: 0; z-index: 3; min-height: 52px; padding: 6px; justify-content: flex-end; border: 0; background: none; }
+.ovw.is-bare .ovw-t { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
+.ovw.is-bare .ovw-x { width: 36px; height: 36px; border-radius: 50%; color: #fff; background: rgba(4, 8, 16, .42); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); }
+.ovw.is-bare .ovw-x:hover, .ovw.is-bare .ovw-x:focus-visible { background: rgba(4, 8, 16, .62); color: #fff; }
+.ovw.is-bare .ovw-b { padding: 0; }
+.ovw.is-bare.is-sheet .ovw-h { padding-top: 14px; }`;
+
+ const stack = [];
+ let uid = 0;
+ function styles() { if (document.getElementById('ovw-styles')) return; const st = document.createElement('style'); st.id = 'ovw-styles'; st.textContent = CSS; document.head.appendChild(st); }
+
+ // The panel's move, tuck and place are OVFloat's (ov-float.js, loaded from beside this file the first time a panel
+ // opens). A page that already loaded it uses that.
+ let floatLoading = null;
+ const floatWait = [];
+ function withFloat(cb) {
+ if (root.OVFloat) { cb(root.OVFloat); return; }
+ if (!HERE || floatLoading === false) return;
+ if (floatLoading) { floatWait.push(cb); return; }
+ floatWait.push(cb);
+ const s = document.createElement('script');
+ const rev = WINDOW_REV['ov-float.js'];
+ s.src = HERE.replace(/ov-window\.js(?:\?.*)?$/, 'ov-float.js') + (rev ? `?v=${rev}` : '');
+ s.onload = () => {
+ floatLoading = null;
+ for (const fn of floatWait.splice(0)) { try { fn(root.OVFloat); } catch (e) { console.error(e); } }
+ };
+ s.onerror = () => { floatLoading = false; floatWait.length = 0; };
+ floatLoading = s;
+ (document.head || document.documentElement).appendChild(s);
+ }
+ const phone = () => !!(root.matchMedia && root.matchMedia('(max-width: 640px)').matches);
+ const focusables = (el) => [...el.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')].filter((x) => x.offsetParent !== null || x === document.activeElement);
+ const place = { get(id) { try { return JSON.parse(root.localStorage.getItem(`ov:window:${id}`) || 'null'); } catch { return null; } }, set(id, v) { try { root.localStorage.setItem(`ov:window:${id}`, JSON.stringify(v)); } catch { /* private mode */ } } };
+ function put(parent, v) { if (v == null) return; if (typeof v === 'string') parent.textContent = v; else parent.appendChild(v); }
+
+ function open(o = {}) {
+ styles();
+ const kind0 = ['dialog', 'sheet', 'panel'].includes(o.kind) ? o.kind : 'dialog';
+ const kind = phone() ? 'sheet' : kind0;
+ const id = o.id || `ovw-${++uid}`;
+ const opener = o.from || document.activeElement;
+ const modal = o.modal !== false && kind !== 'panel';
+ const z = 10040 + stack.length * 2;
+ const scrim = document.createElement('div');
+ scrim.className = `ovw-scrim${modal ? '' : ' is-clear'}`;
+ scrim.style.setProperty('--ovw-z', String(z));
+ const el = document.createElement('section');
+ el.className = `ovw is-${kind} is-${['sm', 'md', 'lg', 'full'].includes(o.size) ? o.size : 'md'}${o.bare ? ' is-bare' : ''}`;
+ el.style.setProperty('--ovw-z', String(z + 1));
+ if (Number(o.width) > 0) el.style.setProperty('--ovw-w', `${Math.round(Number(o.width))}px`);
+ el.setAttribute('role', 'dialog');
+ if (modal) el.setAttribute('aria-modal', 'true');
+ el.dataset.ovWindow = id;
+ const head = document.createElement('header'); head.className = 'ovw-h';
+ const title = document.createElement('h2'); title.className = 'ovw-t'; title.id = `${id}-t`; title.textContent = o.title || '';
+ const x = document.createElement('button'); x.type = 'button'; x.className = 'ovw-x'; x.setAttribute('aria-label', 'Close');
+ x.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>';
+ head.append(title, x);
+ const body = document.createElement('div'); body.className = 'ovw-b'; put(body, o.body);
+ const foot = document.createElement('footer'); foot.className = 'ovw-f'; put(foot, o.foot);
+ el.append(head, body, foot);
+ if (o.title) el.setAttribute('aria-labelledby', title.id); else el.setAttribute('aria-label', o.label || 'Window');
+ if (modal) document.body.append(scrim, el); else document.body.append(el);
+ if (modal) document.documentElement.classList.add('ovw-locked');
+
+ // Where it opens: a panel beside its opener (or where it was left), a dialog centred, a sheet at the bottom.
+ let panelLeft = 0, panelTop = 0;
+ if (kind === 'panel') {
+ const saved = o.id ? place.get(o.id) : null;
+ const cs = document.documentElement;
+ const r = el.getBoundingClientRect(), vw = cs.clientWidth || root.innerWidth, vh = cs.clientHeight || root.innerHeight;
+ let left, top;
+ if (saved) { left = saved.x; top = saved.y; }
+ else if (opener && opener.getBoundingClientRect) { const f = opener.getBoundingClientRect(); left = f.left; top = f.bottom + 8; if (top + r.height > vh - 12) top = Math.max(12, f.top - r.height - 8); }
+ else { left = vw - r.width - 24; top = 72; }
+ left = Math.max(8, Math.min(left, vw - r.width - 8));
+ top = Math.max(8, Math.min(top, vh - 60));
+ panelLeft = left; panelTop = top;
+ el.style.left = `${left}px`;
+ el.style.top = `${top}px`;
+ }
+ if (opener && opener.getBoundingClientRect && kind !== 'sheet') {
+ const r = el.getBoundingClientRect(), f = opener.getBoundingClientRect();
+ el.style.setProperty('--ov-pop-x', `${Math.round(f.left + f.width / 2 - r.left)}px`);
+ el.style.setProperty('--ov-pop-y', `${Math.round(f.top + f.height / 2 - r.top)}px`);
+ }
+
+ let closed = false;
+ const w = {
+ id, el, body, foot,
+ setTitle(t) { title.textContent = t || ''; },
+ close(why) {
+ if (closed) return; closed = true;
+ if (w.float) { try { w.float.destroy(); } catch { /* */ } w.float = null; }
+ const i = stack.indexOf(w); if (i >= 0) stack.splice(i, 1);
+ if (!stack.some((s) => s.modal)) document.documentElement.classList.remove('ovw-locked');
+ const gone = () => { el.remove(); scrim.remove(); };
+ if (document.documentElement.getAttribute('data-ov-motion') === 'off') gone();
+ else { el.classList.add('is-out'); scrim.style.opacity = '0'; scrim.style.transition = 'opacity .16s'; let done = false; const end = () => { if (!done) { done = true; gone(); } }; el.addEventListener('animationend', end, { once: true }); setTimeout(end, 260); }
+ if (opener && opener.focus && document.contains(opener)) { try { opener.focus({ preventScroll: true }); } catch { /* */ } }
+ if (typeof o.onClose === 'function') { try { o.onClose(why || 'close'); } catch (e) { console.error(e); } }
+ },
+ modal,
+ float: null,
+ };
+ stack.push(w);
+ if (kind === 'panel') {
+ const panelId = o.id ? String(o.id) : '';
+ withFloat((OVFloat) => {
+ if (closed) return;
+ w.float = OVFloat.attach(el, {
+ frame: null, key: panelId ? `ov:window:float:${panelId}` : undefined,
+ defaults: {
+ x: Math.max(0, Math.min(1, panelLeft / Math.max(1, (document.documentElement.clientWidth || root.innerWidth)))),
+ y: Math.max(0, Math.min(1, panelTop / Math.max(1, (document.documentElement.clientHeight || root.innerHeight)))),
+ mode: 'float',
+ },
+ aspect: null, size: false, resize: false, doubleTap: false, activeBar: false,
+ viewport: 'client', handle: head, store: null, hintClass: 'ovw-dock-hint',
+ onChange(geom) {
+ if (!panelId) return;
+ // The fractions convert back to the same pixels without reading a rect mid-landing. (The old
+ // key stays as the absolute place older releases wrote, and for the size the grip keeps there.)
+ const vw = document.documentElement.clientWidth || root.innerWidth;
+ const vh = document.documentElement.clientHeight || root.innerHeight;
+ place.set(panelId, Object.assign(place.get(panelId) || {}, { x: Math.round(geom.x * vw), y: Math.round(geom.y * vh) }));
+ },
+ });
+ });
+ }
+ x.addEventListener('click', () => w.close('x'));
+ if (modal) scrim.addEventListener('pointerdown', () => w.close('outside'));
+ el.addEventListener('keydown', (e) => {
+ if (e.key === 'Tab') {
+ const f = focusables(el); if (!f.length) return;
+ const first = f[0], last = f[f.length - 1];
+ if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
+ else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
+ }
+ });
+
+ // A sheet goes down when swiped down on a phone; a panel's head is OVFloat's handle (with its tuck and place).
+ if (kind === 'sheet') {
+ head.addEventListener('pointerdown', (e) => {
+ if (e.target.closest('.ovw-x') || (e.button !== undefined && e.button !== 0)) return;
+ const r = el.getBoundingClientRect(), sx = e.clientX, sy = e.clientY;
+ let moved = false;
+ el.classList.add('is-dragging');
+ try { head.setPointerCapture(e.pointerId); } catch { /* */ }
+ const move = (ev) => {
+ const dx = ev.clientX - sx, dy = ev.clientY - sy;
+ if (!moved && Math.hypot(dx, dy) < 4) return;
+ moved = true;
+ if (kind === 'panel') {
+ el.style.left = `${Math.max(8, Math.min(r.left + dx, root.innerWidth - r.width - 8))}px`;
+ el.style.top = `${Math.max(8, Math.min(r.top + dy, root.innerHeight - 56))}px`;
+ } else el.style.transform = `translateY(${Math.max(0, dy)}px)`;
+ };
+ const up = (ev) => {
+ head.removeEventListener('pointermove', move); head.removeEventListener('pointerup', up); head.removeEventListener('pointercancel', up);
+ el.classList.remove('is-dragging');
+ if (kind === 'panel') { if (moved && o.id) { const b = el.getBoundingClientRect(); place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(b.left), y: Math.round(b.top) })); } return; }
+ const dy = ev.clientY - sy;
+ if (moved && dy > Math.min(140, r.height * 0.28)) w.close('swipe');
+ else { el.style.transition = 'transform .2s cubic-bezier(.2, 1, .3, 1)'; el.style.transform = ''; setTimeout(() => { el.style.transition = ''; }, 220); }
+ };
+ head.addEventListener('pointermove', move); head.addEventListener('pointerup', up); head.addEventListener('pointercancel', up);
+ });
+ }
+
+ if (kind === 'panel' && o.resizable) resizable(el, o);
+
+ const first = o.focus ? body.querySelector(o.focus) : focusables(body)[0];
+ (first || x).focus({ preventScroll: true });
+ return w;
+ }
+
+ /** A panel's corner grip: pointer drag (mouse, finger, pen) and the arrow keys; the size is kept with its place. */
+ function resizable(el, o) {
+ el.classList.add('is-resizable');
+ const minW = Math.max(200, Number(o.minWidth) || 260), minH = Math.max(120, Number(o.minHeight) || 160);
+ const grip = document.createElement('button');
+ grip.type = 'button'; grip.className = 'ovw-grip';
+ grip.setAttribute('aria-label', 'Resize: drag, or use the arrow keys');
+ el.append(grip);
+ const first = { w: el.offsetWidth, h: el.offsetHeight };
+ const set = (w, h, save) => {
+ const r = el.getBoundingClientRect();
+ const W = Math.round(Math.max(minW, Math.min(w, root.innerWidth - r.left - 8)));
+ const H = Math.round(Math.max(minH, Math.min(h, root.innerHeight - r.top - 8)));
+ el.style.width = `${W}px`; el.style.height = `${H}px`;
+ if (save && o.id) place.set(o.id, Object.assign(place.get(o.id) || {}, { x: Math.round(r.left), y: Math.round(r.top), w: W, h: H }));
+ if (typeof o.onResize === 'function') { try { o.onResize({ width: W, height: H }); } catch { /* the page's own */ } }
+ };
+ const saved = o.id ? place.get(o.id) : null;
+ if (saved && saved.w && saved.h) set(saved.w, saved.h, false);
+ grip.addEventListener('pointerdown', (e) => {
+ if (e.button !== undefined && e.button !== 0) return;
+ e.preventDefault();
+ const sx = e.clientX, sy = e.clientY, w0 = el.offsetWidth, h0 = el.offsetHeight;
+ el.classList.add('is-resizing');
+ try { grip.setPointerCapture(e.pointerId); } catch { /* */ }
+ const move = (ev) => set(w0 + ev.clientX - sx, h0 + ev.clientY - sy, false);
+ const up = () => {
+ grip.removeEventListener('pointermove', move); grip.removeEventListener('pointerup', up); grip.removeEventListener('pointercancel', up);
+ el.classList.remove('is-resizing');
+ set(el.offsetWidth, el.offsetHeight, true);
+ };
+ grip.addEventListener('pointermove', move); grip.addEventListener('pointerup', up); grip.addEventListener('pointercancel', up);
+ });
+ grip.addEventListener('keydown', (e) => {
+ const step = e.shiftKey ? 80 : 24;
+ const d = { ArrowRight: [step, 0], ArrowLeft: [-step, 0], ArrowDown: [0, step], ArrowUp: [0, -step] }[e.key];
+ if (e.key === 'Enter' || e.key === 'Home') { e.preventDefault(); set(first.w, first.h, true); return; }
+ if (!d) return;
+ e.preventDefault();
+ set(el.offsetWidth + d[0], el.offsetHeight + d[1], true);
+ });
+ }
+
+ document.addEventListener('keydown', (e) => {
+ if (e.key !== 'Escape' || !stack.length || e.defaultPrevented) return;
+ e.preventDefault();
+ stack[stack.length - 1].close('escape');
+ });
+
+ // The style is the person's display setting (theme-loader: saved, synced to their account, painted at boot as
+ // <html data-ov-window>); setStyle saves through it, or straight onto the page where the theme loader is absent.
+ function setStyle(name) {
+ const v = STYLES.includes(name) ? name : 'glass';
+ const T = root.OpenVibeThemeLoader;
+ if (T && T.display && typeof T.display.set === 'function') { try { T.display.set({ window: v }); } catch { /* */ } }
+ if (v === 'glass') document.documentElement.removeAttribute('data-ov-window'); else document.documentElement.setAttribute('data-ov-window', v);
+ return v;
+ }
+
+ root.OVWindow = { open, setStyle, styles: STYLES.slice(), top: () => stack[stack.length - 1] || null, closeAll: () => [...stack].reverse().forEach((w) => w.close('all')) };
+})(typeof window !== 'undefined' ? window : globalThis);
new file mode 100644
new file mode 100644
@@ -0,0 +1,738 @@
@@ -0,0 +1,738 @@
+// OpenVibe — Theme Loader (browser): the person's look on <html> before first paint. Load it before navbar.js.
+
+(function (root) {
+ 'use strict';
+
+ const COOKIE_NAME = 'ov_theme_id';
+ const STORAGE_KEY = 'ov_theme';
+ // The account's look lives on openvibe.network for every site (init({ apiBase }) overrides it).
+ const DEFAULT_API_BASE = 'https://openvibe.network';
+ let _config = { apiBase: DEFAULT_API_BASE };
+
+ // ── Condensed built-in theme variable maps ───────────────
+ // Kept in sync with packages/openvibe-shared/builtin-themes.js
+ // THEMES:BEGIN — generated by scripts/build-theme-loader.js from builtin-themes.js; do not edit by hand
+ const THEME_KEYS = ['--bg-primary','--bg-secondary','--bg-tertiary','--bg-card','--bg-hover','--bg-input','--text-primary','--text-secondary','--text-muted','--accent','--accent-light','--accent-dark','--border','--border-light','--live-red','--success','--warning','--danger','--info','--shadow','--shadow-lg','--success-text','--warning-text','--danger-text','--info-text','--live-red-text','--accent-strong','--on-accent-strong','--on-accent','--accent-rgb','--accent-glow','--color-scheme'];
+ const THEME_ROWS = {
+ 'vibe': ['#0a0f1c','#101828','#172238','#131c2e','#1c2a44','#0d1424','#e6edf7','#96a7c2','#8192ac','#3b82f6','#60a5fa','#1d4ed8','#1f2d47','#2c3d5c','#ef4444','#22c55e','#f59e0b','#ef4444','#38bdf8','0 2px 12px rgba(0,0,0,0.45)','0 8px 32px rgba(0,0,0,0.6)','#22c55e','#f59e0b','#f26262','#38bdf8','#f26262','#3472d8','#ffffff','#ffffff','59,130,246','rgba(59,130,246,0.35)','dark'],
+ 'violet-hour': ['#0d0d0f','#16161a','#1e1e24','#1a1a20','#242430','#12121a','#e8e6e3','#9a9a9a','#8d8c8c','#8b5cf6','#a78bfa','#6d28d9','#2a2a32','#3a3a44','#e74c3c','#2ecc71','#f39c12','#e74c3c','#3498db','0 2px 12px rgba(0,0,0,0.4)','','#2ecc71','#f39c12','#e95e50','#3498db','#e95e50','#8558ec','','','139,92,246','rgba(139,92,246,0.35)'],
+ 'midnight': ['#0a0e17','#111827','#1a2235','#151d2e','#1e2a42','#0d1220','#e2e8f0','#94a3b8','#8693a5','#6366f1','#818cf8','#4f46e5','#1e293b','#334155','','','','','','','','','','','','','#6164ec','','','99,102,241','rgba(99,102,241,0.35)'],
+ 'forest': ['#0a120a','#121e12','#1a2a1a','#152015','#1e3420','#0d160d','#d4e8d4','#8aaa8a','#849e84','#4ade80','#86efac','#22c55e','#1a3a1a','#2a4a2a','','','','','','','','','','#f36d6d','','#f36d6d','#4ade80','#0b0d10','#0b0d10','74,222,128','rgba(74,222,128,0.35)'],
+ 'neon-tokyo': ['#0a0a0f','#12101a','#1a1625','#15111f','#221a32','#0d0b14','#f0e6ff','#b4a0d0','#8e81a2','#ff006e','#ff4d94','#c9005a','#2a1a3a','#3a2a4a','','','','','','','','','','#ef4848','','#ef4848','#e60063','','','255,0,110','rgba(255,0,110,0.35)'],
+ 'dracula': ['#282a36','#1e1f29','#343746','#2c2e3c','#3a3d50','#21222c','#f8f8f2','#bfbfb2','#9ea8c5','#bd93f9','#d4b8ff','#9b6ddb','#44475a','#565970','#ff5555','#50fa7b','#f1fa8c','#ff5555','#8be9fd','','','#50fa7b','#f1fa8c','#ff8585','#8be9fd','#ff8585','#bd93f9','#0b0d10','#0b0d10','189,147,249','rgba(189,147,249,0.35)'],
+ 'monokai': ['#272822','#1e1f1c','#3e3d32','#2d2e27','#49483e','#22231e','#f8f8f2','#cfcfc2','#b8b6ab','#a6e22e','#c4ff50','#7dac1c','#49483e','#5b5a50','','','#e6db74','#f92672','#66d9ef','','','#45ce78','#e6db74','#fc97bb','#66d9ef','#f79e9e','#a6e22e','#0b0d10','#0b0d10','166,226,46','rgba(166,226,46,0.35)'],
+ 'ocean-deep': ['#0a1215','#0f1a1f','#162228','#121c22','#1a2a32','#0c1418','#d0e8ef','#7aa8ba','#7494a1','#00bcd4','#4dd0e1','#0097a7','#1a3040','#2a4050','','','','','','','','','','#f15e5e','','#f15e5e','#00bcd4','#0b0d10','#0b0d10','0,188,212','rgba(0,188,212,0.35)'],
+ 'sunset': ['#120a0a','#1a1010','#241616','#1e1212','#2e1a1a','#140c0c','#ffe8e0','#c49080','#9d8179','#ff6b35','#ff8f5e','#d04a1a','#3a2020','#4a3030','','','','','','','','','','#f04b4b','','#f04b4b','#ff6b35','#0b0d10','#0b0d10','255,107,53','rgba(255,107,53,0.35)'],
+ 'arctic': ['#0b1620','#0f1e2b','#163040','#122534','#1a3a4c','#0c1a26','#eaf6ff','#9ec3d8','#81a5bb','#22d3ee','#a5f3fc','#0891b2','#1e3f52','#2a5268','','#34d399','','','#7dd3fc','0 2px 12px rgba(2,12,20,0.5)','0 8px 32px rgba(2,12,20,0.65)','#34d399','','#f47c7c','#7dd3fc','#f47c7c','#22d3ee','#0b0d10','#0b0d10','34,211,238','rgba(34,211,238,0.35)'],
+ 'ember': ['#120808','#1a0e0e','#241414','#1e1010','#2e1818','#140a0a','#f0dada','#b0808a','#9b8084','#ef4444','#f87171','#b91c1c','#3a1a20','#4a2a30','','','','','','','','','','#ef4848','','#ef4848','#d73d3d','','','239,68,68','rgba(239,68,68,0.35)'],
+ 'vapor': ['#0e0a14','#14101c','#1c1628','#181220','#241a34','#100c16','#f0e0ff','#c090e0','#967bbb','#e040fb','#ea80fc','#aa00d4','#2a1a40','#3a2a50','','#69f0ae','','','#00e5ff','','','#69f0ae','','#f04b4b','#00e5ff','#f04b4b','#bc36d3','','','224,64,251','rgba(224,64,251,0.35)'],
+ 'slate': ['#0f1118','#161820','#1e2028','#1a1c24','#24262e','#12141c','#e4e6ea','#a0a4b0','#8a8d97','#a78bfa','#c4b5fd','#7c3aed','#2a2c38','#3a3c48','','','','','','','','','','#f15a5a','','#f15a5a','#a78bfa','#0b0d10','#0b0d10','167,139,250','rgba(167,139,250,0.35)'],
+ 'matrix': ['#000000','#050a05','#0a140a','#071007','#0e1e0e','#030803','#00ff41','#00b030','#149836','#00ff41','#66ff8a','#00aa2a','#0a2a0a','#144014','','','','','','','','','','#ef4444','','#ef4444','#00ff41','#0b0d10','#0b0d10','0,255,65','rgba(0,255,65,0.35)'],
+ 'nord': ['#2e3440','#272c36','#3b4252','#333a48','#434c5e','#2a303c','#eceff4','#d8dee9','#b5bcca','#88c0d0','#8fbcbb','#5e81ac','#3b4252','#4c566a','','#a3be8c','#ebcb8b','#bf616a','#81a1c1','','','#acc598','#ebcb8b','#dfb0b5','#a9bfd5','#f8a9a9','#88c0d0','#0b0d10','#0b0d10','136,192,208','rgba(136,192,208,0.35)'],
+ 'gruvbox-dark': ['#1d2021','#282828','#3c3836','#32302f','#504945','#242424','#ebdbb2','#d5c4a1','#c2b8aa','#fabd2f','#fce566','#d79921','#3c3836','#504945','','#b8bb26','#fe8019','#fb4934','#83a598','','','#bec037','#fea65e','#fda49a','#a8c0b7','#f7a5a5','#fabd2f','#0b0d10','#0b0d10','250,189,47','rgba(250,189,47,0.35)'],
+ 'abyss': ['#000000','#080808','#111111','#0a0a0a','#181818','#050505','#cccccc','#888888','#828282','#ffffff','#ffffff','#aaaaaa','#1a1a1a','#252525','','','','','','','','','','#ef4444','','#ef4444','#ffffff','#0b0d10','#0b0d10','255,255,255','rgba(255,255,255,0.35)'],
+ 'copper': ['#110d09','#1a140e','#231c14','#1e1710','#2c2218','#13100b','#e8ddd0','#b49a80','#978976','#cd7f32','#daa060','#a06020','#2a2016','#3a3020','','','','','','','','','','#f15757','','#f15757','#a46628','','','205,127,50','rgba(205,127,50,0.35)'],
+ 'sakura-night': ['#0e0a14','#150f1c','#1c1526','#181020','#221830','#100c16','#f0e0f0','#c0a0c0','#997d99','#f472b6','#f9a8d4','#db2777','#2a1a30','#3a2a40','','','','','','','','','','#ef4848','','#ef4848','#f472b6','#0b0d10','#0b0d10','244,114,182','rgba(244,114,182,0.35)'],
+ 'hacker': ['#000000','#0a0800','#141000','#0e0c00','#1c1800','#060400','#ffb000','#cc8d00','#a97a0f','#ffb000','#ffd050','#cc8d00','#1a1400','#2a2200','','','','','','','','','','#ef4444','','#ef4444','#ffb000','#0b0d10','#0b0d10','255,176,0','rgba(255,176,0,0.35)'],
+ 'daylight': ['#ffffff','#f7f7f8','#eeeef0','#ffffff','#f0f0f2','#f5f5f7','#1a1a2e','#4a4a6a','#68687b','#7a4ddb','#725faa','#6d28d9','#e0e0e6','#d0d0d8','','','','','','0 2px 12px rgba(0,0,0,0.08)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#c43838','#227195','#c43838','#7a4ddb','','','122,77,219','rgba(122,77,219,0.35)','light'],
+ 'paper': ['#faf8f5','#f0ece6','#e8e2d8','#faf8f5','#f0ece6','#f5f2ed','#2c2416','#5a4e3e','#6c6355','#7d5f12','#7b6122','#6a5010','#ddd6c8','#ccc4b0','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7d5f12','','','125,95,18','rgba(125,95,18,0.35)','light'],
+ 'cloud': ['#f0f5ff','#e8eef8','#dde5f0','#f5f8ff','#e4ecf8','#edf2fa','#1a2040','#4a5070','#5e637b','#245dd5','#3c669b','#2563eb','#d0d8ea','#bcc8e0','','','','','','0 2px 12px rgba(0,20,60,0.06)','0 8px 32px rgba(0,20,60,0.10)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#245dd5','','','36,93,213','rgba(36,93,213,0.35)','light'],
+ 'meadow': ['#f2f8f0','#e8f0e4','#dce8d6','#f5faf2','#e4f0de','#edf5ea','#1a2e16','#3a5a32','#526b4a','#137538','#147638','#15803d','#c8dcc0','#b0ccaa','','','','','','0 2px 12px rgba(0,40,0,0.06)','0 8px 32px rgba(0,40,0,0.10)','#147638','#895806','#ba3535','#1f6a8b','#ba3535','#137538','','','19,117,56','rgba(19,117,56,0.35)','light'],
+ 'peach': ['#fff5f0','#fbeae2','#f5ddd2','#fff7f2','#fce8dc','#fdf0ea','#2e1a14','#6a4438','#7e5d51','#ad4107','#925523','#ea580c','#f0d0c0','#e4bfab','','','','','','0 2px 12px rgba(60,20,0,0.06)','0 8px 32px rgba(60,20,0,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#ad4107','','','173,65,7','rgba(173,65,7,0.35)','light'],
+ 'lavender': ['#f8f5ff','#f0eaf8','#e6ddf0','#faf7ff','#eee6f8','#f4f0fa','#1e1430','#4a3a60','#6a5d80','#7443d5','#68569b','#7c3aed','#ddd0ee','#ccc0dd','','','','','','0 2px 12px rgba(40,0,60,0.06)','0 8px 32px rgba(40,0,60,0.10)','#147237','#895806','#b63434','#1f6a8b','#b63434','#7443d5','','','116,67,213','rgba(116,67,213,0.35)','light'],
+ 'gruvbox-light': ['#fbf1c7','#f2e5bc','#ebdbb2','#fbf1c7','#f2e5bc','#f5ecc4','#3c3836','#504945','#685e54','#a73e06','#934a0e','#af3a03','#d5c4a1','#bdae93','','#98971a','#d79921','#cc241d','#458588','0 2px 12px rgba(40,30,0,0.08)','0 8px 32px rgba(40,30,0,0.12)','#646411','#7d5913','#bc211b','#36686a','#b13232','#a73e06','','','167,62,6','rgba(167,62,6,0.35)','light'],
+ 'snow': ['#ffffff','#f8fafc','#f1f5f9','#ffffff','#f1f5f9','#f8fafc','#0f172a','#334155','#657185','#0275b0','#23759a','#0284c7','#e2e8f0','#cbd5e1','','','','','','0 2px 12px rgba(0,0,0,0.05)','0 8px 32px rgba(0,0,0,0.08)','#167e3c','#986207','#c93939','#23759a','#c93939','#0275b0','','','2,117,176','rgba(2,117,176,0.35)','light'],
+ 'sand': ['#f8f4ef','#f0e8de','#e8ddd0','#faf6f0','#f0e8de','#f5f0e8','#2e2418','#5a4e3e','#6b6154','#7e5d06','#7a5c12','#8b6508','#ddd0be','#ccc0aa','','','','','','0 2px 12px rgba(60,40,0,0.06)','0 8px 32px rgba(60,40,0,0.10)','#136e35','#895806','#b13232','#1e6686','#b13232','#7e5d06','','','126,93,6','rgba(126,93,6,0.35)','light'],
+ 'rose': ['#fff5f7','#fce7ec','#f5d5de','#fff7f9','#fce0e8','#fdeef2','#2e1420','#6a3850','#81536c','#bf183d','#b02d44','#be123c','#f0c8d4','#e4b0c0','','','','','','0 2px 12px rgba(60,0,20,0.06)','0 8px 32px rgba(60,0,20,0.10)','#136e35','#845506','#b13232','#1e6686','#b13232','#bf183d','','','191,24,61','rgba(191,24,61,0.35)','light'],
+ 'solarized-dark': ['#002b36','#073642','#0a3f4e','#053542','#0d4a58','#01303d','#fdf6e3','#a6b2b2','#a8b2b0','#b58900','#d4a017','#8a6a00','#0a4050','#105060','','#859900','#cb4b16','#dc322f','#268bd2','','','#aab84d','#e4a186','#ee9997','#78b7e3','#f69696','#947000','','','181,137,0','rgba(181,137,0,0.35)'],
+ 'solarized-light': ['#fdf6e3','#eee8d5','#e4ddc6','#fdf6e3','#eee8d5','#f5eedb','#002b36','#51656c','#4e6467','#795d00','#7b5d0d','#8a6a00','#d6ceb5','#c8c0a5','','#859900','#cb4b16','#dc322f','#268bd2','0 2px 12px rgba(0,30,40,0.08)','0 8px 32px rgba(0,30,40,0.12)','#5a6800','#a63e12','#b92a27','#1b6497','#b13232','#795d00','','','121,93,0','rgba(121,93,0,0.35)','light'],
+ 'catppuccin-mocha': ['#1e1e2e','#181825','#313244','#1e1e2e','#313244','#1a1a2c','#cdd6f4','#bac2de','#989caf','#cba6f7','#dcc0ff','#b07de0','#313244','#45475a','#f38ba8','#a6e3a1','#fab387','#f38ba8','#89b4fa','','','#a6e3a1','#fab387','#f38ba8','#89b4fa','#f38ba8','#cba6f7','#0b0d10','#0b0d10','203,166,247','rgba(203,166,247,0.35)'],
+ 'catppuccin-latte': ['#eff1f5','#e6e9ef','#dce0e8','#eff1f5','#dce0e8','#e8ebf0','#4c4f69','#5c5f77','#5f6176','#7d34dc','#7a46c2','#7028d4','#ccd0da','#bcc0cc','#d20f39','#40a02b','#fe640b','#d20f39','#1e66f5','0 2px 12px rgba(0,0,0,0.06)','0 8px 32px rgba(0,0,0,0.10)','#2d701e','#a84207','#c50e36','#1a5ad8','#c50e36','#7d34dc','','','125,52,220','rgba(125,52,220,0.35)','light'],
+ 'high-contrast': ['#000000','#0a0a0a','#1a1a1a','#0f0f0f','#222222','#0a0a0a','#ffffff','#f0f0f0','#aaaaaa','#ffd700','#ffed4a','#ccac00','#444444','#666666','#ff0000','#00ff00','#ffff00','#ff0000','#00ffff','','','#00ff00','#ffff00','#ff3d3d','#00ffff','#ff3d3d','#ffd700','#0b0d10','#0b0d10','255,215,0','rgba(255,215,0,0.35)'],
+ 'halloween': ['','#0f1626','#151e33','#111a2b','#1a2540','#0c1322','#eef1f6','#a9b4c8','#8c99b0','#ff7a1a','#ffa14f','#d65d00','#1c2840','#2a3858','','#4ade80','#fbbf24','#f87171','#60a5fa','0 2px 12px rgba(0,4,14,0.5)','0 8px 32px rgba(0,4,14,0.65)','#4ade80','#fbbf24','#f87171','#60a5fa','#f15757','#ff7a1a','#0b0d10','#0b0d10','255,122,26','rgba(255,122,26,0.35)'],
+ 'frost': ['#08111b','#0d1926','#132334','#0f1c2b','#18293d','#0a1520','#eaf4fb','#a3b9cc','#7f97ad','#2dd4bf','#7ee8da','#0f9e8c','#1b2d41','#294057','','#4ade80','#fbbf24','#f87171','#93c5fd','0 2px 12px rgba(0,6,14,0.5)','0 8px 32px rgba(0,6,14,0.65)','#4ade80','#fbbf24','#f87171','#93c5fd','#f15e5e','#2dd4bf','#0b0d10','#0b0d10','45,212,191','rgba(45,212,191,0.35)'],
+ 'dawn': ['#f5f6fc','#eceefa','#e1e4f5','#fafbff','#e6e9f8','#f0f2fb','#161a33','#474d6b','#5e647f','#4f46e5','#5558cf','#3730a3','#d6daef','#c3c8e6','','','','','','0 2px 12px rgba(10,14,40,0.07)','0 8px 32px rgba(10,14,40,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#4f46e5','','','79,70,229','rgba(79,70,229,0.35)','light'],
+ 'tokyo-day': ['#fff6fa','#ffedf4','#fde1ec','#fffafc','#fde7f0','#fff0f6','#24101a','#5c3a4a','#7a5566','#cd0058','#c22261','#9c0043','#f3d3e0','#ebbfd1','','','','','','0 2px 12px rgba(40,0,16,0.07)','0 8px 32px rgba(40,0,16,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#cd0058','','','205,0,88','rgba(205,0,88,0.35)','light'],
+ 'alucard': ['#fffbeb','#f6f0d8','#ece4c6','#fffdf3','#f2ead0','#fbf6e2','#1f1f1f','#4f4b38','#6c664b','#644ac9','#6b53bc','#4a33a3','#e3dab8','#d6cba3','','','','','','0 2px 12px rgba(40,30,0,0.07)','0 8px 32px rgba(40,30,0,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#644ac9','','','100,74,201','rgba(100,74,201,0.35)','light'],
+ 'monokai-light': ['#fafaf6','#f1f1ea','#e7e7de','#fdfdfa','#ecece3','#f6f6f0','#272822','#4e4f44','#67685c','#4b7100','#537112','#436600','#dcdcd0','#cdcdbf','','','','','','0 2px 12px rgba(20,20,10,0.07)','0 8px 32px rgba(20,20,10,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#4b7100','','','75,113,0','rgba(75,113,0,0.35)','light'],
+ 'shallows': ['#f0fafb','#e4f4f6','#d5ecef','#f7fcfd','#dcf0f2','#ebf7f8','#0c2226','#3a5458','#516b70','#007384','#00737f','#005662','#c9e3e7','#b4d6db','','','','','','0 2px 12px rgba(0,30,36,0.07)','0 8px 32px rgba(0,30,36,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#007384','','','0,115,132','rgba(0,115,132,0.35)','light'],
+ 'ash': ['#faf7f6','#f3eeec','#eae2df','#fdfbfa','#efe7e4','#f7f3f1','#231716','#54403d','#725b57','#c22727','#bc2f2b','#8e1b1b','#e2d6d2','#d4c4bf','','','','','','0 2px 12px rgba(40,10,8,0.07)','0 8px 32px rgba(40,10,8,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#c22727','','','194,39,39','rgba(194,39,39,0.35)','light'],
+ 'vapor-day': ['#fcf6ff','#f6ecfc','#eedff8','#fefaff','#f2e5fa','#f9f1fe','#22122c','#533f5f','#715c7e','#a21caf','#a921ba','#701a75','#e6d3f1','#dbc0ea','','','','','','0 2px 12px rgba(30,0,40,0.07)','0 8px 32px rgba(30,0,40,0.12)','#147237','#895806','#ba3535','#1f6a8b','#ba3535','#a21caf','','','162,28,175','rgba(162,28,175,0.35)','light'],
+ 'greenbar': ['#f4faf3','#e9f4e7','#dcecd9','#f9fdf8','#e2f0df','#eff7ed','#0d1f10','#3b5240','#556e5a','#13782b','#18772c','#0b5a1f','#cfe3cb','#bcd7b7','','','','','','0 2px 12px rgba(0,30,6,0.07)','0 8px 32px rgba(0,30,6,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#13782b','','','19,120,43','rgba(19,120,43,0.35)','light'],
+ 'nord-light': ['#eceff4','#e5e9f0','#d8dee9','#f2f4f8','#dfe4ec','#e8ecf2','#2e3440','#434c5e','#4c566a','#476283','#506478','#4c6a91','#d0d7e2','#c2cad8','','','','','','0 2px 12px rgba(20,26,40,0.07)','0 8px 32px rgba(20,26,40,0.12)','#136e35','#845506','#b13232','#1e6686','#b13232','#476283','','','71,98,131','rgba(71,98,131,0.35)','light'],
+ 'bright': ['#ffffff','#f6f6f6','#ececec','#ffffff','#f0f0f0','#fafafa','#0a0a0a','#3d3d3d','#5c5c5c','#111111','#333333','#000000','#e2e2e2','#d0d0d0','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#157a3a','#935f07','#bf3636','#227195','#bf3636','#111111','','','17,17,17','rgba(17,17,17,0.35)','light'],
+ 'high-contrast-light': ['#ffffff','#ffffff','#f2f2f2','#ffffff','#e8e8e8','#ffffff','#000000','#1a1a1a','#2b2b2b','#0000c8','#1a1aff','#00008b','#000000','#333333','','','','','','0 2px 12px rgba(0,0,0,0.07)','0 8px 32px rgba(0,0,0,0.12)','#147638','#8e5c06','#bf3636','#206e90','#bf3636','#0000c8','','','0,0,200','rgba(0,0,200,0.35)','light'],
+ 'hayride': ['#fbf6ee','#f6eee2','#efe3d1','#ffffff','#f3e9da','#fffaf2','#23180f','#5a4838','#6b5a4a','#b63d0b','#ad4109','#9a3412','#eadfcd','#e0cfb6','','','','','','0 2px 12px rgba(40,24,8,0.07)','0 8px 32px rgba(40,24,8,0.12)','#147638','#8e5c06','#ba3535','#1f6a8b','#ba3535','#b63d0b','','','182,61,11','rgba(182,61,11,0.35)','light'],
+ 'snowfall': ['#f2f8fb','#e8f2f7','#dbeaf1','#f8fbfd','#e1eef4','#edf5f9','#0e1d26','#3b5260','#526976','#0f746c','#0c7267','#115e59','#cfe2ea','#bcd5df','','','','','','0 2px 12px rgba(0,20,30,0.07)','0 8px 32px rgba(0,20,30,0.12)','#147638','#8e5c06','#ba3535','#206e90','#ba3535','#0f746c','','','15,116,108','rgba(15,116,108,0.35)','light'],
+ };
+ const THEMES = (function () {
+ const out = {}, base = THEME_ROWS.vibe;
+ for (const id in THEME_ROWS) {
+ const row = THEME_ROWS[id], vars = {};
+ for (let i = 0; i < THEME_KEYS.length; i++) { const v = row[i] || base[i]; if (v) vars[THEME_KEYS[i]] = v; }
+ out[id] = vars;
+ }
+ return out;
+ })();
+ const SEASONS = [
+ { id: 'halloween', name: 'Halloween', theme: 'halloween', light: 'hayride', from: '10-01', to: '10-31' },
+ { id: 'winter', name: 'Winter', theme: 'frost', light: 'snowfall', from: '12-01', to: '12-31' },
+ ];
+ const PAIRS = { 'vibe':'cloud','violet-hour':'daylight','midnight':'dawn','forest':'meadow','neon-tokyo':'tokyo-day','dracula':'alucard','monokai':'monokai-light','ocean-deep':'shallows','sunset':'peach','arctic':'snow','ember':'ash','vapor':'vapor-day','slate':'lavender','matrix':'greenbar','nord':'nord-light','gruvbox-dark':'gruvbox-light','abyss':'bright','copper':'sand','sakura-night':'rose','hacker':'paper','daylight':'violet-hour','paper':'hacker','cloud':'vibe','meadow':'forest','peach':'sunset','lavender':'slate','gruvbox-light':'gruvbox-dark','snow':'arctic','sand':'copper','rose':'sakura-night','solarized-dark':'solarized-light','solarized-light':'solarized-dark','catppuccin-mocha':'catppuccin-latte','catppuccin-latte':'catppuccin-mocha','high-contrast':'high-contrast-light','halloween':'hayride','frost':'snowfall','dawn':'midnight','tokyo-day':'neon-tokyo','alucard':'dracula','monokai-light':'monokai','shallows':'ocean-deep','ash':'ember','vapor-day':'vapor','greenbar':'matrix','nord-light':'nord','bright':'abyss','high-contrast-light':'high-contrast','hayride':'halloween','snowfall':'frost' };
+ const MODES = { 'vibe':'dark','violet-hour':'dark','midnight':'dark','forest':'dark','neon-tokyo':'dark','dracula':'dark','monokai':'dark','ocean-deep':'dark','sunset':'dark','arctic':'dark','ember':'dark','vapor':'dark','slate':'dark','matrix':'dark','nord':'dark','gruvbox-dark':'dark','abyss':'dark','copper':'dark','sakura-night':'dark','hacker':'dark','daylight':'light','paper':'light','cloud':'light','meadow':'light','peach':'light','lavender':'light','gruvbox-light':'light','snow':'light','sand':'light','rose':'light','solarized-dark':'dark','solarized-light':'light','catppuccin-mocha':'dark','catppuccin-latte':'light','high-contrast':'dark','halloween':'dark','frost':'dark','dawn':'light','tokyo-day':'light','alucard':'light','monokai-light':'light','shallows':'light','ash':'light','vapor-day':'light','greenbar':'light','nord-light':'light','bright':'light','high-contrast-light':'light','hayride':'light','snowfall':'light' };
+ // THEMES:END
+
+ // ── Seasons: in its dates a season's theme stands in for the default theme unless display `seasonal` is off (a picked
+ // theme stays). <html data-ov-season>, window 'ov:season'; the mark wears it on any theme (data-ov-season-mark).
+ const DEFAULT_ID = 'vibe';
+ function seasonNow(date) {
+ const d = date || new Date();
+ const md = String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
+ for (let i = 0; i < SEASONS.length; i++) {
+ const s = SEASONS[i];
+ if (s.from <= s.to ? (md >= s.from && md <= s.to) : (md >= s.from || md <= s.to)) return s;
+ }
+ return null;
+ }
+ let _season = null;
+ let _shown = { id: DEFAULT_ID, vars: null, custom: false };
+ let _trying = null; // { id, name, vars } while a theme is tried on this tab (?ov-try=, 4.41.0)
+ /** Paint the theme `themeId` (its variables `vars`), or the season's theme in its place. → the id painted. */
+ // ── Light, dark and Auto: every built-in has two halves (PAIRS); display `scheme` 'auto' paints the half the device
+ // asks for (a theme without one gives way to the default's half), 'manual' the picked theme as it is.
+ let _mq = null;
+ function deviceMode() {
+ try { _mq = _mq || window.matchMedia('(prefers-color-scheme: dark)'); return _mq.matches ? 'dark' : 'light'; } catch (e) { return 'dark'; }
+ }
+ function modeOf(id, vars) { return MODES[id] || ((vars && vars['--color-scheme']) === 'light' ? 'light' : 'dark'); }
+ function otherHalf(id, mode) { return PAIRS[id] && MODES[PAIRS[id]] === mode ? PAIRS[id] : (mode === 'light' ? PAIRS[DEFAULT_ID] : DEFAULT_ID); }
+ function halfFor(themeId, vars, custom) {
+ if (readDisplay().scheme !== 'auto') return { id: themeId, vars: vars, custom: custom };
+ const want = deviceMode();
+ if (modeOf(themeId, vars) === want) return { id: themeId, vars: vars, custom: custom };
+ const other = otherHalf(themeId, want);
+ return { id: other, vars: THEMES[other], custom: false };
+ }
+ // The default theme in either mode: Vibe, and its light half. A season stands in for both, in the same mode.
+ function isDefault(id) { return id === DEFAULT_ID || id === PAIRS[DEFAULT_ID]; }
+
+ function show(themeId, vars, custom) {
+ _shown = { id: themeId, vars: vars, custom: !!custom };
+ // A theme being tried (?ov-try=) wins every repaint; _shown keeps the real one for Stop.
+ if (_trying && _trying.vars) return paintTry();
+ const half = halfFor(themeId, vars, !!custom);
+ const s = isDefault(half.id) && !half.custom && readDisplay().seasonal !== 'off' ? seasonNow() : null;
+ const seasonTheme = s ? (modeOf(half.id) === 'light' && s.light ? s.light : s.theme) : null;
+ const seasonal = seasonTheme && THEMES[seasonTheme] ? s : null;
+ const painted = seasonal ? seasonTheme : half.id;
+ applyVars(seasonal ? THEMES[seasonTheme] : half.vars);
+ const changed = (_season && _season.id) !== (seasonal && seasonal.id);
+ _season = seasonal;
+ try {
+ const el = document.documentElement;
+ if (seasonal) el.setAttribute('data-ov-season', seasonal.id); else el.removeAttribute('data-ov-season');
+ const marked = readDisplay().seasonal !== 'off' ? seasonNow() : null;
+ if (marked) el.setAttribute('data-ov-season-mark', marked.id); else el.removeAttribute('data-ov-season-mark');
+ el.setAttribute('data-theme', painted);
+ } catch { /* no DOM */ }
+ if (changed) { try { window.dispatchEvent(new CustomEvent('ov:season', { detail: seasonal })); } catch { /* */ } }
+ return painted;
+ }
+
+ /** The Light / Dark / Auto switch's state: 'auto', else the mode of the theme on screen. */
+ function getScheme() { return readDisplay().scheme === 'auto' ? 'auto' : modeOf(_shown.id, _shown.vars); }
+ /** Light or Dark saves the picked theme's other half (Auto off); Auto follows the device. → the new state. */
+ function setScheme(mode) {
+ if (mode === 'auto') { setDisplay({ scheme: 'auto' }); return 'auto'; }
+ if (mode !== 'light' && mode !== 'dark') return getScheme();
+ const cur = _shown;
+ if (readDisplay().scheme === 'auto') setDisplay({ scheme: 'manual' }, { noRepaint: true });
+ if (modeOf(cur.id, cur.vars) !== mode) save(otherHalf(cur.id, mode), null, { keepSeasonal: true });
+ else show(cur.id, cur.vars || THEMES[cur.id] || THEMES[DEFAULT_ID], cur.custom);
+ return mode;
+ }
+
+ function getCookie(name) {
+ const match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)'));
+ return match ? decodeURIComponent(match[1]) : null;
+ }
+
+ function setCookie(name, value, days) {
+ const maxAge = days * 24 * 60 * 60;
+ const encoded = encodeURIComponent(value);
+ const host = (typeof location !== 'undefined' && location.hostname) || '';
+ // Domain=.openvibe.tools only on openvibe.tools and its subdomains (CONTRACTS.md); host-only everywhere else.
+ if (host && (host.endsWith('.openvibe.tools') || host === 'openvibe.tools')) {
+ document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};domain=.openvibe.tools;SameSite=Lax;Secure`;
+ } else {
+ const secure = (typeof location !== 'undefined' && location.protocol === 'https:') ? ';Secure' : '';
+ document.cookie = `${name}=${encoded};path=/;max-age=${maxAge};SameSite=Lax${secure}`;
+ }
+ }
+
+ /**
+ * Apply a theme's CSS variables to document.documentElement.
+ * If themeId is not found in the built-in map, does nothing.
+ */
+ function applyById(themeId) {
+ const vars = THEMES[themeId];
+ if (!vars) return false;
+ applyVars(vars);
+ return true;
+ }
+
+ // Every token any built-in sets: applying a theme clears the ones it does not set (no inherited --success).
+ const KNOWN_PROPS = (function () { const out = {}; for (const id in THEMES) for (const k in THEMES[id]) out[k] = true; return Object.keys(out); })();
+ /** Readable text on a colour: near-black or white, whichever contrasts more (WCAG relative luminance). */
+ function onColor(c) {
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());
+ if (!m) return null;
+ const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];
+ const lin = (i) => { const v = parseInt(h.slice(i, i + 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
+ const L = 0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4);
+ return (L + 0.05) / 0.05 >= 1.05 / (L + 0.05) ? '#0b0d10' : '#ffffff';
+ }
+ function applyVars(vars) {
+ if (!vars || typeof vars !== 'object') return;
+ // A theme without its own text-on-accent colours (custom and older server themes) gets readable ones derived.
+ if (vars['--accent'] && !vars['--on-accent']) {
+ const on = onColor(vars['--accent']);
+ if (on) vars = Object.assign({}, vars, { '--on-accent': on, '--on-accent-strong': vars['--on-accent-strong'] || onColor(vars['--accent-strong'] || vars['--accent']) || on });
+ }
+ const el = document.documentElement;
+ for (let i = 0; i < KNOWN_PROPS.length; i++) if (!(KNOWN_PROPS[i] in vars)) el.style.removeProperty(KNOWN_PROPS[i]);
+ for (const [prop, val] of Object.entries(vars)) {
+ if (prop.startsWith('--')) el.style.setProperty(prop, val);
+ }
+ // Native controls, scrollbars and form fields follow the theme's light/dark mode.
+ const scheme = vars['--color-scheme'];
+ if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
+ // The canvas follows the variables (value as fallback), so a page that sets --bg-primary later repaints it too.
+ if (vars['--bg-primary']) el.style.background = `var(--bg-primary, ${vars['--bg-primary']})`;
+ if (vars['--text-primary']) el.style.color = `var(--text-primary, ${vars['--text-primary']})`;
+ syncBrowserUi();
+ }
+
+ // ── A correction fades in ─────────────────────────────────
+ // A theme that changes under someone (the account's answer, another tab) crossfades for 0.4 s instead of jumping;
+ // first paint and their own picks stay instant. A colour fade moves nothing, so Calm keeps it; only Off stops it.
+ let _fadeTimer = 0;
+ function fadeNext() {
+ if (typeof document === 'undefined' || !document.head || motionOf() === 'off') return;
+ const el = document.documentElement;
+ if (!document.getElementById('ov-theme-fade-css')) {
+ const st = document.createElement('style'); st.id = 'ov-theme-fade-css';
+ st.textContent = 'html.ov-theme-fade,html.ov-theme-fade *,html.ov-theme-fade *::before,html.ov-theme-fade *::after{transition:background-color .4s ease,color .4s ease,border-color .4s ease,fill .4s ease,stroke .4s ease!important}';
+ document.head.appendChild(st);
+ }
+ el.classList.add('ov-theme-fade');
+ clearTimeout(_fadeTimer); _fadeTimer = setTimeout(function () { el.classList.remove('ov-theme-fade'); }, 600);
+ }
+
+ // ── The browser's UI follows the theme on every apply: theme-color, the favicon (<link rel="icon" data-ov-icon>),
+ // and window 'ov:theme' { accent, accentLight, bg, mode }.
+ // FAVICON:BEGIN — generated by scripts/build-theme-loader.js from mark.js and app-icon.js; do not edit by hand
+ const FAV_DEFS = '<mask id="ovh" maskUnits="userSpaceOnUse" x="-20" y="-20" width="140" height="140"><rect x="-20" y="-20" width="140" height="140" fill="#fff"/><path d="M70.59 68.5L53.31 31.5M70.59 68.5L88.41 31.5" fill="none" stroke="#000" stroke-width="31" stroke-linecap="round"/></mask><linearGradient id="ovgo" gradientUnits="userSpaceOnUse" x1="3.09" y1="23" x2="57.09" y2="77"><stop offset="0" stop-color="#87a6ff"/><stop offset="1" stop-color="#4268d6"/></linearGradient><linearGradient id="ovgv" gradientUnits="userSpaceOnUse" x1="0" y1="31.5" x2="0" y2="68.5"><stop offset="0" stop-color="#ae8df9"/><stop offset="1" stop-color="#784fd4"/></linearGradient>';
+ const FAV_BODY = '<circle cx="30.09" cy="50" r="18" fill="none" stroke="url(#ovgo)" stroke-width="18" mask="url(#ovh)"/><path d="M70.59 68.5L53.31 31.5M70.59 68.5L88.41 31.5" fill="none" stroke="url(#ovgv)" stroke-width="17" stroke-linecap="round" stroke-linejoin="round"/>';
+ const FAV_LIGHT = { cx: 30.09, cy: 50, r: 5.58 };
+ const FAV_SITES = { network: ['#60a5fa', ''], live: ['#60a5fa', '#ef4444'], tools: ['#38bdf8', ''], community: ['#34d399', ''], games: ['#a78bfa', '#facc15'], media: ['#fbbf24', ''], space: ['#818cf8', ''], ai: ['#22d3ee', ''] };
+ // FAVICON:END
+ const COLOR_RE = /^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%/]+\)|hsla?\([\d\s.,%/deg]+\))$/i;
+ function readColor(name, fallback) {
+ try { const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return COLOR_RE.test(v) ? v : fallback; } catch { return fallback; }
+ }
+ /** The tab icon: app-icon.js's favicon() (the Pair, heavy form, the site's glow and light) on the theme's ground. */
+ function faviconSvg(site, bg) {
+ const s = FAV_SITES[site] || FAV_SITES.network, a = s[0];
+ const light = s[1] ? '<circle cx="' + FAV_LIGHT.cx + '" cy="' + FAV_LIGHT.cy + '" r="' + FAV_LIGHT.r + '" fill="' + s[1] + '"/>' : '';
+ return 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="-32 -32 64 64"><defs>' + FAV_DEFS
+ + '\n<radialGradient id="glow"><stop offset="0" stop-color="' + a + '" stop-opacity=".3"/><stop offset="1" stop-color="' + a + '" stop-opacity="0"/></radialGradient></defs>'
+ + '<rect x="-32" y="-32" width="64" height="64" rx="15" fill="' + bg + '"/><g transform="scale(0.58) translate(-50 -50)"><ellipse cx="50" cy="50" rx="52" ry="40" fill="url(#glow)"/>'
+ + FAV_BODY + light + '</g></svg>');
+ }
+ let _browserUiKey = '';
+ function syncBrowserUi() {
+ if (typeof document === 'undefined' || !document.head) return;
+ const accent = readColor('--accent', '#3b82f6'), light = readColor('--accent-light', accent), bg = readColor('--bg-secondary', readColor('--bg-primary', '#0a0f1c'));
+ const mode = document.documentElement.getAttribute('data-theme-mode') || 'dark';
+ const key = [accent, light, bg, mode].join('|'); if (key === _browserUiKey) return; _browserUiKey = key;
+ try {
+ let meta = document.querySelector('meta[name="theme-color"]');
+ if (!meta) { meta = document.createElement('meta'); meta.name = 'theme-color'; document.head.appendChild(meta); }
+ meta.setAttribute('content', bg);
+ const link = document.querySelector('link[rel~="icon"][data-ov-icon]');
+ if (link) { link.type = 'image/svg+xml'; link.href = faviconSvg(link.getAttribute('data-ov-icon'), readColor('--bg-primary', '#0a0f1c')); }
+ } catch { /* cosmetic */ }
+ try { window.dispatchEvent(new CustomEvent('ov:theme', { detail: { accent: accent, accentLight: light, bg: bg, mode: mode } })); } catch { /* */ }
+ }
+
+ /** The theme from localStorage 'ov_theme' (id and variables), else the 'ov_theme_id' cookie, else the default. */
+ function resolveAndApply() {
+ // 1. Try localStorage (same-origin cache with full variables)
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) {
+ const saved = JSON.parse(raw);
+ if (saved && saved.variables && typeof saved.variables === 'object') {
+ return show(saved.id || saved.slug || DEFAULT_ID, saved.variables, saved.custom);
+ }
+ if (saved && (saved.id || saved.slug)) {
+ const id = saved.id || saved.slug;
+ if (THEMES[id]) return show(id, THEMES[id]);
+ }
+ }
+ } catch { /* corrupt localStorage */ }
+
+ // 2. Try cross-domain cookie
+ const cookieId = getCookie(COOKIE_NAME);
+ if (cookieId && THEMES[cookieId]) {
+ // Cache the full variable set so the next load paints from localStorage instantly.
+ cacheLocally(cookieId, THEMES[cookieId]);
+ return show(cookieId, THEMES[cookieId]);
+ }
+
+ // 3. Default — vibe (already the CSS fallback values, but apply explicitly), or the season's theme
+ return show(DEFAULT_ID, THEMES[DEFAULT_ID]);
+ }
+
+ function authToken() {
+ return getCookie('ov_token') ||
+ (typeof localStorage !== 'undefined' && (localStorage.getItem('ov_token') || localStorage.getItem('token'))) ||
+ null;
+ }
+
+ /** Cache the theme's FULL variables (custom ones too) and the date it was chosen, so the next load paints at once. */
+ function cacheLocally(themeId, vars, custom, at) {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({
+ id: themeId, slug: themeId, variables: vars, at: at || Date.now(), ...(custom ? { custom: true } : {}),
+ }));
+ } catch { /* quota */ }
+ setCookie(COOKIE_NAME, themeId, 365);
+ }
+
+ /** Save a theme here and to the account (opts.custom: variable overrides; opts.sync === false: this device only). */
+ function save(themeId, variables, opts) {
+ const custom = (opts && opts.custom) || null;
+ const base = variables || THEMES[themeId] || {};
+ const vars = custom ? Object.assign({}, base, custom) : base;
+ if (!vars || !Object.keys(vars).length) return;
+ // Picking the default theme while a season's stands in for it means "my normal theme": seasonal themes go off.
+ if (isDefault(themeId) && !custom && _season && !(opts && opts.keepSeasonal)) setDisplay({ seasonal: 'off' }, { noRepaint: true });
+ cacheLocally(themeId, vars, !!custom);
+ show(themeId, vars, !!custom); // saving a theme shows it: every token, the canvas and the browser UI follow at once
+ if (opts && opts.sync === false) return;
+ if (typeof fetch === 'undefined') return;
+ const token = authToken();
+ const headers = { 'Content-Type': 'application/json' };
+ if (token) headers['Authorization'] = 'Bearer ' + token;
+ fetch(_config.apiBase + '/api/themes/me', {
+ method: 'PUT', headers: headers, credentials: 'include', keepalive: true,
+ body: JSON.stringify({ theme_id: themeId, custom_variables: custom, at: themeAt() }),
+ }).catch(function () { /* offline / signed out — local cache still holds */ });
+ }
+
+ /** When the theme in this browser was chosen (ms), or 0 for a copy older than 4.64.0. */
+ function themeAt() {
+ try { const t = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null'); return t && Number(t.at) > 0 ? Number(t.at) : 0; } catch { return 0; }
+ }
+
+ /**
+ * Get the current theme ID (from cookie or localStorage).
+ */
+ function getCurrent() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) {
+ const saved = JSON.parse(raw);
+ if (saved && (saved.id || saved.slug)) return saved.id || saved.slug;
+ }
+ } catch {}
+ return getCookie(COOKIE_NAME) || 'vibe';
+ }
+
+ // ── Display settings, one record for every OpenVibe site: display.get() → DISPLAY_OPTS' values; display.set(patch)
+ // applies here and in other tabs and saves to the account. <html data-ov-*> and --ov-text-scale before first paint.
+ const DISPLAY_KEY = 'ov_display', DISPLAY_OPTS = { motion: ['auto', 'full', 'reduced', 'off'], text: ['100', '112', '125'], seasonal: ['on', 'off'], scheme: ['manual', 'auto'], contrast: ['normal', 'more'], font: ['default', 'readable'], colors: ['default', 'cb-safe'], oled: ['off', 'on'], icons: ['auto', 'line', 'solid'], window: ['glass', 'solid', 'outline'] };
+ // Status names some sites use (--error, --error-text, --ok, --warn, --accent-text) that no theme sets: they resolve to the theme's
+ // readable status colours instead of each site's fallback literal (pale on light themes). :where() keeps them
+ // below any site's own value (4.86.0).
+ // A control nobody styled wears the theme, not the browser's grey (4.186.0): only one with no class, only its face,
+ // at no specificity, so any rule a site wrote still wins and no layout moves. Its shape is the kit's job.
+ const BARE = ':where(:not([class]))', FACE = '{font-family:inherit;border:1px solid var(--border-light,var(--border,color-mix(in srgb,currentColor 30%,transparent)));border-radius:10px;color:var(--text-primary,inherit);background:';
+ const BARE_CSS = ':where(button,input:is([type=submit],[type=button],[type=reset]))' + BARE + FACE + 'var(--bg-hover,var(--bg-card,color-mix(in srgb,currentColor 8%,transparent)))}'
+ + ':where(input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=image],[type=submit],[type=button],[type=reset],[type=hidden]),select,textarea)' + BARE + FACE + 'var(--bg-input,var(--bg-primary,transparent))}'
+ + ':where(input:is([type=checkbox],[type=radio],[type=range]),progress)' + BARE + '{accent-color:var(--accent,auto)}';
+ const ALIAS_CSS = ':where(html){--error:var(--danger);--error-text:var(--danger-text);--ok:var(--success-text);--warn:var(--warning-text);--accent-text:var(--accent-strong)}';
+ // Comfort (4.45.0): one <html> attribute each, !important over the inline theme (Okabe-Ito colours, OLED black).
+ const COMFORT_CSS = 'html[data-ov-contrast=more]{--text-secondary:color-mix(in srgb,var(--text-primary) 88%,var(--bg-primary))!important;--text-muted:color-mix(in srgb,var(--text-primary) 76%,var(--bg-primary))!important;--border:color-mix(in srgb,var(--text-primary) 40%,var(--bg-primary))!important;--border-light:color-mix(in srgb,var(--text-primary) 55%,var(--bg-primary))!important}'
+ + 'html[data-ov-contrast=more] :is(p,dd,td) a:not([class]){text-decoration-line:underline;text-underline-offset:2px}html[data-ov-contrast=more] :focus-visible{outline:3px solid var(--accent-strong,var(--accent))!important;outline-offset:2px}'
+ + 'html[data-ov-font=readable]{--ov-font:"Atkinson Hyperlegible",Verdana,Tahoma,"Segoe UI",system-ui,sans-serif!important}html[data-ov-font=readable] body{font-family:var(--ov-font)!important;letter-spacing:.02em;word-spacing:.06em}html[data-ov-font=readable] :is(p,li,dd){line-height:1.7}'
+ + 'html[data-ov-colors=cb-safe]{--success:#56b4e9!important;--success-text:#7cc6ef!important;--warning:#f0e442!important;--warning-text:#f0e442!important;--danger:#e8743b!important;--danger-text:#f0985f!important}'
+ + 'html[data-ov-colors=cb-safe][data-theme-mode=light]{--success:#0072b2!important;--success-text:#005a8c!important;--warning:#e69f00!important;--warning-text:#8a6100!important;--danger:#d55e00!important;--danger-text:#a84400!important}'
+ + 'html[data-ov-oled=on][data-theme-mode=dark]{--bg-primary:#000!important;--bg-secondary:#050608!important;--bg-tertiary:#0b0d12!important;--bg-card:#0a0c10!important;--bg-input:#0b0d12!important;--bg-hover:#12151c!important}';
+ const COMFORT_ATTRS = { contrast: ['data-ov-contrast', 'normal'], font: ['data-ov-font', 'default'], colors: ['data-ov-colors', 'default'], oled: ['data-ov-oled', 'off'], icons: ['data-ov-icons', 'auto'], window: ['data-ov-window', 'glass'] };
+ // ── Motion (4.183.0; the owner, 2026-11-10: "still have full motion but actually just reduce it instead of getting
+ // rid of it"). auto follows the device, full plays everything, reduced is Calm (every effect, smaller and slower),
+ // off alone stops motion. <html data-ov-motion=full|reduced|off> is the one signal; --ov-move is what a moving
+ // thing multiplies its distance by (display.move() in script); under Calm an endless loop runs at CALM_RATE.
+ const CALM_MOVE = 0.35, CALM_RATE = 0.6;
+ let _rm = null;
+ function deviceCalm() { try { _rm = _rm || window.matchMedia('(prefers-reduced-motion: reduce)'); return !!_rm.matches; } catch (e) { return false; } }
+ function motionOf(d) { const m = (d || readDisplay()).motion; return m === 'off' ? 'off' : m === 'reduced' ? 'calm' : m === 'full' ? 'full' : deviceCalm() ? 'calm' : 'full'; }
+ function moveOf() { const m = motionOf(); return m === 'off' ? 0 : m === 'calm' ? CALM_MOVE : 1; }
+ const MOTION_CSS = ':where(html){--ov-move:1}html[data-ov-motion=reduced]{--ov-move:' + CALM_MOVE + ';scroll-behavior:auto!important}html[data-ov-motion=off]{--ov-move:0}'
+ + 'html[data-ov-motion=off] *:not([data-fx-demo] *),html[data-ov-motion=off] *:not([data-fx-demo] *)::before,html[data-ov-motion=off] *:not([data-fx-demo] *)::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}'
+ + 'html[data-ov-motion=off]::view-transition-group(*),html[data-ov-motion=off]::view-transition-old(*),html[data-ov-motion=off]::view-transition-new(*){animation-duration:.001ms!important}'
+ + 'html[data-ov-motion=off] .ov-icon .ovi-comet{display:none}';
+ const _paced = typeof WeakSet === 'function' ? new WeakSet() : null;
+ function paceLoops(scope) {
+ if (!_paced || typeof document === 'undefined') return;
+ try {
+ const calm = document.documentElement.getAttribute('data-ov-motion') === 'reduced';
+ const list = scope && scope.getAnimations ? scope.getAnimations({ subtree: true }) : document.getAnimations();
+ for (let i = 0; i < list.length; i++) {
+ const a = list[i], fx = a.effect, t = fx && fx.getComputedTiming ? fx.getComputedTiming() : null;
+ if (!t || t.iterations !== Infinity) continue;
+ const el = fx.target, slow = calm && !(el && el.closest && el.closest('[data-fx-demo]'));
+ if (slow && !_paced.has(a)) { _paced.add(a); a.updatePlaybackRate(a.playbackRate * CALM_RATE); }
+ else if (!slow && _paced.has(a)) { _paced.delete(a); a.updatePlaybackRate(a.playbackRate / CALM_RATE); }
+ }
+ } catch (e) { /* no Web Animations here: loops keep their own speed */ }
+ }
+ if (typeof document !== 'undefined' && document.addEventListener) {
+ document.addEventListener('animationstart', function (e) { if (document.documentElement.getAttribute('data-ov-motion') === 'reduced') paceLoops(e.target); }, true);
+ }
+ function readDisplay() {
+ const out = { motion: 'auto', text: '100', seasonal: 'on', scheme: 'manual', contrast: 'normal', font: 'default', colors: 'default', oled: 'off', icons: 'auto', window: 'glass' };
+ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null') || {}; for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(d[k])) >= 0) out[k] = String(d[k]); } catch (e) { /* */ }
+ return out;
+ }
+ function applyDisplay(d) {
+ if (typeof document === 'undefined') return;
+ const el = document.documentElement;
+ const m = motionOf(d), attr = m === 'off' ? 'off' : m === 'calm' ? 'reduced' : d.motion === 'full' ? 'full' : '';
+ if (attr) el.setAttribute('data-ov-motion', attr); else el.removeAttribute('data-ov-motion');
+ if (d.text === '100') { el.style.removeProperty('--ov-text-scale'); el.removeAttribute('data-ov-text'); } else { el.style.setProperty('--ov-text-scale', String(parseInt(d.text, 10) / 100)); el.setAttribute('data-ov-text', d.text); }
+ for (const k in COMFORT_ATTRS) { const [attr, off] = COMFORT_ATTRS[k]; if (d[k] && d[k] !== off) el.setAttribute(attr, d[k]); else el.removeAttribute(attr); }
+ if (!document.getElementById('ov-display-css') && document.head) {
+ const st = document.createElement('style'); st.id = 'ov-display-css';
+ st.textContent = 'html[data-ov-text]{font-size:calc(100% * var(--ov-text-scale,1))}' + MOTION_CSS + BARE_CSS + ALIAS_CSS + COMFORT_CSS;
+ document.head.appendChild(st);
+ }
+ paceLoops();
+ try { window.dispatchEvent(new CustomEvent('ov:display', { detail: d })); } catch (e) { /* */ }
+ }
+ /** When the display settings in this browser were last changed (ms), or 0 for a copy older than 4.64.0. */
+ function displayAt() {
+ try { const d = JSON.parse(localStorage.getItem(DISPLAY_KEY) || 'null'); return d && Number(d.at) > 0 ? Number(d.at) : 0; } catch (e) { return 0; }
+ }
+ function setDisplay(patch, opts) {
+ const d = readDisplay();
+ const was = d.seasonal + '|' + d.scheme;
+ for (const k in DISPLAY_OPTS) if (patch && DISPLAY_OPTS[k].indexOf(String(patch[k])) >= 0) d[k] = String(patch[k]);
+ // A change made here is dated now; one adopted from the account or another site keeps its own date.
+ const at = opts && Number(opts.at) > 0 ? Number(opts.at) : (opts && opts.local ? displayAt() : Date.now());
+ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: at }))); } catch (e) { /* */ }
+ applyDisplay(d);
+ if (d.seasonal + '|' + d.scheme !== was && !(opts && opts.noRepaint)) show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom);
+ if (!(opts && opts.local) && typeof fetch !== 'undefined') {
+ const token = authToken(); const headers = { 'Content-Type': 'application/json' }; if (token) headers.Authorization = 'Bearer ' + token;
+ if (token || /(?:^|; )ov_sso_hint=account/.test(document.cookie)) fetch(_config.apiBase + '/api/themes/me/display', { method: 'PUT', headers: headers, credentials: 'include', keepalive: true, body: JSON.stringify(Object.assign({}, d, { at: at })) }).catch(function () {});
+ }
+ return d;
+ }
+
+ // ── Try a theme (4.41.0): ?ov-try=<catalog name> paints it for this tab without saving (?ov-try=off ends it); a bar
+ // offers Use it (saves to the account) and Stop. Only a name is read from the URL, never colours.
+ const TRY_KEY = 'ov_try';
+ const TRY_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
+ const tryName = function (id) { return String(id).split('-').map(function (w) { return w ? w[0].toUpperCase() + w.slice(1) : w; }).join(' '); };
+ function paintTry() {
+ applyVars(_trying.vars);
+ try {
+ const el = document.documentElement;
+ el.setAttribute('data-theme', _trying.id);
+ el.setAttribute('data-ov-trying', _trying.id);
+ el.removeAttribute('data-ov-season');
+ } catch { /* no DOM */ }
+ syncBrowserUi();
+ return _trying.id;
+ }
+ function readTry() {
+ let id = '';
+ try {
+ const p = typeof location !== 'undefined' ? new URLSearchParams(location.search).get('ov-try') : null;
+ if (p === 'off') { sessionStorage.removeItem(TRY_KEY); return null; }
+ if (p && TRY_ID.test(p)) sessionStorage.setItem(TRY_KEY, p);
+ id = sessionStorage.getItem(TRY_KEY) || '';
+ } catch { id = ''; }
+ return TRY_ID.test(id) ? id : null;
+ }
+ function startTry(id, name) {
+ if (!id || !TRY_ID.test(id)) return;
+ try { sessionStorage.setItem(TRY_KEY, id); } catch { /* private mode: this page only */ }
+ const begin = function (vars, label) {
+ if (!vars || !Object.keys(vars).length) { stopTry(false); return; }
+ _trying = { id: id, name: label || tryName(id), vars: vars };
+ fadeNext();
+ paintTry();
+ whenReady(tryBar);
+ };
+ if (THEMES[id]) return begin(THEMES[id], name);
+ if (typeof fetch === 'undefined') return;
+ // A gallery theme comes from the catalog, once the page has run init({ apiBase }) (the script tag after this one).
+ whenReady(function () {
+ fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(id))
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (d) { begin(d && d.theme && d.theme.variables, d && d.theme && d.theme.name); })
+ .catch(function () { stopTry(false); });
+ });
+ }
+ function stopTry(repaint) {
+ _trying = null;
+ try { sessionStorage.removeItem(TRY_KEY); } catch { /* */ }
+ try {
+ document.documentElement.removeAttribute('data-ov-trying');
+ const bar = document.getElementById('ov-try-bar');
+ if (bar) bar.remove();
+ const u = new URL(location.href);
+ if (u.searchParams.has('ov-try')) { u.searchParams.delete('ov-try'); history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); }
+ } catch { /* no DOM */ }
+ if (repaint !== false) { fadeNext(); resolveAndApply(); }
+ }
+ function whenReady(fn) {
+ if (typeof document === 'undefined') return;
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn();
+ }
+ function tryBar() {
+ if (!_trying || document.getElementById('ov-try-bar') || !document.body) return;
+ const t = _trying;
+ const bar = document.createElement('div');
+ bar.id = 'ov-try-bar';
+ bar.setAttribute('role', 'status');
+ bar.style.cssText = 'position:fixed;left:0;right:0;margin:0 auto;width:max-content;bottom:calc(var(--ovnav-appbar-h,0px) + 12px);z-index:2147483000;display:flex;align-items:center;gap:6px;max-width:calc(100vw - 24px);box-sizing:border-box;padding:4px 4px 4px 16px;border-radius:999px;border:1px solid var(--border-light,var(--border,#2c3d5c));background:var(--bg-card,#131c2e);color:var(--text-primary,#e6edf7);box-shadow:0 10px 30px rgba(0,0,0,.35);font:600 14px/1.3 var(--ov-font,system-ui,sans-serif)';
+ const label = document.createElement('span');
+ label.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0';
+ label.textContent = 'Trying ' + t.name;
+ const btn = function (text, strong) {
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.textContent = text;
+ b.style.cssText = 'flex:none;min-height:44px;padding:0 16px;border-radius:999px;cursor:pointer;font:inherit;' + (strong
+ ? 'border:0;background:var(--accent-strong,var(--accent,#3b82f6));color:var(--on-accent-strong,#fff)'
+ : 'border:1px solid var(--border-light,var(--border,#2c3d5c));background:transparent;color:inherit');
+ return b;
+ };
+ const use = btn('Use it', true);
+ const stop = btn('Stop', false);
+ use.addEventListener('click', function () {
+ stopTry(false);
+ save(t.id, t.vars);
+ const done = document.createElement('div');
+ done.setAttribute('role', 'status');
+ done.style.cssText = bar.style.cssText + ';padding:12px 18px';
+ done.textContent = t.name + ' is your theme now, on every OpenVibe site.';
+ document.body.appendChild(done);
+ setTimeout(function () { done.remove(); }, 3500);
+ });
+ stop.addEventListener('click', function () { stopTry(true); });
+ bar.append(label, use, stop);
+ document.body.appendChild(bar);
+ }
+
+ // ── The look travels (4.64.0): each domain keeps its own copy, so a click to another OpenVibe site carries ?ov_look=
+ // (what this page paints, the display and theme with their dates). Added at click time, read only after an OpenVibe
+ // page, only colours pass; the next site keeps it when newer and drops it from the address before first paint.
+ const OV_HOST = /(^|\.)(openvibe\.[a-z]{2,12}|openre\.stream)$/i;
+ const PAINT_KEY = 'ov_paint';
+ const PAINT_ATTRS = ['data-theme', 'data-theme-mode', 'data-ov-season', 'data-ov-season-mark', 'data-ov-motion', 'data-ov-text', 'data-ov-contrast', 'data-ov-font', 'data-ov-colors', 'data-ov-oled', 'data-ov-icons', 'data-ov-window'];
+ const SAFE_VALUE = /^(#[0-9a-f]{3,8}|rgba?\([0-9.,\s%]+\)|[0-9.,\s]+|light|dark|(-?[0-9.]+(px)?\s+){2,4}rgba?\([0-9.,\s%]+\))$/i;
+ const SAFE_ATTR = /^[a-z0-9-]{1,40}$/;
+ function seasonUntil(s) {
+ if (!s) return 0;
+ const d = new Date(), y = d.getFullYear(), to = s.to.split('-');
+ const end = new Date(y + (s.from > s.to && s.from <= String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0') ? 1 : 0), Number(to[0]) - 1, Number(to[1]) + 1);
+ return end.getTime();
+ }
+ function paintRecord() {
+ const el = document.documentElement, a = {};
+ for (let i = 0; i < PAINT_ATTRS.length; i++) { const v = el.getAttribute(PAINT_ATTRS[i]); if (v) a[PAINT_ATTRS[i]] = v; }
+ return { k: THEME_KEYS.map(function (k) { return el.style.getPropertyValue(k).trim(); }), a: a, ts: el.style.getPropertyValue('--ov-text-scale').trim(), u: seasonUntil(_season), at: Date.now() };
+ }
+ let _paintTimer = 0;
+ function rememberPaint() {
+ if (_paintTimer || typeof setTimeout === 'undefined') return;
+ _paintTimer = setTimeout(function () { _paintTimer = 0; if (_trying) return; try { localStorage.setItem(PAINT_KEY, JSON.stringify(paintRecord())); } catch (e) { /* */ } }, 0);
+ }
+ function lookToken() {
+ const rec = { v: 1, d: readDisplay(), da: displayAt(), t: _shown.id, c: _shown.custom ? 1 : 0, ta: themeAt(), p: paintRecord() };
+ return btoa(JSON.stringify(rec)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
+ }
+ function readLook() {
+ try {
+ if (typeof location === 'undefined' || !/[?&]ov_look=/.test(location.search)) return null;
+ const u = new URL(location.href), raw = u.searchParams.get('ov_look') || '';
+ u.searchParams.delete('ov_look');
+ try { history.replaceState(history.state, '', u.pathname + (u.search || '') + u.hash); } catch (e) { /* */ }
+ const from = document.referrer ? new URL(document.referrer).hostname : '';
+ if (!OV_HOST.test(from) || raw.length > 4000) return null;
+ const L = JSON.parse(atob(raw.replace(/-/g, '+').replace(/_/g, '/')));
+ return L && L.v === 1 ? L : null;
+ } catch (e) { return null; }
+ }
+ function adoptLook(L) {
+ if (!L) return;
+ const now = Date.now(), clamp = function (n) { n = Number(n) || 0; return n > now + 60000 ? now : n; };
+ const da = clamp(L.da), ta = clamp(L.ta);
+ if (L.d && typeof L.d === 'object' && da > displayAt()) {
+ const d = readDisplay();
+ for (const k in DISPLAY_OPTS) if (DISPLAY_OPTS[k].indexOf(String(L.d[k])) >= 0) d[k] = String(L.d[k]);
+ try { localStorage.setItem(DISPLAY_KEY, JSON.stringify(Object.assign({}, d, { at: da }))); } catch (e) { /* */ }
+ }
+ const id = String(L.t || '');
+ if (TRY_ID.test(id) && ta > themeAt()) {
+ if (!L.c && THEMES[id]) cacheLocally(id, THEMES[id], false, ta);
+ else if (L.c && L.p && Array.isArray(L.p.k)) {
+ const vars = {};
+ for (let i = 0; i < THEME_KEYS.length; i++) { const v = String(L.p.k[i] || ''); if (v && SAFE_VALUE.test(v)) vars[THEME_KEYS[i]] = v; }
+ if (Object.keys(vars).length > 8) cacheLocally(id, vars, true, ta);
+ }
+ }
+ }
+ function decorateLink(e) {
+ const a = e.target && e.target.closest && e.target.closest('a[href]');
+ if (!a || a.hasAttribute('download')) return;
+ let u; try { u = new URL(a.href, location.href); } catch (err) { return; }
+ if (!/^https?:$/.test(u.protocol) || u.hostname === location.hostname || !OV_HOST.test(u.hostname)) return;
+ try { u.searchParams.set('ov_look', lookToken()); a.href = u.toString(); } catch (err) { /* */ }
+ }
+ const _arrived = readLook();
+ adoptLook(_arrived);
+ applyDisplay(readDisplay());
+ if (typeof document !== 'undefined' && document.addEventListener) {
+ ['pointerdown', 'keydown', 'click'].forEach(function (ev) { document.addEventListener(ev, function (e) { if (ev !== 'keydown' || e.key === 'Enter') decorateLink(e); }, true); });
+ window.addEventListener('ov:theme', rememberPaint);
+ window.addEventListener('ov:display', rememberPaint);
+ }
+
+ const activeId = resolveAndApply();
+ rememberPaint();
+ startTry(readTry());
+ // The icon link may be parsed after this script: repaint once the head is complete.
+ if (typeof document !== 'undefined' && document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { _browserUiKey = ''; syncBrowserUi(); });
+ // A theme picked in another tab (or on another OpenVibe page of this site) applies here at once.
+ if (typeof window !== 'undefined') window.addEventListener('storage', function (e) { if (e.key === 'ov_theme') { try { fadeNext(); resolveAndApply(); } catch (err) { /* */ } } if (e.key === DISPLAY_KEY) { fadeNext(); applyDisplay(readDisplay()); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } });
+
+ if (typeof window !== 'undefined' && window.matchMedia) {
+ try {
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const onDevice = function () { if (readDisplay().scheme === 'auto') { fadeNext(); show(_shown.id, _shown.vars || THEMES[_shown.id] || THEMES[DEFAULT_ID], _shown.custom); } };
+ if (mq.addEventListener) mq.addEventListener('change', onDevice); else if (mq.addListener) mq.addListener(onDevice);
+ } catch (e) { /* no media queries */ }
+ }
+
+ if (typeof window !== 'undefined' && window.matchMedia) {
+ try {
+ const rmq = window.matchMedia('(prefers-reduced-motion: reduce)');
+ const onMotion = function () { if (readDisplay().motion === 'auto') applyDisplay(readDisplay()); };
+ if (rmq.addEventListener) rmq.addEventListener('change', onMotion); else if (rmq.addListener) rmq.addListener(onMotion);
+ } catch (e) { /* no media queries */ }
+ }
+
+ // A theme not in the built-in map (community, custom) comes from the catalog, never falls back to the default.
+ function fetchThemeVars(themeId) {
+ return fetch(_config.apiBase + '/api/themes/' + encodeURIComponent(themeId))
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (d) { return (d && d.theme && d.theme.variables) || null; })
+ .catch(function () { return null; });
+ }
+
+ // ── The account's look from openvibe.network, at once (the cached look is already painted; newest copy wins). No
+ // token gate: the ov_token cookie rides along (credentials: 'include'), for sites whose script cannot read it.
+ let _lastSync = 0;
+ /** This site's id from <html data-ov-service> (openvibe-shared/shell writes it), or '' (4.43.0). */
+ function siteService() {
+ try { const s = document.documentElement.getAttribute('data-ov-service') || ''; return /^[a-z][a-z0-9-]{1,31}$/.test(s) ? s : ''; } catch { return ''; }
+ }
+ function syncFromServer() {
+ if (typeof fetch === 'undefined') return;
+ _lastSync = Date.now();
+ const token = authToken();
+ const headers = token ? { 'Authorization': 'Bearer ' + token } : {};
+ // A per-site look set in OpenVibe.Design ("Games in Neon Tokyo") answers for ?service=<this site> (4.43.0).
+ const site = siteService();
+ fetch(_config.apiBase + '/api/themes/me/active' + (site ? '?service=' + encodeURIComponent(site) : ''), { headers: headers, credentials: 'include' })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (data) {
+ if (data && data.display) {
+ const cur = readDisplay(); const next = {}; for (const k in DISPLAY_OPTS) next[k] = data.display[k] || cur[k];
+ const serverAt = Number(data.display_at) || 0, localAt = displayAt();
+ const takeServer = serverAt ? serverAt > localAt : !(localAt && Date.now() - localAt < 60000);
+ if (takeServer && JSON.stringify(next) !== JSON.stringify(cur)) { fadeNext(); setDisplay(data.display, { local: true, at: serverAt || localAt }); }
+ else if (serverAt && localAt > serverAt && JSON.stringify(next) !== JSON.stringify(cur)) setDisplay(cur, { at: localAt }); // newer here: the account catches up
+ }
+ if (!data || !data.theme_id) return;
+ const serverThemeAt = Number(data.theme_at) || 0, localThemeAt = themeAt();
+ if (serverThemeAt ? serverThemeAt < localThemeAt : (localThemeAt && Date.now() - localThemeAt < 60000)) return; // a newer pick here
+ const serverId = data.theme_id;
+ const custom = (data.custom_variables && typeof data.custom_variables === 'object'
+ && Object.keys(data.custom_variables).length) ? data.custom_variables : null;
+ const builtin = THEMES[serverId];
+ const settle = function (base) {
+ const vars = custom ? Object.assign({}, base || {}, custom) : (base || null);
+ if (!vars || !Object.keys(vars).length) return;
+ cacheLocally(serverId, vars, !!custom, serverThemeAt || localThemeAt || 1); // cacheLocally, not save() — no write-back loop
+ if (_shown.id === serverId && !!_shown.custom === !!custom && JSON.stringify(_shown.vars) === JSON.stringify(vars)) return;
+ fadeNext();
+ show(serverId, vars, !!custom);
+ };
+ if (builtin) return settle(builtin);
+ fetchThemeVars(serverId).then(settle);
+ })
+ .catch(function () { /* offline / signed out — keep the cached theme */ });
+ }
+ syncFromServer();
+ // Asked again when the page comes back into view, and shortly after arriving from another site (a change saved there
+ // just before the link may land after this page first asked).
+ if (typeof document !== 'undefined' && document.addEventListener) {
+ const again = function () { if (Date.now() - _lastSync > 10000) syncFromServer(); };
+ document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') again(); });
+ if (typeof window !== 'undefined' && window.addEventListener) window.addEventListener('pageshow', function (e) { if (e && e.persisted) again(); });
+ let from = '';
+ try { from = document.referrer ? new URL(document.referrer).hostname : ''; } catch (e) { from = ''; }
+ if (from && typeof location !== 'undefined' && from !== location.hostname) setTimeout(syncFromServer, 2500);
+ }
+
+ // ── Public API ───────────────────────────────────────────
+ const OpenVibeThemeLoader = {
+ THEMES: THEMES,
+ display: { get: readDisplay, set: setDisplay, options: DISPLAY_OPTS, motion: function () { return motionOf(); }, move: moveOf },
+ apply: applyById,
+ applyVars: applyVars,
+ // Seasons: SEASONS, now() → the season today (or null), showing() → the season whose theme is on screen (or null)
+ seasons: { SEASONS: SEASONS, now: seasonNow, showing: function () { return _season; } },
+ // Light / Dark / Auto: get() → 'light'|'dark'|'auto', set(mode); pairOf(id) → its other half; modeOf(id)
+ scheme: { get: getScheme, set: setScheme, device: deviceMode },
+ pairOf: function (id) { return PAIRS[id] || null; },
+ modeOf: function (id) { return MODES[id] || null; },
+ onColor: onColor,
+ save: save,
+ // Try a theme on this tab without saving it (?ov-try=): start(id), stop(), current() → the id being tried, or null.
+ try: { start: function (id) { startTry(String(id || '')); }, stop: function () { stopTry(true); }, current: function () { return _trying ? _trying.id : null; } },
+ getCurrent: getCurrent,
+ // This site's id (<html data-ov-service>), the key of its theme exception on the account (4.43.0).
+ site: function () { return siteService(); },
+ resolveAndApply: resolveAndApply,
+ init: function (opts) {
+ if (opts && typeof opts.apiBase === 'string' && opts.apiBase.trim()) {
+ _config.apiBase = opts.apiBase.trim();
+ }
+ return OpenVibeThemeLoader;
+ },
+ };
+
+ if (typeof module !== 'undefined' && module.exports) module.exports = OpenVibeThemeLoader;
+ else root.OpenVibeThemeLoader = OpenVibeThemeLoader;
+
+})(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-extensions",
"name": "openvibe-extensions",
- "version": "0.9.1",
+ "version": "0.9.2",
"private": true,
"private": true,
"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.",
"description": "OpenVibe's first-party client surfaces (ADR-023). Today: the OpenVibe Companion, a Manifest V3 browser extension for Chromium and Firefox: coupon codes from OpenVibe.Coupons, and one click to keep a listing on the OpenVibe.Rent board or watch a page with OpenVibe.Watch.",
"scripts": {
"scripts": {
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
* The side panel in a real browser: headless Chrome (CDP over Node's own WebSocket, no dependencies) loads the
* The side panel in a real browser: headless Chrome (CDP over Node's own WebSocket, no dependencies) loads the
* unpacked extension, opens panel.html with a stubbed Network/Chat/Codes and a live agent view, and checks what a
* unpacked extension, opens panel.html with a stubbed Network/Chat/Codes and a live agent view, and checks what a
* person would see: the Dock on its Agents tab, the agent's stream frame, the line and the meta, the four controls
* person would see: the Dock on its Agents tab, the agent's stream frame, the line and the meta, the four controls
- * (Pause/Resume, Curtain now, Mute/Unmute, End), and the Dock's own styles and nothing a CSP refused.
+ * (Pause/Resume, Curtain now, Mute/Unmute, End, and the local Float), and the Dock's own styles and nothing a CSP refused.
*
*
* node scripts/check-panel.js (CHROME=/path/to/chrome to choose the browser; OV_EXT_DIR=an unpacked build, default browser/)
* node scripts/check-panel.js (CHROME=/path/to/chrome to choose the browser; OV_EXT_DIR=an unpacked build, default browser/)
*
*
@@ -189,7 +189,7 @@ async function main() {
@@ -189,7 +189,7 @@ async function main() {
watch: (() => { const w = q('a.ovda-watch'); return w ? { hidden: w.hidden, text: w.textContent.trim() } : null; })(),
watch: (() => { const w = q('a.ovda-watch'); return w ? { hidden: w.hidden, text: w.textContent.trim() } : null; })(),
frame: (q('iframe.ovda-frame') || {}).src || null,
frame: (q('iframe.ovda-frame') || {}).src || null,
tab: text(q('[role="tab"][aria-selected="true"]')),
tab: text(q('[role="tab"][aria-selected="true"]')),
- styles: !!document.getElementById('ovdk-styles') && !!document.getElementById('ovda-styles') && !!document.getElementById('ovac-styles'),
+ styles: !!document.getElementById('ovdk-styles') && !!document.getElementById('ovda-styles'),
lineDisplay: q('.ovda-line') ? getComputedStyle(q('.ovda-line')).display : null,
lineDisplay: q('.ovda-line') ? getComputedStyle(q('.ovda-line')).display : null,
};
};
})()`);
})()`);
@@ -229,12 +229,12 @@ async function main() {
@@ -229,12 +229,12 @@ async function main() {
if (seen && seen.line !== 'Ship the Dock') failures.push(`the agent's line is "${seen.line}"`);
if (seen && seen.line !== 'Ship the Dock') failures.push(`the agent's line is "${seen.line}"`);
if (seen && !/Paused by @ada/.test(seen.meta || '')) failures.push(`the meta does not say who paused it: "${seen.meta}"`);
if (seen && !/Paused by @ada/.test(seen.meta || '')) failures.push(`the meta does not say who paused it: "${seen.meta}"`);
if (seen && !/2 watching/.test(seen.meta || '')) failures.push(`the meta does not say how many watch: "${seen.meta}"`);
if (seen && !/2 watching/.test(seen.meta || '')) failures.push(`the meta does not say how many watch: "${seen.meta}"`);
- const wantActs = ['pause:Resume', 'curtain:Curtain now', 'mute:Unmute', 'end:End'];
- if (seen && JSON.stringify(seen.acts) !== JSON.stringify(wantActs)) failures.push(`the four controls are ${JSON.stringify(seen.acts)}`);
- if (seen && seen.actLinks.length !== 4) failures.push(`the read-only controls are not links: ${JSON.stringify(seen.actLinks)}`);
+ const wantActs = ['pause:Resume', 'curtain:Curtain now', 'mute:Unmute', 'end:End', 'float:Float'];
+ if (seen && JSON.stringify(seen.acts) !== JSON.stringify(wantActs)) failures.push(`the controls are ${JSON.stringify(seen.acts)}`);
+ if (seen && seen.actLinks.length !== 4) failures.push(`the four server controls are not read-only links: ${JSON.stringify(seen.actLinks)}`);
if (seen && seen.actLinks.some((href) => href !== 'https://openvibe.codes')) failures.push(`a control does not open Codes: ${JSON.stringify(seen.actLinks)}`);
if (seen && seen.actLinks.some((href) => href !== 'https://openvibe.codes')) failures.push(`a control does not open Codes: ${JSON.stringify(seen.actLinks)}`);
if (seen && seen.frame !== AGENT.embed_url) failures.push(`the stream frame is "${seen.frame}"`);
if (seen && seen.frame !== AGENT.embed_url) failures.push(`the stream frame is "${seen.frame}"`);
- if (seen && !seen.styles) failures.push("the Dock's own styles are missing");
+ if (seen && !seen.styles) failures.push("the Dock's and agent view's own styles are missing");
if (seen && seen.lineDisplay !== 'flex') failures.push(`the agent's styles did not apply (line display ${seen.lineDisplay})`);
if (seen && seen.lineDisplay !== 'flex') failures.push(`the agent's styles did not apply (line display ${seen.lineDisplay})`);
if (security.length) failures.push(`CSP refused: ${security.join(' | ')}`);
if (security.length) failures.push(`CSP refused: ${security.join(' | ')}`);
if (!paused.some((u) => u.startsWith('https://openvibe.live/embed/agent/'))) failures.push('the panel never asked for the agent embed');
if (!paused.some((u) => u.startsWith('https://openvibe.live/embed/agent/'))) failures.push('the panel never asked for the agent embed');