/**
* openvibe-shared/search.js — the OpenVibe menu: search, every site and every tool in one panel, opened by the navbar's
* magnifier, "/" or Ctrl+K (4.23.0). The navbar loads this file the first time someone asks.
*
* It used to be two panels side by side, the launcher (the dots: every site) and search (the magnifier), which "kind
* of accomplish the same exact thing" (the owner, 2026-10-28: "combine… what are the most important things… a better
* hierarchy… use this user's history… input history and autocomplete… to give them better results… maybe use tabs").
*
* Before typing, three tabs:
* For you the places this person went lately anywhere on OpenVibe (their Network history, signed in), this site's
* pages, their recent searches, and their sites (most used by them, else most used by everyone: the Frame's
* analytics ranking).
* Sites every OpenVibe site by what people come to do (the Frame's groups), the one you are on marked.
* Tools the tool families and the most used tools (OpenVibe.Tools' catalog).
* Typing makes one list: sites and tools to go to, this site's results and pages, the person's own history, the rest of
* OpenVibe.Search (sites they use most first), their recent searches, and "search everywhere" / "Ask Vibe". The box
* completes what they type from their own searches, history and the site names (Tab or → takes it).
*
* Enter shows every result in the panel (4.29.0, the Search Fabric, plan 2026-10-28 §4.5), unless a row was picked with the
* arrows or the text is a place's exact name (live, tools): tabs All · OpenVibe · Web · Video · News · Products (· Yours,
* signed in). OpenVibe's results come at once (Network's GET /api/frame/query, scopes network[,mine], depth quick); the
* web's arrive on their own (scope web) with skeleton rows meanwhile, each saying where it came from ("via Wikipedia").
* A line at the end says what was searched and how long it took. The person's token goes along, so their own results
* and the paid web providers are theirs; the query is never written into the page as markup.
*
* OpenVibeSearch.open({ service, scope: { owner, label, page }, links, q, tab, frame, token, apiBase, user,
* suggestUrl, icons }) navbar.js calls it
* OpenVibeSearch.close()
* OpenVibeSearch.suggest(q, { owner, limit, signal, url }) → [{ owner, type, id, title, url, site }] for any page's own box
* (url: https://openvibe.network/api/frame/suggest, or https://search.openvibe.network/api/v1/suggest where the
* page's connect-src allows it; both answer any origin)
*
* Suggestions come through OpenVibe.Network's GET /api/frame/suggest (OpenVibe.Search, kept 30 s): every site's
* Content-Security-Policy lets the navbar reach openvibe.network. History is GET /api/history (the person's own token,
* read once a panel opening, kept two minutes). Text is always set with textContent; links are https or same-site paths
* only. Recent searches live in localStorage (ov_search_recent, eight at most) and can be cleared from the panel.
*/
(function (root) {
'use strict';
const API = 'https://search.openvibe.network';
const SUGGEST = 'https://openvibe.network/api/frame/suggest';
const CATALOG_URL = 'https://openvibe.network/api/catalog.json';
const TOOLS_SEARCH = 'https://openvibe.tools/search?q=';
const RECENT_KEY = 'ov_search_recent';
const SITE = { 'openre.stream': 'Restream', 'search.openvibe.network': 'Search', 'ai.openvibe.services': 'AI' };
const TYPE = { vod: 'Recording', clip: 'Clip', job: 'Job', story: 'Story', page: 'Page', thread: 'Thread', paste: 'Paste', post: 'Post', stream: 'Live', channel: 'Channel', deal: 'Deal', coupon: 'Code', review: 'Review', entity: 'Reviews', recipe: 'Recipe', game: 'Game', tool: 'Tool', listing: 'Listing', wiki: 'Wiki' };
// Until the Frame answers (or when it cannot): the network's main sites and the tool families.
const FALLBACK_SITES = [
{ id: 'live', name: 'Live', desc: 'Go live, watch streams', icon: 'live', url: 'https://openvibe.live/' },
{ id: 'chat', name: 'Chat', desc: 'Rooms, servers and messages', icon: 'chat', url: 'https://openvibe.chat/' },
{ id: 'games', name: 'Games', desc: 'Browser games', icon: 'games', url: 'https://openvibe.games/' },
{ id: 'tools', name: 'Tools', desc: 'Every online tool', icon: 'tools', url: 'https://openvibe.tools/' },
{ id: 'community', name: 'Community', desc: 'Forum, pastes and posts', icon: 'community', url: 'https://openvibe.community/' },
{ id: 'network', name: 'Network', desc: 'Your account and themes', icon: 'network', url: 'https://openvibe.network/' },
];
const FALLBACK_FAMILIES = [
{ name: 'Media Tools', icon: 'youtube', url: 'https://yt.openvibe.tools/' },
{ name: 'Image Tools', icon: 'image', url: 'https://img.openvibe.tools/' },
{ name: 'Audio Tools', icon: 'audio', url: 'https://audio.openvibe.tools/' },
{ name: 'PDF & Documents', icon: 'pdf', url: 'https://docs.openvibe.tools/' },
{ name: 'Text Tools', icon: 'text', url: 'https://text.openvibe.tools/' },
{ name: 'Developer Tools', icon: 'code', url: 'https://dev.openvibe.tools/' },
{ name: 'Network Tools', icon: 'dns', url: 'https://net.openvibe.tools/' },
];
// The menu's three tabs (the owner, 11-08: "we should have tabs at the top … a feed tab … remember whichever tab the
// user last had open … the main tab should just show links that exist on that site, and under that the full list
// of sites"). The first is labelled with this site's own name.
const TABS = [['here', ''], ['feed', 'Feed'], ['you', 'You']];
// When the Feed was last looked at, for its "new since" count (11-09, the owner: "if there's anything new in the feed
// since last time you've looked… a badge").
const FEED_SEEN = 'ovs-feed-seen';
const FEED_COUNT = 'ovs-feed-count';
const feedSeen = () => { try { return Number(localStorage.getItem(FEED_SEEN)) || 0; } catch { return 0; } };
function paintFeedDot(count) {
const orb = document.getElementById('openvibe-burger'); if (!orb) return;
let dot = orb.querySelector('.ovs-feed-dot');
if (!dot) { dot = document.createElement('i'); dot.className = 'ovs-feed-dot'; dot.setAttribute('aria-hidden', 'true'); orb.appendChild(dot); }
dot.hidden = !(Number(count) > 0);
}
function paintFeedTabCount(count) {
if (!el || !el.classList.contains('is-open') || !tabsEl) return;
const b = tabsEl.querySelector('[data-tab="feed"]'); if (!b) return;
const old = b.querySelector('.ovs-tabnew'); if (old) old.remove();
const n = Number(count) || 0;
if (n) { const badge = document.createElement('b'); badge.className = 'ovs-tabnew'; badge.textContent = n > 9 ? '9+' : String(n); badge.setAttribute('aria-label', `${n} new since you last looked`); (b.querySelector('.ovs-tab-ic') || b).appendChild(badge); }
}
let feedChannel;
try { if (typeof BroadcastChannel === 'function') { feedChannel = new BroadcastChannel('openvibe-feed-seen'); feedChannel.onmessage = (e) => { const n = Number(e.data && e.data.count) || 0; paintFeedDot(n); paintFeedTabCount(n); }; } } catch { feedChannel = null; }
if (typeof window !== 'undefined') window.addEventListener('storage', (e) => { if (e.key === FEED_COUNT) { paintFeedDot(e.newValue); paintFeedTabCount(e.newValue); } if (e.key === FEED_SEEN) { paintFeedDot(0); paintFeedTabCount(0); } });
const markFeedSeen = () => {
try { localStorage.setItem(FEED_SEEN, String(Date.now())); } catch { /* signed out private window */ }
try { localStorage.setItem(FEED_COUNT, '0'); } catch { /* */ }
paintFeedDot(0); paintFeedTabCount(0);
try { feedChannel?.postMessage({ type: 'seen', count: 0, at: feedSeen() }); } catch { /* */ }
};
let el = null, input = null, ghost = null, listEl = null, tabsEl = null, ctx = {}, items = [], active = -1, inflight = null, timer = null, lastFocus = null;
let tab = 'here', mine = null, catalog = null, completion = '';
// The results view (4.29.0): Enter shows the Search Fabric's answer here, by scope, instead of leaving the page.
const RTABS = [['all', 'All'], ['openvibe', 'OpenVibe'], ['web', 'Web'], ['video', 'Video'], ['news', 'News'], ['products', 'Products'], ['mine', 'Yours']];
const PROVIDER = { open_index: 'OpenVibe\'s index', deepseek: 'DeepSeek', wikipedia: 'Wikipedia', brave: 'Brave', exa: 'Exa', tavily: 'Tavily' };
let view = 'suggest', rtabsEl = null, rq = '', rtab = 'all', rflight = null, moved = false;
// A place of the Dock shown inside the menu (a phone, 312 step 2): which one, its mount, and AI's part.
let door = null, doorEl = null, doorHost = null, doorMount = null, runView = null, runFilter = 'all';
let actsEl = null, actsObs = null, actsTick = 0;
const safeUrl = (u) => { try { const x = new URL(u, location.href); return x.protocol === 'https:' || x.origin === location.origin ? x.href : null; } catch { return null; } };
function siteOf(url) {
try {
const h = new URL(url).hostname.replace(/^www\./, '');
if (SITE[h]) return SITE[h];
const m = /^(?:([a-z0-9-]+)\.)?openvibe\.([a-z]+)$/.exec(h);
if (m) return m[1] ? m[1][0].toUpperCase() + m[1].slice(1) : m[2][0].toUpperCase() + m[2].slice(1);
return h;
} catch { return ''; }
}
const hostOf = (u) => { try { return new URL(u).hostname; } catch { return ''; } };
const recent = {
get() { try { return (JSON.parse(localStorage.getItem(RECENT_KEY) || '[]') || []).filter((x) => typeof x === 'string').slice(0, 8); } catch { return []; } },
add(q) { try { const list = [q, ...recent.get().filter((x) => x.toLowerCase() !== q.toLowerCase())].slice(0, 8); localStorage.setItem(RECENT_KEY, JSON.stringify(list)); } catch { /* private mode */ } },
clear() { try { localStorage.removeItem(RECENT_KEY); } catch { /* */ } },
};
async function suggest(q, { owner = null, limit = 8, signal, url = SUGGEST } = {}) {
const u = new URL(url || SUGGEST);
u.searchParams.set('q', String(q).slice(0, 100));
if (owner) u.searchParams.set('owner', owner);
u.searchParams.set('limit', String(limit));
const res = await fetch(u, { signal, credentials: 'omit', headers: { accept: 'application/json' } });
if (!res.ok) return [];
const d = await res.json();
return (d.suggestions || []).map((s) => ({ owner: s.owner, type: s.type, id: s.id, title: String(s.title || '').slice(0, 160), url: safeUrl(s.canonical_url), site: siteOf(s.canonical_url) })).filter((s) => s.url && s.title);
}
// ── What the panel knows ─────────────────────────────────
/** Every open site from the Frame (most used across OpenVibe first), else the main ones. */
function sites() {
const f = ctx.frame;
if (f && Array.isArray(f.nav) && f.nav.length) return f.nav.map((n) => ({ id: n.id, name: n.name, desc: String(n.tagline || '').replace(/\.\s*$/, ''), icon: n.icon || n.id, url: safeUrl(n.url), group: n.group })).filter((s) => s.url);
return FALLBACK_SITES;
}
/** The site this page belongs to: the longest matching host (play.openvibe.network is Play, not Network). */
// The site this page is on: the longest matching address, and a site that lives under a path (openvibe.my/inventory)
// only on that path.
function hereSite(list) {
const h = location.hostname.toLowerCase(), at = location.pathname || '/'; let best = null, score = 0;
for (const s of list) {
let u; try { u = new URL(s.url); } catch { continue; }
const sh = u.hostname, sp = u.pathname.replace(/\/+$/, '');
if (!sh || !(h === sh || h.endsWith(`.${sh}`))) continue;
if (sp && at !== sp && !at.startsWith(`${sp}/`)) continue;
const n = sh.length * 1000 + sp.length + 1;
if (n > score) { best = s; score = n; }
}
return best;
}
/** The person's own use of each site (their history's counts), for "your sites" and for ranking results. */
const useOf = (id) => { const s = mine && mine.services.find((x) => x.service === id); return s ? s.count : 0; };
/** The person's history: one request per opening, kept two minutes (signed in only; anonymous sessions keep none). */
async function loadHistory() {
if (!ctx.token || !ctx.user || ctx.user.is_anon) return null;
const key = `ov_hist2:${ctx.user.id || ctx.user.username || ''}`;
try { const c = JSON.parse(sessionStorage.getItem(key) || 'null'); if (c && Date.now() - c.at < 120000) return c.data; } catch { /* */ }
try {
const r = await fetch(`${ctx.apiBase || 'https://openvibe.network'}/api/history?limit=60`, { headers: { Authorization: `Bearer ${ctx.token}` }, credentials: 'omit' });
if (!r.ok) return null;
const j = await r.json();
const data = {
items: (j.items || []).map((h) => ({ title: String(h.title || '').slice(0, 160), url: safeUrl(h.url), site: h.service_label || siteOf(h.url), service: h.service, at: utcOf(h.updated_at) })).filter((h) => h.url && h.title),
services: (j.services || []).map((s) => ({ service: String(s.service || ''), count: Number(s.count) || 0 })),
};
try { sessionStorage.setItem(key, JSON.stringify({ at: Date.now(), data })); } catch { /* */ }
return data;
} catch { return null; }
}
/** OpenVibe.Tools' catalog (families and tools), kept 30 minutes; the same copy the navbar used to keep. */
async function loadCatalog() {
try { const c = JSON.parse(sessionStorage.getItem('ov_catalog2') || 'null'); if (c && Date.now() - c.at < 30 * 60000) return c.data; } catch { /* */ }
try {
const r = await fetch(CATALOG_URL, { credentials: 'omit' }); if (!r.ok) return null;
const j = await r.json();
const ok = (u) => { try { const x = new URL(u); return x.protocol === 'https:' ? x.href : null; } catch { return null; } };
const data = {
families: (j.families || []).slice(0, 12).map((f) => ({ name: String(f.name || ''), icon: String(f.icon || 'tools'), url: ok(f.url) })).filter((f) => f.name && f.url),
tools: (j.tools || []).slice(0, 400).map((t) => ({ name: String(t.name || ''), icon: String(t.icon || 'tools'), url: ok(t.url), go: t.hosts && t.hosts.short ? ok(`https://${t.hosts.short}/`) : null,
hosts: [t.hosts && t.hosts.short, t.hosts && t.hosts.canonical].concat((t.hosts && t.hosts.mirrors) || [], (t.hosts && t.hosts.aliases) || []).filter((h) => typeof h === 'string' && h), id: String(t.id || ''),
desc: String(t.tagline || '').slice(0, 120), k: [t.name, t.tagline].concat(t.keywords || []).join(' ').toLowerCase().slice(0, 400) })).filter((t) => t.name && t.url),
};
try { sessionStorage.setItem('ov_catalog2', JSON.stringify({ at: Date.now(), data })); } catch { /* */ }
return data;
} catch { return null; }
}
// Times that keep up while the menu is open (once a minute; nothing runs while it is closed).
let agoTimer = 0;
function tickAgo() {
clearInterval(agoTimer);
agoTimer = setInterval(() => {
if (!el || !el.classList.contains('is-open')) { clearInterval(agoTimer); agoTimer = 0; return; }
el.querySelectorAll('[data-at]').forEach((m) => { const t = ago(m.dataset.at); if (t) m.textContent = [m.dataset.pre, t].filter(Boolean).join(' · '); });
}, 60000);
}
/** A time without a zone is UTC (Network's history and older APIs send "YYYY-MM-DD HH:MM:SS"; a browser reads that as
* local time, and west of UTC every visit looked hours ahead: "just now" for all of them, 11-12). */
function utcOf(t) { const v = String(t || ''); return /^\d{4}-\d\d-\d\d[ T]\d\d:\d\d(:\d\d(\.\d+)?)?$/.test(v) ? `${v.replace(' ', 'T')}Z` : v; }
function ago(iso) {
const t = Date.parse(utcOf(iso)); if (!isFinite(t)) return '';
const m = Math.max(0, Math.round((Date.now() - t) / 60000));
return m < 1 ? 'just now' : m < 60 ? `${m} min ago` : m < 1440 ? `${Math.round(m / 60)} h ago` : `${Math.round(m / 1440)} d ago`;
}
// ── Drawing ──────────────────────────────────────────────
function styles() {
if (document.getElementById('openvibe-search-styles')) return;
const st = document.createElement('style');
st.id = 'openvibe-search-styles';
st.textContent = `
.ovs { position: fixed; inset: 0; z-index: 10020; display: none; background: rgba(3, 6, 12, .55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; color: var(--text-primary, #e8ecf3); }
.ovs.is-open { display: block; animation: ovs-in .14s ease-out; }
/* The button becomes its tab (4.200.0): the flight layer above everything, its parts drawn from the two buttons. */
.ovs-fly { position: fixed; inset: 0; z-index: 2147483600; pointer-events: none; }
.ovs-fly-part { position: absolute; display: grid; place-items: center; color: inherit; will-change: transform, opacity, filter; }
.ovs-fly-ring { position: fixed; z-index: 2147483600; border: 2px solid; border-radius: 50%; pointer-events: none; }
.ovs-fly-part > svg, .ovs-fly-part > .ov-icon { width: 100%; height: 100%; }
.ovs-fly-part > img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovs-fly-part--count { width: auto !important; height: auto !important; }
.ovs-fly-count { display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 999px; background: #ef4444; color: #fff; font: 800 11px/1 system-ui, sans-serif; }
.ovs-panel { position: absolute; left: 50%; top: 8vh; width: min(720px, calc(100vw - 32px)); max-height: 80vh; transform: translateX(-50%); display: flex; flex-direction: column; border-radius: 18px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: var(--bg-secondary, #121826); box-shadow: 0 30px 80px rgba(0,0,0,.55); overflow: hidden; }
.ovs-bar { display: flex; align-items: center; gap: 10px; margin: 10px 10px 6px; padding: 0 6px 0 14px; border-radius: 999px; background: var(--bg-primary, rgba(0,0,0,.25)); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.12)); transition: box-shadow .2s, background .2s; }
.ovs-bar:focus-within { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent), 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); }
/* A site's own focus ring (the orange box on openvibe.download, 10-29) never draws on the field: the pill shows focus. */
.ovs .ovs-input, .ovs .ovs-input:focus, .ovs .ovs-input:focus-visible { outline: none !important; box-shadow: none !important; border: 0 !important; background: transparent !important; }
.ovs-bar > svg { width: 18px; height: 18px; flex: none; color: var(--text-muted, #93a0b4); }
.ovs-field { position: relative; flex: 1; min-width: 0; display: flex; }
.ovs-input, .ovs-ghost { height: 48px; font: inherit; font-size: 17px; letter-spacing: normal; }
.ovs-input { position: relative; z-index: 1; flex: 1; min-width: 0; border: 0; outline: 0; padding: 0; background: transparent; color: inherit; }
.ovs-input::-webkit-search-cancel-button { -webkit-appearance: none; }
.ovs-ghost { position: absolute; inset: 0; display: flex; align-items: center; white-space: pre; overflow: hidden; pointer-events: none; color: var(--text-muted, #93a0b4); opacity: .75; }
.ovs-ghost b { color: transparent; font-weight: inherit; }
.ovs-x { flex: none; height: 36px; padding: 0 12px; border: 0; border-radius: 10px; background: transparent; color: var(--text-muted, #93a0b4); font: inherit; font-size: 13px; cursor: pointer; }
.ovs-x svg { display: none; width: 20px; height: 20px; }
.ovs-x:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
.ovs-x:focus:not(:focus-visible) { outline: none; }
/* 4.223.0 (the owner, 11-12: "Close … ICON"; "these buttons and borders … get extra wide … make that more sleek"): Close
is a round × on every screen; the phone's orb keeps a hairline edge and a soft glass, never a thick ring. */
.ovs-x { display: inline-grid; place-items: center; width: 36px; height: 36px; padding: 0; border-radius: 50%; color: var(--text-secondary, #a8b3c4); transition: background-color .15s, color .15s, transform .12s; }
.ovs-x svg { display: block; width: 18px; height: 18px; }
.ovs-x span { display: none; } /* the button's own aria-label (Close the menu) names it */
.ovs-x:active { transform: scale(.9); }
@media (hover: hover) { .ovs-x:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 10%, transparent); } }
@media (max-width: 640px) {
.ovs.ovs-anchored .ovs-orb { background: color-mix(in srgb, var(--bg-secondary, #121826) 72%, transparent); -webkit-backdrop-filter: blur(12px) saturate(1.4); backdrop-filter: blur(12px) saturate(1.4);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 14%, transparent), 0 8px 22px -10px rgba(0, 0, 0, .6); }
.ovs.ovs-anchored .ovs-orb.is-x::before { border-width: 1px; }
}
/* Phones and small windows: the menu covers the screen (the bar under it too), so Close is always there, a round x at
the end of the search pill (the owner, 11-07: "This needs a close button"). The hit area is 48 px. */
@media (max-width: 860px) {
.ovs-x { position: relative; display: grid; place-items: center; width: 40px; height: 40px; padding: 0; border-radius: 50%; background: var(--bg-hover, rgba(255,255,255,.07)); color: var(--text-secondary, #a8b3c4); }
.ovs-x::after { content: ""; position: absolute; inset: -4px; }
.ovs-x svg { display: block; }
.ovs-x span { display: none; }
.ovs-x:active { background: var(--bg-active, rgba(255,255,255,.14)); color: var(--text-primary, #fff); }
}
.ovs-tabs { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 12px 8px; border-bottom: 1px solid var(--border, rgba(255,255,255,.1)); }
/* Five places in one row (4.194.0): on a phone each is its icon over its word; the doors to the Dock look like the tabs. */
.ovs-starttabs.has-doors .ovs-tab { padding: 0 6px; }
.ovs-starttabs .ovs-tab[data-dock] .ovs-tab-ic > svg { width: 26px; height: 26px; }
@media (max-width: 640px) { .ovs-starttabs.has-doors { gap: 4px; padding: 2px 8px 8px; } .ovs-starttabs.has-doors .ovs-tab { flex-direction: column; gap: 3px; height: 56px; padding: 4px 2px; font-size: 12.5px; border-radius: 14px; } }
.ovs-bridge { flex: 1 0 100%; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; margin-top: 4px; }
.ovs-bridge-b { all: unset; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 0; min-height: 44px; padding: 0 6px; border-radius: 12px; cursor: pointer; font-size: 12.5px; font-weight: 650;
color: var(--text-secondary, #a8b3c4); background: color-mix(in srgb, var(--accent, #3b82f6) 7%, transparent); transition: background-color .15s, color .15s, transform .12s; }
.ovs-bridge-b span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-bridge-i { display: grid; place-items: center; flex: none; color: var(--accent-light, var(--accent, #60a5fa)); }
.ovs-bridge-b:hover, .ovs-bridge-b:focus-visible { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
.ovs-bridge-b:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
.ovs-bridge-b:active { transform: scale(.96); }
@media (max-width: 640px) { .ovs-bridge-b { flex-direction: column; gap: 2px; min-height: 50px; padding: 4px 2px; font-size: 11.5px; } .ovs-bridge-i svg { width: 19px; height: 19px; } }
/* The menu's three tabs (4.149.0): one segmented row under the field; the chosen one in the accent. */
.ovs-starttabs { gap: 6px; padding: 2px 12px 10px; }
.ovs-starttabs .ovs-tab { flex: 1 1 0; min-width: 0; display: inline-flex; align-items: center; justify-content: center; gap: 7px; height: 38px; padding: 0 10px; border-radius: 12px; font-size: 14px; background: color-mix(in srgb, var(--text-primary, #e6edf7) 5%, transparent); }
.ovs-starttabs .ovs-tab span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-starttabs .ovs-tab { height: 42px; gap: 8px; transition: background-color .18s, color .18s; }
.ovs-starttabs .ovs-tab .ovs-tab-ic { position: relative; flex: none; display: grid; place-items: center; width: 22px; height: 22px; overflow: visible; }
.ovs-tab-ic > svg { width: 20px; height: 20px; }
.ovs-tab-ic .ov-icon { display: grid; place-items: center; }
/* 4.236.0, the tab's nudge: this site's tab rolls its glyph and name through the places in its section, and back. */
.ovs-tab .ovs-roll { animation: ovsRoll .38s cubic-bezier(.2, .9, .25, 1); }
@keyframes ovsRoll { from { opacity: 0; transform: translateY(65%); } 60% { opacity: 1; } }
.ovs-tab.is-rolling > span:not(.ovs-tab-ic) { color: var(--accent-light, var(--accent, #60a5fa)); transition: color .3s; }
html[data-ov-motion=off] .ovs-tab .ovs-roll { animation: none; }
/* The site's tab wears its glyph like Feed and You (owner, 11-09): no disc and no ring, the glyph at their weight and in
the label's colour. The icon's box is drawn larger than the slot so the solid glyph itself is about 18 px. */
.ovs-tab-ic .ov-icon .ovi-bg, .ovs-tab-ic .ov-icon .ovi-ring, .ovs-tab-ic .ov-icon .ovi-comet, .ovs-tab-ic .ov-icon .ovi-prog { display: none; }
.ovs-tab-ic .ov-icon { --ovi-glyph: currentColor; color: currentColor; width: 22px; height: 22px; overflow: visible; }
.ovs-tab-ic .ov-icon svg { width: 40px; height: 40px; margin: -9px; flex: none; }
@media (max-width: 420px) { .ovs-starttabs .ovs-tab { padding: 0 7px; gap: 6px; font-size: 13.5px; } }
.ovs-tab-av { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; display: block; box-shadow: 0 0 0 1.5px color-mix(in srgb, var(--text-primary, #e6edf7) 18%, transparent); }
.ovs-tab[aria-selected="true"] .ovs-tab-av { box-shadow: 0 0 0 2px var(--accent, #3b82f6); }
.ovs-tab-on { position: absolute; right: -2px; bottom: -2px; width: 8px; height: 8px; border-radius: 50%; background: var(--success, #22c55e); box-shadow: 0 0 0 2px var(--bg-secondary, #111826); }
.ovs-tabnew { flex: none; min-width: 18px; height: 18px; padding: 0 5px; box-sizing: border-box; border-radius: 999px; font: 750 11px/18px Inter, system-ui, sans-serif; letter-spacing: -.01em; text-align: center; font-variant-numeric: tabular-nums; color: #fff; box-shadow: 0 0 0 2px var(--bg-secondary, #141a24), 0 3px 8px -2px rgba(0, 0, 0, .55); animation: ovsBadgeIn .3s cubic-bezier(.34, 1.56, .64, 1); background: var(--accent, #3b82f6); }
.ovs-tabnew-red { background: var(--danger, #ef4444); }
.openvibe-navbar .ovnav-orb .ovs-feed-dot { position: absolute; top: 2px; right: 2px; width: 7px; height: 7px; border-radius: 50%; background: var(--accent-light, var(--accent, #60a5fa)); }
.openvibe-navbar .ovnav-orb .ovs-feed-dot[hidden] { display: none; }
.ovs-tabdot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--live-red, #ef4444); box-shadow: 0 0 0 3px color-mix(in srgb, var(--live-red, #ef4444) 22%, transparent); }
/* 4.237.0: one badge for every count in the menu (the You tab's, Feed's, Notifications'), on the icon's corner; the live
dot after the word, breathing. */
.ovs-tab-ic > .ovs-tabnew { position: absolute; top: -7px; left: calc(100% - 8px); z-index: 1; }
.ovs-tab span > .ovs-tabdot { display: inline-block; margin: 0 0 1px 5px; vertical-align: middle; animation: ovsLiveDot 2s ease-in-out infinite; }
.ovs-tab > span:not(.ovs-tab-ic) { display: inline-flex; align-items: center; }
/* 4.238.0, round two (the owner, 11-13: "the icons aren't big enough, not even nearly"): the place tabs' glyphs grow,
24 px beside the word and 30 px over it on a phone. */
.ovs-starttabs .ovs-tab .ovs-tab-ic, .ovs-starttabs .ovs-tab .ovs-tab-ic .ov-icon { width: 24px; height: 24px; }
.ovs-starttabs .ovs-tab .ovs-tab-ic > svg, .ovs-starttabs .ovs-tab[data-dock] .ovs-tab-ic > svg { width: 24px; height: 24px; }
.ovs-starttabs .ovs-tab .ovs-tab-ic .ov-icon svg { width: 44px; height: 44px; margin: -10px; }
@media (max-width: 640px) {
.ovs-starttabs.has-doors .ovs-tab { height: 54px; gap: 2px; font-size: 12px; }
.ovs-starttabs.has-doors { padding-bottom: 6px; }
.ovs-starttabs.has-doors .ovs-tab .ovs-tab-ic, .ovs-starttabs.has-doors .ovs-tab .ovs-tab-ic .ov-icon { width: 30px; height: 30px; }
.ovs-starttabs.has-doors .ovs-tab .ovs-tab-ic > svg, .ovs-starttabs.has-doors .ovs-tab[data-dock] .ovs-tab-ic > svg { width: 30px; height: 30px; }
.ovs-starttabs.has-doors .ovs-tab .ovs-tab-ic .ov-icon svg { width: 54px; height: 54px; margin: -12px; }
}
@keyframes ovsLiveDot { 50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--live-red, #ef4444) 10%, transparent); } }
@keyframes ovsBadgeIn { from { transform: scale(.5); opacity: 0; } }
html[data-ov-motion=off] .ovs-tab span > .ovs-tabdot, html[data-ov-motion=off] .ovs-tabnew { animation: none; }
.ovs-grid.is-mini { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 2px; padding: 0 2px 4px; }
.ovs-grid.is-mini .ovs-tile { gap: 8px; min-height: 40px; padding: 4px 8px; border-radius: 10px; }
.ovs-grid.is-mini .ovs-ic { width: 26px; height: 26px; border-radius: 8px; }
.ovs-grid.is-mini .ovs-tile b { font-size: 13.5px; font-weight: 650; }
/* 4.189.0: the sites in their groups are full touch targets with a larger mark, and each group's name runs into a
hairline, so the list reads as sections and not as one long column of small rings. */
.ovs-grid.is-mini { gap: 4px; }
.ovs-grid.is-mini .ovs-tile { min-height: 48px; padding: 6px 8px; border-radius: 12px; transition: background-color .15s, transform .12s; }
/* 4.222.0: a site's whole name, never "Downlo…" or "Commu…": a long name takes a second line instead of an ellipsis. */
.ovs-grid.is-mini .ovs-tile b { white-space: normal; text-overflow: clip; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.15; overflow-wrap: anywhere; }
.ovs-grid.is-mini .ovs-tile:active { transform: scale(.97); }
.ovs-grid.is-mini .ovs-ic { width: 32px; height: 32px; border-radius: 10px; }
/* 4.202.0: on a phone a site's name sits under its mark, so the whole name shows (Download, Community: not "Downlo…"). */
@media (max-width: 480px) { .ovs-grid.is-mini .ovs-tile { flex-direction: column; justify-content: center; gap: 5px; min-height: 64px; padding: 6px 2px 5px; text-align: center; } .ovs-grid.is-mini .ovs-tile b { max-width: 100%; font-size: 12.5px; } }
.ovs-group[data-sec="all"] { display: flex; align-items: center; gap: 10px; }
/* 4.223.0 (the owner, 11-12: "the icons are so small, they're hard to see … make the icons bigger so they actually take up
more of the square … fix the alignment"): a site's icon is an app icon in every tab — its own hue on a rounded square,
the glyph at 58% of it, placed absolutely so no hover transform can move it off centre. */
.ovs .ovs-ic.ovs-site { position: relative; overflow: hidden; border-radius: 30%; background: linear-gradient(150deg, hsl(var(--ovs-h, 215) 72% 58% / .36), hsl(var(--ovs-h, 215) 70% 38% / .2)); box-shadow: inset 0 0 0 1px hsl(var(--ovs-h, 215) 70% 64% / .3); }
.ovs .ovs-ic.ovs-site .ovi-ring, .ovs-ic.ovs-site .ovi-bg, .ovs-ic.ovs-site .ovi-comet, .ovs-ic.ovs-site .ovi-prog { display: none; }
.ovs .ovs-ic.ovs-site > .ov-icon { position: absolute; inset: 0; width: 100%; height: 100%; display: block; --ovi-glyph: hsl(var(--ovs-h, 215) 92% 84%); color: hsl(var(--ovs-h, 215) 92% 84%); }
:root[data-theme="light"] .ovs .ovs-ic.ovs-site > .ov-icon { --ovi-glyph: hsl(var(--ovs-h, 215) 70% 30%); color: hsl(var(--ovs-h, 215) 70% 30%); }
.ovs .ovs-ic.ovs-site > .ov-icon svg { position: absolute; left: -13%; top: -13%; width: 126%; height: 126%; margin: 0; transform: none !important; }
/* The site list: a grid of app icons, four across on a phone and as many as fit beyond, names on two balanced lines. */
.ovs-grid.is-mini { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2px; padding: 2px 0 6px; }
@media (min-width: 560px) { .ovs-grid.is-mini { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); } }
.ovs-grid.is-mini .ovs-tile { flex-direction: column; justify-content: flex-start; align-items: center; gap: 7px; min-height: 0; padding: 6px 0 8px; border-radius: 16px; text-align: center; background: transparent; }
.ovs-grid.is-mini .ovs-tile > span:last-child { max-width: 100%; }
.ovs-grid.is-mini .ovs-ic { width: min(100% - 14px, 64px); height: auto; aspect-ratio: 1; border-radius: 29%; transition: transform .22s cubic-bezier(.2, 1.4, .4, 1), box-shadow .22s; }
.ovs-grid.is-mini .ovs-tile b { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; max-width: 100%; white-space: normal; text-overflow: clip; font-size: 12px; font-weight: 650; line-height: 1.2; text-wrap: balance; }
.ovs-grid.is-mini .ovs-tile.is-here { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 35%, transparent); }
.ovs-grid.is-mini .ovs-tile:active .ovs-ic { transform: scale(.93); transition-duration: .08s; }
@media (hover: hover) { .ovs-grid.is-mini .ovs-tile:hover { background: transparent; } .ovs-grid.is-mini .ovs-tile:hover .ovs-ic { transform: translateY(-3px) scale(1.04); box-shadow: inset 0 0 0 1px hsl(var(--ovs-h, 215) 75% 70% / .5), 0 12px 26px -8px hsl(var(--ovs-h, 215) 75% 45% / .6); } }
html[data-ov-motion=off] .ovs-grid.is-mini .ovs-ic { transition: none; }
/* Each section of sites wears its glyph. */
.ovs-group-ic { order: -1; flex: none; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 8px; background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }
.ovs-group-ic svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2.1; stroke-linecap: round; stroke-linejoin: round; }
/* Names in the brand face (openvibe.design rule: a place's or an app's name in OV Brand 800; words you read in the UI
face): the site list, the tabs, Vibe's switch. */
.ovs-grid.is-mini .ovs-tile b, .ovs-starttabs .ovs-tab { font-family: 'OV Brand', Nunito, Inter, -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 800; letter-spacing: .005em; }
.ovs-grid.is-mini .ovs-tile b { font-size: 13px; color: color-mix(in srgb, var(--text-primary, #e6edf7) 92%, transparent); }
.ovs-group[data-sec="all"] { font-family: 'OV Brand', Nunito, Inter, -apple-system, BlinkMacSystemFont, sans-serif; font-size: 13.5px; font-weight: 800; letter-spacing: .01em; text-transform: none; color: var(--text-primary, #e6edf7); }
/* A slow light drifts behind the menu (Calm slows it, only Off stops it; openvibe.design: calm reduces, only off removes). */
/* 4.223.1: the panel clips its light (overflow: clip is never a scroll container; hidden was, and a focus inside it
scrolled the whole panel up, taking the search field and the tabs off the screen). */
.ovs-panel { isolation: isolate; overflow: clip; }
.ovs-panel::before { content: ""; position: absolute; inset: -20%; z-index: -1; pointer-events: none; opacity: .5;
background: radial-gradient(38% 30% at 20% 10%, color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent), transparent 70%), radial-gradient(34% 28% at 85% 30%, color-mix(in srgb, #a855f7 16%, transparent), transparent 70%), radial-gradient(40% 32% at 50% 95%, color-mix(in srgb, #14b8a6 12%, transparent), transparent 70%);
animation: ovs-drift 26s ease-in-out infinite alternate; }
@keyframes ovs-drift { from { transform: translate3d(-3%, -2%, 0) rotate(0deg); } to { transform: translate3d(3%, 2%, 0) rotate(8deg); } }
html[data-ov-motion=reduced] .ovs-panel::before { animation-duration: 60s; }
html[data-ov-motion=off] .ovs-panel::before { animation: none; }
:root[data-theme="light"] .ovs-panel::before { opacity: .35; }
.ovs-group[data-sec="all"]::after { content: ""; flex: 1; height: 1px; background: var(--border, rgba(255,255,255,.1)); }
.ovs-tabs[hidden] { display: none; }
.ovs-tab { flex: none; height: 32px; padding: 0 14px; border: 0; border-radius: 999px; background: transparent; color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13.5px; font-weight: 650; cursor: pointer; }
.ovs-tab[aria-selected="true"] { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, var(--accent, #8ab4ff)); }
.ovs-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
.ovs-typing .ovs-bar { border-bottom: 0; }
/* Opening, the groups rise in one after another (not while typing: every key redraws the list). */
.ovs:not(.ovs-typing) .ovs-list > li { animation: ovs-rise .32s cubic-bezier(.2, 1.2, .3, 1) both; }
.ovs-list > li:nth-child(2) { animation-delay: 25ms; } .ovs-list > li:nth-child(3) { animation-delay: 50ms; } .ovs-list > li:nth-child(4) { animation-delay: 75ms; }
.ovs-list > li:nth-child(5) { animation-delay: 100ms; } .ovs-list > li:nth-child(6) { animation-delay: 125ms; } .ovs-list > li:nth-child(n+7) { animation-delay: 150ms; }
@keyframes ovs-rise { from { opacity: 0; transform: translateY(6px); } }
.ovs-list { list-style: none; margin: 0; padding: 6px; overflow-y: auto; overscroll-behavior: contain; }
/* A place of the Dock inside the menu (312 step 2, a phone): it fills the sheet under the head and scrolls itself. */
.ovs-door { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.ovs-door[hidden] { display: none; }
.ovs-door-host { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
/* A place's actions in the search pill (4.200.0); the panel's own row of them is hidden inside the menu. */
.ovs-acts { display: flex; flex: none; gap: 2px; }
.ovs-acts:empty, .ovs-door-host .ovdk-bar { display: none; }
/* One UI (4.220.0; the owner, 11-12: "part of the one UI instead of having two different areas"): inside the menu a place has
no head of its own. A conversation keeps a slim way back and its name; its actions are the search pill's; its mark is the
greeting's; no band, no border. */
.ovs-door-host .ovdk-thread-h { min-height: 40px; padding: 2px 12px 2px 6px; gap: 4px; border-bottom: 0; }
.ovs-door-host .ovdk-thread-h > .ovdk-ib, .ovs-door-host .ovdk-vibe-h .ovdk-vibe-mark, .ovs-door-host .ovdk-vibe-name small { display: none; }
.ovs-door-host .ovdk-thread-h .ovdk-back, .ovs-door-host .ovdk-vibe-h .ovdk-vibe-back { width: 36px; height: 36px; min-width: 36px; min-height: 36px; }
.ovs-door-host .ovdk-thread-h b, .ovs-door-host .ovdk-vibe-name b { font-size: 14px; font-weight: 700; color: var(--text-secondary, #b4c0d4); }
.ovs-door-host .ovdk-vibe-start { padding-top: 2px; }
.ovs-act { width: 40px; height: 40px; display: grid; place-items: center; border: 0; border-radius: 50%; background: transparent; color: var(--text-secondary, #b6b6c0); cursor: pointer; }
.ovs-act svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; }
.ovs-act:hover, .ovs-act:focus-visible { background: var(--hover-bg, rgba(127,127,127,.14)); color: var(--text-primary, #ececf1); }
.ovs-act.is-new { background: color-mix(in srgb, var(--accent, #3b82f6) 18%, transparent); color: var(--accent-light, var(--accent, #60a5fa)); }
.ovs-door-host > .ovdk { flex: 1 1 auto; min-height: 0; }
/* 4.225.0: Vibe's runs fill the place under the menu's head (ov-run.js keeps its own filters, thread and box). */
.ovs-door-host > .ovr { flex: 1 1 auto; min-height: 0; padding: 8px 10px 10px; box-sizing: border-box; }
.ovs-door-wait { margin: 18px 16px; font-size: 14px; color: var(--text-muted, #93a0b4); }
@media (min-width: 641px) { .ovs.ovs-in-door .ovs-panel { height: min(820px, 86vh); max-height: 86vh; } .ovs.ovs-in-door .ovs-foot { display: none; } }
.ovs-in-door .ovs-foot { display: none; }
.ovs-group { display: flex; align-items: center; gap: 8px; padding: 10px 10px 4px; font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted, #93a0b4); }
.ovs-group button { margin-left: auto; border: 0; background: none; color: var(--accent-light, #8ab4ff); font: inherit; font-size: 11.5px; font-weight: 650; text-transform: none; letter-spacing: 0; cursor: pointer; }
.ovs-item a { display: flex; align-items: center; gap: 12px; min-height: 46px; padding: 5px 10px; border-radius: 12px; color: inherit; text-decoration: none; }
.ovs-opt.is-active, .ovs-opt:focus-visible { background: var(--bg-hover, rgba(255,255,255,.07)); outline: none; }
/* A tap leaves :hover on what it touched: rows light on hover only where a pointer really hovers. */
@media (hover: hover) { .ovs-item a:hover, .ovs-chip:hover, .ovs-tile:hover, .ovs-x:hover, .ovs-tab:hover { background: var(--bg-hover, rgba(255,255,255,.07)); color: inherit; } }
.ovs-ic { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 10px; background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); color: var(--accent-light, #8ab4ff); font-size: 12px; font-weight: 800; }
.ovs-ic > svg { width: 16px; height: 16px; }
.ovs-ic.ovs-site { background: none; }
.ovs-thumb { flex: none; width: 72px; height: 40px; border-radius: 9px; object-fit: cover; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); }
.ovs-thumb--ico { display: grid; place-items: center; }
.ovs .ov-icon:empty { border-radius: 50%; box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent, #3b82f6) 26%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 10%, transparent); }
.ovs-main { flex: 1; min-width: 0; display: grid; gap: 1px; }
.ovs-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 600; }
.ovs-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-muted, #93a0b4); }
.ovs-key { flex: none; font-size: 11px; color: var(--text-muted, #93a0b4); }
.ovs-chips { display: flex; flex-wrap: wrap; gap: 6px; padding: 2px 8px 6px; }
.ovs-chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; height: 34px; padding: 0 13px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); color: inherit; text-decoration: none; font-size: 13.5px; font-weight: 600; box-sizing: border-box; }
.ovs-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-chip svg { width: 14px; height: 14px; flex: none; color: var(--text-muted, #93a0b4); }
.ovs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 4px; padding: 2px 4px 6px; }
.ovs-grid.is-sites { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
.ovs-tile { display: flex; align-items: center; gap: 10px; min-width: 0; padding: 8px 10px; border-radius: 12px; border: 1px solid transparent; color: inherit; text-decoration: none; }
.ovs-tile > span:last-child { min-width: 0; }
.ovs-tile b { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; font-weight: 650; }
.ovs-tile small { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 11.5px; line-height: 1.3; color: var(--text-muted, #93a0b4); }
.ovs-tile.is-here { border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); }
.ovs-note { padding: 14px 12px; font-size: 13px; color: var(--text-muted, #93a0b4); }
/* The menu before typing (4.128.0): the places dock, this site's pages, the live bar, counted folds. */
.ovs-apps { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 2px; padding: 2px 2px 6px; }
.ovs-app { display: flex; flex-direction: column; align-items: center; gap: 7px; min-width: 0; padding: 10px 2px 8px; border-radius: 16px; color: inherit; text-decoration: none; }
.ovs .ovs-appic { width: 54px; height: 54px; border-radius: 17px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 6%, transparent); box-shadow: inset 0 0 0 1px var(--border, rgba(255,255,255,.1)); transition: transform .18s cubic-bezier(.2,1.4,.4,1), box-shadow .18s; }
.ovs-appname { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; font-weight: 650; color: var(--text-secondary, #a8b3c4); }
/* This site's pages: named as such, in the case they are written in, behind a dot that says "you are here"; each a tile. */
.ovs-group[data-sec="here"] { gap: 9px; padding-top: 12px; font-size: 13.5px; font-weight: 750; letter-spacing: 0; text-transform: none; color: var(--text-secondary, #a8b3c4); }
.ovs-group[data-sec="here"]::before { content: ""; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--accent-light, var(--accent, #60a5fa)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, #3b82f6) 22%, transparent); animation: ovs-here 2.4s ease-in-out infinite; }
@keyframes ovs-here { 50% { box-shadow: 0 0 0 calc(4px + 3px * var(--ov-move, 1)) color-mix(in srgb, var(--accent, #3b82f6) 8%, transparent); } }
.ovs-links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; padding: 2px 4px 6px; }
.ovs-link { display: flex; align-items: center; gap: 10px; min-width: 0; min-height: 46px; padding: 0 12px; border-radius: 12px; color: inherit; text-decoration: none; font-size: 14px; font-weight: 600; background: color-mix(in srgb, var(--text-primary, #e6edf7) 4.5%, transparent); transition: background-color .15s, transform .12s; }
.ovs-link:hover { background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); }
.ovs-link:active { transform: scale(.97); }
.ovs-link svg { width: 16px; height: 16px; flex: none; color: var(--text-muted, #93a0b4); }
.ovs-link span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-link.is-here { background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 42%, transparent); color: var(--text-primary, #e6edf3); }
.ovs-link.is-here svg { color: var(--accent, #3b82f6); }
.ovs-livebar { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding: 8px 6px 2px; }
.ovs-livebar::-webkit-scrollbar { display: none; }
.ovs-live { flex: none; display: inline-flex; align-items: center; gap: 8px; height: 44px; padding: 0 16px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); color: inherit; text-decoration: none; font-size: 13.5px; font-weight: 650; }
.ovs-live.is-count { border-color: color-mix(in srgb, var(--danger, #ef4444) 45%, transparent); background: color-mix(in srgb, var(--danger, #ef4444) 10%, transparent); }
.ovs-live small { color: var(--text-muted, #93a0b4); font-size: 12px; font-weight: 600; }
.ovs-livedot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--danger, #ef4444); animation: ovs-livedot 1.6s ease-in-out infinite; }
@keyframes ovs-livedot { 50% { opacity: .4; transform: scale(.75); } }
.ovs-foldrow { padding: 6px 4px 2px; }
.ovs-fold { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px; padding: 0 12px; border-radius: 12px; border: 1px dashed var(--border, rgba(255,255,255,.16)); background: transparent; color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 13.5px; font-weight: 650; text-align: left; cursor: pointer; }
/* 4.201.0's look: Notifications' head in You; the Timer under Vibe; the Dock's actions in You. */
.ovs-nhead { display: flex; align-items: center; gap: 8px; padding: 10px 14px 6px; }
.ovs-nhead b { font-size: 13px; font-weight: 750; letter-spacing: .04em; text-transform: uppercase; color: var(--text-secondary, #a8b3c4); }
.ovs-nhead-n { padding: 2px 8px; border-radius: 999px; background: var(--live-red, #ef4444); color: #fff; font-size: 11.5px; font-weight: 750; }
.ovs-nhead-n[hidden], .ovs-nhead-more[hidden] { display: none; }
.ovs-nhead-more { margin-left: auto; min-height: 32px; padding: 0 10px; border: 0; border-radius: 9px; background: none; color: var(--accent-light, var(--accent, #60a5fa)); font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; }
.ovs-nhead-more:hover, .ovs-nhead-more:focus-visible { background: color-mix(in srgb, var(--accent, #3b82f6) 14%, transparent); }
.ovs-fold svg { width: 16px; height: 16px; flex: none; transition: transform .2s; }
.ovs-feeds { display: grid; gap: 4px; padding: 4px 6px 8px; }
.ovs-feed { display: grid; grid-template-columns: minmax(0, 1fr) 40px 40px; align-items: center; gap: 4px; padding: 4px 4px 4px 10px; border-radius: 12px; background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); }
.ovs-feed label { display: flex; align-items: center; gap: 12px; min-height: 44px; min-width: 0; cursor: pointer; }
.ovs-feed input { width: 18px; height: 18px; flex: none; accent-color: var(--accent, #3b82f6); }
.ovs-feed label span { display: grid; min-width: 0; }
.ovs-feed b { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-feed small { font-size: 12px; color: var(--text-muted, #8b95a7); text-transform: capitalize; }
.ovs-feed button { width: 40px; height: 40px; border: 0; border-radius: 10px; background: transparent; color: var(--text-secondary, #a8b3c4); font: inherit; font-size: 16px; cursor: pointer; }
.ovs-feed button:hover:not(:disabled) { background: color-mix(in srgb, var(--text-primary, #e8ecf3) 8%, transparent); }
.ovs-feed button:disabled { opacity: .3; cursor: default; }
.ovs-feeds-note { margin: 2px 6px 0; font-size: 12px; color: var(--text-muted, #8b95a7); }
.ovs-fold[aria-expanded="true"] svg { transform: rotate(90deg); }
.ovs-fold:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
.ovs-tag { margin-left: 7px; padding: 1px 7px; border-radius: 999px; font-style: normal; font-size: 10.5px; font-weight: 800; letter-spacing: .03em; vertical-align: 1px; color: color-mix(in srgb, var(--accent, #3b82f6) 72%, var(--text-primary, #e8ecf3)); background: color-mix(in srgb, var(--accent, #3b82f6) 15%, transparent); }
.ovs-card { padding: 2px 6px 6px; }
.ovs-cardbody { border-radius: 16px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: color-mix(in srgb, var(--text-primary, #e8ecf3) 4%, transparent); padding: 12px 14px; }
.ovs-displayrow { padding: 4px 8px 10px; }
.ovs-displayrow .ovnav-display { margin-top: 4px; }
.ovs-host { display: block; margin-top: 2px; font-size: 11px; font-weight: 650; letter-spacing: .01em; color: color-mix(in srgb, var(--accent, #3b82f6) 60%, var(--text-muted, #93a0b4)); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ovs-group .ovs-more { margin-left: auto; color: var(--accent-light, #8ab4ff); font-size: 11.5px; font-weight: 650; text-transform: none; letter-spacing: 0; text-decoration: none; }
@media (hover: hover) { .ovs-app:hover, .ovs-link:hover, .ovs-live:hover, .ovs-fold:hover { background: var(--bg-hover, rgba(255,255,255,.07)); color: inherit; } .ovs-app:hover .ovs-appic { transform: translateY(-2px); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); } }
@media (max-width: 640px) { .ovs-apps { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (pointer: coarse) { .ovs-chip { height: 44px; } .ovs-group button, .ovs-group .ovs-more { min-height: 44px; display: inline-flex; align-items: center; } }
.ovs-foot { display: flex; gap: 14px; padding: 8px 16px; border-top: 1px solid var(--border, rgba(255,255,255,.1)); font-size: 11.5px; color: var(--text-muted, #93a0b4); }
.ovs-foot kbd { font: inherit; font-weight: 700; padding: 1px 5px; border-radius: 5px; border: 1px solid var(--border, rgba(255,255,255,.14)); }
.ovs-busy .ovs-bar { background-image: linear-gradient(90deg, transparent, color-mix(in srgb, var(--accent, #3b82f6) 40%, transparent), transparent); background-size: 40% 2px; background-repeat: no-repeat; background-position: -40% 100%; animation: ovs-scan 1s linear infinite; }
.ovs-snip { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 2px; font-size: 13px; line-height: 1.4; color: var(--text-secondary, #a8b3c4); white-space: normal; }
.ovs-results-on .ovs-item a { align-items: flex-start; padding: 9px 10px; }
.ovs-results-on .ovs-ic { margin-top: 1px; text-transform: lowercase; }
.ovs-rtabs { overflow-x: auto; scrollbar-width: none; padding-top: 8px; }
.ovs-rtabs::-webkit-scrollbar { display: none; }
.ovs-rtabs .ovs-tab[hidden] { display: none; }
.ovs-skels { display: grid; gap: 6px; padding: 4px 6px 8px; }
.ovs-skel { display: block; height: 52px; border-radius: 12px; background: linear-gradient(90deg, var(--bg-hover, rgba(255,255,255,.05)) 0%, color-mix(in srgb, var(--bg-hover, rgba(255,255,255,.05)) 40%, transparent) 50%, var(--bg-hover, rgba(255,255,255,.05)) 100%); background-size: 200% 100%; animation: ovs-shimmer 1.3s linear infinite; }
.ovs-route { padding: 10px 14px 6px; font-size: 12px; color: var(--text-muted, #93a0b4); }
@keyframes ovs-shimmer { to { background-position: -200% 0; } }
@keyframes ovs-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes ovs-scan { to { background-position: 140% 100%; } }
@media (hover: none), (max-width: 640px) { .ovs-foot, .ovs-key { display: none; } }
@media (max-width: 640px) {
.ovs-panel { top: 0; left: 0; width: 100vw; height: 100dvh; max-height: none; transform: none; border-radius: 0; border: 0; }
.ovs-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ovs-grid.is-sites { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ovs-grid:not(.is-sites) .ovs-tile small { -webkit-line-clamp: 1; }
}
/* Dropped from the navbar's orb (4.25.0): under the bar, from the orb's side, the bar still showing. */
.ovs.ovs-anchored { top: var(--ovs-top, 0px); background: rgba(3, 6, 12, .45); }
/* It grows out of the button that was pressed (4.195.0; the owner: "go … to the proper element … what you clicked on"): its
origin is that button's centre, wherever the panel was clamped to (it unfolded from its left corner, though the button is at
the right since 4.193.0), on the dropdown's own curve. */
.ovs.ovs-anchored .ovs-panel { --ovs-pl: clamp(8px, var(--ovs-left, 12px), calc(100vw - min(720px, calc(100vw - 16px)) - 8px)); top: 8px; left: var(--ovs-pl); transform: none; transform-origin: calc(var(--ovs-cx, 40px) - var(--ovs-pl)) 0; max-height: calc(100dvh - var(--ovs-top, 0px) - 24px); animation: ovs-drop .26s cubic-bezier(.2, 1.25, .3, 1); }
@keyframes ovs-drop { from { opacity: 0; transform: translateY(-6px) scale(.9); } to { opacity: 1; transform: none; } }
@media (max-width: 640px) {
/* The whole screen on a phone (the owner, 11-07: "make full use of the height, not compensate for the navbar"),
opening as a circle that grows from the button that opened it. */
.ovs.ovs-anchored { top: 0; }
.ovs.ovs-anchored .ovs-panel { top: 0; left: 0; width: 100vw; height: 100dvh; max-height: none; border-radius: 0; border: 0; padding-top: env(safe-area-inset-top, 0px); animation: ovs-reveal .38s cubic-bezier(.2,.9,.25,1) both; }
}
@keyframes ovs-reveal { from { clip-path: circle(28px at var(--ovs-cx, 40px) var(--ovs-cy, 28px)); } to { clip-path: circle(150% at var(--ovs-cx, 40px) var(--ovs-cy, 28px)); } }
@keyframes ovs-fold { from { clip-path: circle(150% at var(--ovs-cx, 40px) var(--ovs-cy, 28px)); } to { clip-path: circle(20px at var(--ovs-cx, 40px) var(--ovs-cy, 28px)); } }
@keyframes ovs-fade { to { opacity: 0; } }
/* The orb, on top of the phone's menu, in the button's place (4.150.0): it glides into the search pill as the menu grows
from it, its nine dots turning into an x (the corners and the centre stay, the edges fold away); closing, it glides
back and the menu folds into it. */
/* 4.238.0, the top as a composer: the lead (your Actor's face with its state), the tools inside the pill, the Actor's card
under it; the close orb beside the pill on a phone, a solid disc with a bold x. */
.ovs-lead { all: unset; box-sizing: border-box; position: relative; flex: none; display: grid; place-items: center; width: 36px; height: 36px; margin-left: -4px; border-radius: 50%; color: var(--text-muted, #93a0b4); }
.ovs-lead > svg { width: 20px; height: 20px; }
.ovs-lead-on { position: absolute; right: 2px; bottom: 2px; width: 10px; height: 10px; border-radius: 50%; background: #22c55e; box-shadow: 0 0 0 2px var(--bg-secondary, #121826); }
.ovs-lead.is-you { cursor: pointer; height: 40px; -webkit-tap-highlight-color: transparent; transition: transform .18s cubic-bezier(.3, 1.4, .4, 1); }
.ovs-lead.is-you:active { transform: scale(.92); }
.ovs-lead.is-you:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #60a5fa)); outline-offset: 2px; }
.ovs-lead-face { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 50%; overflow: hidden; font: 800 14px/1 Inter, system-ui, sans-serif; color: #fff;
background: linear-gradient(135deg, var(--accent, #3b82f6), color-mix(in oklab, var(--accent, #3b82f6) 50%, #a855f7)); box-shadow: 0 0 0 2px var(--bg-secondary, #121826); }
.ovs-lead-face img { width: 100%; height: 100%; object-fit: cover; }
.ovs-lead-me { position: relative; flex: none; display: grid; place-items: center; z-index: 4; }
.ovs-lead.is-you { display: inline-flex; align-items: center; width: auto; min-width: 40px; padding: 0 2px; }
/* What runs for you, stacked behind your picture: each a small face with its state on a ring (a comet while it works,
amber while it waits for you). They fan out from behind you as the menu opens. */
.ovs-stack { display: inline-flex; align-items: center; margin-left: -4px; padding-right: 5px; }
.ovs-stack-f, .ovs-stack-more { position: relative; flex: none; display: grid; place-items: center; width: 24px; height: 24px; margin-left: -5px; border-radius: 50%; z-index: calc(3 - var(--i, 0));
color: #fff; background: linear-gradient(140deg, hsl(var(--h, 220) 70% 56%), hsl(calc(var(--h, 220) + 40) 65% 40%)); box-shadow: 0 0 0 2px var(--bg-secondary, #121826);
animation: ovs-fan .42s cubic-bezier(.2, 1.3, .3, 1) both; animation-delay: calc(90ms + var(--i, 0) * 70ms); }
.ovs-stack-f:first-child { margin-left: 0; }
.ovs-stack-f img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovs-stack-f svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2.1; stroke-linecap: round; stroke-linejoin: round; }
.ovs-stack-more { align-self: flex-start; width: auto; min-width: 17px; height: 17px; margin: -3px 0 0 -9px; padding: 0 4px; box-sizing: border-box; border-radius: 999px; z-index: 5; background: var(--text-primary, #e6edf7); color: var(--bg-primary, #0b0f19); font: 800 10px/17px Inter, system-ui, sans-serif; letter-spacing: -.02em; }
@keyframes ovs-fan { from { opacity: 0; transform: translateX(-16px) scale(.5); } }
.ovs-stack-f.is-working::after, .ovs-active-f.is-working::after { content: ""; position: absolute; inset: -3px; border-radius: 50%; pointer-events: none;
background: conic-gradient(from 0deg, transparent 0 50%, color-mix(in srgb, var(--accent-light, #60a5fa) 70%, transparent) 80%, #fff 100%);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px)); mask: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2px));
animation: ovs-comet 1.25s linear infinite; }
@keyframes ovs-comet { to { transform: rotate(360deg); } }
.ovs-stack-f.is-asks, .ovs-active-f.is-asks { box-shadow: 0 0 0 2px var(--bg-secondary, #121826), 0 0 0 4px #f59e0b; }
.ovs-stack-f.is-asks { animation: ovs-fan .42s cubic-bezier(.2, 1.3, .3, 1) both, ovs-asks 2.2s ease-in-out 1s infinite; animation-delay: calc(90ms + var(--i, 0) * 70ms), 1s; }
@keyframes ovs-asks { 50% { box-shadow: 0 0 0 2px var(--bg-secondary, #121826), 0 0 0 4px #f59e0b, 0 0 0 8px color-mix(in srgb, #f59e0b 22%, transparent); } }
html[data-ov-motion=reduced] .ovs-stack-f.is-working::after, html[data-ov-motion=reduced] .ovs-active-f.is-working::after { animation-duration: 2.6s; }
html[data-ov-motion=reduced] .ovs-stack-f, html[data-ov-motion=reduced] .ovs-stack-more { animation-duration: .2s; animation-delay: 0s; }
html[data-ov-motion=off] .ovs-stack-f, html[data-ov-motion=off] .ovs-stack-more { animation: none; }
html[data-ov-motion=off] .ovs-stack-f.is-working::after, html[data-ov-motion=off] .ovs-active-f.is-working::after { animation: none; background: var(--accent-light, #60a5fa); opacity: .8; }
/* Your sheet's rows of what is running. */
.ovs-active { display: grid; gap: 4px; }
.ovs-active-h { margin: 2px 6px 0; font: 800 11.5px/1.4 'OV Brand', Nunito, Inter, system-ui, sans-serif; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted, #7c8aa0); }
.ovs-active-row { all: unset; box-sizing: border-box; display: flex; align-items: center; gap: 12px; width: 100%; min-height: 54px; padding: 6px 10px 6px 8px; border-radius: 16px; cursor: pointer; -webkit-tap-highlight-color: transparent;
background: color-mix(in srgb, var(--text-primary, #e6edf7) 4%, transparent); transition: background-color .15s, transform .15s cubic-bezier(.3, 1.4, .4, 1); }
.ovs-active-row:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #60a5fa)); outline-offset: 1px; }
@media (hover: hover) { .ovs-active-row:hover { background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); } }
.ovs-active-row:active { transform: scale(.985); }
.ovs-active-f { position: relative; flex: none; display: grid; place-items: center; width: 36px; height: 36px; margin: 3px; border-radius: 50%; color: #fff; background: linear-gradient(140deg, hsl(var(--h, 220) 70% 56%), hsl(calc(var(--h, 220) + 40) 65% 40%)); }
.ovs-active-f img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
.ovs-active-f svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ovs-active-t { flex: 1 1 auto; min-width: 0; display: grid; line-height: 1.3; }
.ovs-active-t b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 800 14.5px/1.3 'OV Brand', Nunito, Inter, system-ui, sans-serif; color: var(--text-primary, #e6edf7); }
.ovs-active-t small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; color: var(--text-secondary, #a8b3c4); }
.ovs-active-st { flex: none; font: 750 11.5px/1 Inter, system-ui, sans-serif; font-style: normal; padding: 6px 9px; border-radius: 999px; color: var(--accent-light, #93c5fd); background: color-mix(in srgb, var(--accent, #3b82f6) 16%, transparent); }
.ovs-active-st.is-asks { color: #fbbf24; background: color-mix(in srgb, #f59e0b 18%, transparent); }
/* The sheet's parts rise in, one after the other. */
.ovs-youpanel > * { animation: ovs-rise .3s cubic-bezier(.2, .8, .2, 1) both; }
.ovs-youpanel > :nth-child(2) { animation-delay: .05s; } .ovs-youpanel > :nth-child(3) { animation-delay: .1s; }
@keyframes ovs-rise { from { opacity: 0; transform: translateY(8px); } }
html[data-ov-motion=off] .ovs-youpanel > * { animation: none; }
.ovs-tools { flex: none; display: flex; align-items: center; gap: 4px; }
.ovs-tool { all: unset; box-sizing: border-box; display: grid; place-items: center; width: 38px; height: 38px; border-radius: 50%; cursor: pointer; color: var(--text-secondary, #a8b3c4); -webkit-tap-highlight-color: transparent;
transition: background-color .15s, color .15s, transform .15s cubic-bezier(.3, 1.4, .4, 1); }
.ovs-tool[hidden] { display: none; }
.ovs-tool svg { width: 22px; height: 22px; }
.ovs-tool:focus-visible { outline: 2px solid var(--accent-light, var(--accent, #60a5fa)); outline-offset: 1px; }
@media (hover: hover) { .ovs-tool:hover { color: var(--text-primary, #e6edf7); background: color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent); } }
.ovs-tool:active { transform: scale(.9); }
.ovs-send { color: var(--on-accent, #fff); background: var(--accent, #3b82f6); box-shadow: 0 4px 14px -4px color-mix(in srgb, var(--accent, #3b82f6) 70%, transparent); animation: ovs-send-in .22s cubic-bezier(.3, 1.4, .4, 1); }
@media (hover: hover) { .ovs-send:hover { color: var(--on-accent, #fff); background: var(--accent-hover, color-mix(in oklab, var(--accent, #3b82f6) 85%, #fff)); } }
@keyframes ovs-send-in { from { transform: scale(.5); opacity: 0; } }
.ovs-mic.is-on { color: #fff; background: var(--live-red, #ef4444); animation: ovs-mic 1.2s ease-in-out infinite; }
@keyframes ovs-mic { 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--live-red, #ef4444) 25%, transparent); } }
html[data-ov-motion=off] .ovs-send, html[data-ov-motion=off] .ovs-mic.is-on { animation: none; }
.ovs-panel { position: relative; }
/* The list fills the menu's height (You's notifications put their foot at the bottom: the owner, 11-13: "take up the full height properly"). */
.ovs-panel > .ovs-list { flex: 1 1 auto; min-height: 0; }
.ovs-youpanel { position: absolute; z-index: 6; left: 8px; right: 8px; top: var(--ovs-you-top, 64px); max-height: calc(100% - var(--ovs-you-top, 64px) - 12px); overflow: auto; overscroll-behavior: contain;
display: grid; gap: 8px; padding: 10px; border-radius: 22px; background: var(--bg-elevated, var(--bg-secondary, #141a26));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 9%, transparent), 0 24px 60px -18px rgba(0, 0, 0, .75); transform-origin: 32px 0; animation: ovs-drop .24s cubic-bezier(.2, .8, .2, 1); }
.ovs-youpanel[hidden] { display: none; }
@keyframes ovs-drop { from { opacity: 0; transform: translateY(-8px) scale(.96); } }
html[data-ov-motion=off] .ovs-youpanel { animation: none; }
@media (max-width: 640px) { .ovs-bar { gap: 8px; padding: 0 4px 0 8px; } .ovs-bar .ovs-input::placeholder { font-size: 15px; } .ovs-tools { gap: 0; } .ovs-tool { width: 36px; height: 36px; } }
.ovs-orb { display: none; }
.ovs-foot kbd svg { width: 12px; height: 12px; vertical-align: -1px; }
@media (max-width: 640px) {
.ovs.ovs-anchored .ovs-x { display: none; }
.ovs.ovs-anchored .ovs-bar { margin-right: 58px; }
.ovs.ovs-anchored .ovs-orb { position: fixed; z-index: 3; left: var(--orb-x, calc(100vw - 52px)); top: var(--orb-y, 12px); width: 44px; height: 44px; display: grid; place-items: center; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
background: color-mix(in srgb, var(--accent, #3b82f6) 16%, var(--bg-primary, #0b0f19)); color: var(--text-primary, #e6edf7); -webkit-tap-highlight-color: transparent;
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);
--ovo-a: var(--accent-light, var(--accent, #60a5fa)); --ovo-b: color-mix(in oklab, var(--accent, #3b82f6) 45%, #e879f9);
transform: none; transition: transform .42s cubic-bezier(.2, 1.1, .3, 1), background-color .3s, box-shadow .3s; }
.ovs.ovs-anchored .ovs-orb.is-from { transform: translate(var(--orb-dx, 0px), var(--orb-dy, 0px)) scale(.9); background: transparent; box-shadow: none; }
.ovs.ovs-anchored .ovs-orb::after { content: ""; position: absolute; inset: -4px; }
/* A ring leaves the orb as its sheet opens (4.186.0; the Dock's sheet does the same). */
/* 4.229.0: the ring is invisible at rest, so where animations are off (a phone's "Remove animations", reduced motion)
it never stays as a white circle around the x (the owner, 11-12: "why is it white"). */
.ovs.ovs-anchored .ovs-orb.is-x::before { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 2px solid var(--ovo-a); pointer-events: none; opacity: 0; animation: ovs-ring .6s cubic-bezier(.2,.8,.2,1) .1s both; }
@keyframes ovs-ring { from { opacity: .8; transform: scale(.9); } to { opacity: 0; transform: scale(calc(1 + 2.2 * var(--ov-move, 1))); } }
.ovs.ovs-anchored .ovs-orb:active { transform: scale(.9); }
/* 4.213.0: when the pressed button flew to its tab, the close turns in at the pill once it has left (a quarter turn,
a little past its size); closing with the button flying back, it turns away where it is. */
.ovs.ovs-anchored .ovs-orb.is-pop { animation: ovs-orb-in .38s cubic-bezier(.2, 1.3, .3, 1) .1s both; }
.ovs.ovs-anchored .ovs-orb.is-gone { animation: ovs-orb-out .22s cubic-bezier(.5, 0, .75, .2) both; }
@keyframes ovs-orb-in { from { opacity: 0; transform: scale(.45) rotate(-90deg); } }
@keyframes ovs-orb-out { to { opacity: 0; transform: scale(.5) rotate(90deg); } }
.ovs.ovs-anchored .ovs-orb svg { display: block; overflow: visible; width: 24px; height: 24px; }
.ovs.ovs-anchored .ovs-orb.is-x { background: var(--bg-secondary, #121826); box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary, #e6edf7) 14%, transparent), 0 8px 22px -8px rgba(0, 0, 0, .7); }
.ovs.ovs-anchored .ovs-orb.is-x::before { display: none; }
.ovs.ovs-anchored .ovs-orb { width: 48px; height: 48px; }
.ovs.ovs-anchored .ovs-orb:focus { outline: none; }
.ovs.ovs-anchored .ovs-orb:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--bg-primary, #0b0f19), 0 0 0 4px var(--accent-light, var(--accent, #60a5fa)); }
.ovs.ovs-anchored .ovs-orb.is-x { color: var(--text-primary, #e6edf7);
background: radial-gradient(120% 120% at 30% 20%, color-mix(in srgb, var(--accent, #3b82f6) 26%, var(--bg-secondary, #121826)), var(--bg-secondary, #121826) 70%);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 22%, transparent), 0 10px 24px -10px color-mix(in srgb, var(--accent, #3b82f6) 70%, rgba(0, 0, 0, .8)); }
.ovs.ovs-anchored .ovs-orb.is-x svg { width: 26px; height: 26px; transition: transform .4s cubic-bezier(.3, 1.4, .4, 1); }
.ovs.ovs-anchored .ovs-orb.is-x.is-pop { animation: ovs-orb-spin .5s cubic-bezier(.2, 1.3, .3, 1) .06s both; }
@keyframes ovs-orb-spin { from { opacity: 0; transform: scale(.4) rotate(-180deg); } 60% { opacity: 1; } }
.ovs.ovs-anchored .ovs-orb.is-x:active svg { transform: rotate(90deg) scale(.86); }
html[data-ov-motion=off] .ovs.ovs-anchored .ovs-orb.is-x.is-pop { animation: none; }
@media (hover: hover) { .ovs.ovs-anchored .ovs-orb.is-x:hover svg { transform: rotate(90deg); } }
/* 4.239.0 (the owner, 11-13: "make the close button look slightly 3D and look kind of like glowing … a better effect …
make it look kind of cool"): a lit sphere (a highlight up and to the left, a shade under it, a rim of the accent),
a soft glow around it, a sheen that sweeps across once as the menu opens, a brighter glow on hover, and a press
that sinks it. Nothing moves at rest. */
.ovs.ovs-anchored .ovs-orb.is-x { color: #fff; --ovo-a: #fff; --ovo-b: #fff; /* no overflow clip: ::after is the 4 px wider hit area */
background: radial-gradient(120% 120% at 32% 22%, color-mix(in srgb, #fff 30%, var(--accent-light, #60a5fa)) 0%, color-mix(in srgb, var(--accent, #3b82f6) 72%, #0b1220) 42%, color-mix(in srgb, var(--accent, #3b82f6) 30%, #04060c) 100%);
box-shadow: inset 0 1.5px 0 rgba(255, 255, 255, .42), inset 0 -4px 8px rgba(0, 0, 0, .45), 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 55%, transparent),
0 0 22px -2px color-mix(in srgb, var(--accent, #3b82f6) 75%, transparent), 0 10px 22px -8px rgba(0, 0, 0, .8);
transition: transform .42s cubic-bezier(.2, 1.1, .3, 1), box-shadow .25s, filter .25s; }
.ovs.ovs-anchored .ovs-orb.is-x svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .45)); }
.ovs.ovs-anchored .ovs-orb.is-x::before { display: block; content: ""; position: absolute; inset: 0; border-radius: 50%; border: 0; opacity: 1; pointer-events: none;
background: linear-gradient(115deg, transparent 30%, rgba(255, 255, 255, .55) 48%, transparent 62%) no-repeat; background-size: 260% 100%; background-position: 130% 0;
animation: ovs-orb-sheen .9s cubic-bezier(.3, .6, .3, 1) .38s 1 both; }
@keyframes ovs-orb-sheen { to { background-position: -40% 0; } }
@media (hover: hover) { .ovs.ovs-anchored .ovs-orb.is-x:hover { filter: brightness(1.12); box-shadow: inset 0 1.5px 0 rgba(255, 255, 255, .5), inset 0 -4px 8px rgba(0, 0, 0, .4), 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 70%, transparent), 0 0 30px 0 color-mix(in srgb, var(--accent, #3b82f6) 85%, transparent), 0 10px 22px -8px rgba(0, 0, 0, .8); } }
.ovs.ovs-anchored .ovs-orb.is-x:active { transform: scale(.9); box-shadow: inset 0 3px 8px rgba(0, 0, 0, .55), 0 0 0 1px color-mix(in srgb, var(--accent-light, #60a5fa) 50%, transparent), 0 0 14px -2px color-mix(in srgb, var(--accent, #3b82f6) 60%, transparent); }
html[data-ov-motion=off] .ovs.ovs-anchored .ovs-orb.is-x::before { animation: none; background: none; }
html[data-ov-motion=reduced] .ovs.ovs-anchored .ovs-orb.is-x::before { animation-duration: .5s; }
.ovs.is-closing .ovs-panel { animation: ovs-fold .28s cubic-bezier(.5, 0, .75, .2) both; }
.ovs.is-closing { animation: ovs-fade .28s ease-in both; }
}
`;
document.head.appendChild(st);
}
const ICON = {
up: '',
down: '',
mic: '',
plus: '',
send: '',
search: '',
close: '',
clock: '',
go: '',
spark: '',
back: '',
grid: '',
};
// The orb (navbar.js's drawing, root.OpenVibeOrb): on a phone the menu covers the bar, so the menu draws the orb in the
// button's own place, on top, and it turns into the menu's close (4.150.0, the owner: "it renders on top … turns into
// the close button when it's open … a cool effect as it goes in and out").
const orbSvg = () => (root.OpenVibeOrb ? root.OpenVibeOrb.svg(24) : '');
let orbEl = null, closing = 0;
const phoneSheet = () => { try { return !!(el && el.classList.contains('ovs-anchored') && root.matchMedia('(max-width: 640px)').matches); } 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 calm = () => document.documentElement.getAttribute('data-ov-motion') === 'off';
function build() {
styles();
el = document.createElement('div');
el.className = 'ovs';
el.setAttribute('role', 'dialog');
el.setAttribute('aria-modal', 'true');
el.setAttribute('aria-label', 'Search and every OpenVibe site');
el.innerHTML = `
`
+ '
'
+ `
${RTABS.map(([id, label]) => ``).join('')}
`
+ '
';
input = el.querySelector('.ovs-input');
ghost = el.querySelector('.ovs-ghost');
listEl = el.querySelector('.ovs-list');
doorEl = document.createElement('div'); doorEl.className = 'ovs-door'; doorEl.hidden = true;
// 4.225.0: Vibe is one place (the owner, 11-12, crossing out the Chat · Code · Agents switch: "one new unified
// agent … a shared UI"): every chat, task and coding run in one list with its own filters, drawn by ov-run.js.
doorHost = document.createElement('div'); doorHost.className = 'ovs-door-host';
doorEl.append(doorHost);
listEl.after(doorEl);
doorHost.addEventListener('openvibe:open-vibe', (e) => {
if (door !== 'chats' || !canDoor()) return;
e.preventDefault();
openDoor('vibe');
});
tabsEl = el.querySelector('.ovs-tabs');
actsEl = el.querySelector('.ovs-acts');
rtabsEl = el.querySelector('.ovs-rtabs');
rtabsEl.addEventListener('click', (e) => { const b = e.target.closest && e.target.closest('[data-rtab]'); if (b) { showResults(rq, b.getAttribute('data-rtab')); input.focus(); } });
el.addEventListener('mousedown', (e) => { if (e.target === el) close(); });
el.querySelector('.ovs-x').addEventListener('click', close);
wireTop();
// 4.223.1: the panel never scrolls as a whole (only its list does); an engine that still scrolls it on a focus
// is put back at once, so the search field and the tabs stay in place.
{ const panel = el.querySelector('.ovs-panel'); if (panel) panel.addEventListener('scroll', () => { if (panel.scrollTop || panel.scrollLeft) { panel.scrollTop = 0; panel.scrollLeft = 0; } }, { passive: true }); }
orbEl = document.createElement('button');
orbEl.type = 'button'; orbEl.className = 'ovs-orb'; orbEl.setAttribute('aria-label', 'Close the menu'); orbEl.title = 'Close (Esc)';
orbEl.innerHTML = orbSvg();
orbEl.addEventListener('click', close);
el.appendChild(orbEl);
tabsEl.addEventListener('click', (e) => {
const b = e.target.closest && e.target.closest('[data-tab]'); if (!b) return;
leaveDoor();
tab = b.getAttribute('data-tab'); if (tab === 'feed') markFeedSeen(); input.value = ''; render(''); listEl.scrollTop = 0;
let touch = false; try { touch = root.matchMedia('(pointer: coarse)').matches; } catch { /* */ }
if (touch) b.focus(); else input.focus();
});
document.addEventListener('openvibe-notifications-count', () => { if (el && el.classList.contains('is-open')) paintTabs(); });
input.addEventListener('input', () => { moved = false; if (view === 'results') leaveResults(); paintGhost(); schedule(input.value); });
input.addEventListener('keydown', onKey);
listEl.addEventListener('click', (e) => {
const clear = e.target.closest && e.target.closest('[data-ovs-clear]');
if (clear) { e.preventDefault(); recent.clear(); render(input.value); input.focus(); return; }
const fold = e.target.closest && e.target.closest('[data-ovs-fold]');
if (fold) {
e.preventDefault();
const key = fold.getAttribute('data-ovs-fold');
if (key === 'all') allOpen = !allOpen; else if (key === 'here') hereOpen = !hereOpen; else if (key === 'feeds') feedsOpen = !feedsOpen;
render(''); const back = listEl.querySelector(`[data-ovs-fold="${key}"]`); if (back) back.focus(); return;
}
const tick = e.target.closest && e.target.closest('[data-ovs-feed]');
const move = e.target.closest && e.target.closest('[data-ovs-feed-move]');
if (tick || move) {
const shown = [...listEl.querySelectorAll('[data-ovs-feed]')];
let on = shown.filter((x) => x.checked).map((x) => x.getAttribute('data-ovs-feed'));
const cur = feedPref();
if (cur) on = cur.filter((id) => on.includes(id)).concat(on.filter((id) => !cur.includes(id)));
let focusSel = tick ? `[data-ovs-feed="${tick.getAttribute('data-ovs-feed')}"]` : null;
if (move) {
e.preventDefault();
const [dir, id] = move.getAttribute('data-ovs-feed-move').split('|');
const i = on.indexOf(id), j = dir === 'up' ? i - 1 : i + 1;
if (i >= 0 && j >= 0 && j < on.length) { [on[i], on[j]] = [on[j], on[i]]; }
focusSel = `[data-ovs-feed-move="${dir}|${id}"]`;
}
saveFeeds(on);
render('');
const back = focusSel && listEl.querySelector(focusSel); if (back) back.focus();
return;
}
const a = e.target.closest && e.target.closest('a[data-i]');
const it = a && items[Number(a.getAttribute('data-i'))];
if (it && !input.value.trim()) beacon(it, a);
if (it && it.kind === 'dock' && root.OpenVibeNavbar && typeof root.OpenVibeNavbar.openDock === 'function' && ctx.token) {
e.preventDefault(); if (!showDoor(it.dock)) { try { root.OpenVibeNavbar.openDock(it.dock); } catch { location.href = it.url; } } return;
}
if (it && it.kind === 'results') { e.preventDefault(); showResults(input.value.trim()); input.focus(); return; }
if (it && it.kind === 'recent') { e.preventDefault(); input.value = it.title; paintGhost(); showResults(it.title); input.focus(); return; }
if (it) remember(it);
// A page chosen in the menu (owner, 11-09: "it doesn't update the navigation that's highlighted… the menu
// should animate closed when you change pages"): the menu closes as the page changes, whether Boost swaps it in
// place or the browser loads the next one. Its Back entry stays, so Back is not spent on it during the move.
const link = e.target.closest && e.target.closest('a[href]');
if (link && !e.defaultPrevented && !e.button && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey && (!link.target || link.target === '_self') && !/^#/.test(link.getAttribute('href'))) close({ navigating: true });
});
document.body.appendChild(el);
}
function onKey(e) {
if (e.key === 'Escape' && el.classList.contains('ovs-you-open')) { e.preventDefault(); toggleYou(false); return; }
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey) && input.value.trim()) { e.preventDefault(); askVibe(input.value); return; }
if (e.key === 'Escape') { e.preventDefault(); if (input.value) { input.value = ''; paintGhost(); render(''); return; } close(); return; }
if ((e.key === 'Tab' && !e.shiftKey || e.key === 'ArrowRight' && input.selectionStart === input.value.length) && completion) {
e.preventDefault(); input.value = completion; paintGhost(); schedule(input.value); return;
}
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (!items.length) return;
moved = true;
active = (active + (e.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
paintActive();
return;
}
if (e.key === 'Enter') {
e.preventDefault();
const q = input.value.trim();
if (tab === 'feed' && homeLib() && !door) { render(q); return; }
// Vibe: on Chats (the Dock's Vibe) the words go to its box as before; anywhere else they start a run.
if (door === 'vibe' && q) {
if (runFilter === 'chat') { try { if (ctx.ask) ctx.ask(q); } catch { /* the Dock is optional */ } }
else if (runView) runView.start(q).then(() => { input.value = ''; paintGhost(); }, () => {});
return;
}
if (door === 'chats') { try { if (root.OpenVibeDock && root.OpenVibeDock.filterChats) root.OpenVibeDock.filterChats(q); } catch { /* Dock is optional */ } return; }
const it = items[active >= 0 ? active : 0];
// Enter shows every result, unless the person picked a row or typed a place's exact name (live, tools).
if (q && !moved && view !== 'results' && !exact(it, q)) { showResults(q); return; }
if (it && it.kind === 'results') { showResults(q); return; }
if (it && it.kind === 'recent') { input.value = it.title; paintGhost(); showResults(it.title); return; }
if (it) { remember(it); location.href = it.url; }
}
}
const exact = (it, q) => !!it && ['site', 'family', 'tool', 'page'].includes(it.kind) && String(it.title).toLowerCase() === q.toLowerCase();
/** 4.229.0 (the owner, 11-12: "fix the history back button so it properly brings you back to this GUI"): the menu's
* entry remembers where it was when a link took the person away, and Back reopens it there (navbar.js restoreMenu). */
function markBack() {
try {
const back = { tab, door: door || null, scroll: listEl ? Math.round(listEl.scrollTop) : 0, at: Date.now() };
history.replaceState(Object.assign({}, history.state || {}, { ovsMenu: false, ovsBack: back }), '');
} catch { /* a page that cannot keep state: Back shows the page */ }
}
function remember(it) {
if (pushed) { pushed = false; try { history.replaceState(Object.assign({}, history.state || {}, { ovsMenu: false }), ''); } catch { /* */ } }
const q = input && input.value.trim();
if (q && it && it.kind !== 'recent') recent.add(q);
}
function paintActive() {
Array.prototype.forEach.call(listEl.querySelectorAll('.ovs-opt'), (a, i) => {
const on = i === active;
a.classList.toggle('is-active', on);
a.setAttribute('aria-selected', on ? 'true' : 'false');
if (on) { input.setAttribute('aria-activedescendant', a.id); a.scrollIntoView({ block: 'nearest' }); }
});
}
/** What the box would complete to: the person's searches, then their history, then sites and this site's pages. */
function paintGhost() {
const q = input.value;
completion = '';
if (q.trim().length >= 2 && !/\s$/.test(q)) {
const n = q.toLowerCase();
const pool = recent.get().concat(mine ? mine.items.map((h) => h.title) : [], sites().map((s) => s.name), (ctx.links || []).map((l) => l && l.label).filter(Boolean));
completion = pool.find((x) => x && x.length > q.length && x.toLowerCase().startsWith(n)) || '';
if (completion) completion = q + completion.slice(q.length);
}
ghost.textContent = '';
if (!completion) return;
const typed = document.createElement('b'); typed.textContent = q;
ghost.append(typed, document.createTextNode(completion.slice(q.length)));
}
/** A steady hue per site (ov-home.js has the same), so an app wears one colour in every tab. */
function hueOf(id) { let x = 7; const t = String(id || 'ov'); let i = 0; while (i < t.length) { x = (x * 31 + t.charCodeAt(i)) % 360; i++; } return x; }
// The glyphs a section of sites can wear (openvibe-shared/glyphs.js), and which one a section's name asks for.
const GROUP_GLYPH = {
'video': '',
'chat': '',
'game': '',
'book': '',
'store': '',
'wrench': '',
'spark': '',
'code': '',
'compass': '',
};
function groupGlyph(name) {
const n = String(name || '').toLowerCase();
return /watch|stream|video|live/.test(n) ? 'video' : /talk|belong|chat|social|community/.test(n) ? 'chat' : /play|game|fun/.test(n) ? 'game'
: /read|ask|news|learn/.test(n) ? 'book' : /shop|money|buy|market/.test(n) ? 'store' : /tool/.test(n) ? 'wrench'
: /\bai\b|make/.test(n) ? 'spark' : /develop|build|code/.test(n) ? 'code' : 'compass';
}
function icon(it) {
const ic = document.createElement('span');
ic.className = 'ovs-ic';
if (it.site) { ic.classList.add('ovs-site'); ic.style.setProperty('--ovs-h', String(hueOf(it.site))); const s = document.createElement('span'); s.className = 'ov-icon'; s.setAttribute('data-icon', it.site); s.setAttribute('data-size', '28'); s.setAttribute('data-fx', 'none'); ic.appendChild(s); }
else if (ICON[it.icon]) ic.innerHTML = ICON[it.icon];
else ic.textContent = (it.icon || '•').slice(0, 2);
return ic;
}
function option(tag, cls, it) {
const i = items.length;
items.push(it);
const a = document.createElement(tag);
a.className = `ovs-opt ${cls}`;
a.id = `ovs-o${i}`;
a.setAttribute('role', 'option');
a.href = it.url;
a.setAttribute('data-i', String(i));
a.tabIndex = -1;
if (it.here) { a.classList.add('is-here'); a.setAttribute('aria-current', 'page'); }
return a;
}
/** groups: [{ title, items, as: 'rows' | 'chips' | 'tiles' | 'sites' | 'apps' | 'links' | 'livebar' | 'fold', section,
* clear, more: { label, tab | href }, fold: { open, label, key } }]; text only through textContent. */
function foldButton(f, sec) {
const li = document.createElement('li'); li.setAttribute('role', 'presentation'); li.className = 'ovs-foldrow';
if (sec) li.setAttribute('data-sec', sec);
const b = document.createElement('button'); b.type = 'button'; b.className = 'ovs-fold';
b.setAttribute('data-ovs-fold', f.key); b.setAttribute('aria-expanded', f.open ? 'true' : 'false');
b.insertAdjacentHTML('beforeend', ICON.go); const t = document.createElement('span'); t.textContent = f.label; b.appendChild(t);
li.appendChild(b); return li;
}
function paint(groups, note) {
items = [];
listEl.textContent = '';
if (doorMount && !door) { try { doorMount.destroy(); } catch { /* */ } doorMount = null; }
groups.filter((g) => (g.items && g.items.length) || ['note', 'loading', 'route', 'fold', 'card', 'display', 'feeds', 'home', 'walltop', 'wall', 'dock-activity', 'notifications'].includes(g.as)).forEach((g) => {
if (g.as === 'notifications') {
const li = document.createElement('li'); li.className = 'ovs-notifications'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', 'notifications');
const host = document.createElement('div'); host.className = 'ovs-notifications-host'; li.appendChild(host);
const N = root.OpenVibeNotifications;
// 4.201.0's look: the section's head says what it is and how many wait, and Show all sits in it.
const head = document.createElement('div'); head.className = 'ovs-nhead';
const title = document.createElement('b'); title.textContent = 'Notifications';
const unread = document.createElement('span'); unread.className = 'ovs-nhead-n'; unread.hidden = true;
try { const n = N.unread ? Number(N.unread()) : 0; if (n > 0) { unread.textContent = `${n > 99 ? '99+' : n} new`; unread.hidden = false; } } catch { /* the count is optional */ }
const more = document.createElement('button'); more.type = 'button'; more.className = 'ovs-nhead-more'; more.hidden = true;
head.append(title, unread, more);
li.insertBefore(head, host);
let compact = null;
try { compact = N.renderMenuNotifications(host, (total) => { if (total > 3) { more.textContent = `Show all ${total}`; more.hidden = false; } }); } catch { host.textContent = 'Notifications are unavailable right now.'; }
more.addEventListener('click', () => { if (compact) compact.showAll(); more.hidden = true; });
listEl.appendChild(li); return;
}
if (g.as === 'home' || g.as === 'walltop' || g.as === 'wall') {
const H = root.OpenVibeHome; if (!H) return;
const kept = homeViews[g.as];
if (kept) { if (kept.api && kept.api.update) { try { kept.api.update(homeOpts()); } catch { /* keep what shows */ } } listEl.appendChild(kept.li); if (g.as === 'home') drawerIfAsked(kept.api); return; }
const li = document.createElement('li'); li.setAttribute('role', 'presentation');
let api = null;
try { api = (g.as === 'home' ? H.home : g.as === 'walltop' ? H.wallTop : (H.feed || H.timeline))(li, homeOpts()); } catch { return; }
homeViews[g.as] = { li, api };
listEl.appendChild(li);
if (g.as === 'home') drawerIfAsked(api);
return;
}
if (g.as === 'route') {
const r = document.createElement('li'); r.className = 'ovs-route'; r.setAttribute('role', 'presentation'); r.textContent = g.text; listEl.appendChild(r);
return;
}
if (g.as === 'fold') { listEl.appendChild(foldButton(g.fold, g.section)); return; }
if (g.as === 'feeds') {
const li = document.createElement('li'); li.className = 'ovs-feeds'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', 'feeds');
const onCount = g.items.filter((x) => x.on).length;
g.items.forEach((f) => {
const row = document.createElement('div'); row.className = 'ovs-feed';
const lab = document.createElement('label');
const box = document.createElement('input'); box.type = 'checkbox'; box.checked = f.on; box.setAttribute('data-ovs-feed', f.id);
if (!f.on && onCount >= FEEDS_MAX) box.disabled = true;
const t = document.createElement('span'); const b = document.createElement('b'); b.textContent = f.title; const m = document.createElement('small'); m.textContent = f.meta;
t.append(b, m); lab.append(box, t); row.appendChild(lab);
[['up', ICON.up, 'Show it earlier'], ['down', ICON.down, 'Show it later']].forEach(([dir, glyph, name]) => {
const mv = document.createElement('button'); mv.type = 'button'; mv.innerHTML = glyph; mv.setAttribute('aria-label', `${name}: ${f.title}`); mv.setAttribute('data-ovs-feed-move', `${dir}|${f.id}`); mv.disabled = !f.on; row.appendChild(mv);
});
li.appendChild(row);
});
const note = document.createElement('p'); note.className = 'ovs-feeds-note'; note.textContent = 'Every feed shows unless you turn it off; the arrows set the order.'; li.appendChild(note);
listEl.appendChild(li);
return;
}
if (g.as === 'display') {
const li = document.createElement('li'); li.className = 'ovs-displayrow'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', 'display');
try { ctx.display(li); } catch { return; }
listEl.appendChild(li);
return;
}
if (g.as === 'card') {
const body = document.createElement('div'); body.className = 'ovs-cardbody';
try {
g.card.render(body, { close, here: here(), user: ctx.user || null, signedIn: !!(ctx.token && ctx.user && !ctx.user.is_anon) });
} catch { return; }
if (!body.childNodes.length) return;
if (g.title) { const h = document.createElement('li'); h.className = 'ovs-group'; h.setAttribute('role', 'presentation'); h.setAttribute('data-sec', g.section); h.textContent = g.title; listEl.appendChild(h); }
const li = document.createElement('li'); li.className = 'ovs-card'; li.setAttribute('role', 'presentation'); li.setAttribute('data-sec', g.section);
li.appendChild(body); listEl.appendChild(li);
return;
}
const h = document.createElement('li');
h.className = 'ovs-group';
h.setAttribute('role', 'presentation');
if (g.section) h.setAttribute('data-sec', g.section);
// A section of sites wears its glyph (4.223.0), drawn from the glyph registry's set.
h.appendChild(document.createTextNode(g.title));
// The name stays the first node (readers and the launcher read it there); the glyph is drawn before it (order -1).
if (g.section === 'all' && g.title) { const gi = document.createElement('span'); gi.className = 'ovs-group-ic'; gi.setAttribute('aria-hidden', 'true'); gi.innerHTML = ``; h.appendChild(gi); }
if (g.clear) { const b = document.createElement('button'); b.type = 'button'; b.textContent = 'Clear'; b.setAttribute('data-ovs-clear', ''); h.appendChild(b); }
if (g.more && g.more.href) { const m = document.createElement('a'); m.className = 'ovs-more'; m.href = safeUrl(g.more.href) || '#'; m.textContent = g.more.label; h.appendChild(m); }
else if (g.more) { const b = document.createElement('button'); b.type = 'button'; b.textContent = g.more.label; b.setAttribute('data-ovs-tab', g.more.tab); h.appendChild(b); }
if (g.title) listEl.appendChild(h);
const as = g.as || 'rows';
if (as === 'note' || as === 'loading') {
const n = document.createElement('li'); n.setAttribute('role', 'presentation');
if (as === 'loading') { n.className = 'ovs-skels'; n.setAttribute('aria-label', g.text || 'Looking'); for (let k = 0; k < (g.rows || 3); k++) { const sk = document.createElement('span'); sk.className = 'ovs-skel'; n.appendChild(sk); } }
else { n.className = 'ovs-note'; n.textContent = g.text; }
listEl.appendChild(n);
return;
}
if (as === 'rows') {
// A row with a picture needs no site icon beside it (4.139.0); in a group where some rows have one, the
// others carry their icon in a picture-sized tile so the titles line up.
const pictured = g.items.some((x) => x.image);
g.items.forEach((it) => {
const li = document.createElement('li'); li.className = 'ovs-item'; li.setAttribute('role', 'presentation');
if (g.section) li.setAttribute('data-sec', g.section);
const a = option('a', '', it);
const main = document.createElement('span'); main.className = 'ovs-main';
const t = document.createElement('span'); t.className = 'ovs-title'; t.textContent = it.title; main.appendChild(t);
if (it.tag) { const tg = document.createElement('i'); tg.className = 'ovs-tag'; tg.textContent = it.tag; t.appendChild(tg); }
if (it.meta) { const m = document.createElement('span'); m.className = 'ovs-meta'; m.textContent = it.meta; if (it.at) { m.dataset.at = it.at; m.dataset.pre = it.metaPre || ''; } main.appendChild(m); }
if (it.snippet) { const sn = document.createElement('span'); sn.className = 'ovs-snip'; sn.textContent = it.snippet; main.appendChild(sn); }
if (it.image) {
const im = document.createElement('img');
im.className = 'ovs-thumb'; im.src = it.image; im.alt = '';
im.width = 72; im.height = 40; im.loading = 'lazy'; im.decoding = 'async';
a.appendChild(im);
} else if (pictured) {
const tile = document.createElement('span'); tile.className = 'ovs-thumb ovs-thumb--ico';
tile.append(icon(it)); a.appendChild(tile);
} else a.append(icon(it));
a.appendChild(main);
const q = input.value.trim();
if (q && view !== 'results' && (it.kind === 'results' || (items.length === 1 && exact(it, q)))) { const k = document.createElement('span'); k.className = 'ovs-key'; k.textContent = 'Enter'; a.appendChild(k); }
li.appendChild(a); listEl.appendChild(li);
});
return;
}
const box = document.createElement('li');
box.setAttribute('role', 'presentation');
if (g.section) box.setAttribute('data-sec', g.section);
box.className = as === 'chips' ? 'ovs-chips' : as === 'apps' ? 'ovs-apps' : as === 'links' ? 'ovs-links' : as === 'livebar' ? 'ovs-livebar' : `ovs-grid${as === 'sites' ? ' is-sites' : as === 'mini' ? ' is-mini' : ''}`;
g.items.forEach((it, k) => {
if (as === 'chips') {
const a = option('a', 'ovs-chip', it);
// The place's own icon (the navbar's drawn set, never page text), else the menu's.
if (it.drawn && /^