From cd7d7d6251023bef4be6f2fe780e03d1981fc668 Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Sat, 3 Oct 2026 13:20:12 -0400 Subject: [PATCH] feat: implement approved Hope primary conversation shell --- tests/unit/test_chat_web.py | 57 +- web/chat.js | 54 +- web/index.html | 207 +-- web/style.css | 2621 ++++++++++++++--------------------- 4 files changed, 1192 insertions(+), 1747 deletions(-) diff --git a/tests/unit/test_chat_web.py b/tests/unit/test_chat_web.py index 203d9b4..8ad407a 100644 --- a/tests/unit/test_chat_web.py +++ b/tests/unit/test_chat_web.py @@ -73,6 +73,7 @@ def test_chat_crud_replay_safe_markdown_and_preserved_branches(): page.goto('https://hope.test/'); page.wait_for_load_state('networkidle') page.get_by_role('button',name='A saved thought',exact=True).click() expect(page.locator('#chatTimeline')).to_contain_text('Original question') + page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") page.locator('#pinConversation').click(); expect(page.locator('#pinConversation')).to_have_text('Unpin') page.locator('#primaryInput').fill('A new question'); page.locator('#primarySend').click() expect(page.locator('#chatTimeline')).to_contain_text('Answer') @@ -90,11 +91,12 @@ def test_chat_crud_replay_safe_markdown_and_preserved_branches(): expect(page.locator('#chatTitle')).to_have_text('New branch') expect(page.locator('#chatTimeline')).to_contain_text('Revised question') assert messages['c1'][0]['content'] == 'Original question' - page.locator('#openPreferences').click(); page.locator('#paletteSelect').select_option('ocean') + page.locator('#openPreferences').click(); expect(page.locator('#preferencesMessage')).to_have_text(''); page.locator('#paletteSelect').select_option('ocean') page.get_by_role('button',name='Save appearance').click(); expect(page.locator('html')).to_have_attribute('data-palette','ocean') - page.locator('#closePreferences').click(); page.screenshot(path='/tmp/hope-chat-desktop.png') + page.locator('#settingsClose').click(); page.screenshot(path='/tmp/hope-chat-desktop.png') + page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") page.locator('#deleteConversation').click(); page.locator('#chatActionSave').click() - expect(page.locator('#chatTitle')).to_have_text('New chat'); assert 'branch' not in conversations + expect(page.locator('#chatTitle')).to_have_text('New conversation'); assert 'branch' not in conversations assert errors == [] browser.close() @@ -106,17 +108,9 @@ def test_mobile_drawer_keyboard_voice_and_logout(): page.goto('https://hope.test/'); page.wait_for_load_state('networkidle') page.locator('#drawerOpen').click(); expect(page.locator('#drawerOpen')).to_have_attribute('aria-expanded','true') page.keyboard.press('Escape'); expect(page.locator('#drawerOpen')).to_be_focused() - page.locator('#drawerOpen').click(); page.locator('#openVoice').click() - expect(page.locator('#startBtn')).to_be_visible(); expect(page.locator('#settingsBtn')).to_be_visible() - page.locator('#backToChat').click(); expect(page.locator('#primaryChat')).to_be_visible() - page.evaluate("window.dispatchEvent(new CustomEvent('hope:voice-state',{detail:{active:true}}))") - expect(page.locator('#primaryInput')).to_be_disabled() - page.evaluate("window.dispatchEvent(new CustomEvent('hope:voice-state',{detail:{active:false}}))") - expect(page.locator('#primaryInput')).to_be_enabled() - page.wait_for_function("document.getElementById('chatSidebar').getBoundingClientRect().right <= 0") - page.screenshot(path='/tmp/hope-chat-mobile.png') - assert page.evaluate('document.documentElement.scrollWidth <= innerWidth') - page.locator('#drawerOpen').click(); page.locator('#openVoice').click(); page.locator('#logoutBtn').click() + page.locator('#drawerOpen').click(); page.locator('#accountMenuBtn').click() + expect(page.locator('#settingsDialog')).to_be_visible() + page.locator('#logoutBtn').click() expect(page.locator('#loginGate')).to_be_visible() assert 'Original question' not in page.locator('#chatTimeline').inner_text() browser.close() @@ -138,12 +132,12 @@ def test_archived_move_sync_conflict_and_mobile_actions(): page.evaluate("document.dispatchEvent(new Event('visibilitychange'))") expect(page.locator('#chatTitle')).to_have_text('Changed elsewhere') page.locator('#conversationMenu').click(); page.locator('#archiveConversation').click(); page.locator('#chatActionSave').click() - expect(page.locator('#chatTitle')).to_have_text('New chat') + expect(page.locator('#chatTitle')).to_have_text('New conversation') page.locator('#drawerOpen').click(); page.locator('#showArchived').click() page.get_by_role('button',name='Changed elsewhere',exact=True).click() page.locator('#conversationMenu').click(); expect(page.locator('#archiveConversation')).to_have_text('Unarchive') page.locator('#archiveConversation').click(); page.locator('#chatActionSave').click() - expect(page.locator('#chatTitle')).to_have_text('New chat') + expect(page.locator('#chatTitle')).to_have_text('New conversation') assert conversations['c1']['archived'] is False # A stale optimistic write refreshes rather than overwriting another client's title. page.locator('#drawerOpen').click(); page.locator('#showArchived').click() @@ -154,7 +148,8 @@ def test_archived_move_sync_conflict_and_mobile_actions(): route.fulfill(status=409,json={'error':'Stale revision'}) else: route.fallback() page.route('https://hope.test/api/conversations/c1', conflict) - page.locator('#conversationMenu').click(); page.locator('#pinConversation').click() + page.locator('#conversationMenu').click(); page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") + page.locator('#pinConversation').click() expect(page.locator('#chatTitle')).to_have_text('Newest revision') expect(page.locator('#chatNotice')).to_contain_text('Conversation refreshed') browser.close() @@ -180,7 +175,7 @@ def test_voice_history_refresh_keyboard_focus_and_large_text(): page.screenshot(path='/tmp/hope-chat-opal-desktop.png') page.keyboard.press('Control+k'); expect(page.locator('#conversationSearch')).to_be_focused() page.keyboard.press('Escape') - page.locator('#openPreferences').click(); expect(page.locator('#preferencesDialog')).to_be_visible() + page.locator('#openPreferences').click(); expect(page.locator('#settingsDialog')).to_be_visible() page.keyboard.press('Escape'); expect(page.locator('#openPreferences')).to_be_focused() # Native controls remain usable at large text and narrow effective viewport. page.set_viewport_size({'width':640,'height':900}) @@ -202,8 +197,10 @@ def test_qualified_research_and_private_export(): page.locator('#primaryInput').fill('Research this topic'); page.locator('#primarySend').click() expect(page.locator('#chatTimeline')).to_contain_text('Answer') assert next(body for method,path,body in calls if method == 'POST' and path.endswith('/messages'))['mode'] == 'research' + page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") with page.expect_download() as info: page.locator('#exportMarkdown').click() assert info.value.suggested_filename.endswith('.md') + page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") with page.expect_download() as info: page.locator('#exportJSON').click() assert info.value.suggested_filename.endswith('.json') assert all('/api/conversations/' in path for method,path,body in calls if path.endswith('/export')) @@ -219,6 +216,7 @@ def test_immediate_pin_after_response_uses_final_server_revision(): expect(page.locator('#chatTitle')).to_have_text('A saved thought') page.locator('#primaryInput').fill('Finish and immediately pin'); page.locator('#primarySend').click() expect(page.locator('#chatTimeline')).to_contain_text('Answer') + page.evaluate("if (!document.getElementById('conversationActions').matches(':popover-open')) document.getElementById('conversationActions').showPopover()") page.locator('#pinConversation').click() expect(page.locator('#pinConversation')).to_have_text('Unpin') assert conversations['c1']['pinned'] is True @@ -228,25 +226,30 @@ def test_immediate_pin_after_response_uses_final_server_revision(): browser.close() -def test_realtime_voice_is_prominent_on_desktop_and_mobile(): +def test_realtime_voice_is_inline_in_same_thread_and_creates_real_conversation(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) - for width in [1280,390]: + for width in [1440, 390, 320]: page = browser.new_page(viewport={'width':width,'height':900}) + errors = []; page.on('pageerror', lambda error: errors.append(str(error))) calls, streams, conversations, messages = fixture(page) + page.add_init_script("window.addEventListener('hope:start-voice', event => {event.stopImmediatePropagation(); window.startedVoice = event.detail.conversation_id; window.dispatchEvent(new CustomEvent('hope:voice-state', {detail:{active:true,conversation_id:event.detail.conversation_id}}));}, true)") page.goto('https://hope.test/'); page.wait_for_load_state('networkidle') - expect(page.get_by_role('button',name='Talk to Hope',exact=True)).to_be_visible() + # Replace only voice transport; durable conversation creation still uses real chat.js. + + expect(page.locator('#primaryComposer #talkToHope')).to_be_visible() + assert page.locator('.chat-topbar #talkToHope').count() == 0 page.locator('#primaryInput').fill('Keep this draft while I talk') page.locator('#talkToHope').click() expect(page.locator('#legacyVoice')).to_be_visible() - expect(page.locator('#startBtn')).to_be_visible() - expect(page.locator('#voiceModeBtn')).to_have_attribute('aria-pressed','true') - expect(page.locator('#backToChat')).to_be_visible() - page.locator('#backToChat').click() + expect(page.locator('#primaryChat')).to_be_visible() expect(page.locator('#primaryInput')).to_have_value('Keep this draft while I talk') - expect(page.locator('#talkToHope')).to_be_visible() - assert not any(path == '/token' for method,path,body in calls) + expect(page.locator('#primaryInput')).to_be_enabled() + expect(page.locator('#primarySend')).to_be_disabled() + assert page.evaluate('window.startedVoice') == 'c2' + assert any(method == 'POST' and path == '/api/conversations' for method,path,body in calls) assert page.evaluate('document.documentElement.scrollWidth <= innerWidth') page.screenshot(path=f'/tmp/hope-talk-visible-{width}.png') + assert not errors page.close() browser.close() diff --git a/web/chat.js b/web/chat.js index 225b4c1..2c7fff1 100644 --- a/web/chat.js +++ b/web/chat.js @@ -9,14 +9,14 @@ const button = (text, fn, cls = 'btn btn-ghost') => { const b = document.createElement('button'); b.type = 'button'; b.className = cls; b.textContent = text; b.addEventListener('click', () => Promise.resolve().then(fn).catch(error => notice(error.message))); return b; }; function controls() { const ready = HopeAuth.user?.status === 'ready', running = !terminal(state.run) || !!state.stream || state.voiceActive; - $('primaryInput').disabled = !ready || state.busy || running; $('chatTimeline').setAttribute('aria-busy', String(!terminal(state.run))); $('primaryComposer').setAttribute('aria-busy', String(state.busy)); + $('primaryInput').disabled = !ready || state.busy || !terminal(state.run) || !!state.stream; $('chatTimeline').setAttribute('aria-busy', String(!terminal(state.run))); $('primaryComposer').setAttribute('aria-busy', String(state.busy)); $('primarySend').disabled = !ready || state.busy || running || window.HopeFeatures?.hasPendingUploads() || !$('primaryInput').value.trim(); $('cancelRun').hidden = terminal(state.run); $('researchMode').disabled = !ready || running || state.busy; for (const id of ['pinConversation', 'renameConversation', 'moveConversation', 'exportMarkdown', 'exportJSON', 'archiveConversation', 'deleteConversation']) $(id).disabled = !state.conversation || state.busy || !!state.stream || !terminal(state.run); - $('conversationActions').hidden = !state.conversation; $('conversationMenu').hidden = !state.conversation; + $('talkToHope').disabled = !ready || state.busy || !!state.stream || !terminal(state.run); $('conversationMenu').hidden = !state.conversation; $('archiveConversation').textContent = state.conversation?.archived ? 'Unarchive' : 'Archive'; $('pinConversation').textContent = state.conversation?.pinned ? 'Unpin' : 'Pin'; - $('chatTitle').textContent = state.conversation?.title || 'New chat'; + $('chatTitle').textContent = state.conversation?.title || 'New conversation'; } function selected(conversation) { state.conversation = conversation; @@ -36,12 +36,25 @@ $('drawerOpen').onclick = () => drawer(true); $('drawerClose').onclick = $('drawerBackdrop').onclick = () => drawer(false); document.addEventListener('keydown', event => { if (event.key === 'Escape' && $('chatSidebar').classList.contains('drawer-open')) drawer(false); if ((event.metaKey || event.ctrlKey) && event.key === 'k') {event.preventDefault(); drawer(true); $('conversationSearch').focus();} }); const mobile = matchMedia('(max-width: 760px)'); - function adapt() { if (mobile.matches) $('conversationActions').setAttribute('popover', 'auto'); else {$('conversationActions').removeAttribute('popover');} - $('chatSidebar').inert = mobile.matches && !$('chatSidebar').classList.contains('drawer-open'); if (!mobile.matches) { $('primaryChat').inert = false; $('legacyVoice').inert = false; $('drawerBackdrop').hidden = true; } } + function adapt() { $('chatSidebar').inert = mobile.matches && !$('chatSidebar').classList.contains('drawer-open'); if (!mobile.matches) { $('primaryChat').inert = false; $('legacyVoice').inert = false; $('drawerBackdrop').hidden = true; } } mobile.addEventListener('change', adapt); adapt(); - function showVoice() { $('primaryChat').hidden = true; $('legacyVoice').hidden = false; if (mobile.matches) drawer(false); $('voiceModeBtn').click(); $('startBtn').focus(); } - $('openVoice').onclick = showVoice; $('talkToHope').onclick = showVoice; - $('backToChat').onclick = () => { $('primaryChat').hidden = false; $('legacyVoice').hidden = true; $('primaryInput').focus(); }; + async function startVoice() { + if (state.voiceActive) { $('legacyVoice').hidden = false; $('muteBtn').focus(); return; } + if (state.busy || state.stream || !terminal(state.run) || HopeAuth.user?.status !== 'ready') return; + const epoch = state.epoch; state.busy = true; controls(); + try { + if (!state.conversation) {const data = await api('/api/conversations', 'POST', {title: 'Voice conversation', ...(state.workspace ? {workspace_id: state.workspace} : {})}); if (epoch !== state.epoch) return; selected(data.conversation); await list();} + if (epoch !== state.epoch) return; + $('legacyVoice').hidden = false; + window.dispatchEvent(new CustomEvent('hope:start-voice', {detail: {conversation_id: state.conversation.id}})); + } catch (error) {if (epoch === state.epoch) notice(error.message);} finally {if (epoch === state.epoch) {state.busy = false; controls();}} + } + $('talkToHope').onclick = startVoice; + $('openVoice').onclick = startVoice; + $('backToChat').onclick = () => $('primaryInput').focus(); + document.querySelectorAll('.lightwell-symbol').forEach(el => {for (let i=0;i<25;i++) el.append(document.createElement('i'));}); + $('openLibrary').onclick = () => {if (mobile.matches) drawer(false); $('libraryDialog').showModal();}; + $('closeLibrary').onclick = () => $('libraryDialog').close(); // Markdown uses DOM text nodes only: HTML is always literal; unsafe URLs never become links. function inline(parent, text) { const expression = /(`[^`]+`|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^\s)]+\))/g; @@ -74,7 +87,7 @@ const timeline = $('chatTimeline'), atBottom = timeline.scrollHeight - timeline.scrollTop - timeline.clientHeight < 100, scroll = timeline.scrollTop; timeline.replaceChildren(); if (state.messageCursor) timeline.append(button('Load earlier messages', olderMessages)); - if (!state.messages.length) { const welcome = document.createElement('div'); welcome.className = 'chat-welcome'; const h = document.createElement('h2'); h.textContent = 'A little space to think.'; const p = document.createElement('p'); p.textContent = 'Ask a question, work through an idea, or start something new.'; welcome.append(h, p); timeline.append(welcome); } + if (!state.messages.length) { const welcome = document.createElement('div'); welcome.className = 'chat-welcome'; const mark = document.createElement('span'); mark.className = 'hope-mark'; mark.setAttribute('aria-hidden','true'); const h = document.createElement('h2'); h.textContent = 'A little space for you.'; const p = document.createElement('p'); p.textContent = 'Think out loud, work something through, or make something new. You can type, or talk to Hope.'; welcome.append(mark, h, p); timeline.append(welcome); } for (const message of state.messages) { const row = document.createElement('article'); row.className = `chat-message ${message.role}`; row.dataset.messageId = message.id; const name = document.createElement('h2'); name.textContent = message.role === 'user' ? 'You' : 'Hope'; row.append(name, markdown(message.content)); @@ -99,13 +112,14 @@ state.cursor = data.next_cursor; $('loadMoreConversations').hidden = !state.cursor; } async function open(id) { + if (state.voiceActive && id !== state.conversation?.id) {notice('End voice before opening another conversation. Your current thread stays here.'); return;} stopStream(); const epoch = ++state.epoch; state.run = null; state.messages = []; state.pending = null; notice('Loading conversation…'); controls(); const data = await api('/api/conversations/' + enc(id)); if (epoch !== state.epoch) return; selected(data.conversation); state.messages = data.messages || []; state.messageCursor = data.next_cursor; state.run = (data.runs || []).find(r => !terminal(r)) || null; render(); notice(''); if (mobile.matches) drawer(false); if (state.run) stream(state.run); await list(); } async function olderMessages() {const epoch = state.epoch, timeline = $('chatTimeline'), height = timeline.scrollHeight; const data = await api('/api/conversations/' + enc(state.conversation.id) + '?' + new URLSearchParams({cursor: state.messageCursor, limit: '50'})); if (epoch !== state.epoch) return; const ids = new Set(state.messages.map(m => m.id)); state.messages = (data.messages || []).filter(m => !ids.has(m.id)).concat(state.messages); state.messageCursor = data.next_cursor; render(); timeline.scrollTop += timeline.scrollHeight - height;} function reset() { stopStream(); ++state.epoch; state.messages = []; state.messageCursor = null; state.run = null; state.pending = null; state.busy = false; selected(null); render(); notice(''); $('primaryInput').value = ''; controls(); } - $('newConversation').onclick = () => {reset(); if (mobile.matches) drawer(false); $('primaryInput').focus();}; + $('newConversation').onclick = () => {if (state.voiceActive) {notice('End voice before starting a new conversation.'); return;} reset(); if (mobile.matches) drawer(false); $('primaryInput').focus();}; $('conversationSearch').oninput = () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => list().catch(e => notice(e.message)), 200); }; $('showArchived').onclick = () => {state.archived = !state.archived; $('showArchived').setAttribute('aria-pressed', String(state.archived)); $('recentHeading').textContent = state.archived ? 'Archived' : 'Recent'; list().catch(e => notice(e.message));}; $('loadMoreConversations').onclick = () => list(true).catch(e => notice(e.message)); @@ -170,20 +184,32 @@ $('cancelRun').onclick = async () => {try {const data = await api('/api/runs/' + enc(state.run.id) + '/cancel', 'POST'); state.run = data.run; controls();} catch (e) {notice(e.message);} }; function workspaceRows() { $('workspaceList').replaceChildren(button('All conversations', () => {state.workspace = null; list();}, 'conversation-link')); - for (const workspace of state.workspaces) { const row = document.createElement('div'); row.className = 'workspace-row'; row.append(button(workspace.name, () => {state.workspace = workspace.id; notice('Workspace: ' + workspace.name + '. New chats will be created here.'); list();}, 'conversation-link'), button('Instructions', () => workspaceInstructions(workspace)), button('Edit', () => dialog('Rename workspace', workspace.name, async name => {await api('/api/workspaces/' + enc(workspace.id), 'PATCH', {revision: workspace.revision, name}); await workspaces();})), button('Remove', () => dialog('Remove workspace?', '', async () => {await api('/api/workspaces/' + enc(workspace.id), 'DELETE'); if (state.workspace === workspace.id) state.workspace = null; await workspaces(); await list();}, 'Its conversations are kept outside the workspace.', false))); $('workspaceList').append(row); } + for (const workspace of state.workspaces) { + const row = document.createElement('div'); row.className = 'workspace-row'; + row.append(button(workspace.name, () => {state.workspace = workspace.id; notice('Workspace: ' + workspace.name + '. New chats will be created here.'); list();}, 'conversation-link')); + const menu = document.createElement('details'), summary = document.createElement('summary'), tools = document.createElement('div'); menu.className = 'workspace-menu'; summary.textContent = '•••'; summary.setAttribute('aria-label', workspace.name + ' workspace actions'); tools.className = 'workspace-tools'; + tools.append(button('Instructions', () => {menu.open = false; workspaceInstructions(workspace);}), button('Rename', () => {menu.open = false; dialog('Rename workspace', workspace.name, async name => {await api('/api/workspaces/' + enc(workspace.id), 'PATCH', {revision: workspace.revision, name}); await workspaces();});}), button('Remove', () => {menu.open = false; dialog('Remove workspace?', '', async () => {await api('/api/workspaces/' + enc(workspace.id), 'DELETE'); if (state.workspace === workspace.id) state.workspace = null; await workspaces(); await list();}, 'Its conversations are kept outside the workspace.', false);})); + menu.append(summary, tools); row.append(menu); $('workspaceList').append(row); + } } async function workspaces() {const epoch = state.epoch, data = await api('/api/workspaces'); if (epoch !== state.epoch) return; state.workspaces = data.workspaces; workspaceRows();} function workspaceInstructions(workspace) {dialog('Workspace instructions', workspace.instructions || '', async instructions => {await api('/api/workspaces/' + enc(workspace.id), 'PATCH', {revision: workspace.revision, instructions}); await workspaces();}, 'These instructions guide chats in this workspace. They do not isolate tool access.');} $('newWorkspace').onclick = () => dialog('Create workspace', '', async name => {await api('/api/workspaces', 'POST', {name}); await workspaces();}, 'Group related chats. A workspace is not a separate tool sandbox.'); function theme(preferences) {document.documentElement.dataset.palette = preferences.palette; document.documentElement.dataset.appearance = preferences.appearance; $('paletteSelect').value = preferences.palette; $('appearanceSelect').value = preferences.appearance;} - $('openPreferences').onclick = async () => {try {const data = await api('/api/preferences'); state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = ''; if (!HopeAuth.user) return; $('preferencesDialog').showModal();} catch(e) {notice(e.message);} }; + window.HopeAppearance = {load: async () => { + const epoch = state.epoch, user = HopeAuth.user?.id, fields = [...$('preferencesForm').querySelectorAll('select, button[type=submit], button:not([type])')]; + fields.forEach(field => field.disabled = true); + try {const data = await api('/api/preferences'); if (epoch !== state.epoch || user !== HopeAuth.user?.id) return; state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = '';} + finally {if (epoch === state.epoch && user === HopeAuth.user?.id) fields.forEach(field => field.disabled = false);} + }}; + $('openPreferences').onclick = async () => {try {if (window.HopeSettings) {await HopeSettings.open('appearance'); return;} await HopeAppearance.load(); if (!HopeAuth.user) return; $('preferencesDialog').showModal();} catch(e) {notice(e.message);} }; $('closePreferences').onclick = () => $('preferencesDialog').close(); $('preferencesForm').onsubmit = async event => {event.preventDefault(); try {const data = await api('/api/preferences', 'PUT', {revision: state.preferences.revision, palette: $('paletteSelect').value, appearance: $('appearanceSelect').value}); state.preferences = data.preferences; theme(data.preferences); $('preferencesMessage').textContent = 'Appearance saved.';} catch(e) {$('preferencesMessage').textContent = e.message;} }; async function refreshSelected(messages = true) {if (!state.conversation) return; const epoch = state.epoch, cid = state.conversation.id; const data = await api('/api/conversations/' + enc(cid)); if (epoch !== state.epoch || cid !== state.conversation?.id) return; selected(data.conversation); if (messages && terminal(state.run)) {const timeline = $('chatTimeline'), scroll = timeline.scrollTop; const incoming = new Map((data.messages || []).map(m => [m.id,m])); state.messages = state.messages.map(m => incoming.get(m.id) || m); for (const m of incoming.values()) if (!state.messages.some(old => old.id === m.id)) state.messages.push(m); render(); timeline.scrollTop = scroll;} } - async function sync() {if (syncBusy || !HopeAuth.user || document.hidden) return; syncBusy = true; const epoch = state.epoch; try {const data = await api('/api/sync?after=' + state.syncCursor); if (epoch !== state.epoch) return; state.syncCursor = data.cursor; if (!data.changes?.length) return; for (const change of data.changes) {if (change.kind === 'preferences' && change.data && !$('preferencesDialog').open) {state.preferences = change.data; theme(change.data);} if (change.kind === 'conversation' && change.id === state.conversation?.id && change.deleted) {reset(); notice('This conversation was deleted on another device.');}} await Promise.all([list(), workspaces(), api('/api/research').then(data => {if (epoch !== state.epoch) return; state.research = data.available === true && data.mode === 'hermes_tools'; $('researchMode').hidden = !state.research; $('researchModeLabel').hidden = !state.research;}), refreshSelected()]);} catch (error) {/* A subsequent foreground poll retries; never discard the draft. */} finally {syncBusy = false;}} + async function sync() {if (syncBusy || !HopeAuth.user || document.hidden) return; syncBusy = true; const epoch = state.epoch; try {const data = await api('/api/sync?after=' + state.syncCursor); if (epoch !== state.epoch) return; state.syncCursor = data.cursor; if (!data.changes?.length) return; for (const change of data.changes) {if (change.kind === 'preferences' && change.data && !$('preferencesDialog').open && !(window.HopeSettings?.isOpen && HopeSettings.section === 'appearance')) {state.preferences = change.data; theme(change.data);} if (change.kind === 'conversation' && change.id === state.conversation?.id && change.deleted) {reset(); notice('This conversation was deleted on another device.');}} await Promise.all([list(), workspaces(), api('/api/research').then(data => {if (epoch !== state.epoch) return; state.research = data.available === true && data.mode === 'hermes_tools'; $('researchMode').hidden = !state.research; $('researchModeLabel').hidden = !state.research;}), refreshSelected()]);} catch (error) {/* A subsequent foreground poll retries; never discard the draft. */} finally {syncBusy = false;}} document.addEventListener('visibilitychange', () => {if (!document.hidden) sync();}); HopeAuth.onChange((user, changed) => { - if (changed || !user) {clearInterval(syncTimer); state.syncCursor = 0; state.archived = false; state.research = false; $('researchMode').hidden = true; $('researchModeLabel').hidden = true; $('researchMode').value = 'normal'; reset(); state.workspace = null; state.workspaces = []; state.preferences = null; $('pinnedConversations').replaceChildren(); $('recentConversations').replaceChildren(); $('workspaceList').replaceChildren(); $('legacyVoice').hidden = true; $('primaryChat').hidden = false; $('preferencesDialog').close(); $('chatActionDialog').close(); theme({palette: 'opal', appearance: 'system'});} + if (changed || !user) {clearInterval(syncTimer); state.syncCursor = 0; state.archived = false; state.research = false; $('researchMode').hidden = true; $('researchModeLabel').hidden = true; $('researchMode').value = 'normal'; reset(); state.workspace = null; state.workspaces = []; state.preferences = null; $('pinnedConversations').replaceChildren(); $('recentConversations').replaceChildren(); $('workspaceList').replaceChildren(); $('legacyVoice').hidden = true; $('primaryChat').hidden = false; $('preferencesDialog').close(); $('chatActionDialog').close(); $('libraryDialog').close(); theme({palette: 'opal', appearance: 'system'});} controls(); if (!user || !changed) return; const epoch = state.epoch; syncTimer = setInterval(sync, 5000); Promise.all([list(), workspaces(), api('/api/research').then(data => {if (epoch !== state.epoch) return; state.research = data.available === true && data.mode === 'hermes_tools'; $('researchMode').hidden = !state.research; $('researchModeLabel').hidden = !state.research;}), api('/api/preferences').then(data => {if (epoch === state.epoch) {state.preferences = data.preferences; theme(data.preferences);}})]).catch(e => {if (epoch === state.epoch) notice(e.message);}); diff --git a/web/index.html b/web/index.html index 8f45b7b..25f4b1a 100644 --- a/web/index.html +++ b/web/index.html @@ -14,8 +14,7 @@ - - +

Welcome to Hope

Sign in to your private voice and text companion.

@@ -27,134 +26,32 @@

Checking your session…

-
- -

New chat

A little space to think.

Ask a question, work through an idea, or start something new.

Your chats are private to your account. Hope can make mistakes.

- - - + +

Library

Files and saved work, private to your account.

+ + -

Appearance

- - - - - +

Appearance

+ + diff --git a/web/style.css b/web/style.css index 988761e..e885ecd 100644 --- a/web/style.css +++ b/web/style.css @@ -1,1552 +1,1073 @@ -:root { - --canvas: #08111f; - --canvas-deep: #050b14; - --surface: #0d1929; - --surface-raised: #132238; - --surface-soft: #172941; - --line: rgba(183, 204, 235, 0.13); - --line-strong: rgba(183, 204, 235, 0.22); - --ink: #f4f7fb; - --ink-soft: #c5d1e2; - --ink-muted: #8395ad; - --iris: #8ba7ff; - --iris-strong: #6f8ff7; - --iris-deep: #3c5fc9; - --warm: #ffb38e; - --mint: #76d9b2; - --danger: #ff806f; - --user-bubble: #1b3150; - --agent-bubble: #14243a; - --focus: #b5c5ff; - --ease-out: cubic-bezier(0.22, 1, 0.36, 1); - --z-panel: 40; - --z-backdrop: 50; - --z-sheet: 60; - color-scheme: dark; +/* Approved Hope Lightwell shell. Semantic palette stays account-controlled. */ +:root{ + --canvas:var(--hope-ground); + --surface:var(--hope-surface); + --surface-raised:var(--hope-persona-surface); + --ink:var(--hope-ink); + --ink-soft:var(--hope-quiet); + --ink-muted:var(--hope-quiet); + --accent:var(--hope-accent); + --accent-ink:var(--hope-on-accent); + --line:var(--hope-separator); + --focus:var(--hope-focus); + --danger:var(--hope-alert); + --iris:var(--hope-accent); + --z-panel:30; + --z-backdrop:40; + --z-sheet:50} +*{ + box-sizing:border-box} +body{ + margin:0; + background:var(--canvas); + color:var(--ink); + font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; + -webkit-font-smoothing:antialiased} +button,input,textarea,select{ + font:inherit; + color:inherit} +button{ + cursor:pointer} +button:disabled{ + opacity:.45; + cursor:not-allowed} +[hidden]{ + display:none!important} +h1,h2,h3,p{ + margin:0} +h1,h2,h3{ + text-wrap:balance} +p{ + text-wrap:pretty} +a{ + color:var(--accent)} +:focus-visible{ + outline:2px solid var(--focus); + outline-offset:3px} +.sr-only{ + position:absolute; + width:1px; + height:1px; + margin:-1px; + overflow:hidden; + clip:rect(0,0,0,0); + white-space:nowrap} +.skip-link{ + position:fixed; + top:12px; + left:12px; + z-index:var(--z-sheet); + padding:12px; + background:var(--ink); + color:var(--canvas); + transform:translateY(-180%)} +.skip-link:focus{ + transform:none} +.btn{ + display:inline-flex; + align-items:center; + justify-content:center; + gap:8px; + min-height:44px; + padding:9px 13px; + border:0; + border-radius:8px; + background:transparent; + color:var(--ink); + font-weight:500; + transition:background 160ms ease} +.btn:hover:not(:disabled){ + background:var(--surface-raised)} +.btn-primary{ + background:var(--accent); + color:var(--accent-ink)} +.btn-primary:hover:not(:disabled){ + background:var(--accent); + filter:brightness(1.08)} +.btn-danger{ + background:var(--danger); + color:var(--hope-on-alert)} +.btn-secondary{ + border:1px solid var(--line)} +.btn-icon{ + width:44px; + min-width:44px; + padding:0; + font-size:20px} +.btn-ghost{ + color:var(--ink-soft)} + +.app-shell{ + display:flex; + height:100dvh; + overflow:hidden} +.chat-sidebar{ + width:252px; + flex-shrink:0; + display:flex; + flex-direction:column; + padding:28px 18px 16px; + background:var(--surface); + border-right:1px solid var(--line); + min-height:0; + overflow:auto} +.sidebar-brand{ + display:flex; + align-items:center; + gap:11px; + padding:0 10px 28px; + font-size:23px; + letter-spacing:-.03em} +.hope-mark{ + display:inline-grid; + place-items:center; + flex-shrink:0; + width:27px; + height:27px; + background:var(--accent); + border-radius:6px} +.hope-mark:after{ + content:""; + width:11px; + height:11px; + background:var(--accent-ink); + border-radius:2px} +.new-chat{ + width:100%; + justify-content:flex-start; + background:var(--canvas); + border:1px solid var(--line); + margin-bottom:13px} +.chat-sidebar input{ + width:100%; + min-height:44px; + background:transparent; + border:1px solid var(--line); + border-radius:8px; + padding:10px 12px} +input::placeholder,textarea::placeholder{ + color:var(--ink-soft); + opacity:1} +.chat-sidebar nav{ + flex:1; + margin-top:18px} +.chat-sidebar h2{ + font-size:12px; + font-weight:600; + color:var(--ink-soft); + margin:16px 12px 8px} +.nav-heading{ + display:flex; + align-items:center; + justify-content:space-between} +.nav-heading h2{ + margin:0 12px} +.conversation-link{ + display:block; + width:100%; + min-height:44px; + padding:11px 12px; + border:0; + border-radius:8px; + background:transparent; + text-align:left; + white-space:nowrap; + overflow:hidden; + text-overflow:ellipsis} +.conversation-link:hover,.conversation-link[aria-current="page"]{ + background:var(--hope-selected-wash)} +.workspace-row{ + display:flex; + align-items:center; + min-width:0} +.workspace-row>.conversation-link{ + flex:1; + min-width:0} +.workspace-menu{ + flex-shrink:0} +.workspace-menu summary{ + cursor:pointer; + min-width:36px; + min-height:44px; + display:grid; + place-items:center; + list-style:none} +.workspace-menu[open]{ + position:relative} +.workspace-tools{ + position:absolute; + right:0; + z-index:var(--z-panel); + width:150px; + background:var(--canvas); + border:1px solid var(--line); + padding:6px; + border-radius:8px} +.workspace-tools .btn{ + width:100%; + justify-content:flex-start} +.sidebar-footer{ + margin-top:22px; + padding-top:12px; + border-top:1px solid var(--line); + display:flex; + flex-direction:column; + gap:2px} +.sidebar-footer>.btn{ + justify-content:flex-start; + text-align:left} +.account-trigger{ + margin-top:10px; + width:100%} +.account-avatar{ + display:grid; + place-items:center; + width:34px; + height:34px; + border-radius:50%; + background:var(--hope-selected-wash); + color:var(--accent); + font-size:24px} +.account-trigger #identityChip{ + flex:1; + min-width:0} +.account-trigger strong{ + display:block; + overflow:hidden; + text-overflow:ellipsis; + white-space:nowrap; + font-weight:600} +.account-trigger small{ + display:block; + font-size:11px; + color:var(--ink-soft); + margin-top:5px} +.tasks-badge{ + margin-left:auto; + border-radius:5px; + background:var(--hope-selected-wash); + padding:2px 6px; + font-size:11px} +#drawerOpen,#drawerClose{ + display:none} + +.primary-chat{ + display:flex; + flex:1; + flex-direction:column; + min-width:0; + min-height:0} +.chat-topbar{ + display:flex; + align-items:center; + gap:16px; + min-height:86px; + padding:16px 38px; + border-bottom:1px solid var(--line); + flex-shrink:0} +.chat-heading{ + flex:1; + min-width:0} +.chat-heading h1{ + font-size:15px; + font-weight:600; + line-height:1.5; + white-space:nowrap; + overflow:hidden; + text-overflow:ellipsis} +.chat-heading p{ + font-size:12px; + color:var(--ink-soft); + margin-top:5px} +.privacy-badge{ + font-size:11px; + color:var(--ink-soft); + white-space:nowrap} +.conversation-actions{ + position:fixed; + inset:86px 20px auto auto; + margin:0; + width:220px; + padding:8px; + border:1px solid var(--line); + border-radius:10px; + background:var(--canvas); + color:var(--ink)} +.conversation-actions:popover-open{ + display:flex; + flex-direction:column} +.conversation-actions .btn{ + width:100%; + text-align:left; + justify-content:flex-start} +.chat-notice{ + font-size:13px; + color:var(--ink-soft); + padding:0 24px; + flex-shrink:0; + overflow-wrap:anywhere} +.chat-notice:not(:empty){ + padding-top:10px; + padding-bottom:10px} +.chat-timeline{ + flex:1; + min-height:0; + overflow:auto; + padding:40px max(28px,calc((100% - 740px)/2)); + scrollbar-gutter:stable} +.chat-welcome{ + max-width:450px; + margin:9vh auto 40px; + text-align:center} +.chat-welcome .hope-mark{ + width:34px; + height:34px} +.chat-welcome h2{ + font-size:30px; + font-weight:500; + letter-spacing:-.025em; + margin:20px 0 12px; + line-height:1.3} +.chat-welcome p{ + font-size:15px; + line-height:1.8; + color:var(--ink-soft)} +.chat-message{ + max-width:74ch; + font-size:15px; + line-height:1.85; + margin-bottom:32px; + overflow-wrap:anywhere} +.chat-message>h2{ + font-size:13px; + font-weight:600; + margin-bottom:12px} +.chat-message.assistant>h2:before{ + content:""; + display:inline-block; + width:13px; + height:13px; + border-radius:3px; + background:var(--accent); + margin-right:9px; + vertical-align:-2px} +.chat-message.user{ + margin-left:auto; + max-width:85%} +.chat-message.user>h2{ + font-size:11px; + color:var(--ink-soft)} +.chat-message.user .markdown{ + background:var(--hope-selected-wash); + padding:14px 18px; + border-radius:12px} +.markdown p{ + margin:0 0 12px} +.markdown p:last-child{ + margin:0} +.markdown h2,.markdown h3,.markdown h4{ + font-size:18px; + margin:20px 0 10px} +.markdown ul,.markdown ol{ + padding-left:23px; + margin:12px 0} +.markdown pre{ + overflow:auto; + padding:16px; + border-radius:8px; + background:var(--surface); + font-size:13px; + line-height:1.6} +.markdown code{ + font-family:ui-monospace,SFMono-Regular,monospace; + background:var(--surface); + padding:2px 4px; + border-radius:3px} +.markdown pre code{ + background:none; + padding:0} +.markdown blockquote{ + margin:12px 0; + padding:12px; + border:1px solid var(--line); + border-radius:8px} +.markdown-table{ + max-width:100%; + overflow:auto; + margin:14px 0} +.markdown-table table{ + border-collapse:collapse; + width:100%; + font-size:14px} +.markdown-table td,.markdown-table th{ + padding:10px; + border:1px solid var(--line); + text-align:left} +.message-actions{ + display:flex; + flex-wrap:wrap; + align-items:center; + gap:2px; + margin-top:8px; + font-size:12px; + color:var(--ink-soft)} +.message-actions .btn{ + font-size:12px; + padding:5px 9px} +.run-progress{ + width:calc(100% - 48px); + max-width:740px; + margin:0 auto; + font-size:13px; + color:var(--ink-soft)} +.primary-composer{ + width:calc(100% - 64px); + max-width:740px; + margin:12px auto 0; + padding:16px 18px 12px; + border:1px solid var(--hope-control-border); + border-radius:12px; + background:var(--canvas); + flex-shrink:0} +.primary-composer textarea{ + width:100%; + min-height:62px; + max-height:160px; + resize:vertical; + border:0; + background:none; + line-height:1.7; + padding:0; + display:block} +.composer-footer{ + display:flex; + align-items:center; + gap:8px; + margin-top:8px} +.composer-right{ + margin-left:auto; + display:flex; + align-items:center; + gap:10px} +.composer-right>span{ + font-size:11px; + color:var(--ink-soft)} +.lightwell-button{ + width:48px; + min-width:48px; + height:48px; + padding:0; + border-radius:9px; + background:var(--accent); + color:var(--accent-ink)} +.lightwell-button:hover:not(:disabled){ + background:var(--accent)} +.lightwell-symbol{ + display:grid; + width:30px; + height:30px; + grid-template-columns:repeat(5,1fr); + grid-template-rows:repeat(5,1fr); + gap:3px; + padding:2px; + place-items:center} +.lightwell-symbol i{ + width:3px; + height:3px; + background:currentColor; + border-radius:50%; + opacity:.8} +.lightwell-symbol i:nth-child(3n){ + opacity:.4} +.send-button{ + width:44px; + min-width:44px; + padding:0; + font-size:23px} +.chat-footnote{ + display:flex; + justify-content:space-between; + gap:12px; + width:calc(100% - 64px); + max-width:740px; + margin:0 auto; + padding:12px 0 20px; + color:var(--ink-soft); + font-size:11px} +.feature-actions{ + display:flex; + align-items:center} +.feature-actions .btn{ + padding:8px; + font-size:12px} +#researchMode{ + max-width:125px; + min-width:0; + min-height:44px; + border:0; + background:transparent; + font-size:12px} +#researchModeLabel{ + font-size:11px; + color:var(--ink-soft)} +.attachment-preview{ + display:flex; + flex-wrap:wrap; + align-items:center; + gap:8px; + padding:10px 0; + font-size:12px; + overflow-wrap:anywhere} +.attachment-preview img{ + object-fit:cover; + border-radius:6px} + +.inline-voice{ + flex-shrink:0; + display:grid; + grid-template-columns:84px minmax(0,1fr) auto; + align-items:center; + gap:16px; + padding:16px 24px; + border-bottom:1px solid var(--line); + background:var(--hope-persona-surface); + max-height:38dvh; + overflow:auto} +.led-wall{ + width:84px; + height:84px; + display:grid; + grid-template-columns:repeat(21,1fr); + grid-template-rows:repeat(21,1fr); + place-items:center; + padding:4px; + color:var(--accent); + border-radius:12px} +.led-dot{ + width:var(--sz,3px); + height:var(--sz,3px); + min-width:1px; + min-height:1px; + border:0; + background:currentColor; + border-radius:50%; + opacity:.2} +.voice-summary h2{ + font-size:12px; + font-weight:500; + color:var(--ink-soft); + margin-bottom:6px} +.voice-summary #stateLabel{ + font-size:18px; + font-weight:600} +.voice-summary #modeDescription,.voice-summary .status,.voice-summary #bargeHint,.voice-summary #thinkingChip{ + font-size:11px; + line-height:1.6; + color:var(--ink-soft); + margin-top:4px} +.voice-controls{ + display:flex; + flex-wrap:wrap; + gap:6px; + justify-content:flex-end; + max-width:240px} +.voice-controls .btn{ + font-size:12px} +.voice-controls #startBtn:disabled,.voice-controls #stopBtn:disabled{ + display:none} +.mic-meter{ + height:2px; + max-width:170px; + background:var(--line); + margin-top:7px} +.mic-meter span{ + display:block; + height:100%; + width:0; + background:var(--accent)} +.live-transcript{ + grid-column:2/-1; + font-size:13px; + line-height:1.65} +.live-transcript #messages:empty{ + display:none} +.message-row{ + margin:10px 0; + overflow-wrap:anywhere} +.message-row.user{ + font-weight:600} +.message-row.system{ + color:var(--ink-soft); + font-size:11px} +.audio-viz{ + display:none} + +.login-gate{ + width:calc(100% - 32px); + max-width:420px; + margin:12vh auto; + padding:28px; + background:var(--surface); + border:1px solid var(--line); + border-radius:12px} +.login-gate h1{ + font-size:26px; + letter-spacing:-.025em} +.login-gate>p{ + margin:14px 0; + line-height:1.6; + color:var(--ink-soft)} +.account-form{ + display:flex; + flex-direction:column; + gap:10px} +.account-form label{ + display:block; + font-size:12px; + font-weight:500; + margin:5px 0} +.account-form input,.account-form select,.account-form textarea,#voiceSearch{ + width:100%; + min-height:44px; + padding:10px 12px; + border:1px solid var(--hope-control-border); + border-radius:7px; + background:var(--canvas); + color:var(--ink)} +.account-form textarea{ + resize:vertical} +.account-form p{ + font-size:13px; + line-height:1.65; + color:var(--ink-soft)} +.dialog-actions{ + display:flex; + justify-content:flex-end; + gap:8px; + flex-wrap:wrap; + margin-top:12px} +dialog{ + margin:auto; + width:calc(100% - 32px); + max-width:600px; + max-height:90dvh; + overflow:auto; + padding:24px; + border:1px solid var(--line); + border-radius:12px; + background:var(--canvas); + color:var(--ink)} +dialog::backdrop{ + background:rgb(30 20 30 / .35)} +dialog h2{ + font-size:22px; + font-weight:600; + margin-bottom:12px} +dialog h3{ + font-size:18px; + font-weight:600; + margin-bottom:8px} +dialog p{ + color:var(--ink-soft); + font-size:13px; + line-height:1.7; + margin:8px 0 18px} +.dialog-head{ + display:flex; + justify-content:space-between; + align-items:flex-start; + gap:16px} +.settings-window{ + width:min(900px,calc(100% - 32px)); + max-width:900px; + padding:0} +.settings-window>.dialog-head{ + padding:22px 26px; + border-bottom:1px solid var(--line)} +.settings-window>.dialog-head h2{ + margin-bottom:0} +.settings-window>.dialog-head p{ + margin:6px 0 0} +.settings-layout{ + display:flex; + min-height:430px} +.settings-nav{ + width:195px; + flex-shrink:0; + padding:18px 12px; + background:var(--surface); + border-right:1px solid var(--line)} +.settings-nav .btn{ + width:100%; + text-align:left; + justify-content:flex-start; + font-size:13px; + margin-bottom:4px} +.settings-nav .selected{ + background:var(--hope-selected-wash); + color:var(--accent); + font-weight:600} +.settings-nav .admin-link{ + border-top:1px solid var(--line); + border-radius:0; + margin-top:18px; + padding-top:15px; + min-height:55px} +.settings-body{ + flex:1; + min-width:0; + padding:26px 30px} +.account-heading{ + display:flex; + flex-direction:column; + gap:6px; + margin:22px 0} +.account-heading small{ + color:var(--ink-soft); + font-size:12px} +#settingsDialog #settingsSheet{ + position:static; + transform:none; + width:auto; + max-height:none; + height:auto; + background:none; + border:0; + padding:0; + overflow:visible} +#settingsDialog #closeSheetBtn{ + display:none} +.sheet-header{ + margin-bottom:20px} +.model-settings{ + border-bottom:1px solid var(--line); + padding-bottom:22px; + margin-bottom:22px} +.model-settings-actions{ + display:flex; + gap:8px} +.model-settings-status[data-state="error"]{ + color:var(--danger)} +.sheet-section-title{ + font-size:16px; + margin-bottom:12px} +.voice-list{ + display:flex; + flex-direction:column; + gap:6px} +.voice-card{ + display:flex; + align-items:center; + justify-content:space-between; + gap:12px; + padding:12px; + border:1px solid transparent; + border-radius:8px; + background:var(--surface); + cursor:pointer; + min-height:54px} +.voice-card.active{ + border-color:var(--accent); + background:var(--hope-selected-wash)} +.voice-name{ + font-size:13px; + overflow-wrap:anywhere} +.voice-card.active .voice-name:after{ + content:" · Selected"; + color:var(--accent)} +.voice-preview{ + background:none; + border:0; + min-height:44px; + padding:8px; + font-size:12px; + color:var(--ink-soft)} +.voice-preview:before{ + content:"Listen"} +.admin-header{ + display:flex; + align-items:flex-start; + justify-content:space-between; + gap:12px} +.admin-header h2{ + font-size:18px} +.table-scroll{ + overflow:auto; + max-width:100%; + margin:16px 0 24px} +.admin-section table{ + border-collapse:collapse; + width:100%; + font-size:12px; + text-align:left} +.admin-section th,.admin-section td{ + padding:10px; + border-bottom:1px solid var(--line); + vertical-align:top; + overflow-wrap:anywhere} +.admin-section th{ + color:var(--ink-soft); + font-weight:500} +.user-actions .btn{ + font-size:11px; + padding:8px; + white-space:nowrap} +.create-user-form{ + display:grid; + grid-template-columns:repeat(2,minmax(0,1fr)); + gap:12px} +.create-user-form .btn{ + justify-self:start} +.admin-section h3{ + margin-top:20px} +.tasks-panel{ + position:fixed; + inset:0 0 0 auto; + z-index:var(--z-panel); + width:min(430px,100%); + height:100dvh; + overflow:auto; + display:flex; + flex-direction:column; + background:var(--canvas); + border-left:1px solid var(--line); + padding:22px} +.tasks-header{ + display:flex; + align-items:flex-start; + justify-content:space-between; + gap:12px; + border-bottom:1px solid var(--line); + padding-bottom:16px} +.tasks-header h2{ + font-size:20px} +.tasks-header p{ + font-size:12px; + color:var(--ink-soft); + margin-top:6px} +.tasks-list{ + padding-top:16px} +.tasks-empty{ + display:flex; + flex-direction:column; + gap:10px; + margin:40px 0; + text-align:center; + font-size:13px; + line-height:1.7; + color:var(--ink-soft)} +.task-card{ + padding:16px 0; + border-bottom:1px solid var(--line); + font-size:13px; + line-height:1.6; + overflow-wrap:anywhere} +.task-card-header{ + display:flex; + gap:8px} +.task-status-dot{ + width:7px; + height:7px; + border-radius:50%; + background:var(--accent); + flex-shrink:0; + margin-top:7px} +.task-error{ + color:var(--danger)} +.task-steps{ + padding-left:20px} +.audio-unlock{ + position:fixed; + bottom:24px; + left:50%; + transform:translateX(-50%); + z-index:var(--z-sheet); + background:var(--accent); + color:var(--accent-ink)} +#hopeFeatures details{ + padding:14px 0; + border-bottom:1px solid var(--line)} +#hopeFeatures summary{ + cursor:pointer; + min-height:44px; + display:flex; + align-items:center; + font-weight:500} +#hopeFeatures form{ + display:flex; + flex-direction:column; + gap:10px} +#hopeFeatures input,#hopeFeatures textarea,#hopeFeatures select{ + max-width:100%; + width:100%; + padding:10px; + border:1px solid var(--hope-control-border); + border-radius:6px; + background:var(--canvas)} +#hopeFeatures pre{ + overflow:auto; + max-width:100%; + white-space:pre-wrap} +#hopeFeatures p{ + margin:10px 0; + line-height:1.6} +.sheet-backdrop{ + position:fixed; + inset:0; + z-index:var(--z-backdrop); + background:rgb(30 20 30 / .35)} + +@media(max-width:960px){ + .chat-sidebar{ + width:220px} +.chat-topbar{ + padding:16px 24px} +.privacy-badge{ + display:none} +.composer-footer{ + gap:4px} +} +@media(max-width:760px){ + .chat-sidebar{ + position:fixed; + inset:0 auto 0 0; + width:min(280px,88vw); + z-index:var(--z-sheet); + transform:translateX(-100%); + transition:transform 180ms ease} +.chat-sidebar.drawer-open{ + transform:none} +#drawerOpen,#drawerClose{ + display:inline-flex} +#drawerClose{ + margin-left:auto} +.drawer-backdrop{ + position:fixed; + inset:0; + width:100%; + height:100%; + border:0; + border-radius:0; + background:rgb(30 20 30 / .35); + z-index:var(--z-backdrop)} +.chat-topbar{ + min-height:70px; + padding:10px 12px; + gap:6px} +.chat-heading h1{ + font-size:13px} +.chat-heading p{ + font-size:11px} +.conversation-actions{ + top:70px; + right:12px} +.chat-timeline{ + padding:24px 22px 20px} +.chat-welcome h2{ + font-size:26px} +.chat-message{ + font-size:14px; + line-height:1.8} +.primary-composer{ + width:calc(100% - 24px); + padding:13px 12px 10px} +.chat-footnote{ + width:calc(100% - 24px); + justify-content:center; + font-size:10px; + padding-bottom:max(12px,env(safe-area-inset-bottom))} +.keyhint{ + display:none} +.composer-right{ + gap:8px} +.feature-actions .btn{ + max-width:44px; + overflow:hidden; + white-space:nowrap; + padding:10px; + font-size:0} +.feature-actions .btn:before{ + content:"+"; + font-size:22px} +#researchMode{ + max-width:90px; + font-size:11px} +.inline-voice{ + grid-template-columns:64px minmax(0,1fr); + gap:10px; + padding:12px 16px; + max-height:40dvh} +.led-wall{ + width:64px; + height:64px} +.voice-controls{ + grid-column:1/-1; + max-width:none; + justify-content:flex-start} +.live-transcript{ + grid-column:1/-1} +.voice-summary #stateLabel{ + font-size:16px} +.voice-summary #modeDescription{ + display:none} +.settings-window>.dialog-head{ + padding:18px} +.settings-layout{ + flex-direction:column} +.settings-nav{ + width:100%; + display:flex; + flex-wrap:wrap; + padding:10px 12px; + border-right:0; + border-bottom:1px solid var(--line); + gap:4px} +.settings-nav .btn{ + width:auto; + margin:0; + padding:8px 10px; + font-size:12px} +.settings-nav .admin-link{ + border:0; + border-radius:8px; + margin:0; + padding:8px 10px; + min-height:44px} +.settings-body{ + padding:22px 18px} +.create-user-form{ + grid-template-columns:1fr} +.admin-header{ + flex-wrap:wrap} +.admin-section table{ + min-width:580px} +.chat-notice{ + font-size:12px} +.composer-footer{ + flex-wrap:wrap} +.composer-right{ + margin-left:auto} +.chat-message.user{ + max-width:95%} +} +@media(prefers-reduced-motion:reduce){ + *,*:before,*:after{ + animation:none!important; + transition:none!important; + scroll-behavior:auto!important} +} + + +/* Keep all composer actions on one row at the smallest supported width. */ +@media(max-width:360px){ + #researchModeLabel,.composer-right>span{ + display:none} +#researchMode{ + max-width:84px} +.composer-right{ + gap:6px} +.composer-footer{ + flex-wrap:nowrap; + gap:4px} } -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - min-height: 100%; - background: var(--canvas-deep); -} - -body { - min-height: 100vh; - min-height: 100dvh; - padding: calc(1.25rem + env(safe-area-inset-top)) - calc(1.25rem + env(safe-area-inset-right)) - calc(1.25rem + env(safe-area-inset-bottom)) - calc(1.25rem + env(safe-area-inset-left)); - overflow-x: hidden; - color: var(--ink); - background: - radial-gradient(circle at 16% 8%, rgba(86, 115, 207, 0.16), transparent 34rem), - radial-gradient(circle at 88% 92%, rgba(255, 179, 142, 0.07), transparent 30rem), - var(--canvas); - font-family: "Avenir Next", "Segoe UI Variable", "Segoe UI", system-ui, -apple-system, sans-serif; - -webkit-font-smoothing: antialiased; -} - -button, -input, -select { - font: inherit; -} - -button { - color: inherit; -} - -[hidden] { - display: none !important; -} - -.skip-link { - position: fixed; - top: 0.75rem; - left: 0.75rem; - z-index: 100; - padding: 0.7rem 1rem; - border-radius: 8px; - color: var(--canvas); - background: var(--ink); - font-size: 0.85rem; - font-weight: 700; - transform: translateY(-160%); - transition: transform 180ms var(--ease-out); -} - -.skip-link:focus { - transform: translateY(0); -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} - -.app-shell { - width: min(1180px, 100%); - margin: 0 auto; -} - -/* ── Header ─────────────────────────────────────────────────────────────── */ -.topbar { - min-height: 62px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 1.25rem; - margin-bottom: 1.25rem; - padding: 0.25rem 0.15rem; -} - -.brand, -.topbar-actions, -.identity-chip, -.tasks-trigger { - display: flex; - align-items: center; -} - -.brand { - gap: 0.8rem; -} - -.brand-glyph { - position: relative; - width: 38px; - height: 38px; - flex: 0 0 38px; - border: 1px solid rgba(139, 167, 255, 0.55); - border-radius: 50%; -} - -.brand-glyph::before, -.brand-glyph::after, -.brand-glyph i { - content: ""; - position: absolute; - border-radius: 50%; -} - -.brand-glyph::before { - inset: 7px; - border: 1px solid rgba(139, 167, 255, 0.35); -} - -.brand-glyph::after { - width: 8px; - height: 8px; - right: 4px; - top: 5px; - background: var(--warm); - box-shadow: 0 0 12px rgba(255, 179, 142, 0.52); -} - -.brand-glyph i { - width: 6px; - height: 6px; - inset: 0; - margin: auto; - background: var(--iris); -} - -.brand h1 { - color: var(--ink); - font-size: 1.18rem; - font-weight: 680; - letter-spacing: -0.02em; -} - -.brand p { - margin-top: 0.1rem; - color: var(--ink-muted); - font-size: 0.74rem; -} - -.topbar-actions { - justify-content: flex-end; - gap: 0.65rem; -} - -.mode-switch { - height: 38px; - display: flex; - align-items: center; - gap: 2px; - padding: 3px; - border: 1px solid var(--line); - border-radius: 10px; - background: rgba(5, 11, 20, 0.55); -} - -.mode-option { - height: 30px; - display: inline-flex; - align-items: center; - gap: 0.38rem; - padding: 0 0.65rem; - border: 0; - border-radius: 7px; - cursor: pointer; - color: var(--ink-muted); - background: transparent; - font-size: 0.72rem; - font-weight: 650; - transition: color 160ms ease, background 160ms ease, box-shadow 160ms ease; -} - -.mode-option:hover { - color: var(--ink-soft); -} - -.mode-option.active { - color: var(--ink); - background: var(--surface-soft); - box-shadow: 0 1px 5px rgba(2, 7, 14, 0.3); -} - -.mode-option:focus-visible, -.chat-composer textarea:focus-visible, -.send-chat:focus-visible { - outline: 2px solid var(--focus); - outline-offset: 2px; -} - -.identity-chip { - min-height: 38px; - gap: 0.55rem; - padding: 0 0.85rem; - border: 1px solid var(--line); - border-radius: 999px; - color: var(--ink-soft); - background: rgba(13, 25, 41, 0.72); - font-size: 0.78rem; -} - -.identity-chip strong { - color: var(--ink); - font-weight: 650; -} - -.presence-dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--mint); - box-shadow: 0 0 0 4px rgba(118, 217, 178, 0.1); -} - -/* ── Main workspace ─────────────────────────────────────────────────────── */ -.workspace { - display: grid; - grid-template-columns: minmax(390px, 0.92fr) minmax(420px, 1.08fr); - grid-template-rows: minmax(0, 1fr); - height: min(700px, calc(100dvh - 108px)); - min-height: 610px; - border: 1px solid var(--line); - border-radius: 16px; - overflow: hidden; - background: var(--surface); - box-shadow: 0 8px 8px rgba(2, 7, 14, 0.18); -} - -.voice-console { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - min-width: 0; - min-height: 0; - padding: clamp(2rem, 4vw, 3.5rem) clamp(1.5rem, 4vw, 3.25rem) 1.6rem; - overflow: hidden; - background: - radial-gradient(circle at 50% 38%, rgba(90, 121, 215, 0.14), transparent 18rem), - var(--surface); -} - -.voice-console::after { - content: ""; - position: absolute; - left: 10%; - right: 10%; - bottom: 0; - height: 1px; - background: linear-gradient(90deg, transparent, rgba(139, 167, 255, 0.34), transparent); -} - -.console-intro { - position: relative; - z-index: 2; - width: 100%; - text-align: center; -} - -.console-intro h2 { - color: var(--ink); - font-size: 2rem; - font-weight: 620; - letter-spacing: -0.035em; - text-wrap: balance; -} - -.console-intro p { - max-width: 38ch; - margin: 0.55rem auto 0; - color: var(--ink-muted); - font-size: 0.91rem; - line-height: 1.55; - text-wrap: pretty; -} - -/* ── Voice presence ─────────────────────────────────────────────────────── */ -.orb-stage { - position: relative; - z-index: 1; - flex: 1; - width: 100%; - min-height: 310px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding-top: 0.75rem; -} - -/* ── LED wall avatar ────────────────────────────────────────────────────── */ -.led-wall { - position: relative; - z-index: 1; - width: 320px; - height: 320px; - max-width: 100%; - display: grid; - grid-template-columns: repeat(21, 1fr); - grid-template-rows: repeat(21, 1fr); - gap: 2px; - place-items: center; - padding: 12px; - border-radius: 26px; - isolation: isolate; - transition: filter 220ms ease; -} - -/* A soft glass panel that holds the matrix up off the page. */ -.led-wall::before { - content: ""; - position: absolute; - inset: 0; - border-radius: 26px; - border: 1px solid rgba(139, 167, 255, 0.14); - background: - radial-gradient(circle at 50% 50%, rgba(111, 143, 247, 0.1), transparent 62%), - rgba(6, 12, 24, 0.4); - z-index: -1; -} - -.led-dot { - --sz: 5px; - width: var(--sz); - height: var(--sz); - min-width: 3px; - min-height: 3px; - border: 0; - border-radius: 50%; - background: currentColor; - opacity: 0.16; - transition: opacity 90ms linear, width 90ms linear, height 90ms linear; -} - - -/* Palette shifts by state — a cool blue while it listens, warm toward the - center as the agent speaks, amber while thinking, dim when idle. */ -.led-wall[data-state="idle"] { - color: #56739e; -} - -.led-wall[data-state="connecting"], -.led-wall[data-state="thinking"] { - color: #d99a6c; -} - -.led-wall[data-state="listening"] { - color: #6f8ff7; -} - -.led-wall[data-state="speaking"] { - color: #9dc2ff; - filter: drop-shadow(0 0 14px rgba(111, 143, 247, 0.35)); -} - -.orb-labels { - min-height: 46px; - margin-top: 0.2rem; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.35rem; -} - -.state-label { - color: var(--ink-soft); - font-size: 0.83rem; - font-weight: 600; -} - -.thinking-chip, -.barge-hint { - color: var(--ink-muted); - font-size: 0.73rem; -} - -.thinking-chip::before { - content: ""; - display: inline-block; - width: 5px; - height: 5px; - margin: 0 0.42rem 0.1rem 0; - border-radius: 50%; - background: var(--warm); - vertical-align: middle; - animation: think-pulse 1.2s ease-in-out infinite; -} - -@keyframes think-pulse { - 50% { opacity: 0.28; transform: scale(0.75); } -} - -/* ── Buttons and controls ───────────────────────────────────────────────── */ -.controls { - position: relative; - z-index: 2; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.6rem; - width: 100%; -} - -.btn { - min-height: 42px; - border: 0; - border-radius: 9px; - padding: 0.68rem 1rem; - cursor: pointer; - font-size: 0.84rem; - font-weight: 650; - line-height: 1; - transition: color 180ms ease, background 180ms ease, border-color 180ms ease, transform 180ms var(--ease-out), opacity 180ms ease; -} - -.btn:hover:not(:disabled) { - transform: translateY(-1px); -} - -.btn:active:not(:disabled) { - transform: translateY(0); -} - -.btn:focus-visible, -.voice-card:focus-visible { - outline: 2px solid var(--focus); - outline-offset: 3px; -} - -.btn:disabled { - opacity: 0.38; - cursor: not-allowed; -} - -.btn-primary { - position: relative; - min-width: 180px; - color: #07101d; - background: var(--ink); -} - -.btn-primary:hover:not(:disabled) { - background: #dfe7f4; -} - -.button-spark { - display: inline-block; - width: 7px; - height: 7px; - margin-right: 0.5rem; - border-radius: 50%; - background: var(--iris-deep); - box-shadow: 0 0 0 4px rgba(60, 95, 201, 0.12); -} - -.btn-primary:disabled, -.btn-danger:disabled { - display: none; -} - -.btn-danger { - color: #fff4f1; - background: #8f403a; -} - -.btn-danger:hover:not(:disabled) { - background: #a84b43; -} - -.btn-secondary { - color: var(--ink-soft); - background: var(--surface-raised); - border: 1px solid var(--line); -} - -.btn-secondary:hover:not(:disabled) { - color: var(--ink); - border-color: var(--line-strong); - background: var(--surface-soft); -} - -.btn-icon { - width: 42px; - min-width: 42px; - height: 42px; - padding: 0; - display: inline-grid; - place-items: center; - color: var(--ink-muted); - background: var(--surface-raised); - border: 1px solid var(--line); -} - -.btn-icon:hover:not(:disabled) { - color: var(--ink); - border-color: var(--line-strong); - background: var(--surface-soft); -} - -.btn-icon.muted { - color: #fff; - background: var(--danger); - border-color: var(--danger); -} - -.btn-icon.muted:hover:not(:disabled) { - color: #fff; - background: var(--danger); - border-color: var(--danger); -} - -.btn-ghost { - min-height: 34px; - padding: 0.45rem 0.65rem; - color: var(--ink-muted); - background: transparent; -} - -.btn-ghost:hover:not(:disabled) { - color: var(--ink); - background: var(--surface-raised); -} - -.tasks-trigger { - position: relative; - min-height: 38px; - gap: 0.45rem; - padding: 0 0.8rem; - font-size: 0.78rem; -} - -.tasks-badge { - min-width: 17px; - height: 17px; - display: inline-grid; - place-items: center; - padding: 0 4px; - border-radius: 999px; - color: #07101d; - background: var(--warm); - font-size: 0.64rem; - font-weight: 750; -} - -.connection-strip { - width: min(340px, 100%); - flex-shrink: 0; - margin-top: 1rem; -} - -.status { - min-height: 20px; - color: var(--ink-muted); - font-size: 0.72rem; - text-align: center; -} - -.mic-meter { - height: 3px; - margin-top: 0.55rem; - overflow: hidden; - border-radius: 999px; - background: rgba(139, 167, 255, 0.1); -} - -.mic-meter-fill { - display: block; - width: 0; - height: 100%; - border-radius: inherit; - background: var(--iris); - transition: width 80ms linear; -} - -.privacy-note { - display: flex; - flex-shrink: 0; - align-items: center; - gap: 0.4rem; - margin-top: 1.15rem; - color: var(--ink-muted); - font-size: 0.7rem; -} - -.privacy-note svg { - color: var(--mint); -} - -/* ── Conversation ───────────────────────────────────────────────────────── */ -.transcript { - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - padding: clamp(1.4rem, 3vw, 2.1rem); - border-left: 1px solid var(--line); - background: #0a1524; -} - -.transcript-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding-bottom: 1.1rem; - border-bottom: 1px solid var(--line); -} - -.transcript-header h2 { - color: var(--ink); - font-size: 1rem; - font-weight: 650; - letter-spacing: -0.01em; -} - -.transcript-header p { - margin-top: 0.22rem; - color: var(--ink-muted); - font-size: 0.7rem; -} - -#messages { - flex: 1; - min-height: 0; - overflow-y: auto; - padding: 1.25rem 0.2rem 1rem 0; - scroll-behavior: smooth; -} - -#messages:empty { - display: grid; - place-items: center; -} - -#messages:empty::before { - content: "Your conversation will appear here.\A Start when you’re ready—there’s nothing to configure."; - max-width: 30ch; - color: var(--ink-muted); - font-size: 0.84rem; - line-height: 1.65; - text-align: center; - white-space: pre-wrap; -} - -.message-row { - position: relative; - display: flex; - align-items: flex-end; - gap: 0.55rem; - margin-bottom: 0.85rem; - animation: message-in 180ms var(--ease-out); -} - -.message-row.user { - justify-content: flex-end; -} - -.message-row.system { - justify-content: center; -} - -.message-row.system .bubble { - padding: 0.35rem 0.65rem; - color: var(--ink-muted); - background: transparent; - font-size: 0.7rem; -} - -@keyframes message-in { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } -} - -.avatar-dot { - position: relative; - width: 27px; - height: 27px; - flex: 0 0 27px; - margin-bottom: 2px; - border: 1px solid rgba(139, 167, 255, 0.38); - border-radius: 50%; -} - -.avatar-dot::after { - content: ""; - position: absolute; - inset: 8px; - border-radius: 50%; - background: var(--iris); -} - -.bubble { - max-width: min(82%, 58ch); - padding: 0.72rem 0.9rem; - border-radius: 12px; - color: var(--ink-soft); - font-size: 0.9rem; - line-height: 1.55; - overflow-wrap: anywhere; -} - -.message-row.user .bubble { - color: #e8efff; - background: var(--user-bubble); - border-bottom-right-radius: 3px; -} - -.message-row.agent .bubble { - background: var(--agent-bubble); - border-bottom-left-radius: 3px; -} - -.message-row.partial, -.bubble.partial { - opacity: 0.58; -} - -.message-row::after { - content: attr(data-ts); - position: absolute; - bottom: -0.9rem; - color: var(--ink-muted); - font-size: 0.62rem; - opacity: 0; - pointer-events: none; - transition: opacity 150ms ease; -} - -.message-row:hover::after { - opacity: 0.75; -} - -.message-row.user::after { right: 0; } -.message-row.agent::after { left: 2rem; } - -.audio-viz { - height: 13px; - margin-left: 0.5rem; - display: inline-flex; - align-items: flex-end; - gap: 2px; - vertical-align: middle; -} - -.audio-viz i { - width: 2px; - height: 15%; - border-radius: 2px; - background: var(--iris); - opacity: 0.78; - transition: height 70ms linear; -} - -/* ── Text composer ──────────────────────────────────────────────────────── */ -.chat-composer { - position: relative; - display: none; - align-items: flex-end; - gap: 0.55rem; - padding: 0.75rem 0 1.25rem; - border-top: 1px solid var(--line); -} - -body[data-mode="text"] .chat-composer { - display: flex; -} - -.chat-composer textarea { - width: 100%; - min-height: 48px; - max-height: 144px; - resize: none; - padding: 0.8rem 3.2rem 0.8rem 0.9rem; - border: 1px solid var(--line-strong); - border-radius: 12px; - color: var(--ink); - background: var(--surface-raised); - font: inherit; - font-size: 0.9rem; - line-height: 1.4; - caret-color: var(--iris); - transition: border-color 160ms ease, background 160ms ease; -} - -.chat-composer textarea::placeholder { - color: var(--ink-muted); -} - -.chat-composer textarea:focus { - border-color: rgba(139, 167, 255, 0.56); - background: var(--surface-soft); -} - -.send-chat { - position: absolute; - right: 0.55rem; - bottom: 2.05rem; - width: 34px; - height: 34px; - display: grid; - place-items: center; - border: 0; - border-radius: 9px; - cursor: pointer; - color: #08111f; - background: var(--iris); - transition: opacity 150ms ease, transform 150ms var(--ease-out), background 150ms ease; -} - -.send-chat:hover:not(:disabled) { - background: #a9bbff; - transform: translateY(-1px); -} - -.send-chat:disabled { - cursor: default; - opacity: 0.25; -} - -.composer-hint { - position: absolute; - left: 0.2rem; - bottom: 0.25rem; - color: var(--ink-muted); - font-size: 0.62rem; -} - -/* Text mode makes the transcript the primary surface while preserving Hope's - live presence and the same underlying conversation. */ -body[data-mode="text"] .workspace { - grid-template-columns: minmax(240px, 0.62fr) minmax(480px, 1.38fr); -} - -body[data-mode="text"] .voice-console { - padding: 2rem 1.5rem 1.4rem; -} - -body[data-mode="text"] .console-intro h2 { - font-size: 1.55rem; -} - -body[data-mode="text"] .orb-stage { - min-height: 220px; -} - -body[data-mode="text"] .led-wall { - width: 260px; - height: 260px; -} - -body[data-mode="text"] .controls { - max-width: 220px; -} - -body[data-mode="text"] #muteBtn { - display: none; -} - -body[data-mode="text"] .mic-meter { - visibility: hidden; -} - -/* ── Voice settings ─────────────────────────────────────────────────────── */ -.sheet-backdrop { - position: fixed; - inset: 0; - z-index: var(--z-backdrop); - background: rgba(2, 7, 14, 0.68); - opacity: 0; - transition: opacity 200ms ease; -} - -.sheet-backdrop:not([hidden]) { - opacity: 1; -} - -.settings-sheet { - position: fixed; - top: 0; - left: 50%; - z-index: var(--z-sheet); - width: min(660px, 100%); - height: 100dvh; - padding: 0.75rem 1.25rem calc(1.25rem + env(safe-area-inset-bottom)); - display: flex; - flex-direction: column; - border: 1px solid var(--line-strong); - border-radius: 16px; - background: #101e31; - transform: translate(-50%, 102%); - transition: transform 240ms var(--ease-out); -} - -.settings-sheet.open { - transform: translate(-50%, 0); -} - -.sheet-handle { - width: 36px; - height: 3px; - margin: 0 auto 1rem; - border-radius: 999px; - background: var(--line-strong); -} - -.sheet-header, -.tasks-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.sheet-header { - padding-bottom: 1rem; -} - -.sheet-header h2, -.tasks-header h2 { - color: var(--ink); - font-size: 1rem; - font-weight: 650; -} - -.sheet-header p, -.tasks-header p { - margin-top: 0.25rem; - color: var(--ink-muted); - font-size: 0.72rem; -} - -.sheet-section-title { - margin: 0.4rem 0 0.55rem; - padding: 0 0.2rem; - color: var(--ink-muted); - font-size: 0.68rem; - font-weight: 700; - letter-spacing: 0.14em; - text-transform: uppercase; -} - -.sheet-note { - margin: 0.5rem 0 0; - color: var(--ink-muted); - font-size: 0.72rem; -} - -.voice-list { - min-height: 0; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 0.15rem 0.2rem 0 0; -} - -/* One scroll area keeps both model controls and all voices reachable. */ -.sheet-content { - min-height: 0; - overflow-y: auto; - padding: 0.15rem 0.2rem 0.25rem; -} -.settings-sheet .sheet-header { flex-shrink: 0; } -.settings-sheet .voice-list { overflow-y: visible; } -#voiceSearch { - width: 100%; min-width: 0; min-height: 44px; margin-top: 0.4rem; - padding: 0.65rem 0.75rem; border: 1px solid var(--line); border-radius: 10px; - background: var(--surface-soft); color: var(--ink); font: inherit; -} -#voiceSearch::placeholder { color: var(--ink-soft); opacity: 1; } -#voiceSearch:focus-visible { outline: 2px solid var(--iris); outline-offset: 2px; } -#voiceCatalogStatus { margin: 0.6rem 0; } -#retryVoices { margin-bottom: 0.6rem; } -.voice-name { overflow-wrap: anywhere; } -.model-settings { - min-width: 0; - padding-bottom: 1.25rem; - margin-bottom: 1rem; - border-bottom: 1px solid var(--line); -} -.model-settings h3 { font-size: 0.9rem; font-weight: 650; } -.model-settings select { - width: 100%; - max-width: 100%; - min-width: 0; - text-overflow: ellipsis; - font-size: 0.85rem; -} -.model-settings select:disabled { color: var(--ink-soft); cursor: not-allowed; } -.model-settings-note, -.model-settings-status { - color: var(--ink-soft); - font-size: 0.78rem; - line-height: 1.55; - overflow-wrap: anywhere; -} -#thinkingLevelNote { margin-top: 0.5rem; } -.model-settings-status[data-state="error"] { color: var(--danger); } -.model-settings-status[data-state="success"] { color: var(--mint); } -.model-settings-actions { display: flex; flex-wrap: wrap; gap: 0.65rem; } -.model-settings-actions .btn { min-width: 90px; } -.model-settings-actions .btn:disabled { display: inline-block; } - -.voice-card { - min-height: 54px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.8rem; - padding: 0.65rem 0.75rem 0.65rem 1rem; - border: 1px solid transparent; - border-radius: 10px; - cursor: pointer; - color: var(--ink-soft); - background: rgba(23, 41, 65, 0.55); - transition: color 160ms ease, background 160ms ease, border-color 160ms ease; -} - -.voice-card:hover { - color: var(--ink); - background: var(--surface-soft); -} - -.voice-card.active { - color: var(--ink); - border-color: rgba(139, 167, 255, 0.42); - background: rgba(60, 95, 201, 0.18); -} - -.voice-name { - font-size: 0.86rem; - font-weight: 600; -} - -.voice-card.active .voice-name::after { - content: "Selected"; - display: inline-block; - margin-left: 0.55rem; - color: var(--iris); - font-size: 0.66rem; - font-weight: 550; -} - -.voice-preview { - min-height: 32px; - color: var(--ink-muted); -} - -.voice-preview::before { - content: "Listen"; -} - -/* ── Tasks ──────────────────────────────────────────────────────────────── */ -.tasks-panel { - position: fixed; - top: 0; - right: 0; - z-index: var(--z-panel); - width: min(380px, 92vw); - height: 100dvh; - display: flex; - flex-direction: column; - border-left: 1px solid var(--line-strong); - background: #0d1929; - transform: translateX(102%); - transition: transform 240ms var(--ease-out); -} - -.tasks-panel.open { - transform: translateX(0); -} - -.tasks-header { - padding: calc(1.1rem + env(safe-area-inset-top)) 1.1rem 1rem; - border-bottom: 1px solid var(--line); -} - -.tasks-list { - flex: 1; - overflow-y: auto; - padding: 0.9rem; - display: flex; - flex-direction: column; - gap: 0.65rem; -} - -.tasks-empty { - min-height: 180px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 0.35rem; - color: var(--ink-muted); - text-align: center; - font-size: 0.78rem; -} - -.tasks-empty strong { - color: var(--ink-soft); - font-size: 0.86rem; -} - -.task-card { - padding: 0.85rem; - border-radius: 10px; - color: var(--ink-soft); - background: var(--surface-raised); - font-size: 0.8rem; -} - -.task-card-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.45rem; -} - -.task-status-dot { - width: 7px; - height: 7px; - flex: 0 0 7px; - border-radius: 50%; - background: var(--ink-muted); -} - -.task-status-dot.running { - background: var(--iris); - animation: status-pulse 1.4s ease-in-out infinite; -} - -.task-status-dot.completed { background: var(--mint); } -.task-status-dot.failed { background: var(--danger); } - -@keyframes status-pulse { - 50% { opacity: 0.3; } -} - -.task-desc { - flex: 1; - overflow: hidden; - color: var(--ink); - font-weight: 600; - text-overflow: ellipsis; - white-space: nowrap; -} - -.task-steps { - max-height: 200px; - margin-top: 0.5rem; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 0.3rem; - list-style: none; -} - -.task-step, -.task-result, -.task-error { - padding: 0.42rem 0.5rem; - border-radius: 6px; - font-size: 0.72rem; -} - -.task-step { - color: var(--ink-muted); - background: rgba(5, 11, 20, 0.3); -} - -.task-step.tool_call { color: #a9bbff; } -.task-step.tool_result { color: var(--ink-muted); } -.task-step.text { color: var(--ink-soft); } -.task-result { margin-top: 0.5rem; color: #bcebd8; background: rgba(118, 217, 178, 0.1); } -.task-error { margin-top: 0.5rem; color: #ffc2ba; background: rgba(255, 128, 111, 0.1); } - -/* ── Responsive ─────────────────────────────────────────────────────────── */ -@media (max-height: 760px) and (min-width: 881px) { - .workspace { - min-height: 540px; - } - - .voice-console { - padding-top: 1.5rem; - padding-bottom: 1rem; - } - - .console-intro h2 { - font-size: 1.65rem; - } - - .console-intro p { - margin-top: 0.35rem; - } - - .orb-stage { - min-height: 220px; - margin: -1rem 0; - } - - .led-wall { - width: 260px; - height: 260px; - } - - .connection-strip, - .privacy-note { - margin-top: 0.65rem; - } -} - -@media (max-width: 880px) { - body { - padding: calc(0.85rem + env(safe-area-inset-top)) - calc(0.85rem + env(safe-area-inset-right)) - calc(0.85rem + env(safe-area-inset-bottom)) - calc(0.85rem + env(safe-area-inset-left)); - } - - .workspace, - body[data-mode="text"] .workspace { - grid-template-columns: 1fr; - height: auto; - min-height: auto; - } - - .voice-console { - min-height: 620px; - } - - .transcript { - min-height: 430px; - max-height: 62dvh; - border-top: 1px solid var(--line); - border-left: 0; - } -} - -@media (max-width: 560px) { - body { - padding-right: env(safe-area-inset-right); - padding-left: env(safe-area-inset-left); - } - - .topbar { - padding-right: 0.85rem; - padding-left: 0.85rem; - margin-bottom: 0.65rem; - } - - .brand p, - .identity-chip, - .mode-option svg { - display: none; - } - - .mode-option { - padding: 0 0.55rem; - } - - .workspace { - border-right: 0; - border-left: 0; - border-radius: 0; - } - - .voice-console { - min-height: calc(100dvh - 82px); - padding: 2.1rem 1.15rem 1.25rem; - } - - body[data-mode="text"] .voice-console { - min-height: 270px; - padding: 1.35rem 1rem 1rem; - } - - body[data-mode="text"] .orb-stage { - min-height: 120px; - margin: -0.75rem 0; - } - - body[data-mode="text"] .led-wall { - width: 210px; - height: 210px; - } - - .console-intro h2 { - font-size: 1.7rem; - } - - .orb-stage { - min-height: 300px; - } - - .led-wall { - width: 270px; - height: 270px; - padding: 9px; - } - - .controls { - max-width: 330px; - } - - .btn-primary, - .btn-danger { - flex: 1 1 180px; - } - - .privacy-note { - margin-top: 1rem; - } - - .transcript { - min-height: 480px; - max-height: none; - padding: 1.25rem 1rem; - } - - .tasks-panel { - width: 100vw; - } - - .bubble { - max-width: 86%; - } -} - -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - scroll-behavior: auto !important; - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} - -/* Account surfaces share the conversation's existing tokens. */ -.login-gate { width: min(440px, calc(100% - 2rem)); margin: 12vh auto 3rem; padding: 2rem; border: 1px solid var(--line-strong); border-radius: 16px; background: var(--surface); } -.login-gate h1 { font-size: 1.6rem; letter-spacing: -.025em; } -.login-gate > p { margin: .75rem 0 1.5rem; color: var(--ink-soft); line-height: 1.6; } -.account-form { display: flex; flex-direction: column; gap: .65rem; } -.account-form label { display: block; font-size: .85rem; color: var(--ink-soft); margin-bottom: .3rem; } -.account-form input, .account-form select { width: 100%; min-height: 44px; padding: .65rem .75rem; border: 1px solid var(--line-strong); border-radius: 8px; color: var(--ink); background: var(--canvas); } -.account-form input:focus-visible, .account-form select:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; } -.account-form .btn { margin-top: .5rem; } -.account-form .btn:disabled { display: inline-block; } -.account-toolbar { display: flex; align-items: center; gap: .65rem; margin: 0 0 1rem; padding: 0 .15rem; } -.account-toolbar p { flex: 1; color: var(--warm); font-size: .85rem; line-height: 1.5; } -.admin-section { margin-bottom: 1.5rem; padding: 1.5rem; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); } -.admin-header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; } -.admin-header h2 { font-size: 1.2rem; } -.admin-header p, #adminMessage, #createUserMessage { margin: .5rem 0 1rem; color: var(--ink-soft); font-size: .85rem; line-height: 1.5; } -.table-scroll { overflow-x: auto; margin-bottom: 1.5rem; } -.admin-section table { width: 100%; border-collapse: collapse; text-align: left; font-size: .8rem; } -.admin-section th, .admin-section td { padding: .8rem .6rem; border-bottom: 1px solid var(--line); vertical-align: top; } -.admin-section th { color: var(--ink-soft); font-weight: 600; } -.admin-section td { max-width: 28ch; overflow-wrap: anywhere; } -.user-actions { min-width: 190px; } -.user-actions .btn { margin: 0 .3rem .4rem 0; font-size: .75rem; } -.admin-section h3 { font-size: 1rem; margin-bottom: .8rem; } -.create-user-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; align-items: end; } -.create-user-form .btn { justify-self: start; } -#accountDialog { margin: auto; padding: 1.5rem; width: min(480px, calc(100% - 2rem)); border: 1px solid var(--line-strong); border-radius: 12px; color: var(--ink); background: var(--surface); } -#accountDialog::backdrop { background: rgba(2, 7, 14, .75); } -#accountDialog h2 { font-size: 1.2rem; overflow-wrap: anywhere; } -#accountDialog p { color: var(--ink-soft); line-height: 1.55; font-size: .9rem; } -.dialog-actions { display: flex; gap: .75rem; justify-content: flex-end; } -.task-card p { margin-top: .5rem; line-height: 1.5; overflow-wrap: anywhere; } -.audio-unlock { position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); z-index: var(--z-sheet); } -.topbar-actions { flex-wrap: wrap; } -.identity-chip { max-width: 28ch; overflow-wrap: anywhere; } -@media (max-width: 560px) { - .topbar { flex-wrap: wrap; } - .topbar-actions { width: 100%; justify-content: flex-start; } - .identity-chip { display: flex; } - .account-toolbar { padding: 0 .85rem; flex-wrap: wrap; } - .account-toolbar p { flex-basis: 100%; } - .admin-section { padding: 1rem; border-radius: 0; } - .create-user-form { grid-template-columns: 1fr; } - .admin-header { flex-wrap: wrap; } - .login-gate { padding: 1.5rem; margin-top: 7vh; } -} - -/* Primary Hope shell: quiet Opal, system-native typography, a single reading column. */ -:root { --accent: #8c3d63; --accent-ink: #fff; } -body {padding:0;background:var(--canvas);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;} -.app-shell {display:grid;grid-template-columns:264px minmax(0,1fr);height:100dvh;width:100%;max-width:none;margin:0;gap:0;} -.chat-sidebar {background:var(--surface);border-right:1px solid var(--line);display:flex;flex-direction:column;padding:24px 16px 16px;overflow:auto;min-width:0;} -.sidebar-brand {display:flex;align-items:center;gap:12px;margin-bottom:24px;padding:0 10px;font-size:23px;letter-spacing:-.025em;} -.hope-mark {font-size:32px;color:var(--accent);line-height:1;} -.new-chat {justify-content:flex-start;background:var(--surface-raised);width:100%;margin-bottom:16px;} -.chat-sidebar input {width:100%;padding:11px 12px;background:var(--canvas);border:1px solid var(--line);border-radius:8px;color:var(--ink);font-size:14px;} -.chat-sidebar nav {margin-top:22px;flex:1;} -.chat-sidebar h2 {font-size:12px;font-weight:600;color:var(--ink-muted);margin:22px 10px 8px;letter-spacing:0;text-transform:none;} -.conversation-link {border:0;background:transparent;color:var(--ink);display:block;text-align:left;border-radius:8px;width:100%;padding:10px;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-height:40px;cursor:pointer;} -.conversation-link:hover,.conversation-link[aria-current="page"] {background:var(--surface-raised);} -.nav-heading {display:flex;align-items:center;justify-content:space-between;} -.workspace-row {display:flex;align-items:center;flex-wrap:wrap;} -.workspace-row .conversation-link {width:100%;} -.workspace-row .btn {font-size:12px;min-height:30px;padding:4px 10px;color:var(--ink-muted);} -.sidebar-footer {display:flex;flex-direction:column;gap:4px;margin-top:20px;border-top:1px solid var(--line);padding-top:14px;} -.sidebar-footer .btn {justify-content:flex-start;} -.primary-chat {display:flex;flex-direction:column;min-width:0;min-height:0;background:var(--canvas);} -.chat-topbar {display:flex;align-items:center;gap:16px;padding:18px 28px;border-bottom:1px solid var(--line);min-height:76px;flex-wrap:wrap;} -.chat-topbar h1 {font-size:16px;line-height:1.4;letter-spacing:-.015em;flex:1;min-width:100px;overflow-wrap:anywhere;} -.conversation-actions {display:flex;gap:2px;flex-wrap:wrap;} -.conversation-actions .btn {font-size:12px;padding:8px;min-height:36px;} -.chat-notice {font-size:14px;color:var(--ink-muted);margin:0 28px;} -.chat-notice:not(:empty) {padding:12px 0;} -.chat-timeline {flex:1;min-height:0;overflow:auto;padding:30px max(24px,calc((100% - 740px)/2));scrollbar-gutter:stable;} -.chat-welcome {margin:clamp(50px,15vh,180px) auto 32px;text-align:center;max-width:440px;} -.chat-welcome h2 {font-size:28px;font-weight:500;letter-spacing:-.025em;line-height:1.2;margin:20px 0 12px;text-wrap:balance;} -.chat-welcome p {color:var(--ink-muted);font-size:15px;line-height:1.7;} -.chat-message {margin:0 0 32px;max-width:74ch;font-size:16px;line-height:1.75;overflow-wrap:anywhere;} -.chat-message h2 {font-size:13px;font-weight:600;margin-bottom:10px;color:var(--ink-muted);} -.chat-message.user .markdown {background:var(--surface);border-radius:12px;padding:14px 18px;} -.markdown p {margin:0 0 10px;text-wrap:pretty;} -.markdown p:last-child {margin-bottom:0;} -.markdown h2,.markdown h3,.markdown h4 {font-size:19px;margin:20px 0 8px;color:var(--ink);} -.markdown ul,.markdown ol {padding-left:24px;margin:12px 0;} -.markdown a {color:var(--accent);text-decoration:underline;} -.markdown pre {overflow:auto;max-width:100%;padding:16px;background:var(--surface);border-radius:8px;font-size:13px;line-height:1.6;margin:14px 0;} -.markdown code {font-family:ui-monospace,SFMono-Regular,Consolas,monospace;background:var(--surface-raised);padding:2px 4px;border-radius:3px;} -.markdown pre code {padding:0;background:none;} -.markdown blockquote {border:1px solid var(--line);padding:12px 16px;margin:12px 0;border-radius:8px;} -.message-actions {display:flex;flex-wrap:wrap;gap:2px;margin-top:8px;align-items:center;font-size:12px;color:var(--ink-muted);} -.message-actions .btn {font-size:12px;min-height:32px;padding:4px 8px;} -.primary-composer {width:calc(100% - 48px);max-width:740px;margin:12px auto 0;border:1px solid var(--line);border-radius:14px;padding:16px;background:var(--surface);} -.primary-composer textarea {display:block;resize:vertical;min-height:62px;max-height:220px;width:100%;border:0;background:none;color:var(--ink);font:inherit;line-height:1.6;outline-offset:4px;} -textarea::placeholder,input::placeholder {color:var(--ink-muted);opacity:1;} -.composer-footer {display:flex;align-items:center;gap:8px;justify-content:flex-end;margin-top:12px;} -.composer-footer span {font-size:11px;color:var(--ink-muted);margin-right:auto;} -.chat-footnote {font-size:11px;color:var(--ink-muted);text-align:center;padding:12px 20px 18px;} -.run-progress {max-width:740px;width:calc(100% - 48px);margin:0 auto;font-size:14px;color:var(--ink-muted);} -.btn {border-radius:8px;min-height:40px;padding:9px 14px;box-shadow:none;font-weight:500;cursor:pointer;} -.btn-primary {background:var(--accent);color:var(--accent-ink);border:0;} -.btn-primary:hover:not(:disabled) {filter:brightness(1.08);transform:none;} -.btn-secondary,.btn-ghost,.btn-icon {background:transparent;color:var(--ink);border:0;} -.btn:hover:not(:disabled) {background:var(--surface-raised);transform:none;} -.btn-primary:hover:not(:disabled) {background:var(--accent);} -button:disabled {opacity:.45;cursor:not-allowed;} -:focus-visible {outline:2px solid var(--focus,var(--accent));outline-offset:3px;} -#drawerClose,#drawerOpen {display:none;} -.legacy-voice {overflow:auto;padding:28px;min-width:0;} -.legacy-voice .workspace {grid-template-columns:1fr;} -.legacy-voice .topbar {margin-top:24px;} -.login-gate {width:calc(100% - 40px);max-width:420px;margin:12vh auto;padding:28px;background:var(--surface);border:1px solid var(--line);border-radius:12px;} -.login-gate h1 {font-size:26px;} -.login-gate p {margin:14px 0;} -dialog {max-width:520px;width:calc(100% - 32px);border:1px solid var(--line);border-radius:12px;background:var(--surface);color:var(--ink);padding:24px;} -dialog::backdrop {background:rgb(0 0 0 / .35);} -dialog h2 {font-size:22px;margin-bottom:16px;} -.account-form input,.account-form select,.account-form textarea {font:inherit;color:var(--ink);background:var(--canvas);border:1px solid var(--line);border-radius:6px;padding:10px;width:100%;} -.settings-sheet,.tasks-panel {background:var(--surface);color:var(--ink);} -.settings-sheet {overflow:auto;max-height:90dvh;} -.led-wall {display:grid;grid-template-columns:repeat(21,1fr);width:240px;height:240px;gap:3px;} -.led-dot {border-radius:50%;background:var(--accent);border:0;} -@media(max-width:760px) { - .app-shell {grid-template-columns:minmax(0,1fr);} - .chat-sidebar {position:fixed;inset:0 auto 0 0;width:min(310px,85vw);z-index:60;transform:translateX(-100%);transition:transform 180ms ease-out;} - .chat-sidebar.drawer-open {transform:translateX(0);} - #drawerClose,#drawerOpen {display:inline-flex;} - #drawerClose {margin-left:auto;} - .drawer-backdrop {position:fixed;inset:0;z-index:50;border:0;background:rgb(0 0 0 / .35);} - .chat-topbar {padding:10px 12px;gap:8px;min-height:64px;} - .conversation-actions {width:100%;justify-content:flex-end;} - .chat-timeline {padding:24px 18px;} - .primary-composer {width:calc(100% - 24px);padding:12px;} - .composer-footer span {display:none;} - .chat-welcome h2 {font-size:25px;} - .chat-footnote {padding-bottom:calc(12px + env(safe-area-inset-bottom));} - .legacy-voice {padding:16px;} -} -@media(prefers-reduced-motion:reduce) {*,*::before,*::after {animation:none!important;transition:none!important;scroll-behavior:auto!important;}} - -:root {--canvas:var(--hope-ground,#fcf9fb);--surface:var(--hope-surface,#f4eef2);--surface-raised:var(--hope-persona-surface,#eee2e9);--ink:var(--hope-ink,#332831);--ink-muted:var(--hope-quiet,#695662);--ink-soft:var(--hope-quiet,#695662);--line:var(--hope-control-border,#d4c3cf);--accent:var(--hope-accent,#8c3d63);--accent-ink:var(--hope-on-accent,#fff);--focus:var(--hope-focus,#8c3d63);--iris:var(--hope-accent,#8c3d63);--danger:var(--hope-alert,#a32438);color-scheme:light dark;} - -.sidebar-footer .btn,.new-chat {text-align:left;display:flex;justify-content:flex-start;} - -.primary-composer .btn-primary {min-width:84px;flex:0 0 auto;} -.primary-composer .btn-primary:disabled,.account-form .btn-primary:disabled,.login-gate .btn-primary:disabled {display:inline-flex;} - -.app-shell {padding:0;border:0;border-radius:0;} -#conversationMenu {display:none;min-width:44px;min-height:44px;} -#showArchived {font-size:12px;min-height:40px;padding:8px;} -.markdown-table {max-width:100%;overflow:auto;margin:14px 0;} -.markdown-table table {border-collapse:collapse;width:100%;font-size:14px;} -.markdown-table th,.markdown-table td {padding:10px 12px;border:1px solid var(--line);text-align:left;} -.markdown-table th {background:var(--surface);font-weight:600;} -.primary-composer textarea:focus:not(:focus-visible) {outline:none;box-shadow:none;} -@media(max-width:760px) { - #conversationMenu:not([hidden]) {display:inline-flex;} - .conversation-actions {width:220px;padding:8px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:var(--ink);margin:76px 12px 0 auto;} - .conversation-actions:not(:popover-open) {display:none;} - .conversation-actions:popover-open {display:flex;flex-direction:column;} - .conversation-actions .btn {min-height:44px;justify-content:flex-start;text-align:left;width:100%;font-size:14px;} - .chat-topbar {flex-wrap:nowrap;} - .chat-topbar h1 {min-width:0;} - .feature-actions {max-width:120px;} - #drawerOpen {min-height:44px;min-width:44px;} -} - -#researchMode {max-width:130px;padding:7px 8px;border:1px solid var(--line);border-radius:6px;background:var(--canvas);color:var(--ink);font:inherit;font-size:13px;} -#researchModeLabel {font-size:12px;color:var(--ink-muted);} - -.talk-trigger {display:inline-flex;align-items:center;justify-content:center;min-height:44px;min-width:106px;background:var(--surface-raised);color:var(--accent);font-weight:600;white-space:nowrap;} -.talk-trigger::before {content:"";display:inline-block;width:8px;height:13px;border:1.5px solid currentColor;border-radius:6px;margin-right:8px;} -@media(max-width:760px) {.talk-trigger {min-width:106px;padding:10px;font-size:13px;} .chat-topbar {gap:4px;} .feature-actions:has(button:disabled) {display:none;}}