diff --git a/scripts/css_snapshot.py b/scripts/css_snapshot.py index ba927775f..360fa11da 100644 --- a/scripts/css_snapshot.py +++ b/scripts/css_snapshot.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -"""Computed-style snapshot harness for ``static/style.css``. +"""Computed-style snapshot harness for the shipped app CSS cascade. -``static/style.css`` is a single 51k-line stylesheet whose rendered result -depends on source order: hundreds of selectors are declared more than once and +The app CSS is an ordered multi-file cascade whose rendered result depends +on source order: hundreds of selectors are declared more than once and ``!important`` is used throughout. Any restructuring - extracting a block into its own file, reordering ```` tags, moving an ``@media`` rule - can silently change which declaration wins, and nothing else in the suite would diff --git a/scripts/verify_background_research_cards.mjs b/scripts/verify_background_research_cards.mjs index 16d580fba..158bc9b2e 100644 --- a/scripts/verify_background_research_cards.mjs +++ b/scripts/verify_background_research_cards.mjs @@ -1,10 +1,26 @@ /** Card layout and reconciliation against the served assets; no user mutations. */ import { chromium } from 'playwright'; + +const ORIGIN = 'http://127.0.0.1:7011'; + +/** The `` tags the app shell ships, in shell order. */ +async function shellStylesheets() { + const response = await fetch(`${ORIGIN}/static/index.html`); + if (!response.ok) throw new Error(`static/index.html returned ${response.status}`); + const links = (await response.text()).match(/]*rel=["']stylesheet["'][^>]*>/gi) || []; + if (!links.length) throw new Error('no stylesheet links found in static/index.html'); + return links.join(''); +} + const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); - await page.goto('http://127.0.0.1:7011/static/test-fixtures/browser-catalog.html'); - await page.setContent('

Existing conversation

'); + await page.goto(`${ORIGIN}/static/test-fixtures/browser-catalog.html`); + // Read the shell's stylesheets out of index.html rather than naming one + // here. style.css is now a set of ordered fragments, and a hardcoded link + // to a file that has moved does not fail - it renders unstyled and the + // layout checks below pass against nothing. + await page.setContent(`${await shellStylesheets()}

Existing conversation

`); const checks = await page.evaluate(async () => { const { renderResearchCards } = await import('/static/js/backgroundToolJobs.js'); const box = document.querySelector('#chat-history'); diff --git a/static/app.js b/static/app.js index 8c04bfd47..0555bf21e 100644 --- a/static/app.js +++ b/static/app.js @@ -37,7 +37,7 @@ import { UI_VIS_DEFAULT_OFF, resolveVisibility } from './js/ui_visibility.js?v=2 import tasksModule from './js/tasks.js?v=20260914taskmodel1'; import calendarModule from './js/calendar.js?v=20260914emailsource11'; import notesModule from './js/notes.js?v=20260911notesselectioncancel1'; -import adminModule from './js/admin.js?v=20260921chatgptusage2'; +import adminModule from './js/admin.js?v=20260929csssplit'; import settingsModule from './js/settings.js?v=20260912writingstyle3'; // Eagerly bind unified minimize/restore behavior across all tool modals. import './js/modalManager.js'; diff --git a/static/css/00-tokens.css b/static/css/00-tokens.css new file mode 100644 index 000000000..3c9e49d3a --- /dev/null +++ b/static/css/00-tokens.css @@ -0,0 +1,183 @@ +/* 00-tokens - part of the former static/style.css. + * + * The stylesheet is split by area, but the split is mechanical: rules were + * moved verbatim and NOT regrouped. The numeric prefix is the load order, and + * that order is what preserves the cascade. A rule sits in the file its + * original position fell into, so an area's rules are not all in its file and + * a file is not only that area's rules. + * + * Do not reorder the tags in static/index.html, and do not move a rule + * between files: with 1,151 redeclarations and 1,886 !important in this + * cascade, either can change which declaration wins. + * + * static/index.html and the PRECACHE list in static/sw.js must agree on this + * order. tests/test_static_stylesheet_manifest.py and + * tests/test_css_computed_style_snapshot.py fail if that drifts. + */ +/* ============================================ */ +/* Odysseus UI — Consolidated Stylesheet */ +/* ============================================ */ + + +/* ── Variables ── + * + * Theme-public variables (override via theme.js or custom themes): + * Core: --bg, --fg, --panel, --border, --red + * Syntax: --hl-keyword, --hl-string, --hl-comment, --hl-function, + * --hl-number, --hl-builtin, --hl-variable, --hl-params, + * --hl-bg, --hl-fg + * Accents: --accent-primary, --accent-error (set by theme.js) + * Semantic: --color-error, --color-success, --color-warning, --color-danger, + * --color-accent, --color-muted, --color-muted-alt + */ + +:root { + /* Core palette */ + --bg: #282c34; + --fg: #9cdef2; + --panel: #111; + --border: #355a66; + --red: #e06c75; + /* Were `var(--green)` / `var(--warn)` — self-referential, so they + resolved to invalid and every site fell back to its own literal + (or, for sites with no fallback, painted as transparent/inherit). + Anchor them to real hex so the token layer actually works. */ + --green: #50fa7b; + --warn: #f0ad4e; + + /* Syntax highlighting */ + --hl-bg: #1e2228; + --hl-fg: #9cdef2; + --hl-keyword: #c678dd; + --hl-string: #e5c07b; + --hl-comment: #828997; + --hl-function: #61afef; + --hl-number: #d19a66; + --hl-builtin: #56b6c2; + --hl-variable: #abb2bf; + --hl-params: #a8c0d4; + + /* Semantic colors */ + --color-error: #ff4444; + --color-error-light: #ff6666; + --color-success: #4caf50; + --color-warning: #f0ad4e; + --color-danger: #c0392b; + --color-recording: #ff3b30; + --color-recording-hover: #d63031; + --color-muted: #888; + --color-muted-alt: #6b7280; + --color-accent: #00aaff; + --color-agent-active: #00ff00; + --color-brand-blue: #3b82f6; + --color-blind-orange: #ff9800; + --color-save-green: var(--color-success); + --color-link-hover: #66c7ff; + --color-subheader: #6b8a94; + --select-bg: var(--bg); + --select-fg: var(--fg); + --select-option-bg: color-mix(in srgb, var(--panel) 74%, var(--bg)); + --select-option-fg: var(--fg); + --select-option-active-bg: color-mix(in srgb, var(--accent, var(--red)) 24%, var(--panel)); + /* Warm accent — used by the Goals/Today UI in Notes. Lives as a token so + themes can override without touching the goal CSS. */ + --accent-warm: #d19a66; +} + +:root.light { + --bg: #f5f5f5; + --fg: #2b2b2b; + --panel: #fff; + --border: #bbb; + --hl-bg: #f9f9f9; + --hl-fg: #2b2b2b; + --hl-keyword: #7928a1; + --hl-string: #986801; + --hl-comment: #6a737d; + --hl-function: #005cc5; + --hl-number: #986801; + --hl-builtin: #0070a0; + --hl-variable: #383a42; + --hl-params: #4a4f5c; + --select-bg: #eaeaea; + --select-fg: var(--fg); + --select-option-bg: var(--panel); + --select-option-fg: var(--fg); + --select-option-active-bg: color-mix(in srgb, var(--red) 16%, var(--panel)); +} + +/* ── Reset & Base ── */ + +* { box-sizing: border-box; } +html, body { overflow-x: hidden; height: 100%; margin: 0; overscroll-behavior: none; } +body { + background-color: var(--bg); + color: var(--fg); + /* Keep the base padding transition for older layout paths that still adjust + the body directly. Edge docks reserve workspace room on the flex panes + below so left + right docks can coexist without skewing the whole body. */ + transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1), + padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1); + font-family: var(--font-family, 'Fira Code', monospace); + display: flex; + height: 100%; + height: 100dvh; /* dynamic viewport height — adapts when mobile keyboard opens */ + overflow: hidden; +} + +/* Self-hosted Fira Code font */ +@font-face { font-family: 'Fira Code'; font-weight: 300; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Light.woff2') format('woff2'); } +@font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); } +@font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); } + +/* GohuFont is the default UI face across themes; keep the shipped font local + so the default is available before custom-font discovery completes. */ +@font-face { font-family: 'GohuFont'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/custom/GohuFont.ttf') format('truetype'); } + +/* Self-hosted OpenDyslexic — dyslexia-friendly accessibility font option (SIL OFL 1.1) */ +@font-face { font-family: 'OpenDyslexic'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Regular.woff2') format('woff2'); } +@font-face { font-family: 'OpenDyslexic'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Bold.woff2') format('woff2'); } + +/* Code block baseline */ +pre, code, .hljs { + font-size: 11px; + line-height: 1.5; +} + +/* Scrollbar styling */ +@supports selector(::-webkit-scrollbar) { + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--panel); + } + ::-webkit-scrollbar-thumb { + background-color: var(--red); + border-radius: 4px; + border: 2px solid var(--panel); + } + ::-webkit-scrollbar-thumb:hover { + background-color: color-mix(in srgb, var(--red) 80%, white); + } +} + +html { + scrollbar-color: var(--red) var(--panel); + scrollbar-width: thin; +} + +/* Utility */ +.red-text { color: var(--red); } + +/* ── Density Overrides ── */ + +:root.density-compact { font-size: 13px; } +:root.density-compact .msg { padding: 6px 10px; margin-bottom: 4px; } +:root.density-compact .list-item { padding: 4px 8px; } +:root.density-compact .sidebar .section { padding: 0; } +:root.density-spacious { font-size: 16px; } +:root.density-spacious .msg { padding: 14px 18px; margin-bottom: 12px; } +:root.density-spacious .list-item { padding: 8px 12px; } +:root.density-spacious .sidebar .section { padding: 0; } diff --git a/static/css/01-agent-chat.css b/static/css/01-agent-chat.css new file mode 100644 index 000000000..f06f3b050 --- /dev/null +++ b/static/css/01-agent-chat.css @@ -0,0 +1,7807 @@ +/* 01-agent-chat - part of the former static/style.css. + * + * The stylesheet is split by area, but the split is mechanical: rules were + * moved verbatim and NOT regrouped. The numeric prefix is the load order, and + * that order is what preserves the cascade. A rule sits in the file its + * original position fell into, so an area's rules are not all in its file and + * a file is not only that area's rules. + * + * Do not reorder the tags in static/index.html, and do not move a rule + * between files: with 1,151 redeclarations and 1,886 !important in this + * cascade, either can change which declaration wins. + * + * static/index.html and the PRECACHE list in static/sw.js must agree on this + * order. tests/test_static_stylesheet_manifest.py and + * tests/test_css_computed_style_snapshot.py fail if that drifts. + */ +#cookbook-modal .modal-content { height: 94vh; max-height: 94vh; } +.pdf-export-overlay .modal-content { max-height: 86vh; } + +/* ── Background Patterns ── */ + +:root { --bg-effect-intensity: 1; } + +/* Canvas-based effects — single source of truth for intensity */ +#synapse-canvas, #rain-canvas, #constellations-canvas, +#perlin-flow-canvas, #petals-canvas, #sparkles-canvas, +#embers-canvas, #starfield-depth-canvas, #ascii-fireflies-background { + opacity: var(--bg-effect-intensity, 1); +} + +body.bg-pattern-dots { + background-image: radial-gradient(color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px); + background-size: 20px 20px; background-attachment: fixed; +} +body.bg-pattern-synapse { + /* CSS grid as base, canvas pulses overlay */ + background-image: linear-gradient(color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(3.5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(3.5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px); + background-size: 24px 24px; background-attachment: fixed; +} +body.memory-disabled.bg-pattern-synapse { + background-image: none; +} +body.memory-disabled #synapse-canvas { + display: none; +} +body.bg-pattern-perlin-flow, +body.bg-pattern-petals, +body.bg-pattern-sparkles { + /* canvas-only backgrounds */ +} +body.bg-pattern-starfield-depth, +body.bg-pattern-ascii-fireflies { + /* rendered by their fixed background layer */ +} +#ascii-fireflies-background { + position: fixed; + inset: 0; + z-index: 0; + box-sizing: border-box; + margin: 0; + padding: 14px 16px; + overflow: hidden; + pointer-events: none; + white-space: pre; + color: var(--bg-effect-color, var(--fg)); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; + line-height: 1; + letter-spacing: 0; +} + + +/* ── Layout ── */ + + /* Top bar — session meta + incognito, single row */ + .chat-top-bar { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + position: relative; + z-index: 2; + padding: 5px 0 0; + min-height: 25px; + box-sizing: border-box; + } + .chat-top-bar::after { + content: none; + } + body.sidebar-collapsed.hamburger-left .chat-top-bar { + padding-left: 38px; + } + body.sidebar-collapsed.hamburger-right .chat-top-bar { + padding-right: 38px; + } + .incognito-indicator { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + background: color-mix(in srgb, var(--accent) 12%, transparent); + border: 1px solid var(--accent); + color: var(--accent); + cursor: pointer; + padding: 4px; + width: 26px; + height: 26px; + box-sizing: border-box; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.15s, background 0.15s, left 0.15s; + z-index: 1; + opacity: 0.7; + } + body.sidebar-collapsed.hamburger-left .incognito-indicator { + left: 12px; + } + .incognito-indicator:hover { + opacity: 1; + background: color-mix(in srgb, var(--accent) 20%, transparent); + } + + .chat-new-btn { + position: absolute; + right: 7px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--fg); + opacity: 0.6; + cursor: pointer; + padding: 4px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.08s, background 0.08s, left 0.15s, right 0.15s; + flex-shrink: 0; + z-index: 1; + } + .chat-new-btn:hover { + opacity: 1; + color: var(--accent); + } + /* Flip new-chat and incognito when sidebar is on the right */ + body:has(.sidebar.right-side) .chat-new-btn { + right: auto; + left: 12px; + } + body:has(.sidebar.right-side) .incognito-indicator { + left: auto; + right: 12px; + } + body.sidebar-collapsed.hamburger-right .incognito-indicator { + right: 42px; + } + + /* Session meta — sits at top of chat area, scrolls with content */ + .chat-meta-overlay { + position: absolute; + left: 44px; + right: 44px; + top: 50%; + transform: translateY(calc(-50% - 2px)); + font-size: 0.75em; + line-height: 1; + /* 70% mix keeps the chat title clearly above the WCAG AA 4.5:1 + contrast threshold (40% only reached ~2.8:1). */ + color: color-mix(in srgb, var(--fg) 70%, transparent); + white-space: nowrap; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + pointer-events: none; + min-width: 0; + max-width: calc(100% - 88px); + overflow: hidden; + } + .chat-meta-overlay > * { + pointer-events: auto; + flex-shrink: 0; + } + .chat-meta-overlay #current-meta { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + cursor: pointer; + min-width: 0; + flex: 0 1 auto; + max-width: min(56vw, 520px); + line-height: 1.25; + } + .chat-top-bar:has(.incognito-indicator:not([style*="display:none"]):not([style*="display: none"])) .chat-meta-overlay { + left: 54px; + right: 44px; + max-width: calc(100% - 98px); + } + body:has(.sidebar.right-side) .chat-top-bar:has(.incognito-indicator:not([style*="display:none"]):not([style*="display: none"])) .chat-meta-overlay { + left: 44px; + right: 54px; + } + .chat-meta-overlay:hover { + color: color-mix(in srgb, var(--fg) 75%, transparent); + } + /* Offline model rows */ + .models-row-offline { + opacity: 0.4; + pointer-events: none; + } + .models-row-offline .model-chat-btn { + pointer-events: auto; + } + /* Offline badge next to endpoint name */ + .endpoint-offline-badge { + color: var(--danger, #f44); + font-size: 0.8em; + margin-left: 4px; + opacity: 0.7; + } + .chat-meta-overlay:empty, + .chat-meta-overlay:not(:has(#current-meta:not(:empty))) { + display: none; + } + .export-dropdown-wrap { + position: relative; + display: inline-flex; + flex-shrink: 0; + margin-left: -4px; + margin-right: -20px; + } + .export-dl-btn { + background: none; border: none; + color: inherit; + cursor: pointer; + padding: 4px 5px; border-radius: 4px; + display: flex; align-items: center; + transition: color 0.08s, background 0.08s; + } + .export-dl-btn > svg { color: var(--accent, var(--red)); } + .export-dl-btn:hover { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 12%, transparent); + } + .export-dropdown-menu { + display: none; + position: fixed; + background: var(--panel); border: 1px solid var(--border); + border-radius: 8px; padding: 4px; min-width: 120px; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); + z-index: 300; + color: var(--fg); + } + .export-dropdown-menu.open { display: block; } + .export-dropdown-item { + padding: 6px 8px; font-size: 11px; cursor: pointer; + border-radius: 6px; color: var(--fg); white-space: nowrap; + display: flex; align-items: center; gap: 10px; + transition: background 0.1s; + } +.export-dropdown-item:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); } +.export-settings-text-only .dropdown-icon { display: none; } + #export-compact-btn { + display: none !important; + } + #export-compact-btn .dropdown-icon { + display: none; + } + .export-compact-pill { + display: inline; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: inherit; + line-height: inherit; + } + .export-dropdown-item .dropdown-icon { + width: 14px; height: 14px; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; opacity: 0.5; + } + /* On mobile the chat-header export dropdown was cramped — items + were 11px font with 6px padding. Bump to readable touch targets: + larger min-width, taller rows, bigger icons + text. */ + @media (max-width: 768px) { + .export-dropdown-menu { + min-width: 200px; + padding: 6px; + border-radius: 10px; + } + .export-dropdown-item { + padding: 12px 14px; + font-size: 14px; + gap: 12px; + min-height: 44px; + } + #export-compact-btn { + margin: 5px 6px 7px; + min-height: 38px; + padding: 9px 14px; + } + .export-dropdown-item .dropdown-icon { + width: 18px; height: 18px; opacity: 0.7; + } + .export-dropdown-item .dropdown-icon svg { + width: 18px; height: 18px; + } + } + .sidebar { + width: 240px; + background: var(--sidebar-bg, var(--panel)); + border-right: 1px solid var(--border); + transition: width 0.25s ease, opacity 0.2s ease, padding 0.25s ease; + display: flex; + flex-direction: column; + flex-shrink: 0; + overflow: hidden; + min-height: 0; + margin: 0; + padding: 0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(10px); + border: 1px solid color-mix(in srgb, var(--fg) 11%, transparent); + position: relative; + } + .sidebar-resize-handle { + position: absolute; + top: 0; + right: -3px; + width: 6px; + height: 100%; + cursor: col-resize; + z-index: 10; + background: transparent; + } + .sidebar-resize-handle:hover, + .sidebar-resize-handle.dragging { + background: var(--accent); + opacity: 0.6; + } + .sidebar.right-side .sidebar-resize-handle { + right: auto; + left: -3px; + } + .sidebar.resizing { + transition: none; + user-select: none; + } + .sidebar.right-side { + order: 2; + margin: 0; + border-right: none; + border-left: 1px solid var(--border); + } + .sidebar.hidden { + /* !important so it beats the inline width init.js restores from storage — + otherwise the width never changes and only opacity animates, making the + collapse look instant. With this, width animates from the inline value + down to 0 via the .sidebar width transition. */ + width: 0 !important; + padding: 0 !important; + border: none; + overflow: hidden; + opacity: 0; + } + /* ===== Sidebar User Bar ===== */ + .sidebar-user-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 12px; + flex-shrink: 0; + gap: 4px; + min-height: 48px; + } + + .user-bar-left { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; + cursor: pointer; + padding: 6px 8px; + border-radius: 8px; + transition: background 0.15s; + } + + .user-bar-left:hover { + background: color-mix(in srgb, var(--fg) 6%, transparent); + } + + .user-bar-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + background: color-mix(in srgb, var(--fg) 12%, transparent); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; + color: var(--fg); + opacity: 0.7; + flex-shrink: 0; + text-transform: uppercase; + } + + #user-bar-avatar { position: relative; left: -4px; } + + .user-bar-name { + font-size: 9.75px; + font-weight: 500; + color: var(--fg); + opacity: 0.8; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .user-bar-actions { + display: flex; + gap: 1px; + flex-shrink: 0; + } + + .user-bar-btn { + background: none; + border: none; + color: var(--fg); + opacity: 0.35; + cursor: pointer; + padding: 6px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.12s, background 0.12s; + } + + .user-bar-btn:hover { + opacity: 1; + background: color-mix(in srgb, var(--fg) 8%, transparent); + } + + + .sidebar.hidden .sidebar-user-bar { + display: none; + } + + /* Sticky sidebar header — logo, never scrolls */ + .sidebar-header { + display: flex; + align-items: center; + justify-content: flex-end; /* right-align when sidebar is on left */ + gap: 8px; + padding: 15px 10px 0 40px; /* top padding aligns logo with fixed hamburger */ + flex-shrink: 0; + min-height: 40px; + border: none !important; + box-shadow: none !important; + position: relative; + z-index: 3; + background: var(--sidebar-bg, var(--panel)); + } + .sidebar-hamburger { + display: none !important; /* external #hamburger-btn is the only toggle */ + } + .sidebar-inner { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior-y: none; + scrollbar-width: none; + display: flex; + flex-direction: column; + gap: 0; + padding: 10px 8px 8px; + min-height: 0; + transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1); + } + .sidebar-brand { + display: flex; + align-items: center; + flex-shrink: 0; + min-height: 24px; + } + .sidebar-brand-title { + font-size: 1rem; + font-weight: 600; + line-height: 1.35; + color: var(--brand-color, var(--red)); + white-space: nowrap; + user-select: none; + position: relative; + top: 0; + left: -10px; + } + .sidebar-sep { + display: none; + } + #sidebar-search-btn, + #sidebar-new-chat-btn { + margin: 0; + padding: 8px 8px; + } + #sessions-section { + margin-top: -1px; + min-height: 0; + flex: 0 0 auto; + } + /* Keep a large chat history from pushing the rest of the sidebar below + the fold. The chat rows scroll independently; the sections beneath + them stay reachable in the main sidebar scroll area. */ + #sessions-section #session-list { + max-height: min(42vh, 420px); + flex: 0 0 auto; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: thin; + min-height: 0; + } + #tools-section { + margin-top: 1px; + } + + @media (max-width: 768px) { + #sessions-section { + flex: 0 0 auto !important; + overflow: visible !important; + } + #sessions-section #session-list { + max-height: min(34vh, 300px) !important; + flex: 0 0 auto !important; + overflow-y: auto !important; + overflow-x: hidden !important; + } + } + #tools-section .list-item { + padding: 8px 8px; + } + .sidebar.right-side .sidebar-header { + justify-content: flex-start; + padding-left: 10px; + padding-right: 40px; + } + .sidebar.right-side .sidebar-inner { + padding: 8px; + } + .sidebar.right-side .sidebar-brand { + justify-content: flex-start; + padding: 2px 30px 4px 4px; + } + .sidebar.right-side .sidebar-brand-title { + margin-left: 10px; + } + /* Fixed hamburger — always visible, toggles sidebar */ + .hamburger-btn { + position: fixed; + top: 12px; + left: 9px; + right: auto; + z-index: 210; + width: 30px; + height: 30px; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--hamburger-color, var(--fg)); + cursor: pointer; + opacity: 0.5; + -webkit-tap-highlight-color: transparent; + outline: none; + transition: opacity 0.15s; + padding: 0; + display: flex; + } + body.hamburger-right .hamburger-btn { + left: auto; + right: 9px; + } + .mobile-new-chat-btn { + display: none; + } + .hamburger-btn:hover { + opacity: 1; + } + /* Icon rail — mini sidebar that replaces the wide .sidebar when it's + hidden (mutually exclusive — see sidebar-layout.js:57). Fullscreen + panels reserve this strip of width via `left: var(--icon-rail-w)` + so the rail stays visible without needing a z-index hack (which + used to cover the fixed-position hamburger button). */ + .icon-rail { + width: 48px; + flex-shrink: 0; + background: var(--panel); + border-right: 1px solid var(--border); + display: none; + flex-direction: column; + align-items: center; + padding: 48px 4px 8px 4px; + gap: 4px; + margin: 0; + position: relative; + box-sizing: border-box; + /* Allow hover labels (e.g. the rail-new-chat "New" tooltip) to extend + outside the 48px column. overflow:hidden was clipping them. */ + overflow: visible; + } + body.sidebar-collapsed:not(.hamburger-only) .icon-rail, + html.ody-sidebar-mini .icon-rail { + display: flex; + } + html.ody-sidebar-mini .sidebar, + html.ody-sidebar-off .sidebar { + width: 0 !important; + min-width: 0 !important; + opacity: 0; + overflow: hidden; + pointer-events: none; + padding: 0 !important; + border: 0 !important; + } + html.ody-sidebar-off .icon-rail { + display: none !important; + } + .rail-resize-handle { + position: absolute; + top: 0; + right: -3px; + width: 6px; + height: 100%; + cursor: col-resize; + z-index: 10; + background: transparent; + } + .rail-resize-handle:hover, + .rail-resize-handle.dragging { + background: var(--accent); + opacity: 0.6; + } + .icon-rail.right-side .rail-resize-handle { + right: auto; + left: -3px; + } + .icon-rail.right-side { + order: 2; + margin: 0; + border-right: none; + border-left: 1px solid var(--border); + } + .icon-rail-divider { + width: 24px; + height: 1px; + background: var(--border); + margin: 4px 0; + } + .icon-rail-btn { + position: relative; + width: 34px; + height: 34px; + border: none; + background: transparent; + color: var(--accent, var(--red)); + font-size: 16px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.5; + transition: opacity 0.08s, background 0.08s; + } + .rail-notes-badge { + position: absolute; + top: 1px; + right: 1px; + min-width: 14px; + height: 14px; + padding: 0 3px; + border-radius: 7px; + background: color-mix(in srgb, var(--accent) 85%, var(--bg)); + color: var(--bg); + font-size: 9px; + font-weight: 700; + line-height: 14px; + text-align: center; + box-sizing: border-box; + pointer-events: none; + box-shadow: 0 0 0 1px var(--bg); + } + .rail-notes-badge.fired { + background: var(--red); + animation: rail-notes-pulse 1.6s ease-in-out infinite; + } + @keyframes rail-notes-pulse { + 0%, 100% { box-shadow: 0 0 0 1px var(--bg), 0 0 0 0 color-mix(in srgb, var(--red) 50%, transparent); } + 50% { box-shadow: 0 0 0 1px var(--bg), 0 0 0 4px color-mix(in srgb, var(--red) 15%, transparent); } + } + /* Main sidebar notes button — dot when a reminder has fired */ + .tool-notes-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--red); + /* Match the Deep Research badge: right-aligned (auto) with the same + 4px left nudge, so both sidebar buttons' dots line up identically. */ + margin-left: auto; + position: relative; + left: -4px; + flex-shrink: 0; + align-self: center; + animation: rail-notes-pulse 1.6s ease-in-out infinite; + pointer-events: none; + } + /* Individual card — subtle accent border tint when a reminder has fired */ + .note-card.note-card-reminder-due .note-card-reminder { + background: color-mix(in srgb, var(--red) 22%, transparent); + color: var(--red); + font-weight: 600; + } + .icon-rail-btn:hover { opacity: 1; background: color-mix(in srgb, var(--accent) 12%, transparent); } + .icon-rail-btn.active-section { opacity: 1; background: color-mix(in srgb, var(--color-accent) 15%, transparent); } + /* Unified "minimized" indicator for any rail/sidebar button whose modal is held open */ + .rail-minimized { position: relative; } + .rail-minimized::after { + content: ''; position: absolute; + /* Default for inline spans like #email-section-title — sit just to the + right of the element so it never overlaps the icon. */ + top: 50%; right: -10px; transform: translateY(-50%); + width: 6px; height: 6px; border-radius: 50%; + background: var(--accent, var(--red)); + box-shadow: 0 0 0 2px var(--bg); + pointer-events: none; + animation: rail-min-pulse 2s ease-in-out infinite; + } + /* Compact icon-rail buttons: top-right corner of the icon */ + .icon-rail-btn.rail-minimized::after { top: 4px; right: 4px; transform: none; } + /* Sidebar list-items are wider — anchor right-aligned vertically centered */ + .list-item.rail-minimized::after { top: 50%; right: 8px; transform: translateY(-50%); } + #tool-memory-btn.rail-minimized::after { right: 12px; } + /* Per-user nudge: the listed tools' minimized dot sits 2px further + in from the right edge so it doesn't look glued to the border. */ + #tool-theme-btn.rail-minimized::after, + #tool-tasks-btn.rail-minimized::after, + #tool-notes-btn.rail-minimized::after, + #tool-library-btn.rail-minimized::after, + #tool-gallery-btn.rail-minimized::after, + #tool-compare-btn.rail-minimized::after, + #tool-calendar-btn.rail-minimized::after { right: 12px; } + @media (max-width: 768px) { + /* On mobile the list-items are taller (touch-sized), so the tabbed-down + pulsing dot reads a hair low and right. Email uses its own dot and is + already aligned — only the tool list-item dots need the nudge: + 4px left (right 8 → 12) and 2px up. */ + .list-item.rail-minimized::after { + right: 13px; + transform: translateY(calc(-50% - 2px)); + } + #tool-memory-btn.rail-minimized::after { right: 17px; } + } + @keyframes rail-min-pulse { + 0%,100% { opacity: 1; } + 50% { opacity: 0.4; } + } + /* Compact minimize button for modal headers — matches the close + button's round control so the two read as a paired set. The + header's h4 carries margin-right:auto, which groups minimize + close + on the right; this button just needs a small gap before close. */ + .modal-minimize-btn { + background: transparent; + color: color-mix(in srgb, var(--fg) 58%, transparent); + border: 1px solid transparent; + cursor: pointer; + width: 24px; + height: 24px; + padding: 0; + margin-left: 0; + margin-right: 4px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + flex-shrink: 0; + transition: background 0.15s, color 0.15s; + } + .modal-minimize-btn:hover { + background: color-mix(in srgb, var(--fg) 10%, transparent); + border-color: color-mix(in srgb, var(--fg) 12%, transparent); + color: var(--fg); + } + .modal-minimize-btn svg { + transform: translateY(2px); + } + .modal.modal-minimized { display: none !important; } + /* Window tile snap ghost (desktop only) */ + #tile-ghost { + position: fixed; pointer-events: none; z-index: 9000; + background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); + border: 2px solid color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); + border-radius: 8px; + opacity: 0; transform: scale(0.96); + transition: left 0.12s ease, top 0.12s ease, width 0.12s ease, height 0.12s ease, opacity 0.12s, transform 0.12s; + } + #tile-ghost.visible { opacity: 1; transform: scale(1); } + /* Bottom dock — chip per minimized modal */ + #minimized-dock { + position: fixed; bottom: var(--composer-clearance, 12px); left: 50%; transform: translateX(-50%); + display: flex; gap: 6px; flex-wrap: wrap; + max-width: calc(100vw - 24px); + padding: 4px; + z-index: 100; + pointer-events: none; + } + .minimized-dock-chip { + pointer-events: auto; + display: inline-flex; align-items: center; gap: 6px; + padding: 6px 8px 6px 10px; + background: var(--panel, var(--bg)); + border: 1px solid var(--border); + border-radius: 999px; + color: var(--fg); font-family: inherit; font-size: 12px; + cursor: grab; touch-action: none; user-select: none; + box-shadow: 0 4px 14px rgba(0,0,0,0.35); + transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s, border-color 0.15s; + animation: dock-chip-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + .minimized-dock-chip:hover { + background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--panel, var(--bg))); + border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border)); + } + .minimized-dock-chip:active { cursor: grabbing; } + .minimized-dock-chip.dragging { + cursor: grabbing; z-index: 1000; + box-shadow: 0 8px 24px rgba(0,0,0,0.55); + transition: none; + opacity: 0.95; + } + /* Whole-dock drag (grabbed an edge chip) */ + #minimized-dock.dock-dragging { cursor: grabbing; } + #minimized-dock.dock-dragging .minimized-dock-chip { + transition: none; + box-shadow: 0 8px 24px rgba(0,0,0,0.45); + } + /* Subtle visual cue that edge chips drag the whole dock */ + #minimized-dock .minimized-dock-chip:first-child:not(:only-child), + #minimized-dock .minimized-dock-chip:last-child:not(:only-child) { + box-shadow: 0 4px 14px rgba(0,0,0,0.35), + inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 25%, transparent); + } + .minimized-dock-chip svg { opacity: 0.7; flex-shrink: 0; } + .minimized-dock-label { white-space: nowrap; } + + /* Mobile: chips are icon-only round pills. Tap to restore, drag toward + the trash zone at top-center to close. touch-action:none lets the + pointermove listener claim the gesture instead of the page scroll. */ + @media (max-width: 768px) { + .minimized-dock-chip { + width: 40px; height: 40px; + padding: 0 !important; + border-radius: 50% !important; + justify-content: center; + position: relative; + overflow: visible; + touch-action: none; + } + .minimized-dock-chip svg { width: 18px; height: 18px; opacity: 0.9; } + .minimized-dock-label, + .minimized-dock-x { display: none !important; } + .minimized-dock-chip.chip-free-drag { + box-shadow: 0 8px 22px rgba(0,0,0,0.55), + 0 0 0 2px color-mix(in srgb, var(--accent, var(--red)) 50%, transparent) !important; + } + /* Long-press hint — chip swells and settles while the detach timer + counts down so the user feels feedback before the bubble peels out + of the chain. Returns to scale(1) at the end so there's no visual + jump when the timer fires and the chip becomes a free-drag puck. */ + .minimized-dock-chip.chip-long-press { + background: + radial-gradient(circle at 30% 25%, color-mix(in srgb, #fff 28%, transparent), transparent 36%), + linear-gradient(135deg, + color-mix(in srgb, var(--accent, var(--red)) 34%, var(--panel, var(--bg))), + color-mix(in srgb, #7dd3fc 26%, var(--panel, var(--bg))) 52%, + color-mix(in srgb, #f0abfc 22%, var(--panel, var(--bg)))); + border-color: color-mix(in srgb, var(--accent, var(--red)) 72%, #fff 12%) !important; + animation: chip-long-press-pulse 0.82s ease-in-out infinite; + z-index: 10030; + } + .minimized-dock-chip.chip-long-press::before { + content: ''; + position: absolute; + inset: -96px; + border-radius: inherit; + background: + radial-gradient(circle, + color-mix(in srgb, var(--accent, var(--red)) 42%, transparent) 0 18%, + color-mix(in srgb, #7dd3fc 34%, transparent) 34%, + color-mix(in srgb, #f0abfc 30%, transparent) 50%, + transparent 72%); + pointer-events: none; + z-index: -1; + animation: chip-long-press-ripple 0.82s ease-out infinite; + } + @keyframes chip-long-press-pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.42); + box-shadow: 0 14px 42px rgba(0,0,0,0.66), + 0 0 0 16px color-mix(in srgb, var(--accent, var(--red)) 42%, transparent), + 0 0 72px color-mix(in srgb, #7dd3fc 44%, transparent), + 0 0 118px color-mix(in srgb, #f0abfc 32%, transparent); } + } + @keyframes chip-long-press-ripple { + 0% { opacity: 0.92; transform: scale(0.08); } + 72% { opacity: 0.28; transform: scale(3.6); } + 100% { opacity: 0; transform: scale(5.4); } + } + /* Chip whose modal is currently open — accent ring so the user can + tell at a glance which floating bubble belongs to the visible + modal. Tap it to minimize. */ + .minimized-dock-chip.chip-active { + border-color: var(--accent, var(--red)) !important; + box-shadow: 0 4px 14px rgba(0,0,0,0.35), + 0 0 0 2px color-mix(in srgb, var(--accent, var(--red)) 35%, transparent); + } + .minimized-dock-chip.chip-active svg { opacity: 1; } + } + /* Magnetic close zone — slides in from the side opposite the chip's + starting position so it never overlaps the dock. Always accent + color, larger than the chips, snappy spring transition. */ + #dock-trash-zone { + position: fixed; + left: 50%; + width: 88px; height: 88px; + border-radius: 50%; + --trash-zone-color: var(--accent, var(--red, #e53935)); + background: var(--trash-zone-color); + color: #fff; + display: flex; align-items: center; justify-content: center; + pointer-events: none; + opacity: 0; + z-index: 9000; + box-shadow: 0 8px 28px color-mix(in srgb, var(--trash-zone-color) 55%, transparent); + transition: opacity 0.18s ease, + transform 0.26s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.18s ease; + } + /* Off-screen start position depends on which side the chip is on so + the X slides in from the opposite edge with a snappy overshoot. */ + #dock-trash-zone[data-side="top"] { transform: translateX(-50%) translateY(-180%) scale(0.7); } + #dock-trash-zone[data-side="bottom"] { transform: translateX(-50%) translateY(180%) scale(0.7); } + #dock-trash-zone.visible { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } + #dock-trash-zone.engaged { + transform: translateX(-50%) translateY(0) scale(var(--trash-zone-scale, 1.22)); + box-shadow: 0 0 0 14px color-mix(in srgb, var(--trash-zone-color) 22%, transparent), + 0 12px 36px color-mix(in srgb, var(--trash-zone-color) 60%, transparent); + animation: trash-zone-breathe 1.35s ease-in-out infinite; + } + /* Whirlpool ring — fades in when chip is in capture range and spins + continuously, then bursts on drop. */ + #dock-trash-zone .whirlpool { + position: absolute; inset: -8px; + border-radius: 50%; + border: 2px solid transparent; + border-top-color: color-mix(in srgb, var(--trash-zone-color) 95%, transparent); + border-right-color: color-mix(in srgb, var(--trash-zone-color) 65%, transparent); + border-bottom-color: color-mix(in srgb, var(--trash-zone-color) 30%, transparent); + opacity: 0; + pointer-events: none; + animation: whirlpool-spin 0.85s linear infinite; + transition: opacity 0.15s ease; + } + #dock-trash-zone.engaged .whirlpool { opacity: 1; } + #dock-trash-zone.dropping .whirlpool { + opacity: 1; + animation: whirlpool-burst 0.36s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } + /* When a chip is swirling into the trash X, its inline `rotate(720deg)` + drags every child + ::after badge along with it — the count/dot pill + spinning looks chaotic. Fade those out fast at the start of the close + so visually only the icon glyph rotates. */ + .minimized-dock-chip.chip-trashing > :not(svg), + .minimized-dock-chip.chip-trashing::after, + .minimized-dock-chip.chip-trashing::before { + opacity: 0 !important; + transition: opacity 0.16s ease-out !important; + } + @keyframes whirlpool-spin { to { transform: rotate(360deg); } } + @keyframes trash-zone-breathe { + 0%, 100% { box-shadow: 0 0 0 10px color-mix(in srgb, var(--trash-zone-color) 16%, transparent), 0 10px 30px color-mix(in srgb, var(--trash-zone-color) 48%, transparent); } + 50% { box-shadow: 0 0 0 16px color-mix(in srgb, var(--trash-zone-color) 25%, transparent), 0 14px 38px color-mix(in srgb, var(--trash-zone-color) 68%, transparent); } + } + @keyframes whirlpool-burst { + 0% { transform: rotate(0deg) scale(1); opacity: 1; } + 60% { transform: rotate(540deg) scale(1.5); opacity: 0.6; } + 100% { transform: rotate(900deg) scale(2.2); opacity: 0; } + } + + /* Email chip badges: + - email-lib-modal chip: "1" badge when an email is expanded + inside (JS sets data-has-expanded). + - email-reader-* chips: auto-numbered 1, 2, 3 … via CSS counter, + so multiple opened-email windows are visually distinguishable. */ + .minimized-dock-chip[data-modal-id="email-lib-modal"], + .minimized-dock-chip[data-modal-id^="email-reader-"] { + position: relative; + } + .minimized-dock-chip[data-modal-id^="email-reader-"][data-tab-num]::after { + content: attr(data-tab-num); + position: absolute; + top: -4px; + right: -4px; + min-width: 16px; + height: 16px; + padding: 0 4px; + background: var(--accent, var(--red)); + color: #fff; + font-size: 9px; + font-weight: 700; + line-height: 16px; + text-align: center; + border-radius: 8px; + box-shadow: 0 0 0 2px var(--bg); + pointer-events: none; + } + .minimized-dock-chip[data-modal-id="email-lib-modal"][data-email-unread-label]::after { + content: attr(data-email-unread-label); + position: absolute; + top: -6px; + right: 10px; + height: 16px; + padding: 0 6px; + background: var(--accent, var(--red)); + color: #fff; + font-size: 9px; + font-weight: 700; + line-height: 16px; + white-space: nowrap; + border-radius: 8px; + box-shadow: 0 0 0 2px var(--bg); + pointer-events: none; + } + + .minimized-dock-x { + display: inline-flex; align-items: center; justify-content: center; + width: 16px; height: 16px; border-radius: 50%; + font-size: 14px; line-height: 1; opacity: 0.4; + margin-left: 3px; + } + .minimized-dock-x:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 12%, transparent); } + @keyframes dock-chip-in { + from { opacity: 0; transform: translateY(20px) scale(0.85); } + to { opacity: 1; transform: translateY(0) scale(1); } + } + .rail-new-chat svg { transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); } + .rail-new-chat:hover svg { transform: rotate(90deg); } + /* Sleek hover labels for the mini sidebar. Use a real child span instead + of ::before/::after so status dots and minimized indicators keep working. */ + .rail-hover-label { + position: absolute; + left: 100%; + top: 50%; + margin-left: 6px; + transform: translateY(-50%) translateX(-6px); + opacity: 0; + pointer-events: none; + white-space: nowrap; + font-size: 10px; + font-weight: 600; + color: var(--fg); + background: var(--panel); + padding: 2px 6px; + border-radius: 4px; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + z-index: 20; + transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); + } + .icon-rail-btn:hover .rail-hover-label { + opacity: 1; + transform: translateY(-50%) translateX(0); + } + #rail-admin, + #rail-settings { color: var(--fg); } + .rail-separator { width: 20px; height: 1px; background: var(--border); margin: 4px auto; } + .rail-dynamic { + position: relative; + } + .rail-dynamic::after { + content: ''; + position: absolute; + bottom: 2px; + left: 50%; + transform: translateX(-50%); + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--accent, var(--red)); + opacity: 0.7; + } + /* ── Sidebar sections (flat layout) ── */ + .section { padding: 0; border: none; background: none; border-radius: 0; box-shadow: none; margin: 0; } + + /* Section header row — identical sizing to .list-item */ + .section-header-flex { + display: flex; align-items: center; gap: 6px; + padding: 8px 8px; margin: 1px 0; border-radius: 4px; + background: transparent; cursor: pointer; + transition: background 0.08s; + height: 29px; + box-sizing: border-box; + } + .section-header-flex:hover { background: color-mix(in srgb, var(--red) 8%, transparent); } + .section-header-flex::after { content: none; } + .section-header-flex h4::before { content: none; } + + /* Section title text — same font as .list-item .grow */ + .section-header-flex h4, + .section-header-flex .section-title { + flex: 1; cursor: pointer; user-select: none; + display: flex; align-items: center; gap: 6px; + margin: 0; padding: 0; border: none; + font-size: 10px; font-weight: 400; font-family: inherit; + /* 1.3 (not 1) so Fira Code's tall glyph box isn't vertically clipped in + Chromium/Edge — mirrors the .list-item fix. The title is flex-centred + in a fixed-height (29px) header, so this adds headroom without reflow. */ + line-height: 1.3; letter-spacing: 0; text-transform: none; + color: var(--fg); + } + .section-icon, + .sidebar-action-icon { + flex-shrink: 0; + stroke: var(--accent, var(--red)); + position: relative; + left: -1px; + color: var(--accent, var(--red)); + } + /* Shared notification dot for sidebar section titles. Single source of + truth so chats / email / assistant / future-section dots all sit at + the same offset from their label. */ + .sidebar-notif-dot { + display: inline-block; + width: 6px; + height: 6px; + /* Push to the right edge of the flex section-title so chats / email + / assistant dots all line up vertically in the same column + instead of trailing right after each (differently-sized) label. */ + margin-left: auto; + border-radius: 50%; + background: var(--accent, var(--red)); + flex-shrink: 0; + vertical-align: middle; + } + /* The email notification gets a soft breathing glow so new-mail catches + the eye without being shouty. Vertical alignment stays with the + inherited .sidebar-notif-dot rule so it lines up with chats/assistant. */ + #email-unread-dot.sidebar-notif-dot { + animation: email-notif-breathe 2.2s ease-in-out infinite; + /* Nudge in from the far-right edge so it doesn't crowd the corner. */ + margin-right: 4px; + /* Tiny vertical nudge to center with the email label. */ + position: relative; + top: 0.1px; + } + @media (prefers-reduced-motion: reduce) { + #email-unread-dot.sidebar-notif-dot { animation: none; } + } + @media (max-width: 768px) { + /* Nudge the sidebar notification dot up 1px on mobile so it lines + up with the bigger section titles. vertical-align:middle drifts + a hair low against the larger touch-friendly text. */ + .sidebar-notif-dot { + position: relative; + top: -1px; + } + /* Cookbook's sidebar status dot carries an inline top:-1px, so override + with the ID + !important to nudge it 2px left / 1px up on mobile. */ + #cookbook-notif-dot { + left: -1px !important; + top: -2px !important; + } + } + #sidebar-new-chat-btn svg { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); + } + #sidebar-new-chat-btn:hover svg { + transform: rotate(90deg); + } + + /* Sort/select buttons in header — compact */ + .section-header-flex > div { display: flex; align-items: center; gap: 2px; } + .section-header-btn { + all: unset; + cursor: pointer; opacity: 0.4; padding: 1px 3px; border-radius: 4px; + display: inline-flex; align-items: center; justify-content: center; + transition: opacity 0.08s, background 0.08s; + } + .section-header-btn:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 7%, transparent); } + .section-header-btn.active { opacity: 0.9; color: var(--accent); } + .section-header-btn svg { width: 12px; height: 12px; } + #session-sort-btn { transform: translateX(2px); } + + /* Chats library — grid icon, hover-reveal so the header only toggles + collapse. Uses !important to win over .list-item-plus-btn's + opacity:1!important (the email-style plus button forces always-on, + which we don't want here — the manage button should fade until the + user actually hovers the section). */ + #sessions-section .chats-manage-btn { + opacity: 0.4 !important; + transition: opacity 0.12s, background 0.08s; + } + #sessions-section .section-header-flex:hover .chats-manage-btn, + #sessions-section .chats-manage-btn:hover, + #sessions-section .chats-manage-btn:focus-visible { + opacity: 0.85 !important; + } + #sessions-section .chats-manage-btn:hover, + #sessions-section .chats-manage-btn:focus-visible { + opacity: 1 !important; + } + /* No hover background on the Chats manage button — the inherited + section-header-btn rule was painting a tinted box behind the + 'manage' label as it slid in. */ + #sessions-section .chats-manage-btn, + #sessions-section .chats-manage-btn:hover, + #sessions-section .chats-manage-btn:focus-visible { + background: transparent !important; + } + @media (hover: none) { + #sessions-section .chats-manage-btn { opacity: 0.35 !important; } + #sessions-section .chats-manage-btn:active { opacity: 1 !important; } + } + /* Pull the 'manage' label out of email's absolute-positioning trick + so it sits in the flex flow and expands the button's clickable + area as it slides in. Otherwise hovering the visible 'manage' + text would actually be outside the button's bounding box. */ + #sessions-section .chats-manage-btn .list-item-plus-label { + position: static; + transform: none; + max-width: 0; + overflow: hidden; + padding-right: 0; + transition: max-width 0.2s ease, opacity 0.18s ease, padding-right 0.18s ease; + } + #sessions-section .chats-manage-btn:hover .list-item-plus-label, + #sessions-section .chats-manage-btn:focus-visible .list-item-plus-label, + #sessions-section .section-header-flex:hover .chats-manage-btn .list-item-plus-label { + max-width: 80px; + padding-right: 4px; + } + + /* Collapse chevron */ + .section-collapse-btn { + all: unset; + cursor: pointer; display: inline-flex; align-items: center; padding: 0 2px; border-radius: 4px; + } + .section-collapse-btn:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); } + .section-collapse-chevron { display: inline-flex; opacity: 0.4; transition: transform 0.2s, opacity 0.15s; } + .section-collapse-btn:hover .section-collapse-chevron { opacity: 0.6; } + .section.collapsed .section-collapse-chevron { transform: rotate(-90deg); opacity: 0.4; } + .section-header-flex:has(.section-header-btn) .section-collapse-btn { display: none; } + .section.collapsed .section-collapse-btn { display: inline-flex !important; } + + /* Collapsed state */ + .section.collapsed > *:not(h4):not(.section-title):not(.section-header-flex) { display: none !important; } + .section.collapsed .section-header-flex { margin-bottom: 0; } + .section.collapsed .section-header-btn { display: none; } + .section.collapsed { cursor: pointer; } + + /* Domino expand: every time a section goes from .collapsed → open + (toggleCollapse in section-management.js adds .section-just-expanded + for ~700ms), the .list-item children cascade in one after another, + same feel as the chat input's tools menu. Each row springs in + from a tiny offset below + scaled-down, staggered by nth-child. */ + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header) { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; + } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(1) { animation-delay: 0.04s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(2) { animation-delay: 0.08s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(3) { animation-delay: 0.12s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(4) { animation-delay: 0.16s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(5) { animation-delay: 0.20s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(6) { animation-delay: 0.24s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(7) { animation-delay: 0.28s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(8) { animation-delay: 0.32s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(9) { animation-delay: 0.36s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(10) { animation-delay: 0.40s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(11) { animation-delay: 0.44s; } + .section.section-just-expanded :is(.list-item, .models-row, .date-section-header):nth-child(12) { animation-delay: 0.48s; } + @keyframes section-domino-in { + 0% { opacity: 0; transform: translateY(8px) translateX(-4px) scale(0.92); } + 60% { opacity: 1; } + 100% { opacity: 1; transform: translateY(0) translateX(0) scale(1); } + } + + /* Domino collapse: when toggleCollapse goes open → closed, JS adds + .section-just-collapsing for ~530ms before flipping in .collapsed. + During that window the items fade/slide DOWN one after another so + you see them peel off instead of vanishing as a block. Uses + nth-last-child so the BOTTOM item leaves first and the cascade + rolls upward — mirrors the "stacked deck" feeling of the open + animation reversed. */ + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header) { + animation: section-domino-out 0.22s ease-in forwards; + } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(1) { animation-delay: 0.00s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(2) { animation-delay: 0.025s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(3) { animation-delay: 0.05s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(4) { animation-delay: 0.075s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(5) { animation-delay: 0.10s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(6) { animation-delay: 0.125s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(7) { animation-delay: 0.15s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(8) { animation-delay: 0.175s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(9) { animation-delay: 0.20s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(10) { animation-delay: 0.225s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(11) { animation-delay: 0.25s; } + .section.section-just-collapsing :is(.list-item, .models-row, .date-section-header):nth-last-child(12) { animation-delay: 0.275s; } + @keyframes section-domino-out { + 0% { opacity: 1; transform: translateY(0) translateX(0) scale(1); } + 100% { opacity: 0; transform: translateY(6px) translateX(-3px) scale(0.94); } + } + + .row { display:flex; gap:6px; align-items:center; } + .list-item:hover, + .models-row:hover { + background: color-mix(in srgb, var(--red) 8%, transparent); + border-color: var(--red); + } + #models.models-refreshing { + opacity: 0.72; + transition: opacity 0.16s ease; + } + /* Disabled tool — dimmed to signal its feature is turned off globally */ + .list-item.tool-disabled { + opacity: 0.4; + } + .list-item.tool-disabled:hover { + opacity: 0.7; + } + /* Session bulk select mode */ + .session-bulk-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-bottom: 1px solid var(--border); + background: var(--sidebar-bg, var(--panel)); + position: sticky; + top: 0; + z-index: 3; + font-size: 11px; + } + .session-bulk-bar.hidden { display: none; } + #session-select-all-dot { + display: inline-block; + position: relative; + top: -2px; + } + .session-bulk-btn { + background: none; + border: none; + border-radius: 4px; + color: var(--fg); + padding: 4px; + font-family: inherit; + cursor: pointer; + opacity: 0.4; + transition: opacity 0.1s; + display: inline-flex; + align-items: center; + justify-content: center; + } + .session-bulk-btn:hover { opacity: 1; } + .session-bulk-btn-danger { color: var(--red); opacity: 0.5; } + .session-bulk-btn-danger:hover { opacity: 1; } + /* Checkbox in select mode */ + .session-select-cb { + accent-color: var(--accent, var(--red)); + margin: 0 4px 0 0; + flex-shrink: 0; + cursor: pointer; + } + + .session-icon, + .model-icon { + flex-shrink: 0; + opacity: 0.35; + display: inline-flex; + align-items: center; + } + .session-icon.has-docs { + color: inherit; + opacity: 0.5; + } + .session-star { + width: 10px; height: 10px; + border-radius: 50%; + border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent); + flex-shrink: 0; + position: relative; + } + .session-star.processing { + animation: none; + } + .session-star.provider-logo.processing svg { + animation: none; + } + .session-star.notify { + animation: none; + opacity: 1; + /* Completed runs keep the provider slot but turn it into a compact + success check, so the logo does not simply appear to vanish. */ + width: 14px; height: 14px; + background: var(--color-success, #4caf50); + border-color: var(--color-success, #4caf50); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + } + /* The dot doubles as the provider-logo holder; hide that logo in the + done state so the solid notif dot doesn't collide with the SVG behind it. */ + .session-star.notify svg { display: none; } + .session-star.notify::after { + content: '\2713'; + font-family: system-ui, sans-serif; + font-size: 10px; + font-weight: 700; + line-height: 1; + transform: translateY(-0.5px); + } + .session-fav { + flex-shrink: 0; + cursor: pointer; + color: var(--red); + opacity: 0.6; + display: inline-flex; + align-items: center; + transition: opacity 0.1s; + } + .session-fav:hover { + opacity: 1; + } + .list-item.active-session { + background: color-mix(in srgb, var(--red) 10%, transparent); + border-color: var(--red); + border-left-color: var(--red); + } + .list-item .provider-logo { opacity: 0.4 !important; } + .list-item:has(.session-star.processing) .provider-logo { opacity: 1 !important; } + .list-item.stream-complete .provider-logo { opacity: 1 !important; } + .list-item.active { + background: color-mix(in srgb, var(--red) 10%, transparent); + border-color: var(--red); + border-left-color: var(--red); + } + /* Only show the red focus ring for keyboard navigation (Tab). Mouse + clicks shouldn't leave a sticky red outline on a sidebar item. */ + .list-item:focus { outline: none; } + .list-item:focus-visible { + outline: none; + border-color: var(--red, var(--color-error)); + box-shadow: 0 0 0 1px var(--red, var(--color-error)); + } + /* Drag handles hidden by default, shown via body.rearrange-mode */ + .drag-handle, .item-drag-handle, .folder-drag-handle { display: none; } + body.rearrange-mode .drag-handle, + body.rearrange-mode .item-drag-handle, + body.rearrange-mode .folder-drag-handle { display: inline; } + + /* Drag sorting styles for list items */ + .item-drag-handle { + cursor: grab; + opacity: 0.4; + font-size: 10px; + padding: 0 4px; + user-select: none; + letter-spacing: -2px; + transition: opacity 0.15s; + } + .item-drag-handle:hover { + opacity: 1; + color: var(--red); + } + .item-drag-handle:active { + cursor: grabbing; + } + .list-item.dragging, + .models-row.dragging, + .session-folder.dragging { + opacity: 0.95; + box-shadow: 0 8px 24px color-mix(in srgb, var(--red) 30%, transparent); + border-color: var(--red); + background: var(--panel); + cursor: grabbing; + } + .session-item.dragging-to-folder { + opacity: 0.55; + border-color: var(--red); + box-shadow: 0 4px 14px color-mix(in srgb, var(--red) 22%, transparent); + } + .list-item.dragging .item-drag-handle, + .models-row.dragging .item-drag-handle, + .session-folder.dragging .folder-drag-handle { + opacity: 1; + color: var(--red); + } + /* ── Model category grouping ── */ + .models-category-header { + display: flex; align-items: center; gap: 6px; + padding: 4px 8px; cursor: pointer; + font-size: 0.85em; font-weight: 600; + color: color-mix(in srgb, var(--fg) 55%, transparent); + border-radius: 4px; user-select: none; + margin-top: 4px; + transition: opacity 0.08s, background 0.08s, color 0.08s; + } + .models-category-header:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); } + .models-category-header .folder-count { font-weight: 400; opacity: 0.5; font-size: 0.9em; } + .models-endpoint-label { + display: flex; align-items: center; gap: 6px; + padding: 4px 8px; cursor: pointer; + font-size: 0.85em; font-weight: 600; + color: color-mix(in srgb, var(--fg) 55%, transparent); + border-radius: 4px; user-select: none; + transition: opacity 0.08s, background 0.08s; + } + .models-endpoint-label:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); } + .models-endpoint-label .folder-count { font-weight: 400; opacity: 0.5; } + .models-group-content.indented { + padding-left: 4px; + } + .models-group-content.indented .models-row { border-bottom: 1px solid color-mix(in srgb, var(--fg) 7%, transparent); } + .models-group-content.indented .models-row:last-child { border-bottom: none; } + /* ── Session folders ── */ + .session-folder { margin: 2px 0; } + .session-folder-header { + display: flex; align-items: center; gap: 6px; + padding: 4px 8px; cursor: pointer; + font-size: 0.88em; font-weight: 600; + color: color-mix(in srgb, var(--fg) 55%, transparent); + border-radius: 4px; user-select: none; + transition: opacity 0.08s, background 0.08s; + } + .session-folder-header:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); } + .session-folder-header .folder-count { font-weight: 400; opacity: 0.5; } + .session-folder-header.drag-over { + outline: 2px dashed var(--red); background: color-mix(in srgb, var(--red) 13%, transparent); opacity: 1; + } + .session-folder.dragging-folder { opacity: 0.4; } + .folder-toggle { font-size: 0.7em; width: 10px; text-align: center; } + .folder-name { font-weight: inherit; flex: 1; } + .folder-count { font-size: 0.85em; opacity: 0.5; } + .folder-drag-handle { + cursor: grab; opacity: 0.3; font-size: 0.8em; padding: 0 2px; + transition: opacity 0.08s; + } + .session-folder-header:hover .folder-drag-handle { opacity: 0.7; } + .folder-delete-btn { + background: none; border: none; color: var(--fg); opacity: 0; + cursor: pointer; font-size: 1.1em; padding: 0 2px; line-height: 1; + min-height: 0; height: auto; + transition: opacity 0.08s, color 0.08s; + } + .session-folder-header:hover .folder-delete-btn { opacity: 0.5; } + .folder-delete-btn:hover { opacity: 1 !important; color: var(--color-error); } + .session-folder-content { padding-left: 4px; } + .session-folder-content .list-item { border-bottom: 1px solid color-mix(in srgb, var(--fg) 7%, transparent); } + .session-folder-content .list-item:last-child { border-bottom: none; } + .session-show-more-btn { + display: block; + width: 100%; + background: none; + border: none; + color: var(--fg); + opacity: 0.4; + font-size: 0.8em; + padding: 6px 8px; + cursor: pointer; + text-align: center; + transition: opacity 0.15s; + } + .session-show-more-btn:hover { opacity: 0.8; } + .drag-folder-placeholder { + height: 4px; margin: 2px 0; border-radius: 2px; + background: var(--red); opacity: 0.6; + } + .unfiled-drop-zone { + min-height: 8px; border-radius: 4px; transition: all 0.15s; + } + .unfiled-drop-zone.drag-over { + min-height: 24px; outline: 2px dashed var(--red); + background: color-mix(in srgb, var(--red) 9%, transparent); + } + + /* Mobile swipe-to-delete */ + .swipe-delete-action { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 60px; + background: color-mix(in srgb, var(--color-error) 12%, transparent); + color: var(--color-error); + display: flex; + align-items: center; + justify-content: center; + border-radius: 0 4px 4px 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + cursor: pointer; + z-index: 1; + } + .swipe-delete-action::before { + content: ''; + display: block; + width: 18px; + height: 18px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23ff4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 6 5 6 21 6'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E"); + background-size: contain; + background-repeat: no-repeat; + } + .swipe-delete-action:active { + background: color-mix(in srgb, var(--color-error) 22%, transparent); + } + .drag-placeholder { + background: color-mix(in srgb, var(--red) 10%, transparent); + border: 2px dashed color-mix(in srgb, var(--color-accent) 40%, transparent); + border-radius: 6px; + margin: 4px 0; + transition: height 0.15s ease; + } + .list-item, + .models-row { + display: flex; + gap: 6px; + align-items: center; + padding: 3px 8px; + margin: 0; + border-radius: 4px; + border: none; + line-height: 1.3; + font-size: 13px; + background: transparent; + transition: background 0.08s; + cursor: pointer; + touch-action: pan-y; + } + .list-item button { + margin: 0; + height: 24px; + padding: 0 8px; + font-size: 9px; + } + /* Inline "+" action on a tool/section row (Library → new document, + Email → compose). Sizing forced + min-* zeroed so the mobile + touch-target rule (44px) can't inflate it. Selector intentionally + NOT scoped to `.list-item` so the same class also styles buttons + sitting in `.section-header-flex` (e.g. #email-compose-btn). */ + .list-item-plus-btn { + all: unset; + box-sizing: border-box; + flex-shrink: 0; + position: relative; + left: 4px; + display: inline-flex !important; + align-items: center; + justify-content: center; + height: 14px !important; + min-height: 0 !important; + width: auto !important; + min-width: 0 !important; + padding: 0 5px !important; + border-radius: 4px; + color: var(--fg); + /* Always visible at rest. On hover (devices that have it) the + spins + 90° and "new" reveals to its right — neat expand affordance. */ + opacity: 1 !important; + cursor: pointer; + gap: 0; + overflow: visible; + z-index: 1; + transition: background 0.12s, color 0.12s, gap 0.18s ease; + } + .list-item-plus-btn svg.list-item-plus-icon { + width: 13px; height: 13px; + transition: transform 0.22s ease; + } + /* Legacy fallback for plus-btns without the `.list-item-plus-icon` class. */ + .list-item-plus-btn svg:not(.list-item-plus-icon) { width: 13px; height: 13px; } + /* Label is absolutely positioned to the LEFT of the icon so the + stays + put — it just appears beside it on hover instead of pushing it. The + button's intrinsic width remains the icon, so neighbours don't shift. */ + .list-item-plus-label { + position: absolute; + right: 100%; + top: 50%; + transform: translateY(-50%); + padding-right: 5px; + opacity: 0; + pointer-events: none; + white-space: nowrap; + font-size: 9.5px; + line-height: 1; + letter-spacing: 0.02em; + transition: opacity 0.18s ease, transform 0.22s ease; + } + @media (hover: hover) { + .list-item-plus-btn:hover .list-item-plus-icon { + transform: rotate(90deg); + } + .list-item-plus-btn:hover .list-item-plus-label { + opacity: 1; + transform: translateY(-50%) translateX(0); + pointer-events: auto; + } + .list-item-plus-btn .list-item-plus-label { + transform: translateY(-50%) translateX(6px); + } + } + .list-item-plus-btn:hover { + background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); + color: var(--accent, var(--red)); + } + .list-item span { + color: var(--fg); + font-size: 9.75px; + } + .grow { + flex: 1; + overflow: hidden; + text-overflow: clip; + white-space: nowrap; + } + input, textarea, button, select { + background:var(--bg); + color:var(--fg); + border:1px solid var(--border); + font-family:inherit; + padding:6.9px; + border-radius: 4px; + transition: background 0.08s, border-color 0.08s, color 0.08s, opacity 0.08s; + } + input:hover, textarea:hover, button:hover, select:hover { + border-color: var(--fg); + background-color: var(--panel); + } + :root.light input, + :root.light textarea, + :root.light button, + :root.light select { + background:#eaeaea; + color-scheme: light; + } + input[type="text"] { height:32px; width:100%; } + textarea { width:100%; min-height:32px; height:auto; max-height:30lh; overflow-y:auto; resize:none; } + button { height:32px; padding:0 10px; } + #chat-form button[type="submit"] { height:38px; } + select { + height:32px; + color-scheme: dark; + background-color: var(--select-bg); + color: var(--select-fg); + } + select option, + select optgroup { + background-color: var(--select-option-bg); + color: var(--select-option-fg); + } + select option:checked { + background-color: var(--select-option-active-bg); + color: var(--select-option-fg); + } + :root.light select { color-scheme: light; } + .chat-container { + flex:1; + display:flex; + flex-direction:column; + padding:0 16px; + overflow:hidden; + position:relative; + min-height:0; + min-width:0; + margin-top:8px; + margin-bottom: 0; + transition: margin-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1), + margin-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1); + } + .chat-meta { font-size:12px; color:color-mix(in srgb, var(--fg) 60%, transparent); margin-bottom:6px; } + .chat-history { + display:flex; + flex-direction:column; + flex:1; + overflow-y:auto; + overflow-x:hidden; + overscroll-behavior-y: none; + margin-bottom:8px; + white-space:normal; + min-height:0; + /* Streaming completion replaces the live turn with its persisted DOM. + We preserve the scroll position explicitly; native anchoring would + otherwise fight that replacement and create a visible bounce. */ + overflow-anchor: none; + --chat-max: 800px; + padding-left: max(0px, calc((100% - var(--chat-max)) / 2)); + padding-right: max(12px, calc((100% - var(--chat-max)) / 2 + 12px)); + } + .chat-history > * { + flex: 0 0 auto; + } + .chat-pull-refresh { + --pull-refresh-y: 0px; + --pull-refresh-progress: 0; + position: absolute; + top: 8px; + left: 50%; + z-index: 48; + width: 34px; + height: 34px; + display: none; + align-items: center; + justify-content: center; + border: 1px solid color-mix(in srgb, var(--border) 75%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--panel) 94%, transparent); + box-shadow: 0 8px 20px color-mix(in srgb, #000 16%, transparent); + opacity: calc(0.25 + (var(--pull-refresh-progress) * 0.75)); + pointer-events: none; + transform: translate(-50%, calc(-42px + var(--pull-refresh-y))) scale(calc(0.82 + (var(--pull-refresh-progress) * 0.18))); + transition: opacity 0.16s ease, transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1), border-color 0.16s ease; + } + .chat-pull-refresh.is-visible, + .chat-pull-refresh.is-refreshing { + display: inline-flex; + } + .chat-pull-refresh.is-ready, + .chat-pull-refresh.is-refreshing { + border-color: color-mix(in srgb, var(--accent, var(--red)) 58%, transparent); + color: var(--accent, var(--red)); + } + .chat-pull-refresh .spinner-whirlpool { + margin: 0 !important; + width: 18px !important; + height: 18px !important; + } + .chat-pull-refresh canvas { + display: block; + } + #email-lib-modal .email-pull-refresh { + top: 112px; + z-index: 4; + } + /* Sortable Cookbook column headers had no visual cue, so users couldn't tell + a header was clickable (the Newest sort on the Model column was invisible). + Show a pointer + hover highlight, and underline the active sort column. */ + .hwfit-header .hwfit-sortable { cursor: pointer; transition: color .12s; } + .hwfit-header .hwfit-sortable:hover { color: var(--fg); text-decoration: underline dotted; } + .hwfit-header .hwfit-sort-active { color: var(--fg); font-weight: 600; } + /* Welcome screen — centered in available space above input bar */ + #welcome-screen { + position:absolute; + top:40%; + left:50%; + transform:translate(-50%,-50%); + display:flex; + flex-direction:column; + align-items:center; + text-align:center; + pointer-events:none; + animation: welcome-enter 0.4s ease-out both; + transition: top 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, transform 0.3s ease; + } + #welcome-screen .welcome-tip, + #welcome-screen .welcome-sub, + #welcome-screen .welcome-version { + transition: opacity 0.25s ease, max-height 0.25s ease, margin 0.25s ease; + max-height: 60px; + overflow: hidden; + } + /* The tip is a full sentence that wraps to 4-5 lines on narrow phones, + where the welcome block shrink-wraps small; the shared 60px ceiling + (sized for the one-line sub/version) clipped its last line. Give the tip + a taller ceiling so it isn't truncated. Kept above the max-height:650px + block below so that rule's max-height:0 still collapses it on short + viewports. */ + #welcome-screen .welcome-tip { + max-height: 120px; + } + @media (max-height: 650px) { + #welcome-screen { top: 28%; } + #welcome-screen .welcome-tip { opacity: 0; max-height: 0; margin: 0; } + #welcome-screen .welcome-version { margin-top: 6px; } + .incognito-btn { margin-top: 8px; } + } + @media (max-height: 500px) { + #welcome-screen { top: 22%; } + #welcome-screen .welcome-name { font-size: 1.4rem; margin-bottom: 0; } + #welcome-screen .welcome-sub { opacity: 0; max-height: 0; margin: 0; } + .incognito-btn { margin-top: 4px !important; } + } + @media (max-height: 380px) { + #welcome-screen { opacity: 0; pointer-events: none; } + } + #welcome-screen.hidden { display:none; } + #welcome-screen.kb-hidden { + opacity: 0; + pointer-events: none; + transform: translate(-50%, -50%); + transition: opacity 0.16s ease; + } + @media (max-width: 768px) { + #welcome-screen { top: 42%; } + #welcome-screen .welcome-name { margin-bottom: 2px; } + #welcome-screen .welcome-sub { margin-bottom: 0; } + #welcome-screen .incognito-btn { margin-top: 6px; } + } + #welcome-screen .welcome-name { + font-size:2.2rem; + font-weight:700; + background: linear-gradient(135deg, var(--brand-color, var(--red)), color-mix(in srgb, var(--brand-color, var(--red)) 60%, var(--fg))); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing:0.03em; + margin-bottom:10px; + } + .welcome-boat { + width: 1.8rem; + height: 1.8rem; + margin-right: 0.4rem; + vertical-align: -0.15em; + color: var(--brand-color, var(--red)); + } + #welcome-screen .welcome-sub { + font-size:0.85rem; + color:color-mix(in srgb, var(--fg) 60%, transparent); + opacity:0.6; + line-height:1.5; + max-width:300px; + } + #welcome-screen .welcome-version { + font-size:0.7rem; + opacity:0.25; + margin-top:12px; + pointer-events:none; + user-select:none; + } + #welcome-screen .welcome-tip { + font-size:0.75rem; + color:var(--fg); + opacity:0.2; + margin-top:24px; + max-width:320px; + line-height:1.4; + } + #welcome-screen .welcome-tip.nobody-session-note { + display:flex; + flex-direction:column; + gap:2px; + } + /* Incognito toggle — on welcome screen */ + .incognito-btn { + pointer-events: auto; + background: none; + border: 1px solid var(--border); + color: var(--fg); + opacity: 0.25; + cursor: pointer; + padding: 6px 12px; + border-radius: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.15s; + margin-top: 16px; + font-family: inherit; + font-size: 11px; + max-width: min(160px, 70vw); + } + .incognito-btn:hover { + opacity: 0.6; + background: color-mix(in srgb, var(--fg) 6%, transparent); + } + .incognito-btn.active { + opacity: 1; + color: var(--accent); + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 10%, transparent); + } + .incognito-btn.active .eye-open { display: none; } + .incognito-btn.active .eye-blinded { display: inline !important; } + .incognito-label { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.3px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + /* When welcome is active, push input bar up toward center */ + .chat-container .chat-input-bar { + transition: margin 0.3s ease, max-width 0.3s ease; + } + .chat-container.welcome-active .chat-input-bar { + margin-bottom:30vh; + margin-left:auto; + margin-right:auto; + max-width: 800px; + width: 100%; + } + @media (max-width:768px) { + .chat-meta-overlay { + left: 36px; + right: 36px; + } + #welcome-screen .welcome-name { font-size:1.8rem; } + #welcome-screen .welcome-sub { font-size:0.8rem; } + .chat-container.welcome-active .chat-input-bar { + margin-bottom:0; + margin-left:0; + margin-right:0; + position: relative; + z-index: 20; + } + .chat-container.welcome-active:has(.chat-input-bar:focus-within) #welcome-screen, + .chat-container.welcome-active:has(textarea#message:not(:placeholder-shown)) #welcome-screen { + opacity: 0; + pointer-events: none; + transform: translate(-50%, -50%); + transition: opacity 0.16s ease; + } + .chat-container.welcome-active:has(.input-icon-btn.expanded) #welcome-screen, + .chat-container.welcome-active:has(.send-btn.newchat-expanded) #welcome-screen, + .chat-container.welcome-active:has(#model-picker-wrap:not(.picker-auto-hidden) .model-picker-menu:not(.hidden)) #welcome-screen { + opacity: 0.45; + transform: translate(-50%, -76%) scale(0.94); + } + .chat-container.welcome-active:has(.input-icon-btn.expanded) #welcome-screen .welcome-tip, + .chat-container.welcome-active:has(.send-btn.newchat-expanded) #welcome-screen .welcome-tip, + .chat-container.welcome-active:has(#model-picker-wrap:not(.picker-auto-hidden) .model-picker-menu:not(.hidden)) #welcome-screen .welcome-tip { + opacity: 0.14; + } + .chat-container.welcome-active textarea#message { + max-height: min(34vh, 150px); + } + } + @media (max-width: 420px) { + .chat-meta-overlay { + left: 34px; + right: 34px; + } + .chat-meta-overlay #session-cost-display, + .chat-meta-overlay .chat-meta-count { + display: none !important; + } + #welcome-screen { + width: calc(100vw - 36px); + } + #welcome-screen .welcome-name, + #welcome-screen .welcome-sub, + #welcome-screen .welcome-tip { + max-width: 100%; + } + .incognito-btn { + padding: 6px 9px; + max-width: 120px; + } + } + @media (max-width: 340px) { + .incognito-btn .incognito-label { + display: none; + } + .incognito-btn { + width: 30px; + height: 30px; + padding: 0; + border-radius: 999px; + } + } + .msg { + margin: 8px 0; + position: relative; + display: flex; + flex-direction: column; + width: fit-content; + max-width: 85%; + min-width: 80px; + border-radius: 12px; + padding: 10px 12px; + line-height: 1.4; + word-wrap: break-word; + overflow-wrap: break-word; + overflow: hidden; + animation: msg-enter 0.3s ease-out both; + } + .msg-user { + align-items: flex-end; + margin-left: auto; + margin-right: 8px; + background: var(--user-bubble-bg, color-mix(in srgb, var(--fg) 8%, var(--bg))); + border: 1px solid var(--bubble-border, var(--border)); + border-radius: 18px 18px 0 18px; + align-self: flex-end; + width: fit-content; + max-width: 85%; + min-width: 80px; + word-wrap: break-word; + overflow-wrap: break-word; + overflow: hidden; + } + .msg-user:has(textarea) { + width: 85%; + } + .msg-ai { + align-items: flex-start; + margin-right: auto; + margin-left: 8px; + background: var(--ai-bubble-bg, var(--panel)); + border: 1px solid var(--bubble-border, var(--border)); + border-radius: 18px 18px 18px 0; + align-self: flex-start; + width: 85%; + max-width: 85%; + min-width: 80px; + word-wrap: break-word; + overflow-wrap: break-word; + overflow: hidden; + transition: min-height 0.25s ease-out; + } + .msg .role { + font-weight: 600; + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .msg .role::before { + content: ''; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--model-dot, color-mix(in srgb, var(--fg) 30%, transparent)); + flex-shrink: 0; + } + .msg .role.has-logo::before { display: none; } + .role-provider-logo { display: inline-flex; align-items: center; flex-shrink: 0; } + .role-provider-logo svg { width: 12px; height: 12px; } + .msg-user .role { + color: color-mix(in srgb, var(--fg) 60%, transparent); + } + .msg-user .role::before { + background: color-mix(in srgb, var(--fg) 40%, transparent); + } + .user-mode-pill { + display: inline-flex; + align-items: center; + justify-content: center; + height: 15px; + min-width: 15px; + padding: 0 3px; + border-radius: 4px; + border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); + background: color-mix(in srgb, var(--fg) 6%, transparent); + color: color-mix(in srgb, var(--fg) 58%, transparent); + font-size: 10px; + font-weight: 600; + line-height: 15px; + text-transform: lowercase; + } + .user-mode-pill svg { + display: block; + width: 11px; + height: 11px; + } + .user-mode-pill[data-auto="true"]::after { + content: 'auto'; + margin-left: 4px; + opacity: 0.65; + font-weight: 500; + } + .msg .body { + width: 100%; + white-space: normal; + word-break: break-word; + overflow-wrap: anywhere; + line-height: 1.5; + font-size: 0.95em; + margin: 0; + overflow: hidden; + max-width: 100%; + } + .msg .body > * { + margin-top: 8px; + margin-bottom: 8px; + } + .msg .body > *:first-child { + margin-top: 0; + } + .msg .body > *:last-child { + margin-bottom: 0; + } + .msg .body p:empty { + display: none; + } + .msg-user .body { + color: var(--fg); + } + .chat-queued-bubble-host { + order: 2147483647; + display: flex; + flex-direction: column; + align-items: stretch; + flex: 0 0 auto; + width: 100%; + min-width: 0; + } + .msg-user.msg-user-queued { + opacity: 0.62; + cursor: pointer; + animation: none; + border: 1px dashed color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); + background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--input-bg, var(--panel))); + transition: opacity 0.12s ease, border-color 0.12s ease, transform 0.12s ease; + } + .msg-user.msg-user-queued:hover { + opacity: 0.9; + border-color: color-mix(in srgb, var(--accent, var(--red)) 85%, transparent); + transform: translateY(-1px); + } + .msg-user.msg-user-queued .queued-pill { + display: inline-flex; + align-items: center; + gap: 3px; + height: 14px; + padding: 0 5px; + margin-left: 5px; + border-radius: 999px; + border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent); + color: var(--accent, var(--red)); + background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent); + font-size: 8px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + vertical-align: 1px; + } + .msg-user.msg-user-queued .queued-pill svg { + flex: 0 0 auto; + opacity: 0.9; + } +.msg-ai .body { + color: var(--fg); +} +.model-endpoint-add-btn { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 7px; + padding: 2px 7px; + border: 1px solid color-mix(in srgb, var(--red) 34%, var(--border)); + border-radius: 999px; + background: color-mix(in srgb, var(--red) 8%, transparent); + color: var(--red); + font: inherit; + font-size: 0.78em; + line-height: 1.45; + cursor: pointer; + vertical-align: 1px; +} +.model-endpoint-add-btn:hover { + background: color-mix(in srgb, var(--red) 14%, transparent); + border-color: color-mix(in srgb, var(--red) 55%, var(--border)); +} +.model-endpoint-add-btn:disabled { + cursor: default; + opacity: 0.72; +} +.model-endpoint-add-btn.added { + color: var(--color-save-green, #4caf50); + border-color: color-mix(in srgb, var(--color-save-green, #4caf50) 45%, var(--border)); + background: color-mix(in srgb, var(--color-save-green, #4caf50) 9%, transparent); +} +.task-completed-marker { + display: inline-flex; + align-items: center; + gap: 7px; + margin: 7px 0 2px; + padding: 5px 9px; + border: 1px solid color-mix(in srgb, var(--color-save-green, #4caf50) 42%, var(--border)); + border-radius: 999px; + background: color-mix(in srgb, var(--color-save-green, #4caf50) 9%, transparent); + color: var(--color-save-green, #4caf50); + font-size: 0.86em; + font-weight: 600; +} +.task-completed-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 17px; + height: 17px; + border-radius: 50%; + background: color-mix(in srgb, var(--color-save-green, #4caf50) 18%, transparent); + flex: 0 0 auto; +} +.rag-sources { + margin-top: 12px; + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px; + font-size: 11px; + } + .rag-sources summary { + cursor: pointer; + color: var(--red); + font-weight: bold; + font-size: 11px; + } + .rag-source-item { + margin-top: 8px; + padding: 6px; + background: color-mix(in srgb, var(--fg) 4%, transparent); + border-radius: 4px; + border-left: 2px solid var(--red); + } + .rag-similarity { + color: color-mix(in srgb, var(--fg) 50%, transparent); + font-size: 10px; + margin-left: 6px; + } + .rag-snippet { + color: color-mix(in srgb, var(--fg) 65%, transparent); + font-size: 11px; + margin-top: 4px; + white-space: pre-wrap; + max-height: 60px; + overflow: hidden; + } + .rag-file-delete { + background: none; + border: 1px solid var(--border); + border-radius: 4px; + color: color-mix(in srgb, var(--fg) 50%, transparent); + cursor: pointer; + font-size: 10px; + padding: 1px 5px; + } + .rag-file-delete:hover { color: var(--fg); border-color: var(--fg); } + .rag-file-size { + color: color-mix(in srgb, var(--fg) 50%, transparent); + font-size: 10px; + margin-left: 4px; + } + .rag-upload-zone { + border: 2px dashed var(--border); + border-radius: 6px; + padding: 12px; + text-align: center; + color: color-mix(in srgb, var(--fg) 45%, transparent); + font-size: 11px; + cursor: pointer; + transition: border-color 0.2s; + } + .rag-upload-zone.dragover { + border-color: var(--red); + color: var(--red); + } + .msg .timestamp { + font-size: 10px; + color: color-mix(in srgb, var(--fg) 45%, transparent); + margin-top: 4px; + text-align: right; + opacity: 0.7; + } + .msg-user .timestamp { + color: color-mix(in srgb, var(--fg) 72%, transparent); + } + pre { + overflow: hidden; + padding: 6px 4px 2px 4px; + border: 1px solid var(--border); + background: var(--bg); + position: relative; + line-height: 1.4; + font-size: 0.95em; + font-family: 'Fira Code', 'Courier New', monospace; + min-height: 38px; + max-width: 100%; + margin: 8px 0; + white-space: pre-wrap; + word-break: break-word; + border-radius: 4px; + min-height: 20px; + min-width: 80px; + } + + /* Ensure code block headers are only slightly larger than regular text */ + pre h1, pre h2, pre h3, pre h4, pre h5, pre h6 { + font-size: 1.1em; + font-weight: bold; + margin: 8px 0 4px 0; + color: var(--fg); + } + .code-lang { + font-size:0.8em; + color:color-mix(in srgb, var(--fg) 60%, transparent); + display:block; + margin-bottom:2px; + font-style:italic; + } + code { font-family:inherit; } + .loading { color:var(--red); font-style:italic; } + #chat-form { display:none; } + + /* Unified chat input bar */ + .chat-input-bar { + background: var(--input-bg, var(--panel)); + border: 1px solid var(--input-border, var(--border)); + border-radius: 16px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; + max-width: 800px; + margin-left: auto; + margin-right: auto; + width: 100%; + transition: border-color 0.16s ease, background 0.16s ease; + } + body.plan-mode-active .chat-input-bar { + border-style: dashed; + border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); + background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--input-bg, var(--panel))); + } + .chat-input-top { + width: 100%; + position: relative; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: start; + } + .plan-mode-status { + position: absolute; + right: 0; + bottom: 2px; + z-index: 245; + display: inline-flex; + align-items: center; + gap: 7px; + height: 22px; + padding: 0 6px 0 8px; + border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--input-bg, var(--panel))); + color: var(--accent, var(--red)); + font-size: 10px; + font-weight: 700; + line-height: 1; + transition: opacity 0.22s ease, transform 0.22s ease; + } + .plan-mode-status[hidden] { + display: none !important; + } + .plan-mode-status.plan-mode-status-autohide { + opacity: 0; + pointer-events: none; + transform: translateY(-4px) scale(0.96); + } + .plan-mode-status-toggle { + width: 26px; + height: 14px; + padding: 0; + border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent); + cursor: pointer; + position: relative; + flex-shrink: 0; + } + .plan-mode-status-toggle span { + position: absolute; + top: 2px; + right: 2px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent, var(--red)); + } + .plan-mode-status-toggle:hover { + background: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent); + } + .chat-input-top > .model-picker-wrap { + position: relative; + grid-column: 2; + grid-row: 1; + top: 0; + right: auto; + z-index: 250; + transform-origin: top right; + transition: opacity 0.22s ease, transform 0.22s ease; + will-change: opacity, transform; + } + body.plan-mode-active .chat-input-top > .model-picker-wrap { + opacity: 0; + pointer-events: none; + transform: translateY(-4px) scale(0.96); + } + .chat-input-top > .model-picker-wrap.model-picker-autohide, + .chat-input-top > .model-picker-wrap.picker-auto-hidden { + display: none !important; + opacity: 0; + pointer-events: none; + transform: translateY(-4px) scale(0.96); + } + .ghost-text-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + pointer-events: none; + white-space: pre-wrap; + overflow-wrap: break-word; + font-family: inherit; + font-size: 14px; + line-height: 1.5; + padding: 0; + border: 1px solid transparent; + color: transparent; + z-index: 1; + } + .ghost-text-overlay .ghost-suggestion { + color: color-mix(in srgb, var(--fg) 30%, transparent); + } + .chat-input-bar textarea#message { + grid-column: 1; + grid-row: 1; + min-width: 0; + width: 100%; + background: transparent; + border: none; + outline: none; + resize: none; + font-size: 14px; + line-height: 1.5; + color: var(--fg); + min-height: 24px; + max-height: 200px; + padding: 0; + font-family: inherit; + transition: height 0.12s ease-out; + } + .chat-input-bar textarea#message::placeholder { + color: color-mix(in srgb, var(--fg) 35%, transparent); + } + .chat-input-bottom { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 4px; + } + /* Progressive shrinkage: as the chat input bar gets narrow, sacrifice + chrome before the typing area. First the Agent/Chat toggle drops out, + then the model picker — textarea + send button always stay. Container + queries so it responds to *bar width*, not viewport width (the bar + can be in a split pane). */ + .chat-input-bar { container-type: inline-size; container-name: chatbar; } + @container chatbar (max-width: 340px) { + .chat-input-right .mode-toggle { display: none !important; } + } + .chat-input-left { + display: flex; + gap: 4px; + align-items: center; + flex-wrap: nowrap; + overflow: hidden; + min-width: 0; + flex: 1; + } + /* Collapsible buttons shrink away; collapsed ones disappear */ + .chat-input-left > .input-icon-btn { + flex-shrink: 0; + } + .chat-input-left > .input-icon-btn.toolbar-collapsed { + display: none !important; + } + /* Always keep overflow wrapper visible and leftmost */ + .chat-input-left > .overflow-wrapper { + flex-shrink: 0; + position: relative; + z-index: 1; + } + .input-divider { + width: 1px; + height: 16px; + background: var(--border); + opacity: 0.4; + margin: 0 2px; + flex-shrink: 0; + } + .chat-input-right { + display: flex; + gap: 8px; + align-items: center; + flex-shrink: 0; + } + /* Reasoning effort pill / dropdown in composer */ + .reasoning-effort-wrap { + position: relative; + display: inline-flex; + align-items: center; + } + .reasoning-effort-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 28px; + padding: 0 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: color-mix(in srgb, var(--fg) 4%, transparent); + color: var(--fg); + font-size: 11.5px; + font-weight: 500; + cursor: pointer; + user-select: none; + transition: background 0.15s, border-color 0.15s; + } + .reasoning-effort-btn:hover { + background: color-mix(in srgb, var(--fg) 8%, transparent); + border-color: color-mix(in srgb, var(--fg) 20%, transparent); + } + .reasoning-effort-chevron { + opacity: 0.5; + transition: transform 0.15s; + flex-shrink: 0; + } + .reasoning-effort-btn[aria-expanded="true"] .reasoning-effort-chevron { + transform: rotate(180deg); + } + .reasoning-effort-menu { + position: absolute; + bottom: calc(100% + 6px); + right: 0; + min-width: 140px; + background: var(--bg-surface, var(--bg)); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35); + padding: 4px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 2px; + } + .reasoning-effort-menu.hidden { + display: none !important; + } + .reasoning-effort-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 10px; + border-radius: 5px; + font-size: 12px; + color: var(--fg); + cursor: pointer; + background: transparent; + border: none; + text-align: left; + width: 100%; + transition: background 0.12s; + } + .reasoning-effort-option:hover { + background: color-mix(in srgb, var(--fg) 8%, transparent); + } + .reasoning-effort-option.active { + color: var(--accent); + font-weight: 600; + } + @container chatbar (max-width: 480px) { + .reasoning-effort-prefix { display: none; } + } + @media (max-width: 480px) { + .reasoning-effort-prefix { display: none; } + } + .input-icon-btn { + background: none; + border: none; + color: var(--fg); + opacity: 0.5; + cursor: pointer; + padding: 6px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.15s, background 0.15s, color 0.15s; + position: relative; + } + .input-icon-btn:hover { + opacity: 0.8; + background: color-mix(in srgb, var(--accent) 8%, transparent); + } + .input-icon-btn.active, + .input-icon-btn.expanded { + opacity: 1; + color: var(--fg); + background: color-mix(in srgb, var(--fg) 9%, transparent); + } + .plan-mode-btn { + width: 28px; + height: 28px; + padding: 0; + flex-shrink: 0; + top: -2px; + } + .plan-mode-btn.active { + color: var(--accent); + background: color-mix(in srgb, var(--accent) 14%, transparent); + } + /* GitHub-style task lists (- [ ] / - [x]) */ + li.task-item { + list-style: none; + margin-left: -1.2em; + display: flex; + align-items: flex-start; + gap: 8px; + } + li.task-item .task-check { + flex: 0 0 auto; + width: 15px; + height: 15px; + margin-top: 3px; + border-radius: 4px; + border: 1.5px solid color-mix(in srgb, var(--fg) 45%, transparent); + box-sizing: border-box; + position: relative; + } + li.task-item.task-done .task-check { + background: var(--accent); + border-color: var(--accent); + } + li.task-item.task-done .task-check::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid var(--bg); + border-width: 0 2px 2px 0; + transform: rotate(45deg); + } + li.task-item.task-done .task-text { + opacity: 0.6; + text-decoration: line-through; + } + .plan-inline-actions { + display: flex; + align-items: center; + gap: 7px; + margin-top: 10px; + } + .plan-inline-execute, + .plan-inline-clear { + height: 26px; + border-radius: 7px; + border: 1px solid color-mix(in srgb, var(--fg) 13%, transparent); + background: transparent; + color: var(--fg); + font: inherit; + font-size: 11px; + padding: 0 9px; + display: inline-flex; + align-items: center; + gap: 5px; + cursor: pointer; + } + .plan-inline-execute { + background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent); + border-color: color-mix(in srgb, var(--accent, var(--red)) 35%, transparent); + color: var(--accent, var(--red)); + font-weight: 650; + } + .plan-review-panel { + position: fixed; + right: 24px; + bottom: 92px; + z-index: 500; + width: min(520px, calc(100vw - 32px)); + max-height: min(560px, calc(100vh - 120px)); + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, var(--border)); + border-radius: 8px; + background: var(--panel); + color: var(--fg); + box-shadow: 0 14px 40px color-mix(in srgb, #000 32%, transparent); + } + .plan-review-panel[hidden] { display: none !important; } + .plan-review-panel-header, + .plan-review-actions { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + } + .plan-review-panel-header { + justify-content: space-between; + border-bottom: 1px solid var(--border); + } + .plan-review-panel-header h3 { + margin: 0; + font-size: 13px; + line-height: 1.2; + } + .plan-review-close { + width: 24px; + height: 24px; + padding: 0; + border: 0; + background: transparent; + color: var(--fg); + font-size: 19px; + line-height: 1; + cursor: pointer; + opacity: 0.65; + } + .plan-review-close:hover { opacity: 1; } + .plan-review-content { + margin: 0; + padding: 12px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-size: 12px; + line-height: 1.55; + font-family: inherit; + } + .plan-review-actions { + border-top: 1px solid var(--border); + } + .plan-review-execute, + .plan-review-clear { + height: 28px; + padding: 0 9px; + border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent); + border-radius: 6px; + background: transparent; + color: var(--fg); + font: inherit; + font-size: 11px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 5px; + } + .plan-review-execute { + border-color: color-mix(in srgb, var(--accent, var(--red)) 35%, transparent); + background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent); + color: var(--accent, var(--red)); + font-weight: 650; + } + /* While the menu is open the chevron stays in its highlighted state + — don't run the opacity fade transition so we never flash from + 0.5 → hover-1.0 → drop-back. The state holds steady. */ + .input-icon-btn.expanded { transition: none; } + /* Accent color for Research, Compare toolbar indicators */ + #research-toggle-btn.active, + .tool-indicator.active { + color: var(--red); + background: color-mix(in srgb, var(--red) 12%, transparent); + } + #workspace-indicator-btn.active { + opacity: 1; + color: var(--fg); + background: color-mix(in srgb, var(--fg) 9%, transparent); + } + #workspace-indicator-btn.active:hover { + opacity: 1; + background: color-mix(in srgb, var(--fg) 9%, transparent); + } + #workspace-indicator-btn.active > svg:not(.tool-indicator-x), + #workspace-indicator-btn.active > span { + opacity: 1; + } + #workspace-indicator-btn .workspace-indicator-icon { + opacity: 1 !important; + color: currentColor !important; + stroke: currentColor !important; + flex-shrink: 0; + } + #workspace-indicator-btn > svg:first-of-type { + opacity: 1 !important; + color: currentColor !important; + stroke: currentColor !important; + } + /* Research button glow while actively running */ + #research-toggle-btn.research-running { + opacity: 1 !important; + color: var(--red); + animation: research-pulse 2s ease-in-out infinite; + } + .tool-indicator-x { + margin-left: 2px; + opacity: 0.4; + flex-shrink: 0; + transition: opacity 0.15s; + } + .tool-indicator:hover .tool-indicator-x { + opacity: 1; + } + /* Character indicator — hide name text below 768px, icon always visible */ + @media (max-width: 768px) { + #character-indicator-name { display: none; } + } + + /* Document indicator — hidden by default, shown when docs exist */ + #doc-indicator-btn { display: none !important; } + #doc-indicator-btn.visible { display: inline-flex !important; } + /* On mobile, the minimized-dock chip replaces this indicator — keep + it hidden regardless of `.visible`. */ + @media (max-width: 768px) { + #doc-indicator-btn, + #doc-indicator-btn.visible { display: none !important; } + } + .doc-indicator-active { + color: var(--accent, var(--accent-primary, var(--red))) !important; + opacity: 1 !important; + } + #doc-indicator-btn.active { + color: var(--accent, var(--accent-primary, var(--red))) !important; + opacity: 1 !important; + background: color-mix(in srgb, var(--fg) 9%, transparent) !important; + } + #overflow-doc-btn.has-docs, + #overflow-doc-btn.active { + color: var(--accent, var(--red)); + opacity: 1; + } + #overflow-doc-btn.has-docs.active { + background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent); + } + .send-btn { + /* Prefer the theme accent (--red). A stored custom `--send-btn-bg` + override only kicks in if it's set to a non-white value — guards + against ChatGPT-style themes where the stored override leaked + to white and rendered the send button invisible. */ + background: var(--send-btn-bg, var(--red)); + color: #fff; + border: none; + border-radius: 8px; + min-width: 32px; + width: 32px; + height: 32px !important; + padding: 0; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.25s, border-color 0.25s, color 0.25s, width 0.3s cubic-bezier(0.34, 1, 0.64, 1), padding 0.3s, border-radius 0.3s, opacity 0.1s; + flex-shrink: 0; + overflow: hidden; + } + /* Instant feedback while the send handler is spinning up before streaming begins */ + .send-btn.send-pending { + opacity: 0.55; + pointer-events: none; + animation: send-pending-pulse 0.9s ease-in-out infinite; + } + @keyframes send-pending-pulse { + 0%, 100% { opacity: 0.55; } + 50% { opacity: 0.85; } + } + /* Send button icon transitions — send mode forces no animation */ + .send-btn[data-mode="send"] svg { animation: none !important; } + /* Spin out: + rotates away, then arrow spins in via anim-spin */ + .send-btn.anim-spin-swap svg { animation: btn-spin-out 0.15s ease-in forwards; } + .send-btn.anim-spin-swap .send-btn-label { opacity: 0; transition: opacity 0.1s; } + @keyframes btn-spin-out { + 0% { transform: rotate(0deg) scale(1); opacity: 1; } + 100% { transform: rotate(90deg) scale(0); opacity: 0; } + } + .send-btn.anim-spin svg { animation: btn-spin-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } + .send-btn.anim-launch svg { animation: btn-launch 0.3s cubic-bezier(0.6, 0, 0.4, 1) forwards; } + .send-btn.anim-land svg { animation: btn-land 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); } + @keyframes btn-spin-in { + 0% { transform: rotate(-180deg) scale(0); opacity: 0; } + 100% { transform: rotate(0deg) scale(1); opacity: 1; } + } + /* Arrow flies UP and OUT of the button before the stop icon lands in. + Stays opaque most of the way so it reads as a real launch instead of a + fade-in-place. .anim-launch temporarily lifts the button's overflow so + the icon escapes the 32px box. */ + @keyframes btn-launch { + 0% { transform: translateY(0) scale(1); opacity: 1; } + 70% { transform: translateY(-30px) scale(0.95); opacity: 1; } + 100% { transform: translateY(-58px) scale(0.55); opacity: 0; } + } + .send-btn.anim-launch { overflow: visible !important; } + .send-btn.anim-launch svg { transform-origin: 50% 50%; } + @keyframes btn-land { + 0% { transform: translateY(10px) scale(0.3); opacity: 0; } + 100% { transform: translateY(0) scale(1); opacity: 1; } + } + /* Stop button — Processing: Siren pulse, Receiving: Quarter turn spin */ + .send-btn[data-mode="streaming"][data-phase="processing"] svg { + animation: siren-icon 1.5s ease-in-out infinite; + } + .send-btn[data-mode="streaming"][data-phase="receiving"] svg { + animation: quarter-turn 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; + } + @keyframes siren-icon { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(0.8); } + } + @keyframes quarter-turn { + 0%, 15% { transform: rotate(0deg); } + 25%, 40% { transform: rotate(90deg); } + 50%, 65% { transform: rotate(180deg); } + 75%, 90% { transform: rotate(270deg); } + 100% { transform: rotate(360deg); } + } + .send-btn:hover { + background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white)); + color: #fff; + } + .send-btn.mic-mode, + .send-btn.newchat-mode { + /* Idle / new-chat / mic states blend the picked Send Btn color into + the panel so the user's color choice is visible across every state, + not only briefly while typing. */ + background: color-mix(in srgb, var(--send-btn-bg, var(--red)) 30%, var(--panel, #2a2a2a)); + color: var(--fg); + border: 1px solid var(--border); + transition: width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s, border-color 0.25s, color 0.25s, border-radius 0.3s; + } + .send-btn.mic-mode:hover, + .send-btn.newchat-mode:hover { + background: color-mix(in srgb, var(--send-btn-hover, var(--send-btn-bg, var(--red))) 85%, var(--panel, #2a2a2a)); + color: #fff; + } + /* Hover: just spin the + 90° (no size change). The "New chat" affordance + is the tooltip (title="New chat") plus the icon rotation. + Gated on data-mode="newchat" so the arrow variant (empty-session state + which keeps the newchat-mode class but shows the send arrow) does NOT + rotate on hover. */ + .send-btn[data-mode="newchat"] svg { + transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); + } + .send-btn[data-mode="newchat"]:hover svg { + transform: rotate(90deg); + } + /* "New Session" expanding label */ + .send-btn-label { + width: 0; + max-width: 0; + overflow: hidden; + white-space: nowrap; + font-size: 11px; + font-weight: 500; + letter-spacing: 0.3px; + opacity: 0; + padding: 0; + margin: 0; + transition: max-width 0.35s cubic-bezier(0.34, 1.2, 0.64, 1), width 0.35s, opacity 0.25s, margin-left 0.25s; + } + .send-btn.newchat-expanded .send-btn-label { + width: auto; + max-width: 50px; + opacity: 1; + margin-left: 4px; + } + .send-btn.newchat-expanded { + width: 68px; + padding: 0 10px 0 8px; + } + .send-btn.recording { + background: var(--red) !important; + color: #fff !important; + border: none !important; + animation: pulse-recording 1.5s infinite; + } + @keyframes pulse-recording { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + /* Mode toggle — Agent / Chat */ + .mode-toggle { + display: flex; + flex-shrink: 0; + height: 28px; + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + position: relative; + } + .mode-toggle::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: color-mix(in srgb, var(--fg) 10%, transparent); + border-radius: 9px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 0; + } + .mode-toggle.mode-chat::before, + .mode-toggle.mode-right::before { + transform: translateX(100%); + } + .mode-toggle.mode-toggle-three::before { + width: 33.3333%; + } + .mode-toggle.mode-toggle-three.mode-mid::before { + transform: translateX(100%); + } + .mode-toggle.mode-toggle-three.mode-third::before { + transform: translateX(200%); + } + #mode-agent-btn { + border-radius: 10px 0 0 10px; + } + #mode-chat-btn { + border-radius: 0 10px 10px 0; + } + .mode-toggle-btn { + background: none; + border: none; + color: color-mix(in srgb, var(--fg) 40%, transparent); + cursor: pointer; + padding: 0 10px; + font-size: 11px; + font-weight: 500; + font-family: inherit; + transition: color 0.2s; + white-space: nowrap; + height: 100%; + position: relative; + z-index: 1; + } + .mode-toggle-btn:not(.active):hover { + color: color-mix(in srgb, var(--fg) 60%, transparent); + } + .mode-toggle-btn:hover { + background: none !important; + border-color: transparent !important; + } + .mode-toggle-btn.active, + .mode-toggle-btn.active:hover, + .mode-toggle-btn.active:focus { + color: var(--fg) !important; + background: none !important; + border-color: transparent !important; + cursor: default; + } + .mode-toggle-btn + .mode-toggle-btn { + border-left: none; + } + @media (max-width: 768px) { + .mode-toggle.mode-toggle-three [data-llama-mode="unified"] > span { + font-size: 0; + } + .mode-toggle.mode-toggle-three [data-llama-mode="unified"] > span::after { + content: 'Unif'; + font-size: 11px; + } + } + /* Message count badge in the chat-meta header (next to the title). + Auto-hides when empty so a brand-new chat doesn't show "0 msgs". */ + .chat-meta-count { + font-size: inherit; + font-weight: 400; + color: color-mix(in srgb, var(--fg) 35%, transparent); + white-space: nowrap; + user-select: none; + margin-left: 6px; + } + .chat-meta-count:empty { display: none; } + /* Session cost indicator (next to chevron in header) */ + .session-cost-display { + font-size: inherit; + font-weight: 400; + color: color-mix(in srgb, var(--fg) 35%, transparent); + white-space: nowrap; + user-select: none; + margin-left: 7px; + margin-right: 5px; + } + .chat-context-pill { + display: inline-flex; + align-items: center; + gap: 3px; + height: auto; + padding: 0; + margin-left: 7px; + border: 0; + border-radius: 0; + background: transparent; + color: color-mix(in srgb, var(--fg) 35%, transparent); + font-family: inherit; + font-size: 0.7rem; + font-weight: 400; + line-height: 1; + cursor: pointer; + white-space: nowrap; + position: relative; + z-index: 2; + --ctx-stroke: var(--color-muted, #888); + transition: color 0.15s, opacity 0.15s; + } + .chat-context-pill[hidden] { + display: none !important; + } + .chat-context-pill:hover, + .chat-context-pill.open { + color: color-mix(in srgb, var(--fg) 70%, transparent); + --ctx-stroke: var(--ctx-color); + border: 0; + background: transparent; + } + .chat-context-pill.loading { + opacity: 0.55; + } + .chat-context-pill.warn { + color: var(--orange, #f59e0b); + background: transparent; + } + .chat-context-pill.danger { + color: var(--red, #ef4444); + background: transparent; + } + .chat-context-pill svg { + flex-shrink: 0; + } + .chat-context-pill .ctx-ring-pct { + color: currentColor; + font-size: inherit; + font-weight: inherit; + line-height: 1; + } + .chat-context-dot { + display: none; + } + .chat-context-popup { + position: fixed; + z-index: 10060; + width: 238px; + padding: 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--panel); + color: var(--fg); + box-shadow: 0 12px 34px color-mix(in srgb, #000 26%, transparent); + font-size: 11px; + line-height: 1.35; + } + .chat-context-popup.warn { + color: var(--orange, #f59e0b); + } + .chat-context-popup.danger { + color: var(--red, #ef4444); + } + .chat-context-popup-title { + font-weight: 650; + margin-bottom: 8px; + color: var(--accent, var(--red)); + } + .chat-context-popup-bar { + height: 6px; + overflow: hidden; + border-radius: 999px; + background: color-mix(in srgb, var(--fg) 10%, transparent); + margin-bottom: 6px; + } +.chat-context-popup-fill { + height: 100%; + width: 0; + border-radius: inherit; + background: var(--red, var(--fg)); +} + .chat-context-popup-row { + display: flex; + justify-content: space-between; + gap: 10px; + margin-top: 4px; + color: color-mix(in srgb, var(--fg) 70%, transparent); + } + .chat-context-popup-row span:last-child { + color: var(--fg); + text-align: right; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 148px; + } + .chat-context-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid color-mix(in srgb, var(--border) 45%, transparent); + } + .chat-context-toggle-copy { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + color: color-mix(in srgb, var(--fg) 76%, transparent); + } + .chat-context-toggle-copy > span:first-child { + white-space: nowrap; + color: var(--accent, var(--red)); + } + .chat-context-toggle-state { + font-size: 10px; + color: color-mix(in srgb, var(--fg) 45%, transparent); + } + .chat-context-toggle { + position: relative; + flex: 0 0 auto; + width: 32px; + height: 18px; + border: 1px solid color-mix(in srgb, var(--border) 75%, transparent); + border-radius: 999px; + background: color-mix(in srgb, var(--fg) 8%, transparent); + cursor: pointer; + transition: background 0.15s, border-color 0.15s, opacity 0.15s; + } + .chat-context-toggle::after { + content: ""; + position: absolute; + top: 3px; + left: 3px; + width: 10px; + height: 10px; + border-radius: 50%; + background: color-mix(in srgb, var(--fg) 62%, transparent); + transition: transform 0.15s, background 0.15s; + } + .chat-context-toggle.active { + border-color: color-mix(in srgb, var(--accent) 55%, transparent); + background: color-mix(in srgb, var(--accent) 18%, transparent); + } + .chat-context-toggle.active::after { + transform: translateX(14px); + background: var(--fg); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 18%, transparent); + } + .chat-context-toggle:disabled { + opacity: 0.6; + cursor: default; + } + .chat-context-threshold-row { + margin-top: 8px; + } + .chat-context-threshold-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 5px; + color: color-mix(in srgb, var(--fg) 70%, transparent); + } + .chat-context-threshold-top span:last-child { + color: var(--fg); + font-variant-numeric: tabular-nums; + } + .chat-context-threshold-slider { + width: 100%; + margin: 0; + } + .chat-context-compact-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 1px; + width: 100%; + height: 26px; + margin-top: 9px; + border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent); + cursor: pointer; + font: inherit; + font-size: 11px; + font-weight: 600; + } + .chat-context-compact-btn svg { display: block; flex: 0 0 auto; margin-left: -2px; } + .chat-context-compact-btn svg circle:last-child { + stroke: var(--accent, var(--red)); + transition: stroke-dasharray 0.22s ease; + } + .chat-context-compact-btn:hover svg circle:last-child { + stroke-dasharray: 35 0; + } + .chat-context-compact-btn:hover { + background: color-mix(in srgb, var(--accent) 18%, transparent); + } + .chat-context-popup, + .chat-context-popup * { + font-size: 10px !important; + } + /* Model picker — input bar drop-up */ + .model-picker-wrap { + position: relative; + flex-shrink: 0; + } + .model-picker-btn { + display: inline-flex; + align-items: center; + gap: 4px; + height: 21px; + padding: 0 6px; + font-size: 11px; + font-weight: 500; + font-family: inherit; + background: none; + border: 1px solid transparent; + border-radius: 4px; + /* 65% mix lifts the model label above the WCAG AA 4.5:1 threshold + against the dark chat-bar (40% only reached ~2.9:1). */ + color: color-mix(in srgb, var(--fg) 65%, transparent); + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, color 0.15s, border-color 0.15s; + } + .model-picker-btn:hover { + border-color: var(--border); + background: color-mix(in srgb, var(--fg) 8%, transparent); + color: var(--fg); + } + .model-picker-btn #model-picker-label { + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + } + .model-picker-btn svg { + flex-shrink: 0; + opacity: 0.5; + } + .model-picker-logo { + display: inline-flex; + align-items: center; + vertical-align: -2px; + } + .model-picker-logo svg { + width: 12px; + height: 12px; + opacity: 0.7; + } + .model-picker-menu { + position: absolute; + bottom: calc(100% + 16px); + right: 0; + z-index: 250; + min-width: 260px; + max-width: 360px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 -4px 20px rgba(0,0,0,0.4); + padding: 6px; + animation: picker-roll-up 0.2s ease-out; + transform-origin: bottom right; + } + .model-picker-menu.closing { + animation: picker-roll-down 0.15s ease-in forwards; + } + .model-picker-menu.hidden { display: none; } + .model-picker-search-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 30px; + gap: 4px; + align-items: center; + margin-bottom: 4px; + transition: grid-template-columns 0.18s ease, gap 0.18s ease; + } + .model-picker-menu.no-models .model-picker-search-row { + margin-bottom: 0; + } + .model-picker-search-row.searching { + grid-template-columns: minmax(0, 1fr) 0px; + gap: 0; + } + .model-picker-search-row.searching .model-picker-action-btn { + opacity: 0; + transform: translateX(10px) scale(0.88); + pointer-events: none; + } + .model-picker-search-wrap { + position: relative; + min-width: 0; + } + .model-picker-menu input[type="text"] { + width: 100%; + box-sizing: border-box; + padding: 6px 30px 6px 8px; + font-size: 0.82em; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--bg); + color: var(--fg); + font-family: inherit; + outline: none; + min-width: 0; + transition: border-color 0.15s, padding 0.18s ease, background 0.18s ease; + } + .model-picker-menu input[type="text"]:focus { + border-color: var(--red); + } + .model-picker-menu input[type="text"]::placeholder { + color: color-mix(in srgb, var(--fg) 30%, transparent); + } + .model-picker-refresh-btn { + appearance: none; + position: absolute; + top: 50%; + right: 4px; + transform: translateY(-50%); + width: 22px; + height: 22px; + border: 0; + border-radius: 4px; + background: transparent; + color: color-mix(in srgb, var(--fg) 54%, transparent); + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + cursor: pointer; + } + .model-picker-refresh-btn:hover, + .model-picker-refresh-btn:focus-visible { + color: var(--fg); + background: color-mix(in srgb, var(--fg) 8%, transparent); + outline: none; + } + .model-picker-refresh-btn:disabled { + opacity: 0.7; + cursor: default; + } + .model-picker-refresh-btn.spinning svg { + animation: model-picker-refresh-spin 0.75s linear infinite; + } + .model-picker-action-btn { + appearance: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + border: 1px solid var(--border); + border-radius: 4px; + background: color-mix(in srgb, var(--fg) 4%, transparent); + color: color-mix(in srgb, var(--fg) 66%, transparent); + cursor: pointer; + padding: 0; + overflow: hidden; + transform: translateX(0) scale(1); + transition: opacity 0.16s ease, transform 0.18s ease, border-color 0.15s, color 0.15s, background 0.15s; + } + .model-picker-action-btn:hover, + .model-picker-action-btn:focus-visible { + border-color: var(--red); + color: var(--fg); + background: color-mix(in srgb, var(--red) 10%, var(--panel)); + outline: none; + } + .model-picker-action-btn.primary { + color: var(--red); + background: color-mix(in srgb, var(--red) 8%, transparent); + } + .model-picker-action-btn svg { + width: 14px; + height: 14px; + transition: transform 0.28s ease; + } + .model-picker-action-btn:hover svg, + .model-picker-action-btn:focus-visible svg { + transform: rotate(90deg); + } + .model-picker-list { + max-height: min(280px, 50dvh); + overflow-y: auto; + } + .model-picker-list.is-loading { + max-height: min(120px, 28dvh); + overflow: hidden; + } + .model-picker-list.is-empty { + max-height: 0; + overflow: hidden; + } + .model-picker-list .model-picker-loading-row { + justify-content: center; + gap: 7px; + min-height: 38px; + padding: 8px 10px; + color: color-mix(in srgb, var(--fg) 58%, transparent); + font-size: 0.82em; + } + .model-picker-list .model-picker-loading-row .ai-spinner-whirlpool { + position: relative; + top: 1px; + } + .model-picker-list .model-switch-item { + display: flex; + align-items: center; + padding: 5px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 0.82em; + color: var(--fg); + gap: 6px; + } + .model-picker-list .model-switch-item:hover { + background: color-mix(in srgb, var(--red) 8%, transparent); + } + .model-picker-list .model-switch-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 5px 8px; + color: color-mix(in srgb, var(--fg) 50%, transparent); + font-size: 0.82em; + } + .model-picker-list .mp-section-label { + font-size: 0.72em; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.4; + padding: 6px 8px 2px; + } + .model-picker-list .mp-section-label:first-child { + padding-top: 2px; + } + /* Model name takes the slack so the endpoint label + favorite dot sit on the right. */ + .model-picker-list .model-switch-item .mp-model-name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .model-picker-list .model-switch-item .model-switch-ep { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.9em; + opacity: 0.45; + } + /* Keyboard navigation highlight (Arrow keys in the search box). */ + .model-picker-list .model-switch-item.kb-active { + background: color-mix(in srgb, var(--red) 14%, transparent); + } + /* Inline favorite bookmark — always visible (works on touch), active when on. */ + .model-picker-list .mp-fav-dot { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 24px; + margin: -5px -8px -5px 8px; + padding: 0; + border: none; + background: transparent; + cursor: pointer; + color: color-mix(in srgb, var(--fg) 22%, transparent); + font-family: inherit; + font-size: 13px; + line-height: 1; + transition: color 0.15s ease, opacity 0.15s ease, transform 0.12s ease; + -webkit-tap-highlight-color: transparent; + } + .model-picker-list .mp-fav-dot:hover { + color: color-mix(in srgb, var(--fg) 68%, transparent); + } + .model-picker-list .mp-fav-dot:focus-visible { + outline: none; + color: color-mix(in srgb, var(--fg) 68%, transparent); + } + .model-picker-list .mp-fav-dot.active { + color: var(--accent, var(--red)); + opacity: 1; + } + .model-picker-list .mp-fav-dot svg { display: block; } + .model-picker-list .mp-fav-dot.active:hover { + color: var(--accent, var(--red)); + opacity: 0.72; + } + .model-picker-list .mp-fav-dot.pulse { + animation: mpFavPulse 0.32s ease-out; + } + @keyframes mpFavPulse { + 0% { text-shadow: 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 0%, transparent); } + 45% { text-shadow: 0 0 10px color-mix(in srgb, var(--accent, var(--red)) 60%, transparent); } + 100% { text-shadow: 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 0%, transparent); } + } + /* First-run hint when a large catalog has no Recent/Favorites yet. */ + .model-picker-list .mp-empty-hint { + flex-direction: column; + gap: 2px; + padding: 14px 8px; + text-align: center; + } + .model-picker-list .mp-empty-hint .mp-empty-title { + font-size: 1.05em; + color: color-mix(in srgb, var(--fg) 70%, transparent); + } + .model-picker-list .mp-empty-hint .mp-empty-sub { + font-size: 0.92em; + opacity: 0.7; + } + /* Provider group headers */ + .model-picker-list .mp-provider-header { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 8px; + cursor: pointer; + font-size: 0.78em; + font-weight: 500; + color: var(--fg); + border-radius: 4px; + user-select: none; + } + .model-picker-list .mp-provider-header:hover { + background: color-mix(in srgb, var(--fg) 6%, transparent); + } + .model-picker-list .mp-provider-chevron { + display: inline-flex; + opacity: 0.4; + transition: transform 0.2s, opacity 0.15s; + flex-shrink: 0; + } + .model-picker-list .mp-provider-header:hover .mp-provider-chevron { + opacity: 0.7; + } + .model-picker-list .mp-provider-chevron.collapsed { + transform: rotate(-90deg); + } + .model-picker-list .mp-provider-name { flex: 1; } + .model-picker-list .mp-provider-count { font-size: 0.85em; opacity: 0.4; } + /* Domino expand (15% faster than sidebar) */ + .mp-provider-group.mp-just-expanded .model-switch-item { + animation: mp-domino-in 0.31s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; + } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(1) { animation-delay: 0.035s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(2) { animation-delay: 0.07s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(3) { animation-delay: 0.105s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(4) { animation-delay: 0.14s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(5) { animation-delay: 0.175s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(6) { animation-delay: 0.21s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(7) { animation-delay: 0.245s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(8) { animation-delay: 0.28s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(9) { animation-delay: 0.315s; } + .mp-provider-group.mp-just-expanded .model-switch-item:nth-child(10) { animation-delay: 0.35s; } + @keyframes mp-domino-in { + 0% { opacity: 0; transform: translateY(6px) scale(0.94); } + 60% { opacity: 1; } + 100% { opacity: 1; transform: translateY(0) scale(1); } + } + /* Comfortable touch targets on phones / narrow screens. */ + @media (hover: none) and (pointer: coarse), (max-width: 768px) { + .model-picker-list .model-switch-item { + padding-top: 8px; + padding-bottom: 8px; + } + .model-picker-list .mp-fav-dot { + width: 30px; + height: 30px; + margin: -7px -8px -7px 8px; + } + } + /* Overflow "+" menu */ + .overflow-wrapper { + position: relative; + display: flex; + align-items: center; + } + .plus-active-dot { + position: absolute; + top: 4px; + right: 2px; + width: 6px; + height: 6px; + background: var(--fg); + border-radius: 50%; + display: none; + } + .overflow-plus-btn.has-active .plus-active-dot { + display: block; + } + .overflow-menu { + position: fixed; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 10px; + padding: 4px; + min-width: 170px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); + z-index: 1000; + /* Container spring-in: the rounded panel scales/grows out of the + chevron's position before the menu items domino in on top. */ + transform-origin: bottom left; + animation: overflow-menu-pop 0.08s ease-out; + } + @keyframes overflow-menu-pop { + 0% { transform: scale(0.96) translateY(2px); opacity: 0; } + 100% { transform: scale(1) translateY(0); opacity: 1; } + } + .overflow-menu.hidden { + display: none; + } + /* Closing state: JS adds `.closing` to play the fold-in animation, + waits for it to finish, then flips to `.hidden`. Container + scales/translates back into the chevron while items peel off from + the top down so the menu collapses into its anchor. */ + .overflow-menu.closing { + animation: overflow-menu-pop-out 0.08s ease-in forwards; + animation-delay: 0s; + } + @keyframes overflow-menu-pop-out { + 0% { transform: scale(1) translateY(0); opacity: 1; } + 100% { transform: scale(0.96) translateY(2px); opacity: 0; } + } + .overflow-menu-item { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 8px 12px; + background: none; + border: none; + color: var(--fg); + opacity: 0.7; + cursor: pointer; + border-radius: 6px; + font-size: 13px; + font-family: inherit; + transition: background 0.15s, opacity 0.15s, color 0.15s; + /* Domino-style cascade: each item slides up + fades in with a tiny + delay after the previous one (set via nth-last-child below so the + BOTTOM item appears first and the cascade rolls upward — visually + feels like the menu is "stacking up" from the chevron). The + container itself springs in via .overflow-menu's keyframe. */ + animation: overflow-item-in 0.08s ease-out backwards; + animation-delay: 0s !important; + } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(1) { animation-delay: 0.06s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(2) { animation-delay: 0.10s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(3) { animation-delay: 0.14s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(4) { animation-delay: 0.18s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(5) { animation-delay: 0.22s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(6) { animation-delay: 0.26s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(7) { animation-delay: 0.30s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(8) { animation-delay: 0.34s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(9) { animation-delay: 0.38s; } + .overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(10) { animation-delay: 0.42s; } + @keyframes overflow-item-in { + 0% { opacity: 0; transform: translateY(10px) translateX(-6px) scale(0.9); } + 60% { opacity: 1; } + 100% { opacity: 1; transform: translateY(0) translateX(0) scale(1); } + } + /* Fold-in: items peel off top-down (mirror of the open's bottom-up + cascade) so the menu visibly empties before the container scales + back into the chevron. */ + .overflow-menu.closing .overflow-menu-item { + animation: overflow-item-out 0.06s ease-in forwards; + animation-delay: 0s !important; + } + .overflow-menu.closing .overflow-menu-item:nth-child(1) { animation-delay: 0.00s; } + .overflow-menu.closing .overflow-menu-item:nth-child(2) { animation-delay: 0.02s; } + .overflow-menu.closing .overflow-menu-item:nth-child(3) { animation-delay: 0.04s; } + .overflow-menu.closing .overflow-menu-item:nth-child(4) { animation-delay: 0.06s; } + .overflow-menu.closing .overflow-menu-item:nth-child(5) { animation-delay: 0.08s; } + .overflow-menu.closing .overflow-menu-item:nth-child(6) { animation-delay: 0.10s; } + .overflow-menu.closing .overflow-menu-item:nth-child(7) { animation-delay: 0.12s; } + .overflow-menu.closing .overflow-menu-item:nth-child(8) { animation-delay: 0.14s; } + .overflow-menu.closing .overflow-menu-item:nth-child(9) { animation-delay: 0.16s; } + .overflow-menu.closing .overflow-menu-item:nth-child(10) { animation-delay: 0.18s; } + @keyframes overflow-item-out { + 0% { opacity: 1; transform: translateY(0) translateX(0) scale(1); } + 100% { opacity: 0; transform: translateY(6px) translateX(-3px) scale(0.92); } + } + .overflow-menu-item:hover { + opacity: 1; + background: color-mix(in srgb, var(--red) 10%, transparent); + } + #overflow-attach-btn { + position: relative; + font-weight: 600; + } + #overflow-attach-btn svg { + transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1); + } + .overflow-menu-item.active { + opacity: 1; + color: var(--fg); + } + .overflow-active-dot { + width: 6px; + height: 6px; + background: var(--fg); + border-radius: 50%; + margin-left: auto; + display: none; + flex-shrink: 0; + } + .overflow-menu-item.active .overflow-active-dot { + display: block; + } + .attach-strip { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin: 0 0 8px; + min-height: 0; + } + .attach-strip:empty { + display: none; + } + .attach-strip { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin: 6px 0 0; + min-height: 32px; + padding: 2px; + } + .attachment-placeholder { + display: flex; + align-items: center; + color: var(--fg); + opacity: 0.7; + font-style: italic; + padding: 4px 8px; + border-radius: 4px; + background: color-mix(in srgb, var(--fg) 6%, transparent); + } + .attach-btn { width:32px; display:grid; place-items:center; } + .hidden { display:none; } + .toggle { position:relative; display:inline-block; width:30px; height:16px; vertical-align:middle; } + .toggle input { opacity:0; width:0; height:0; } + .toggle .slider { + position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; + background:color-mix(in srgb, var(--fg) 15%, transparent); transition:background .08s; border-radius:8px; + } + .toggle .slider:before { + position:absolute; content:""; height:12px; width:12px; left:2px; top:2px; + background:var(--panel); border-radius:50%; transform:translateX(0); transition:transform .08s; + box-shadow:0 1px 2px rgba(0,0,0,0.25); + } + .toggle input:checked + .slider { background:var(--red); } + .toggle input:checked + .slider:before { transform:translateX(14px); } + .copy-btn { + position:absolute; top:6px; right:6px; + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + font-size:12px; height:24px; padding:0 8px; cursor:pointer; + opacity:0; transition:.15s; + } + .msg:hover .copy-btn { opacity:1; } + .role-timestamp { + font-size:0.7rem; color:var(--color-muted-alt); font-weight:normal; margin-left:6px; + } + .msg-footer { + display:flex; align-items:center; gap:6px; + flex-wrap: wrap; + margin-top:6px; + color:var(--color-muted-alt); font-size:0.75rem; + position: relative; + } + .msg-footer .timestamp { + font-size:0.75rem; color:var(--color-muted-alt); + margin:0; opacity:1; + } + .msg-footer .response-metrics { + font-size:0.75rem; color:var(--color-muted-alt); + transition: color .15s; + } + .msg-footer .response-metrics:hover { color:var(--fg); } + .agent-thread > .agent-thread-footer { + margin-top: 4px; + margin-left: 2px; + } + /* Keep the context ring and percentage together at the footer's left edge. */ + .ctx-ring { + display: inline-flex; + align-items: center; + gap: 3px; + margin-left: 0; + line-height: 0; + opacity: 0.6; + cursor: default; + transition: opacity 0.15s; + --ctx-stroke: var(--color-muted, #888); + } + .ctx-ring .ctx-ring-pct { + color: var(--color-muted, #888); + transition: color 0.1s ease; + } + .ctx-ring svg circle { + transition: stroke 0.1s ease; + } + .ctx-ring:hover { + opacity: 1; + --ctx-stroke: var(--ctx-color); + } + .ctx-ring:hover .ctx-ring-pct { + color: var(--ctx-color); + } + .ctx-ring-pct { + font-size: 0.7rem; + font-weight: 600; + line-height: 1; + } + /* Context detail popup */ + .ctx-detail-popup { + position: fixed; + z-index: 200; + background: var(--panel, var(--bg)); + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px 14px; + box-shadow: 0 4px 20px rgba(0,0,0,0.4); + min-width: 220px; + max-width: 280px; + font-size: 0.85rem; + color: var(--fg); + } + .ctx-bar-wrap { + width: 100%; + height: 6px; + background: var(--border); + border-radius: 4px; + overflow: hidden; + } + .ctx-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s; + } + .ctx-compact-btn { + display: block; + width: 100%; + margin-top: 10px; + padding: 6px 0; + background: none; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 0.8rem; + cursor: pointer; + transition: border-color 0.15s, color 0.15s; + } + .ctx-compact-btn:hover { + border-color: var(--accent, var(--red)); + color: var(--accent, var(--red)); + } + .ctx-compact-btn:disabled { + opacity: 0.5; + cursor: default; + } + .compact-wave { + display: inline-block; + color: var(--accent, var(--red)); + letter-spacing: 1px; + font-size: 0.9em; + } + /* Memory-used indicator pill */ + .memory-used-pill { + display: inline-flex; + align-items: center; + background: var(--panel); + border: 1px solid var(--border); + color: var(--fg); + font-size: 0.7rem; + padding: 2px 8px; + border-radius: 10px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s, background 0.15s; + white-space: nowrap; + position: relative; + z-index: 1; + } + .memory-used-pill:hover { opacity: 1; background: var(--border); } + /* Nudge label text 1px down so it visually centers with the icon. */ + .memory-used-pill-text { position: relative; top: 1px; display: inline-block; } + .memory-used-detail { + position: fixed; + background: var(--panel); + border: 1px solid var(--border); + border-radius: 8px; + padding: 6px 8px; + min-width: 200px; + max-width: min(360px, calc(100vw - 16px)); + max-height: 50vh; + overflow-y: auto; + z-index: 300; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + font-size: 0.75rem; + } + .memory-used-row { + display: flex; + align-items: flex-start; + gap: 6px; + padding: 3px 0; + line-height: 1.3; + } + .memory-used-row + .memory-used-row { + border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + } + .memory-used-badge { + flex-shrink: 0; + font-size: 0.7rem; + width: 16px; + text-align: center; + } + .memory-used-badge.pinned { color: var(--red); } + .memory-used-badge.recalled { color: var(--fg); opacity: 0.5; } + .memory-used-text { + color: var(--fg); + opacity: 0.85; + } + + .msg-actions { + display:inline-flex; align-items:center; gap:4px; + } + .footer-copy-btn { + background:none; border:none; + color:var(--color-muted-alt); cursor:pointer; + padding:2px 6px; border-radius:4px; + transition: color .15s; + line-height:1; + display: inline-flex; align-items: center; justify-content: center; + } + .footer-copy-btn:hover { color:var(--accent); } + .footer-open-gallery-btn { + gap: 4px; + font-size: 11px; + } + /* Delete action — same chrome as copy/download/edit but hover reveals + the destructive red tint so the user can tell it's not a benign op. */ + .footer-delete-btn:hover { color: var(--red); } + .regen-btn { + background:none; border:none; + color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer; + padding:2px 6px; border-radius:4px; + transition: color .15s; + line-height:1; + } + .regen-btn:hover { color:var(--accent); } + .fork-btn { + background:none; border:none; + color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer; + padding:2px 6px; border-radius:4px; + transition: color .15s; + line-height:1; + } + .fork-btn:hover { color:var(--accent); } + .msg-action-btn { + background:none; border:none; + color:var(--muted, var(--color-muted-alt)); font-size:1.1rem; cursor:pointer; + padding:2px 6px; border-radius:4px; + transition: color .15s; + line-height:1; + } + .msg-action-btn:hover { color:var(--accent); } + .msg-action-btn .resend-message-icon { + display:block; + width:13px; + height:13px; + transform:translateY(1px); + } + .msg-action-btn[data-action="shorten"] { position:relative; top:1px; font-size:1.25rem; } + .msg-delete-btn { position:relative; top:1px; } + .msg-delete-btn:hover { color:var(--red); } + .msg-more-btn { + font-size:0.7rem; letter-spacing:0.5px; + } + .msg-overflow-menu { + position:fixed; + background:var(--panel); + border:1px solid var(--border); + border-radius:6px; + padding:4px; + box-shadow:0 4px 16px rgba(0,0,0,0.4); + z-index:100; + min-width:150px; + } + .msg-overflow-item { + display:flex; + align-items:center; + gap:6px; + width:100%; + background:none; + border:none; + border-bottom:1px solid color-mix(in srgb, var(--border) 40%, transparent); + color:var(--fg); + font-size:0.8rem; + padding:5px 8px; + border-radius:4px; + cursor:pointer; + text-align:left; + font-family:inherit; + transition:background .1s; + } + .msg-overflow-item:last-child { border-bottom:none; } + .msg-overflow-item:hover { + background:color-mix(in srgb, var(--red) 8%, transparent); + } + .overflow-icon { + width:16px; + text-align:center; + flex-shrink:0; + font-size:1rem; + } + .overflow-icon .resend-message-icon { + display:block; + width:13px; + height:13px; + margin:0 auto; + } + .variant-nav { + display:inline-flex; + align-items:center; + gap:0px; + margin-left:auto; + font-size:0.85em; + font-family:inherit; + opacity:0.6; + } + .variant-divider { + opacity:0.25; + margin:0 4px 0 2px; + } + .variant-tag { + font-size:1.1em; + opacity:0.8; + margin-right:2px; + position:relative; + top:-1px; + } + .variant-tag-scissors { + top:-1px; + } + /* The ✂ "Rewrite shorter" action button glyph sits a touch low against the + other footer icons — nudge it up 2px. */ + .msg-action-btn[data-action="shorten"] { + position: relative; + top: -2px; + } + /* The "?" Explain-simpler glyph also sits slightly low — nudge it up 1px. */ + .msg-action-btn[data-action="explain"] { + position: relative; + top: -1px; + } + .variant-btn { + background:none; + border:none; + color:var(--fg); + cursor:pointer; + padding:4px 6px; + font-size:1em; + font-family:inherit; + opacity:0.7; + line-height:1; + } + .variant-btn:hover { opacity:1; color:var(--fg); } + .variant-btn:disabled { opacity:0.3; cursor:default; } + .variant-num { + background:none; + border:none; + color:var(--fg); + cursor:pointer; + padding:4px 4px; + font-size:1em; + font-family:inherit; + opacity:0.7; + line-height:1; + } + .variant-num:hover { opacity:1; color:var(--fg); } + .variant-num:disabled { opacity:0.5; cursor:default; } + .variant-slash { + opacity:0.4; + font-size:1em; + } + .continue-separator { + display:inline; + opacity:0.35; + font-size:0.85em; + font-style:italic; + } + .stopped-indicator { + color:var(--red); + margin-top:8px; + font-size:0.85em; + opacity:0.8; + display:flex; + align-items:center; + gap:8px; + flex-wrap:wrap; + } + + /* Message edit UI */ + .msg-edit-textarea { + background:var(--bg); + color:var(--fg); + border:1px solid var(--border); + border-radius:6px; + padding:10px; + font-family:inherit; + font-size:inherit; + line-height:1.6; + resize:vertical; + box-sizing:border-box; + } + .msg-edit-textarea:focus { outline:1px solid var(--hl-function, #61afef); border-color:var(--hl-function, #61afef); } + .msg-edit-bar { + display:flex; + gap:8px; + margin-top:6px; + } + .msg-edit-save, .msg-edit-cancel { + background:var(--bg); + color:var(--fg); + border:1px solid var(--border); + border-radius:6px; + padding:4px 14px; + cursor:pointer; + font-size:0.85em; + } + .msg-edit-save:hover { border-color:var(--color-save-green, #4caf50); color:var(--color-save-green, #4caf50); } + .msg-edit-cancel:hover { border-color:var(--red); color:var(--red); } + + /* Edited indicator — similar to stopped-indicator */ + .edited-indicator { + color:var(--fg); + margin-top:8px; + font-size:0.85em; + opacity:0.4; + font-style:italic; + } + .continue-btn { + background:none; + border:none; + color:var(--fg); + opacity:0.5; + cursor:pointer; + font-size:2.6em; + padding:2px 2px 0; + line-height:1; + } + .continue-btn:hover { + opacity:0.8; + } + .continue-btn.resume-btn { + display:inline-flex; + align-items:center; + justify-content:space-between; + gap:8px; + padding:3px 8px 3px 5px; + border:none; + border-radius:5px; + font-size:0.85em; + font-weight:400; + line-height:1.2; + position:relative; + top:2px; + } + .continue-btn.resume-btn:hover { + border:none; + } + .continue-btn.resume-btn .resume-btn-icon { + display:block; + width:16px; + height:16px; + flex:0 0 16px; + margin-left:auto; + } + + /* Round-cap "Continue" affordance — a cohesive centered pill at the chat + bottom (not the bare red in-message stopped style). */ + .rounds-exhausted { + justify-content:center; + gap:12px; + width:fit-content; + max-width:90%; + margin:14px auto 4px; + padding:7px 8px 7px 16px; + border:1px solid var(--border); + border-radius:999px; + background:color-mix(in srgb, var(--fg) 4%, transparent); + opacity:1; + } + .rounds-exhausted .rounds-exhausted-label { + color:color-mix(in srgb, var(--fg) 60%, transparent); + font-size:0.95em; + } + .rounds-exhausted .continue-btn { + font-size:0.9em; + font-weight:600; + opacity:1; + color:var(--bg); + background:var(--accent, var(--red)); + border-radius:999px; + padding:4px 14px; + line-height:1.3; + } + .rounds-exhausted .continue-btn:hover { + opacity:0.88; + } + .ctx-indicator { + display:inline-flex; align-items:center; gap:1px; + font-size:0.75rem; + } + .ctx-popup { + position:fixed; + z-index:250; + background:var(--panel); + border:1px solid var(--border); + border-radius:8px; + padding:11px 13px 12px; + font-size:0.78rem; + color:var(--fg); + box-shadow:0 8px 24px rgba(0,0,0,0.4); + min-width:250px; + max-width:min(360px, calc(100vw - 16px)); + line-height:1.45; + } + .ctx-label { + display:block; + width:auto; + color:var(--color-muted-alt); + font-size:0.75rem; + } + .ctx-popup-title { + display:flex; + align-items:center; + gap:6px; + margin-bottom:8px; + padding-bottom:7px; + border-bottom:1px solid color-mix(in srgb, var(--border) 65%, transparent); + font-weight:650; + letter-spacing:.01em; + } + .ctx-popup-title::before { + content:''; + width:6px; + height:6px; + flex:0 0 6px; + border-radius:50%; + background:var(--accent, var(--red)); + box-shadow:0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); + } + .ctx-stat-section { + display:flex; + flex-direction:column; + gap:4px; + } + .ctx-stat-section + .ctx-stat-section { + margin-top:9px; + padding-top:8px; + border-top:1px solid color-mix(in srgb, var(--border) 45%, transparent); + } + .ctx-stat-row { + display:grid; + grid-template-columns:minmax(0, 1fr) auto; + align-items:baseline; + gap:14px; + min-height:18px; + } + .ctx-stat-row .ctx-label { + white-space:nowrap; + } + .ctx-stat-value { + color:var(--fg); + text-align:right; + font-variant-numeric:tabular-nums; + white-space:nowrap; + } + .edit-btn { + background:none; border:none; + color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer; + padding:2px 6px; border-radius:4px; + transition: color .15s; + line-height:1; + } + .edit-btn:hover { color:var(--accent); } + .edit-textarea { + width:100%; background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + padding:8px; font-family:inherit; font-size:0.95rem; + resize:vertical; outline:none; + min-height:80px; + } + .edit-textarea:focus { border-color:var(--red); } + .edit-save-btn, .edit-cancel-btn { + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + padding:4px 12px; cursor:pointer; font-size:0.8rem; + display:inline-flex; align-items:center; justify-content:center; gap:5px; + } + .edit-save-btn:hover { background:var(--panel); } + .edit-cancel-btn:hover { background:var(--panel); } + pre .copy-code { + position:absolute; right:6px; + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + width:28px; height:28px; padding:0; cursor:pointer; + opacity:0; transition: opacity .15s, color .15s, border-color .15s; + display:flex; align-items:center; justify-content:center; + } + pre .copy-code { top:6px; } + pre .copy-code.bottom { top:auto; bottom:6px; } + pre:hover .copy-code { opacity:0.7; } + pre .copy-code:hover { opacity:1; } + pre .copy-code.copied { + opacity: 1; + color: var(--color-save-green, #4caf50); + border-color: var(--color-save-green, #4caf50); + background: color-mix(in srgb, var(--color-save-green, #4caf50) 18%, var(--bg)); + animation: code-copy-pulse 0.36s cubic-bezier(0.34, 1.56, 0.64, 1); + } + @keyframes code-copy-pulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.15); } + 100% { transform: scale(1); } + } + /* Slim text-button variant: swap "Copy" → "✓ Copied" via the + ::before content while still inheriting the green flash + pulse. */ + pre.pre-compact .copy-code.copied::before { content: '✓ Copied'; } + + /* Edit code button — positioned left of copy button */ + pre .edit-code { + position:absolute; right:42px; top:6px; + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + width:28px; height:28px; padding:0; cursor:pointer; + opacity:0; transition: opacity .15s, color .15s, border-color .15s; + display:flex; align-items:center; justify-content:center; + } + pre .edit-code.bottom { top:auto; bottom:6px; } + pre:hover .edit-code { opacity:0.7; } + pre .edit-code:hover { opacity:1; } + /* When the edit-code button is in "save" mode (checkmark), use the + theme accent so it matches the EDITING outline + label that are + also accent-coloured — clearer that this is the confirm action. */ + pre .edit-code.active { + opacity: 1; + color: var(--accent-primary, var(--red)); + border-color: var(--accent-primary, var(--red)); + background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, var(--bg)); + } + pre .use-code { + position:absolute; right:42px; top:6px; + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + width:28px; height:28px; padding:0; cursor:pointer; + opacity:0; transition: opacity .15s, color .15s, border-color .15s; + display:flex; align-items:center; justify-content:center; + } + pre .use-code.bottom { top:auto; bottom:6px; } + pre:hover .use-code { opacity:0.7; } + pre .use-code:hover { opacity:1; } + pre .use-code.used { + opacity: 1; + color: var(--color-save-green, #4caf50); + border-color: var(--color-save-green, #4caf50); + background: color-mix(in srgb, var(--color-save-green, #4caf50) 18%, var(--bg)); + animation: code-copy-pulse 0.36s cubic-bezier(0.34, 1.56, 0.64, 1); + } + .setup-trigger-link, .setup-clickable-provider, .setup-clickable-code { + transition: color 0.15s ease, opacity 0.15s ease; + } + .setup-trigger-link:hover, + .setup-clickable-provider:hover, + .setup-clickable-code:hover { + color: var(--accent, var(--red)) !important; + opacity: 0.9; + } + + /* Tapping the code body (not a button) toggles the overlay buttons off so + they stop covering the text on touch screens. Tap again to bring back. */ + pre.buttons-hidden .copy-code, + pre.buttons-hidden .edit-code, + pre.buttons-hidden .run-code { opacity:0 !important; pointer-events:none !important; } + + /* Editing state — subtle border on the code block */ + /* Editing state: was a 1px subtle outline that was almost invisible on + mobile, so users couldn't tell their tap-to-edit had actually engaged. + Use the accent colour + a tinted background so it reads at a glance. */ + pre.editing { + outline: 2px solid var(--accent-primary, var(--red)); + outline-offset: -2px; + background: color-mix(in srgb, var(--accent-primary, var(--red)) 6%, var(--bg)) !important; + } + pre.editing code.editing { outline:none; cursor:text; } + pre.editing::before { + content: 'EDITING'; + position: absolute; top: 0; left: 0; + padding: 2px 8px; + font-size: 9px; font-weight: 700; letter-spacing: 0.5px; + background: var(--accent-primary, var(--red)); + color: #fff; + border-radius: 0 0 4px 0; + z-index: 2; + pointer-events: none; + } + + /* Run code button — positioned left of edit button */ + pre .run-code { + position:absolute; right:78px; top:6px; + background:var(--bg); color:var(--fg); + border:1px solid var(--border); border-radius:6px; + width:28px; height:28px; padding:0; cursor:pointer; + opacity:0; transition: opacity .15s, color .15s, border-color .15s; + display:flex; align-items:center; justify-content:center; + } + pre .run-code.bottom { top:auto; bottom:6px; } + pre:hover .run-code { opacity:0.7; } + pre .run-code:hover { opacity:1; color:var(--hl-function, #61afef); border-color:var(--hl-function, #61afef); } + + /* Compact (single-line) code blocks: slim buttons so the row doesn't + double the height of a 1-line bash. Copy is text ("Copy" → "✓ Copied"), + Run and Edit keep their icons but at smaller sizes. Edit swaps to + a "Save" text label when its .active state is on. */ + pre.pre-compact { padding-right: 200px; min-height: 0; } + pre.pre-compact .copy-code, + pre.pre-compact .edit-code, + pre.pre-compact .run-code { + height: 20px; + padding: 0; + font-size: 10px; + font-weight: 500; + line-height: 20px; + top: 3px; + } + /* Copy: text-only, hide SVG */ + pre.pre-compact .copy-code { + width: auto; + padding: 0 8px; + right: 6px; + gap: 0; + } + pre.pre-compact .copy-code svg { display: none; } + pre.pre-compact .copy-code::before { content: 'Copy'; } + pre.pre-compact .copy-code.copied::before { content: '✓ Copied'; } + /* Edit: icon + "Edit" label, swap to "Save" when editing */ + pre.pre-compact .edit-code { + width: auto; + padding: 0 8px 0 6px; + gap: 3px; + right: 64px; + } + pre.pre-compact .edit-code svg { width: 12px; height: 12px; } + pre.pre-compact .edit-code::after { content: 'Edit'; } + pre.pre-compact .edit-code.active::after { content: 'Save'; } + /* Run: icon + "Run" label */ + pre.pre-compact .run-code { + width: auto; + padding: 0 8px 0 6px; + gap: 3px; + right: 126px; + } + pre.pre-compact .run-code svg { width: 12px; height: 12px; } + pre.pre-compact .run-code::after { content: 'Run'; } + /* Bottom-positioned slim buttons (when pre is near the top of the + viewport, the existing JS toggles .bottom to flip them down). */ + pre.pre-compact .copy-code.bottom, + pre.pre-compact .edit-code.bottom, + pre.pre-compact .run-code.bottom { top: auto; bottom: 3px; } + + /* Touch devices: no hover, so always show copy/run/edit buttons */ + @media (hover: none) { + pre .copy-code { opacity:0.7; } + pre .edit-code { opacity:0.7; } + pre .run-code { opacity:0.7; } + } + + /* Code runner output panel */ + .code-runner-output { + position:relative; + border:1px solid var(--border); border-top:2px solid var(--hl-function, #61afef); + border-radius:0 0 4px 4px; + background:var(--bg); + margin:-4px 0 8px 0; + padding:8px 12px; + max-height:400px; + overflow:auto; + } + + /* Chat-native visual explanations render in an isolated preview. */ + .chat-svg-visual { + width: 100%; + max-width: 760px; + margin: 12px 0 16px; + } + .chat-svg-preview { + display: block; + width: 100%; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + } + .code-runner-pre { + margin:0; padding:0; + font-family:'Fira Code', 'Courier New', monospace; + font-size:0.9em; line-height:1.5; + white-space:pre-wrap; word-break:break-word; + color:var(--fg); + background:none !important; + border:none !important; + } + .code-runner-error { color:var(--red); } + .code-runner-loading { font-style:italic; color:var(--red); padding:4px 0; } + .code-runner-close { + position:absolute; top:4px; right:4px; + background:none; border:none; color:var(--fg); + cursor:pointer; opacity:0.5; font-size:14px; padding:2px 6px; + } + .code-runner-close:hover { opacity:1; } + /* Labeled copy pill — pinned top-right INSIDE the run-output panel, not + in a separate footer. Panel is position:relative so absolute works. */ + .code-runner-copy-inline { + position: absolute; top: 6px; right: 32px; /* sits LEFT of the X close (top:4 right:4 ~24px wide) */ + z-index: 2; + background: var(--panel); color: var(--fg); + border: 1px solid var(--border); border-radius: 6px; + padding: 3px 10px; font-size: 11px; cursor: pointer; + display: inline-flex; align-items: center; + transition: border-color 0.15s, color 0.15s, background 0.15s; + } + .code-runner-copy-inline:hover { + border-color: var(--accent-primary, var(--red)); + color: var(--accent-primary, var(--red)); + } + /* Reserve room on the right so the output text doesn't slide under + either the Copy pill or the Close X. Applies to both the chat run + panel AND the document panel (doc-run-output reuses the same children). */ + .code-runner-output, + .doc-run-output { padding-right: 110px; } + + .toast { + position:fixed; + top: 16px; right: 16px; + left: auto; bottom: auto; + background:var(--panel); color:var(--fg); + border:1px solid color-mix(in srgb, var(--accent) 30%, transparent); + border-left: 3px solid var(--accent); + padding:9px 9px 9px 10px; border-radius:8px; font-size:12px; opacity:0; + /* Off-screen to the right by default; .show slides to 0; + removing .show transitions to -120% (off-screen left). */ + transform: translateX(120%); + transition: opacity .35s cubic-bezier(0.22, 1, 0.36, 1), + transform .45s cubic-bezier(0.22, 1, 0.36, 1); + /* Toasts must stay above the chat-context settings popup so a setting + change is visible while that popup remains open. */ + z-index: 10070; pointer-events: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + backdrop-filter: blur(12px); + max-width: min(420px, calc(100vw - 32px)); + min-width: min(240px, calc(100vw - 32px)); + min-height: 40px; + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: start; + column-gap: 8px; + box-sizing: border-box; + } + .toast.show { opacity:1; transform: translateX(0); } + .toast:not(:has(.toast-checkmark, .toast-whirlpool, .toast-error-icon)) { + grid-template-columns: minmax(0, 1fr) auto; + } + .toast-message { + min-width: 0; + padding: 3px 0 2px; + position: relative; + top: 0; + line-height: 1.35; + overflow-wrap: anywhere; + } + .toast.toast-ai-reply-progress .toast-message { + top: 2px; + } + .toast.toast-ai-reply-result .toast-message { + top: 0; + } + .toast.toast-sending .toast-message { + top: 2px; + } + .toast.toast-message-sent .toast-message { + top: 4px; + } + .toast-actions { + display: inline-flex; + align-items: center; + align-self: start; + gap: 3px; + margin-left: 2px; + } + .toast-action-btn { + min-height: 26px; + padding: 3px 7px 3px 9px; + border: 1px solid color-mix(in srgb, var(--fg) 34%, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--fg) 4%, transparent); + color: var(--fg); + cursor: pointer; + font: inherit; + font-weight: 600; + line-height: 1; + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 5px; + transition: border-color 0.15s ease, background 0.15s ease; + } + .toast-action-btn:hover { + border-color: color-mix(in srgb, var(--accent) 65%, var(--border)); + background: color-mix(in srgb, var(--accent) 10%, transparent); + } + .toast-action-icon { display: inline-flex; align-items: center; } + .toast-action-hint { + padding: 2px 4px; + border: 0; + border-radius: 3px; + background: color-mix(in srgb, var(--fg) 9%, transparent); + color: inherit; + font: 500 8px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + letter-spacing: 0.2px; + opacity: 0.62; + text-transform: uppercase; + pointer-events: none; + } + .toast .toast-checkmark { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-top: 5px; + margin-right: 0; + position: relative; + top: 4px; + color: var(--accent, var(--red)); + vertical-align: -3px; + transform: scale(0.65); + opacity: 0; + animation: toastCheckPop 360ms cubic-bezier(0.2, 0.9, 0.25, 1.25) forwards; + } + .toast .toast-checkmark svg polyline { + stroke-dasharray: 24; + stroke-dashoffset: 24; + animation: toastCheckDraw 420ms ease-out 120ms forwards; + } + .toast .toast-whirlpool { + position: relative; + top: 4px; + } + @keyframes toastCheckPop { + 0% { opacity: 0; transform: scale(0.65); } + 65% { opacity: 1; transform: scale(1.16); } + 100% { opacity: 1; transform: scale(1); } + } + @keyframes toastCheckDraw { + to { stroke-dashoffset: 0; } + } + .toast-close-btn { + margin-left: 0; + padding: 0; + width: 22px; + height: 22px; + line-height: 1; + border: none; + background: none; + color: var(--fg); + opacity: 0.5; + cursor: pointer; + font-size: 16px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + pointer-events: auto; + flex-shrink: 0; + transition: transform 0.22s ease, opacity 0.15s ease, background 0.15s ease; + } + .toast-error-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 17px; + height: 17px; + margin-top: 4px; + border: 1px solid currentColor; + border-radius: 50%; + font-size: 10px; + font-weight: 800; + line-height: 1; + } + .toast-close-btn:hover { + opacity: 1; + transform: rotate(90deg); + background: color-mix(in srgb, var(--fg) 8%, transparent); + } + .toast.exiting { + opacity: 0; + transform: translateX(-120%); + } + .toast.error { + border-color: color-mix(in srgb, var(--color-error) 40%, transparent); + border-left-color: var(--color-error); + color: var(--color-error); + } + /* When the notes panel is docked to the right, the toast enters from the + panel side. ui.js supplies the exact right edge and available width. */ + body:has(.notes-pane.modal-right-docked) .toast { + transform: translateX(-120%); + } + body:has(.notes-pane.modal-right-docked) .toast.show { transform: translateX(0); } + body:has(.notes-pane.modal-right-docked) .toast.exiting { transform: translateX(120%); } + /* Notes-only panel-aware placement. Documents and Email retain the normal + top-right toast position. */ + .toast.toast-panel-offset { transform: translateX(120%); } + .toast.toast-panel-offset.show { transform: translateX(0); } + .toast.toast-panel-offset.exiting { transform: translateX(-120%); } + @media (max-width: 768px) { + .toast { + top: 12px; + right: 12px; + max-width: calc(100vw - 24px); + /* Receive touches so the swipe-to-dismiss gesture works (desktop keeps + pointer-events:none so the toast never blocks clicks). Horizontal pan + only, so it doesn't fight page scroll. */ + pointer-events: auto; + touch-action: pan-x; + } + .toast-action-hint { display: none; } + } + + .stop-btn { + position:absolute; top:2px; right:2px; + background:var(--panel); + color:var(--fg); + border:1px solid var(--fg); + font-family:inherit; + font-size:1em; line-height:1; padding:2px 5px; + cursor:pointer; + } + .small-note { font-size:12px; color:color-mix(in srgb, var(--fg) 60%, transparent); margin-top:4px; } + .row-end { justify-content:flex-end; } + .model-chat-btn { + height:32px; padding:0 10px; margin-left:auto; + } + /* Nudge the "+ Chat" label down 1px to sit centered in the button. */ + .model-chat-btn-label { + position: relative; + top: 1px; + } + .openai-row { + display:flex; align-items:center; gap:6px; + } + .models-row { + display:flex; align-items:center; gap:6px; border:1px solid var(--border); padding:4px; margin:4px 0; border-radius: 4px; + } + .models-row .grow, + .models-row select { + flex:1; + display:flex; + align-items:center; + font-size: 9.75px; + } + .model-fav-btn { + width: 8px; height: 8px; + border-radius: 50%; + border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent); + flex-shrink: 0; + cursor: pointer; + transition: all 0.15s; + position: relative; + margin-left: 4px; + } + .model-fav-btn::before { + content: ''; + position: absolute; + top: -10px; left: -10px; right: -10px; bottom: -10px; + } + .model-fav-btn:hover { + border-color: var(--fg); + background: color-mix(in srgb, var(--fg) 27%, transparent); + transform: scale(1.3); + } + .model-fav-btn.active { + background: var(--fg); + border-color: var(--fg); + } + .model-fav-btn.active:hover { + opacity: 0.6; + } + .model-search-input { + width: 100%; + padding: 6px 10px; + margin-bottom: 4px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--fg); + font-family: inherit; + font-size: 0.8rem; + outline: none; + box-sizing: border-box; + transition: border-color 0.15s; + } + .model-search-input:focus { + border-color: var(--red); + } + .model-search-input::placeholder { + color: color-mix(in srgb, var(--fg) 30%, transparent); + } + .models-row button { + font-size: 9px; + height: 24px; + padding: 0 8px; + } + @media (max-width:768px){ + .box { max-height:none; } + .chat-container { padding:10px; flex:1; margin-top:0; padding-top:42px; min-height:0; } + .scroll-nav-btn { width:44px; height:44px; font-size:14px; margin-bottom:0; } + .send-btn { width:48px; height:48px !important; border-radius:12px; } + .send-btn svg { width:22px; height:22px; } + #compare-toggle-btn { display:none !important; } + .section[draggable] { -webkit-user-drag:none; } + .drag-handle, .item-drag-handle, .folder-drag-handle { display:none !important; } + /* Sidebar overlays chat on mobile */ + .sidebar { + position: fixed !important; + top: 0; bottom: 0; left: 0; + z-index: 400; + width: 80% !important; + max-width: 340px; + box-shadow: 4px 0 20px rgba(0,0,0,0.5); + transition: transform 0.25s ease, opacity 0.25s ease !important; + opacity: 1 !important; + overflow: visible !important; + /* Force opaque background — Firefox mobile renders the + backdrop-filter+background combo as semi-transparent on + the slide-out drawer, letting the chat input model picker + label show through the sidebar. */ + /* Keep the drawer tied to the Sidebar color picker. This used to + force --panel on mobile, leaving only the header responsive to + --sidebar-bg. */ + background: var(--sidebar-bg, var(--panel)) !important; + backdrop-filter: none !important; + } + .sidebar.hidden { + width: 80% !important; + transform: translateX(-100%); + pointer-events: none; + overflow: hidden !important; + } + .sidebar.right-side.hidden { + transform: translateX(100%); + } + .sidebar.right-side { + left: auto; right: 0; + box-shadow: -4px 0 20px rgba(0,0,0,0.5); + } + html.ody-sidebar-off .sidebar, + html.ody-mobile-startup-sidebar-hidden .sidebar { + transform: translateX(-100%) !important; + pointer-events: none !important; + opacity: 0 !important; + } + html.ody-sidebar-off .sidebar.right-side, + html.ody-mobile-startup-sidebar-hidden .sidebar.right-side { + transform: translateX(100%) !important; + } + html.ody-sidebar-off .icon-rail, + html.ody-mobile-startup-sidebar-hidden .icon-rail { + display: none !important; + } + /* Backdrop behind sidebar */ + #sidebar-backdrop { + position: fixed; + inset: 0; + z-index: 390; + background: rgba(0,0,0,0.4); + opacity: 0; + pointer-events: none; + transition: opacity 0.35s ease; + } + #sidebar-backdrop.visible { opacity: 1; pointer-events: auto; } + /* Elastic overscroll on sidebar */ + .sidebar-inner { + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: auto; + overflow-y: scroll !important; + padding-bottom: 40px; + } + .sidebar:not(.hidden) { + overscroll-behavior: auto; + } + /* Sidebar header — room for hamburger */ + .sidebar-header { + padding: 18px 12px 8px 12px; + min-height: 44px; + } + .sidebar-brand-title { + font-size: 1.2rem; + left: 0 !important; + } + /* Sidebar inner — bigger spacing */ + .sidebar-inner { + padding: 12px 10px 12px !important; + gap: 4px !important; + } + /* Section headers — match list item sizing */ + .section-header-flex { + padding: 12px 10px !important; + height: 48px !important; + border-radius: 8px; + box-sizing: border-box; + } + .section-header-flex h4, + .section-header-flex .section-title { + font-size: 14px !important; + gap: 11px !important; + } + .section-icon, + .sidebar-action-icon { + width: 16px !important; + height: 16px !important; + left: 0 !important; + } + /* List items — bigger touch targets, bigger text */ + .list-item { + padding: 12px 10px !important; + min-height: 48px; + font-size: 14px; + border-radius: 8px; + gap: 10px !important; + } + .list-item .grow { + font-size: 14px !important; + } + /* Search, New Chat & Assistant */ + #sidebar-search-btn, + #sidebar-new-chat-btn, + .sidebar-assistant-entry { + padding: 12px 10px !important; + min-height: 48px !important; + } + #sidebar-search-btn .grow, + #sidebar-new-chat-btn .grow, + .sidebar-assistant-entry .grow { + font-size: 14px !important; + left: 0 !important; + } + /* Section separator — more breathing room */ + .section { + margin-top: 4px; + border-radius: 8px; + } + /* Wave accent — slightly bigger on mobile */ + .section-header-flex h4::before { + height: 18px; + } + /* Compact top bar on mobile — align with sidebar header */ + .chat-top-bar { + padding: 1px 8px; + min-height: 14px; + margin-top: -31px; + padding-top: 31px; + } + .chat-top-bar .chat-new-btn { display: none; } + .chat-meta-overlay { + font-size: 0.65em; + left: 50% !important; + right: auto !important; + top: 50%; + transform: translate(-50%, -50%); + position: absolute; + width: min(calc(100vw - 104px), 320px); + max-width: calc(100vw - 104px); + overflow: hidden; + justify-content: center; + } + .chat-meta-overlay #current-meta { + max-width: 100%; + } + .chat-meta-overlay .export-dl-btn { display: inline-flex; } + #export-compact-btn { + position: relative; + left: -2px; + } + /* Incognito — smaller on mobile */ + .incognito-btn { + padding: 4px 10px; + font-size: 10px; + } + /* Keep the active Nobody indicator inside the chat header on mobile. + Pinning it next to the hamburger made it collide with the title/splash + on tight screens and detached it from the chat state it represents. */ + .incognito-indicator { + display: none !important; + } + /* Icon rail on mobile — hidden by default, shown in mini-sidebar state */ + .icon-rail { display: none !important; } + .icon-rail.mobile-mini { + display: flex !important; + position: fixed; + top: 0; bottom: 0; left: 0; + z-index: 200; + width: 48px; + box-shadow: 2px 0 12px rgba(0,0,0,0.4); + } + .icon-rail.mobile-mini.right-side { + left: auto; right: 0; + box-shadow: -2px 0 12px rgba(0,0,0,0.4); + } + /* Chat bubbles — AI stretches full width, user stays compact */ + .msg { font-size: 0.85em; } + .msg .body { font-size: 0.9em; } + .msg-user { max-width: 90% !important; margin-left: auto; margin-right: 4px; } + .msg-ai, .agent-thread { width: 100% !important; max-width: 100% !important; margin: 8px 0; } + /* Prevent inner scrollable elements from trapping vertical scroll */ + .msg pre, + .agent-tool-output pre, + .agent-thread-cmd, + .msg details { + overflow-y: hidden !important; + max-height: none !important; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + touch-action: pan-y pan-x; + } + /* Models row — match list items */ + .models-row { + padding: 12px 10px; + min-height: 48px; + } + /* Input icon buttons — bigger touch targets */ + .input-icon-btn { + padding: 10px; + min-width: 44px; + min-height: 44px; + } + /* Tool indicators — icon only on mobile (hide text, keep x) */ + .tool-indicator > span { + display: none !important; + } + .tool-indicator .tool-indicator-x { + display: inline-block !important; + opacity: 0.6; + } + /* Hamburger — always right on mobile */ + .hamburger-btn { + width: 44px; + height: 44px; + top: 6px; + left: auto !important; + right: 4px !important; + -webkit-tap-highlight-color: transparent; + } + .hamburger-btn:hover, + .hamburger-btn:active { + background: none !important; + border: none !important; + box-shadow: none !important; + } + /* New chat — always left on mobile */ + .mobile-new-chat-btn { + display: flex; + position: fixed; + top: 12px; + left: 8px; + z-index: 210; + width: 32px; + height: 32px; + align-items: center; + justify-content: center; + background: none; + border: none; + color: var(--fg); + cursor: pointer; + opacity: 0.5; + padding: 0; + } + /* Modal close button — bigger on mobile */ + .close-btn, + .modal-close { + min-width: 44px; + min-height: 44px; + width: 44px; + height: 44px; + font-size: 14px; + } + /* Code block buttons — slightly bigger touch targets */ + pre .copy-code, + pre .edit-code, + pre .run-code { + width: 44px; + height: 44px; + } + /* Touch-friendly targets for small buttons */ + .export-dl-btn { + min-width: 44px; + min-height: 44px; + } + .section-header-btn { + min-width: 44px; + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + } + /* 44×44 touch buttons starting from right:6 ends at 50. ~8px gap is + enough to be distinguishable without spreading them too far apart. */ + pre .edit-code { + right: 58px; + } + pre .run-code { + right: 110px; + } + /* Dropdowns — bigger touch targets on mobile */ + .dropdown-item-compact { + padding: 12px 12px !important; + font-size: 14px !important; + min-height: 44px; + gap: 10px !important; + } + .dropdown-item-compact .dropdown-icon { + width: 18px !important; + height: 18px !important; + } + .dropdown-item-compact .dropdown-icon svg { + width: 16px !important; + height: 16px !important; + } + .dropdown, + .session-dropdown { + padding: 6px !important; + border-radius: 12px !important; + } + /* Safe area padding for notched devices */ + .chat-input-bar { + padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px)); + } + /* Mode toggle — larger touch targets */ + .mode-toggle { + height: 34px; + border-radius: 12px; + } + .mode-toggle::before { + border-radius: 11px; + } + .mode-toggle-btn { + padding: 0 14px; + font-size: 12px; + min-height: 34px; + } + #mode-agent-btn { border-radius: 12px 0 0 12px; } + #mode-chat-btn { border-radius: 0 12px 12px 0; } + /* Diff mode — stack toolbar, bigger buttons */ + .diff-toolbar { + padding: 8px 12px; + gap: 6px; + flex-wrap: wrap; + } + .diff-toolbar-btn { + padding: 6px 12px; + font-size: 12px; + min-height: 36px; + } + .diff-chunk-btn { + width: 28px; + height: 28px; + font-size: 14px; + } + /* Document/email/gallery/research library — full-height bottom-sheet + on mobile. Keep the rounded top corners + border-top so they match + the cookbook/calendar/compare look instead of looking like raw + full-bleed panels. */ + .doclib-modal-content, + .gallery-modal-content { + width: 100vw !important; + max-width: 100vw !important; + /* vh fallback, dvh override so the modal adapts to mobile + URL-bar show/hide. Order matters: later same-specificity rule + wins, so dvh must come after vh. */ + max-height: 100vh !important; + max-height: 100dvh !important; + height: 100vh; + height: 100dvh; + border-radius: 14px 14px 0 0 !important; + border: none !important; + border-top: 1px solid var(--border) !important; + box-shadow: none !important; + padding: 6px !important; + padding-bottom: env(safe-area-inset-bottom, 6px) !important; + margin: 0 !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + #email-lib-modal .doclib-grid { + max-height: none; + flex: 1; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + /* Library modal layout: pin the header + tab strip, give the active + panel (admin-card) the remaining height, and let the grid scroll + internally. The load-more button sits below the grid as a sibling + inside admin-card so it stays visible at the bottom of the panel + without competing with the grid for scroll. */ + #doclib-modal .doclib-modal-content { + display: flex !important; + flex-direction: column !important; + overflow: hidden !important; + } + #doclib-modal .modal-header, + #doclib-modal .lib-tabs { + flex: 0 0 auto !important; + } + #doclib-modal .modal-body { + flex: 1 1 0 !important; + min-height: 0 !important; + overflow: hidden !important; + } + #doclib-modal .admin-card { + flex: 1 1 0 !important; + min-height: 0 !important; + } + /* :not(:has(.doclib-card-expanded)) scopes these to the normal list + state — when a document card is expanded, the existing expand-state + rules take over (grid claims flex:1 with overflow:hidden so the + expanded card can scroll itself). */ + #doclib-modal .doclib-grid:not(:has(.doclib-card-expanded)) { + flex: 1 1 0 !important; + min-height: 0 !important; + max-height: none !important; + overflow-y: auto !important; + -webkit-overflow-scrolling: touch; + } + #doclib-modal .doclib-load-more { + margin: 8px auto 12px !important; + } + /* Squeeze a little more height for the preview content by tightening + the spacing inside an expanded doc card on mobile. */ + #doclib-modal .doclib-card.doclib-card-expanded, + #email-lib-modal .doclib-card.doclib-card-expanded, + #memory-modal .doclib-card.doclib-card-expanded { + gap: 2px !important; + padding: 4px 6px !important; + height: 100% !important; + } + /* Skills preview kept stopping at partial height because the flex + chain (modal → tabs → panel → admin-card → grid → card) wouldn't + reliably hand the card a full-height parent. But the skills LIST + already scrolls correctly inside #skills-list, so that grid box + already has the right bounded height (the area below the tabs). + Anchor the expanded card to THAT box with position:absolute so it + fills the list area exactly — header + tabs stay visible. + + NOTE: position:relative here is UNCONDITIONAL (not gated behind + :has(.doclib-card-expanded)). Firefox mobile builds without :has() + support never applied the gated rule, so the absolute card lost its + anchor and only filled ~50% — while Chromium/desktop-narrow worked. + Relative-when-collapsed is harmless (no abs children then). */ + #memory-modal #skills-list.doclib-grid { + position: relative !important; + } + #memory-modal .doclib-card.skill-card.doclib-card-expanded { + position: absolute !important; + inset: 0 !important; + /* Height is set in JS (skills.js _fillSkillCardHeight) as an explicit + px value — Firefox does NOT treat inset:0 stretch OR height:100% + (against the flex-sized grid) as a definite height, so grid/flex + children never filled. An explicit px height is unambiguous. */ + margin: 0 !important; + padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px)) !important; + background: var(--bg) !important; + /* Flex column. JS (_fillSkillCardHeight) sets EXPLICIT px heights on + the preview +
; in a flex column an explicit-height item
+           (flex:0 0 auto + height) is honoured. (Grid was worse here — the
+           collapsed 1fr track overrode the preview's explicit height.) */
+        display: flex !important;
+        flex-direction: column !important;
+        overflow: hidden !important;
+        border: none !important;
+        border-radius: 0 !important;
+        box-sizing: border-box !important;
+      }
+      /* Preview is the 1fr grid track — give it a definite-height flex column
+         so the 
 (flex:1) fills and the footer pins at the bottom. */
+      #memory-modal .doclib-card.skill-card.doclib-card-expanded > .doclib-card-preview {
+        display: flex !important;
+        flex-direction: column !important;
+        min-height: 0 !important;
+        overflow: hidden !important;
+      }
+      /* The card now fills the screen, but a base rule (.skill-card...
+         .doclib-card-preview { flex: 0 1 auto }) sizes the preview to its
+         CONTENT — so a medium SKILL.md left the preview at ~half the card
+         (the "only 50%" the debug confirmed: card was full, content wasn't).
+         Force the preview AND the 
 to FILL the card. Extra .skill-card
+         in the selector out-specifies both the base and the generic mobile
+         rule so this wins without ambiguity. */
+      #memory-modal .doclib-card.skill-card.doclib-card-expanded > .doclib-card-preview {
+        flex: 1 1 auto !important;
+        min-height: 0 !important;
+      }
+      #memory-modal .doclib-card.skill-card.doclib-card-expanded .skill-md-pre {
+        flex: 1 1 auto !important;
+        min-height: 0 !important;
+      }
+      /* Flatten the expanded doc/email card on mobile — drop the inner
+         border + background so it doesn't read as a "subwindow" inside the
+         modal (the chat preview doesn't have that nested-card look). The
+         body prefix beats the desktop email rule later in the file that
+         paints a 2px accent border + box-shadow with !important. */
+      body #doclib-modal .doclib-card.doclib-card-expanded,
+      body #email-lib-modal .doclib-card.doclib-card-expanded,
+      body #memory-modal .doclib-card.doclib-card-expanded {
+        background: transparent !important;
+        border: none !important;
+        border-radius: 0 !important;
+        box-shadow: none !important;
+      }
+      /* Same layout pattern the chat preview uses: preview itself clips,
+         the 
 (or PDF iframe) inside owns the scroll, and the action
+         bar pins to the bottom with extra bottom padding so it floats
+         above the iOS safe-area / home-indicator strip. */
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview,
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview {
+        padding: 4px 6px 20px !important;
+        flex: 1 1 auto !important;
+        /* The preview lives inside an already bounded expanded card. Giving
+           it a viewport-sized minimum pushes its footer below that card,
+           where overflow:hidden clips it. The text child owns scrolling. */
+        min-height: 0 !important;
+        overflow: hidden !important;
+        display: flex !important;
+        flex-direction: column !important;
+        border-top: none !important;
+      }
+      /* The "Load more" button is flex-shrink:0 and sits in the panel AFTER the
+         grid, so even when a card is expanded it reserves ~40px at the panel
+         bottom — shrinking the visible grid and clipping the action bar by that
+         much (the "black bar"). The global collapse rule's max-height:0 can't
+         remove a flex-shrink:0 item, so hide it outright on expand. */
+      #doclib-modal [data-doclib-panel="documents"]:has(.doclib-card-expanded) .doclib-load-more,
+      #doclib-modal [data-doclib-panel="documents"]:has(.doclib-card-expanded) .doclib-inline-load-more {
+        display: none !important;
+      }
+      /* Small bottom padding now that the load-more no longer steals space —
+         the action bar sits low, just clearing the home-indicator safe area. */
+      #doclib-modal [data-doclib-panel="documents"] .doclib-card.doclib-card-expanded .doclib-card-preview {
+        padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px)) !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .doclib-card-pdf-frame,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-body,
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview .skill-md-editor {
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+        overflow-y: auto !important;
+        -webkit-overflow-scrolling: touch;
+        /* Strip the code-box visual treatment so the 
 / reader body
+           doesn't read as a "sub-window" inside the modal. */
+        background: transparent !important;
+        border: none !important;
+        box-shadow: none !important;
+        padding: 0 !important;
+        margin: 0 !important;
+        border-radius: 0 !important;
+      }
+      /* The skill editor textarea keeps a light frame on mobile (it's an
+         input, not read-only text) — override the strip-down above. */
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview .skill-md-editor {
+        background: var(--bg) !important;
+        border: 1px solid var(--border) !important;
+        padding: 8px !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code,
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview code.hljs,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview code.hljs,
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code {
+        background: transparent !important;
+        border: none !important;
+        padding: 0 !important;
+        box-shadow: none !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions,
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions {
+        padding: 6px 4px 0 !important;
+        margin-top: 4px !important;
+        flex-shrink: 0 !important;
+      }
+      /* Email reader on mobile inherits the library modal's horizontal
+         padding (the .doclib-modal-content default — 6px). No extra
+         overrides for the modal-content / modal-body / admin-card / grid
+         chain; we just clean up the inner email reader header/body
+         padding so the From / To lines align with the email subject. */
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-header,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-atts,
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-body {
+        padding-left: 6px !important;
+        padding-right: 6px !important;
+      }
+      #email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-body {
+        margin-left: -6px !important;
+        margin-right: -6px !important;
+        padding-left: 12px !important;
+        padding-right: 12px !important;
+        background: var(--panel) !important;
+        box-shadow: inset 0 1px 0 color-mix(in srgb, var(--border) 65%, transparent) !important;
+      }
+      /* Mobile email-reader header: meta on the left, actions on the right
+         (two stacked rows). The two .email-reader-actions-row siblings
+         render as their own flex-row strips so primary (reply/reply-all/
+         forward) sits above secondary (AI/summary/more), instead of
+         flattening into one line that collided with the recipient chips. */
+      #email-lib-modal .email-reader-header,
+      .email-reader-tab-modal .email-reader-header,
+      .email-window-modal .email-reader-header {
+        padding: 8px 8px !important;
+        gap: 6px !important;
+        flex-direction: row !important;
+        /* Top-align meta + actions on mobile. With the two-row action
+           cluster (Summary/More above Reply/Forward/AI), align-items:
+           center pulled the meta's From:/To: rows down into the visual
+           middle of the header, leaving them visibly off-axis. */
+        align-items: flex-start !important;
+      }
+      #email-lib-modal .email-reader-actions,
+      .email-reader-tab-modal .email-reader-actions,
+      .email-window-modal .email-reader-actions {
+        display: flex !important;
+        flex-direction: column !important;
+        align-items: flex-end !important;
+        gap: 4px !important;
+        flex-shrink: 0 !important;
+        margin-left: auto !important;
+        order: 0 !important;
+      }
+      #email-lib-modal .email-reader-actions-row,
+      .email-reader-tab-modal .email-reader-actions-row,
+      .email-window-modal .email-reader-actions-row {
+        display: flex !important;
+        flex-direction: row !important;
+        flex-wrap: nowrap !important;
+        align-items: center !important;
+        justify-content: flex-end !important;
+        gap: 4px !important;
+      }
+      /* Meta on mobile: From / To / Cc each on their own row, label
+         tight to chips, chips scroll horizontally per row. */
+      #email-lib-modal .email-reader-meta,
+      .email-reader-tab-modal .email-reader-meta,
+      .email-window-modal .email-reader-meta {
+        display: flex !important;
+        flex-direction: column !important;
+        gap: 2px !important;
+        min-width: 0 !important;
+        width: 100% !important;
+      }
+      #email-lib-modal .email-reader-meta-row,
+      .email-reader-tab-modal .email-reader-meta-row,
+      .email-window-modal .email-reader-meta-row {
+        display: flex !important;
+        grid-template-columns: none !important;
+        align-items: center !important;
+        gap: 6px !important;
+        padding-right: 0 !important;
+        min-width: 0 !important;
+      }
+      #email-lib-modal .email-reader-meta-row .recipient-chips,
+      .email-reader-tab-modal .email-reader-meta-row .recipient-chips,
+      .email-window-modal .email-reader-meta-row .recipient-chips {
+        display: inline-flex !important;
+        flex-wrap: nowrap !important;
+        overflow-x: auto !important;
+        overflow-y: hidden !important;
+        max-width: 100% !important;
+        scrollbar-width: none !important;
+        gap: 4px !important;
+      }
+      #email-lib-modal .email-reader-meta-row .recipient-chips::-webkit-scrollbar,
+      .email-reader-tab-modal .email-reader-meta-row .recipient-chips::-webkit-scrollbar,
+      .email-window-modal .email-reader-meta-row .recipient-chips::-webkit-scrollbar {
+        display: none !important;
+      }
+      #email-lib-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn,
+      .email-reader-tab-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn,
+      .email-window-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn {
+        width: auto !important;
+        height: 44px !important;
+        flex: 0 0 auto !important;
+        display: inline-flex !important;
+        flex-direction: column !important;
+        align-items: center !important;
+        justify-content: center !important;
+        gap: 3px !important;
+        padding: 4px 2px !important;
+      }
+      #email-lib-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg,
+      .email-reader-tab-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg,
+      .email-window-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg {
+        width: 16px !important;
+        height: 16px !important;
+      }
+      /* Gallery-style label under each button — only on mobile. The
+         global rule below the @media block hides them on desktop. */
+      #email-lib-modal .reader-btn-label,
+      .email-reader-tab-modal .reader-btn-label,
+      .email-window-modal .reader-btn-label {
+        display: inline-block !important;
+        font-size: 8.5px !important;
+        font-weight: 500 !important;
+        line-height: 1 !important;
+        letter-spacing: 0.02em !important;
+        opacity: 0.75 !important;
+        white-space: nowrap !important;
+      }
+      /* List-view cards keep the framed look from their own .doclib-card /
+         .memory-item base styles — no extra grid padding here, so the
+         spacing matches the documents/library tab exactly. */
+      /* Tighten the top of the email sheet — modal header + description +
+         account row + toolbar were stacking with full desktop spacing and
+         pushed the email subjects way down. */
+      #email-lib-modal .modal-header {
+        padding: 4px 8px !important;
+        min-height: 0 !important;
+      }
+      #email-lib-modal .modal-header h4 {
+        /* Match the other tool headers (base .modal-header h4 = 1rem); was
+           13px, which read noticeably smaller than Calendar/Tasks/etc. */
+        font-size: 1rem !important;
+        line-height: 1.2 !important;
+      }
+      #email-lib-modal .modal-body {
+        gap: 4px !important;
+      }
+      #email-lib-modal .admin-card {
+        gap: 4px !important;
+      }
+      #email-lib-modal .admin-card > .doclib-desc {
+        display: none !important;
+      }
+      /* When an email is expanded the list-mode toolbar siblings are
+         already hidden by the existing :has rule. Hide the modal-header
+         entirely AND zero out the modal-content top padding so the email
+         reader claims the full sheet height. The swipe-down gesture (and
+         the dock chip) still dismiss the modal. */
+      #email-lib-modal:has(.doclib-card-expanded) .modal-header,
+      #email-lib-modal.email-reading .modal-header {
+        display: none !important;
+      }
+      #email-lib-modal:has(.doclib-card-expanded) .doclib-modal-content,
+      #email-lib-modal.email-reading .doclib-modal-content {
+        padding-top: 0 !important;
+      }
+      #email-lib-modal:has(.doclib-card-expanded) .modal-body,
+      #email-lib-modal.email-reading .modal-body {
+        gap: 0 !important;
+      }
+      /* Flatten the From / To bar so it doesn't read as a cut-off framed
+         strip with a hard background change. Remove the background, drop
+         the border-bottom to a faint divider, and let it blend with the
+         sheet. */
+      #email-lib-modal .email-reader-header {
+        background: transparent !important;
+        border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent) !important;
+      }
+      #email-lib-modal .email-card-nav-btn {
+        padding: 6px 10px !important;
+        min-width: 40px !important;
+        height: 38px !important;
+      }
+      #email-lib-modal .email-card-nav-btn svg {
+        width: 18px !important;
+        height: 18px !important;
+      }
+      /* Nudge the prev/next arrow cluster ~4px to the right so it sits
+         comfortably out at the edge instead of crowding the subject text. */
+      #email-lib-modal .email-card-nav-arrows {
+        transform: translateX(4px);
+      }
+      /* Done check — extend the actual TAP area via padding + negative
+         margin (a transparent ::before doesn't change the parent's hit
+         box). Layout stays the same height as library cards because the
+         margin cancels the padding visually, but the clickable region is
+         ~37px square. */
+      #email-lib-modal .email-card-done {
+        position: relative !important;
+        z-index: 5 !important;
+        pointer-events: auto !important;
+        touch-action: manipulation !important;
+        padding: 12px !important;
+        margin: -12px !important;
+      }
+      #email-lib-modal .email-card-done svg {
+        width: 13px !important;
+        height: 13px !important;
+      }
+      /* Make sure no overlay or pseudo intercepts the tap. */
+      #email-lib-modal .email-card-done * { pointer-events: none; }
+      /* Tighten the From / To meta bar and the email body — they had
+         desktop padding (10–14px) that wasted real estate on phones. */
+      #email-lib-modal .email-reader-header {
+        padding: 6px 4px !important;
+        gap: 4px !important;
+      }
+      #email-lib-modal .email-reader-meta {
+        font-size: 11px !important;
+      }
+      #email-lib-modal .email-reader-meta-row strong {
+        min-width: 28px !important;
+      }
+      #email-lib-modal .email-reader-atts {
+        padding: 6px 4px !important;
+      }
+      #email-lib-modal .email-reader-body {
+        padding: 8px 4px !important;
+      }
+      /* Make sure the grid claims the full admin-card height when a doc
+         is expanded — the existing rule sets flex:1, but on mobile we also
+         need a hard height fallback because the parent uses dvh which
+         desktop-tuned selectors don't always trickle through. */
+      #doclib-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid,
+      #memory-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid {
+        height: 100% !important;
+      }
+      /* Documents need the same bounded flex chain as skills when a card is
+         opened. Without this, the grid falls back to its list layout and the
+         expanded preview only grows to its content height on mobile. */
+      #doclib-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid {
+        display: flex !important;
+        flex: 1 1 0 !important;
+        flex-direction: column !important;
+        min-height: 0 !important;
+        overflow: hidden !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded {
+        flex: 1 1 auto !important;
+        height: 100% !important;
+        min-height: 0 !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview {
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-pdf-frame {
+        height: auto !important;
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+      }
+      /* Skills modal: keep the header + tab strip visible; the expanded
+         card fills the skills-list area below them via position:absolute
+         (see the #skills-list rule above). Just make sure the tab-panel +
+         its card give the list its full height. */
+      #memory-modal .memory-tab-panel[data-memory-panel="skills"] > .admin-card:has(.doclib-card-expanded) {
+        flex: 1 1 auto !important;
+        min-height: 0 !important;
+      }
+      .doclib-card-header {
+        padding: 10px 8px;
+        gap: 4px;
+      }
+      .doclib-card-session,
+      .doclib-card-time {
+        display: none;
+      }
+      .doclib-card-expanded-actions {
+        flex-wrap: nowrap;
+      }
+      /* Keep the footer buttons identical to the chat/research footers on
+         mobile too — those have no mobile enlargement, so neither should
+         these (otherwise the doc footer reads in a larger/different font). */
+      .doclib-card-action-btn {
+        font-size: 10px;
+        padding: 3px 8px;
+      }
+      /* On phones, keep Delete explicit while the other secondary actions
+         stay available from the kebab. */
+      #doclib-modal .doclib-card-expanded-actions > .doclib-expanded-archive-btn,
+      #doclib-modal .doclib-card-expanded-actions .doclib-expanded-clone-btn,
+      #doclib-modal .doclib-card-expanded-actions .doclib-expanded-export-btn {
+        display: none !important;
+      }
+      #doclib-modal .doclib-card-expanded-actions > .doclib-expanded-delete-btn {
+        display: inline-flex !important;
+        order: 0;
+        left: 0 !important;
+        margin-left: 0;
+      }
+      #doclib-modal .doclib-card-expanded-actions > .doclib-action-group {
+        order: 2;
+        margin-left: 6px !important;
+        min-width: 0;
+      }
+      #doclib-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions {
+        box-sizing: border-box;
+        width: 100%;
+        max-width: 100%;
+        min-width: 0;
+        overflow: visible;
+      }
+      #doclib-modal .doclib-card-expanded-actions .doclib-expanded-open-btn {
+        min-height: 30px;
+        padding: 5px 10px;
+        max-width: 100%;
+      }
+      #doclib-modal .doclib-card-expanded-actions > .doclib-expanded-mobile-more {
+        display: inline-flex;
+        order: 1;
+        width: 32px;
+        min-width: 32px;
+        min-height: 30px;
+        margin-left: auto;
+        padding: 0;
+      }
+      /* Chat top bar — adjusted for reduced height */
+      /* Suggestion nav — bigger touch targets */
+      .doc-suggestion-nav-btn {
+        padding: 6px 8px;
+        font-size: 18px;
+      }
+      .doc-suggestion-close {
+        padding: 10px 12px;
+        margin: -10px -12px;
+      }
+    }
+    #mobile-backdrop, #mobile-menu-btn { display:none !important; }
+    #sidebar-backdrop { display:none !important; }
+    /* ----- Loading spinner ----- */
+
+    .spinner {
+      width: 24px;
+      height: 24px;
+      margin: 8px auto;
+      border: 3px solid var(--border);
+      border-top-color: var(--red);
+      border-radius: 50%;
+      animation: spin 0.9s linear infinite;
+    }
+    /* Inline spinner for buttons */
+    .btn-spinner {
+      display: inline-block;
+      width: 12px;
+      height: 12px;
+      border: 2px solid transparent;
+      border-top-color: currentColor;
+      border-radius: 50%;
+      animation: spin 1s linear infinite;
+      margin-right: 6px;
+    }
+    .search-status {
+      font-size: 0.85em;
+      color: var(--red);
+      margin-top: 4px;
+      padding: 4px;
+      border-left: 2px solid var(--red);
+      background: color-mix(in srgb, var(--red) 5%, transparent);
+    }
+    /* Loading indicator for messages */
+    .loading-indicator {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 10px;
+    }
+    .loading-dots {
+      display: flex;
+      gap: 4px;
+    }
+    .loading-dot {
+      width: 6px;
+      height: 6px;
+      background-color: var(--red);
+      border-radius: 50%;
+    }
+    .loading-dot:nth-child(1) {
+      animation: loading-bounce 1.4s infinite ease-in-out both;
+    }
+    .loading-dot:nth-child(2) {
+      animation: loading-bounce 1.4s infinite ease-in-out both;
+      animation-delay: -0.32s;
+    }
+    .loading-dot:nth-child(3) {
+      animation: loading-bounce 1.4s infinite ease-in-out both;
+      animation-delay: -0.64s;
+    }
+    .session-loading-state {
+      min-height: 100%;
+      display: flex;
+      flex-direction: column;
+      justify-content: flex-start;
+      gap: 12px;
+      color: var(--fg);
+      font-size: 12px;
+      pointer-events: none;
+      position: relative;
+      padding: 16px 14px 24px;
+      box-sizing: border-box;
+      overflow: hidden;
+    }
+    .session-loading-state::before { content: none; }
+    .session-skeleton-bubble {
+      width: min(78%, 680px);
+      border: 1px solid color-mix(in srgb, var(--border) 74%, transparent);
+      border-radius: 10px;
+      background: color-mix(in srgb, var(--panel, var(--bg)) 72%, transparent);
+      padding: 10px 12px 12px;
+      box-shadow: 0 8px 22px rgba(0, 0, 0, 0.14);
+    }
+    .session-skeleton-bubble.is-user {
+      align-self: flex-end;
+      width: min(64%, 520px);
+    }
+    .session-skeleton-bubble.is-ai {
+      align-self: flex-start;
+    }
+    .session-skeleton-line {
+      position: relative;
+      overflow: hidden;
+      background: color-mix(in srgb, var(--fg) 10%, transparent);
+      border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
+      border-radius: 999px;
+    }
+    .session-skeleton-line {
+      height: 10px;
+      margin-top: 8px;
+      opacity: 0.62;
+    }
+    .session-skeleton-line::after {
+      content: '';
+      position: absolute;
+      inset: -1px;
+      transform: translateX(-120%);
+      background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--fg) 14%, transparent), transparent);
+      animation: session-skeleton-shimmer 1.25s ease-in-out infinite;
+    }
+    .session-skeleton-bubble:nth-child(2) .session-skeleton-line::after { animation-delay: 0.12s; }
+    .session-skeleton-bubble:nth-child(3) .session-skeleton-line::after { animation-delay: 0.24s; }
+    .session-skeleton-bubble:nth-child(4) .session-skeleton-line::after { animation-delay: 0.36s; }
+    @keyframes session-skeleton-shimmer {
+      100% { transform: translateX(120%); }
+    }
+    @media (max-width: 768px) {
+      .session-loading-state { padding: 12px 10px 18px; gap: 10px; }
+      .session-skeleton-bubble { width: 86%; padding: 9px 10px 11px; border-radius: 9px; }
+      .session-skeleton-bubble.is-user { width: 72%; }
+      .session-skeleton-line { height: 9px; margin-top: 7px; }
+    }
+
+    /* Modal styling */
+    .modal {
+      position:fixed;
+      top:0; left:0; width:100%; height:100%;
+      background:none;
+      display:flex; align-items:center; justify-content:center;
+      z-index:250;
+      backdrop-filter:none;
+      pointer-events:none;
+    }
+    /* Cookbook always sits above Gallery so the "Serve a model in
+       Cookbook…" flow (and any other "open Cookbook from inside another
+       modal") is visible no matter which modal was opened first. */
+    #cookbook-modal { z-index: 260; }
+    .modal.hidden { display:none; }
+
+    /* Tool windows open centered in the CHAT AREA (the space right of the
+       sidebar + icon rail), rather than the full viewport.
+       We narrow the overlay to the chat area so its flex-centering lands the
+       window there; fullscreen / docked states use position:fixed so they
+       escape this narrowed overlay and still fill the screen. The
+       --sidebar-w / --icon-rail-w vars track collapse state live, so when a
+       window (e.g. Cookbook) hides the sidebar this naturally re-centers.
+       Desktop only — on mobile these are full-screen sheets. */
+    @media (min-width: 769px) {
+      #calendar-modal,
+      #gallery-modal,
+      #tasks-modal,
+      #memory-modal,
+      #doclib-modal,
+      #compare-model-overlay,
+      #research-overlay,
+      #theme-modal,
+      #settings-modal,
+      #email-lib-modal {
+        left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px));
+        width: calc(100% - (var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)));
+        box-sizing: border-box;
+        /* Slide in sync with the sidebar's 0.25s collapse/expand so the
+           centered window glides instead of jumping when the nav toggles. */
+        transition: left 0.25s ease, width 0.25s ease;
+      }
+    }
+    .modal-content {
+      background:var(--panel);
+      border:1px solid var(--border);
+      width:min(520px, 92vw); max-height:85vh; padding:10px;
+      box-sizing:border-box; font-size:14px;
+      font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+      letter-spacing: -0.015em;
+      display:flex; flex-direction:column;
+      position:relative;
+      overflow-y:auto;
+      border-radius:10px;
+      box-shadow:0 8px 32px rgba(0,0,0,0.45);
+      pointer-events:auto;
+      animation: modal-enter 0.25s ease-out both;
+    }
+    .memory-modal-content,
+    .tasks-modal-content,
+    .preset-modal-content,
+    #cookbook-modal .modal-content,
+    #theme-popup,
+    .doclib-modal-content,
+    .gallery-modal-content {
+      container-type: inline-size;
+    }
+    .modal-header {
+      display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;
+      cursor:grab; user-select:none;
+      /* Pin the header (with its close button) to the top of the
+         scrollable modal-content so users can always dismiss the modal
+         even after scrolling far down — especially important on mobile
+         where the modal can be taller than the viewport. */
+      position: sticky;
+      top: 0;
+      z-index: 5;
+      /* Inherit the modal-content's background so the header matches the
+         panel body. Many tool modals set their content to var(--bg) inline
+         while this header was hard-coded to var(--panel) — making the title
+         strip a different shade in every theme. `inherit` tracks whatever
+         the content uses (var(--bg) or the default var(--panel)) and stays
+         opaque, so the sticky header still masks scrolled content. */
+      background-color: inherit;
+    }
+    .modal-header:active { cursor:grabbing; }
+    /* Edge/corner window resize (windowResize.js). While a resize is in
+       progress, suppress text selection and force the active resize cursor
+       across the whole document so it does not flicker as the pointer passes
+       over child elements mid-drag. */
+    body.window-resizing-active { user-select:none !important; }
+    body.window-resizing-active * { cursor:inherit !important; }
+    /* Suppress only TRANSITIONS while resizing so the edge tracks the cursor
+       crisply. We deliberately do NOT toggle `animation` here: toggling
+       animation off→on re-triggers the modal open-animation (a scale-in) on
+       mouseup, which both mis-measures the final size and visibly "pops" the
+       window. windowResize.js instead kills the one-shot open animation inline
+       once, in begin(). */
+    .window-resizing {
+      transition:none !important;
+    }
+    /* Cookbook opts out of the sticky header on mobile; the title bar scrolls
+       away with the content instead of following. Background is inherited from
+       the shared .modal-header rule so frosted/glass themes stay consistent. */
+    #cookbook-modal .modal-header {
+      position: static;
+      top: auto;
+    }
+    .modal-header h4 {
+      margin:0;
+      /* Push every header control (opacity slider, minimize, close) into one
+         group on the right — works whether or not optional controls like the
+         opacity slider are present, so the minimize button never floats to
+         the centre when a sibling is hidden. */
+      margin-right:auto;
+      font-size:1rem;
+      font-weight:600;
+      letter-spacing:-0.03em;
+      color:var(--red);
+    }
+    /* Headers with an optional Peek control already have the title's
+       auto-margin grouping all right-side controls. A second auto-margin on
+       the injected minimize button splits the group and makes it look
+       different from the other modal headers. */
+    .modal-header .theme-opacity-wrap + .modal-minimize-btn {
+      margin-left: 0 !important;
+    }
+    .close-btn,
+    .modal-close {
+      background:transparent;
+      color:color-mix(in srgb, var(--fg) 58%, transparent);
+      border:1px solid transparent;
+      font-size:0;
+      width:24px;
+      height:24px;
+      padding:0;
+      display:inline-flex;
+      align-items:center;
+      justify-content:center;
+      line-height:1;
+      text-indent:0;
+      cursor:pointer;
+      border-radius:999px;
+      line-height:1;
+      flex-shrink:0;
+      position:relative;
+    }
+    .close-btn > svg,
+    .modal-close > svg {
+      display:none;
+    }
+    .close-btn::before,
+    .modal-close::before {
+      content:"";
+      width:10px;
+      height:10px;
+      display:block;
+      background:currentColor;
+      -webkit-mask:
+        url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 2L8 8M8 2L2 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E")
+        center / contain no-repeat;
+      mask:
+        url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 2L8 8M8 2L2 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E")
+        center / contain no-repeat;
+    }
+    .close-btn:hover,
+    .modal-close:hover {
+      background:color-mix(in srgb, var(--fg) 10%, transparent);
+      border-color:color-mix(in srgb, var(--fg) 12%, transparent);
+      color:var(--fg);
+    }
+    /* Minimize button — sits beside the close button on every modal */
+    .minimize-btn {
+      background:transparent;
+      color:color-mix(in srgb, var(--fg) 58%, transparent);
+      border:1px solid transparent;
+      font-size:0;
+      font-weight:700;
+      width:24px;
+      height:24px;
+      padding:0;
+      display:inline-flex;
+      align-items:center;
+      justify-content:center;
+      line-height:1;
+      cursor:pointer;
+      border-radius:999px;
+      flex-shrink:0;
+      margin-left:auto; /* push to the right so it docks next to .close-btn */
+      margin-right:4px;
+    }
+    .minimize-btn::before {
+      content:"";
+      width:9px;
+      height:2px;
+      border-radius:999px;
+      background:currentColor;
+      display:block;
+      transform:translateY(2px);
+    }
+    .modal-minimize-btn.minimize-btn::before {
+      content:none;
+      display:none;
+    }
+    .minimize-btn:hover {
+      background:color-mix(in srgb, var(--fg) 10%, transparent);
+      border-color:color-mix(in srgb, var(--fg) 12%, transparent);
+      color:var(--fg);
+    }
+    @media (max-width: 768px) {
+      .minimize-btn { display: none !important; }
+      #modal-dock { display: none !important; }
+    }
+    /* Minimized modals are hidden but stay in the DOM with their state intact */
+    .modal.minimized { display:none !important; }
+    /* Bottom dock for minimized modals */
+    #modal-dock {
+      position:fixed;
+      bottom:var(--composer-clearance, 0px);
+      left:0;
+      right:0;
+      display:flex;
+      flex-wrap:wrap;
+      gap:4px;
+      padding:4px 8px;
+      z-index:240;
+      pointer-events:none;
+      justify-content:flex-start;
+    }
+    #modal-dock:empty { display:none; }
+    .modal-dock-item {
+      background:var(--panel);
+      border:1px solid var(--border);
+      border-bottom:none;
+      border-radius:6px 6px 0 0;
+      padding:4px 4px 4px 10px;
+      display:inline-flex;
+      align-items:center;
+      gap:6px;
+      cursor:pointer;
+      pointer-events:auto;
+      font-size:12px;
+      color:var(--fg);
+      max-width:220px;
+      box-shadow:0 -2px 8px rgba(0,0,0,0.25);
+      transition:background 0.15s;
+    }
+    .modal-dock-item:hover { background:var(--bg); }
+    .modal-dock-label {
+      white-space:nowrap;
+      overflow:hidden;
+      text-overflow:ellipsis;
+      max-width:180px;
+    }
+    .modal-dock-close {
+      background:transparent;
+      border:none;
+      color:var(--fg);
+      cursor:pointer;
+      font-size:14px;
+      padding:0 4px;
+      line-height:1;
+      opacity:0.6;
+    }
+    .modal-dock-close:hover { color:var(--red); opacity:1; }
+    .modal-body { flex:1; overflow-y:auto; }
+    .modal-body button { margin-top:6px; }
+    /* Styled confirm dialog — keeps backdrop */
+    #styled-confirm-overlay {
+      background:rgba(0,0,0,0.5);
+      backdrop-filter:blur(4px);
+      pointer-events:auto !important;
+      z-index: 99999 !important;
+      position: fixed !important;
+      top: 0 !important;
+      left: 0 !important;
+      width: 100% !important;
+      height: 100% !important;
+      top: 0; left: 0; width: 100%; height: 100%;
+    }
+    #styled-confirm-overlay .modal-content {
+      position: relative;
+      z-index: 10001;
+    }
+    .email-attachment-warning-modal {
+      z-index: 99999 !important;
+    }
+    .email-attachment-warning-modal .modal-content {
+      position: relative;
+      z-index: 100001;
+    }
+    .styled-confirm-box {
+      width:360px; max-width:90vw;
+      max-height:none; /* override modal-content's 85vh */
+      padding:14px 18px;
+    }
+    .styled-confirm-box .modal-header { margin-bottom:4px; }
+    .styled-confirm-box .modal-body p {
+      margin:8px 0 12px; color:var(--fg); font-size:0.92rem; line-height:1.45;
+      white-space:pre-line;
+    }
+    .styled-confirm-box .modal-footer {
+      display:flex; justify-content:flex-end; gap:8px; padding-top:6px;
+      border-top:1px solid var(--border); margin-top:4px;
+    }
+    @media (max-width:768px) {
+      .styled-confirm-box {
+        width: 85vw;
+        padding: 12px 16px;
+        font-size: 0.88rem;
+        border-radius: 12px;
+      }
+      .styled-confirm-box .modal-header h4 { font-size: 0.9rem; }
+      .styled-confirm-box .modal-body p { font-size: 0.85rem; margin: 6px 0 10px; }
+      .styled-confirm-box .modal-footer { gap: 10px; }
+      .styled-confirm-box .confirm-btn {
+        flex: 1;
+        /* More bottom than top padding nudges the label up ~2px so it isn't
+           sitting low in the taller mobile buttons. */
+        padding: 8px 12px 12px;
+        font-size: 0.85rem;
+        border-radius: 8px;
+        text-align: center;
+      }
+    }
+    .confirm-btn {
+      /* Asymmetric padding nudges the label UP ~2px from where it was (more
+         bottom than top padding), so the confirm-dialog text isn't sitting low. */
+      padding:3px 16px 5px; border-radius:4px; font-size:0.85rem;
+      cursor:pointer; border:1px solid var(--border);
+      font-family:inherit;
+    }
+    @media (max-width: 820px) {
+      /* Mobile: flip the asymmetry to shift the text 1 px UP from
+         centre instead (the bigger touch targets on mobile read better
+         with the label sitting slightly high). */
+      .confirm-btn { padding:2px 16px 4px; }
+    }
+    .confirm-btn-secondary { background:var(--bg); color:var(--fg); }
+    .confirm-btn-secondary:hover { background:var(--border); }
+    #styled-confirm-cancel .styled-confirm-cancel-label {
+      position:relative;
+      top:2px;
+    }
+    .confirm-btn-primary { background:var(--accent-primary, var(--red, #4a9eff)); color:#fff; border-color:transparent; }
+    .confirm-btn-primary:hover { filter:brightness(1.15); }
+    .confirm-btn-danger { background:var(--color-danger); color:#fff; border-color:transparent; }
+    .confirm-btn-danger:hover { background:var(--color-error); }
+        .cookbook-gguf-delete-box {
+      width:560px;
+      max-width:92vw;
+    }
+    .cookbook-gguf-delete-list {
+      display:flex;
+      flex-direction:column;
+      gap:6px;
+      max-height:42vh;
+      overflow:auto;
+      padding:2px 2px 4px;
+    }
+    .cookbook-gguf-delete-row {
+      display:grid;
+      grid-template-columns:18px minmax(0,1fr);
+      gap:7px 8px;
+      align-items:start;
+      padding:7px 8px;
+      border:1px solid var(--border);
+      border-radius:7px;
+      background:color-mix(in srgb, var(--panel, var(--bg)) 92%, var(--fg) 8%);
+      cursor:pointer;
+    }
+    .cookbook-gguf-delete-row:hover {
+      border-color:color-mix(in srgb, var(--accent-primary, var(--fg)) 45%, var(--border));
+    }
+    .cookbook-gguf-delete-cb {
+      -webkit-appearance:none;
+      appearance:none;
+      width:8px !important;
+      height:8px !important;
+      min-width:8px;
+      min-height:8px;
+      padding:0;
+      margin:4px 0 0;
+      border:1px solid var(--border);
+      border-radius:50%;
+      background:transparent;
+      box-sizing:content-box;
+      cursor:pointer;
+      transition:background 0.15s, border-color 0.15s, transform 0.12s;
+    }
+    .cookbook-gguf-delete-cb:hover {
+      border-color:var(--accent, var(--red));
+      transform:scale(1.12);
+    }
+    .cookbook-gguf-delete-cb:checked {
+      background:var(--accent, var(--red));
+      border-color:var(--accent, var(--red));
+    }
+    .cookbook-gguf-delete-main,
+    .cookbook-gguf-delete-path {
+      min-width:0;
+      overflow:hidden;
+      text-overflow:ellipsis;
+      white-space:nowrap;
+    }
+    .cookbook-gguf-delete-main {
+      font-size:0.86rem;
+      color:var(--fg);
+    }
+    .cookbook-gguf-delete-path {
+      grid-column:2;
+      margin-top:-2px;
+      font-size:0.74rem;
+      opacity:0.58;
+    }
+        /* Styled prompt — text-input dialog (used in place of window.prompt) */
+    #styled-prompt-overlay {
+      background:rgba(0,0,0,0.5);
+      backdrop-filter:blur(4px);
+      pointer-events:auto !important;
+      z-index: 99999 !important;
+      position: fixed !important;
+      top: 0 !important;
+      left: 0 !important;
+      width: 100% !important;
+      height: 100% !important;
+    }
+    #styled-prompt-overlay .modal-content {
+      position: relative;
+      z-index: 10001;
+    }
+    .styled-prompt-box { width: min(400px, 92vw); max-width: 100%; box-sizing: border-box; }
+    .styled-prompt-box .modal-body { padding-top: 4px; }
+    .styled-prompt-input {
+      width:100%;
+      box-sizing:border-box;
+      margin-top:8px;
+      padding:9px 12px;
+      border:1px solid var(--border);
+      border-radius:6px;
+      background:var(--bg);
+      color:var(--fg);
+      font:inherit;
+      font-size:0.95rem;
+      outline:none;
+      transition: border-color 0.15s, box-shadow 0.15s;
+    }
+    .styled-prompt-input:focus {
+      border-color: var(--accent-primary, var(--red, #4a9eff));
+      box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-primary, var(--red, #4a9eff)) 25%, transparent);
+    }
+    @media (max-width:768px) {
+      .styled-prompt-box { width: 85vw; }
+      .styled-prompt-input { font-size: 0.9rem; padding: 10px 12px; }
+    }
+    /* Scroll navigation buttons */
+    .scroll-nav-btn {
+      position:fixed;
+      background:var(--panel);
+      color: var(--accent);
+      border:none;
+      border-radius:10px;
+      width:38px; height:38px;
+      padding:0;
+      display:flex; align-items:center; justify-content:center;
+      font-size:14px;
+      line-height:1;
+      font-family:inherit;
+      cursor:pointer;
+      opacity:0;
+      pointer-events:none;
+      transition: opacity .2s, transform .3s cubic-bezier(0.25, 1, 0.5, 1), background .15s;
+      z-index:100;
+      transform: translateY(0);
+    }
+    .scroll-nav-btn::before {
+      content: '';
+      position: absolute;
+      inset: 0;
+      border-radius: 10px;
+      padding: 1px;
+      background: linear-gradient(to bottom, var(--border), color-mix(in srgb, var(--border) 30%, transparent));
+      -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+      mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+      pointer-events: none;
+    }
+    .scroll-nav-btn.agent-working::after,
+    body.chat-agent-working #scroll-bottom-btn.show::after {
+      content: '';
+      position: absolute;
+      inset: 0;
+      border-radius: 10px;
+      padding: 1px;
+      background: conic-gradient(
+        from 0deg,
+        transparent 0deg,
+        transparent 245deg,
+        color-mix(in srgb, var(--accent, var(--red)) 78%, transparent) 315deg,
+        transparent 360deg
+      );
+      -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+      mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
+      -webkit-mask-composite: xor;
+      mask-composite: exclude;
+      opacity: 0.65;
+      pointer-events: none;
+      animation: scrollAgentWorkingRing 1.05s linear infinite;
+    }
+    @keyframes scrollAgentWorkingRing {
+      to { transform: rotate(360deg); }
+    }
+    #scroll-bottom-btn.show { opacity:1; pointer-events:auto; }
+    .scroll-nav-caret {
+      display: inline-block;
+      font-family: Arial, Helvetica, sans-serif;
+      font-size: 16px;
+      line-height: 1;
+      transform: translateY(-1px);
+      pointer-events: none;
+    }
+    @media (hover: hover) and (pointer: fine) {
+      #scroll-bottom-btn.show:hover {
+        border-color: color-mix(in srgb, var(--fg) 25%, transparent);
+      }
+    }
+    #scroll-bottom-btn.slide-out {
+      transform: translateY(20px);
+      opacity: 0 !important;
+      pointer-events: none;
+    }
+    /* Focus outline for accessibility */
+    :focus-visible {
+      outline: 2px solid var(--red);
+      outline-offset: 2px;
+    }
+    /* Hamburger menu button */
+    .hamburger {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 32px;
+      height: 32px;
+      background: none;
+      border: 1px solid transparent;
+      border-radius: 6px;
+      padding: 0;
+      cursor: pointer;
+      transition: background 0.15s, border-color 0.15s;
+    }
+    .hamburger:hover {
+      background: color-mix(in srgb, var(--fg) 7%, transparent);
+      border-color: var(--border);
+    }
+    .hamburger span {
+      display: block;
+      width: 16px;
+      height: 2px;
+      background: var(--fg);
+      border-radius: 1px;
+      transition: transform 0.2s, opacity 0.2s;
+    }
+    .hamburger span + span { margin-top: 3px; }
+    /* Agent indicator */
+    #agent-indicator {
+      position: fixed;
+      top: 20px;
+      right: 20px;
+      background: var(--bg);
+      color: var(--fg);
+      border: 1px solid var(--border);
+      padding: 6px 12px;
+      border-radius: 6px;
+      font-size: 12px;
+      display: none;
+      z-index: 100;
+      cursor: pointer;
+      transition: all 0.2s ease;
+    }
+    #agent-indicator.active {
+      display: block;
+      border-color: var(--color-agent-active);
+      box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
+    }
+    #agent-indicator:hover {
+      border-color: var(--color-agent-active);
+      background: var(--panel);
+    }
+    #research-toggle-btn:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+
+/* ── Drag & Drop ── */
+
+/* ---------- Color palette (dark / light) ---------- */
+
+    /* Drag and drop styling */
+    .section[dnd-active="true"] {
+      background: color-mix(in srgb, var(--red) 10%, transparent) !important;
+      border-color: var(--red) !important;
+    }
+
+    .section[dnd-over="true"] {
+      background: color-mix(in srgb, var(--red) 20%, transparent) !important;
+      border-color: var(--red) !important;
+      transform: scale(1.02);
+    }
+
+    .drag-handle {
+      cursor: grab;
+      opacity: 0.5;
+      padding: 0 6px;
+      user-select: none;
+    }
+
+    .drag-handle:hover {
+      opacity: 0.8;
+    }
+
+    .drag-handle:active {
+      cursor: grabbing;
+    }
+
+/* ── UI Controls (Radio, Presets, Toolbar, Settings) ── */
+
+
+    /* Custom radio button styling */
+    .radio-option {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      padding: 8px;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      background: color-mix(in srgb, var(--fg) 6%, transparent);
+      cursor: pointer;
+      transition: all 0.2s ease;
+    }
+
+    .radio-option:hover {
+      background: color-mix(in srgb, var(--fg) 9%, transparent);
+      border-color: var(--fg);
+    }
+
+    .radio-option input[type="radio"] {
+      appearance: none;
+      -webkit-appearance: none;
+      width: 18px;
+      height: 18px;
+      border: 2px solid var(--border);
+      border-radius: 50%;
+      outline: none;
+      margin: 0;
+      background: var(--bg);
+      transition: all 0.2s ease;
+      position: relative;
+      flex-shrink: 0;
+    }
+
+    .radio-option input[type="radio"]:checked {
+      border-color: var(--red);
+      background: var(--red);
+    }
+
+    .radio-option input[type="radio"]:focus {
+      box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 30%, transparent);
+    }
+
+    .radio-label {
+      color: var(--fg);
+      font-size: 14px;
+      user-select: none;
+    }
+
+    /* Preset buttons */
+    .preset-btn {
+      height: 27.2px; /* 15% smaller than 32px */
+      padding: 0 8.5px; /* 15% smaller than 10px */
+      margin-left: 4px;
+      border: 1px solid var(--border);
+      border-radius: 4px;
+      background: var(--bg);
+      color: var(--fg);
+      font-family: 'Fira Code', monospace;
+      font-size: 10.2px; /* 15% smaller than 12px */
+      cursor: pointer;
+      transition: all 0.2s ease;
+    }
+
+    .preset-btn:hover {
+      background: var(--panel);
+      border-color: var(--fg);
+    }
+
+    .preset-btn.active {
+      background: var(--panel);
+      border-color: var(--fg);
+      box-shadow: 0 0 0 1px var(--fg), 0 0 8px color-mix(in srgb, var(--fg) 16%, transparent);
+      font-weight: 600;
+    }
+
+    /* All preset buttons use the same blue color */
+    .preset-btn {
+      border-color: var(--red); /* Blue color */
+    }
+
+    .preset-btn.active {
+      border-color: var(--red); /* Blue color when active */
+      box-shadow: 0 0 0 1px var(--red), 0 0 8px color-mix(in srgb, var(--red) 30%, transparent);
+    }
+
+    /* Custom preset modal: the base .preset-modal-content sets overflow:hidden
+       (for desktop rounded-corner clipping), which on the mobile sheet clipped
+       the footer (Start/Cancel) off the bottom with no way to reach it. Let the
+       whole sheet scroll — same as every other mobile modal (.modal-content is
+       overflow-y:auto on mobile) — so the footer is always reachable. No flex
+       changes, so the body can't collapse. */
+    #custom-preset-modal .preset-modal-content {
+      overflow-y: auto !important;
+    }
+    #custom-preset-modal .modal-body label {
+      font-size: 13px;
+      font-weight: 500;
+      color: var(--fg);
+      margin-top: 8px;
+      margin-bottom: 4px;
+      display: block;
+    }
+
+    #custom-preset-modal .modal-body input,
+    #custom-preset-modal .modal-body textarea {
+      width: 100%;
+      margin-bottom: 8px;
+      box-sizing: border-box;
+    }
+    #custom-preset-modal .modal-body textarea,
+    #custom-preset-modal .modal-body input[type="text"] {
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      color: var(--fg);
+      padding: 8px 10px;
+      font-size: 13px;
+      font-family: inherit;
+      transition: border-color 0.15s;
+    }
+    #custom-preset-modal .modal-body textarea {
+      resize: vertical;
+    }
+    #custom-preset-modal .modal-body textarea:focus,
+    #custom-preset-modal .modal-body input[type="text"]:focus {
+      outline: none;
+      border-color: var(--red);
+    }
+
+    #custom-preset-modal .modal-footer {
+      display: flex;
+      justify-content: flex-end;
+      gap: 8px;
+      margin-top: 12px;
+      padding-top: 10px;
+      border-top: 1px solid var(--border);
+    }
+
+    #custom-preset-modal .modal-footer button {
+      padding: 7px 14px;
+      border-radius: 6px;
+      font-size: 12px;
+      font-weight: 500;
+      border: 1px solid var(--border);
+      background: none;
+      color: var(--fg);
+      cursor: pointer;
+      transition: all 0.15s;
+    }
+    #custom-preset-modal .modal-footer button:hover {
+      background: color-mix(in srgb, var(--fg) 8%, transparent);
+    }
+
+    #custom-preset-modal .modal-footer button#save-custom-preset {
+      /* The theme's accent is stored in --red (theme.js sets --red = accentHex)
+         and --accent is undefined, so the canonical accent expression is
+         var(--accent, var(--red)). The old bare var(--accent) resolved to nothing
+         which, with color:var(--bg), made the button invisible. */
+      background: var(--accent, var(--red));
+      color: var(--bg);
+      border-color: var(--accent, var(--red));
+    }
+    #custom-preset-modal .modal-footer button#save-custom-preset:hover {
+      opacity: 0.85;
+    }
+    /* Toolbar visibility tab */
+    .toolbar-hint {
+      font-size: 12px;
+      color: color-mix(in srgb, var(--fg) 55%, transparent);
+      margin-bottom: 12px;
+    }
+    /* ── Appearance visibility toggles ── */
+    .vis-toggles {
+      display: flex;
+      flex-direction: column;
+    }
+    .vis-row {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      cursor: pointer;
+      padding: 5px 6px;
+      border-radius: 4px;
+      transition: background 0.12s;
+    }
+    .vis-row:hover {
+      background: color-mix(in srgb, var(--fg) 5%, transparent);
+    }
+    .vis-row input[type="checkbox"] {
+      display: none;
+    }
+    .vis-icon {
+      width: 20px;
+      height: 20px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      color: color-mix(in srgb, var(--fg) 40%, transparent);
+    }
+    .vis-icon-text {
+      font-size: 10px;
+      font-weight: 700;
+      font-family: inherit;
+      letter-spacing: -0.5px;
+    }
+    .vis-label {
+      flex: 1;
+      font-size: 12px;
+      color: var(--fg);
+      user-select: none;
+    }
+    .vis-switch {
+      position: relative;
+      width: 30px;
+      height: 16px;
+      background: color-mix(in srgb, var(--fg) 15%, transparent);
+      border-radius: 8px;
+      transition: background 0.2s;
+      flex-shrink: 0;
+    }
+    .vis-switch::after {
+      content: '';
+      position: absolute;
+      top: 2px;
+      left: 2px;
+      width: 12px;
+      height: 12px;
+      background: var(--panel);
+      border-radius: 50%;
+      transition: transform 0.2s;
+      box-shadow: 0 1px 2px rgba(0,0,0,0.25);
+    }
+    .vis-row input:checked + .vis-switch {
+      background: var(--red);
+    }
+    .vis-row input:checked + .vis-switch::after {
+      transform: translateX(14px);
+    }
+    .vis-row input:checked ~ .vis-icon,
+    .vis-row:has(input:checked) .vis-icon {
+      color: var(--fg);
+    }
+    .persona-display-name-row .vis-label {
+      position: relative;
+      top: -4px;
+    }
+    .persona-display-name-row .vis-switch {
+      position: relative;
+      left: 4px;
+    }
+    .md-attach-paperclip-icon {
+      position: relative;
+      top: 1px;
+    }
+    /* Compare model selector — match the calendar modal's clean header
+       (no border underline). */
+    #compare-model-overlay .modal-header h4 {
+      pointer-events: none;
+    }
+    /* Compare model selector: keep manually-resized/tiny windows contained.
+       Picker dropdowns are appended to document.body, so the card itself can
+       clip and scroll without cropping the dropdown list. */
+    #compare-model-overlay .modal-content {
+      display: flex;
+      flex-direction: column;
+      max-height: min(720px, calc(100dvh - 48px));
+      overflow: hidden;
+      min-height: 180px;
+    }
+    #compare-model-overlay .modal-body {
+      overflow: auto;
+      flex: 1 1 auto;
+      min-height: 0;
+    }
+    .vis-hint {
+      font-size: 10px;
+      color: color-mix(in srgb, var(--fg) 30%, transparent);
+      font-weight: 400;
+      margin-left: 2px;
+    }
+    /* Settings toggle — admin-only lock indicator */
+    .ui-vis-lock {
+      display: none;
+    }
+    /* (legacy toolbar-toggle styles removed — now using .vis-* classes) */
+
+    /* Demo highlight pulse */
+    .odysseus-highlight {
+      outline: 2px solid var(--accent, var(--red)) !important;
+      outline-offset: 1px;
+      box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+      animation: ody-pulse 1.5s ease-in-out infinite;
+      z-index: 100;
+      position: relative;
+    }
+    @keyframes ody-pulse {
+      0%, 100% { outline-color: var(--accent, var(--red)); box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
+      50%       { outline-color: color-mix(in srgb, var(--accent, var(--red)) 40%, transparent); box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 8%, transparent); }
+    }
+    /* Floating breathing halo. Rendered as a body-level div positioned over
+       the target, so we don't fight the target's own outline / box-shadow /
+       overflow chain. JS keeps it in sync with the target's bounding rect. */
+    .tour-halo {
+      position: fixed;
+      pointer-events: none;
+      border: 3px solid var(--accent, var(--red));
+      border-radius: 10px;
+      z-index: 10000;
+      animation: ody-breathe 1.4s ease-in-out infinite;
+      opacity: 0;
+      transform: scale(0.94);
+      transition: opacity 0.35s ease-out, transform 0.35s ease-out;
+    }
+    .tour-halo.tour-fade-in {
+      opacity: 1;
+      transform: scale(1);
+    }
+    @keyframes ody-breathe {
+      0%, 100% {
+        box-shadow: 0 0 0 3px  color-mix(in srgb, var(--accent, var(--red)) 55%, transparent),
+                    0 0 22px 4px color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
+        border-color: var(--accent, var(--red));
+      }
+      50% {
+        box-shadow: 0 0 0 6px  color-mix(in srgb, var(--accent, var(--red)) 30%, transparent),
+                    0 0 40px 14px color-mix(in srgb, var(--accent, var(--red)) 70%, transparent);
+        border-color: color-mix(in srgb, var(--accent, var(--red)) 80%, transparent);
+      }
+    }
+    /* While the tour is active, lift overflow:hidden on common clipping
+       ancestors so the halo around a highlighted child isn't cropped. */
+    body.tour-active .sidebar,
+    body.tour-active .sidebar-inner,
+    body.tour-active .chat-input-bar,
+    body.tour-active .chat-input-top,
+    body.tour-active .chat-input-wrap,
+    body.tour-active .chat-input-right,
+    body.tour-active .mode-toggle,
+    body.tour-active .model-picker-wrap {
+      overflow: visible !important;
+    }
+
+    /* ── Secret tour hint (drag-to-snap on first modal open) ── */
+    .tour-hint {
+      position: fixed;
+      z-index: 10002;
+      background: var(--bg);
+      color: var(--fg);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 12px 14px 10px;
+      width: 240px;
+      font-size: 0.78rem;
+      line-height: 1.5;
+      box-shadow: 0 6px 20px rgba(0, 0, 0, 0.32);
+      opacity: 0;
+      transform: translateY(-4px);
+      transition: opacity 0.28s ease-out, transform 0.28s ease-out;
+      pointer-events: auto;
+    }
+    .tour-hint.tour-hint-in  { opacity: 1; transform: translateY(0); }
+    .tour-hint.tour-hint-out { opacity: 0; transform: translateY(-4px); }
+    .tour-hint-visual {
+      display: flex;
+      justify-content: center;
+      margin-bottom: 8px;
+      color: var(--accent, var(--red));
+    }
+    .tour-hint-visual svg { display: block; }
+    .tour-hint-text { margin-bottom: 10px; opacity: 0.92; }
+    .tour-hint-text b { color: var(--accent, var(--red)); font-weight: 600; }
+    .tour-hint-dismiss {
+      display: block;
+      margin: 0 0 0 auto;
+      background: none;
+      border: 1px solid var(--border);
+      color: var(--fg);
+      border-radius: 6px;
+      padding: 3px 12px;
+      cursor: pointer;
+      font-family: inherit;
+      font-size: 0.72rem;
+      opacity: 0.85;
+      transition: opacity 0.15s, background 0.15s;
+    }
+    .tour-hint-dismiss:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 8%, transparent); }
+
+    /* SVG dance: cursor approaches title bar, drags right, modal snaps to a
+       right-half zone, holds, returns. 3.2s loop. */
+    .th-cursor { animation: th-cursor 3.2s ease-in-out infinite; transform-origin: 0 0; }
+    .th-modal-group { animation: th-modal-slide 3.2s ease-in-out infinite; transform-origin: 39px 31px; }
+    .th-zone { animation: th-zone-flash 3.2s ease-in-out infinite; }
+    @keyframes th-cursor {
+      0%, 5%    { transform: translate(35px, 32px); }
+      18%       { transform: translate(35px, 22px); }
+      48%       { transform: translate(80px, 22px); }
+      72%       { transform: translate(80px, 22px); }
+      90%, 100% { transform: translate(35px, 32px); }
+    }
+    @keyframes th-modal-slide {
+      0%, 18%   { transform: translate(0, 0) scale(1, 1); }
+      48%       { transform: translate(45px, 0) scale(1, 1); }
+      58%       { transform: translate(30px, -19px) scale(1.4, 2.4); }
+      72%       { transform: translate(30px, -19px) scale(1.4, 2.4); }
+      90%, 100% { transform: translate(0, 0) scale(1, 1); }
+    }
+    @keyframes th-zone-flash {
+      0%, 38%   { opacity: 0; }
+      48%       { opacity: 0.22; }
+      58%, 72%  { opacity: 0; }
+      100%      { opacity: 0; }
+    }
+    @media (prefers-reduced-motion: reduce) {
+      .th-cursor, .th-modal-group, .th-zone { animation: none !important; }
+    }
+    .odysseus-hl-label {
+      position: absolute;
+      top: -22px;
+      left: 8px;
+      background: var(--red);
+      color: var(--bg);
+      font-size: 0.7rem;
+      padding: 2px 8px;
+      border-radius: 4px;
+      white-space: nowrap;
+      z-index: 101;
+      pointer-events: none;
+    }
+
+    /* Generated images inside chat bubbles */
+    .msg.generated-image-wrap .body { text-align: center; }
+    .generated-image {
+      max-width: 100%;
+      max-height: 512px;
+      border-radius: 8px;
+      cursor: pointer;
+      transition: transform 0.2s;
+      display: inline-block;
+      margin: 0 auto;
+    }
+    .generated-image:hover { transform: scale(1.02); }
+    .generated-image-caption {
+      font-size: 0.8rem;
+      opacity: 0.5;
+      margin-top: 6px;
+      font-style: italic;
+      text-align: center;
+    }
+
+    /* Setup wizard */
+    .setup-wizard { padding: 16px; max-width: 500px; }
+    .setup-wizard .setup-title { font-size: 1.1rem; font-weight: 600; margin-bottom: 12px; color: var(--fg); }
+    .setup-wizard .setup-label { font-size: 0.85rem; color: var(--fg); opacity: 0.7; margin-bottom: 8px; }
+    .setup-wizard .setup-presets { display: flex; flex-wrap: wrap; gap: 8px; }
+    .setup-wizard .setup-preset-btn {
+      padding: 8px 14px; border: 1px solid var(--border); border-radius: 6px;
+      background: var(--panel); color: var(--fg); cursor: pointer;
+      font-size: 0.85rem; transition: all 0.2s;
+    }
+    .setup-wizard .setup-preset-btn:hover { border-color: var(--red); background: color-mix(in srgb, var(--red) 11%, transparent); }
+    .setup-wizard .setup-input {
+      display: block; width: 100%; padding: 8px 12px; margin-bottom: 8px;
+      background: var(--panel); border: 1px solid var(--border);
+      border-radius: 6px; color: var(--fg); font-size: 0.85rem; box-sizing: border-box;
+    }
+    .setup-wizard .setup-input:focus { border-color: var(--red); outline: none; }
+    .setup-wizard .setup-connect-btn {
+      padding: 8px 20px; background: var(--red); color: var(--bg);
+      border: none; border-radius: 6px; cursor: pointer; font-weight: 600; margin-top: 4px;
+    }
+    .setup-wizard .setup-connect-btn:hover { opacity: 0.9; }
+    .setup-wizard .setup-status { font-size: 0.8rem; margin-top: 8px; color: var(--fg); opacity: 0.7; }
+    .setup-wizard .setup-model-list { display: flex; flex-wrap: wrap; gap: 8px; }
+    .setup-wizard .setup-model-btn {
+      padding: 8px 14px; border: 1px solid var(--border); border-radius: 6px;
+      background: var(--panel); color: var(--fg); cursor: pointer;
+      font-size: 0.85rem; transition: all 0.2s;
+    }
+    .setup-wizard .setup-model-btn:hover { border-color: var(--red); background: color-mix(in srgb, var(--red) 11%, transparent); }
+    .setup-wizard .setup-step.hidden { display: none; }
+
+    /* Dropdown menu styles */
+    .dropdown {
+      position: absolute;
+      top: calc(100% + 6px);
+      right: 0;
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3), 0 0 0 1px color-mix(in srgb, var(--fg) 5%, transparent);
+      z-index: 1000;
+      display: none;
+      min-width: 220px;
+      padding: 6px;
+      backdrop-filter: blur(12px);
+    }
+
+    .dropdown.show {
+      display: block;
+      animation: dropdown-in 0.15s ease-out;
+    }
+    @keyframes dropdown-in {
+      from { opacity:0; transform:translateY(-6px) scale(0.97); }
+      to { opacity:1; transform:translateY(0) scale(1); }
+    }
+
+    .dropdown-item {
+      padding: 8px 10px;
+      border-radius: 6px;
+      cursor: pointer;
+      transition: background 0.12s ease;
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+    }
+
+    .dropdown-item:last-child {
+      border-bottom: none;
+    }
+
+    .dropdown-item .menu-icon {
+      width: 28px;
+      height: 28px;
+      border-radius: 6px;
+      background: color-mix(in srgb, var(--fg) 5%, transparent);
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 14px;
+      flex-shrink: 0;
+    }
+
+    .dropdown-item .menu-text h4 {
+      margin: 0;
+      font-size: 12.5px;
+      font-weight: 500;
+      color: var(--fg);
+      line-height: 1.3;
+    }
+
+    .dropdown-item .menu-text p {
+      margin: 0;
+      font-size: 10.5px;
+      color: var(--color-subheader);
+      line-height: 1.3;
+    }
+
+    .dropdown-item:hover {
+      background: color-mix(in srgb, var(--red) 10%, transparent);
+    }
+    .dropdown-item:hover .menu-icon {
+      background: color-mix(in srgb, var(--red) 15%, transparent);
+    }
+
+    /* Compact dropdown items (session/model context menus) */
+    .dropdown-item-compact {
+      cursor: pointer;
+      padding: 6px 8px;
+      font-size: 11px;
+      border-radius: 8px;
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      color: var(--fg);
+      transition: background 0.1s;
+    }
+    .dropdown-item-compact:hover {
+      background: color-mix(in srgb, var(--accent) 10%, transparent);
+    }
+    .dropdown-item-compact .dropdown-icon {
+      width: 14px;
+      height: 14px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      opacity: 0.5;
+    }
+    .dropdown-item-compact .dropdown-icon svg {
+      width: 14px;
+      height: 14px;
+    }
+    .dropdown-item-compact .dropdown-shortcut {
+      margin-left: auto;
+      font-size: 9.5px;
+      opacity: 0.35;
+      font-weight: 400;
+    }
+    /* Keyboard shortcut hints (⌘+Alt+D etc.) are meaningless on touch —
+       hide them in the per-chat actions menu on mobile. */
+    @media (max-width: 768px) {
+      .session-dropdown .dropdown-shortcut { display: none; }
+    }
+    .dropdown-item-danger {
+      color: var(--red) !important;
+    }
+    .dropdown-item-danger .dropdown-icon {
+      opacity: 0.7;
+    }
+
+    /* Inline rename input for sessions */
+    .session-rename-input {
+      width: 100%;
+      background: var(--bg);
+      color: var(--fg);
+      border: 1px solid var(--accent, var(--accent-primary));
+      border-radius: 4px;
+      padding: 2px 6px;
+      font-family: inherit;
+      font-size: 12px;
+      line-height: 1.3;
+      outline: none;
+    }
+
+    /* Session dropdown container */
+    .session-dropdown-menu {
+      position: fixed;
+      z-index: 1000;
+      display: none;
+      min-width: auto;
+      width: max-content;
+      padding: 4px;
+      animation: dropdown-in 0.15s ease-out;
+    }
+
+    /* Folder move submenu */
+    .session-folder-submenu {
+      position: fixed;
+      z-index: 1001;
+      display: none;
+      min-width: auto;
+      width: max-content;
+      padding: 4px;
+    }
+
+    .dropdown-divider {
+      height: 1px;
+      background: var(--border);
+      margin: 4px 8px;
+    }
+
+    /* Search toggle styles */
+    .search-toggle {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 6px 0;
+      border-bottom: 1px solid var(--border);
+      margin-bottom: 8px;
+    }
+
+    .search-provider-label {
+      font-size: 14px;
+      color: var(--fg);
+    }
+
+/* ── Voice, Search, Themes, Comparison, Censor, Print ── */
+
+    /* Voice recording styles */
+    #mic-btn {
+      color: var(--fg);
+      border: 1px solid var(--border);
+      border-radius: 4px;
+      transition: all 0.2s ease;
+    }
+
+    #mic-btn:hover {
+      background: color-mix(in srgb, var(--fg) 6%, transparent);
+      border-color: var(--fg);
+    }
+
+    #mic-btn.recording {
+      background: var(--color-recording);
+      border-color: var(--color-recording);
+      animation: pulse 1.5s infinite;
+    }
+
+
+
+    #recording-indicator {
+      position: fixed;
+      top: 10px;
+      left: 0;
+      right: 0;
+      background: rgba(0, 0, 0, 0.8);
+      backdrop-filter: blur(10px);
+      border: 1px solid var(--border);
+      border-radius: 8px;
+      padding: 12px;
+      margin: 10px;
+      z-index: 1000;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+    }
+
+    #recording-indicator.hidden {
+      display: none !important;
+    }
+
+    .recording-content {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+      color: white;
+    }
+
+    .recording-icon {
+      color: var(--color-recording);
+      font-size: 20px;
+      animation: pulse 1.5s infinite;
+    }
+
+    .recording-text {
+      font-size: 16px;
+      font-weight: 500;
+    }
+
+    #stop-recording {
+      background: var(--color-recording);
+      color: white;
+      border: none;
+      border-radius: 6px;
+      padding: 6px 12px;
+      font-size: 14px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: background 0.2s ease;
+    }
+
+    #stop-recording:hover {
+      background: var(--color-recording-hover);
+    }
+
+    /* Error state for recording */
+    #recording-indicator.error {
+      background: rgba(173, 26, 26, 0.9);
+    }
+
+    .recording-error {
+      color: var(--color-recording);
+      font-size: 14px;
+      margin-top: 4px;
+    }
+
+    @media (max-width: 768px) {
+      #recording-indicator {
+        margin: 8px;
+        padding: 10px;
+      }
+
+      .recording-text {
+        font-size: 14px;
+      }
+
+      #stop-recording {
+        padding: 6px 10px;
+        font-size: 12px;
+      }
+    }
+/* SYNTAX HIGHLIGHTING — uses theme vars from style.css, no hardcoded overrides */
+.hljs { color: var(--hl-fg, #9cdef2); background: none !important; }
+pre { background: var(--code-bg, var(--hl-bg, #282c34)) !important; }
+
+    /* ---------- Search overlay (Ctrl+K command palette) ---------- */
+    .search-overlay {
+      position: fixed;
+      top: 0; left: 0; width: 100%; height: 100%;
+      background: rgba(0, 0, 0, 0.6);
+      display: flex;
+      align-items: flex-start;
+      justify-content: center;
+      padding-top: var(--search-overlay-top, 15vh);
+      z-index: 300;
+      backdrop-filter: blur(6px);
+    }
+    .search-overlay.hidden { display: none; }
+    .search-popup {
+      width: 520px;
+      max-width: 90vw;
+      background: var(--bg);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      max-height: 60vh;
+    }
+    .search-popup input#search-input {
+      width: 100%;
+      background: transparent;
+      border: none;
+      border-bottom: 1px solid var(--border);
+      outline: none;
+      color: var(--fg);
+      font-size: 16px;
+      font-family: 'Fira Code', monospace;
+      padding: 14px 16px;
+      box-sizing: border-box;
+    }
+    .search-popup input#search-input::placeholder {
+      color: color-mix(in srgb, var(--fg) 30%, transparent);
+    }
+    .search-results {
+      overflow-y: auto;
+      flex: 1;
+      padding: 4px;
+    }
+    .search-results:empty {
+      display: none;
+    }
+    .search-group-header {
+      font-size: 10px;
+      font-weight: 600;
+      color: var(--color-subheader);
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+      padding: 10px 12px 4px;
+    }
+    .search-result-item {
+      display: flex;
+      align-items: baseline;
+      gap: 8px;
+      padding: 8px 12px;
+      border-radius: 6px;
+      cursor: pointer;
+      transition: background 0.1s;
+    }
+    .search-result-item:hover,
+    .search-result-item.selected {
+      background: color-mix(in srgb, var(--red) 10%, transparent);
+    }
+    .search-result-role {
+      font-size: 10px;
+      font-weight: 600;
+      color: var(--color-subheader);
+      flex-shrink: 0;
+      min-width: 24px;
+    }
+    .search-result-snippet {
+      flex: 1;
+      font-size: 12px;
+      color: var(--fg);
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .search-result-time {
+      font-size: 10px;
+      color: color-mix(in srgb, var(--fg) 35%, transparent);
+      flex-shrink: 0;
+      white-space: nowrap;
+    }
+    mark.search-highlight {
+      /* Was orange-on-orange (0.35 alpha bg + orange text) — too low-contrast
+         to read. Solid accent fill with bg-colored text reads clearly. */
+      background: var(--accent, #e8a830);
+      color: var(--bg, #1a1a1a);
+      border-radius: 2px;
+      padding: 0 2px;
+      font-weight: 600;
+    }
+            .search-empty {
+      text-align: center;
+      color: color-mix(in srgb, var(--fg) 35%, transparent);
+      padding: 24px;
+      font-size: 13px;
+    }
+
+    /* ---------- Theme popup (uses .modal > .modal-content frame) ---------- */
+    #theme-modal { z-index: 260; }
+    #theme-popup .theme-popup-sub { color: color-mix(in srgb, var(--fg) 50%, transparent); font-size: 11px; margin-bottom: 10px; }
+    /* `max-height` instead of a fixed height so the popup shrinks to fit its
+       content (avoids the leftover whitespace after the time-based switching
+       card was removed). The inner .theme-tab-panel keeps overflow:auto, so
+       any taller content still scrolls inside. */
+    #theme-popup { overflow-y: hidden; max-height: min(85vh, 600px); }
+    #theme-popup .modal-header { flex-shrink: 0; }
+    #theme-popup .admin-tabs { margin: 0 -10px 8px; padding: 0 10px; flex-shrink: 0; }
+    .theme-tab-panel { overflow-y: auto; min-height: 0; flex: 1; padding-bottom: 10px; }
+
+    .theme-grid {
+      display: grid; grid-template-columns: repeat(auto-fill, minmax(66px, 1fr));
+      gap: 6px; margin-bottom: 12px;
+    }
+    .theme-swatch {
+      border: 2px solid var(--border); border-radius: 8px; cursor: pointer;
+      padding: 5px; text-align: center; font-size: 0.65rem; color: var(--fg);
+      transition: border-color 0.15s, transform 0.15s;
+    }
+    .theme-swatch:hover { transform: scale(1.06); }
+    .theme-swatch.active { border-color: var(--red); box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 33%, transparent); }
+    .theme-swatch-colors {
+      display: flex; justify-content: center; margin-bottom: 3px;
+    }
+    .theme-swatch-colors span {
+      width: 15px; height: 15px; border-radius: 50%;
+      margin-left: -5px;
+      border: 1.5px solid color-mix(in srgb, var(--fg) 12%, transparent);
+    }
+    .theme-swatch-colors span:first-child { margin-left: 0; }
+    .theme-custom-label { font-size: 11px; font-weight: 600; color: color-mix(in srgb, var(--fg) 50%, transparent); text-transform: uppercase; letter-spacing: 0.06em; margin: 10px 0 6px; }
+    .theme-custom { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px; }
+    .color-row { display: flex; align-items: center; gap: 4px; position: relative; }
+    .color-row label { font-size: 13px; font-weight: 500; color: var(--fg); opacity: 0.7; flex: 1; }
+    .color-row input[type="color"],
+    .color-row input.cp-swatch-input {
+      width: 24px; height: 24px; border: 1px solid var(--border); border-radius: 50%;
+      background: none; cursor: pointer; padding: 0; flex-shrink: 0;
+      overflow: hidden;
+      -webkit-appearance: none;
+      appearance: none;
+    }
+    .color-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
+    .color-row input[type="color"]::-webkit-color-swatch { border: none; border-radius: 50%; }
+    .color-row input[type="color"]::-moz-color-swatch { border: none; border-radius: 50%; }
+    #theme-bg-effect-color { position: relative; top: 2px; }
+    .color-row input.cp-swatch-input {
+      color: transparent;
+      text-shadow: none;
+      caret-color: transparent;
+      font-size: 0;
+      user-select: none;
+    }
+    .color-row input.cp-swatch-input::selection { background: transparent; }
+    .color-row input.cp-swatch-input:focus { outline: 1px solid var(--red); outline-offset: 1px; }
+    .color-reset-btn {
+      width: 24px; height: 24px; border: none; background: none; cursor: pointer;
+      color: var(--fg); opacity: 0; font-size: 1.15rem; padding: 0; line-height: 1;
+      transition: opacity 0.15s, color 0.15s; flex-shrink: 0; pointer-events: none;
+      position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
+    }
+    .color-row input[type="color"],
+    .color-row input.cp-swatch-input { margin-left: auto; }
+    /* Keep the harmony hex readout on the left, with the swatch dot anchored
+       to the right like the other color controls. */
+    #theme-harmony-card #harmony-accent-hex {
+      order: -1;
+      margin-right: auto;
+    }
+    .color-row:has(.color-reset-btn.changed) input[type="color"],
+    .color-row:has(.color-reset-btn.changed) input.cp-swatch-input { margin-right: 28px; }
+    .color-reset-btn.changed { opacity: 0.4; pointer-events: auto; }
+    .color-reset-btn.changed:hover { opacity: 1; color: var(--red); }
+    .theme-custom-divider {
+      grid-column: 1 / -1; font-size: 11px; color: var(--fg); opacity: 0.5;
+      text-transform: uppercase; letter-spacing: 0.04em; margin: 6px 0 2px;
+      border-top: 1px solid var(--border); padding-top: 6px;
+    }
+    .theme-swatch[data-custom] { position: relative; overflow: visible; }
+    /* Accent-coloured circular X — always visible (mobile-friendly), sits
+       INSIDE the swatch's top-right corner so it never crops into a
+       neighbouring swatch. */
+    .theme-delete-btn {
+      position: absolute;
+      top: -2px;
+      right: -2px;
+      width: 20px;
+      height: 20px;
+      padding: 0;
+      border: none;
+      border-radius: 50%;
+      background: var(--accent, var(--red, #d92534));
+      color: #fff;
+      cursor: pointer;
+      z-index: 2;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
+      transition: transform 0.12s, background 0.12s;
+    }
+    .theme-delete-btn:hover {
+      background: color-mix(in srgb, var(--accent, var(--red)) 80%, white);
+      transform: scale(1.12);
+    }
+    .theme-delete-btn:active { transform: scale(0.95); }
+    .theme-delete-btn svg {
+      display: block;
+      /* Optical nudge — the X reads off-center inside the circle without it. */
+      position: relative;
+      left: 1px;
+      top: -1px;
+    }
+    .theme-save-row {
+      display: flex; gap: 6px; margin-top: 8px;
+    }
+    .theme-save-row input {
+      flex: 1; padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px;
+      background: var(--bg); color: var(--fg); font-size: 12px; font-family: inherit;
+    }
+    .theme-save-row input:focus { outline: none; border-color: var(--red); }
+    .theme-save-row input::placeholder { color: color-mix(in srgb, var(--fg) 35%, transparent); }
+    .theme-save-row button {
+      padding: 6px 12px; border: 1px solid var(--red); border-radius: 6px;
+      background: transparent; color: var(--red); cursor: pointer;
+      font-size: 12px; font-family: inherit; white-space: nowrap; transition: all 0.15s;
+    }
+    .theme-save-row button:hover { background: color-mix(in srgb, var(--red) 11%, transparent); }
+    .theme-save-error {
+      font-size: 11px; color: var(--red); margin-top: 2px; display: none;
+    }
+    /* Import/Export */
+    .theme-io-row { display: flex; gap: 6px; margin-top: 6px; }
+    .theme-io-btn {
+      flex: 1; padding: 5px 10px; border: 1px solid var(--border); border-radius: 6px;
+      background: transparent; color: var(--fg); cursor: pointer;
+      font-size: 12px; opacity: 0.7; transition: all 0.15s; font-family: inherit;
+    }
+    .theme-io-btn:hover { opacity: 1; border-color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
+
+    #memory-import-btn svg,
+    #memory-export-btn svg {
+      color: var(--accent, var(--red));
+    }
+    .theme-import-area {
+      width: 100%; margin-top: 6px; padding: 6px 8px;
+      border: 1px solid var(--border); border-radius: 6px;
+      background: var(--bg); color: var(--fg); font-size: 0.7rem;
+      font-family: inherit; resize: vertical; min-height: 48px;
+    }
+    .theme-import-area:focus { outline: none; border-color: var(--red); }
+    .theme-import-area.hidden { display: none; }
+    .theme-import-actions { display: flex; gap: 6px; margin-top: 4px; }
+    .theme-import-actions.hidden { display: none; }
+    /* Font & Density */
+    .theme-fd-row { display: flex; gap: 8px; margin-bottom: 8px; }
+    .theme-fd-group { flex: 1; display: flex; flex-direction: column; gap: 3px; }
+    .theme-fd-label { font-size: 12px; font-weight: 500; color: var(--fg); opacity: 0.6; }
+    .theme-fd-select {
+      padding: 5px 8px; border: 1px solid var(--border); border-radius: 6px;
+      background: var(--bg); color: var(--fg); font-size: 12px;
+      font-family: inherit; cursor: pointer;
+    }
+    .theme-fd-select:focus { outline: none; border-color: var(--red); }
+    @media (max-width: 768px) {
+      /* The switch is too cramped beside the Font and Density selects on
+         narrow screens; give Frosted a dedicated row instead. */
+      .theme-fd-row:first-of-type {
+        flex-wrap: wrap;
+      }
+      .theme-fd-row:first-of-type #theme-frosted-group {
+        flex: 1 1 100%;
+        flex-direction: row;
+        align-items: center;
+        justify-content: space-between;
+        min-width: 0;
+      }
+    }
+    .theme-fd-range {
+      width: 100%;
+      max-width: 100%;
+      box-sizing: border-box;
+      margin: 0;
+      padding: 0;
+      height: 24px;
+      background: transparent;
+      cursor: pointer;
+      -webkit-appearance: none;
+      appearance: none;
+      accent-color: var(--red);
+    }
+    .theme-fd-range::-webkit-slider-runnable-track {
+      height: 4px; background: var(--border); border-radius: 2px;
+    }
+    .theme-fd-range::-moz-range-track {
+      height: 4px; background: var(--border); border-radius: 2px;
+    }
+    .theme-fd-range::-webkit-slider-thumb {
+      -webkit-appearance: none; appearance: none;
+      width: 14px; height: 14px; border-radius: 50%;
+      background: var(--red); border: none; margin-top: -5px; cursor: pointer;
+    }
+    .theme-fd-range::-moz-range-thumb {
+      width: 14px; height: 14px; border-radius: 50%;
+      background: var(--red); border: none; cursor: pointer;
+    }
+    .theme-fd-range:focus { outline: none; }
+    /* Color Harmony Generator */
+    .theme-harmony-row { display: flex; gap: 8px; align-items: flex-end; }
+    .harmony-generate-btn {
+      padding: 5px 14px; border: 1px solid var(--red); border-radius: 6px;
+      background: transparent; color: var(--red); cursor: pointer;
+      font-size: 12px; white-space: nowrap; transition: all 0.15s;
+      font-family: inherit; width: 100%;
+    }
diff --git a/static/css/02-compare.css b/static/css/02-compare.css
new file mode 100644
index 000000000..ae04ba663
--- /dev/null
+++ b/static/css/02-compare.css
@@ -0,0 +1,1805 @@
+/* 02-compare - part of the former static/style.css.
+ *
+ * The stylesheet is split by area, but the split is mechanical: rules were
+ * moved verbatim and NOT regrouped. The numeric prefix is the load order, and
+ * that order is what preserves the cascade. A rule sits in the file its
+ * original position fell into, so an area's rules are not all in its file and
+ * a file is not only that area's rules.
+ *
+ * Do not reorder the  tags in static/index.html, and do not move a rule
+ * between files: with 1,151 redeclarations and 1,886 !important in this
+ * cascade, either can change which declaration wins.
+ *
+ * static/index.html and the PRECACHE list in static/sw.js must agree on this
+ * order. tests/test_static_stylesheet_manifest.py and
+ * tests/test_css_computed_style_snapshot.py fail if that drifts.
+ */
+    .harmony-generate-btn:hover { background: color-mix(in srgb, var(--red) 11%, transparent); }
+    .harmony-preview {
+      display: flex; height: 20px; border-radius: 6px; overflow: hidden;
+      margin-top: 8px; border: 1px solid var(--border);
+    }
+    .harmony-preview:empty { display: none; border: none; }
+    .harmony-preview span { flex: 1; }
+    #theme-reset-btn {
+      margin-top: 6px; width: 100%; padding: 6px; border: 1px solid var(--border);
+      border-radius: 6px; background: var(--bg); color: var(--fg); cursor: pointer;
+      font-size: 12px; font-family: inherit; opacity: 0.7; transition: opacity 0.15s;
+    }
+    #theme-reset-btn:hover { opacity: 1; }
+    .theme-adv-toggle {
+      margin-top: 10px; margin-bottom: 10px;
+      padding: 6px 8px; cursor: pointer;
+      font-size: 11px; color: var(--red); opacity: 0.8;
+      border: 1px solid var(--border); border-radius: 6px;
+      transition: opacity 0.15s, background 0.15s;
+      user-select: none;
+    }
+    .theme-adv-toggle:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 4%, transparent); }
+    .theme-adv-toggle .theme-adv-arrow {
+      display: inline-block; transition: transform 0.2s; font-size: 10px;
+      margin-right: 4px;
+    }
+    .theme-adv-toggle.open .theme-adv-arrow { transform: rotate(90deg); }
+    .theme-adv-section { margin-top: 8px; margin-bottom: 10px; }
+    .theme-adv-section.hidden { display: none; }
+    .theme-adv-group { margin-bottom: 8px; }
+    .theme-adv-group-label {
+      font-size: 10px; color: var(--fg); opacity: 0.5;
+      text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px;
+    }
+    .theme-adv-clear-btn {
+      margin-top: 6px; width: 100%; padding: 5px; border: 1px solid var(--border);
+      border-radius: 6px; background: transparent; color: var(--fg); opacity: 0.5;
+      cursor: pointer; font-size: 12px; font-family: inherit; transition: opacity 0.15s;
+    }
+    .theme-adv-clear-btn:hover { opacity: 1; }
+
+    /* Customization is an editor, so keep its sections visually related while
+       preserving the existing controls and their order. */
+    #theme-tab-customize {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+    }
+    #theme-tab-customize > .admin-card,
+    #theme-tab-customize > .theme-adv-toggle,
+    #theme-tab-customize > .theme-adv-section {
+      margin-top: 0;
+      margin-bottom: 0;
+    }
+    #theme-tab-customize > .admin-card {
+      padding: 12px;
+    }
+    #theme-tab-customize > .admin-card > h2 {
+      display: flex;
+      align-items: center;
+      min-height: 20px;
+      margin: 0 0 10px;
+      padding-bottom: 7px;
+      border-bottom: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
+      font-size: 13px;
+    }
+    #theme-tab-customize .theme-custom {
+      gap: 6px 14px;
+    }
+    #theme-tab-customize .color-row {
+      min-height: 30px;
+      padding: 3px 6px;
+      box-sizing: border-box;
+      border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
+      background: color-mix(in srgb, var(--fg) 2%, transparent);
+    }
+    #theme-tab-customize .color-row label {
+      font-size: 12px;
+    }
+    #theme-tab-customize .theme-adv-toggle {
+      flex-shrink: 0;
+      margin: 0;
+      background: color-mix(in srgb, var(--fg) 2%, transparent);
+    }
+    #theme-tab-customize .theme-adv-section {
+      padding: 10px 12px 2px;
+      border: 1px solid color-mix(in srgb, var(--border) 75%, transparent);
+      border-radius: 7px;
+      background: color-mix(in srgb, var(--panel) 55%, transparent);
+    }
+    #theme-tab-customize .theme-adv-group {
+      margin: 0;
+      padding: 0 0 9px;
+      border-bottom: 1px solid color-mix(in srgb, var(--border) 48%, transparent);
+    }
+    #theme-tab-customize .theme-adv-group + .theme-adv-group {
+      padding-top: 9px;
+    }
+    #theme-tab-customize .theme-adv-group:last-of-type {
+      border-bottom: 0;
+    }
+    #theme-tab-customize .theme-adv-group-label {
+      margin-bottom: 6px;
+      color: var(--red);
+      opacity: 0.8;
+      font-weight: 600;
+    }
+    #theme-tab-customize .theme-fd-row,
+    #theme-tab-customize .theme-harmony-row {
+      gap: 12px;
+      margin-bottom: 10px;
+    }
+    #theme-tab-customize .theme-fd-row:last-child,
+    #theme-tab-customize .theme-harmony-row:last-child {
+      margin-bottom: 0;
+    }
+    #theme-tab-customize .theme-fd-group {
+      min-width: 0;
+    }
+    #theme-tab-customize .theme-fd-select {
+      min-height: 30px;
+    }
+    #theme-tab-customize #theme-harmony-card .theme-fd-label {
+      font-size: 11px;
+    }
+    #theme-tab-customize .theme-save-row,
+    #theme-tab-customize .theme-io-row {
+      gap: 8px;
+    }
+    #theme-tab-customize #theme-reset-btn {
+      margin-top: 8px;
+    }
+    @media (max-width: 560px) {
+      #theme-tab-customize .theme-custom {
+        grid-template-columns: 1fr;
+      }
+      #theme-tab-customize .theme-fd-row,
+      #theme-tab-customize .theme-harmony-row {
+        flex-direction: column;
+        align-items: stretch;
+      }
+      #theme-tab-customize .theme-fd-group,
+      #theme-tab-customize .theme-harmony-row .theme-fd-group {
+        width: 100%;
+      }
+    }
+
+
+    /* Mobile: bottom sheet modals — slide up from bottom */
+    @media (max-width: 768px) {
+      .modal {
+        align-items: flex-end;
+        background: rgba(0,0,0,0.4);
+        pointer-events: auto;
+        /* Anchor the bottom sheet to the DYNAMIC viewport. The base overlay is
+           position:fixed; height:100% = the layout viewport, whose bottom sits
+           UNDER Firefox Android's bottom URL bar — so flex-end parked the sheet
+           (and its footer / Start button) beneath the bar. dvh excludes the bar
+           so the footer lands above it. Extra safe-area pad for iOS home bar. */
+        height: 100dvh;
+        padding-bottom: env(safe-area-inset-bottom, 0px);
+      }
+      /* Confirm dialog stays centered, not a bottom sheet */
+      #styled-confirm-overlay {
+        align-items: center;
+      }
+      .styled-confirm-box {
+        border-radius: 12px !important;
+        border: 1px solid var(--border) !important;
+        animation: modal-enter 0.25s ease-out both !important;
+        max-height: none !important;
+      }
+      .styled-confirm-box.modal-closing {
+        animation: modal-exit 0.18s ease-in both !important;
+      }
+      .styled-confirm-box::before {
+        display: none !important;
+      }
+      .styled-confirm-box .close-btn {
+        display: none !important;
+      }
+      #theme-popup {
+        width: 100% !important;
+        height: 65vh !important;
+        max-height: 65vh !important;
+        top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
+        position: fixed !important;
+        border-radius: 14px 14px 0 0;
+        border: none;
+        border-top: 1px solid var(--border);
+        padding: 6px 12px 12px;
+        animation: sheet-enter 0.2s ease-out forwards;
+        overflow-y: hidden !important;
+      }
+      #theme-popup .theme-tab-panel {
+        touch-action: pan-y;
+        overscroll-behavior: contain;
+        -webkit-overflow-scrolling: touch;
+      }
+      #theme-popup.sheet-ready {
+        animation: none;
+      }
+      #theme-popup.modal-closing {
+        animation: sheet-exit 0.15s ease-in both !important;
+      }
+      .modal-content,
+      .memory-modal-content,
+      .settings-modal-content,
+      #compare-model-overlay .modal-content {
+        width: 100% !important;
+        /* 85dvh leaves comfortable headroom above the sheet so the user can
+           still see the chat behind it and the drag handle has breathing
+           room. Was 65vh (too short, content clipped) → 90vh (too tall, top
+           hugged the status bar). */
+        max-height: 85dvh !important;
+        max-height: 85vh !important; /* fallback for browsers without dvh */
+        height: auto !important;
+        border-radius: 14px 14px 0 0;
+        border: none;
+        border-top: 1px solid var(--border);
+        padding-top: 6px !important;
+        /* Clip children to the rounded top corners — otherwise the sticky
+           modal-header's var(--panel) background paints a darker
+           rectangle past the radius and the corners look square again. */
+        overflow: hidden;
+        animation: sheet-enter 0.2s ease-out forwards;
+      }
+      /* Keep the tall tool drawers on a shared mobile height ceiling. The
+         final mobile drawer rule below makes this content-sized so shorter
+         tools remain compact while tall ones leave a visible top gap. */
+      #cookbook-modal .modal-content,
+      #tasks-modal .modal-content,
+      #calendar-modal .modal-content,
+      #memory-modal .memory-modal-content,
+      #settings-modal .settings-modal-content,
+      #email-lib-modal .modal-content,
+      #doclib-modal .doclib-modal-content {
+        max-height: 90vh !important;
+        max-height: 90dvh !important;
+        height: auto !important;
+        /* Reserve the iOS home-indicator / bottom-bar strip so an expanded
+           skill card's footer (Delete / Edit / Run) isn't hidden under it.
+           dvh already excludes the URL bar; this handles the safe area. */
+        padding-bottom: env(safe-area-inset-bottom, 0px) !important;
+      }
+      /* Grab handle pill on the non-standard content classes too. Memory's
+         desktop rule defines a radial-glow ::before later in the file —
+         these body-prefixed selectors win the specificity battle on mobile. */
+      body .memory-modal-content::before,
+      body .settings-modal-content::before {
+        content: '';
+        display: block;
+        position: static;
+        inset: auto;
+        width: 36px; height: 4px;
+        background: var(--fg);
+        opacity: 0.25;
+        border-radius: 2px;
+        margin: 0 auto 4px;
+        flex-shrink: 0;
+        padding: 0;
+        border-top: 10px solid transparent;
+        border-bottom: 6px solid transparent;
+        background-clip: padding-box;
+        animation: none;
+      }
+      .memory-modal-content .close-btn,
+      .memory-modal-content .modal-close,
+      .settings-modal-content .close-btn,
+      .settings-modal-content .modal-close {
+        display: none !important;
+      }
+      .memory-modal-content,
+      .settings-modal-content {
+        touch-action: pan-y;
+        overscroll-behavior: contain;
+      }
+      /* Library modals use the same bottom-sheet sizing as the other tools.
+         The shared mobile drawer rule below provides the top breathing room. */
+      #cookbook-modal .modal-content,
+      #calendar-modal .modal-content,
+      #email-lib-modal .modal-content,
+      #doclib-modal .modal-content,
+      #gallery-modal .modal-content,
+      #tasks-modal .modal-content {
+        /* vh-first as fallback for very old browsers, then dvh wins so
+           the modal shrinks/grows with the mobile URL bar. Wrong order
+           previously made the expanded chat preview extend below the
+           visible viewport on Chrome/Safari mobile (URL bar covered the
+           action buttons row). */
+        max-height: 90vh !important;
+        max-height: 90dvh !important;
+        height: auto !important;
+      }
+      /* Anchor those modals to the top so the full height is usable */
+      #cookbook-modal,
+      #calendar-modal,
+      #email-lib-modal,
+      #doclib-modal,
+      #gallery-modal,
+      #tasks-modal {
+        padding-top: 0 !important;
+        align-items: stretch !important;
+      }
+      /* Deep Research already gets the swipe grab-handle pill from the
+         shared `.modal-content::before` rule (the pane carries that class).
+         The previous header-level pill was redundant and rendered as a
+         SECOND pill stacked above the real one — removed. */
+      /* The inner body must flex to fill the new full height, and the
+         tasks list inside it must scroll independently — otherwise the
+         list crops at whatever pre-mobile height the desktop layout had. */
+      #tasks-modal .modal-content {
+        display: flex !important;
+        flex-direction: column !important;
+      }
+      #tasks-modal .modal-body {
+        flex: 1 1 auto !important;
+        min-height: 0 !important;
+      }
+      /* Memory/Skills: the body lacks flex:1, so on the fixed-height mobile
+         sheet (overflow:hidden) it grew past the viewport and clipped the
+         bottom of the skills list + its row action buttons with no way to
+         scroll there. Bound the body so the inner list scrolls internally. */
+      #memory-modal .memory-modal-content {
+        display: flex !important;
+        flex-direction: column !important;
+      }
+      /* flex-basis MUST be 0 (not auto) — matches the working #doclib-modal
+         .modal-body. With basis:auto, Firefox (incl. mobile) sizes the body
+         to its content (~half height) instead of filling, while Chromium
+         fills it regardless — which is exactly why the skill expand worked
+         on desktop/Chromium but stuck at ~50% on Firefox mobile. */
+      #memory-modal .memory-modal-body {
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+        overflow: hidden !important;
+      }
+      /* Same basis:0 fix down the rest of the skills chain so every layer
+         fills instead of sizing to content under Firefox. */
+      #memory-modal .memory-tab-panel[data-memory-panel="skills"] {
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+      }
+      #memory-modal .memory-tab-panel[data-memory-panel="skills"] > .admin-card {
+        flex: 1 1 0 !important;
+        min-height: 0 !important;
+      }
+      /* The skills modal carries an extra toolbar row (search/sort/select +
+         bulk bar) the doc/email libraries don't, so the shared 82dvh preview
+         min-height overflowed here and pushed the expanded footer (Delete /
+         Edit / Run) off the bottom of the screen. Let flexbox size it from
+         the resolved card height instead, so the footer always pins inside
+         the visible area. */
+      #memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview {
+        min-height: 0 !important;
+      }
+      /* Once enter animation finishes, clear it so inline transform works for dragging */
+      .modal-content.sheet-ready {
+        animation: none;
+      }
+      .modal-content.modal-closing {
+        animation: sheet-exit 0.15s ease-in both !important;
+      }
+      /* Grab handle — large touch target, visible pill */
+      #theme-popup::before,
+      .modal-content::before {
+        content: '';
+        display: block;
+        width: 36px; height: 4px;
+        background: var(--fg); opacity: 0.25;
+        border-radius: 2px;
+        margin: 0 auto 4px;
+        flex-shrink: 0;
+        padding: 0;
+        /* Expand touch target without changing visual size */
+        border-top: 10px solid transparent;
+        border-bottom: 6px solid transparent;
+        background-clip: padding-box;
+      }
+      /* Hide close X on mobile — swipe down to dismiss */
+      .modal-content .close-btn,
+      .modal-content .modal-close,
+      #theme-popup .close-btn {
+        display: none !important;
+      }
+      /* Hide the auto-injected minimize (_) button on mobile — the dock
+         chip already represents the minimized state, and the swipe-down
+         gesture is the canonical minimize action. */
+      .modal-minimize-btn,
+      .minimize-btn,
+      [data-minimize] {
+        display: none !important;
+      }
+      /* Lock modals to vertical touch only, prevent horizontal dragging */
+      .modal-content {
+        touch-action: pan-y;
+        overscroll-behavior: contain;
+      }
+      .modal-content .cookbook-body,
+      .modal-content .modal-body {
+        touch-action: pan-y;
+        overscroll-behavior: contain;
+      }
+      .modal-header {
+        cursor: default;
+        touch-action: none;
+      }
+      @keyframes sheet-enter {
+        from { transform: translateY(100%); }
+        to   { transform: translateY(0); }
+      }
+      @keyframes sheet-exit {
+        from { transform: translateY(0); }
+        to   { transform: translateY(100%); }
+      }
+    }
+
+    /* ── Model A/B Comparison ── */
+
+    /* -- Extracted inline-style classes -- */
+    .cmp-header-action-btn {
+      background: none; border: 1px solid var(--border); color: var(--fg);
+      cursor: pointer; padding: 3px 10px; font-size: 11px; font-weight: 600;
+      opacity: 0.7; transition: all 0.15s; line-height: 1; border-radius: 4px;
+      display: inline-flex; align-items: center; font-family: inherit;
+    }
+    .cmp-form-control {
+      padding: 8px; background: var(--bg); color: var(--fg);
+      border: 1px solid var(--border); border-radius: 6px; font-size: 0.85em;
+    }
+    .cmp-btn-secondary {
+      background: transparent; color: var(--fg); border: 1px solid var(--border);
+      border-radius: 6px; cursor: pointer;
+    }
+    .cmp-btn-primary {
+      background: var(--fg); color: var(--bg); border: none;
+      border-radius: 6px; cursor: pointer; font-weight: 600;
+      transition: filter 0.12s, background 0.12s, color 0.12s;
+    }
+    /* Override global button:hover (which switches bg to var(--panel) =
+       very dark) — keep the bright primary look and just brighten slightly. */
+    .cmp-btn-primary:hover:not(:disabled) {
+      background: var(--fg); color: var(--bg);
+      filter: brightness(1.1);
+    }
+    .cmp-btn-primary:active:not(:disabled) { filter: brightness(0.95); }
+    .cmp-btn-secondary:hover:not(:disabled) {
+      background: color-mix(in srgb, var(--fg) 8%, transparent);
+      border-color: var(--fg); color: var(--fg);
+    }
+    .cmp-model-row {
+      display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
+      flex-wrap: wrap;
+      padding: 2px 0;
+      transition: margin-left 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s, border-color 0.15s;
+    }
+    .cmp-model-row-duplicate {
+      margin-left: -4px;
+      margin-right: -4px;
+      padding: 4px;
+      border-radius: 6px;
+      background: color-mix(in srgb, var(--color-error) 8%, transparent);
+      border: 1px solid color-mix(in srgb, var(--color-error) 20%, transparent);
+    }
+    .cmp-model-row-swap-target {
+      margin-left: -4px;
+      margin-right: -4px;
+      padding: 4px;
+      border-radius: 6px;
+      background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 42%, transparent);
+    }
+    .cmp-model-row-swap-target .cmp-row-label {
+      color: var(--accent, var(--red));
+      opacity: 1;
+    }
+    .cmp-row-label {
+      color: var(--fg); font-size: 0.82em; font-weight: 600; min-width: 20px;
+      opacity: 0.4; text-align: center; flex-shrink: 0;
+    }
+    .cmp-model-meta {
+      order: 3;
+      flex: 1 0 calc(100% - 28px);
+      margin-left: 28px;
+      margin-top: -4px;
+      display: flex;
+      align-items: center;
+      gap: 5px;
+      min-width: 0;
+      font-size: 10px;
+      line-height: 1.2;
+    }
+    .cmp-model-meta-chip,
+    .cmp-model-meta-warning,
+    .cmp-model-meta-muted {
+      display: inline-flex;
+      align-items: center;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      border-radius: 999px;
+      padding: 1px 6px;
+      letter-spacing: 0;
+    }
+    .cmp-model-meta-chip {
+      color: color-mix(in srgb, var(--fg) 60%, transparent);
+      background: color-mix(in srgb, var(--fg) 5%, transparent);
+      border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+    }
+    .cmp-model-meta-warning {
+      color: var(--color-error);
+      background: color-mix(in srgb, var(--color-error) 10%, transparent);
+      border: 1px solid color-mix(in srgb, var(--color-error) 24%, transparent);
+      font-weight: 600;
+    }
+    .cmp-model-meta-muted {
+      color: color-mix(in srgb, var(--fg) 36%, transparent);
+    }
+    .cmp-rm-btn {
+      background: none; border: none; color: var(--accent, var(--red)); cursor: pointer;
+      min-width: 20px; font-size: 16px; font-weight: 600; opacity: 0.3;
+      transition: all 0.15s; padding: 0; line-height: 1; text-align: center;
+      position: relative; top: -2px;
+      order: 2;
+      margin-left: auto;
+      align-self: center;
+    }
+    .cmp-prov-select {
+      flex: 0 0 auto; width: 120px; font-size: 0.8em;
+    }
+    /* Eval-prompts picker — only shown during compare; absolute top-right
+       inside .chat-input-top, matching .model-picker-wrap's slot. */
+    .chat-input-top > .cmp-eval-wrap {
+      position: relative;
+      grid-column: 2;
+      grid-row: 1;
+      top: -2px; right: 0;
+      z-index: 2;
+    }
+    .cmp-eval-btn {
+      display: inline-flex; align-items: center; gap: 4px;
+      padding: 4px 8px;
+      background: transparent;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      color: var(--fg);
+      font-size: 11px; font-weight: 500;
+      font-family: inherit;
+      cursor: pointer; opacity: 0.75;
+      transition: opacity 0.15s, border-color 0.15s;
+    }
+    .cmp-eval-btn:hover { opacity: 1; border-color: var(--fg); }
+    .cmp-eval-clear {
+      display: inline-flex; align-items: center; gap: 4px; margin-left: 4px;
+      border: 0; background: transparent; color: var(--fg); opacity: .55;
+      padding: 3px 5px; font: inherit; font-size: 10px; cursor: pointer;
+    }
+    .cmp-eval-clear:hover { opacity: 1; }
+    .cmp-eval-caret { opacity: 0.7; transform: rotate(180deg); }
+    .cmp-eval-menu {
+      position: absolute; bottom: calc(100% + 4px); right: 0;
+      min-width: 220px; max-width: 280px;
+      max-height: 360px; overflow-y: auto;
+      background: var(--panel);
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      box-shadow: 0 -4px 16px rgba(0,0,0,0.3);
+      padding: 4px;
+      z-index: 1000;
+    }
+    .cmp-eval-menu.hidden { display: none; }
+    .cmp-eval-group-label {
+      font-size: 9px; text-transform: uppercase; letter-spacing: 0.5px;
+      opacity: 0.45; font-weight: 600;
+      padding: 6px 8px 2px;
+    }
+    .cmp-eval-item {
+      display: block; width: 100%;
+      text-align: left;
+      padding: 5px 8px;
+      background: none; border: none;
+      color: var(--fg); font-size: 11px;
+      font-family: inherit;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+    .cmp-eval-item:hover {
+      background: color-mix(in srgb, var(--fg) 8%, transparent);
+    }
+    .cmp-eval-empty {
+      padding: 10px; text-align: center;
+      font-size: 11px; opacity: 0.5;
+    }
+    /* Tick on items that ship a known expected answer */
+    .cmp-eval-item-tick {
+      float: right;
+      margin-left: 6px;
+      font-size: 10px;
+      color: var(--color-success, #4caf50);
+      opacity: 0.8;
+    }
+    /* Expected-answer panel — floats as its own little window above the
+       chat-input-bar. Distinct surface, padded, drop-shadow, slightly
+       lifted so it reads as a separate UI element, not part of the input. */
+    .chat-input-bar:has(.cmp-eval-expected) { position: relative; }
+    .cmp-eval-expected {
+      position: absolute;
+      bottom: calc(100% + 8px);
+      right: 0;
+      display: inline-flex; align-items: center; gap: 8px;
+      padding: 8px 12px;
+      font-size: 11px;
+      background: var(--panel);
+      border: 1px solid color-mix(in srgb, var(--color-success, #4caf50) 50%, transparent);
+      border-radius: 8px;
+      box-shadow: 0 6px 18px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.2);
+      color: var(--fg);
+      width: fit-content;
+      z-index: 5;
+      pointer-events: auto;
+    }
+    .cmp-eval-expected.hidden { display: none; }
+    .cmp-eval-expected-label {
+      opacity: 0.6;
+      text-transform: uppercase;
+      font-size: 9px;
+      letter-spacing: 0.5px;
+      font-weight: 600;
+    }
+    .cmp-eval-expected-value {
+      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+      font-size: 11px;
+    }
+    .cmp-eval-expected-close {
+      background: none; border: none; color: var(--fg);
+      font-size: 14px; line-height: 1; padding: 0 0 0 4px;
+      opacity: 0.5; cursor: pointer; font-family: inherit;
+    }
+    .cmp-eval-expected-close:hover { opacity: 1; }
+    /* Auto-grade badge — stamped on a pane after stream completes when an
+       eval prompt with a known expected answer was used. */
+    .pane-grade-badge {
+      display: inline-flex; align-items: center; justify-content: center;
+      width: 18px; height: 18px;
+      margin: 0 4px;
+      font-size: 12px; font-weight: 700;
+      border-radius: 50%;
+      border: 1px solid currentColor;
+      flex-shrink: 0;
+    }
+    .pane-grade-badge.pass {
+      color: var(--color-success, #4caf50);
+      background: color-mix(in srgb, var(--color-success, #4caf50) 12%, transparent);
+    }
+    .pane-grade-badge.fail {
+      color: var(--color-error, #e55);
+      background: color-mix(in srgb, var(--color-error, #e55) 12%, transparent);
+    }
+
+    /* Compare probe overlay */
+    .compare-probe-overlay {
+      position: fixed; inset: 0; z-index: 300;
+      background: rgba(0,0,0,0.5); display: flex;
+      align-items: center; justify-content: center;
+      padding: 12px;
+      box-sizing: border-box;
+    }
+    .compare-probe-card {
+      width: min(460px, calc(100% - 16px));
+      max-height: min(620px, calc(100dvh - 24px));
+      box-sizing: border-box;
+      background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px;
+      display: flex; flex-direction: column; align-items: stretch; gap: 10px;
+      min-width: 280px; box-shadow: 0 8px 28px rgba(0,0,0,0.28);
+      overflow: hidden;
+      font-family: inherit;
+    }
+    .compare-probe-title {
+      font-size: 13px; font-weight: 400; color: var(--fg);
+    }
+    .compare-probe-title-label { font-weight: 600; }
+    .compare-probe-title-status { font-weight: 400; }
+    .compare-probe-list {
+      display: grid; grid-template-columns: 1fr 1fr; gap: 6px; min-width: 0; width: 100%;
+      overflow-y: auto;
+    }
+    .compare-probe-row {
+      display: flex; align-items: center; gap: 6px; padding: 6px 10px;
+      border-radius: 6px; background: color-mix(in srgb, var(--fg) 4%, transparent);
+      font-size: 12px; transition: background 0.2s; overflow: hidden;
+    }
+    .compare-probe-row.fail {
+      background: color-mix(in srgb, var(--color-error, #f44) 8%, transparent);
+    }
+    .compare-probe-spinner {
+      width: 24px; text-align: center; font-size: 10px; flex-shrink: 0;
+      letter-spacing: -1px; opacity: 0.6;
+    }
+    .compare-probe-spinner.ok { color: var(--color-success); animation: none; }
+    .compare-probe-spinner.fail { color: var(--color-error, #f44); animation: none; }
+    .compare-probe-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    .compare-probe-status { font-size: 11px; opacity: 0.5; flex-shrink: 0; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+    .compare-probe-status.ok { color: var(--color-success); opacity: 1; }
+    .compare-probe-status.fail { color: var(--color-error, #f44); opacity: 1; }
+    .compare-probe-action-btn {
+      display: inline-flex; align-items: center; gap: 5px;
+      min-height: 25px; padding: 3px 8px; background: transparent; color: var(--fg); border: 1px solid var(--border);
+      border-radius: 5px; cursor: pointer; font-size: 10px; font-family: inherit;
+      opacity: 0.78; transition: opacity 0.15s, border-color 0.15s, background 0.15s; white-space: nowrap; flex-shrink: 0;
+    }
+    .compare-probe-action-btn svg,
+    .compare-probe-footer-btn svg { width: 12px; height: 12px; flex: 0 0 auto; color: var(--accent, var(--red)); }
+    .compare-probe-action-btn:hover { opacity: 1; border-color: var(--accent, var(--red)); background: color-mix(in srgb, var(--fg) 5%, transparent); }
+    .compare-probe-feedback { display: flex; flex-direction: column; gap: 5px; }
+    .compare-probe-feedback:empty { display: none; }
+    .compare-probe-detail {
+      display: flex; align-items: center; gap: 6px; min-width: 0;
+      padding: 6px 8px; font-size: 10px;
+      background: color-mix(in srgb, var(--color-error, #f44) 6%, transparent);
+      border-left: 2px solid color-mix(in srgb, var(--color-error, #f44) 72%, transparent);
+      border-radius: 4px;
+    }
+    .compare-probe-detail-icon {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      flex: 0 0 14px;
+      width: 14px;
+      height: 14px;
+      color: var(--accent, var(--red));
+      font-family: inherit;
+      font-size: 11px;
+      font-weight: 700;
+      line-height: 1;
+    }
+    .compare-probe-detail-message { flex: 1 1 auto; min-width: 0; line-height: 1.35; color: color-mix(in srgb, var(--fg) 72%, transparent); overflow-wrap: anywhere; }
+    .compare-probe-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; margin-top: 2px; }
+    .compare-probe-footer-btn {
+      display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 5px 10px;
+      font-family: inherit; font-size: 11px; font-weight: 600; line-height: 1;
+    }
+    .compare-probe-start-anyway,
+    .compare-probe-start-anyway:hover:not(:disabled) {
+      background: var(--accent, var(--red));
+      border: 1px solid var(--accent, var(--red));
+      color: var(--bg);
+    }
+    .compare-probe-start-anyway svg { color: currentColor; }
+    .compare-probe-start-anyway span { font-weight: 700; }
+
+    @keyframes pane-shake {
+      0%, 100% { transform: translateX(0); }
+      15% { transform: translateX(-3px) rotate(-0.5deg); }
+      30% { transform: translateX(3px) rotate(0.5deg); }
+      45% { transform: translateX(-2px); }
+      60% { transform: translateX(2px); }
+      75% { transform: translateX(-1px); }
+    }
+
+    .chat-container.compare-active {
+      display: flex;
+      flex-direction: column;
+      padding: 0;
+      overflow: hidden;
+      animation: compare-enter 0.3s ease-out;
+    }
+    @keyframes compare-enter {
+      from { opacity: 0; transform: translateY(8px); }
+      to { opacity: 1; transform: translateY(0); }
+    }
+    .chat-container.compare-active .chat-input-bar {
+      margin-bottom: 0;
+    }
+    .chat-container.compare-active #model-picker-wrap {
+      display: none !important;
+    }
+    .compare-grid {
+      display: grid;
+      gap: 4px;
+      flex: 1 1 0;
+      min-height: 0;
+      overflow: hidden;
+      position: relative;
+    }
+    .compare-mobile-tabs { display: none; }
+    .compare-grid[data-cols="2"] { grid-template-columns: 1fr 1fr; }
+    .compare-grid[data-cols="3"] { grid-template-columns: 1fr 1fr 1fr; }
+    .compare-grid[data-cols="4"] { grid-template-columns: repeat(4, 1fr); }
+    .compare-grid[data-cols="5"] { grid-template-columns: repeat(4, 1fr); }
+    .compare-grid[data-cols="6"] { grid-template-columns: repeat(4, 1fr); }
+    .compare-grid[data-cols="7"] { grid-template-columns: repeat(4, 1fr); }
+    .compare-grid[data-cols="8"] { grid-template-columns: repeat(4, 1fr); }
+    .compare-grid { grid-auto-rows: 1fr; }
+    .compare-shuffle-notice {
+      position: absolute;
+      left: 50%;
+      top: 50%;
+      transform: translate(-50%, -50%) scale(0.94);
+      z-index: 30;
+      display: inline-flex;
+      align-items: center;
+      gap: 8px;
+      padding: 10px 14px;
+      border: 1px solid color-mix(in srgb, var(--red) 28%, var(--border));
+      border-radius: 8px;
+      background: color-mix(in srgb, var(--panel, var(--bg)) 92%, transparent);
+      color: var(--fg);
+      box-shadow: 0 10px 30px color-mix(in srgb, #000 24%, transparent);
+      font-size: 12px;
+      font-weight: 700;
+      line-height: 1;
+      opacity: 0;
+      pointer-events: none;
+      transition: opacity 0.14s ease, transform 0.14s ease;
+    }
+    .compare-shuffle-notice.show {
+      opacity: 1;
+      transform: translate(-50%, -50%) scale(1);
+    }
+    .compare-shuffle-notice-icon {
+      display: inline-flex;
+      color: var(--red);
+      animation: compare-shuffle-dice 0.56s ease both;
+    }
+    .compare-shuffle-notice-icon svg {
+      display: block;
+      width: 18px;
+      height: 18px;
+    }
+    @keyframes compare-shuffle-dice {
+      0% { transform: rotate(-18deg) scale(0.9); }
+      55% { transform: rotate(380deg) scale(1.08); }
+      100% { transform: rotate(360deg) scale(1); }
+    }
+    /* Sequential waterfall layout — stacked rows, staggered left, flush right */
+    .compare-grid.sequential-layout {
+      display: flex !important;
+      flex-direction: column !important;
+      grid-template-columns: none !important;
+      gap: 4px;
+      overflow-y: auto;
+    }
+    .compare-grid.sequential-layout .compare-pane {
+      flex-shrink: 0;
+      min-height: 200px;
+      transition: margin-left 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease;
+    }
+    /* Herringbone diagonal cut on left side of sequential pane headers */
+    .compare-grid.sequential-layout .compare-pane .pane-header {
+      clip-path: polygon(20px 0, 100% 0, 100% 100%, 0 100%);
+      padding-left: 26px;
+    }
+    .compare-pane {
+      display: flex;
+      flex-direction: column;
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      overflow: hidden;
+      min-height: 0;
+      min-width: 0;
+      position: relative;
+      transition: border-color 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
+    }
+    .compare-pane::before {
+      content: "";
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      height: 2px;
+      z-index: 2;
+      pointer-events: none;
+      background: transparent;
+      transform-origin: left center;
+    }
+    .compare-pane.is-streaming {
+      border-color: color-mix(in srgb, var(--accent, var(--red)) 34%, var(--border));
+      box-shadow: inset 0 1px 0 color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+    }
+    .compare-pane.is-streaming::before {
+      background: linear-gradient(90deg, transparent 0%, var(--accent, var(--red)) 35%, var(--red) 50%, var(--accent, var(--red)) 65%, transparent 100%);
+      animation: compare-pane-progress 1.25s linear infinite;
+    }
+    .compare-pane.is-awaiting-input {
+      border-color: color-mix(in srgb, var(--red) 42%, var(--border));
+      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--red) 12%, transparent);
+    }
+    .compare-pane.is-awaiting-input::before {
+      background: var(--red);
+      animation: none;
+    }
+    .compare-pane.is-done {
+      border-color: color-mix(in srgb, var(--green, #50fa7b) 20%, var(--border));
+    }
+    .compare-pane.is-failed {
+      border-color: color-mix(in srgb, var(--color-error) 42%, var(--border));
+    }
+    .compare-pane.is-failed::before {
+      background: var(--color-error);
+    }
+    @keyframes compare-pane-progress {
+      from { transform: translateX(-100%); }
+      to { transform: translateX(100%); }
+    }
+    .compare-pane .pane-header {
+      display: flex;
+      flex-direction: column;
+      align-items: stretch;
+      gap: 4px;
+      padding: 4px 10px;
+      background: color-mix(in srgb, var(--fg) 4%, transparent);
+      border-bottom: 1px solid var(--border);
+      font-size: 0.82em;
+      font-weight: 600;
+      color: var(--fg);
+      transition: background 0.4s;
+      flex-shrink: 0;
+      overflow: visible;
+      min-width: 0;
+      flex-wrap: nowrap;
+    }
+    .pane-header-row { display: flex; align-items: center; width: 100%; min-width: 0; }
+    .pane-header-primary { min-height: 24px; }
+    .pane-header-secondary { min-height: 20px; }
+    .pane-primary-actions, .pane-stats { display: flex; align-items: center; gap: 4px; min-width: 0; }
+    .pane-primary-actions { margin-left: auto; flex: 0 0 auto; }
+    .pane-stats { flex: 1 1 auto; overflow: hidden; }
+    .compare-header-bar button:hover,
+    .compare-header-bar button:focus-visible {
+      background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent) !important;
+      border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border)) !important;
+      color: var(--fg) !important;
+      opacity: 1 !important;
+      outline: none;
+    }
+    .compare-header-bar button:active {
+      background: color-mix(in srgb, var(--accent, var(--red)) 24%, transparent) !important;
+      transform: scale(0.96);
+    }
+    .compare-more-wrap { position: relative; display: inline-flex; }
+    .compare-more-btn {
+      display: inline-flex; align-items: center; justify-content: center;
+      width: 28px; height: 24px; padding: 0 !important;
+      border: 1px solid transparent; border-radius: 5px;
+      background: none; color: var(--fg); cursor: pointer;
+      opacity: 0.5; transition: opacity .15s, background .15s;
+    }
+    .compare-header-bar .compare-close-btn {
+      width: 24px; height: 24px; min-width: 24px; padding: 0;
+      border-radius: 999px; font-size: 0; display: inline-flex;
+      align-items: center; justify-content: center;
+    }
+    .compare-header-bar .compare-close-btn:hover,
+    .compare-header-bar .compare-close-btn:focus-visible {
+      background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
+      border-color: transparent !important;
+      color: var(--fg) !important;
+      opacity: 1 !important;
+      outline: none;
+    }
+    .compare-header-bar .compare-more-btn:hover,
+    .compare-header-bar .compare-more-btn:focus-visible,
+    .compare-header-bar .compare-more-btn[aria-expanded="true"] {
+      background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
+      border-color: transparent !important;
+      color: var(--fg) !important;
+      opacity: 1 !important;
+      outline: none;
+    }
+    .compare-more-menu {
+      position: absolute; top: calc(100% + 5px); right: 0; z-index: 10002;
+      display: none; flex-direction: column; gap: 3px; min-width: 132px;
+      padding: 5px; border: 1px solid var(--border); border-radius: 8px;
+      background: var(--panel, var(--bg)); box-shadow: 0 8px 24px rgba(0,0,0,.3);
+    }
+    .compare-more-menu.is-open { display: flex; }
+    .compare-more-menu > .compare-export-wrap,
+    .compare-more-menu > button {
+      width: 100%; display: flex; align-items: center;
+    }
+    .compare-more-menu #compare-export-btn,
+    .compare-more-menu #compare-shuffle-btn,
+    .compare-more-menu #compare-check-btn,
+    .compare-more-menu #compare-add-menu-btn {
+      width: 100%; min-height: 32px; height: 32px;
+      box-sizing: border-box; justify-content: flex-start; gap: 7px;
+      padding: 7px 9px !important;
+      border: 1px solid transparent !important; border-radius: 6px;
+      background: transparent !important; color: var(--fg) !important;
+      font-size: 11px; font-weight: 500; opacity: .72;
+      transition: background .12s ease, opacity .12s ease, color .12s ease;
+    }
+    .compare-more-menu #compare-export-btn:hover,
+    .compare-more-menu #compare-shuffle-btn:hover,
+    .compare-more-menu #compare-check-btn:hover,
+    .compare-more-menu #compare-add-menu-btn:hover,
+    .compare-more-menu #compare-export-btn:focus-visible,
+    .compare-more-menu #compare-shuffle-btn:focus-visible,
+    .compare-more-menu #compare-check-btn:focus-visible,
+    .compare-more-menu #compare-add-menu-btn:focus-visible {
+      opacity: 1; background: color-mix(in srgb, var(--red) 10%, transparent) !important;
+      border-color: transparent !important; color: var(--fg) !important; outline: none;
+    }
+    .compare-more-menu #compare-export-btn svg,
+    .compare-more-menu #compare-shuffle-btn svg,
+    .compare-more-menu #compare-check-btn svg,
+    .compare-more-menu #compare-add-menu-btn svg { flex: 0 0 auto; opacity: .65; }
+    .compare-more-menu #compare-export-btn span,
+    .compare-more-menu #compare-shuffle-btn span,
+    .compare-more-menu #compare-check-btn span,
+    .compare-more-menu #compare-add-menu-btn span {
+      font-size: 11px !important; margin-left: 0 !important;
+      color: var(--fg) !important; opacity: 1 !important; font-weight: 500 !important;
+    }
+    .compare-more-menu #compare-export-btn,
+    .compare-more-menu #compare-shuffle-btn,
+    .compare-more-menu #compare-check-btn,
+    .compare-more-menu #compare-add-menu-btn {
+      color: var(--fg) !important; opacity: 1 !important; font-weight: 500 !important;
+    }
+    .compare-more-menu #compare-export-btn svg,
+    .compare-more-menu #compare-shuffle-btn svg,
+    .compare-more-menu #compare-check-btn svg,
+    .compare-more-menu #compare-add-menu-btn svg { color: var(--fg) !important; opacity: .65 !important; }
+    .compare-export-item:hover,
+    .compare-export-item:focus-visible {
+      background: color-mix(in srgb, var(--red) 10%, transparent) !important;
+      border-color: transparent !important; outline: none;
+    }
+    .compare-export-item { display: flex; align-items: center; gap: 8px; }
+    .compare-export-item > svg { flex: 0 0 auto; opacity: .65; }
+    .compare-export-menu .compare-export-item,
+    .compare-export-menu .compare-export-item > span {
+      color: var(--fg) !important;
+      opacity: 1 !important;
+    }
+    .compare-add-flap {
+      position: absolute; z-index: 20; right: -8px; top: 50%;
+      width: 34px; min-width: 34px; height: 30px; box-sizing: border-box;
+      padding: 0 !important;
+      display: inline-flex; flex-direction: row; align-items: center; justify-content: center;
+      gap: 0; border: 1px solid var(--border);
+      border-radius: 0 7px 7px 0; background: var(--panel, var(--bg));
+      color: var(--fg); cursor: pointer; opacity: .7;
+      box-shadow: 3px 2px 9px color-mix(in srgb, var(--fg) 12%, transparent);
+      transform: translate(0, -50%); transition: width .18s ease, opacity .15s, background .15s, box-shadow .15s;
+    }
+    .compare-add-flap:hover, .compare-add-flap:focus-visible {
+      width: 70px; opacity: 1; background: color-mix(in srgb, var(--fg) 8%, var(--panel));
+      box-shadow: 4px 3px 12px color-mix(in srgb, var(--fg) 18%, transparent);
+      outline: none;
+    }
+    .compare-add-flap span {
+      max-width: 0; overflow: hidden; white-space: nowrap; margin-left: 0 !important;
+      font-size: 10px; opacity: 0; transition: max-width .18s ease, margin-left .18s ease, opacity .12s;
+    }
+    .compare-add-flap:hover span, .compare-add-flap:focus-visible span {
+      max-width: 32px; margin-left: 3px !important; opacity: 1;
+    }
+    .compare-add-flap svg { flex-shrink: 0; position: relative; left: -3px; }
+    @media (max-width: 768px) {
+      .compare-add-flap {
+        top: 8px; right: 8px; width: auto; min-width: 32px; height: 32px;
+        padding: 5px 8px !important; flex-direction: row; transform: none;
+        border-right: 1px solid var(--border); border-radius: 6px;
+      }
+      .compare-add-flap span { opacity: 1; }
+    }
+    .pane-actions {
+      display: flex; gap: 4px; align-items: center; margin-left: auto; flex-shrink: 0;
+    }
+    .compare-pane-footer {
+      font-size: 0.72em; opacity: 0.4; padding: 4px 10px;
+      border-top: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+      text-align: center; flex-shrink: 0;
+    }
+    /* Per-pane vote button. Sits at the bottom of each compare pane so
+       the action lives right under the response it judges. */
+    .pane-vote-footer {
+      padding: 6px 8px;
+      border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+      background: color-mix(in srgb, var(--fg) 3%, transparent);
+      flex-shrink: 0;
+    }
+    .pane-vote-btn {
+      width: 100%;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      gap: 4px;
+      background: var(--bg);
+      color: var(--fg);
+      border: 1px solid var(--border);
+      border-radius: 6px;
+      padding: 6px 10px;
+      font-family: inherit;
+      font-size: 12px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: background 0.15s, border-color 0.15s, opacity 0.15s;
+    }
+    .pane-vote-btn:hover:not(:disabled) {
+      background: color-mix(in srgb, var(--accent, var(--fg)) 12%, var(--bg));
+      border-color: var(--accent, var(--fg));
+    }
+    .pane-vote-btn:disabled { cursor: not-allowed; }
+    .pane-vote-label {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      min-width: 0;
+    }
+    .pane-action-btn {
+      background: none; border: none; color: var(--fg); cursor: pointer;
+      opacity: 0.3; padding: 2px; border-radius: 4px;
+      display: flex; align-items: center; transition: all 0.15s;
+    }
+    .pane-action-btn:hover { opacity: 0.8; background: color-mix(in srgb, var(--fg) 6%, transparent); }
+    /* Pane title as clickable model-swap button */
+    .pane-title-btn {
+      background: none; border: none; cursor: pointer;
+      font-size: 10px; font-weight: 400; font-family: inherit;
+      color: var(--fg); padding: 0;
+      text-align: left; display: flex; align-items: center; gap: 4px;
+      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+      transition: opacity 0.15s;
+      min-width: 0; flex: 1 1 0;
+    }
+    .pane-title-btn:hover { opacity: 0.7; }
+    .pane-title-caret { font-size: 0.6em; opacity: 0.35; flex-shrink: 0; position: relative; top: 2px; }
+    .pane-title-btn:hover .pane-title-caret { opacity: 0.7; }
+    .pane-mode-badge {
+      display: inline-flex;
+      align-items: center;
+      gap: 4px;
+      flex: 0 0 auto;
+      height: 18px;
+      padding: 0 6px;
+      border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+      border-radius: 999px;
+      background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+      color: color-mix(in srgb, var(--fg) 76%, transparent);
+      font-size: 9px;
+      font-weight: 600;
+      line-height: 1;
+      letter-spacing: 0;
+      white-space: nowrap;
+    }
+    .pane-mode-dot {
+      width: 5px;
+      height: 5px;
+      border-radius: 50%;
+      background: currentColor;
+      opacity: 0.75;
+      flex: 0 0 auto;
+    }
+    .pane-mode-detail {
+      opacity: 0.52;
+      font-weight: 500;
+    }
+    .pane-mode-badge.pane-mode-icon-only {
+      width: 20px;
+      padding: 0;
+      justify-content: center;
+    }
+    .pane-mode-settings {
+      opacity: 0.65;
+      cursor: pointer;
+    }
+    .pane-settings-btn { width: 20px; height: 20px; justify-content: center; }
+    .pane-settings-btn[aria-expanded="true"] { opacity: 1; background: color-mix(in srgb, var(--fg) 9%, transparent); }
+    .response-metrics[data-action="settings"] { cursor: pointer; }
+    .response-thinking-state { color: color-mix(in srgb, var(--fg) 55%, transparent); }
+    .pane-mode-settings:hover,
+    .pane-mode-settings:focus-visible,
+    .pane-mode-settings[aria-expanded="true"] {
+      opacity: 1;
+      color: var(--fg);
+      background: color-mix(in srgb, var(--fg) 9%, var(--bg));
+      border-color: color-mix(in srgb, var(--accent, var(--fg)) 45%, var(--border));
+      outline: none;
+    }
+    .pane-mode-icon,
+    .pane-mode-icon svg {
+      display: block;
+      width: 12px;
+      height: 12px;
+    }
+    .pane-mode-agent {
+      color: color-mix(in srgb, var(--fg) 76%, transparent);
+      background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+      border-color: color-mix(in srgb, var(--border) 70%, transparent);
+    }
+    .pane-mode-search {
+      color: color-mix(in srgb, var(--accent, var(--red)) 82%, var(--fg));
+      background: color-mix(in srgb, var(--accent, var(--red)) 10%, var(--bg));
+      border-color: color-mix(in srgb, var(--accent, var(--red)) 28%, var(--border));
+    }
+    .pane-mode-research {
+      color: color-mix(in srgb, #36c48f 82%, var(--fg));
+      background: color-mix(in srgb, #36c48f 10%, var(--bg));
+      border-color: color-mix(in srgb, #36c48f 28%, var(--border));
+    }
+    .compare-check-label {
+      position: relative;
+      top: -2px;
+      font-size: 11px;
+      margin-left: 3px;
+    }
+    .compare-check-icon {
+      position: relative;
+      top: 1px;
+    }
+    #compare-check-btn > .spinner-whirlpool {
+      position: relative;
+      top: -2px;
+      transform: none;
+    }
+    .compare-pane .pane-close-btn { opacity: 0.3; }
+    .compare-pane .pane-close-btn:hover { opacity: 1; color: var(--color-error); }
+    /* Model swap dropdown under pane title */
+    .pane-model-dropdown {
+      position: absolute;
+      z-index: 1000;
+      min-width: 220px;
+      max-height: 300px;
+      overflow-y: auto;
+      background: var(--bg);
+      border: 1px solid var(--border);
+      border-radius: 8px;
+      box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+      padding: 4px;
+    }
+    .pane-model-item {
+      display: block; width: 100%;
+      box-sizing: border-box;
+      padding: 8px 10px; font-size: 11px; line-height: 1.3;
+      text-align: left; background: none; border: none; border-radius: 6px;
+      color: var(--fg); cursor: pointer; transition: background 0.12s ease;
+      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+    }
+    .pane-model-item:hover,
+    .pane-model-item:focus-visible { background: color-mix(in srgb, var(--red) 10%, transparent); outline: none; }
+    .pane-model-item.current { color: var(--red); font-weight: 600; }
+    .pane-timer {
+      font-size: 10px; font-weight: 400; opacity: 0.45; font-variant-numeric: tabular-nums;
+      white-space: nowrap; padding-right: 4px;
+    }
+    .pane-summary {
+      display: inline-flex;
+      align-items: center;
+      position: relative;
+      top: 1px;
+      min-height: 16px;
+      max-width: 150px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      padding: 0 5px;
+      border-radius: 999px;
+      background: color-mix(in srgb, var(--fg) 4%, transparent);
+      color: color-mix(in srgb, var(--fg) 58%, transparent);
+      font-size: 9px;
+      font-weight: 500;
+      line-height: 1;
+      font-variant-numeric: tabular-nums;
+      cursor: pointer;
+      transition: max-width .15s ease, white-space .15s ease;
+    }
+    .pane-summary:empty { display: none; }
+    .pane-stats-popup {
+      position: fixed; z-index: 10040; width: 210px; padding: 6px;
+      background: var(--panel, var(--bg)); color: var(--fg);
+      border: 1px solid var(--border); border-radius: 7px;
+      box-shadow: 0 8px 24px rgba(0,0,0,.3); font-size: 11px;
+    }
+    .pane-stats-popup-title { padding: 4px 6px 7px; font-weight: 600; border-bottom: 1px solid var(--border); }
+    .pane-stats-popup-row { display: flex; justify-content: space-between; gap: 12px; padding: 6px; }
+    .pane-stats-popup-row span { color: color-mix(in srgb, var(--fg) 55%, transparent); }
+    .pane-stats-popup-row strong { font-weight: 500; font-variant-numeric: tabular-nums; text-align: right; }
+    /* Metrics stay on the lower row even in dense comparisons. */
+    .compare-grid[data-cols="4"] .pane-timer,
+    .compare-grid[data-cols="5"] .pane-timer,
+    .compare-grid[data-cols="6"] .pane-timer {
+      width: auto; order: initial; margin-top: 0; padding-bottom: 0; padding-left: 0;
+    }
+    .pane-finish-badge {
+      font-weight: 600; color: var(--green, #50fa7b);
+      position: relative;
+      top: 2px;
+    }
+    .compare-pane.winner .pane-header {
+      background: color-mix(in srgb, var(--green, #50fa7b) 12%, transparent);
+      border-bottom-color: color-mix(in srgb, var(--green, #50fa7b) 30%, var(--border));
+    }
+    .compare-pane.winner .pane-title {
+      color: var(--green, #50fa7b);
+    }
+    .compare-pane.loser .pane-header {
+      opacity: 0.5;
+    }
+    .confetti-piece {
+      position: fixed;
+      pointer-events: none;
+      z-index: 1000000;
+    }
+    .compare-pane.expanded { grid-column: 1 / -1; }
+    .compare-pane .chat-history {
+      flex: 1 1 0;
+      min-height: 0;
+      overflow-y: auto !important;
+      overflow-x: hidden;
+      padding: 8px;
+      display: flex;
+      flex-direction: column;
+    }
+    .compare-pane .chat-history .msg {
+      flex-shrink: 0;
+    }
+    .compare-gen-image {
+      max-width: 100%;
+      max-height: 100%;
+      object-fit: contain;
+      border-radius: 4px;
+      cursor: pointer;
+    }
+    .compare-section {
+      margin-bottom: 14px;
+    }
+    .compare-section:last-child {
+      margin-bottom: 0;
+    }
+    .compare-section-label {
+      font-size: 9px;
+      text-transform: uppercase;
+      letter-spacing: 0.6px;
+      opacity: 0.5;
+      margin-bottom: 5px;
+      font-weight: 600;
+    }
+    /* Active-type/mode readout next to "Type:"/"Mode:" — only needed on mobile
+       (where the tab/toggle text labels are hidden); hidden on desktop. */
+    .compare-type-current,
+    .compare-mode-current { display: none; }
+    /* Contextual one-liner under the mode toggles describing what you just
+       changed — empty until the first toggle, then a subtle hint. */
+    .compare-mode-hint {
+      font-size: 11px;
+      opacity: 0.55;
+      margin-top: 6px;
+      min-height: 0;
+      line-height: 1.3;
+    }
+    .compare-mode-hint:empty { display: none; }
+    .compare-type-tabs .compare-mode-tab > svg,
+    .compare-type-current > svg {
+      color: var(--accent, var(--red));
+    }
+
+    .compare-mode-tabs {
+      display: flex;
+      gap: 4px;
+      flex-wrap: wrap;
+      min-width: 0;
+    }
+    /* Type tabs match Mode toggles 1:1 (same flex column layout, same metrics) */
+    .compare-mode-tab {
+      display: flex; flex-direction: column; align-items: center; justify-content: center;
+      width: 56px; height: 38px; flex: 1 1 0;
+      padding: 5px 4px 4px; border: 1px solid var(--border); border-radius: 6px;
+      cursor: pointer; user-select: none; transition: all 0.15s;
+      background: none; color: var(--fg); opacity: 0.35;
+      flex-shrink: 0; gap: 0;
+      font-family: inherit;
+    }
+    .compare-mode-tab > svg {
+      width: 13px; height: 13px; flex: 0 0 13px;
+    }
+    .compare-mode-tab:hover {
+      opacity: 0.7; background: color-mix(in srgb, var(--fg) 6%, transparent);
+    }
+    .compare-mode-tab.active {
+      opacity: 1; border-color: var(--fg);
+      background: color-mix(in srgb, var(--fg) 10%, transparent);
+    }
+    .compare-sources-box {
+      display: flex; align-items: center; gap: 6px;
+      padding: 6px 10px; margin-bottom: 8px;
+      border-radius: 6px; font-size: 0.78em;
+      background: color-mix(in srgb, var(--red) 8%, transparent);
+      border: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+      color: color-mix(in srgb, var(--fg) 70%, transparent);
+      cursor: default;
+    }
+    .compare-sources-box .sources-label { font-weight: 600; }
+    /* Compact tool blocks inside compare panes */
+    .compare-pane .agent-thread-node {
+      margin: 4px 0; font-size: 0.85em;
+    }
+    .compare-pane .agent-thread-cmd {
+      max-height: 80px; overflow-y: auto;
+    }
+    .compare-pane .agent-tool-output pre {
+      max-height: 120px; overflow-y: auto;
+    }
+    .compare-vote-bar {
+      display: flex; justify-content: center; gap: 8px;
+      padding: 8px; border-top: 1px solid var(--border);
+      flex-shrink: 0; flex-wrap: wrap;
+    }
+    .compare-vote-bar.hidden { display: none; }
+    .compare-vote-btn {
+      padding: 6px 13px; border: 1px solid var(--border); border-radius: 6px;
+      background: var(--panel); color: var(--fg); cursor: pointer; font-size: 0.8em;
+      transition: all 0.15s; white-space: nowrap;
+    }
+    .compare-score-label {
+      position: relative;
+      top: -2px;
+      display: inline-block;
+    }
+    .compare-vote-btn:hover { border-color: var(--red); background: color-mix(in srgb, var(--red) 11%, transparent); }
+    .compare-vote-tie { opacity: 0.7; }
+    .compare-rematch-btn {
+      display: flex; align-items: center; gap: 6px; margin-left: 8px;
+      border-color: color-mix(in srgb, var(--accent, var(--red)) 60%, var(--border));
+      background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+      color: var(--fg); opacity: 1; font-weight: 600;
+    }
+    .compare-rematch-btn:hover { border-color: var(--accent, var(--red)); background: color-mix(in srgb, var(--accent, var(--red)) 24%, transparent); }
+    /* Preview button accent in pane header */
+    .pane-preview-btn.active { color: var(--red); }
+    /* Full-pane iframe for HTML preview */
+    .compare-pane-iframe {
+      flex: 1;
+      width: 100%;
+      border: none;
+      border-radius: 0 0 8px 8px;
+      background: #fff;
+    }
+    /* ---- Add-pane "+" button in pane header (last pane only) ---- */
+    .pane-add-btn { display: none !important; font-size: 16px; font-weight: 600; }
+    .compare-pane:last-child .pane-add-btn { display: flex !important; }
+    /* Dropdown for adding a pane */
+    .add-pane-dropdown {
+      position: absolute;
+      right: 0;
+      z-index: 100;
+      background: var(--panel, var(--bg));
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      max-height: 300px;
+      overflow-y: auto;
+      min-width: 220px;
+      box-shadow: 0 8px 24px rgba(0,0,0,0.3), 0 0 0 1px color-mix(in srgb, var(--fg) 5%, transparent);
+      padding: 6px;
+      backdrop-filter: blur(12px);
+      animation: dropdown-in 0.15s ease-out;
+    }
+    .add-pane-search {
+      width: 100%;
+      padding: 6px 8px;
+      border: 1px solid var(--border);
+      border-radius: 4px;
+      background: var(--bg);
+      color: var(--fg);
+      font-size: 0.85em;
+      box-sizing: border-box;
+      margin-bottom: 4px;
+      position: sticky;
+      top: 0;
+      z-index: 1;
+    }
+    /* Compare toggle buttons — icon + label stacked */
+    .compare-toggle-label {
+      display: block;
+      font-size: 9px;
+      line-height: 1;
+      margin-top: 2px;
+      font-weight: 500;
+    }
+    .compare-blind-toggle,
+    .compare-save-toggle,
+    .compare-dice-toggle,
+    .compare-parallel-toggle,
+    .compare-reset-toggle {
+      display: flex; flex-direction: column; align-items: center; justify-content: center;
+      width: 56px; height: auto; flex: 1 1 0;
+      padding: 5px 4px 4px; border: 1px solid var(--border); border-radius: 6px;
+      cursor: pointer; user-select: none; transition: all 0.15s;
+      background: none; color: var(--fg); opacity: 0.35;
+      flex-shrink: 0; gap: 0;
+    }
+    .compare-blind-toggle:hover,
+    .compare-save-toggle:hover,
+    .compare-dice-toggle:hover,
+    .compare-parallel-toggle:hover,
+    .compare-reset-toggle:hover {
+      opacity: 0.7; background: color-mix(in srgb, var(--fg) 6%, transparent);
+    }
+    .compare-blind-toggle.active {
+      opacity: 1; color: var(--accent, var(--red)); border-color: var(--accent, var(--red));
+      background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+    }
+    .compare-blind-toggle.active > svg {
+      color: var(--accent, var(--red));
+    }
+    .compare-save-toggle.active {
+      opacity: 1; color: var(--color-save-green); border-color: var(--color-save-green);
+      background: color-mix(in srgb, var(--red) 10%, transparent);
+    }
+    .compare-dice-toggle.active {
+      opacity: 1; color: var(--red); border-color: var(--red);
+      background: color-mix(in srgb, var(--red) 10%, transparent);
+    }
+    .compare-parallel-toggle {
+      opacity: 1; color: #e0a050; border-color: #e0a050;
+      background: rgba(224, 160, 80, 0.1);
+    }
+    .compare-parallel-toggle.active {
+      color: var(--accent, var(--red)); border-color: var(--accent, var(--red));
+      background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+    }
+    @media (max-width: 520px) {
+      .compare-toggle-label { display: none; }
+      /* Tab text labels are hidden on mobile, so spell out the active type next
+         to "Type:" with its icon. */
+      .compare-type-current {
+        display: inline-flex;
+        align-items: center;
+        gap: 4px;
+        margin-left: 5px;
+        font-weight: 600;
+        vertical-align: -3px;
+      }
+      .compare-type-current svg { width: 14px; height: 14px; }
+      /* Mode list — comma-separated, each name already coloured inline. */
+      .compare-mode-current {
+        display: inline;
+        margin-left: 5px;
+        font-weight: 600;
+      }
+      .compare-blind-toggle,
+      .compare-save-toggle,
+      .compare-dice-toggle,
+      .compare-parallel-toggle,
+      .compare-reset-toggle {
+        width: 32px; height: 32px; min-width: 32px; padding: 0;
+      }
+      /* The Group tab's seq/parallel toggle is a single full-width button (not
+         a row of compact icons like the Compare header), so keep its label and
+         make it a comfortable touch target with the text beside the icon. */
+      #group-mode-btn {
+        flex-direction: row !important;
+        width: auto !important;
+        height: auto !important;
+        min-height: 44px;
+        padding: 8px 14px !important;
+        gap: 8px !important;
+        font-size: 13px;
+      }
+      #group-mode-btn .compare-toggle-label {
+        display: inline !important;
+        font-size: 13px;
+        margin-top: 0;
+      }
+      #group-mode-btn svg { width: 18px; height: 18px; }
+      /* Compare header: hide labels + close button, show icons only */
+      #compare-shuffle-btn span {
+        display: none;
+      }
+      #compare-shuffle-btn,
+      #compare-check-btn,
+      #compare-add-btn {
+        padding: 3px 6px;
+      }
+      /* Save space so the header buttons fit on one row: tighter padding +
+         smaller labels (icons kept full size). (Score now lives in the vote bar.) */
+      .compare-header-bar button { padding: 3px 5px !important; }
+      .compare-header-bar #compare-check-btn > span,
+      .compare-header-bar #compare-add-btn > span {
+        font-size: 10px; margin-left: 2px;
+      }
+      /* Compare mobile: keep the close X visible — it's the only way out
+         now that the hamburger is hidden during compare mode. */
+      .compare-header-bar {
+        padding: 14px 8px 10px 8px !important;
+        min-height: 44px;
+      }
+      .compare-pane .pane-header {
+        align-items: stretch;
+        row-gap: 3px;
+      }
+      .compare-pane .pane-title-btn {
+        flex: 1 1 auto;
+        width: auto;
+        padding-right: 2px;
+      }
+      /* Compare is already visibly in chat mode; the Chat speech-bubble
+         badge adds a redundant second row on narrow screens. */
+      .compare-pane .pane-mode-chat { display: none; }
+      .compare-pane .pane-timer,
+      .compare-pane .pane-summary,
+      .compare-pane .pane-finish-badge,
+      .compare-pane .pane-mode-badge,
+      .compare-pane .pane-actions {
+        order: 1;
+        align-self: center;
+      }
+      .compare-pane .pane-finish-badge { top: 0; }
+      .compare-pane .pane-summary {
+        max-width: 110px;
+      }
+      .compare-pane .pane-mode-detail {
+        display: none;
+      }
+      .compare-grid[data-cols="4"] .compare-pane .pane-timer,
+      .compare-grid[data-cols="5"] .compare-pane .pane-timer,
+      .compare-grid[data-cols="6"] .compare-pane .pane-timer {
+        width: auto;
+        order: 1;
+        margin-top: 0;
+        padding-bottom: 0;
+        padding-left: 0;
+      }
+      .compare-pane .pane-actions {
+        margin-left: auto;
+      }
+      /* Mode tabs: icons only, centered */
+      .compare-mode-tab span { display: none; }
+      .compare-mode-tabs { justify-content: center; }
+      /* Header action buttons: hide text labels on Export / Shuffle /
+         Model on mobile so the close X fits on the right. Score keeps
+         its "Score" label because its icon (4-square grid) reads too
+         similar to Shuffle's icon without text. */
+      .compare-header-bar #compare-export-btn > span,
+      .compare-header-bar #compare-shuffle-btn > span {
+        display: none;
+      }
+      /* Override the desktop override: on mobile the model picker overlay
+         MUST cap its height to the viewport so the dropdown doesn't run
+         past the bottom of the screen. */
+      #compare-model-overlay .modal-content {
+        max-height: 85dvh !important;
+        max-height: 85vh !important;
+        overflow: hidden !important;
+      }
+      #compare-model-overlay .modal-body {
+        overflow: auto !important;
+        flex: 1 1 auto !important;
+        min-height: 0 !important;
+      }
+    }
+    /* Hide number input spinners */
+    input[type="number"]::-webkit-inner-spin-button,
+    input[type="number"]::-webkit-outer-spin-button {
+      -webkit-appearance: none; margin: 0;
+    }
+
+    /* ── Sensitive info censor ── */
+    .censored-item {
+      filter: blur(5px);
+      cursor: pointer;
+      transition: filter 0.2s;
+      border-radius: 2px;
+      padding: 0 2px;
+      background: rgba(255,100,100,0.08);
+      user-select: none;
+    }
+    .censored-item:hover { filter: blur(3px); background: rgba(255,100,100,0.15); }
+    .censored-item.revealed {
+      filter: none;
+      background: rgba(100,255,100,0.08);
+      user-select: auto;
+      cursor: text;
+    }
+
+    #settings-menu-list .list-item.active {
+      background: color-mix(in srgb, var(--accent) 10%, transparent);
+      border-color: var(--accent);
+    }
+
+    /* ── Print / PDF Export ── */
+    @media print {
+      body { background: #fff !important; color: #000 !important; }
+      #sidebar, .sidebar, #icon-rail, .hamburger-btn, #sidebar-backdrop, .chat-input-bar, .input-bar-wrapper,
+      #welcome-screen, .chat-top-bar, .chat-meta-overlay, .msg-footer,
+      .modal, .toast, .overflow-wrapper, .mode-toggle, .incognito-btn,
+      button, .dropdown, .session-dropdown,
+      .agent-tool-spinner, .agent-thread-node.running { display: none !important; }
+      main.chat-container { width: 100% !important; margin: 0 !important; padding: 0 !important; max-height: none !important; overflow: visible !important; }
+      #chat-history { max-height: none !important; overflow: visible !important; height: auto !important; padding: 0 !important; }
+      .msg { break-inside: avoid; page-break-inside: avoid; border: none !important; box-shadow: none !important; }
+      .msg-ai { background: #f5f5f5 !important; color: #000 !important; }
+      .msg-user { background: #e8e8e8 !important; color: #000 !important; }
+      .msg .role { color: #333 !important; font-weight: bold; }
+      .msg .body { color: #000 !important; }
+      pre, code { background: #f0f0f0 !important; color: #000 !important; border: 1px solid #ccc !important; }
+      details { display: block !important; }
+      details[open] summary ~ * { display: block !important; }
+      details > summary { list-style: none; }
+      details > summary::before { content: "" !important; }
+      #chat-history::before { content: attr(data-print-title); display: block; font-size: 1.3em; font-weight: bold; margin-bottom: 1em; color: #000; }
+      a { color: #000 !important; text-decoration: underline; }
+    }
+
+
+/* ── Components (from style.css) ── */
+
+/* Self-hosted Fira Code font */
+@font-face { font-family: 'Fira Code'; font-weight: 300; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Light.woff2') format('woff2'); }
+@font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); }
+@font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); }
+
+/* Scrollbar styling */
+
+/* Code block styling */
+pre, code, .hljs {
+  font-size: 11px;
+  line-height: 1.5;
+}
+
+/* WebKit (Chrome, Edge, Safari) */
+/* Utility class for red text */
+.red-text {
+  color: var(--red);
+}
+
+/* Internal chat links (search results, session references) */
+a.chat-link {
+  color: var(--hl-function);
+  text-decoration: none;
+  border-bottom: 1px dotted var(--hl-function);
+  cursor: pointer;
+}
+a.chat-link:hover {
+  opacity: 0.8;
+  border-bottom-style: solid;
+}
+
+a.chat-link[href^="#email-"] {
+  display: inline;
+  padding: 1px 5px;
+  border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 34%, transparent);
+  border-radius: 5px;
+  background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+  color: var(--fg);
+  font-weight: 600;
+  border-bottom-style: solid;
+}
+a.chat-link[href^="#email-"]:hover {
+  opacity: 1;
+  background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+  border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+}
+
+a.chat-link[href^="#notes-more-"],
+a.chat-link[href^="#skills-more-"],
+a.chat-link[href^="#memories-more-"],
+a.chat-link[href^="#events-more-"],
+a.chat-link[href^="#sessions-more-"] {
+  color: var(--muted);
+  border-bottom: 1px dotted color-mix(in srgb, var(--muted) 55%, transparent);
+  font-size: 11px;
+  font-weight: 500;
+}
+a.chat-link[href^="#notes-more-"]:hover,
+a.chat-link[href^="#skills-more-"]:hover,
+a.chat-link[href^="#memories-more-"]:hover,
+a.chat-link[href^="#events-more-"]:hover,
+a.chat-link[href^="#sessions-more-"]:hover {
+  color: var(--fg);
+  opacity: 1;
+  border-bottom-style: solid;
+}
+.more-list-expanded {
+  display: block;
+  max-height: 0;
+  overflow: hidden;
+  opacity: 0;
+  color: inherit;
+  font: inherit;
+  line-height: inherit;
+  transition: max-height 220ms ease, opacity 160ms ease;
+}
+.more-list-expanded.is-open {
+  opacity: 1;
+}
diff --git a/static/css/03-agent-chat.css b/static/css/03-agent-chat.css
new file mode 100644
index 000000000..0536fe3b6
--- /dev/null
+++ b/static/css/03-agent-chat.css
@@ -0,0 +1,2846 @@
+/* 03-agent-chat - part of the former static/style.css.
+ *
+ * The stylesheet is split by area, but the split is mechanical: rules were
+ * moved verbatim and NOT regrouped. The numeric prefix is the load order, and
+ * that order is what preserves the cascade. A rule sits in the file its
+ * original position fell into, so an area's rules are not all in its file and
+ * a file is not only that area's rules.
+ *
+ * Do not reorder the  tags in static/index.html, and do not move a rule
+ * between files: with 1,151 redeclarations and 1,886 !important in this
+ * cascade, either can change which declaration wins.
+ *
+ * static/index.html and the PRECACHE list in static/sw.js must agree on this
+ * order. tests/test_static_stylesheet_manifest.py and
+ * tests/test_css_computed_style_snapshot.py fail if that drifts.
+ */
+.more-list-expanded-content {
+  color: inherit;
+  font: inherit;
+  line-height: inherit;
+}
+
+/* Session items */
+.session-item { position: relative; }
+.text-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.session-menu-btn { padding: 0 2px !important; min-width: 20px; height: 20px; display: inline-flex !important; align-items: center; justify-content: center; background: none !important; border-color: transparent !important; }
+.session-menu-btn:hover { background: none !important; border-color: transparent !important; }
+@media (max-width: 768px) {
+  .session-menu-btn { display: none !important; }
+  .item-drag-handle { display: none !important; }
+}
+.session-menu-btn svg { transition: transform 0.2s ease; }
+
+/* First-time swipe hint */
+.swipe-hint {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 0.7rem;
+  color: var(--color-error, #f44);
+  opacity: 0.8;
+  transition: opacity 0.5s ease;
+  pointer-events: none;
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  z-index: 2;
+}
+.swipe-hint-arrow {
+  animation: swipe-nudge 1s ease-in-out infinite;
+}
+@keyframes swipe-nudge {
+  0%, 100% { transform: translateX(0); }
+  50% { transform: translateX(-6px); }
+}
+
+/* Utility classes */
+.muted { opacity: 0.5; }
+.muted-sm { opacity: 0.35; font-size: 0.8em; }
+.accent-link { color: var(--accent-primary, var(--color-accent)); cursor: pointer; font-size: 0.85em; }
+.models-empty-state { text-align: center; padding: 12px 8px; line-height: 1.6; }
+
+/* Provider logo inside favorite dot */
+.provider-logo {
+  border: none !important;
+  background: none !important;
+  width: 14px !important; height: 14px !important;
+  display: inline-flex; align-items: center; justify-content: center;
+  transition: opacity 0.15s;
+}
+.provider-logo svg { width: 14px; height: 14px; display: block; }
+.provider-logo:hover { opacity: 1 !important; transform: scale(1.2); }
+
+/* Hide session menu button until hover — use width:0 so it doesn't steal space from text */
+.list-item .hamburger { opacity: 0; width: 0; min-width: 0; overflow: hidden; padding: 0 !important; transition: opacity 0.15s, width 0.15s, padding 0.15s; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
+.list-item:hover .hamburger { opacity: 1; width: 24px; min-width: 24px; padding: 0 4px !important; }
+@media (max-width: 768px) {
+  .list-item .hamburger { opacity: 0.5; width: 28px; min-width: 28px; padding: 0 4px !important; }
+  .list-item .hamburger:active { opacity: 1; }
+}
+
+/* Hamburger menu button styling (overrides default button appearance) */
+button.hamburger {
+  background: none;
+  border: none;
+  padding: 0;
+  cursor: pointer;
+}
+
+/* ============================================ */
+/* HEADER SIZING FOR CHAT MESSAGES */
+/* ============================================ */
+/* Markdown in chat messages — colorful, scannable */
+.msg h1, .msg h2, .msg h3, .msg h4, .msg h5, .msg h6 {
+  margin: 0.6em 0 0.3em 0;
+  line-height: 1.3;
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.msg h1 {
+  font-size: 1.15em;
+  font-weight: 700;
+  color: var(--hl-keyword, #c678dd);
+}
+
+.msg h2 {
+  font-size: 1.1em;
+  font-weight: 600;
+  color: var(--hl-function, #5b8def);
+}
+
+.msg h3 {
+  font-size: 1.05em;
+  font-weight: 600;
+  color: var(--hl-string, #98c379);
+}
+
+.msg h4 {
+  font-size: 1.02em;
+  font-weight: 600;
+  color: var(--hl-builtin, #e5c07b);
+}
+
+.msg h5 {
+  font-size: 1em;
+  font-weight: 600;
+  color: var(--hl-variable, #61afef);
+}
+
+.msg h6 {
+  font-size: 0.95em;
+  font-weight: 600;
+  color: var(--hl-number, #d19a66);
+}
+
+/* Bold text — subtle accent color */
+.msg strong, .msg b {
+  color: var(--hl-builtin, #e5c07b);
+  font-weight: 600;
+}
+
+/* Italic — softer highlight */
+.msg em, .msg i {
+  color: var(--hl-params, #abb2bf);
+  font-style: italic;
+}
+
+/* Bold + italic */
+.msg strong em, .msg em strong,
+.msg b i, .msg i b,
+.msg b em, .msg em b,
+.msg strong i, .msg i strong {
+  color: var(--hl-keyword, #c678dd);
+}
+
+/* Strikethrough */
+.msg del {
+  color: color-mix(in srgb, var(--fg) 45%, transparent);
+  text-decoration: line-through;
+}
+
+/* Inline code */
+.msg code:not(pre code) {
+  color: var(--hl-string, #98c379);
+  background: color-mix(in srgb, var(--fg) 6%, transparent);
+  padding: 1px 5px;
+  border-radius: 4px;
+  font-size: 0.9em;
+}
+
+/* Blockquotes */
+.msg blockquote {
+  border-left: 3px solid var(--hl-function, #5b8def);
+  padding: 4px 12px;
+  margin: 0.5em 0;
+  color: color-mix(in srgb, var(--fg) 75%, transparent);
+  background: var(--bg);
+  border-radius: 0 6px 6px 0;
+}
+.msg blockquote p { margin: 0.3em 0; }
+
+/* Horizontal rules */
+.msg hr {
+  border: none;
+  height: 1px;
+  background: linear-gradient(90deg, transparent, var(--border), transparent);
+  margin: 0.8em 0;
+}
+
+/* Lists */
+.msg ul, .msg ol {
+  margin: 0.3em 0 0.3em 1.2em;
+  padding: 0;
+}
+.msg li {
+  margin: 0.15em 0;
+}
+.msg li::marker {
+  color: var(--hl-function, #5b8def);
+}
+
+/* Links */
+.msg a {
+  color: var(--hl-function, #5b8def);
+  text-decoration: none;
+  border-bottom: 1px solid rgba(91, 141, 239, 0.3);
+  transition: border-color 0.15s;
+}
+.msg a:hover {
+  border-bottom-color: var(--hl-function, #5b8def);
+}
+.msg a.is-loading {
+  opacity: 0.65;
+  border-bottom-color: var(--accent, #bd93f9);
+  cursor: progress;
+}
+
+/* Tables */
+.msg table {
+  border-collapse: collapse;
+  margin: 0.5em 0;
+  font-size: 0.9em;
+  display: block;
+  width: max-content;
+  min-width: 100%;
+  max-width: 100%;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+  table-layout: auto;
+}
+.msg th {
+  background: color-mix(in srgb, var(--fg) 7%, transparent);
+  color: var(--hl-keyword, #c678dd);
+  font-weight: 600;
+  padding: 6px 12px;
+  border: 1px solid var(--border);
+  text-align: left;
+  min-width: 9ch;
+  word-break: normal;
+  overflow-wrap: break-word;
+}
+.msg td {
+  padding: 5px 12px;
+  border: 1px solid var(--border);
+  min-width: 9ch;
+  word-break: normal;
+  overflow-wrap: break-word;
+}
+
+/* Agent UI Styling */
+.agent-controls {
+  background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+  border: 1px solid #dee2e6;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 12px;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.agent-toggle label {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  font-weight: 500;
+  margin-bottom: 8px;
+}
+
+#workflow-selector {
+  margin-top: 8px;
+}
+
+#workflow-type {
+  width: 100%;
+  padding: 6px 12px;
+  border: 1px solid #ced4da;
+  border-radius: 4px;
+  background: white;
+  font-size: 14px;
+}
+
+.agent-progress {
+  background: #ffebee;
+  border: 1px solid #ef9a9a;
+  border-radius: 6px;
+  padding: 12px;
+  margin: 8px 0;
+  text-align: center;
+}
+
+.agent-working {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  font-style: italic;
+  color: var(--red);
+}
+
+.loading-dots::after {
+  content: '...';
+  animation: dots 1.5s infinite;
+}
+
+@keyframes dots {
+  0%, 20% { opacity: 0; }
+  40% { opacity: 0.5; }
+  60%, 100% { opacity: 1; }
+}
+
+.workflow-info {
+  background: #f8f9fa;
+  border: 1px solid var(--red);
+  border-radius: 6px;
+  padding: 8px 12px;
+  margin: 4px 0;
+  font-size: 0.9em;
+  color: var(--red);
+  text-align: center;
+}
+
+/* Scrollbar uses --red from :root (set at top of file) */
+
+/* Loading spinner */
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+.spinner {
+  width: 24px;
+  height: 24px;
+  margin: 8px auto;
+  border: 3px solid var(--border);
+  border-top-color: var(--red);
+  border-radius: 50%;
+  animation: spin 0.9s linear infinite;
+}
+
+/* Inline spinner for buttons */
+.btn-spinner {
+  display: inline-block;
+  width: 12px;
+  height: 12px;
+  border: 2px solid transparent;
+  border-top-color: currentColor;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-right: 6px;
+}
+
+/* Loading indicator for messages */
+.loading-indicator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 10px;
+}
+.loading-dots {
+  display: flex;
+  gap: 4px;
+}
+.loading-dot {
+  width: 6px;
+  height: 6px;
+  background-color: var(--fg);
+  border-radius: 50%;
+  opacity: 0.6;
+}
+.loading-dot:nth-child(1) {
+  animation: loading-bounce 1.4s infinite ease-in-out both;
+}
+.loading-dot:nth-child(2) {
+  animation: loading-bounce 1.4s infinite ease-in-out both;
+  animation-delay: -0.32s;
+}
+.loading-dot:nth-child(3) {
+  animation: loading-bounce 1.4s infinite ease-in-out both;
+  animation-delay: -0.64s;
+}
+@keyframes loading-bounce {
+  0%, 80%, 100% {
+    transform: scale(0);
+  }
+  40% {
+    transform: scale(1);
+  }
+}
+
+/* Hamburger menu button */
+.hamburger {
+  display: inline-flex;
+  flex-direction: column;
+  justify-content: space-between;
+  width: 24px;
+  height: 18px !important;
+  background: none;
+  border: none;
+  padding: 0;
+  cursor: pointer;
+}
+.hamburger span {
+  display: block;
+  width: 100%;
+  height: 3px;
+  background: var(--fg);
+  border-radius: 2px;
+}
+
+/* Agent indicator */
+#agent-indicator {
+  position: fixed;
+  top: 20px;
+  right: 20px;
+  background: var(--sidebar-bg, var(--panel));
+  color: var(--fg);
+  border: 1px solid var(--border);
+  padding: 6px 12px;
+  border-radius: 6px;
+  font-size: 12px;
+  display: none;
+  z-index: 100;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+#agent-indicator.active {
+  display: block;
+  border-color: var(--color-agent-active);
+  box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
+}
+#agent-indicator:hover {
+  border-color: var(--color-agent-active);
+  background: var(--panel);
+}
+
+/* Preset buttons */
+.preset-btn {
+  height: 27.2px;
+  padding: 0 8.5px;
+  margin-left: 4px;
+  border: 1px solid var(--red);
+  border-radius: 4px;
+  background: var(--sidebar-bg, var(--panel));
+  color: var(--fg);
+  font-family: inherit;
+  font-size: 10.2px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.preset-btn:hover {
+  background: var(--panel);
+  border-color: var(--fg);
+}
+
+.preset-btn.active {
+  background: var(--panel);
+  border-color: var(--red);
+  box-shadow: 0 0 0 1px var(--red), 0 0 8px color-mix(in srgb, var(--red) 30%, transparent);
+  font-weight: 600;
+}
+
+/* Custom preset modal — inherits from .modal base class */
+
+/* Unified chat input area */
+.chat-input-area {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  background: var(--panel);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  padding: 12px;
+  margin-top: 12px;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.chat-controls-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  gap: 8px;
+}
+
+.chat-controls-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.chat-controls-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.control-group {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.control-label {
+  font-size: 11px;
+  color: var(--fg);
+  opacity: 0.8;
+}
+
+.toggle-switch {
+  position: relative;
+  display: inline-block;
+  width: 30px;
+  height: 16px;
+}
+
+.toggle-switch input {
+  opacity: 0;
+  width: 0;
+  height: 0;
+}
+
+.toggle-slider {
+  position: absolute;
+  cursor: pointer;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: color-mix(in srgb, var(--fg) 15%, transparent);
+  border-radius: 8px;
+  transition: background 0.08s;
+}
+
+.toggle-slider:before {
+  position: absolute;
+  content: "";
+  height: 12px;
+  width: 12px;
+  left: 4px;
+  top: 2px;
+  background-color: var(--panel);
+  border-radius: 50%;
+  transition: transform 0.08s;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.25);
+}
+
+.toggle-switch input:checked + .toggle-slider {
+  background-color: var(--toggle-active, var(--red));
+}
+
+.toggle-switch input:checked + .toggle-slider:before {
+  transform: translateX(14px);
+}
+
+.preset-buttons-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-wrap: wrap;
+}
+
+.chat-input-form {
+  display: flex;
+  gap: 8px;
+  align-items: flex-end;
+}
+
+#message {
+  flex: 1;
+  min-height: 34px;
+  max-height: 120px;
+  resize: none;
+  font-size: 13px !important;
+  overflow-y: auto !important;
+  line-height: 1.4 !important;
+  font-family: inherit !important;
+}
+
+.action-button {
+  width: 34px;
+  height: 34px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+  margin: 0;
+  background: none;
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  cursor: pointer;
+  color: var(--fg);
+  transition: all 0.2s ease;
+}
+
+.action-button:hover {
+  background: color-mix(in srgb, var(--fg) 6%, transparent);
+  border-color: var(--fg);
+}
+
+.action-button:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.action-button.recording {
+  background: var(--color-recording);
+  border-color: var(--color-recording);
+  animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+  0% { opacity: 1; }
+  50% { opacity: 0.7; }
+  100% { opacity: 1; }
+}
+
+#stop-icon {
+  display: none;
+  width: 14px;
+  height: 14px;
+  background: var(--color-recording);
+  border-radius: 2px;
+}
+
+/* Attachment strip — centered + max-width to match the chat-input-bar below,
+   otherwise the chip floats flush-left while the input is centered (visible on
+   desktop where the chat area is wider than 800px). */
+.attach-strip {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  padding: 2px 8px;
+  max-width: 800px;
+  width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+  box-sizing: border-box;
+}
+.attach-strip:empty { display: none; }
+
+/* Upload-in-progress feedback: the message bubble shows immediately, so while
+   the files are still uploading we put a whirlpool ON each attachment chip and
+   dim the chip's content — making it obvious that file is being sent, not stuck. */
+.attach-strip.attach-uploading .thumb {
+  position: relative;
+  pointer-events: none;
+}
+.attach-strip.attach-uploading .thumb > :not(.thumb-upload-spinner) {
+  opacity: 0.4;
+}
+.thumb-upload-spinner {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+  z-index: 3;
+}
+
+.thumb {
+  border: 1px solid var(--border);
+  background: color-mix(in srgb, var(--fg) 11%, transparent);
+  padding: 3px 6px;
+  font-size: 12px;
+  display: flex;
+  gap: 6px;
+  align-items: center;
+  border-radius: 4px;
+  transition: all 0.2s ease;
+  max-width: 180px;
+}
+.thumb-img {
+  max-width: 60px;
+  max-height: 40px;
+  border-radius: 3px;
+  object-fit: cover;
+}
+.attach-image-preview {
+  margin: 4px 0;
+}
+.attach-image-preview img {
+  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
+  /* Same border as the chat bubbles. */
+  border: 1px solid var(--bubble-border, var(--border));
+}
+/* Image chips: image fills the chip, X overlays as a corner accent badge.
+   Same on desktop and mobile — doc/text chips keep the beside-X layout. */
+.thumb.thumb-image {
+  position: relative;
+  padding: 0;
+}
+.thumb.thumb-image .thumb-img {
+  max-height: 56px;
+  display: block;
+}
+@media (max-width: 768px) {
+  .thumb.thumb-image .thumb-img {
+    cursor: pointer;
+    touch-action: manipulation;
+  }
+}
+.thumb.thumb-image button {
+  position: absolute;
+  /* Sit on the top-right corner edge as an accent badge. */
+  top: -7px;
+  right: -7px;
+  width: 24px;
+  height: 24px;
+  min-width: 0;
+  padding: 0;
+  border: 2px solid var(--bg);
+  border-radius: 50%;
+  background: var(--accent-primary, var(--red));
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 15px;
+  line-height: 1;
+  z-index: 3;
+  transition: transform 0.12s ease, filter 0.12s ease, box-shadow 0.12s ease;
+}
+.thumb.thumb-image button:hover {
+  transform: scale(1.12);
+  filter: brightness(1.12);
+  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
+}
+.thumb.thumb-image button:active {
+  transform: scale(0.96);
+}
+
+.attach-crop-overlay {
+  position: fixed;
+  inset: 0;
+  z-index: var(--attach-crop-z, 100001);
+  background: rgba(0, 0, 0, 0.72);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 14px;
+  box-sizing: border-box;
+}
+.attach-crop-panel {
+  width: min(94vw, 520px);
+  max-height: min(86vh, 720px);
+  background: var(--panel, #111);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  box-shadow: 0 18px 50px rgba(0,0,0,0.45);
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+.attach-crop-stage {
+  position: relative;
+  min-height: 280px;
+  height: min(66vh, 560px);
+  background: #050505;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  touch-action: none;
+}
+.attach-crop-img {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  user-select: none;
+  pointer-events: none;
+}
+.attach-crop-box {
+  position: absolute;
+  border: 2px solid var(--accent, var(--red));
+  box-shadow: 0 0 0 9999px rgba(0,0,0,0.42);
+  cursor: move;
+  touch-action: none;
+  box-sizing: border-box;
+}
+.attach-crop-box::before,
+.attach-crop-box::after {
+  content: '';
+  position: absolute;
+  inset: 33.333% 0 auto;
+  border-top: 1px solid rgba(255,255,255,0.38);
+}
+.attach-crop-box::after {
+  inset: 66.666% 0 auto;
+}
+.attach-crop-handle {
+  position: absolute;
+  right: -9px;
+  bottom: -9px;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: var(--accent, var(--red));
+  border: 2px solid var(--bg);
+  box-sizing: border-box;
+  cursor: nwse-resize;
+}
+.attach-crop-actions {
+  display: flex;
+  gap: 8px;
+  padding: 10px;
+  background: color-mix(in srgb, var(--bg) 78%, transparent);
+}
+.attach-crop-btn {
+  flex: 1;
+  height: 34px;
+  border: 1px solid var(--border);
+  border-radius: 5px;
+  background: var(--bg);
+  color: var(--fg);
+  font: inherit;
+  font-size: 12px;
+}
+.attach-crop-primary {
+  background: var(--accent, var(--red));
+  color: #fff;
+  border-color: var(--accent, var(--red));
+}
+@media (max-width: 768px) {
+  /* Collapsed "N files" badge: use the same corner-X accent badge as image thumbs. */
+  .thumb-collapsed { position: relative; }
+  .thumb-collapsed .thumb-collapsed-x {
+    position: absolute;
+    top: -7px;
+    right: -7px;
+    width: 24px;
+    height: 24px;
+    min-width: 0;
+    padding: 0;
+    border: 2px solid var(--bg);
+    border-radius: 50%;
+    background: var(--accent-primary, var(--red));
+    color: #fff;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 15px;
+    line-height: 1;
+    z-index: 3;
+    opacity: 1;
+  }
+  /* Bigger remove-X tap target for non-image (doc/text) chips on mobile too. */
+  .thumb button {
+    height: 28px;
+    min-width: 28px;
+    font-size: 15px;
+  }
+}
+.thumb span {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.thumb:hover {
+  background: color-mix(in srgb, var(--fg) 16%, transparent);
+  border-color: var(--fg);
+}
+
+.thumb button {
+  height: 24px;
+  padding: 0 5px;
+  font-size: 0.95em;
+  border-radius: 4px;
+  color: var(--accent-primary, var(--red));
+}
+
+.thumb-collapsed {
+  cursor: pointer;
+  color: var(--red);
+  border-color: var(--red);
+  background: color-mix(in srgb, var(--red) 10%, transparent);
+  font-weight: 600;
+  gap: 8px;
+  border-radius: 999px;   /* pill — rounder than the square file chips */
+  padding-left: 12px;
+}
+.thumb-collapsed:hover {
+  background: color-mix(in srgb, var(--red) 20%, transparent);
+}
+.thumb-collapsed-label { white-space: nowrap; }
+.thumb-collapsed-x {
+  height: 24px; padding: 0 7px; font-size: 13px; border-radius: 4px;
+  color: var(--accent-primary, var(--red));
+  background: none; border: none; cursor: pointer; opacity: 0.6;
+}
+.thumb-collapsed-x:hover { opacity: 1; }
+
+/* Recording indicator */
+#recording-indicator {
+  position: fixed;
+  top: 10px;
+  left: 0;
+  right: 0;
+  background: rgba(0, 0, 0, 0.8);
+  backdrop-filter: blur(10px);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  padding: 12px;
+  margin: 10px;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+#recording-indicator.hidden {
+  display: none !important;
+}
+
+.recording-content {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  color: white;
+}
+
+.recording-icon {
+  color: var(--color-recording);
+  font-size: 20px;
+  animation: pulse 1.5s infinite;
+}
+
+.recording-text {
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.stop-recording-btn {
+  background: var(--color-recording);
+  color: white;
+  border: none;
+  border-radius: 6px;
+  padding: 6px 12px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: background 0.2s ease;
+}
+
+.stop-recording-btn:hover {
+  background: var(--color-recording-hover);
+}
+
+#recording-indicator.error {
+  background: rgba(173, 26, 26, 0.9);
+}
+
+.recording-error {
+  color: var(--color-recording);
+  font-size: 14px;
+  margin-top: 4px;
+}
+
+/* Mermaid diagram containers */
+.mermaid-container {
+  margin: 12px 0;
+  padding: 16px;
+  background: color-mix(in srgb, var(--bg) 95%, var(--fg));
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  overflow-x: auto;
+  text-align: center;
+}
+.mermaid-container svg { max-width: 100%; height: auto; }
+
+/* KaTeX math overrides */
+.katex-display { margin: 0.8em 0; overflow-x: auto; overflow-y: hidden; }
+.katex { font-size: 1.1em; }
+
+/* Hide thinking sections globally via settings toggle */
+body.hide-thinking .thinking-section { display: none !important; }
+
+/* Widen chat area via settings toggle */
+body.fullwidth-chat .chat-history {
+  padding-left: 0 !important;
+  padding-right: 12px !important;
+}
+
+/* Thinking process styles — colors follow theme accent */
+.msg .body .stream-content {
+  width: 100%;
+}
+.thinking-section {
+  margin: 12px 0;
+  width: 100%;
+  max-width: 100%;
+  box-sizing: border-box;
+  border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+  border-radius: 8px;
+  background: color-mix(in srgb, var(--red) 5%, transparent);
+  overflow: hidden;
+  transition: all 0.3s ease;
+}
+
+.thinking-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 6px 12px;
+  cursor: pointer;
+  user-select: none;
+  background: color-mix(in srgb, var(--red) 8%, transparent);
+  border-bottom: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+  transition: background 0.2s ease;
+}
+
+.thinking-header > .token-new {
+  display: none;
+}
+
+.thinking-header > div:last-child {
+  flex-shrink: 0;
+}
+
+.thinking-header:hover {
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+
+.thinking-header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 0.9em;
+  color: var(--red);
+  font-weight: 500;
+  overflow: hidden;
+  min-width: 0;
+}
+.thinking-header-left span {
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  transition: opacity 0.2s ease;
+}
+
+.thinking-icon {
+  font-size: 1.1em;
+}
+
+.thinking-toggle {
+  font-size: 0.9em;
+  color: var(--red);
+  transition: transform 0.3s ease;
+}
+.thinking-toggle::after {
+  content: '\25BC'; /* ▼ */
+}
+
+.thinking-toggle.expanded {
+  transform: rotate(180deg);
+}
+
+.thinking-content {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.3s ease, padding 0.3s ease;
+  padding: 0 12px;
+}
+
+.thinking-content.expanded {
+  max-height: 300px;
+  overflow-y: auto;
+  padding: 12px;
+}
+
+.thinking-content-inner {
+  font-size: 0.85em;
+  color: var(--fg);
+  opacity: 0.9;
+  line-height: 1.5;
+}
+.live-reply-content {
+  animation: fadeSlideIn 0.3s ease-out;
+}
+@keyframes fadeSlideIn {
+  from { opacity: 0; transform: translateY(4px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+/* Thinking indicator animation */
+.thinking-indicator {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  color: var(--red);
+  font-style: italic;
+  padding: 8px 0;
+}
+
+.thinking-dots::after {
+  content: '...';
+  animation: thinking-dots 1.5s infinite;
+  display: inline-block;
+  width: 20px;
+  text-align: left;
+}
+
+@keyframes thinking-dots {
+  0%, 20% { content: '.'; }
+  40% { content: '..'; }
+  60%, 100% { content: '...'; }
+}
+
+.thinking-complete {
+  color: var(--red);
+  font-size: 0.9em;
+  padding: 4px 0;
+  opacity: 0.8;
+}
+
+.research-context-continue-hint {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  margin: 5px 2px 0;
+  color: var(--fg-muted, var(--fg));
+  font-size: 10px;
+  opacity: 0.55;
+}
+.research-context-continue-hint svg {
+  flex: 0 0 auto;
+  opacity: 0.8;
+}
+
+/* ── Sources section — collapsible source citations ── */
+.sources-section {
+  margin: 8px 0 12px;
+  border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+  border-radius: 8px;
+  overflow: hidden;
+  transition: all 0.3s ease;
+}
+.sources-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8px 12px;
+  cursor: pointer;
+  background: color-mix(in srgb, var(--red) 8%, transparent);
+  border-bottom: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+  transition: background 0.2s ease;
+  user-select: none;
+}
+.sources-header:hover {
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+.sources-header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 0.85em;
+  font-weight: 500;
+  color: var(--red);
+}
+.sources-header-left svg {
+  width: 14px;
+  height: 14px;
+  flex-shrink: 0;
+  opacity: 0.7;
+}
+.sources-toggle {
+  font-size: 0.8em;
+  color: var(--red);
+  opacity: 0.7;
+  transition: none;
+}
+.sources-toggle::after {
+  content: '\25B6'; /* ▶ right arrow */
+}
+.sources-toggle[data-arrow="down"]::after {
+  content: '\25BC'; /* ▼ down arrow */
+}
+.sources-content {
+  max-height: 0;
+  overflow: hidden;
+  transition: max-height 0.3s ease, padding 0.3s ease;
+  padding: 0 10px;
+}
+.sources-content.expanded {
+  max-height: 3000px;
+  padding: 8px 10px;
+}
+.sources-content-inner {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.source-link {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 5px 8px;
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--fg) 4%, transparent);
+  text-decoration: none;
+  color: var(--fg);
+  transition: background 0.15s ease;
+}
+.source-link:hover {
+  background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.source-num {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: color-mix(in srgb, var(--fg) 15%, transparent);
+  color: var(--fg);
+  font-size: 0.7em;
+  font-weight: 600;
+  flex-shrink: 0;
+}
+.source-title {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 0.82em;
+}
+.source-domain {
+  font-size: 0.72em;
+  opacity: 0.45;
+  flex-shrink: 0;
+}
+
+.ai-spinner {
+  color: var(--red);
+}
+/* Nudge the Tidy button 2px left. */
+#memory-tidy-btn { position: relative; left: -2px; }
+/* Tidy button's whirlpool nudge — sits 1px lower so it visually centers on
+   the Tidy label baseline. */
+#memory-tidy-btn .ai-spinner-whirlpool,
+#memory-tidy-btn .spinner-whirlpool {
+  position: relative;
+  top: 1px;
+}
+.list-item.stream-complete {
+  animation: none;
+}
+.session-run-state {
+  flex: 0 0 auto;
+  margin-left: 5px;
+  font-size: 10px;
+  line-height: 1;
+  color: var(--fg-dim, #888);
+  opacity: 0.9;
+}
+.session-run-state.is-working {
+  color: var(--accent, var(--red));
+}
+.session-run-state .session-run-whirlpool {
+  width: 12px !important;
+  height: 12px !important;
+  margin: 0 !important;
+  display: inline-flex;
+  align-items: center;
+}
+.session-run-state .session-run-whirlpool .ai-spinner-whirlpool {
+  display: inline-flex;
+  width: 12px;
+  height: 12px;
+}
+.session-run-state.is-done {
+  color: var(--color-success, #4caf50);
+  font-weight: 600;
+}
+.cookbook-notif-active svg { opacity: 1 !important; }
+
+/* Rail notification dot — pulsing indicator on icon-rail buttons */
+.icon-rail-btn.rail-notify {
+  opacity: 1 !important;
+  position: relative;
+}
+.icon-rail-btn.rail-notify::before {
+  content: '';
+  position: absolute;
+  top: 5px;
+  right: 4px;
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--accent, var(--red));
+  animation: rail-notif-pulse 2s ease-in-out infinite;
+  z-index: 1;
+}
+.icon-rail-btn.rail-notify.rail-notify-success::before {
+  background: var(--color-success, #4caf50);
+}
+@keyframes rail-notif-pulse {
+  0%, 100% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.4; transform: scale(0.8); }
+}
+@keyframes stream-complete-pulse {
+  0%, 100% { box-shadow: none; }
+  50% { box-shadow: inset 0 0 0 1.5px var(--accent); }
+}
+
+/* ===== SLASH COMMAND RESPONSES ===== */
+.msg.msg-system {
+  padding: 6px 12px; margin: 4px auto 4px 8px;
+  max-width: 85%; background: none; border-left: 2px solid var(--border);
+}
+.msg.msg-system .body { padding: 0; }
+.msg.msg-system pre { margin: 4px 0; white-space: pre-wrap; font-size: 0.85em; }
+.dropdown-divider {
+  height: 1px;
+  margin: 3px 6px;
+  background: color-mix(in srgb, var(--border) 60%, transparent);
+}
+.msg.stream-done-toast {
+  cursor: pointer;
+  border-left-color: var(--accent, var(--border));
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--accent) 6%, transparent);
+  transition: background 0.15s;
+}
+.msg.stream-done-toast:hover {
+  background: color-mix(in srgb, var(--accent) 12%, transparent);
+}
+.msg.stream-done-toast .body {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.stream-done-indicator {
+  font-family: monospace;
+  font-size: 1.1em;
+  line-height: 1;
+  color: var(--accent);
+  flex-shrink: 0;
+  animation: bar-pulse 1.2s ease-in-out infinite;
+}
+@keyframes bar-pulse {
+  0%, 100% { opacity: 0.4; }
+  50% { opacity: 1; }
+}
+
+/* ===== AGENT MULTI-BUBBLE ===== */
+.msg.msg-tool {
+  display: none; /* legacy — hidden, replaced by agent-thread */
+}
+.msg.msg-continuation {
+  margin-top: 2px;
+}
+
+/* ===== AGENT THREAD TIMELINE ===== */
+.agent-thread {
+  position: relative;
+  margin: 2px 0 2px 28px;
+  padding: 4px 0 4px 22px;
+  max-width: calc(85% - 20px);
+  box-sizing: border-box;
+}
+.agent-thread::before {
+  content: '';
+  position: absolute;
+  left: 5px;
+  top: 14px;
+  bottom: 14px;
+  width: 2px;
+  background: color-mix(in srgb, var(--red) 18%, transparent);
+  border-radius: 1px;
+}
+/* Extend line to connect to chat bubble above/below */
+.agent-thread.has-top::before {
+  top: -6.5px;
+}
+.agent-thread.has-bottom::before {
+  bottom: -5px;
+}
+.agent-thread.has-ask-user-bottom::before {
+  bottom: -12px;
+}
+/* Terminating dot at bottom when no bubble below and last node is expanded */
+.agent-thread:not(.has-bottom) .agent-thread-node.open:last-child::after {
+  content: '';
+  position: absolute;
+  /* Centre the 6px dot on the rail: the rail sits at left:5px + 2px wide,
+     so its centre is 6px from the thread's left edge and the dot's left
+     edge belongs at 3px — i.e. 3px - padding-left. Keep this in step with
+     .agent-thread-dot and with the mobile padding override below. */
+  left: -19px;
+  bottom: 5px;
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--red);
+  box-shadow: 0 0 4px 1px color-mix(in srgb, var(--red) 55%, transparent),
+              0 0 0 3px color-mix(in srgb, var(--red) 18%, transparent);
+}
+
+/* Synapse pulse — a bright dot traveling down the line */
+.agent-thread::after {
+  content: '';
+  position: absolute;
+  left: 4px;
+  width: 4px;
+  height: 4px;
+  border-radius: 50%;
+  background: var(--red);
+  box-shadow: 0 0 3px 1px color-mix(in srgb, var(--red) 50%, transparent);
+  pointer-events: none;
+  top: 0%;
+  opacity: 0;
+}
+.agent-thread.streaming::after {
+  animation: synapse-capped-short 0.8s ease-in-out infinite;
+}
+.agent-thread.streaming.has-top::after {
+  animation: synapse-capped 0.8s ease-in-out infinite;
+}
+.agent-thread.streaming.has-bottom::after {
+  animation: synapse-travel-short 0.8s ease-in-out infinite;
+}
+.agent-thread.streaming.has-top.has-bottom::after {
+  animation: synapse-travel 0.8s ease-in-out infinite;
+}
+@keyframes synapse-travel {
+  0% { top: 0%; opacity: 0; }
+  5% { opacity: 0.5; }
+  85% { opacity: 0.35; }
+  100% { top: 100%; opacity: 0; }
+}
+@keyframes synapse-capped {
+  0% { top: 0%; opacity: 0; }
+  5% { opacity: 0.5; }
+  70% { opacity: 0.35; top: calc(100% - 20px); }
+  100% { opacity: 0; top: calc(100% - 20px); }
+}
+@keyframes synapse-travel-short {
+  0% { top: 14px; opacity: 0; }
+  5% { opacity: 0.5; }
+  85% { opacity: 0.35; }
+  100% { top: 100%; opacity: 0; }
+}
+@keyframes synapse-capped-short {
+  0% { top: 14px; opacity: 0; }
+  5% { opacity: 0.5; }
+  70% { opacity: 0.35; top: calc(100% - 20px); }
+  100% { opacity: 0; top: calc(100% - 20px); }
+}
+.agent-thread-node {
+  position: relative;
+  padding: 5px 0;
+}
+.agent-thread-node + .agent-thread-node {
+  margin-top: 2px;
+}
+/* Supervisor ladder cards — same chrome as tool cards but tinted so the
+   user can tell at a glance "this is the agent recovering" vs "this is
+   the agent doing work". Stop rung gets the red accent. */
+.agent-thread-node.supervisor-step .agent-thread-tool {
+  color: color-mix(in srgb, var(--accent, #c08a3e) 80%, var(--fg));
+  font-style: italic;
+}
+.agent-thread-node.supervisor-step .agent-thread-dot {
+  background: color-mix(in srgb, var(--accent, #c08a3e) 60%, transparent);
+}
+.agent-thread-node.supervisor-step[data-rung="stop"] .agent-thread-tool {
+  color: var(--red, #d65a5a);
+}
+.agent-thread-node.supervisor-step[data-rung="stop"] .agent-thread-dot {
+  background: color-mix(in srgb, var(--red, #d65a5a) 60%, transparent);
+}
+.agent-thread-dot {
+  position: absolute;
+  left: -20px;
+  top: 10px;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--red);
+  border: 2px solid var(--bg);
+  z-index: 1;
+}
+.agent-thread-node.running .agent-thread-dot {
+  background: var(--red);
+  box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent);
+  animation: thread-pulse 1.5s ease-in-out infinite;
+  top: 10px;
+}
+@keyframes thread-pulse {
+  0%, 100% { box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 20%, transparent); }
+  50% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--red) 10%, transparent); }
+}
+.agent-thread-node.error .agent-thread-dot {
+  background: var(--color-error);
+}
+.agent-thread-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  font-size: 0.85em;
+  color: color-mix(in srgb, var(--fg) 70%, transparent);
+  user-select: none;
+  padding: 2px 0;
+}
+.agent-thread-header:hover {
+  color: var(--fg);
+}
+.agent-thread-icon {
+  font-size: 0.9em;
+  color: var(--red);
+}
+.agent-thread-tool-icon {
+  width: 14px;
+  height: 14px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 14px;
+  color: color-mix(in srgb, var(--fg) 64%, transparent);
+  opacity: 0.85;
+}
+.agent-thread-tool-icon svg {
+  display: block;
+  width: 12px;
+  height: 12px;
+}
+.agent-thread-tool-icon-link {
+  color: inherit;
+  text-decoration: none;
+  cursor: pointer;
+}
+.agent-thread-tool-icon-link:hover {
+  color: var(--red);
+  opacity: 1;
+}
+.agent-thread-node.error .agent-thread-icon {
+  color: var(--color-error);
+}
+.agent-thread-tool {
+  font-weight: 600;
+  color: var(--red);
+  text-transform: uppercase;
+  letter-spacing: 0.3px;
+  font-size: 0.9em;
+}
+.agent-thread-header-link {
+  width: 17px;
+  height: 17px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 17px;
+  margin-left: -3px;
+  color: var(--red);
+  opacity: 0.72;
+  text-decoration: none;
+  border: 0;
+  background: transparent;
+}
+.agent-thread-header-link:hover {
+  opacity: 1;
+  color: var(--red);
+}
+.agent-thread-header-link svg {
+  display: block;
+}
+.agent-thread-header-link-calendar {
+  transform: translateY(-2px);
+}
+.msg-ai .body a[href^="#event-"],
+.msg-ai .body a[href^="#email-"],
+.msg-ai .body a[href^="#document-"],
+.msg-ai .body a[href^="#note-"],
+.msg-ai .body a[href^="#task-"],
+.msg-ai .body a[href^="#skill-"],
+.msg-ai .body a[href^="#memory-"],
+.msg-ai .body a[href^="#session-"] {
+  display: inline-flex;
+  align-items: baseline;
+  max-width: 100%;
+  padding: 1px 5px;
+  border-radius: 5px;
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+  color: var(--red);
+  font-weight: 600;
+  text-decoration: none;
+  box-decoration-break: clone;
+  -webkit-box-decoration-break: clone;
+}
+.msg-ai .body a[href^="#event-"],
+.msg-ai .body a[href^="#note-"],
+.msg-ai .body a[href^="#skill-"],
+.msg-ai .body a[href^="#memory-"],
+.msg-ai .body a[href^="#session-"] {
+  font-size: 11px;
+  line-height: 1.35;
+}
+.msg-ai .body p:has(> .emoji:first-child):has(> a.chat-link[href^="#note-"]) {
+  margin-top: 0.15em;
+  margin-bottom: 0.15em;
+  line-height: 1.35;
+}
+.msg-ai .body a[href^="#event-"]:hover,
+.msg-ai .body a[href^="#email-"]:hover,
+.msg-ai .body a[href^="#document-"]:hover,
+.msg-ai .body a[href^="#note-"]:hover,
+.msg-ai .body a[href^="#task-"]:hover,
+.msg-ai .body a[href^="#skill-"]:hover,
+.msg-ai .body a[href^="#memory-"]:hover,
+.msg-ai .body a[href^="#session-"]:hover {
+  background: color-mix(in srgb, var(--red) 18%, transparent);
+  text-decoration: underline;
+}
+.agent-thread-status {
+  font-size: 0.85em;
+  opacity: 0.5;
+}
+.agent-thread-chevron {
+  width: 12px;
+  height: 12px;
+  flex: 0 0 12px;
+  display: inline-grid;
+  place-items: center;
+  opacity: 0.4;
+}
+.agent-thread-chevron::before {
+  content: "";
+  width: 0;
+  height: 0;
+  border-top: 4px solid transparent;
+  border-bottom: 4px solid transparent;
+  border-left: 6px solid currentColor;
+  transform-origin: 50% 50%;
+  transition: transform 0.2s ease;
+}
+.agent-thread-node.open .agent-thread-chevron::before {
+  transform: rotate(90deg);
+}
+.agent-thread-node.browser-preview-node .agent-thread-chevron {
+  display: none;
+}
+.agent-thread-wave {
+  font-family: monospace;
+  font-size: 0.85em;
+  color: var(--red);
+  letter-spacing: -1px;
+}
+/* Live "cooking" timer on a running tool — prominent (accent, tabular) so a
+   long-running command always reads as alive, not frozen. */
+.agent-thread-elapsed {
+  margin: 0 6px 0 4px;
+  font-size: 11px;
+  font-weight: 600;
+  font-variant-numeric: tabular-nums;
+  color: var(--accent, var(--red));
+}
+/* Stall watchdog banner — shown when the stream has been silent for ~1min. */
+.stall-banner {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin: 8px auto;
+  padding: 8px 12px;
+  max-width: 90%;
+  font-size: 12px;
+  border-radius: 8px;
+  background: color-mix(in srgb, var(--color-warning, #f0ad4e) 12%, var(--bg));
+  border: 1px solid color-mix(in srgb, var(--color-warning, #f0ad4e) 40%, transparent);
+}
+.stall-banner-txt { flex: 1; opacity: 0.85; }
+.stall-banner-btn {
+  font-size: 11px;
+  font-weight: 600;
+  padding: 4px 10px;
+  border-radius: 6px;
+  border: none;
+  background: var(--accent, var(--red));
+  color: #fff;
+  cursor: pointer;
+  flex-shrink: 0;
+}
+.stall-banner-stop {
+  background: none;
+  color: var(--fg);
+  border: 1px solid var(--border);
+}
+.agent-thread-content {
+  display: none;
+  padding: 4px 0 2px 0;
+  overflow-x: auto;
+  overflow-y: hidden;
+}
+.agent-thread-node.open .agent-thread-content {
+  display: block;
+}
+.agent-thread-cmd {
+  background: color-mix(in srgb, var(--fg) 5%, transparent);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  padding: 8px 12px;
+  margin: 4px 0;
+  color: var(--fg);
+  font-size: 0.85em;
+  white-space: pre-wrap;
+  word-break: break-word;
+  line-height: 1.4;
+  overflow-x: auto;
+}
+.agent-thread-summary {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  max-width: 100%;
+  margin: 4px 0;
+  padding: 4px 8px;
+  border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 26%, var(--border));
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+  color: color-mix(in srgb, var(--fg) 84%, var(--accent, var(--red)));
+  font-size: 12px;
+  font-variant-numeric: tabular-nums;
+  line-height: 1.35;
+}
+.agent-thread-summary-link {
+  cursor: pointer;
+  text-decoration: none;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.agent-thread-summary-link:hover {
+  color: var(--accent, var(--red));
+  border-color: color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
+  background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+
+.agent-image-progress {
+  margin: 6px 0 0;
+}
+.agent-image-progress-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  font-size: 11px;
+  line-height: 1.2;
+  color: color-mix(in srgb, var(--fg) 72%, transparent);
+}
+.agent-image-progress-label {
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.agent-image-progress-value {
+  flex-shrink: 0;
+  font-variant-numeric: tabular-nums;
+  color: color-mix(in srgb, var(--accent, var(--red)) 75%, var(--fg));
+}
+.agent-image-progress-track {
+  display: none !important;
+}
+
+.private-browser-preview {
+  width: 100%;
+  max-height: min(300px, 36vh);
+  margin: 6px 0 2px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  border: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+  border-radius: 8px;
+  background: color-mix(in srgb, var(--red) 5%, transparent);
+  color: var(--fg);
+  box-shadow: none;
+}
+.private-browser-preview-header {
+  height: 32px;
+  min-height: 32px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 0 6px 0 10px;
+  border-bottom: 1px solid color-mix(in srgb, var(--red) 15%, transparent);
+  background: color-mix(in srgb, var(--red) 10%, transparent);
+  font-size: 0.85em;
+  font-weight: 500;
+  cursor: pointer;
+}
+.private-browser-preview-title {
+  flex-shrink: 0;
+  color: var(--red);
+  font-size: inherit;
+  font-weight: inherit;
+  line-height: 1;
+  letter-spacing: 0;
+}
+.private-browser-preview-status {
+  min-width: 0;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: color-mix(in srgb, var(--fg) 68%, transparent);
+  font-size: inherit;
+  line-height: 1;
+}
+.private-browser-preview-fold {
+  width: 18px;
+  height: 18px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  left: 2px;
+  flex-shrink: 0;
+  padding: 0;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  color: var(--red);
+  cursor: pointer;
+  font-size: 0.95em;
+  font-family: inherit;
+  font-weight: 500;
+  line-height: 1;
+  text-shadow: none;
+  -webkit-text-stroke: 0;
+}
+.private-browser-preview-fold:hover {
+  background: transparent;
+  color: var(--red);
+}
+.private-browser-preview-frame {
+  position: relative;
+  min-height: 130px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: color-mix(in srgb, #000 72%, var(--bg));
+  box-shadow:
+    inset 0 1px 0 color-mix(in srgb, #fff 8%, transparent),
+    inset 0 0 0 1px color-mix(in srgb, #000 35%, transparent),
+    0 8px 18px color-mix(in srgb, #000 18%, transparent);
+}
+.private-browser-preview[data-browser-url] .private-browser-preview-frame {
+  cursor: zoom-in;
+}
+.private-browser-preview[data-browser-url] .private-browser-preview-frame::after {
+  content: "";
+  position: absolute;
+  inset: 0;
+  pointer-events: none;
+  opacity: 0.55;
+  transition: opacity 150ms ease;
+  background:
+    radial-gradient(circle at center, transparent 42%, color-mix(in srgb, #000 30%, transparent) 100%),
+    color-mix(in srgb, var(--bg) 10%, transparent);
+}
+.private-browser-preview[data-browser-url] .private-browser-preview-frame:hover::after {
+  opacity: 0.9;
+}
+.private-browser-preview.folded .private-browser-preview-frame {
+  display: none;
+}
+.private-browser-preview.folded .private-browser-preview-header {
+  border-bottom: 0;
+}
+.private-browser-preview-frame img {
+  width: 100%;
+  height: auto;
+  max-height: calc(min(300px, 36vh) - 32px);
+  display: block;
+  object-fit: contain;
+  transform: scale(1.1);
+  transform-origin: center center;
+  animation: private-browser-image-in 180ms ease-out both;
+}
+.private-browser-preview-frame img:not([src]),
+.private-browser-preview-frame img[src=""] {
+  display: none;
+}
+.private-browser-preview-empty {
+  padding: 22px 14px;
+  width: auto;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 7px;
+  color: color-mix(in srgb, var(--fg) 48%, transparent);
+  font-size: 11px;
+  line-height: 1.35;
+}
+.private-browser-preview-spinner {
+  width: 13px;
+  height: 13px;
+  flex: 0 0 13px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+}
+.private-browser-preview-spinner .spinner-whirlpool,
+.private-browser-preview-spinner .ai-spinner-whirlpool {
+  margin: 0 !important;
+}
+@keyframes private-browser-image-in {
+  from { opacity: 0; transform: scale(1.08); }
+  to { opacity: 1; transform: scale(1.1); }
+}
+
+@media (max-width: 700px) {
+  .private-browser-preview-frame img {
+    max-height: calc(36vh - 32px);
+  }
+}
+
+/* Mobile: constrain thread content */
+@media (max-width: 768px) {
+  .agent-thread {
+    margin-left: 16px;
+    padding-left: 18px;
+    max-width: calc(90% - 16px);
+  }
+  .agent-thread-cmd {
+    font-size: 0.78em;
+    padding: 6px 8px;
+    max-width: 100%;
+    overflow-x: auto;
+  }
+  .agent-thread-content {
+    max-width: calc(100vw - 90px);
+  }
+  .agent-tool-output pre {
+    font-size: 0.8em;
+    max-width: 100%;
+    overflow-x: auto;
+  }
+  .agent-thread-header {
+    font-size: 0.8em;
+  }
+  .agent-thread-dot {
+    left: -16px;
+    top: 10px;
+  }
+  /* padding-left drops to 18px here, so the terminating dot needs its own
+     offset (3px - 18px) to stay centred on the rail. */
+  .agent-thread:not(.has-bottom) .agent-thread-node.open:last-child::after {
+    left: -15px;
+  }
+}
+
+/* ===== AGENT TOOL OUTPUT (inside thread nodes) ===== */
+.agent-tool-output {
+  margin-top: 8px;
+  background: color-mix(in srgb, var(--red) 5%, transparent);
+  border: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+  border-radius: 8px;
+  overflow: hidden;
+}
+.agent-tool-output summary {
+  color: var(--red);
+  background: color-mix(in srgb, var(--red) 10%, transparent);
+  border-bottom: 1px solid color-mix(in srgb, var(--red) 15%, transparent);
+  cursor: pointer;
+  font-size: 0.85em;
+  user-select: none;
+  padding: 6px 10px;
+  font-weight: 500;
+  /* Chevron on the right (like the thinking section) instead of the default
+     left-side disclosure triangle. */
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  list-style: none;
+}
+.agent-tool-output summary::-webkit-details-marker { display: none; }
+/* File-write diff — neutral chrome (not the red error tint) + colored lines */
+.agent-tool-diff {
+  background: color-mix(in srgb, var(--fg) 4%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.agent-tool-diff summary {
+  color: var(--fg);
+  background: color-mix(in srgb, var(--fg) 7%, transparent);
+  border-bottom-color: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.agent-tool-diff .diff-stat {
+  font-weight: 600;
+  opacity: 0.7;
+  font-family: var(--mono, monospace);
+}
+/* Collapsed diff summary: filename + +adds/−dels (theme green/red). */
+.agent-tool-diff summary {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.agent-tool-diff .diff-file {
+  font-family: var(--mono, monospace);
+  font-weight: 600;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.agent-tool-diff .diff-summary-stats {
+  margin-left: auto;
+  font-family: var(--mono, monospace);
+  font-weight: 600;
+  flex-shrink: 0;
+}
+.agent-tool-diff .diff-summary-stats .diff-stat-add { color: var(--green, #2ecc71); }
+.agent-tool-diff .diff-summary-stats .diff-stat-del { color: var(--red, #e74c3c); }
+.agent-tool-diff .diff-summary-stats .diff-stat-new { color: var(--accent, var(--red)); opacity: 0.85; }
+.diff-pre {
+  margin: 0;
+  padding: 8px 10px;
+  overflow-x: auto;
+  font-family: var(--mono, monospace);
+  font-size: 0.82em;
+  line-height: 1.45;
+}
+.diff-pre span { display: block; white-space: pre; }
+.diff-pre .diff-add { background: color-mix(in srgb, #2ecc71 22%, transparent); }
+.diff-pre .diff-del { background: color-mix(in srgb, #e74c3c 22%, transparent); }
+.diff-pre .diff-hunk { color: var(--accent); opacity: 0.85; }
+.diff-pre .diff-meta { opacity: 0.55; }
+.diff-pre .diff-ctx { opacity: 0.8; }
+/* Suppress the global `summary::before { content: '▶' }` left arrow — this
+   section uses a right-side chevron instead. */
+.agent-tool-output summary::before { content: none; }
+.agent-tool-output summary::after {
+  content: '\25BC'; /* ▼ */
+  color: var(--red);
+  font-size: 0.9em;
+  transition: transform 0.3s ease;
+}
+.agent-tool-output[open] > summary::after {
+  transform: rotate(180deg);
+}
+.agent-tool-output summary:hover {
+  background: color-mix(in srgb, var(--red) 15%, transparent);
+}
+.agent-thinking-dots .ai-spinner {
+  font-size: 12px;
+  letter-spacing: 0.5px;
+}
+
+.agent-tool-output[open] {
+  background: color-mix(in srgb, var(--red) 6%, transparent);
+}
+.agent-tool-output[open] > :not(summary) {
+  animation: detail-reveal 0.25s ease-out both;
+}
+@keyframes detail-reveal {
+  from { opacity: 0; transform: translateY(-4px); }
+  to   { opacity: 1; transform: translateY(0); }
+}
+.agent-tool-output pre {
+  background: transparent;
+  border: none;
+  border-radius: 0;
+  padding: 10px 14px;
+  margin-top: 0;
+  max-height: 300px;
+  overflow-y: auto;
+  font-size: 0.95em;
+  color: var(--fg);
+  opacity: 0.85;
+  white-space: pre-wrap;
+  word-break: break-all;
+  line-height: 1.5;
+}
+
+/* Mobile responsive styles */
+@media (max-width: 768px) {
+  #recording-indicator {
+    margin: 8px;
+    padding: 10px;
+  }
+
+  .recording-text {
+    font-size: 14px;
+  }
+
+  .stop-recording-btn {
+    padding: 6px 10px;
+    font-size: 12px;
+  }
+}
+/* ===== PANE STYLES (shared by compare) ===== */
+
+.pane-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 6px 0 4px;
+  border-bottom: 1px solid var(--border);
+}
+.close-split-btn,
+.pane-close-btn {
+  background: none;
+  border: none;
+  color: var(--color-error);
+  font-size: 18px;
+  cursor: pointer;
+  padding: 0 4px;
+  line-height: 1;
+  opacity: 0;
+  transition: opacity 0.15s;
+}
+.pane-header:hover .close-split-btn,
+.pane-header:hover .pane-close-btn {
+  opacity: 1;
+}
+.close-split-btn:hover,
+.pane-close-btn:hover {
+  color: var(--color-error-light);
+}
+
+/* Compare pane close controls use the same shared circular close treatment
+   as modal/title close buttons. */
+.compare-pane .pane-close-btn.close-btn {
+  width: 24px;
+  height: 24px;
+  min-width: 24px;
+  padding: 0;
+  border: 1px solid transparent;
+  border-radius: 999px;
+  background: transparent;
+  color: color-mix(in srgb, var(--fg) 58%, transparent);
+  font-size: 0;
+  opacity: 0.5;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  transition: background 0.15s, color 0.15s, opacity 0.15s;
+}
+.compare-pane .pane-close-btn.close-btn:hover,
+.compare-pane .pane-close-btn.close-btn:focus-visible {
+  background: color-mix(in srgb, var(--fg) 10%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+  color: var(--fg);
+  opacity: 1;
+  outline: none;
+}
+.compare-pane .pane-action-btn[data-action="expand"] {
+  width: 24px;
+  height: 24px;
+  min-width: 24px;
+  padding: 0;
+  border: 1px solid transparent;
+  border-radius: 999px;
+  justify-content: center;
+  transition: background 0.15s, color 0.15s, opacity 0.15s;
+}
+.compare-pane .pane-action-btn[data-action="expand"]:hover,
+.compare-pane .pane-action-btn[data-action="expand"]:focus-visible {
+  background: color-mix(in srgb, var(--fg) 10%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+  color: var(--fg);
+  opacity: 1;
+  outline: none;
+}
+
+
+/* ============================================ */
+/* RESEARCH DETAILS EXPANDABLE SECTION - UPDATED */
+/* ============================================ */
+
+/* Style the details element */
+details {
+  background: color-mix(in srgb, var(--hl-string) 5%, transparent);
+  border: 1px solid color-mix(in srgb, var(--hl-string) 30%, transparent);
+  border-radius: 8px;
+  margin: 12px 0;
+  padding: 0;
+  overflow: hidden;
+  transition: all 0.3s ease;
+}
+
+details[open] {
+  background: color-mix(in srgb, var(--hl-string) 8%, transparent);
+}
+details[open] > :not(summary) {
+  animation: detail-reveal 0.25s ease-out both;
+}
+
+/* Style the summary (clickable header) - NO CURSIVE, NORMAL SIZE */
+summary {
+  cursor: pointer;
+  padding: 10px 14px;
+  background: color-mix(in srgb, var(--hl-string) 10%, transparent);
+  border-bottom: 1px solid color-mix(in srgb, var(--hl-string) 20%, transparent);
+  font-weight: 500;
+  font-size: 0.95em;
+  font-style: normal;
+  font-family: inherit;
+  color: var(--hl-string);
+  user-select: none;
+  list-style: none;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  transition: background 0.2s ease;
+}
+
+summary:hover {
+  background: color-mix(in srgb, var(--hl-string) 15%, transparent);
+}
+
+/* Add custom arrow */
+summary::before {
+  content: '▶';
+  display: inline-block;
+  transition: transform 0.3s ease;
+  font-size: 0.75em;  /* Smaller arrow */
+}
+
+details[open] summary::before {
+  transform: rotate(90deg);
+}
+
+/* Hide default marker in webkit browsers */
+summary::-webkit-details-marker {
+  display: none;
+}
+
+/* Style the content inside details - SMALLER, MORE COMPACT */
+details > div,
+details > p,
+details > ul,
+details > ol {
+  padding: 12px 14px;  /* Less padding */
+  animation: fadeIn 0.3s ease;
+  font-size: 0.9em;  /* Smaller text */
+  line-height: 1.5;
+}
+
+@keyframes fadeIn {
+  from {
+    opacity: 0;
+    transform: translateY(-10px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* Style research findings inside details - SMALLER HEADINGS */
+details h3 {
+  margin-top: 12px;
+  margin-bottom: 6px;
+  color: var(--hl-string);
+  font-size: 0.95em;
+  font-weight: 500;
+}
+
+details h4 {
+  margin-top: 10px;
+  margin-bottom: 5px;
+  color: var(--hl-string);
+  font-size: 0.9em;
+  font-weight: 500;
+}
+
+details ul {
+  margin-left: 18px;
+  margin-bottom: 10px;
+}
+
+details li {
+  margin-bottom: 5px;
+  line-height: 1.4;
+  font-size: 0.85em;  /* Smaller list items */
+}
+
+details strong {
+  color: var(--hl-string);
+  font-weight: 500;
+}
+
+details a {
+  color: var(--accent);
+  text-decoration: none;
+  transition: color 0.2s ease;
+}
+
+details a:hover {
+  color: var(--color-link-hover);
+  text-decoration: underline;
+}
+
+/* Research metadata - SMALLER */
+details .research-meta {
+  font-size: 0.8em;
+  color: var(--fg);
+  opacity: 0.8;
+  margin-top: 4px;
+}
+
+/* Source links - SMALLER */
+details .source-link {
+  display: inline-block;
+  margin-top: 3px;
+  padding: 2px 5px;
+  background: color-mix(in srgb, var(--accent) 10%, transparent);
+  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
+  border-radius: 4px;
+  font-size: 0.68em;
+  color: var(--accent);
+}
+
+details .source-link:hover {
+  background: color-mix(in srgb, var(--accent) 20%, transparent);
+  border-color: var(--accent);
+}
+
+/* Research report links - clean and subtle */
+details a {
+  color: var(--accent);
+  text-decoration: none;
+  font-weight: normal;  /* Remove bold */
+  font-size: 0.85em;
+  opacity: 0.9;
+  transition: all 0.2s ease;
+  word-break: break-all;  /* Break long URLs nicely */
+}
+
+details a:hover {
+  color: var(--color-link-hover);
+  text-decoration: underline;
+  opacity: 1;
+}
+
+/* ============================================ */
+/* CUSTOM SYNTAX HIGHLIGHTING - Theme-Reactive  */
+/* ============================================ */
+.hljs {
+  background: var(--code-bg, var(--hl-bg, var(--panel)));
+  color: var(--code-fg, var(--hl-fg, var(--fg)));
+  padding: 8px;
+  border-radius: 4px;
+}
+
+/* Keywords & control flow — purple/magenta */
+.hljs-keyword,
+.hljs-selector-tag { color: var(--hl-keyword); }
+
+/* Strings & regex — warm yellow/orange */
+.hljs-string,
+.hljs-regexp,
+.hljs-addition { color: var(--hl-string); }
+
+/* Comments & docs — muted. font-style intentionally omitted: italics shift
+   glyph widths in the highlight overlay relative to the transparent textarea
+   above it, which makes the caret drift away from the visible character. */
+.hljs-comment,
+.hljs-quote,
+.hljs-meta { color: var(--hl-comment); }
+
+/* Functions & method names — blue */
+.hljs-function,
+.hljs-title,
+.hljs-title.function_,
+.hljs-section { color: var(--hl-function); }
+
+/* Numbers & constants — distinct from strings */
+.hljs-number,
+.hljs-literal { color: var(--hl-number, var(--hl-string)); }
+
+/* Built-ins & types — teal/cyan tint */
+.hljs-built_in,
+.hljs-type,
+.hljs-class,
+.hljs-title.class_ { color: var(--hl-builtin, var(--hl-function)); }
+
+/* Variables & identifiers — fg with slight distinction */
+.hljs-variable,
+.hljs-template-variable,
+.hljs-attr { color: var(--hl-variable, var(--hl-fg, var(--fg))); }
+
+/* Operators & punctuation — slightly dimmed fg */
+.hljs-operator,
+.hljs-punctuation { color: var(--hl-fg, var(--fg)); opacity: 0.8; }
+
+/* Parameters */
+.hljs-params { color: var(--hl-params, var(--hl-fg, var(--fg))); }
+
+/* Property access, attributes in HTML/XML */
+.hljs-property,
+.hljs-selector-class,
+.hljs-selector-id { color: var(--hl-variable, var(--hl-function)); }
+
+/* Tags (HTML) */
+.hljs-tag { color: var(--hl-keyword); }
+.hljs-name { color: var(--hl-keyword); }
+
+/* Deletion/diff */
+.hljs-deletion { color: var(--red); }
+
+/* Symbol, special */
+.hljs-symbol { color: var(--hl-string); }
+.hljs-link { color: var(--hl-function); text-decoration: underline; }
+
+/* Emphasis — applied globally (chat messages, rendered markdown in docs
+   preview, etc.) but explicitly NEUTRALIZED in the doc editor's overlay so
+   the textarea-and-overlay caret alignment stays bit-perfect. Bold / italic
+   shift glyph widths and that drifts the click-to-row mapping. */
+.hljs-emphasis { font-style: italic; }
+.hljs-strong { font-weight: bold; }
+.doc-editor-highlight .hljs-emphasis,
+.doc-editor-highlight .hljs-strong {
+  font-style: normal !important;
+  font-weight: inherit !important;
+}
+/* ===== Markdown highlighting — document editor =====
+   IMPORTANT: this is an overlay layered behind a transparent textarea, so
+   every styled token must occupy the EXACT same width as the corresponding
+   characters in the textarea. Anything that changes glyph metrics
+   (font-weight/style, padding, border, letter-spacing) makes the caret drift
+   away from the rendered glyph underneath. Color / background / text-
+   decoration are safe — they don't change layout width. */
+.doc-editor-highlight .language-markdown .hljs-section {
+  color: var(--hl-keyword, #c678dd);
+}
+.doc-editor-highlight .language-markdown .hljs-strong {
+  color: var(--hl-number, #d19a66);
+}
+.doc-editor-highlight .language-markdown .hljs-emphasis {
+  color: var(--hl-string, #e5c07b);
+}
+.doc-editor-highlight .language-markdown .hljs-bullet {
+  color: var(--hl-builtin, #56b6c2);
+}
+.doc-editor-highlight .language-markdown .hljs-code {
+  color: var(--hl-builtin, #56b6c2);
+  background: color-mix(in srgb, var(--hl-builtin, #56b6c2) 10%, transparent);
+  border-radius: 2px;
+}
+.doc-editor-highlight .language-markdown .hljs-link {
+  color: var(--hl-function, #61afef);
+  text-decoration: underline;
+}
+.doc-editor-highlight .language-markdown .hljs-quote {
+  color: var(--hl-comment, #828997);
+}
+.doc-editor-highlight .language-markdown .hljs-symbol {
+  color: var(--red);
+}
+/* Standalone [bracketed text] — scene directions, annotations */
+.doc-editor-highlight .language-markdown .md-bracket {
+  color: var(--hl-builtin, #56b6c2);
+  opacity: 0.85;
+}
+/* Heading # markers — dimmer than the heading text */
+.doc-editor-highlight .language-markdown .md-heading-marker {
+  color: var(--hl-comment, #828997);
+  font-weight: 400;
+}
+
+/* ===== CUSTOM PRESET MODAL ===== */
+
+.preset-modal-content {
+  width: min(460px, 90vw);
+  border-radius: 12px;
+  overflow: hidden;
+}
+.preset-modal-body {
+  display: flex;
+  flex-direction: column;
+  overflow-x: hidden;
+}
+
+/* Footer Start/Cancel buttons get a leading icon. Done via ::before + a masked
+   SVG (not an inline  child) because the labels are set with .textContent,
+   which would wipe a child element on every tab switch. background:currentColor
+   makes the icon follow the button's text color. */
+#save-custom-preset,
+#cancel-custom-preset {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+#save-custom-preset::before,
+#cancel-custom-preset::before {
+  content: "";
+  width: 13px; height: 13px;
+  flex-shrink: 0;
+  background-color: currentColor;
+  -webkit-mask: var(--_btn-ic) center / contain no-repeat;
+  mask: var(--_btn-ic) center / contain no-repeat;
+}
+#save-custom-preset::before {
+  --_btn-ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpolygon points='5 3 19 12 5 21 5 3'/%3E%3C/svg%3E");
+}
+#cancel-custom-preset::before {
+  --_btn-ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3C/svg%3E");
+}
+
+.preset-tabs {
+  display: flex;
+  gap: 0;
+  border-bottom: 1px solid var(--border);
+  margin: 0 -10px 12px;
+  padding: 0 10px;
+  margin: 0 -16px;
+  padding: 0 16px;
+  margin-bottom: 12px;
+}
+
+.preset-tab {
+  flex: 1;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 8px 10px;
+  background: none;
+  border: none;
+  border-bottom: 2px solid transparent;
+  color: var(--color-muted);
+  font-family: inherit;
+  font-size: 13px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.preset-tab-icon { flex-shrink: 0; }
+/* On narrow widths the icon + label can crowd 4 tabs — drop the labels to
+   icon-only so the row stays clean. */
+@media (max-width: 460px) {
+  .preset-tab span { display: none; }
+}
+
+.preset-tab:hover {
+  color: var(--fg);
+  background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+
+.preset-tab.active {
+  color: var(--red);
+  border-bottom-color: var(--red);
+}
+
+.preset-tab-content {
+  overflow: hidden;
+}
+.preset-tab-content.hidden {
+  display: none;
+}
+
+.preset-templates-hint {
+  font-size: 11px;
+  color: var(--color-muted);
+  margin: 0 0 8px;
+}
+
+.prompt-templates-list {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.prompt-template-card {
+  padding: 10px 12px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--fg) 3%, transparent);
+  cursor: pointer;
+  transition: all 0.15s;
+}
+
+.prompt-template-card:hover {
+  background: color-mix(in srgb, var(--accent) 8%, transparent);
+  border-color: color-mix(in srgb, var(--accent) 30%, transparent);
+}
+
+.prompt-template-card.selected {
+  border-color: var(--accent);
+  background: color-mix(in srgb, var(--accent) 10%, transparent);
+}
+
+.prompt-template-name {
+  font-size: 12px;
+  font-weight: 600;
+  color: var(--fg);
+  margin-bottom: 4px;
+}
+
+.prompt-template-preview {
+  font-size: 11px;
+  color: var(--color-muted);
+  line-height: 1.4;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.preset-slider-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 12px;
+  margin-bottom: 6px;
+}
+
+.preset-slider-row label {
+  font-size: 13px;
+  color: var(--fg);
+  font-weight: 500;
+  margin: 0;
+}
+
+.preset-slider-value {
+  font-size: 12px;
+  color: var(--fg);
+  font-weight: 600;
+  min-width: 40px;
+  text-align: right;
+}
+
+.preset-range {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 100%;
+  height: 6px;
+  background: var(--border);
+  border-radius: 4px;
+  outline: none;
+  margin-bottom: 4px;
+  box-sizing: border-box;
+  display: block;
+  padding: 0;
+  margin-left: 0;
+  margin-right: 0;
+}
+
+.preset-range::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: var(--red, var(--fg));
+  cursor: pointer;
+  border: 2px solid var(--panel);
+  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
+}
+
+.preset-range::-moz-range-thumb {
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: var(--red, var(--fg));
+  cursor: pointer;
+  border: 2px solid var(--panel);
+  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
+}
+
+.preset-range::-webkit-slider-runnable-track {
+  height: 6px;
+  border-radius: 4px;
+}
+
+.preset-range::-moz-range-track {
+  height: 6px;
+  background: var(--border);
+  border-radius: 4px;
+}
+
+.preset-temp-hints {
+  display: flex;
+  font-size: 10px;
+  color: var(--color-muted);
+  margin-top: -4px;
+  margin-bottom: 10px;
+  padding: 0 2px;
+  opacity: 0.7;
+}
+.preset-temp-hints span {
+  flex: 1;
+}
+.preset-temp-hints span:nth-child(2) {
+  text-align: center;
+}
+.preset-temp-hints span:last-child {
+  text-align: right;
+}
+
+.preset-clear-btn {
+  padding: 7px 14px;
+  background: none;
+  border: 1px solid var(--border);
+  color: var(--color-muted);
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 12px;
+  font-weight: 500;
+  transition: all 0.15s;
+}
+
+.preset-clear-btn:hover {
+  color: var(--color-error);
+  border-color: var(--color-error);
+}
+
+.preset-hint-icon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 15px;
+  height: 15px;
+  border-radius: 50%;
+  border: 1px solid var(--border);
+  font-size: 10px;
+  font-weight: 600;
+  color: var(--color-muted);
+  cursor: help;
+  vertical-align: middle;
+  margin-left: 4px;
+  transition: all 0.15s;
+}
+.preset-hint-icon:hover {
+  color: var(--fg);
+  border-color: var(--fg);
+}
+
+.preset-section-header {
+  font-size: 11px;
+  font-weight: 600;
+  color: var(--color-muted);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  margin-bottom: 6px;
+  padding: 0 2px;
+}
+
+.user-template-card {
+  position: relative;
+}
+.user-template-delete {
+  background: none;
+  border: none;
+  color: var(--color-muted);
+  font-size: 13px;
+  cursor: pointer;
+  padding: 0 2px;
+  line-height: 1;
+  opacity: 0;
+  transition: opacity 0.15s, color 0.15s;
+}
+.user-template-card:hover .user-template-delete {
+  opacity: 1;
+}
+.user-template-delete:hover {
+  color: var(--color-error);
+}
+
+.preset-save-template-btn {
+  padding: 7px 14px;
+  border-radius: 6px;
+  font-size: 12px;
+  font-weight: 500;
+  border: 1px solid var(--border);
+  background: none;
+  color: var(--fg);
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.preset-save-template-btn:hover {
+  border-color: var(--accent, var(--red));
+  color: var(--accent, var(--red));
+}
+
+.char-prompt-wrap {
+  position: relative;
+}
+.char-prompt-wrap textarea {
+  padding-bottom: 28px;
+}
+.char-expand-btn {
+  position: absolute;
+  bottom: 14px;
+  right: 6px;
+  background: var(--panel);
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  color: var(--color-muted);
+  font-size: 11px;
+  padding: 2px 8px;
+  cursor: pointer;
+  transition: all 0.15s;
+  margin: 0;
+  z-index: 1;
+}
+.char-expand-btn:hover {
+  color: var(--red);
+  border-color: var(--red);
+}
+.char-expand-btn.expanding {
+  opacity: 0.5;
+  pointer-events: none;
+}
+
+/* Memory scope bar (My Memories / Characters) */
+.memory-scope-bar {
+  display: flex;
+  gap: 0;
+  margin: 0 0 8px 0 !important;
+  padding: 0 !important;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  overflow: hidden;
+}
+.memory-scope-btn {
+  flex: 1;
+  padding: 8px 12px !important;
+  margin: 0 !important;
+  font-size: 11px !important;
+  font-weight: 600;
+  background: none;
+  border: none;
+  color: var(--fg);
+  opacity: 0.5;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.memory-scope-btn + .memory-scope-btn {
+  border-left: 1px solid var(--border);
+}
+.memory-scope-btn.active {
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+  color: var(--red);
+  opacity: 1;
+}
diff --git a/static/css/04-memory.css b/static/css/04-memory.css
new file mode 100644
index 000000000..71b2537bd
--- /dev/null
+++ b/static/css/04-memory.css
@@ -0,0 +1,1675 @@
+/* 04-memory - part of the former static/style.css.
+ *
+ * The stylesheet is split by area, but the split is mechanical: rules were
+ * moved verbatim and NOT regrouped. The numeric prefix is the load order, and
+ * that order is what preserves the cascade. A rule sits in the file its
+ * original position fell into, so an area's rules are not all in its file and
+ * a file is not only that area's rules.
+ *
+ * Do not reorder the  tags in static/index.html, and do not move a rule
+ * between files: with 1,151 redeclarations and 1,886 !important in this
+ * cascade, either can change which declaration wins.
+ *
+ * static/index.html and the PRECACHE list in static/sw.js must agree on this
+ * order. tests/test_static_stylesheet_manifest.py and
+ * tests/test_css_computed_style_snapshot.py fail if that drifts.
+ */
+.memory-scope-btn:hover:not(.active) {
+  background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.memory-char-list {
+  display: flex;
+  gap: 4px;
+  flex-wrap: wrap;
+  margin-bottom: 8px;
+}
+.memory-char-chip {
+  padding: 4px 10px;
+  font-size: 11px;
+  font-weight: 500;
+  border: 1px solid var(--border);
+  border-radius: 12px;
+  background: none;
+  color: var(--fg);
+  cursor: pointer;
+  transition: all 0.15s;
+  margin: 0;
+}
+.memory-char-chip.active {
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+  border-color: var(--red);
+  color: var(--red);
+}
+.memory-char-chip:hover:not(.active) {
+  background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+
+/* Disabled state dims the form */
+#char-fields-wrap.disabled {
+  opacity: 0.35;
+  pointer-events: none;
+  filter: grayscale(0.5);
+  transition: opacity 0.2s, filter 0.2s;
+}
+#char-fields-wrap {
+  transition: opacity 0.2s, filter 0.2s;
+}
+
+/* Name combo: input + delete btn */
+.char-name-combo {
+  display: flex;
+  gap: 4px;
+  align-items: center;
+  margin-bottom: 8px;
+}
+.char-name-combo input,
+.char-name-combo select {
+  flex: 1;
+}
+.char-template-select {
+  background: var(--panel);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  color: var(--fg);
+  padding: 6px 8px;
+  font-size: 13px;
+  font-family: inherit;
+  cursor: pointer;
+}
+.char-template-select:focus {
+  outline: none;
+  border-color: var(--red);
+}
+.char-action-btn {
+  background: none;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  color: var(--color-muted);
+  font-size: 11px;
+  padding: 4px 8px;
+  cursor: pointer;
+  transition: all 0.15s;
+  flex-shrink: 0;
+  margin: 0 !important;
+  white-space: nowrap;
+  /* Uniform width so the trailing button column matches between the select
+     row (+ New) and the name row (Reset) — that keeps the select and the
+     name input the same width, since both fields flex:1 into the leftover. */
+  min-width: 64px;
+  text-align: center;
+}
+.char-action-btn:hover {
+  color: var(--fg);
+  border-color: var(--fg);
+}
+#char-delete-template-btn:hover {
+  color: var(--color-error);
+  border-color: var(--color-error);
+}
+
+/* Character toggle row in preset modal */
+.preset-toggle-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 12px;
+  font-size: 13px;
+  color: var(--fg);
+}
+.preset-sub-option {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 6px;
+  padding: 8px 10px;
+  font-size: 12px;
+  color: var(--color-muted);
+  background: var(--panel);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+}
+.preset-mem-choice {
+  flex: 1;
+  padding: 6px 10px;
+  font-size: 12px;
+  font-weight: 500;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  background: none;
+  color: var(--fg);
+  cursor: pointer;
+  transition: all 0.15s;
+  margin: 0;
+}
+.preset-mem-choice.active {
+  background: color-mix(in srgb, var(--red) 12%, transparent);
+  border-color: var(--red);
+  color: var(--red);
+}
+.preset-mem-choice:hover:not(.active) {
+  background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+
+
+/* ===== MEMORY MODAL ===== */
+
+.memory-modal-content {
+  width: min(560px, 90vw);
+  max-height: 78vh;
+  font-size: 12px;
+  overflow: hidden;   /* clip both axes; the inner .memory-modal-body owns scrolling.
+                         (overflow-x alone promotes overflow-y to `auto` → a stray vertical scrollbar) */
+  /* Subtle synapse-pulse — a soft radial glow that breathes in/out.
+     Layered over the existing modal bg so it shows through without
+     overpowering content. */
+  position: relative;
+  isolation: isolate;
+}
+.memory-modal-content::before { content: none; }
+@media (prefers-reduced-motion: reduce) {
+  .memory-modal-content::before { animation: none; opacity: 0.4; }
+}
+.memory-modal-content .modal-header h4 {
+  font-size: 1rem;
+}
+
+.memory-modal-body {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  overflow-y: auto;
+  overflow-x: hidden;        /* Stop synapse-pulse pseudo-elements from triggering a sideways scrollbar */
+  overscroll-behavior: contain;
+  min-height: 0;
+  /* Fill the modal-content's height so the flex chain (tab-panel → admin-card
+     → list → expanded card) is bounded. Without this the chain grows to
+     content, so an expanded skill card pushed its footer off-screen; capping
+     the preview then left it floating too high. Bounding here lets the
+     preview flex to fill and the footer pin to the bottom naturally. */
+  flex: 1 1 auto;
+}
+.memory-tabs {
+  display: flex;
+  gap: 0;
+  border-bottom: 1px solid var(--border);
+  margin: -4px -4px 0;
+  padding: 0 4px;
+  flex-shrink: 0;
+}
+#memory-modal [data-memory-scope].hidden {
+  display: none !important;
+}
+.memory-tab {
+  background: none;
+  border: none;
+  color: var(--fg);
+  opacity: 0.5;
+  font-size: 12px;
+  font-family: inherit;
+  padding: 8px 14px;
+  cursor: pointer;
+  border-bottom: 2px solid transparent;
+  transition: opacity 0.15s, border-color 0.15s, color 0.15s, background 0.15s;
+}
+.memory-tab:hover {
+  opacity: 0.8;
+  background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.memory-tab.active {
+  opacity: 1;
+  color: var(--red);
+  border-bottom-color: var(--red);
+}
+.memory-tab-panel {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+  overflow-y: auto;
+  /* overflow-y:auto makes the browser compute overflow-x to `auto` too, which
+     produced a stray horizontal scrollbar whenever a child was slightly too
+     wide (long unbroken memory text, or the skills two-column row). Clip X
+     explicitly — inner code blocks keep their own overflow-x:auto. */
+  overflow-x: hidden;
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+}
+.memory-tab-panel.hidden { display: none; }
+/* Browse: bounded flex column so #memory-list gets remaining height (not 0px).
+   height:min(78vh,max-content) gives a definite cap when long, natural height
+   when short. flex-basis:auto (not 0) on the list avoids collapse in auto-sized
+   parents. Toolbar siblings are flex-shrink:0; only #memory-list grows. */
+#memory-modal .memory-modal-content:has(
+  .memory-tab-panel[data-memory-panel="browse"]:not(.hidden)
+) {
+  display: flex;
+  flex-direction: column;
+  max-height: 78vh;
+  height: 78vh;
+  overflow: hidden;
+}
+#memory-modal .memory-modal-content:has(
+  .memory-tab-panel[data-memory-panel="browse"]:not(.hidden)
+) .modal-header,
+#memory-modal .memory-modal-content:has(
+  .memory-tab-panel[data-memory-panel="browse"]:not(.hidden)
+) .memory-tabs {
+  flex: 0 0 auto;
+}
+#memory-modal .memory-modal-content:has(
+  .memory-tab-panel[data-memory-panel="browse"]:not(.hidden)
+) .memory-modal-body {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow: hidden;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow: hidden;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] > .admin-card {
+  display: flex;
+  flex-direction: column;
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow: hidden;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] > .admin-card > *:not(#memory-list):not(#memory-suggestions-body) {
+  flex: 0 0 auto;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] #memory-list:not(.hidden),
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] #memory-suggestions-body:not(.hidden) {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow-y: auto;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="browse"] #memory-suggestions-body:not(.hidden) .memory-suggestions-header {
+  flex-shrink: 0;
+  position: sticky;
+  top: 0;
+  z-index: 1;
+  background: var(--bg);
+}
+#memory-modal .memory-tab-panel[data-memory-panel].hidden {
+  display: none;
+}
+/* Settings cards dim + mute when their toggle is OFF (matches the
+   .memory-toolbar-toggle "off" treatment elsewhere). */
+#memory-modal .memory-tab-panel[data-memory-panel="settings"] .admin-card {
+  transition: opacity 0.15s, border-color 0.15s, background 0.15s;
+}
+#memory-modal .memory-tab-panel[data-memory-panel="settings"] .admin-card:has(.admin-switch input:not(:checked)) {
+  opacity: 0.55;
+  border-color: color-mix(in srgb, var(--fg) 8%, transparent);
+  background: color-mix(in srgb, var(--fg) 2%, transparent);
+}
+/* Skills tab — two-column layout: skills list (left, wider) + Add Skill
+   form (right, narrower). Collapses to a single column on narrow screens. */
+.memory-tab-panel[data-memory-panel="skills"] {
+  flex-direction: row;
+  align-items: stretch;
+  gap: 10px;
+}
+.memory-tab-panel[data-memory-panel="skills"] > .admin-card:first-of-type {
+  flex: 2 1 0;
+  min-width: 0;
+}
+/* The shared chip-scroll frame is a flex-growing row by default. In the
+   vertical Skills card it must stay content-sized so #skills-list keeps the
+   remaining height for the skill cards. */
+.memory-tab-panel[data-memory-panel="skills"] > .admin-card > .doclib-chip-scroll-frame {
+  flex: 0 0 auto;
+  min-height: 25px;
+}
+.memory-tab-panel[data-memory-panel="skills"] > .admin-card:nth-of-type(2) {
+  flex: 1 1 0;
+  margin-top: 0 !important;
+  min-width: 220px;
+}
+@media (max-width: 640px) {
+  .memory-tab-panel[data-memory-panel="skills"] { flex-direction: column; }
+  .memory-tab-panel[data-memory-panel="skills"] > .admin-card:nth-of-type(2) {
+    margin-top: 12px !important;
+  }
+}
+.memory-desc {
+  margin: 0;
+  font-size: 11px;
+  line-height: 1.5;
+  /* 65% keeps this description text above WCAG AA 4.5:1 (50% was ~3.9:1). */
+  color: color-mix(in srgb, var(--fg) 65%, transparent);
+}
+
+.memory-add-row {
+  display: flex;
+  gap: 6px;
+  align-items: center;
+  height: 32px;
+}
+
+/* Skill Import beside URL field — match input height; cancel modal-body button margin. */
+.memory-add-row .theme-io-btn {
+  flex: none;
+  height: 28px;
+  box-sizing: border-box;
+  margin-top: 0;
+  padding: 5px 10px;
+}
+
+.memory-add-input {
+  flex: 1;
+  height: 28px;
+  padding: 0 10px;
+  border-radius: 6px;
+  border: 1px solid var(--border);
+  background: var(--bg);
+  color: var(--fg);
+  font-family: inherit;
+  font-size: 12px;
+  box-sizing: border-box;
+}
+/* Textareas need explicit vertical padding — inputs vertically center text
+   via line-height/height; textareas would otherwise pin text to the top. */
+textarea.memory-add-input {
+  height: auto;
+  padding: 6px 10px;
+  line-height: 1.4;
+}
+
+.memory-add-input:focus {
+  outline: none;
+  border-color: var(--red);
+}
+
+.memory-add-btn {
+  width: 28px;
+  height: 28px;
+  border-radius: 6px;
+  border: 1px solid var(--border);
+  background: var(--bg);
+  color: var(--fg);
+  font-size: 16px;
+  box-sizing: border-box;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.15s;
+  flex-shrink: 0;
+}
+
+.memory-add-btn:hover {
+  background: var(--panel);
+  border-color: var(--red);
+  color: var(--red);
+}
+
+.memory-toolbar {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  padding: 4px 0 8px;
+}
+
+.memory-toolbar-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+
+
+.memory-toolbar-btn {
+  background: none;
+  border: 1px solid var(--border);
+  color: color-mix(in srgb, var(--fg) 60%, transparent);
+  font-size: 11px;
+  height: 24px;
+  padding: 0 8px;
+  border-radius: 6px;
+  cursor: pointer;
+  font-family: inherit;
+  transition: all 0.15s;
+  white-space: nowrap;
+}
+
+
+
+.memory-toolbar-btn:hover {
+  border-color: var(--fg);
+  color: var(--fg);
+}
+
+.memory-toolbar-btn.active {
+  background: color-mix(in srgb, var(--red) 15%, transparent);
+  border-color: color-mix(in srgb, var(--red) 40%, transparent);
+  color: var(--red);
+}
+.email-note-actions .memory-toolbar-btn.active:hover {
+  color: var(--fg);
+}
+
+.memory-toolbar-btn.danger {
+  color: var(--color-error);
+  border-color: var(--color-error);
+}
+
+.memory-toolbar-btn.danger:hover {
+  background: color-mix(in srgb, var(--color-error) 10%, transparent);
+}
+
+.memory-toolbar-btn:disabled {
+  opacity: 0.35;
+  cursor: default;
+  outline: none;
+}
+.memory-toolbar-btn.spinning {
+  border-color: transparent;
+  background: none;
+}
+.memory-toolbar-toggle {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-size: 11px;
+  height: 24px;
+  color: color-mix(in srgb, var(--fg) 60%, transparent);
+  cursor: pointer;
+  padding: 0 4px;
+  user-select: none;
+  transition: all 0.15s;
+}
+.memory-toolbar-toggle:hover {
+  color: var(--fg);
+}
+.memory-toolbar-toggle .admin-switch {
+  vertical-align: middle;
+}
+.memory-toolbar-toggle:has(input:not(:checked)) {
+  opacity: 0.7;
+}
+
+/* Bulk action bar */
+.memory-bulk-bar {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 6px 2px;
+  margin-bottom: 2px;
+  border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+  border-radius: 8px;
+  background: color-mix(in srgb, var(--red) 5%, transparent);
+  font-size: 11px;
+}
+
+.memory-bulk-bar.hidden {
+  display: none;
+}
+
+/* Custom Memory sort picker — replaces the native 

. */ +.a11y-visually-hidden { + position: absolute !important; + width: 1px !important; height: 1px !important; + padding: 0 !important; margin: -1px !important; + overflow: hidden !important; clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; border: 0 !important; +} + +/* ── Custom language type picker (replaces visible chrome of native */ +.adm-provider-picker { position: relative; margin-bottom: 6px; } +.adm-provider-combo { + display: flex; + align-items: stretch; +} +.adm-provider-combo input { + flex: 1; + min-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.adm-provider-btn { + width: 100%; + display: flex; align-items: center; gap: 8px; + background: var(--bg); color: var(--fg); + border: 1px solid var(--border); border-radius: 6px; + padding: 5px 8px; font-family: inherit; font-size: 12px; + cursor: pointer; text-align: left; +} +.adm-provider-combo .adm-provider-btn { + width: 128px; + flex-shrink: 0; + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + justify-content: space-between; +} +.adm-provider-btn:hover { border-color: color-mix(in srgb, var(--fg) 30%, var(--border)); } +.adm-provider-current { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; } +.adm-provider-logo { + width: 14px; height: 14px; + display: inline-flex; align-items: center; justify-content: center; + flex-shrink: 0; color: var(--fg); opacity: 0.85; +} +.adm-provider-logo svg { width: 14px; height: 14px; } +/* Pull the settings-select 4px closer to its logo chip so the row + reads as logo+select instead of having an extra gap. */ +.adm-model-logo + .settings-select { margin-left: -4px; } +/* Fallback rows: stretch the model select so the trash button on + the right aligns with the right edge of the Endpoint and Model + dropdowns above the fallback list. Without flex:1 the model + select shrinks to its content and the trash button rides up + tight to it. */ +.settings-fallback-row > .settings-select { flex: 1 1 0; min-width: 0; } +/* Constrain logo SVGs in settings dropdown chips. Many provider + icons in providers.js declare only viewBox (no width/height), + so without this rule the browser falls back to 300x150 and + blows up the row. */ +.settings-fallback-row > span[style*="width:18px"] svg:not([width]), +.settings-row > span[style*="width:18px"] svg:not([width]), +span[id$="-logo"] svg:not([width]) { + width: 100%; + height: 100%; + display: block; +} +.adm-provider-logo:empty { + background: color-mix(in srgb, var(--fg) 12%, transparent); + border-radius: 50%; +} +.adm-provider-caret { flex-shrink: 0; transition: transform 0.15s; } +.adm-provider-picker:has(.adm-provider-menu:not(.hidden)) .adm-provider-caret { + transform: rotate(180deg); +} +.adm-provider-menu { + position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 100; + max-height: 280px; overflow-y: auto; + background: var(--panel); + border: 1px solid var(--border); border-radius: 6px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25); + padding: 4px; +} +.adm-provider-menu.hidden { display: none; } +.adm-provider-item { + display: flex; align-items: center; gap: 8px; + padding: 5px 8px; border-radius: 4px; + font-size: 12px; cursor: pointer; +} +.adm-provider-item:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); } +.adm-provider-item.active { background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); } + +/* When the Appearance tab is open, the #settings-modal-opacity slider (in + the header) fades the modal so the user can see the rest of the UI react + as toggles flip. The fade is applied as a background color-mix in JS + (settings.js) rather than element opacity, so the controls/text stay + crisp — mirroring the Theme customizer's opacity slider. */ + +/* Search provider fallback chain — chips + drag-reorder */ +.search-fallback-chain { + flex: 1; display: flex; align-items: center; + flex-wrap: wrap; gap: 6px; min-height: 28px; +} +.search-fb-chip { + display: inline-flex; align-items: center; gap: 4px; + padding: 3px 4px 3px 6px; + background: color-mix(in srgb, var(--fg) 6%, transparent); + border: 1px solid var(--border); border-radius: 10px; + font-size: 11px; cursor: grab; user-select: none; +} +.search-fb-chip.dragging { opacity: 0.4; } +.search-fb-grip { opacity: 0.35; font-size: 10px; line-height: 1; cursor: grab; } +.search-fb-logo { + width: 12px; height: 12px; + display: inline-flex; align-items: center; justify-content: center; + flex-shrink: 0; opacity: 0.85; +} +.search-fb-logo svg { width: 12px; height: 12px; } +.search-fb-remove { + background: none; border: none; color: var(--fg); + opacity: 0.45; cursor: pointer; + font-size: 14px; line-height: 1; padding: 0 2px; + transition: opacity 0.15s, color 0.15s; +} +.search-fb-remove:hover { opacity: 1; color: var(--red); } +.search-fb-add { + background: var(--bg); color: var(--fg); + border: 1px dashed var(--border); border-radius: 10px; + padding: 2px 6px; font-family: inherit; font-size: 11px; + outline: none; cursor: pointer; +} +.search-fb-add:focus, +.search-fb-add:hover { + border-color: var(--accent, var(--red)); + border-style: solid; +} + +.mcp-tools-search { + width: 100%; + box-sizing: border-box; + margin-bottom: 6px; + padding: 5px 8px; + font-size: 11px; + font-family: inherit; + background: color-mix(in srgb, var(--fg) 4%, transparent); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--fg); + outline: none; +} +.mcp-tools-search:focus { + border-color: color-mix(in srgb, var(--accent) 60%, var(--border)); +} +.mcp-tools-count { + font-size: 11px; + font-weight: 600; + opacity: 0.7; +} + +/* Badges */ +.admin-badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + background: color-mix(in srgb, var(--red) 20%, transparent); + color: var(--red); + font-weight: 600; +} +.admin-badge-off { + background: color-mix(in srgb, var(--color-error) 20%, transparent); + color: var(--color-error); +} + +/* Buttons */ +.admin-btn-delete { + background: none; + border: 1px solid color-mix(in srgb, var(--color-error) 27%, transparent); + color: var(--color-error); + padding: 4px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 11px; + font-family: inherit; + transition: all 0.15s; +} +.admin-btn-delete:hover { + background: var(--color-error); + border-color: var(--color-error); + color: #fff; +} +.admin-btn-add { + padding: 4px 10px; + border: 1px solid var(--accent, var(--red)); + border-radius: 6px; + background: var(--accent, var(--red)); + color: var(--bg); + cursor: pointer; + font-weight: 600; + font-size: 11px; + font-family: inherit; + box-sizing: border-box; + transition: opacity 0.15s, background 0.15s, border-color 0.15s; +} +.admin-btn-add:hover { + background: var(--accent, var(--red)); + border-color: var(--accent, var(--red)); + opacity: 0.9; +} +.admin-btn-add:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.admin-btn-sm { + padding: 3px 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--panel); + color: var(--fg); + cursor: pointer; + font-size: 11px; +} +.admin-btn-sm:hover { + background: var(--border); + border-color: var(--red); +} +.admin-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid var(--border); + border-top-color: var(--red); + border-radius: 50%; + animation: admin-spin 0.6s linear infinite; + vertical-align: -2px; + margin-right: 2px; +} + +/* Forms */ +.admin-add-form { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 8px; +} +.admin-add-form input { + flex: 1; + min-width: 120px; + padding: 5px 8px; + height: 32px; + box-sizing: border-box; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-family: inherit; + font-size: 12px; +} +.admin-add-form input:focus { outline: none; border-color: var(--red); } +.admin-switch-inline { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: color-mix(in srgb, var(--fg) 60%, transparent); + cursor: default; +} + +.admin-model-form { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 8px; +} +.admin-model-form input, +.admin-model-form select { + padding: 5px 8px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 12px; +} +.admin-model-form input:focus { outline: none; border-color: var(--red); } +.admin-model-form-row { + display: flex; + gap: 6px; + flex-wrap: wrap; /* let buttons drop to a new row on narrow widths + instead of overflowing the modal */ + align-items: center; +} +.admin-model-form-row input { + flex: 1 1 180px; /* api-key input takes the long axis but allows + dropping below the buttons at small widths */ + min-width: 0; /* don't refuse to shrink past content width */ +} +.admin-model-form-row select { + flex: 0 0 auto; +} +.admin-model-form-row button { + flex: 0 0 auto; + white-space: nowrap; +} +/* On narrow screens, give buttons their own row + push the Add button + to the right so it remains the obvious primary action. */ +@media (max-width: 540px) { + .admin-model-form-row input { flex-basis: 100%; } + .admin-model-form-row .admin-btn-add { margin-left: auto; } +} +.adm-ep-inline-msg { + min-height: 16px; + margin-top: 5px; + font-size: 11px; +} + +/* Endpoint items */ +.admin-ep-item { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: var(--bg); + border-radius: 6px; + margin-bottom: 4px; + font-size: 12px; +} +.admin-ep-info { flex: 1; overflow: hidden; } +.admin-ep-name { color: var(--fg); font-weight: 500; } +.admin-ep-detail { + color: color-mix(in srgb, var(--fg) 40%, transparent); + font-size: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: -4px; +} + +/* Settings panel — tint each card's h2 title icon with the user's accent + colour. Direct-child selector (`> h2 > svg`) so SVGs nested inside + header buttons (Test, kebab, etc.) are untouched. Each title icon uses + stroke="currentColor", so setting color propagates; existing inline + opacity:0.6 stays so the tint reads soft. */ +/* Selected integration card — class beats inline borderColor reset */ +.intg-card.intg-card-active { border-color: var(--accent, var(--red)) !important; } + +/* Edge-dock resize handle: invisible at rest, accent stripe animates in + on hover. Inline background:transparent + transition come from + modalSnap.js; this rule paints the accent gradient on hover, beating + the inline style with !important. */ +.edge-dock-resize-handle-left:hover { + background: linear-gradient(to right, transparent 0 3px, color-mix(in srgb, var(--accent, var(--red)) 35%, transparent) 3px 7px, transparent 7px 10px) !important; +} +.edge-dock-resize-handle-right:hover { + background: linear-gradient(to left, transparent 0 3px, color-mix(in srgb, var(--accent, var(--red)) 35%, transparent) 3px 7px, transparent 7px 10px) !important; +} + +[data-settings-panel] .admin-card > h2 > svg, +#theme-popup .admin-card > h2 > svg, +[data-settings-panel="added-models"] .adm-ep-section-head > svg { + color: var(--accent, var(--red)); +} +.admin-ep-actions { + display: flex; + gap: 4px; + flex-shrink: 0; +} + +/* Status messages */ +.admin-error { color: var(--color-error); font-size: 11px; margin-top: 4px; } +.admin-success { color: var(--color-success); font-size: 11px; margin-top: 4px; } +.admin-empty { + color: color-mix(in srgb, var(--fg) 40%, transparent); + font-size: 12px; + padding: 10px 0; + text-align: center; +} +/* Endpoint-list empty states ("No endpoints configured" / "None") get a + left-aligned, accent-colored treatment so they read as "this row is your + placeholder, click Add" rather than a centered "nothing here" grey blob. */ +#adm-epList-local .admin-empty, +#adm-epList-api .admin-empty { + text-align: left; + padding: 8px 4px; + color: var(--accent-primary, var(--accent, var(--red))); +} + +/* RAG upload zone */ +.admin-rag-upload-zone { + border: 2px dashed var(--border); + border-radius: 8px; + padding: 14px; + text-align: center; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; + margin-bottom: 8px; +} +.admin-rag-upload-zone:hover, +.admin-rag-upload-zone.dragover { + border-color: var(--red); + background: color-mix(in srgb, var(--red) 7%, transparent); +} +.admin-rag-upload-zone p { + color: color-mix(in srgb, var(--fg) 50%, transparent); + font-size: 11px; + margin-top: 4px; +} +.admin-rag-icon { font-size: 18px; } +.admin-rag-dir-row { + display: flex; + gap: 6px; + margin-bottom: 8px; +} +.admin-rag-dir-row input { + flex: 1; + padding: 5px 8px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 12px; +} +.admin-rag-dir-row input:focus { outline: none; border-color: var(--red); } +.admin-rag-dir-row button { + padding: 5px 8px; + border: none; + border-radius: 6px; + background: var(--red); + color: #fff; + cursor: pointer; + font-size: 12px; + white-space: nowrap; +} +.admin-rag-toolbar { + display: flex; + justify-content: flex-end; + margin-bottom: 6px; +} +.admin-rag-section-label { + font-size: 10px; + color: color-mix(in srgb, var(--fg) 50%, transparent); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 8px 0 4px; +} +.admin-rag-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + background: var(--bg); + border-radius: 6px; + margin-bottom: 4px; + font-size: 12px; +} +.admin-rag-item-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--fg); +} +.admin-rag-item-meta { + color: color-mix(in srgb, var(--fg) 40%, transparent); + font-size: 10px; + margin: 0 6px; + flex-shrink: 0; +} +.admin-rag-status { + font-size: 11px; + color: color-mix(in srgb, var(--fg) 50%, transparent); + margin-top: 6px; +} + +/* Token reveal */ +.admin-token-reveal { + margin-top: 8px; + padding: 8px; + background: color-mix(in srgb, var(--red) 8%, var(--bg)); + border: 1px solid color-mix(in srgb, var(--red) 25%, var(--border)); + border-radius: 8px; +} + +/* Selects in admin cards */ +.admin-card select { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 12px; +} +.admin-card input { + font-family: inherit; + font-size: 12px; +} +.admin-card label { + font-size: 12px; +} + +/* ---- Document Library ---- */ + +.doclib-modal-content { + width: min(600px, 92vw); + max-height: 85vh; + font-size: 12px; + background: var(--bg); +} +/* Anchor doclib modal to top so only the bottom edge moves on resize */ +#doclib-modal { + align-items: flex-start; + padding-top: 8vh; +} +.doclib-modal-content .modal-header h4 { + font-size: 1rem; +} +/* The "open in new tab" email modal inherited the 1rem doclib header, which + read way bigger than the email subject in the regular (inline) reader. Pin + it smaller so the two views are consistent. */ +.email-reader-tab-modal .modal-header h4 { + font-size: 13px; + font-weight: 600; +} +/* Match the sticky modal-header to the doclib modal-content body color + (var(--bg)) instead of the global var(--panel) default — otherwise the + header reads as a darker stripe above the email list. */ +.doclib-modal-content > .modal-header { + background: var(--bg); +} +.doclib-stats { + font-size: 11px; + color: var(--fg-muted); + margin-bottom: 8px; +} +.doclib-toolbar { + display: flex; + gap: 8px; + margin-bottom: 8px; + align-items: center; +} +.doclib-search { + flex: 1; + padding: 6px 8px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 12px; + font-family: inherit; + outline: none; + transition: border-color 0.15s; +} +#archive-select-btn { height: auto; padding: 5px 8px; } +.doclib-search:focus { + border-color: var(--red); +} +.doclib-sort { + padding: 6px 8px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); + font-size: 12px; + font-family: inherit; + outline: none; + cursor: pointer; +} +.doclib-chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 8px; +} +.doclib-chip { + padding: 2px 10px; + border-radius: 12px; + font-size: 10px; + border: 1px solid var(--border); + background: transparent; + color: var(--fg-muted); + cursor: pointer; + user-select: none; + transition: background 0.15s, border-color 0.15s; +} +.doclib-chip:hover { + border-color: var(--red); +} +.doclib-chip.active { + background: color-mix(in srgb, var(--red) 15%, transparent); + border-color: color-mix(in srgb, var(--red) 40%, transparent); + color: var(--red); +} +/* Document library — language chip row */ +.doclib-lang-chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + padding: 2px 0; + max-height: 52px; + overflow-y: auto; +} +.doclib-lang-chips:empty { display: none; } +/* Mobile: keep tag chips on ONE row and scroll horizontally instead of wrapping + into a tall multi-line block (Serve, library — anywhere .doclib-lang-chips is used). */ +@media (max-width: 768px) { + .doclib-lang-chips { + flex-wrap: nowrap; + overflow-x: auto; + overflow-y: hidden; + max-height: none; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + .doclib-lang-chips::-webkit-scrollbar { display: none; } + .doclib-lang-chips > * { flex-shrink: 0; } +} +#doclib-tidy-btn, #doclib-select-btn, #doclib-chats-tidy-btn { + position: relative; + top: -3px; +} +/* Document library — list layout */ +.doclib-grid { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 400px; + overflow-y: auto; + padding: 0; + position: relative; +} + +/* Gallery grid domino-in cascade on open — same recipe as the document/email + libraries. Applied to #gallery-grid in gallery.js on the first render after + each open, removed after the longest delay so re-renders feel instant. */ +.gallery-just-opened > .gallery-card { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; +} +.gallery-just-opened > :nth-child(1) { animation-delay: 0.02s; } +.gallery-just-opened > :nth-child(2) { animation-delay: 0.04s; } +.gallery-just-opened > :nth-child(3) { animation-delay: 0.06s; } +.gallery-just-opened > :nth-child(4) { animation-delay: 0.08s; } +.gallery-just-opened > :nth-child(5) { animation-delay: 0.10s; } +.gallery-just-opened > :nth-child(6) { animation-delay: 0.12s; } +.gallery-just-opened > :nth-child(7) { animation-delay: 0.14s; } +.gallery-just-opened > :nth-child(8) { animation-delay: 0.16s; } +.gallery-just-opened > :nth-child(9) { animation-delay: 0.18s; } +.gallery-just-opened > :nth-child(10) { animation-delay: 0.20s; } +.gallery-just-opened > :nth-child(11) { animation-delay: 0.22s; } +.gallery-just-opened > :nth-child(12) { animation-delay: 0.24s; } +.gallery-just-opened > :nth-child(13) { animation-delay: 0.26s; } +.gallery-just-opened > :nth-child(14) { animation-delay: 0.28s; } +.gallery-just-opened > :nth-child(15) { animation-delay: 0.30s; } +.gallery-just-opened > :nth-child(16) { animation-delay: 0.32s; } +.gallery-just-opened > :nth-child(17) { animation-delay: 0.34s; } +.gallery-just-opened > :nth-child(18) { animation-delay: 0.36s; } +.gallery-just-opened > :nth-child(19) { animation-delay: 0.38s; } +.gallery-just-opened > :nth-child(20) { animation-delay: 0.40s; } +.gallery-just-opened > :nth-child(n+21) { animation-delay: 0.42s; } + +/* Tasks list domino-in cascade on open — mirrors gallery / doclib. Applied to + #tasks-list in tasks.js on the first render after each open, removed after + the longest delay so re-renders feel instant. */ +.tasks-just-opened > .task-card { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; +} +.tasks-just-opened > :nth-child(1) { animation-delay: 0.02s; } +.tasks-just-opened > :nth-child(2) { animation-delay: 0.04s; } +.tasks-just-opened > :nth-child(3) { animation-delay: 0.06s; } +.tasks-just-opened > :nth-child(4) { animation-delay: 0.08s; } +.tasks-just-opened > :nth-child(5) { animation-delay: 0.10s; } +.tasks-just-opened > :nth-child(6) { animation-delay: 0.12s; } +.tasks-just-opened > :nth-child(7) { animation-delay: 0.14s; } +.tasks-just-opened > :nth-child(8) { animation-delay: 0.16s; } +.tasks-just-opened > :nth-child(9) { animation-delay: 0.18s; } +.tasks-just-opened > :nth-child(10) { animation-delay: 0.20s; } +.tasks-just-opened > :nth-child(11) { animation-delay: 0.22s; } +.tasks-just-opened > :nth-child(12) { animation-delay: 0.24s; } +.tasks-just-opened > :nth-child(13) { animation-delay: 0.26s; } +.tasks-just-opened > :nth-child(14) { animation-delay: 0.28s; } +.tasks-just-opened > :nth-child(15) { animation-delay: 0.30s; } +.tasks-just-opened > :nth-child(16) { animation-delay: 0.32s; } +.tasks-just-opened > :nth-child(17) { animation-delay: 0.34s; } +.tasks-just-opened > :nth-child(18) { animation-delay: 0.36s; } +.tasks-just-opened > :nth-child(19) { animation-delay: 0.38s; } +.tasks-just-opened > :nth-child(20) { animation-delay: 0.40s; } +.tasks-just-opened > :nth-child(n+21) { animation-delay: 0.42s; } + +/* Document library cascade — same recipe as email library, applied per-tab + the first time content loads (chats / archive / research / documents). + Module-level Set in documentLibrary.js prevents re-firing on tab swaps + or filter/sort re-renders within the same page session. */ +.doclib-just-opened > .memory-item, +.doclib-just-opened > .doclib-card { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; +} +.doclib-just-opened > :nth-child(1) { animation-delay: 0.02s; } +.doclib-just-opened > :nth-child(2) { animation-delay: 0.04s; } +.doclib-just-opened > :nth-child(3) { animation-delay: 0.06s; } +.doclib-just-opened > :nth-child(4) { animation-delay: 0.08s; } +.doclib-just-opened > :nth-child(5) { animation-delay: 0.10s; } +.doclib-just-opened > :nth-child(6) { animation-delay: 0.12s; } +.doclib-just-opened > :nth-child(7) { animation-delay: 0.14s; } +.doclib-just-opened > :nth-child(8) { animation-delay: 0.16s; } +.doclib-just-opened > :nth-child(9) { animation-delay: 0.18s; } +.doclib-just-opened > :nth-child(10) { animation-delay: 0.20s; } +.doclib-just-opened > :nth-child(11) { animation-delay: 0.22s; } +.doclib-just-opened > :nth-child(12) { animation-delay: 0.24s; } +.doclib-just-opened > :nth-child(13) { animation-delay: 0.26s; } +.doclib-just-opened > :nth-child(14) { animation-delay: 0.28s; } +.doclib-just-opened > :nth-child(15) { animation-delay: 0.30s; } +.doclib-just-opened > :nth-child(16) { animation-delay: 0.32s; } +.doclib-just-opened > :nth-child(17) { animation-delay: 0.34s; } +.doclib-just-opened > :nth-child(18) { animation-delay: 0.36s; } +.doclib-just-opened > :nth-child(19) { animation-delay: 0.38s; } +.doclib-just-opened > :nth-child(20) { animation-delay: 0.40s; } +.doclib-just-opened > :nth-child(n+21) { animation-delay: 0.42s; } + +/* Domino cascade on first open of the email library — mirrors the sidebar + .section-just-expanded recipe so list items spring in staggered. Class + is applied to #email-lib-grid in emailLibrary.js on the first render + only, then removed after the longest delay so re-renders feel instant. */ +.email-lib-just-opened :is(.doclib-card, .date-section-header) { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; +} +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(1) { animation-delay: 0.02s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(2) { animation-delay: 0.04s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(3) { animation-delay: 0.06s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(4) { animation-delay: 0.08s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(5) { animation-delay: 0.10s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(6) { animation-delay: 0.12s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(7) { animation-delay: 0.14s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(8) { animation-delay: 0.16s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(9) { animation-delay: 0.18s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(10) { animation-delay: 0.20s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(11) { animation-delay: 0.22s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(12) { animation-delay: 0.24s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(13) { animation-delay: 0.26s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(14) { animation-delay: 0.28s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(15) { animation-delay: 0.30s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(16) { animation-delay: 0.32s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(17) { animation-delay: 0.34s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(18) { animation-delay: 0.36s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(19) { animation-delay: 0.38s; } +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(20) { animation-delay: 0.40s; } +/* Cap the cascade at 20 — beyond that they animate together so opening a + long inbox doesn't take forever to settle. */ +.email-lib-just-opened :is(.doclib-card, .date-section-header):nth-child(n+21) { animation-delay: 0.42s; } + +/* Inside the email library modal, the grid needs to grow with the modal — + but `flex: 1 1 0` collapses to 0 when the parent isn't height-constrained + (which is the non-fullscreen default). Use `auto` basis + a sensible + `min-height` floor so it shows ~400px naturally and absorbs more when + the modal is resized / fullscreened. */ +#email-lib-modal .doclib-grid { + max-height: none; + height: auto; + flex: 1 1 auto; + min-height: 400px; +} + +/* ── Mobile compose FAB (email) ── + Replaces the top-right "New" button on phones with a round mail-icon button + bottom-right that collapses to a circle while the list scrolls and springs + back out to "New" when scrolling stops. Desktop is unchanged. */ +.email-lib-fab { display: none; } +@media (max-width: 768px) { + /* The absolute FAB anchors to the email panel card. */ + #email-lib-modal .admin-card { position: relative; } + /* Hide the toolbar "New" button — the FAB is the mobile entry point. */ + #email-lib-compose-btn { display: none; } + + #email-lib-modal .email-lib-fab { + --fab-size: 56px; + display: flex; + align-items: center; + gap: 9px; + /* Hidden until the email list has rendered; JS adds .fab-revealed to pop + it in (scale-from-center). Avoids the button flashing at the top and + sliding down before _positionFab() places it. */ + transform: scale(0); + opacity: 0; + transform-origin: center center; + position: absolute; + right: calc(16px + env(safe-area-inset-right, 0px)); + bottom: calc(18px + env(safe-area-inset-bottom, 0px)); + height: var(--fab-size); + min-width: var(--fab-size); + padding: 0 20px 0 18px; + border: none; + border-radius: 999px; + background: var(--accent-primary, var(--red)); + color: #fff; + font-family: inherit; + font-size: 15px; + font-weight: 600; + line-height: 1; + cursor: pointer; + box-shadow: 0 6px 18px rgba(0,0,0,.38), 0 2px 6px rgba(0,0,0,.28); + z-index: 30; + pointer-events: auto; + /* This (base) transition governs the EXPAND — slower + graceful. */ + transition: + padding 420ms cubic-bezier(0.22, 1, 0.36, 1), + box-shadow 240ms ease, + transform 140ms ease, + background 140ms ease; + will-change: padding, transform; + } + #email-lib-modal .email-lib-fab svg { flex: 0 0 auto; display: block; } + #email-lib-modal .email-lib-fab .email-lib-fab-label { + overflow: hidden; + white-space: nowrap; + max-width: 120px; + opacity: 1; + /* EXPAND timing — label eases out a touch after the width opens up. */ + transition: + max-width 420ms cubic-bezier(0.22, 1, 0.36, 1), + opacity 300ms 60ms cubic-bezier(0.22, 1, 0.36, 1), + margin 420ms cubic-bezier(0.22, 1, 0.36, 1); + } + /* Collapsed (while scrolling) → pure circle, icon only. We DON'T set an + explicit width: the container is auto-width and shrinks smoothly as the + padding + label max-width animate (min-width keeps it circular). Setting a + fixed width here made the collapse snap, since auto↔px width can't tween. + This (.collapsed) transition governs the COLLAPSE — kept snappy. */ + #email-lib-modal .email-lib-fab.collapsed { + padding: 0; + justify-content: center; + transition: + padding 230ms cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 200ms ease, + transform 140ms ease; + } + #email-lib-modal .email-lib-fab.collapsed .email-lib-fab-label { + max-width: 0; + opacity: 0; + margin-left: -9px; + transition: + max-width 230ms cubic-bezier(0.4, 0, 0.2, 1), + opacity 150ms cubic-bezier(0.4, 0, 0.2, 1), + margin 230ms cubic-bezier(0.4, 0, 0.2, 1); + } + /* Entrance: circle-expand-from-middle pop, played once when revealed. No + `forwards` fill so the resting transform (scale 1) comes from this rule — + that keeps the :active press transform working afterwards. */ + #email-lib-modal .email-lib-fab.fab-revealed { + transform: scale(1); + opacity: 1; + animation: emailFabPop 380ms cubic-bezier(0.34, 1.56, 0.64, 1); + } + #email-lib-modal .email-lib-fab:active { + transform: scale(0.93); + filter: brightness(0.92); + box-shadow: 0 3px 10px rgba(0,0,0,.4); + } +} +@keyframes emailFabPop { + from { transform: scale(0); opacity: 0; } + 60% { transform: scale(1.06); opacity: 1; } + to { transform: scale(1); opacity: 1; } +} +@media (prefers-reduced-motion: reduce) { + #email-lib-modal .email-lib-fab, + #email-lib-modal .email-lib-fab .email-lib-fab-label { transition-duration: 1ms; } + #email-lib-modal .email-lib-fab.fab-revealed { animation: none; } +} +/* When the modal-content has explicit height (fullscreen or user-resized), + drop the floor so the grid sizes purely from flex. */ +#email-lib-modal.email-lib-fullscreen .doclib-grid, +#email-lib-modal .modal-content[style*="height"] .doclib-grid { + min-height: 0; +} +/* Document library: same fix as the email library (#74). When fullscreened + the modal-content gets an inline `height: 100vh`, but the inner + .doclib-grid stays capped at the 400px default so the list looks like a + tiny strip floating in a giant empty modal. Mirror the email recipe: + make the modal a flex column, give the body/admin-card claim of the + remaining height, and let the grid take the rest. Scoped to the + fullscreen class so windowed-mode layout is unchanged. */ +/* Inner flex chain that lets the chats/documents grid claim the full + remaining height of the modal. Applies in BOTH fullscreen AND + right-docked states — without the docked state included, dragging a + fullscreen doclib to the right snap zone breaks the flex layout + because exitFullscreen removes .doclib-fullscreen, and the grid + falls back to its base max-height: 400px showing only ~8 items. + The CSS variable :is() selector lets both states share one rule. */ +#doclib-modal.doclib-fullscreen .doclib-modal-content, +#doclib-modal.modal-right-docked .doclib-modal-content, +#doclib-modal.modal-left-docked .doclib-modal-content { + display: flex; + flex-direction: column; + overflow: hidden; +} +/* Fullscreen positioning — scoped to NOT apply when also right-docked. + Without this exclusion, dragging a fullscreen doclib to the right snap + zone keeps .doclib-fullscreen on the modal and these !important rules + override the dock's inline styles, leaving the modal stuck fullscreen + instead of becoming a side panel. */ +#doclib-modal.doclib-fullscreen:not(.modal-right-docked) .doclib-modal-content { + position: fixed !important; + top: 0 !important; + left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important; + right: 0 !important; + bottom: 0 !important; + width: auto !important; + max-width: none !important; + height: 100vh !important; + max-height: 100vh !important; + border-radius: 0 !important; + transform: none !important; +} +#doclib-modal.doclib-fullscreen .modal-header, +#doclib-modal.doclib-fullscreen .lib-tabs, +#doclib-modal.modal-right-docked .modal-header, +#doclib-modal.modal-right-docked .lib-tabs, +#doclib-modal.modal-left-docked .modal-header, +#doclib-modal.modal-left-docked .lib-tabs { + flex: 0 0 auto; +} +#doclib-modal.doclib-fullscreen .modal-body, +#doclib-modal.modal-right-docked .modal-body, +#doclib-modal.modal-left-docked .modal-body { + flex: 1 1 0; + min-height: 0; + overflow: hidden; + display: flex; + flex-direction: column; +} +#doclib-modal.doclib-fullscreen .admin-card, +#doclib-modal.modal-right-docked .admin-card, +#doclib-modal.modal-left-docked .admin-card { + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; +} +#doclib-modal.doclib-fullscreen .doclib-grid:not(:has(.doclib-card-expanded)), +#doclib-modal.modal-right-docked .doclib-grid:not(:has(.doclib-card-expanded)), +#doclib-modal.modal-left-docked .doclib-grid:not(:has(.doclib-card-expanded)) { + max-height: none; + height: auto; + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; +} +/* "Reply" docks the open email modal to the left as a sidebar so the doc + panel (which slides in from the right of the chat area) is visible + side-by-side. The .modal backdrop is already pointer-events:none for + email modals — only the content takes pointer events — so the chat / + doc panel underneath stays interactive. */ +.modal.email-snap-left { + align-items: stretch; + justify-content: flex-start; + left: 0 !important; + width: 100% !important; +} +.modal.email-snap-left .modal-content { + left: var(--email-doc-split-left-x, 0px) !important; + width: var(--email-doc-split-email-w, 420px) !important; + max-width: var(--email-doc-split-email-w, 420px) !important; + border-right: 1px solid var(--border); + box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18); + border-radius: 0; +} +@media (min-width: 769px) { + body.email-doc-split-active.doc-view #email-lib-modal, + body.email-doc-split-active.doc-view.email-front #email-lib-modal, + body.email-doc-split-active.doc-view .modal[id^="email-reader-"], + body.email-doc-split-active.doc-view.email-front .modal[id^="email-reader-"] { + z-index: 150 !important; + } + body.email-doc-split-active #email-lib-modal.email-snap-left, + body.email-doc-split-active #email-lib-modal.modal-left-docked { + left: var(--email-doc-split-left-x, 0px) !important; + width: var(--email-doc-split-email-w, 420px) !important; + overflow: hidden !important; + z-index: 155 !important; + } + body.email-doc-split-active #email-lib-modal.email-snap-left:not(.modal-dragging) .modal-content, + body.email-doc-split-active #email-lib-modal.modal-left-docked:not(.modal-dragging) .modal-content, + body.email-doc-split-active .modal[id^="email-reader-"].email-snap-left:not(.modal-dragging) .modal-content, + body.email-doc-split-active .modal[id^="email-reader-"].modal-left-docked:not(.modal-dragging) .modal-content { + position: absolute !important; + left: 0 !important; + top: 0 !important; + bottom: 0 !important; + right: auto !important; + width: var(--email-doc-split-email-w, 420px) !important; + max-width: var(--email-doc-split-email-w, 420px) !important; + height: 100vh !important; + max-height: 100vh !important; + transform: none !important; + margin: 0 !important; + } + body.email-doc-split-active.doc-view .doc-divider { + display: none !important; + } + body.email-doc-split-active.doc-view .doc-editor-pane { + position: fixed !important; + left: var(--email-doc-split-right-x, 420px) !important; + right: var(--right-dock-w, 0px) !important; + top: 0 !important; + bottom: 0 !important; + width: auto !important; + max-width: none !important; + height: 100vh !important; + z-index: 260 !important; + margin-top: 0 !important; + transform: none !important; + border-left: none !important; + } +} + +/* Edge docking. Docked panels are fixed to the viewport edge; the workspace + panes reserve room with margins so left + right docks can be active at the + same time without skewing the entire body box. */ +body.right-dock-active { + padding-right: 0; +} +body.left-dock-active { + padding-left: 0; +} +body.left-dock-active:not(.email-doc-split-active) .chat-container { + margin-left: var(--left-dock-w, 0px); +} +body.right-dock-active .chat-container, +body.right-dock-active:not(.email-doc-split-active) .doc-editor-pane { + margin-right: var(--right-dock-w, 0px); +} +.modal.modal-right-docked { + align-items: stretch; + justify-content: flex-end; +} +.modal.modal-right-docked .modal-content { + border-left: 1px solid var(--border); + box-shadow: -4px 0 14px rgba(0, 0, 0, 0.18); + border-radius: 0; +} diff --git a/static/css/07-documents.css b/static/css/07-documents.css new file mode 100644 index 000000000..ea843c0ca --- /dev/null +++ b/static/css/07-documents.css @@ -0,0 +1,1283 @@ +/* 07-documents - part of the former static/style.css. + * + * The stylesheet is split by area, but the split is mechanical: rules were + * moved verbatim and NOT regrouped. The numeric prefix is the load order, and + * that order is what preserves the cascade. A rule sits in the file its + * original position fell into, so an area's rules are not all in its file and + * a file is not only that area's rules. + * + * Do not reorder the tags in static/index.html, and do not move a rule + * between files: with 1,151 redeclarations and 1,886 !important in this + * cascade, either can change which declaration wins. + * + * static/index.html and the PRECACHE list in static/sw.js must agree on this + * order. tests/test_static_stylesheet_manifest.py and + * tests/test_css_computed_style_snapshot.py fail if that drifts. + */ +.modal.modal-left-docked { + align-items: stretch; + justify-content: flex-start; +} +.modal.modal-left-docked .modal-content { + border-right: 1px solid var(--border); + box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18); + border-radius: 0; + /* Pin transitions OFF on the dock's positioning properties. The wide + sidebar collapse/expand changes --sidebar-w instantly, which means + the docked modal's `left: calc(...)` jumps by ~240px. Any CSS + transition on `left` (inherited or otherwise) would animate that + jump as a fly-across. Same defense for right-docked / fullscreen + panels in case future themes add a generic transition. */ + transition: none !important; +} +.modal.modal-right-docked .modal-content, +#email-lib-modal.email-lib-fullscreen .modal-content, +#doclib-modal.doclib-fullscreen .doclib-modal-content { + transition: none !important; +} +.modal.modal-right-docked .email-reader-header, +.modal.modal-left-docked .email-reader-header { + flex-direction: column; + /* Stretch the meta to the full header width so the absolute + action cluster's right:0 lands at the right edge (was sizing + meta to chip content width and centering it). */ + align-items: stretch; + gap: 6px; +} +/* Docked panes: 4px between From→To and 4px between To→Cc. + Meta has its own 2px gap (From→details) so an extra 2px on + details margin-top gives a 4px From→To distance, and gap:4 on + the details flex column gives 4px between To and Cc. */ +.modal.modal-right-docked .email-reader-meta-details, +.modal.modal-left-docked .email-reader-meta-details { + margin-top: 2px; + gap: 4px; +} +/* Docked: always show To/Cc — there's vertical room reserved for + the action cluster already and the chevron toggle just adds + friction. Force the hidden attribute open and hide the chevron. */ +.modal.modal-right-docked .email-reader-meta-details[hidden], +.modal.modal-left-docked .email-reader-meta-details[hidden] { + display: flex !important; +} +.modal.modal-right-docked .email-reader-meta-toggle, +.modal.modal-left-docked .email-reader-meta-toggle { + display: none; +} +.modal.modal-right-docked .email-reader-header > .email-reader-actions, +.modal.modal-left-docked .email-reader-header > .email-reader-actions { + position: static; + align-self: flex-end; + background: none; + padding-left: 0; +} +/* Docked modal: keep all meta rows as flex so From / To / Cc + labels align at the same x. Chips already overflow-scroll + inside each row when there are too many. No per-row margin-top + here — the spacing between rows comes entirely from + .email-reader-meta-details gap so From→To and To→Cc match. */ +.modal.modal-right-docked .email-reader-meta-row:not(.email-reader-meta-from), +.modal.modal-left-docked .email-reader-meta-row:not(.email-reader-meta-from) { + padding-right: 0; +} +.modal.modal-right-docked .email-reader-meta-row strong, +.modal.modal-left-docked .email-reader-meta-row strong { + min-width: 0; +} +.modal.modal-right-docked .recipient-chip, +.modal.modal-left-docked .recipient-chip { + max-width: 100%; +} +.archive-list { + margin-top: 8px; + border-top: 1px solid var(--border); + padding-top: 8px; +} +#archive-modal .memory-sort-select, +#archive-modal .memory-toolbar-btn { + height: 32px; +} +.archive-menu-btn { + position: relative; + top: 2px; +} +#archive-chips .doclib-chip { + height: 22px; + display: inline-flex; + align-items: center; + font-size: 10px; + padding: 0 8px; +} +/* Library tab panels — Chats / Archive / Research / Documents share the + same toolbar dimensions so the sort dropdown + Select button line up + identically across tabs. */ +#doclib-panel-chats .memory-sort-select, +#doclib-panel-archive .memory-sort-select, +#doclib-panel-research .memory-sort-select, +[data-doclib-panel="documents"] .memory-sort-select { + height: 24px; + font-size: 11px; + width: 86px; +} +#doclib-panel-chats .memory-toolbar-btn, +#doclib-panel-archive .memory-toolbar-btn, +#doclib-panel-research .memory-toolbar-btn, +[data-doclib-panel="documents"] .memory-toolbar-btn { + height: 24px; + font-size: 11px; + position: relative; + top: -3px; +} +/* Research's Recent (sort) + Select sat 1px lower than the other tabs — nudge up. */ +#doclib-panel-research .memory-sort-select { position: relative; top: 1.5px; } +#doclib-panel-research .memory-toolbar-btn { top: -4.5px; } +.library-sort-picker { + width: 108px; + flex: 0 0 108px; + top: 1px; +} +.library-sort-picker .library-sort-btn { + top: 0; + width: 100%; +} +.library-sort-picker .library-sort-menu { + top: calc(100% + 4px); + width: max(100%, 154px); +} +.library-sort-picker .library-sort-item[aria-selected="true"] { + background: color-mix(in srgb, var(--accent, var(--red)) 13%, transparent); +} +#doclib-panel-chats #doclib-chats-tidy-btn, +#doclib-panel-chats #doclib-chats-select-btn, +#doclib-panel-archive #doclib-arc-select-btn { + top: 1px; +} +#doclib-panel-chats .doclib-chip-scroll-arrow { + top: calc(50% - 9px) !important; +} +[data-doclib-panel="documents"] .doclib-chip-scroll-arrow { + top: calc(50% - 9px) !important; +} +#doclib-modal .doclib-chip-scroll-arrow { + top: calc(50% - 19px) !important; +} +[data-doclib-panel="documents"] #doclib-tidy-btn, +[data-doclib-panel="documents"] #doclib-select-btn { + top: 1px; +} +#doclib-panel-research #doclib-research-tidy-btn, +#doclib-panel-research #doclib-research-select-btn { + top: -0.5px; +} +#doclib-panel-research .library-sort-picker { top: -0.5px; } +[data-doclib-panel] { padding-top: 6px !important; } +#doclib-panel-chats, #doclib-panel-archive { padding-top: 14px !important; } +#doclib-panel-chats .memory-desc, #doclib-panel-archive .memory-desc, #doclib-panel-research .memory-desc { margin-top: 7px; } +#doclib-panel-chats .memory-search-input, +#doclib-panel-archive .memory-search-input { + height: 30px !important; + min-height: 30px !important; + flex-shrink: 0; + font-size: 11px; +} +/* Unified search bar across Library tabs + Memory modal — same height, + same magnifying-glass icon at the start, consistent padding. */ +#doclib-panel-chats .memory-search-input, +#doclib-panel-archive .memory-search-input, +#doclib-panel-research .memory-search-input, +[data-doclib-panel="documents"] .memory-search-input, +#memory-modal .memory-search-input, +#tasks-modal .memory-search-input { + height: 30px; + min-height: 30px; + font-size: 11px; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: 9px center; + padding-left: 28px; +} +#doclib-panel-chats .doclib-chip { + height: 28px; +} +#doclib-panel-archive .doclib-chip { + height: 28px; +} +#doclib-panel-chats .doclib-chip, +#doclib-panel-archive .doclib-chip { + display: inline-flex; + align-items: center; + font-size: 9px; + padding: 0 8px; + border-radius: 10px; +} +.doclib-grid:has(.doclib-card-expanded) > .doclib-card:not(.doclib-card-expanded) { + display: none; +} +/* Hide everything except the grid when a card is expanded */ +.admin-card:has(.doclib-card-expanded) > *:not(.doclib-grid):not(.hwfit-cached-list) { + opacity: 0; + max-height: 0; + overflow: hidden; + pointer-events: none; + margin: 0 !important; + padding: 0 !important; + transition: opacity 0.12s ease, max-height 0.2s ease; +} +.admin-card:has(.doclib-card-expanded) > .memory-bulk-bar { + display: none; +} +.admin-card:has(.doclib-card-expanded) > .doclib-grid, +.admin-card:has(.doclib-card-expanded) > .hwfit-cached-list { + max-height: none !important; + min-height: 0 !important; + overflow: visible !important; +} +/* Firefox-mobile (no :has()) fallback: when reading an email, fully remove the + list-mode chrome (accounts row, toolbar, bulk bar, desc) — display:none so no + residual flex-gap leaves an empty strip above the reader — and zero the + admin-card gap so the grid/reader starts flush at the top. */ +#email-lib-modal.email-reading .admin-card > *:not(.doclib-grid):not(.hwfit-cached-list) { + display: none !important; +} +#email-lib-modal .admin-card:has(.doclib-card-expanded), +#email-lib-modal.email-reading .admin-card { + gap: 0 !important; + padding: 0 !important; + margin-bottom: 0 !important; + border: 0 !important; + border-radius: 0 !important; + background: transparent !important; +} +/* Override for chat-rows + research-rows: those expand inline with a + bounded preview (max-height: 60vh), so the grid must stay clipped + and scrollable. Without this scoping the overflow:visible above lets + the expanded chat preview escape the grid and overlap whatever's + underneath/beside the modal (e.g., a docked sibling panel) — what + the user calls "merging with other windows". */ +.admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid { + overflow: hidden auto !important; +} +/* Email modal needs the grid to STAY a constrained flex container when + expanded — overflow:visible (set above for cookbook) lets content escape + instead of letting the expanded reader fill via flex:1. We keep the same + max-height:none + min-height:0 but flip overflow back so the reader + actually fills the modal. */ +#email-lib-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid, +#email-lib-modal.email-reading .admin-card > .doclib-grid { + overflow: hidden !important; + display: flex !important; + flex: 1 1 auto !important; +} +#email-lib-modal.email-reading #email-lib-grid > .date-section-header { + display: none !important; +} +#email-lib-modal.email-reading .doclib-modal-content { + min-height: var(--email-reading-modal-min-h, auto); +} +#email-lib-modal .doclib-card.doclib-card-expanded { + flex: 1 1 auto !important; + height: 100% !important; + /* Neutral frame on the active email — the accent border + glow felt + overbearing on desktop; the size jump alone is enough signal. */ + border: 1px solid var(--border) !important; + box-shadow: 0 6px 18px rgba(0,0,0,0.12) !important; + animation: none !important; +} +/* Desktop-only, ONLY on the currently-expanded email card. Nudges the title + row down 6px / right 2px, bolds the subject, hides the timestamp from the + meta line (the reader header carries its own date), and pulls the prev/next + nav arrows in 4px. */ +@media (min-width: 769px) { + #email-lib-modal .doclib-card-expanded .email-card-titlerow, + #email-lib-modal .doclib-card-expanded .email-card-titlerow > * { + position: relative; + top: 6px; + left: 2px; + } + /* Smaller subject — the reader header below already carries weight. */ + #email-lib-modal .doclib-card-expanded .email-card-titlerow .memory-item-title { + font-weight: 700; + font-size: 12px; + } + /* A bit more breathing room around the title row so the smaller bold text + doesn't crowd the meta line below. (Timestamp kept — user changed mind.) */ + #email-lib-modal .doclib-card-expanded .email-card-titlerow { + padding: 4px 0 6px; + line-height: 1.35; + } + /* Nudge the date down/left in the meta row, and give the meta line +4px of + extra bottom space so the shifted date isn't clipped. */ + #email-lib-modal .doclib-card-expanded .email-meta-date { + position: relative; + top: 4px; + left: 0; + } + #email-lib-modal .doclib-card-expanded .memory-item-meta { + padding-bottom: 4px; + } + #email-lib-modal .doclib-card-expanded .email-card-nav-arrows { + position: relative; + left: -4px; + } +} +/* Skills modal: same mechanism as email above. The default expanded-grid + rule (overflow:visible, no flex) lets the card expand inline to ~content + height instead of filling — which is why the skill preview stopped at + partial height. Flip the grid back to a constrained flex container and + let the expanded card fill it, exactly like the email reader. */ +#memory-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid { + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + flex: 1 1 auto !important; +} +#memory-modal .doclib-card.doclib-card-expanded { + flex: 1 1 auto !important; + height: 100% !important; +} +#email-lib-modal .modal-content { + transition: width 0.22s ease-out, height 0.22s ease-out; +} +@media (min-width: 769px) { + body:not(.email-doc-split-active) #email-lib-modal:not(.email-lib-fullscreen):not(.modal-left-docked):not(.modal-right-docked) .modal-content:not([style*="height"]) { + min-height: min(560px, 85vh); + } +} + +/* Serve panel specifically: the admin-card inline style is + `overflow:hidden` (so the toolbar/header don't drift), and the list + inside has overflow:visible. On short windows that combination + clipped the cards off the bottom with no scrollbar. Make the list + itself the scroll surface so the rest of the card stays put. */ +.cookbook-group[data-backend-group="Serve"] > .admin-card { + min-height: 0; +} +.cookbook-group[data-backend-group="Serve"] > .admin-card > #hwfit-cached-list, +.cookbook-group[data-backend-group="Serve"] > .admin-card > .hwfit-cached-list { + flex: 1 1 0; + min-height: 0; + overflow-y: auto !important; + overscroll-behavior: contain; +} +.cookbook-group[data-backend-group="Serve"] > .admin-card > .hwfit-cached-list .doclib-card.doclib-card-expanded { + flex: 0 0 auto !important; + overflow: visible !important; +} +/* Launch model cards use the same domino entrance as the other library lists + when a cached-model scan finishes. The class is removed after the one-shot + entrance, so filtering and sorting stay instantaneous. */ +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card { + animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards; +} +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(1) { animation-delay: 0.04s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(2) { animation-delay: 0.08s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(3) { animation-delay: 0.12s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(4) { animation-delay: 0.16s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(5) { animation-delay: 0.20s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(6) { animation-delay: 0.24s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(7) { animation-delay: 0.28s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(8) { animation-delay: 0.32s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(9) { animation-delay: 0.36s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(10) { animation-delay: 0.40s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(11) { animation-delay: 0.44s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(12) { animation-delay: 0.48s; } +#cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card:nth-child(n+13) { animation-delay: 0.48s; } +@media (prefers-reduced-motion: reduce) { + #cookbook-modal .hwfit-cached-list.cookbook-serve-models-just-loaded > .doclib-card { + animation: none; + } +} +/* Drag-and-drop visual hint for the email compose pane. Subtle accent + outline + tinted overlay so it's obvious files will attach if dropped. */ +.doc-editor-pane.email-dragover { + outline: 2px dashed var(--accent, #2563eb); + outline-offset: -8px; + background: color-mix(in srgb, var(--accent, #2563eb) 6%, transparent); +} +.doc-editor-pane.email-dragover::after { + content: 'Drop to attach'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--accent, #2563eb); + color: #fff; + padding: 8px 16px; + border-radius: 6px; + font-size: 12px; + pointer-events: none; + z-index: 1000; +} + +/* Email reader window — make the body always fill the available vertical + space regardless of how the window is positioned/resized. The user can + drag the bottom-right corner to resize; the body re-flexes automatically. */ +.email-window-modal .modal-content { + display: flex !important; + flex-direction: column !important; + resize: both; + overflow: hidden; + min-width: 320px; + min-height: 240px; + /* When user resizes/drags so the window covers a side, force the inner + body to fill all the way to the bottom. The default max-height: 85vh + (set inline by _makeDraggable's exitFullscreen) and 80vh from the + opener both cap at the *viewport* — if the browser window then changes + size (e.g. OS aero-snap), the modal stays at the old height and + truncates rows. Drop the cap whenever an explicit width OR height has + been written inline by the resize handle / drag handler. */ +} +.email-window-modal .modal-content[style*="width"], +.email-window-modal .modal-content[style*="height"] { + max-height: calc(100vh - 16px) !important; +} +.email-window-modal .email-window-body { + flex: 1 1 auto !important; + min-height: 0 !important; + /* Explicit basis of 0 makes the body grow to fill remaining space rather + than its content's intrinsic height (which was capping the thread). */ + flex-basis: 0 !important; + max-height: none !important; + overflow: auto !important; + overscroll-behavior: contain; +} + +/* Individual email reader window — fullscreen mode (drag-to-top snap or + double-click header). Same recipe as the library modal: pin to viewport + edges and let the inner body absorb the remaining height. */ +body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .modal-content { + position: fixed !important; + inset: 0 !important; + left: 0 !important; + top: 0 !important; + width: 100vw !important; + max-width: 100vw !important; + height: 100vh !important; + max-height: 100vh !important; + border-radius: 0 !important; + transform: none !important; + display: flex !important; + flex-direction: column !important; +} +body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .email-window-body { + flex: 1 1 auto !important; + min-height: 0 !important; + max-height: none !important; +} + +/* Email library modal: fullscreen mode + larger mobile sheet (mirrors the + cookbook treatment so the toolbar and bulk actions stay reachable). + Leaves whichever navigation strip is currently visible on the left + (the wide sidebar OR the icon rail — they're mutually exclusive, so + one of the two CSS vars is 0 at any given time, set by init.js). */ +body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content { + position: fixed !important; + top: 0 !important; + left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important; + right: 0 !important; + bottom: 0 !important; + width: auto !important; + max-width: none !important; + height: 100vh !important; + max-height: 100vh !important; + border-radius: 0 !important; + transform: none !important; +} +/* Mobile: the /email route adds .email-lib-fullscreen, but the desktop full- + bleed rule above squares off the corners and offsets the email by the icon + rail. On phones the email should be the normal bottom-sheet — full width, + rounded top corners. Same specificity as the rule above + later in source, + so it wins on mobile. */ +@media (max-width: 768px) { + body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content { + left: 0 !important; + right: 0 !important; + width: 100vw !important; + max-width: 100vw !important; + height: 100dvh !important; + max-height: 100dvh !important; + border-radius: 14px 14px 0 0 !important; + border-top: 1px solid var(--border) !important; + } +} +/* Make the inner panes actually grow to fill the fullscreen container + instead of staying at their natural size. The body owns the remaining + height below the header; the admin-card + grid then expand into it. */ +#email-lib-modal.email-lib-fullscreen .modal-body { + flex: 1 1 auto !important; + min-height: 0 !important; + overflow: hidden !important; +} +#email-lib-modal.email-lib-fullscreen .modal-body > .admin-card { + flex: 1 1 auto !important; + min-height: 0 !important; +} +#email-lib-modal.email-lib-fullscreen .doclib-grid { + flex: 1 1 auto !important; + min-height: 0 !important; + max-height: none !important; + overflow: auto !important; +} +@media (max-width: 768px) { + /* Mobile email modal sizing — keep in sync with the rule earlier in the + file. 75dvh tall always so flex children (the expanded email reader) + have a real height to grow into. */ + #email-lib-modal .modal-content { + max-height: 90dvh !important; + max-height: 90vh !important; + height: 90dvh !important; + height: 90vh !important; + } + /* Inner panes grow to fill the modal-content — without flex:1 on the + body, the expanded email reader sits in a tiny box because there's + nothing pushing it to take the remaining height. overflow:hidden + + min-height:0 lets each layer pass its constraints down. */ + #email-lib-modal .modal-body, + #email-lib-modal .admin-card { + flex: 1 1 auto !important; + min-height: 0 !important; + overflow: hidden !important; + max-height: none !important; + } + #email-lib-modal .doclib-grid { + flex: 1 1 auto !important; + min-height: 0 !important; + max-height: none !important; + overflow-y: auto !important; + } + /* modal-content keeps its scroll OFF here — the inner flex children + (doclib-grid for the list view, email-reader-body for the expanded + reader) own the scroll surfaces. Without this, double-scroll layouts + trap touches and the reader can't claim full height. */ + #email-lib-modal .modal-content { + overflow: hidden !important; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + touch-action: pan-y; + } + /* Attachment row on phones: cap to ~2 rows so it never dominates the + reader. If more chips exist, the row scrolls vertically. Smaller chip + padding + max-width keeps each chip compact so two rows usually fit + everything anyway. */ + #email-lib-modal .email-reader-atts { + padding: 4px 10px !important; + gap: 3px !important; + max-height: calc(2 * 22px + 12px); /* 2 rows × chip height + padding */ + overflow-y: auto; + align-content: flex-start; + } +#email-lib-modal .email-attachment-chip { + padding: 1px 6px !important; + font-size: 10px !important; + max-width: min(240px, calc(100vw - 44px)) !important; + line-height: 18px !important; + } +} + + +/* Mobile: only ONE scroll surface inside the cookbook modal. The + modal-content is the scroller. Everything inside (cookbook-body, + cookbook-group, all the inner lists/panels) gets overflow: visible + so touch-pan never gets trapped in a nested scroller. + Without this, three levels of overflow:auto + max-height combinations + produce dead-zone areas where swipes do nothing. */ +@media (max-width: 768px) { + #cookbook-modal .modal-content { + overflow-y: auto !important; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + touch-action: pan-y; + } + #cookbook-modal .cookbook-body, + #cookbook-modal .cookbook-group, + #cookbook-modal .cookbook-section-body, + #cookbook-modal .hwfit-cached-list, + #cookbook-modal .doclib-grid, + #cookbook-modal .hwfit-list, + #cookbook-modal .hwfit-panel-cmd { + overflow: visible !important; + max-height: none !important; + min-height: 0 !important; + } + /* Running tmux output: cap how tall an expanded card gets (a long-lived job + can leave thousands of lines) so it doesn't balloon, but keep its own + scroll so you can read it. Outputs default collapsed on mobile now, so + you're usually looking at one expanded card at a time — no wall of nested + scrollers, and you collapse it to move past. */ + #cookbook-modal .cookbook-output-pre, + #cookbook-modal .cookbook-task .cookbook-output-pre { + max-height: 45vh !important; + min-height: 0 !important; + overflow-y: auto !important; + overflow-x: hidden !important; + overscroll-behavior: auto; /* chain to the modal at the scroll boundary */ + } + /* cookbook-body's flex:1 was needed when it owned scrolling — drop it + so the inner content drives modal-content's scroll height. */ + #cookbook-modal .cookbook-body { + flex: 0 0 auto !important; + height: auto !important; + } + /* Launch tab's cached-list normally has `flex: 1 1 0; min-height: 0` + (so it fills the modal on desktop). On mobile the parent now has + `height: auto`, which collapses `flex: 1 1 0` to ZERO PX — + models render but the list area is invisible because the flex + basis is 0 and there's no free space to grow into. Switch to + content-sized flex so the list grows with its children. */ + #cookbook-modal .cookbook-group[data-backend-group="Serve"] > .admin-card > .hwfit-cached-list, + #cookbook-modal .cookbook-group[data-backend-group="Serve"] > .admin-card > #hwfit-cached-list { + flex: 0 0 auto !important; + overflow: visible !important; + } +} +#cookbook-modal .hwfit-cached-list { + flex-shrink: 0; +} +.memory-toolbar { + transition: opacity 0.12s ease, max-height 0.2s ease; + max-height: 120px; +} +.doclib-card { + background: color-mix(in srgb, var(--fg) 3%, transparent); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: all 0.15s, opacity 0.3s, max-height 0.3s ease; + position: relative; +} +.doclib-card.doclib-card-deleting { + opacity: 0; + max-height: 0 !important; + overflow: hidden; + margin: 0; + padding: 0; + border-color: transparent; +} +.doclib-card.selected, +/* Universal selection highlight: any card-shaped row (documents, chats, + archive, research, skills, memories, tasks) gets the same accent outline + when its checkbox is checked. Done via :has() so the highlight follows + selection without per-renderer JS changes. The canonical checkbox class + is .memory-select-cb across every list. */ +.memory-item:has(.memory-select-cb:checked), +.doclib-card:has(.memory-select-cb:checked), +.doclib-chat-row:has(.memory-select-cb:checked) { + border-color: color-mix(in srgb, var(--red) 40%, transparent); + background: color-mix(in srgb, var(--red) 4%, transparent); + /* 2px accent outline overlaid on the 1px border — reads as a thicker + selected border without shifting layout (every card would otherwise + need a 2px transparent border to keep the same width). */ + outline: 2px solid var(--accent-primary, var(--red)); + outline-offset: -1px; +} +.doclib-card-header { + display: flex; + align-items: center; + padding: 7px 8px; + gap: 6px; + min-width: 0; +} +/* Mobile only: push the unexpanded email card's title-row text down 4px so + it sits visually centered with the surrounding card padding. Targets the + actual title-row class (.email-card-titlerow) — the earlier attempt used + .doclib-card-header which the email card builds differently. */ +@media (max-width: 768px) { + #email-lib-modal .doclib-card:not(.doclib-card-expanded) .email-card-titlerow { + margin-top: 4px; + } +} +.doclib-card-title { + font-size: 11px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + flex: 1; +} +.doclib-card-ver { + padding: 1px 6px; + border-radius: 8px; + background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent); + color: var(--accent, var(--red)); + font-size: 9px; + flex-shrink: 0; + font-weight: 600; + letter-spacing: 0.3px; + min-width: 24px; + text-align: left; + box-sizing: border-box; +} +.doclib-card-ver-muted { + background: color-mix(in srgb, var(--fg) 6%, transparent); + color: color-mix(in srgb, var(--fg) 35%, transparent); +} +.doclib-card-lang { + padding: 1px 6px; + border-radius: 8px; + background: color-mix(in srgb, var(--accent) 10%, transparent); + color: var(--accent); + font-size: 9px; + text-align: left; + flex-shrink: 0; + min-width: 52px; + box-sizing: border-box; +} +.doclib-card-session { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 60px; + width: 100px; + max-width: 100px; + font-size: 9px; + color: var(--fg-muted); + flex-shrink: 0; + text-align: left; +} +.doclib-card-time { + flex-shrink: 0; + opacity: 0.5; + font-size: 9px; + color: var(--fg-muted); + min-width: 50px; + text-align: left; +} +/* Footer — used by archive cards */ +.doclib-card-footer { + display: contents; +} +/* Preview — hidden by default, shown on expand */ +.doclib-card-preview { + display: none; + position: relative; + padding: 8px 12px; + font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace; + font-size: 9.5px; + line-height: 1.5; + color: var(--code-fg, var(--hl-fg, var(--fg))); + border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent); + margin: 0; +} +.doclib-card-preview pre { + margin: 0; + white-space: pre-wrap; + word-break: break-all; +} +.doclib-card-expanded .doclib-card-preview pre { + flex: 1 1 0; + overflow-y: auto; + min-height: 0; +} +.doclib-card-preview code.hljs { + background: none; + padding: 0; +} +/* Expanded-only action bar — inside preview. Buttons shifted up 4px by + trimming the row's top margin so they sit closer to the preview text. */ +.doclib-card-expanded-actions { + display: none; + align-items: flex-start; + gap: 6px; + padding: 8px 0 2px; + border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent); + margin-top: 4px; +} +.doclib-card-expanded-actions > .doclib-card-action-btn, +.doclib-card-expanded-actions .doclib-action-btn-row > .doclib-card-action-btn { + position: relative; + top: -4px; +} +.doclib-action-group { + display: flex; + flex-direction: column; + gap: 3px; +} +.doclib-action-btn-row { + display: flex; + gap: 6px; +} +.doclib-action-hint-row { + display: flex; + gap: 6px; +} +/* Match the chat/research footer buttons exactly: flat, bordered, + app font, accent on hover. */ +.doclib-card-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + box-sizing: border-box; + font-size: 10px; + padding: 3px 8px; + border-radius: 4px; + background: none; + border: 1px solid var(--border); + color: var(--fg-muted); + cursor: pointer; + /* These buttons live inside .doclib-card-preview, which forces a + monospace font. The chat/research footer buttons instead inherit the + modal font, which is the app font (Fira Code) on mobile and Inter on + desktop (.modal-content's Inter rule is inside @media min-width:769px). + Mirror that here so all three footers match in both contexts. */ + font-family: var(--font-family, 'Fira Code', monospace); + transition: border-color 0.15s, color 0.15s; +} +@media (min-width: 769px) { + .doclib-card-action-btn { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + letter-spacing: -0.015em; + } +} +.doclib-card-action-btn:hover { + border-color: var(--accent, var(--red)); + color: var(--accent, var(--red)); +} +.doclib-card-text-btn-danger { + color: var(--color-danger, #e06c75); + border-color: var(--color-danger, #e06c75); +} +.doclib-card-text-btn-danger:hover { + color: #ff4d4d; + border-color: #ff4d4d; +} +.doclib-card-expanded-actions > .doclib-card-action-btn { + font-size: 10px; + padding: 3px 8px; +} +/* Push the Open/Clone group to the right edge of the action bar — Delete + anchors the left, primary actions sit opposite. */ +.doclib-card-expanded-actions > .doclib-action-group { + margin-left: auto; +} +.doclib-card-expanded-actions > .doclib-expanded-delete-btn, +.doclib-card-expanded-actions > .doclib-expanded-archive-btn { + left: 0 !important; +} +.doclib-expanded-mobile-more { display: none; } +.doclib-btn-hint { + font-weight: normal; + opacity: 0.35; + font-size: 8px; + min-width: 58px; + padding: 0 8px; + box-sizing: border-box; +} +@media (max-width: 768px) { + .doclib-btn-hint { display: none !important; } +} + +/* Chat row expand preview — matches the documents-tab expand style. */ +.doclib-card-chevron { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: center; + flex-shrink: 0; + width: 14px; + height: 14px; + line-height: 0; + opacity: 0.3; + transform: rotate(0deg); + transition: transform 0.2s ease, opacity 0.15s; +} +.doclib-card-chevron svg { display: block; } +.doclib-card-expanded .doclib-card-chevron, +.task-card.expanded .doclib-card-chevron { + opacity: 0.8; + transform: rotate(180deg); +} +.doclib-card > .memory-item-actions { order: 1; } +.doclib-card > .doclib-card-chevron { order: 2; } +.doclib-chat-header .memory-item-actions, +.task-card .memory-item-actions { order: 2; } +.doclib-chat-header .doclib-card-chevron, +.task-card .doclib-card-chevron { order: 3; } +.hwfit-cached-list > .doclib-card > .memory-item-actions { order: 2; } +.hwfit-cached-list > .doclib-card > .doclib-card-chevron { order: 3; } +#email-lib-modal .doclib-card > .memory-item-actions { order: 1; align-self: center; } +#email-lib-modal .doclib-card > .doclib-card-chevron { display: none; } +.doclib-chat-row { flex-direction: column; align-items: stretch; } +.doclib-chat-row .doclib-card-chevron { + display: inline-flex; + opacity: 0.55; + transition: transform 0.2s ease, opacity 0.15s; + flex-shrink: 0; +} +.doclib-chat-row:not(.doclib-card-expanded) .doclib-card-chevron { + transform: rotate(0deg); +} +.doclib-chat-row.doclib-card-expanded .doclib-card-chevron { + opacity: 0.8; + transform: rotate(180deg); +} +/* Release .memory-item's base max-height: 200px when a chat row is + expanded. Without this, the row clips at 200px on desktop and the + preview's messages/actions visually spill past the row's box — + "colliding" with the next chat in the column. The mobile takeover + below already handles this via the same override; this rule is the + desktop-or-any-viewport equivalent. */ +.doclib-chat-row.doclib-card-expanded { + max-height: none; + overflow: visible; +} +/* When a chat row is expanded, take over the grid the same way documents + do: hide siblings, hide the admin-card toolbar, and let this row plus + its preview claim the full available height. .memory-item's default + max-height: 200px must be overridden or the preview will be tiny. */ +/* Mobile-only "card takeover" for an expanded chat — hides siblings, + hides the admin-card toolbar, and lets this row + its preview claim + the full available height. On desktop the expanded preview just + renders inline below the row with its messages constrained by + max-height + overflow:auto so the user gets a normal scroll. */ +@media (max-width: 820px) { + .doclib-grid:has(.doclib-chat-row.doclib-card-expanded) > .doclib-chat-row:not(.doclib-card-expanded) { + display: none; + } + .admin-card:has(.doclib-chat-row.doclib-card-expanded) > *:not(.doclib-grid) { + display: none; + } + .admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid { + flex: 1 1 auto !important; + max-height: none !important; + overflow-y: auto !important; + } + .doclib-chat-row.doclib-card-expanded { + flex: 1 1 auto !important; + max-height: none !important; + min-height: 0 !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + align-items: stretch !important; + } + .doclib-chat-row.doclib-card-expanded .doclib-chat-header { + flex: 0 0 auto !important; + } + .doclib-chat-row.doclib-card-expanded .doclib-chat-preview { + flex: 1 1 auto !important; + min-height: 0 !important; + overflow: hidden !important; + display: flex !important; + flex-direction: column !important; + padding-bottom: 4px !important; + } + .doclib-chat-row.doclib-card-expanded .doclib-chat-preview-messages { + -webkit-overflow-scrolling: touch; + max-height: none !important; + flex: 1 1 auto !important; + min-height: 0 !important; + } +} +.doclib-chat-preview { + font-size: 11px; + padding: 0 4px 6px; +} +.doclib-chat-preview .doclib-chat-open-btn { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + padding: 3px 8px; + border-radius: 4px; + color: var(--fg-muted); + border: 1px solid var(--border); + background: none; + cursor: pointer; + font-family: inherit; + transition: border-color 0.15s, color 0.15s; +} +.doclib-chat-preview .task-completed-report-btn { + color: var(--accent, var(--red)); + border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border)); +} +.doclib-chat-preview .doclib-chat-open-btn:hover { + border-color: var(--accent, var(--red)); + color: var(--accent, var(--red)); +} +.doclib-chat-preview-messages { + margin-top: 6px; + max-height: 320px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + padding-right: 4px; +} +/* Desktop expanded state — match the documents-tab behavior: hide + sibling chats, hide the admin-card toolbar/header, and let the + expanded row + its messages preview claim the full modal height. + Chat rows use `memory-item doclib-chat-row` (no `doclib-card` class) + so the document-tab's global rules at line 12284-12304 skip them — + these mirror those rules scoped to chat rows. The mobile media query + above (820px) keeps the same takeover behavior for phones. */ +@media (min-width: 821px) { + .doclib-grid:has(.doclib-chat-row.doclib-card-expanded) > .doclib-chat-row:not(.doclib-card-expanded) { + display: none; + } + .admin-card:has(.doclib-chat-row.doclib-card-expanded) > *:not(.doclib-grid) { + display: none; + } + .admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid { + flex: 1 1 auto !important; + max-height: none !important; + overflow: hidden auto !important; + } + .doclib-chat-row.doclib-card-expanded { + flex: 1 1 auto !important; + max-height: none !important; + min-height: 0 !important; + display: flex !important; + flex-direction: column !important; + } + .doclib-chat-row.doclib-card-expanded .doclib-chat-preview { + flex: 1 1 auto !important; + min-height: 0 !important; + display: flex !important; + flex-direction: column !important; + } + .doclib-chat-row.doclib-card-expanded .doclib-chat-preview-messages { + flex: 1 1 auto !important; + min-height: 0 !important; + max-height: none !important; + overflow-y: auto !important; + } +} +.doclib-chat-preview-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + padding: 2px 0 2px; + border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent); + margin-top: 2px; + flex-shrink: 0; + /* Whole action footer nudged down 5px. */ + position: relative; + top: 5px; +} +.doclib-chat-delete-btn, +.doclib-chat-archive-btn, +.doclib-chat-restore-btn, +.doclib-chat-discuss-btn, +.doclib-chat-copy-btn { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + padding: 3px 8px; + border-radius: 4px; + background: none; + cursor: pointer; + font-family: inherit; + transition: border-color 0.15s, color 0.15s; +} +.doclib-chat-archive-btn, +.doclib-chat-restore-btn, +.doclib-chat-discuss-btn, +.doclib-chat-copy-btn { + color: var(--fg-muted); + border: 1px solid var(--border); +} +.doclib-chat-restore-btn:hover { + color: var(--accent, var(--red)); + border-color: var(--accent, var(--red)); +} +.doclib-chat-archive-btn:hover, +.doclib-chat-discuss-btn:hover, +.doclib-chat-copy-btn:hover { + color: var(--accent, var(--red)); + border-color: var(--accent, var(--red)); +} +.doclib-chat-delete-btn { + color: var(--color-danger, #e06c75); + border: 1px solid var(--color-danger, #e06c75); +} +.doclib-chat-delete-btn:hover { + color: #ff4d4d; + border-color: #ff4d4d; +} +/* When a chat is archived there's no Archive button, so Delete becomes + the only left-side action and needs the auto margin to push Open right. */ +.doclib-chat-preview-actions:not(:has(.doclib-chat-archive-btn)) > .doclib-chat-delete-btn { + margin-right: auto; +} +/* Hide the "..." menu while the chat card is expanded — archive + + delete live in the preview footer instead. */ +.doclib-chat-row.doclib-card-expanded ._chat-menu { display: none; } +.doclib-chat-preview-actions.doclib-chat-preview-actions-top { + border-top: none; + border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent); + margin-top: 4px; + margin-bottom: 6px; + padding: 0 0 6px; + justify-content: flex-start; +} +.doclib-chat-preview-messages .doclib-chat-msg { + margin: 4px 0 10px; + padding-left: 8px; + border-left: 2px solid color-mix(in srgb, var(--border) 70%, transparent); +} +.doclib-chat-preview-messages .doclib-chat-msg.user { + border-left-color: color-mix(in srgb, var(--accent, var(--red)) 60%, transparent); +} +.doclib-chat-preview-messages .doclib-chat-msg-role { + font-size: 9px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + opacity: 0.55; + margin-bottom: 2px; +} +.doclib-chat-preview-messages .doclib-chat-msg.user .doclib-chat-msg-role { + color: var(--accent, var(--red)); + opacity: 0.85; +} +.doclib-chat-preview-messages .doclib-chat-msg-body { + white-space: pre-wrap; + word-break: break-word; + opacity: 0.85; + line-height: 1.45; +} +/* Chat-bubble preview — mirrors the real chat layout. User bubbles hug + right with an accent tint; assistant bubbles hug left with a neutral + panel tint and a small model tag at the top. */ +.doclib-chat-bubble-row { + display: flex; + margin: 6px 0; +} +.doclib-chat-bubble-row.user { justify-content: flex-end; } +.doclib-chat-bubble-row.assistant { justify-content: flex-start; } +.doclib-chat-bubble { + max-width: 85%; + padding: 6px 10px 8px; + border-radius: 14px; + font-size: 11px; + line-height: 1.45; + border: 1px solid var(--border); + background: color-mix(in srgb, var(--fg) 4%, transparent); +} +.doclib-chat-bubble-row.user .doclib-chat-bubble { + background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); + border-color: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent); + border-bottom-right-radius: 4px; +} +.doclib-chat-bubble-row.assistant .doclib-chat-bubble { + border-bottom-left-radius: 4px; +} +.doclib-chat-msg-model { + display: inline-block; + font-size: 8px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + opacity: 0.55; + margin-bottom: 3px; + padding: 1px 6px; + border-radius: 6px; + background: color-mix(in srgb, var(--fg) 8%, transparent); + color: var(--fg-muted, var(--fg)); +} +.doclib-chat-bubble-body { word-break: break-word; } +.doclib-chat-bubble-body p { margin: 4px 0; } +.doclib-chat-bubble-body p:first-child { margin-top: 0; } +.doclib-chat-bubble-body p:last-child { margin-bottom: 0; } +.doclib-chat-bubble-body pre { + font-size: 10px; + margin: 4px 0; + padding: 6px 8px; + background: color-mix(in srgb, var(--bg) 60%, transparent); + border-radius: 6px; + overflow-x: auto; +} +.doclib-chat-bubble-body code { + font-size: 10px; + padding: 1px 4px; + border-radius: 3px; + background: color-mix(in srgb, var(--fg) 8%, transparent); +} +.doclib-chat-bubble-body pre code { background: none; padding: 0; } +.doclib-chat-bubble-body ul, +.doclib-chat-bubble-body ol { + margin: 4px 0; + padding-left: 18px; +} +.doclib-card-collapse-chevron { + display: none; + align-items: center; + justify-content: center; + width: 24px; + height: 22px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--color-muted, var(--fg)); + font: inherit; + opacity: 0.3; + cursor: pointer; + flex-shrink: 0; + margin: 0 0 0 4px; + transition: opacity 0.15s, background 0.15s, border-color 0.15s; +} +.doclib-card-collapse-chevron:hover { + opacity: 1; + background: color-mix(in srgb, var(--fg) 8%, transparent); + border-color: color-mix(in srgb, var(--fg) 24%, transparent); +} +.cookbook-serve-title { + display: flex; + align-items: center; + min-width: 0; +} +.cookbook-serve-title .doclib-card-collapse-chevron { + display: inline-flex; + margin-left: auto; + margin-top: 0; +} +.doclib-card-expanded .doclib-card-collapse-chevron { + display: inline-flex; +} +/* Expanded card — fills the whole grid */ +.doclib-card.doclib-card-expanded { + flex: 1; + min-height: 0; + background: color-mix(in srgb, var(--fg) 3%, transparent); + border: 1px solid var(--border); + border-radius: 8px; + display: flex; + flex-direction: column; + overflow: hidden; + animation: doclib-expand 0.2s ease-out; +} +.doclib-card.doclib-card-expanded { + flex-direction: column !important; + align-items: stretch !important; + max-height: none !important; + gap: 4px !important; +} +.doclib-card.doclib-card-expanded > .memory-item-actions { display: none; } +/* Non-preview children of an expanded doc card carry an inline flex:1 from + the row layout. With the card now flex-column those children would steal + half the height and shrink the preview — pin them at auto height. */ +.doclib-card.doclib-card-expanded > div:not(.doclib-card-preview):not(.doclib-card-header):not(.memory-item-actions):not(.email-card-reader) { + flex: 0 0 auto !important; +} +/* The email reader IS the scroll container — keep its flex:1 so its + internal body can claim the rest of the card's height and scroll. */ +.doclib-card.doclib-card-expanded > .email-card-reader { + flex: 1 1 auto !important; + min-height: 0 !important; +} +.doclib-card.doclib-card-expanded .doclib-card-preview { + display: flex; + flex-direction: column; + flex: 1 1 auto !important; + /* The preview matches chat rows: document content scrolls, while the + footer stays in the card rather than becoming the last scroll item. */ + overflow: hidden; + min-height: 0; +} +.doclib-card.doclib-card-expanded .doclib-card-expanded-actions { + display: flex; + flex-shrink: 0; +} diff --git a/static/css/08-skills.css b/static/css/08-skills.css new file mode 100644 index 000000000..9f01ee333 --- /dev/null +++ b/static/css/08-skills.css @@ -0,0 +1,1088 @@ +/* 08-skills - part of the former static/style.css. + * + * The stylesheet is split by area, but the split is mechanical: rules were + * moved verbatim and NOT regrouped. The numeric prefix is the load order, and + * that order is what preserves the cascade. A rule sits in the file its + * original position fell into, so an area's rules are not all in its file and + * a file is not only that area's rules. + * + * Do not reorder the tags in static/index.html, and do not move a rule + * between files: with 1,151 redeclarations and 1,886 !important in this + * cascade, either can change which declaration wins. + * + * static/index.html and the PRECACHE list in static/sw.js must agree on this + * order. tests/test_static_stylesheet_manifest.py and + * tests/test_css_computed_style_snapshot.py fail if that drifts. + */ +/* Document cards can be expanded in the normal windowed library, where the + grid has no definite height. A zero-basis
 then collapses to nothing
+   while the footer remains visible. Keep the document text's intrinsic height
+   in that layout; bounded/fullscreen layouts can still shrink it and scroll. */
+#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview > pre {
+  flex: 1 1 auto;
+  min-height: 1.5em;
+}
+
+/* Collapsible skills section headers (Your skills / Built-in). */
+.skills-section-header {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  cursor: pointer;
+  user-select: none;
+  padding: 6px 4px 4px;
+  font-size: 10px;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  opacity: 0.6;
+  font-weight: 600;
+}
+.skills-section-header:hover { opacity: 0.9; }
+.skills-section-chevron { transition: transform 0.15s ease; flex-shrink: 0; }
+.skills-section-header.collapsed .skills-section-chevron { transform: rotate(-90deg); }
+.skills-section-count {
+  margin-left: auto;
+  opacity: 0.6;
+  font-variant-numeric: tabular-nums;
+  font-weight: normal;
+}
+.skill-card-section-hidden { display: none !important; }
+/* Warning banner shown when previewing/editing a built-in capability. */
+.skill-builtin-warn {
+  font-size: 9.5px;
+  line-height: 1.45;
+  color: var(--color-warning, #f0ad4e);
+  background: color-mix(in srgb, var(--color-warning, #f0ad4e) 12%, transparent);
+  border: 1px solid color-mix(in srgb, var(--color-warning, #f0ad4e) 35%, transparent);
+  border-radius: 6px;
+  padding: 6px 9px;
+  margin: 0 0 8px;
+  flex-shrink: 0;
+}
+
+/* #skills-list wears both .memory-list and .doclib-grid. doclib-grid's
+   max-height:400px would cap the list AND clamp an expanded card — keep
+   the memory-list fill-the-modal behaviour instead. */
+#skills-list.doclib-grid {
+  max-height: none;
+  flex: 1;
+  min-height: 0;
+}
+#skills-list.doclib-grid:has(.doclib-card-expanded) {
+  overflow: hidden;  /* expanded card owns its own scroll */
+}
+/* When a skill is expanded, hide the sibling Add-Skill form card so the
+   expanded SKILL.md uses the full panel. Otherwise the two admin-cards
+   split the height ~50/50 and the expanded skill is stuck in its half
+   while the Add form idles in the other. */
+.memory-tab-panel[data-memory-panel="skills"]:has(.doclib-card-expanded) > .admin-card:nth-of-type(2) {
+  display: none !important;
+}
+
+/* Skills cards reuse the doclib expand/footer machinery. The SKILL.md
+   preview + the edit textarea need to fill the expanded card and own
+   their own scroll, same as a document preview. */
+/* Collapsed skill rows used a smaller (0.9em code) title, so the click row
+   read slimmer than document/chat/library cards. Give the header a min-height
+   so the tap target matches the other library items. */
+.skill-card > .skill-card-header {
+  min-height: 53px;
+  box-sizing: border-box;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+/* Skill rows share Tasks' card hover colors. Draft state belongs in the
+   status badge, not in a second opacity layer across the whole row. */
+/* Name + description column. Name wraps to 2 lines (skill slugs are long —
+   use the space rather than truncating to "check-model-downl…"). */
+.skill-card-textcol {
+  flex: 1 1 auto;
+  min-width: 0;
+  overflow: hidden;
+}
+.skill-card-title-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  min-width: 0;
+}
+.skill-card-name {
+  font-family: inherit;
+  font-weight: 500;
+  font-size: 12px;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  word-break: normal;
+  line-height: normal;
+}
+.skill-card-title-row .skill-card-name { min-width: 0; }
+.skill-status-published {
+  width: 20px;
+  height: 20px;
+  padding: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--color-success, #4ade80);
+  background: color-mix(in srgb, var(--color-success, #4ade80) 15%, transparent);
+  border-color: color-mix(in srgb, var(--color-success, #4ade80) 32%, transparent);
+  border-radius: 50%;
+}
+.skill-stats .skill-status-published {
+  width: 15px;
+  height: 15px;
+  margin-left: 3px;
+  vertical-align: middle;
+}
+.skill-stats .skill-status-published svg {
+  width: 9px;
+  height: 9px;
+}
+.skill-card-desc {
+  font-size: 10px;
+  opacity: 0.4;
+  margin-top: -1px;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.skills-audit-model-field {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-top: 10px;
+  font-size: 11px;
+}
+.skills-audit-model-field > span {
+  flex: 0 0 auto;
+  color: color-mix(in srgb, var(--fg) 68%, transparent);
+}
+.skills-audit-model-field select {
+  min-width: 0;
+  flex: 1;
+  height: 26px;
+  padding: 0 6px;
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  background: var(--bg);
+  color: var(--fg);
+  font: inherit;
+}
+.skill-card-tags {
+  display: flex;
+  gap: 4px;
+  flex-wrap: wrap;
+  margin-top: 4px;
+}
+.skill-tag-pill {
+  padding: 1px 5px;
+  font-size: 9px;
+}
+.skill-usage-pill {
+  margin-left: auto;
+  color: color-mix(in srgb, var(--fg) 62%, transparent);
+}
+.skill-tag-filter-chip { cursor: pointer; }
+
+@media (max-width: 768px) {
+  .skill-card > .skill-card-header {
+    align-items: flex-start;
+  }
+  .skill-card-textcol {
+    min-width: 0;
+  }
+  .skill-card-tags {
+    max-width: 100%;
+    overflow: hidden;
+  }
+  .skill-card-tags .skill-tag-pill {
+    max-width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+}
+/* Right cluster — pills (right-aligned), stats, then the menu/chevron. */
+.skill-card-right {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-shrink: 0;
+  margin-left: auto;
+}
+.skill-card-right .memory-cat-badge { flex-shrink: 0; }
+.skill-status-bin {
+  background: color-mix(in srgb, var(--color-danger, #d9534f) 20%, transparent);
+  color: var(--color-danger, #d9534f);
+}
+.skill-model-pill {
+  max-width: 150px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.skill-model-student {
+  background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.skill-audit-pass {
+  color: var(--color-success, #4ade80);
+  background: color-mix(in srgb, var(--color-success, #4ade80) 16%, transparent);
+}
+.skill-audit-needs_work {
+  color: var(--color-warning, #f0ad4e);
+  background: color-mix(in srgb, var(--color-warning, #f0ad4e) 16%, transparent);
+}
+.skill-audit-fail {
+  color: var(--color-danger, #e06c75);
+  background: color-mix(in srgb, var(--color-danger, #e06c75) 16%, transparent);
+}
+.skill-audit-inconclusive,
+.skill-audit-skipped,
+.skill-audit-unknown {
+  color: color-mix(in srgb, var(--fg) 64%, transparent);
+  background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.skill-model-teacher {
+  background: color-mix(in srgb, var(--color-warning, #f0ad4e) 24%, transparent);
+  color: var(--color-warning, #f0ad4e);
+}
+.skill-necessity-pill {
+  max-width: 150px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.skill-necessity-duplicate {
+  background: color-mix(in srgb, var(--color-warning, #f0ad4e) 22%, transparent);
+  color: var(--color-warning, #f0ad4e);
+}
+.skill-necessity-trivial {
+  background: color-mix(in srgb, var(--fg) 8%, transparent);
+  color: color-mix(in srgb, var(--fg) 62%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 16%, transparent);
+}
+.skill-necessity-irrelevant {
+  background: color-mix(in srgb, var(--color-danger, #e06c75) 18%, transparent);
+  color: var(--color-danger, #e06c75);
+}
+.skills-summary-strip {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  flex-wrap: nowrap;
+  overflow-x: auto;
+  overflow-y: hidden;
+  overscroll-behavior-x: contain;
+  touch-action: pan-x;
+  -webkit-overflow-scrolling: touch;
+  scrollbar-width: none;
+  margin: -3px 0 8px;
+}
+.skills-summary-strip::-webkit-scrollbar { display: none; }
+.skills-summary-strip:empty {
+  display: none;
+}
+.skills-summary-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+  height: 23px;
+  padding: 0 8px;
+  border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--fg) 4%, transparent);
+  color: color-mix(in srgb, var(--fg) 70%, transparent);
+  font-size: 10px;
+  line-height: 1;
+  cursor: pointer;
+  flex: 0 0 auto;
+}
+.skills-summary-chip:hover {
+  background: color-mix(in srgb, var(--fg) 8%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.skills-summary-chip.active {
+  background: color-mix(in srgb, var(--accent, var(--red)) 13%, transparent);
+  border-color: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
+  color: var(--fg);
+}
+.skills-summary-chip strong {
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  font-size: 10px;
+  color: var(--fg);
+}
+
+/* Shared filter/tag strip contract. These rows all sit immediately below
+   their screen's search or primary toolbar and stay one line at every width. */
+:is(
+  .skills-summary-strip,
+  #memory-category-filters,
+  .memory-category-filters:has(> .memory-cat-chip),
+  .doclib-lang-chips,
+  .doclib-chips,
+  .notes-labels-bar,
+  .tasks-activity-filters,
+  .gallery-tag-chips,
+  .gallery-album-chips,
+  .gallery-ai-tags,
+  .cal-filters
+) {
+  align-items: center;
+  flex-wrap: nowrap !important;
+  gap: 6px;
+  width: 100%;
+  max-width: 100%;
+  min-width: 0;
+  max-height: none;
+  overflow-x: auto !important;
+  overflow-y: hidden !important;
+  overscroll-behavior-x: contain;
+  touch-action: pan-x;
+  -webkit-overflow-scrolling: touch;
+  scrollbar-width: none;
+  box-sizing: border-box;
+  padding: 0;
+  margin: -3px 0 8px;
+}
+:is(
+  .skills-summary-strip,
+  #memory-category-filters,
+  .memory-category-filters:has(> .memory-cat-chip),
+  .doclib-lang-chips,
+  .doclib-chips,
+  .notes-labels-bar,
+  .tasks-activity-filters,
+  .gallery-tag-chips,
+  .gallery-album-chips,
+  .gallery-ai-tags,
+  .cal-filters
+)::-webkit-scrollbar { display: none; }
+:is(
+  .skills-summary-strip > .skills-summary-chip,
+  #memory-category-filters > .skills-summary-chip,
+  .memory-category-filters > .memory-cat-chip,
+  .doclib-lang-chips > :is(.memory-cat-chip, .skills-summary-chip),
+  .doclib-chips > .doclib-chip,
+  .notes-labels-bar > .notes-label-chip,
+  .tasks-activity-filters > .memory-cat-chip,
+  .gallery-tag-chips > .gallery-chip,
+  .gallery-album-chips > .gallery-chip,
+  .gallery-ai-tags > .gallery-ai-chip,
+  .cal-filters > .cal-filter-item
+) {
+  flex: 0 0 auto;
+  box-sizing: border-box;
+  height: 23px !important;
+  min-height: 23px;
+  padding: 0 8px !important;
+  border-radius: 6px !important;
+  font-size: 10px !important;
+  line-height: 1;
+  white-space: nowrap;
+}
+.doclib-lang-chips {
+  flex: 0 0 auto;
+  min-height: 25px;
+  padding: 1px 0 !important;
+  scroll-padding-inline: 1px;
+}
+.doclib-chip-scroll-frame {
+  position: relative;
+  display: flex;
+  align-items: center;
+  flex: 1 1 100%;
+  min-width: 0;
+  max-width: 100%;
+}
+.doclib-chip-scroll-frame > .chip-scroll-strip {
+  flex: 1 1 auto;
+  min-width: 0;
+  margin-left: 0;
+  margin-right: 0;
+}
+.doclib-chip-scroll-arrow {
+  display: none;
+  position: absolute;
+  top: calc(50% - 1px) !important;
+  transform: translateY(-50%);
+  margin-top: 0;
+  z-index: 2;
+  align-items: center;
+  justify-content: center;
+  width: 20px;
+  height: 20px;
+  padding: 0;
+  border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
+  border-radius: 50%;
+  box-sizing: border-box;
+  background: color-mix(in srgb, var(--bg) 86%, var(--fg) 14%);
+  color: var(--accent, var(--red));
+  box-shadow: 0 6px 18px rgba(0,0,0,0.16);
+  font-size: 18px;
+  line-height: 18px;
+  cursor: pointer;
+  opacity: 0.84;
+  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
+}
+.doclib-chip-scroll-arrow-glyph {
+  display: inline-block;
+  position: relative;
+  top: -1px;
+  line-height: 1;
+}
+.doclib-chip-scroll-arrow:hover {
+  opacity: 1;
+  background: color-mix(in srgb, var(--bg) 76%, var(--fg) 24%);
+}
+@media (max-width: 768px) {
+  .doclib-chip-scroll-arrow { display: none !important; }
+}
+.doclib-chip-scroll-frame.has-overflow > .doclib-chip-scroll-arrow:not([hidden]) {
+  display: inline-flex;
+}
+.doclib-chip-scroll-arrow.left { left: 0; }
+.doclib-chip-scroll-arrow.right { right: 0; }
+.doclib-chip-scroll-frame.has-overflow > .chip-scroll-strip {
+  padding-left: 0 !important;
+  padding-right: 22px !important;
+}
+.doclib-chip-scroll-frame > .chip-scroll-strip.is-pointer-dragging {
+  cursor: grabbing;
+  user-select: none;
+}
+.doclib-chip-scroll-frame > .chip-scroll-strip:not(.is-pointer-dragging) {
+  cursor: grab;
+}
+/* Calendar tags are wrapped by chipScroll.js after render. Keep that wrapper
+   content-sized inside the calendar's column layout; the default flex growth
+   otherwise turns the one-line tag strip into a large empty panel. */
+#cal-body > .doclib-chip-scroll-frame:has(> .cal-filters) {
+  flex: 0 0 auto;
+  min-height: 0;
+}
+/* The completed-task filters are a compact toolbar, not a flexible panel.
+   Without this override the shared chip frame grows into the available
+   height of the card, which is especially visible the first time the mobile
+   view switches to Completed. */
+.tasks-completed-card > .doclib-chip-scroll-frame:has(> #tasks-completed-status-chips) {
+  flex: 0 0 auto;
+  min-height: 0;
+}
+/* The active Tasks filter row is also a compact toolbar.  Keep its wrapper
+   content-sized so changing a filter cannot make the mobile panel grow a
+   large empty gap between the chips and the task cards. */
+.admin-card > .doclib-chip-scroll-frame:has(> #tasks-filter-chips) {
+  flex: 0 0 auto;
+  min-height: 0;
+}
+#memory-category-filters {
+  flex: 0 0 auto;
+  min-height: 25px;
+  padding: 1px 0 !important;
+  scroll-padding-inline: 1px;
+}
+.cookbook-serve-tag-label {
+  display: inline-flex;
+  align-items: center;
+  min-width: 0;
+}
+#serve-search,
+#hwfit-cache-select,
+#serve-tags {
+  position: relative;
+  top: -2px;
+}
+#hwfit-cache-select {
+  top: -4px;
+}
+/* Calendar previously framed its tags as a separate card. It now follows the
+   same unframed placement as the other filter strips. */
+.cal-filters {
+  border: 0;
+  background: none;
+  box-shadow: none;
+}
+.skill-duplicate-keep {
+  background: color-mix(in srgb, var(--color-success, #4ade80) 22%, transparent);
+  color: var(--color-success, #4ade80);
+}
+.skill-duplicate-group {
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  color: color-mix(in srgb, var(--fg) 62%, transparent);
+  background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.skill-duplicate-lower {
+  background: color-mix(in srgb, var(--color-danger, #e06c75) 14%, transparent);
+  color: var(--color-danger, #e06c75);
+}
+.skill-stats {
+  font-size: 9px;
+  font-family: monospace;
+  white-space: nowrap;
+  color: color-mix(in srgb, var(--fg) 45%, transparent);
+}
+.skill-turns-saved {
+  font-size: 9px;
+  opacity: 0.82;
+  font-variant-numeric: tabular-nums;
+  white-space: nowrap;
+  flex: 0 0 auto;
+}
+.skill-expanded-savings {
+  display: flex;
+  align-items: center;
+  min-height: 18px;
+  margin: 0 0 5px;
+}
+.skill-turns-positive {
+  color: color-mix(in srgb, var(--color-success, #4ade80) 82%, var(--fg));
+}
+.skill-turns-zero,
+.skill-turns-unknown {
+  color: color-mix(in srgb, var(--fg) 48%, transparent);
+}
+.skill-turns-negative {
+  color: color-mix(in srgb, var(--color-danger, #e06c75) 80%, var(--fg));
+}
+.skill-conf {
+  display: inline-block;
+  min-width: 4ch;
+  text-align: right;
+  font-weight: 700;
+  color: var(--accent, var(--red));
+}
+.skill-verified,
+.skill-teachermark,
+.skill-needsmark {
+  display: inline-flex;
+  align-items: center;
+  vertical-align: middle;
+  margin-right: 3px;
+}
+.skill-verified { color: var(--accent, #4ade80); }
+.skill-teachermark { color: var(--color-warning, #f0ad4e); }
+.skill-needsmark { color: var(--color-danger, #e06c75); cursor: help; }
+
+/* The card currently being processed by an "Audit now" run glows + pulses so
+   it's obvious which one the audit is on. */
+@keyframes skill-audit-pulse {
+  0%, 100% { box-shadow: 0 0 6px 0 color-mix(in srgb, var(--accent, #4ade80) 55%, transparent); }
+  50%      { box-shadow: 0 0 16px 3px color-mix(in srgb, var(--accent, #4ade80) 85%, transparent); }
+}
+.skill-card.skill-audit-active {
+  border-color: var(--accent, #4ade80) !important;
+  animation: skill-audit-pulse 1.3s ease-in-out infinite;
+}
+
+/* A test is running for this skill — the app's whirlpool spinner is injected
+   next to the name from JS (see _setCardRunning) so the collapsed/folded card
+   still makes it obvious work is in progress. */
+/* Collapsed bar shows the kebab menu; expanded shows an up-chevron to
+   collapse. (Toggled by the .doclib-card-expanded class.) */
+.skill-kebab-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background: none;
+  border: none;
+  padding: 4px;
+  margin: 0;
+  cursor: pointer;
+  color: var(--fg);
+  opacity: 0.5;
+  border-radius: 5px;
+  flex-shrink: 0;
+  position: relative;
+  top: -2px;
+}
+.skill-kebab-btn:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 10%, transparent); }
+.skill-chevron-up { display: none; align-items: center; opacity: 0.5; flex-shrink: 0; }
+/* Kebab dropdown */
+.skill-kebab-menu {
+  position: fixed;
+  /* z-index is set inline via topPortalZ() at open time (#4720); a static
+     value here loses once the modal bring-to-front counter climbs past it. */
+  min-width: auto;
+  width: max-content;
+  padding: 4px;
+  background: var(--panel, var(--bg));
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+  display: flex;
+  flex-direction: column;
+  gap: 1px;
+}
+.skill-kebab-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+  padding: 6px 8px;
+  background: none;
+  border: none;
+  border-radius: 8px;
+  cursor: pointer;
+  color: var(--fg);
+  font-size: 11px;
+  text-align: left;
+}
+.skill-kebab-item:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
+.skill-kebab-item.danger { color: var(--color-error, #e55); }
+.skill-kebab-item.danger:hover { background: color-mix(in srgb, var(--color-error, #e55) 15%, transparent); }
+/* Section labels separating "Your skills" from the read-only "Built-in
+   capabilities" list. */
+.skills-section-label {
+  font-size: 10px;
+  opacity: 0.5;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  margin: 10px 2px 4px;
+  flex-shrink: 0;
+}
+.skills-section-label:first-child { margin-top: 0; }
+/* When a learned skill is expanded the grid hides sibling cards; hide the
+   section labels too so they don't orphan above the collapsed list. */
+#skills-list:has(.doclib-card-expanded) .skills-section-label { display: none; }
+/* Built-in cards are read-only — no expand/hover-pointer affordance. */
+.skill-builtin-card { cursor: default; }
+.skill-builtin-card:hover { border-color: var(--border); }
+.skill-card-preview .skill-md-pre {
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  font-size: 11.5px;
+  line-height: 1.5;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.skill-preview-copy {
+  position: absolute;
+  top: 6px;
+  right: 8px;
+  z-index: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  padding: 0;
+  opacity: 0;
+  color: var(--fg);
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  cursor: pointer;
+  transition: opacity .15s, color .15s, border-color .15s;
+}
+.skill-card-preview:hover .skill-preview-copy { opacity: 0.7; }
+.skill-preview-copy:hover,
+.skill-preview-copy:focus-visible { opacity: 1 !important; }
+.skill-preview-copy.copied {
+  opacity: 1;
+  color: var(--color-save-green, #4caf50);
+  border-color: var(--color-save-green, #4caf50);
+}
+/* :has()-free expand layout. skills.js adds .skills-has-expanded to the
+   skills admin-card when a skill opens, so this works on engines that don't
+   support :has() (e.g. older Firefox mobile, where the expand stuck at
+   ~50%). Hide everything but the list so the grid fills the card; the
+   absolutely-positioned expanded card (mobile) then fills the grid. */
+.admin-card.skills-has-expanded > *:not(#skills-list) {
+  display: none !important;
+}
+.admin-card.skills-has-expanded > #skills-list.doclib-grid {
+  flex: 1 1 auto !important;
+  height: 100% !important;
+  min-height: 0 !important;
+  overflow: hidden !important;
+}
+/* Expand animation — a clean opacity fade. The mobile card snaps to a
+   full-screen overlay (position:absolute), so any translate/scale on it
+   reads as a jarring "explosion"; a pure fade just appears smoothly. (No
+   transform also keeps skills.js's height measurement accurate.) */
+@keyframes skill-card-expand {
+  from { opacity: 0; }
+  to   { opacity: 1; }
+}
+.skill-card.doclib-card-expanded {
+  animation: skill-card-expand 0.18s ease-out;
+}
+/* Switching directly from one expanded card to another: no fade (it would
+   show the previous card collapsing through the semi-transparent new one). */
+.skill-card.doclib-card-expanded.skill-expand-instant {
+  animation: none !important;
+}
+@media (prefers-reduced-motion: reduce) {
+  .skill-card.doclib-card-expanded,
+  .skill-card.doclib-card-expanded .doclib-card-preview {
+    animation: none !important;
+  }
+}
+/* The preview WRAPPER inherits flex:1 (grow) from the generic doclib expand
+   rule — that's what stretched the card and left a void / pushed the footer
+   out. Size it to content (shrink + scroll only when long) so the footer
+   sits right under the preview and stays fully visible. */
+.skill-card.doclib-card-expanded .doclib-card-preview {
+  flex: 0 1 auto !important;
+  min-height: 0;
+  /* The footer lives INSIDE this wrapper. Don't let the wrapper scroll, or
+     the footer scrolls off with the content. Keep it clipped; the 
+     inside is the scroller, so the footer stays pinned at the bottom. */
+  overflow: hidden !important;
+}
+.skill-card.doclib-card-expanded .skill-md-pre {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow-y: auto;
+}
+/* Skill footer buttons sit 4px high (shared rule sets top:-4px) — drop them
+   back down 4px so they're vertically centered in the skill card footer. */
+.skill-card .doclib-card-expanded-actions > .doclib-card-action-btn,
+.skill-card .doclib-card-expanded-actions .doclib-action-btn-row > .doclib-card-action-btn {
+  top: 0;
+}
+/* Skills select-mode bulk bar — nudge Cancel / Approve / Delete up 2px. */
+#skills-bulk-bar .memory-toolbar-btn {
+  position: relative;
+  top: -2px;
+}
+/* ── Audit-all progress panel ── */
+.skills-audit-panel {
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  padding: 8px 10px;
+  margin: 6px 0;
+  background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.skills-audit-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+.skills-audit-title { font-size: 12px; font-weight: 600; }
+.skills-audit-bar { height: 4px; border-radius: 2px; background: color-mix(in srgb, var(--fg) 12%, transparent); margin: 6px 0; overflow: hidden; }
+.skills-audit-fill { height: 100%; background: var(--accent, var(--red)); transition: width 0.3s ease; }
+.skills-audit-summary { font-size: 11px; opacity: 0.7; margin-bottom: 4px; }
+.skills-audit-log {
+  max-height: 22vh; overflow-y: auto;
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  font-size: 10.5px; line-height: 1.5; opacity: 0.8;
+}
+/* ── Skill test monitor + AI eval verdict ── */
+.skill-test { display: flex; flex-direction: column; gap: 8px; min-height: 0; }
+.skill-test-log {
+  flex: 1 1 auto;
+  min-height: 120px;
+  max-height: 48vh;
+  overflow-y: auto;
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  font-size: 9.5px;
+  line-height: 1.45;
+  padding: 8px 10px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  background: var(--bg);
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.skill-test-log > div { margin: 1px 0; }
+.skill-test-meta  { opacity: 0.5; }
+.skill-test-task  { color: var(--accent, var(--red)); font-weight: 500; font-size: 9px; opacity: 0.85; }
+.skill-test-round { opacity: 0.6; margin-top: 6px !important; }
+.skill-test-tool  { color: var(--accent, var(--red)); opacity: 0.85; }
+.skill-test-out   { opacity: 0.7; padding-left: 10px; }
+.skill-test-say   { white-space: pre-wrap; }
+.skill-test-err   { color: var(--color-danger, #e06c75); }
+.skill-eval-head  { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
+.skill-eval-badge {
+  font-size: 10px; font-weight: 700; letter-spacing: 0.03em;
+  padding: 2px 8px; border-radius: 10px; flex-shrink: 0;
+}
+.skill-eval-ok   { background: color-mix(in srgb, var(--color-success, #4ade80) 26%, transparent); color: var(--color-success, #4ade80); }
+.skill-eval-warn { background: color-mix(in srgb, var(--color-warning, #f0ad4e) 26%, transparent); color: var(--color-warning, #f0ad4e); }
+.skill-eval-bad  { background: color-mix(in srgb, var(--color-danger, #e06c75) 26%, transparent); color: var(--color-danger, #e06c75); }
+.skill-eval-unknown { background: color-mix(in srgb, var(--fg) 14%, transparent); }
+.skill-eval-summary { font-size: 11px; opacity: 0.85; }
+.skill-eval-issues { margin: 6px 0 0; padding-left: 18px; font-size: 11px; opacity: 0.8; }
+.skill-eval-issues li { margin: 2px 0; }
+.skill-eval-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 8px; }
+.skill-eval-approve.suggested {
+  border-color: var(--color-success, #4ade80) !important;
+  color: var(--color-success, #4ade80) !important;
+}
+/* Already published — the button confirms the state (click to unpublish). */
+.skill-eval-approve.is-approved {
+  border-color: var(--color-success, #4ade80) !important;
+  color: var(--color-success, #4ade80) !important;
+  background: color-mix(in srgb, var(--color-success, #4ade80) 14%, transparent) !important;
+}
+/* Add-Skill form: a "rich placeholder" overlay so only the FIRST word (Title,
+   When to use, How, Tags) is accent-colored while the rest stays muted — real
+   placeholders can't be partially colored. The native placeholder is a single
+   space so :placeholder-shown still toggles the overlay. */
+.skill-ph-wrap { position: relative; }
+.skill-ph-wrap .skill-hint-input { width: 100%; box-sizing: border-box; }
+.skill-rich-ph {
+  position: absolute;
+  left: 11px; right: 11px; top: 50%;
+  transform: translateY(-50%);
+  pointer-events: none;
+  font-size: 12px;
+  line-height: 1.2;
+  color: color-mix(in srgb, var(--fg) 40%, transparent);
+  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+/* Textarea: align the hint to the first line instead of vertical-centering. */
+.skill-rich-ph-top { top: 7px; transform: none; }
+.skill-rich-ph .k { color: var(--accent, var(--red)); }
+/* Keep the memory-input enter hint pinned to the field's right edge instead
+   of letting it drift after the example text. */
+#new-memory-input ~ .skill-rich-ph > svg {
+  position: absolute;
+  right: 0;
+  top: 50%;
+  transform: translateY(-50%);
+  margin-left: 0 !important;
+}
+/* Hide the overlay once the field has real content. */
+.skill-hint-input:not(:placeholder-shown) ~ .skill-rich-ph { display: none; }
+.skill-md-editor {
+  flex: 1 1 auto;
+  min-height: 0;
+  width: 100%;
+  box-sizing: border-box;
+  resize: none;
+  font-family: ui-monospace, 'SF Mono', 'Fira Code', monospace;
+  font-size: 11.5px;
+  line-height: 1.5;
+  padding: 8px;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  background: var(--bg);
+  color: var(--fg);
+}
+.doclib-empty {
+  text-align: center;
+  color: color-mix(in srgb, var(--fg) 35%, transparent);
+  padding: 32px 16px;
+  font-size: 12px;
+  font-style: italic;
+}
+/* Unified loading row across every Library tab (Chats / Documents / Research /
+   Archive): one opacity, one size, with the whirlpool spinner next to it. */
+.lib-loading-row {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  padding: 28px 16px;
+  font-size: 12px;
+  color: color-mix(in srgb, var(--fg) 45%, transparent);
+}
+.pdf-loading-state {
+  min-height: 120px;
+  margin: 0;
+  box-sizing: border-box;
+  flex-direction: column;
+  gap: 10px;
+  border: 0;
+  border-radius: 0;
+  background: transparent;
+  color: color-mix(in srgb, var(--fg) 68%, transparent);
+  box-shadow: none;
+  font-size: 12px;
+  font-weight: 600;
+  letter-spacing: 0.01em;
+}
+.pdf-loading-state .ai-spinner-whirlpool {
+  color: var(--accent, var(--red));
+  opacity: 0.95;
+}
+.pdf-loading-state .ai-spinner-whirlpool canvas {
+  display: block;
+}
+.doclib-load-more {
+  display: block;
+  margin: 10px auto 0;
+  padding: 6px 16px;
+  background: transparent;
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  color: var(--fg-muted);
+  font-size: 11px;
+  cursor: pointer;
+  transition: border-color 0.15s, color 0.15s;
+  flex-shrink: 0;
+  position: relative;
+  top: 10px;
+}
+/* Soft dark gradient above the button — blends the list content into the
+   load-more strip instead of a hard cutoff. Spans the modal width and
+   sits behind/above the button. */
+.doclib-load-more::before {
+  content: "";
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  bottom: 100%;
+  margin-bottom: 4px;
+  width: min(600px, 92vw);
+  height: 34px;
+  pointer-events: none;
+  background: linear-gradient(to top, color-mix(in srgb, #000 24%, transparent), transparent);
+}
+.doclib-load-more:hover {
+  border-color: var(--red);
+  color: var(--red);
+}
+.doclib-inline-load-more {
+  border: 0;
+  border-radius: 0;
+  padding: 0;
+  background: none;
+  top: 0;
+  font-size: 9px !important;
+  line-height: 1.2 !important;
+}
+
+/* Document library toolbar buttons */
+.doclib-toolbar-btn {
+  background: none;
+  border: 1px solid var(--border);
+  color: var(--fg-muted);
+  font-size: 11px;
+  padding: 5px 10px;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.15s;
+  white-space: nowrap;
+  font-family: inherit;
+  display: inline-flex;
+  align-items: center;
+  gap: 3px;
+}
+.doclib-toolbar-btn:hover {
+  color: var(--fg);
+  border-color: var(--fg);
+}
+.doclib-toolbar-btn.active {
+  background: color-mix(in srgb, var(--red) 15%, transparent);
+  border-color: color-mix(in srgb, var(--red) 40%, transparent);
+  color: var(--red);
+}
+.doclib-toolbar-btn.danger {
+  color: var(--color-error, #e55);
+}
+.doclib-toolbar-btn.danger:hover:not(:disabled) {
+  border-color: var(--color-error, #e55);
+}
+.doclib-toolbar-btn:disabled {
+  opacity: 0.4;
+  cursor: default;
+}
+
+/* Bulk action bar */
+.doclib-bulk-bar {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 4px 8px;
+  border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+  border-radius: 6px;
+  background: color-mix(in srgb, var(--red) 5%, transparent);
+  font-size: 10px;
+  margin-bottom: 8px;
+}
+.doclib-bulk-bar.hidden {
+  display: none;
+}
+.doclib-bulk-check-all {
+  display: flex;
+  align-items: center;
+  gap: 3px;
+  cursor: pointer;
+  color: color-mix(in srgb, var(--fg) 60%, transparent);
+  font-size: 10px;
+}
+#doclib-bulk-bar .memory-toolbar-btn {
+  position: relative;
+  top: -3px;
+}
+
+/* Custom checkboxes */
+.doclib-select-cb,
+.doclib-bulk-check-all input {
+  -webkit-appearance: none;
+  appearance: none;
+  width: 13px;
+  height: 13px;
+  border: 1px solid var(--border);
+  border-radius: 4px;
+  background: var(--bg);
+  cursor: pointer;
+  flex-shrink: 0;
+  margin: 0;
+  position: relative;
+  transition: all 0.15s;
+}
+.doclib-select-cb:hover,
+.doclib-bulk-check-all input:hover {
+  border-color: var(--red);
+}
+.doclib-select-cb:checked,
+.doclib-bulk-check-all input:checked {
+  background: var(--red);
+  border-color: var(--red);
+}
+.doclib-select-cb:checked::after,
+.doclib-bulk-check-all input:checked::after {
+  content: '';
+  position: absolute;
+  left: 3px;
+  top: 0px;
+  width: 4px;
+  height: 8px;
+  border: solid var(--bg);
+  border-width: 0 1.5px 1.5px 0;
+  transform: rotate(45deg);
+}
+
+
+/* + button outside scroll area (when doc is on right side) */
+
+@media (max-width: 600px) {
+  .doclib-toolbar {
+    flex-direction: column;
+  }
+  .doclib-card-session,
+  .doclib-card-time {
+    display: none;
+  }
+  .doclib-card-preview {
+    max-height: 40vh;
+  }
+}
+
+/* Collapse card tags before they compete with attachment/favorite controls. */
+.email-card-tags .email-tags-more {
+  color: var(--email-tags-more-color, var(--fg));
+  background: color-mix(in srgb, var(--email-tags-more-color, var(--fg)) 18%, transparent);
+}
+.email-tags-more-single,
+[data-email-tags-count-mini] { display: none; }
+.email-tags-more-single svg { display: none; }
+.email-card-tags.email-tags-mini:not(.email-tags-expanded) > .email-tag,
+.email-card-tags.email-tags-mini:not(.email-tags-expanded) > .email-tag-extra {
+  display: none;
+}
+.email-card-tags.email-tags-mini:not(.email-tags-expanded) .email-tags-more-single,
+.email-card-tags.email-tags-expanded .email-tags-more-single {
+  display: inline-flex;
+}
+.email-card-tags.email-tags-mini:not(.email-tags-expanded) [data-email-tags-count-normal] { display: none; }
+.email-card-tags.email-tags-mini:not(.email-tags-expanded) [data-email-tags-count-mini],
+.email-card-tags.email-tags-expanded .email-tags-more-single [data-email-tags-count-mini] { display: inline; }
+
+/* Keep the reader actions in drafting order. */
+.email-reader-actions-inline > [data-act="ai-reply"] { order: 1; }
+.email-reader-actions-inline > [data-act="reply"] { order: 2; }
+.email-reader-actions-inline > [data-act="reply-all"] { order: 3; }
+.email-reader-actions-inline > [data-act="forward"] { order: 4; }
diff --git a/static/css/09-gallery.css b/static/css/09-gallery.css
new file mode 100644
index 000000000..6554a5813
--- /dev/null
+++ b/static/css/09-gallery.css
@@ -0,0 +1,1382 @@
+/* 09-gallery - part of the former static/style.css.
+ *
+ * The stylesheet is split by area, but the split is mechanical: rules were
+ * moved verbatim and NOT regrouped. The numeric prefix is the load order, and
+ * that order is what preserves the cascade. A rule sits in the file its
+ * original position fell into, so an area's rules are not all in its file and
+ * a file is not only that area's rules.
+ *
+ * Do not reorder the  tags in static/index.html, and do not move a rule
+ * between files: with 1,151 redeclarations and 1,886 !important in this
+ * cascade, either can change which declaration wins.
+ *
+ * static/index.html and the PRECACHE list in static/sw.js must agree on this
+ * order. tests/test_static_stylesheet_manifest.py and
+ * tests/test_css_computed_style_snapshot.py fail if that drifts.
+ */
+.email-reader-actions-inline.email-reader-actions-compact > .reader-action-overflowed {
+  display: none !important;
+}
+
+/* ── Archive browser ── */
+.archive-list {
+  grid-template-columns: 1fr !important;
+  gap: 4px !important;
+}
+.archive-row {
+  flex-direction: row !important;
+}
+.archive-row .doclib-card-header {
+  flex: 1;
+  padding: 8px 10px;
+}
+.archive-row .doclib-card-title {
+  flex: 1;
+  min-width: 0;
+}
+/* Archive column layout */
+.archive-header {
+  display: flex;
+  align-items: center;
+  padding: 4px 10px;
+  gap: 6px;
+  font-size: 9px;
+  font-weight: 600;
+  opacity: 0.35;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+.archive-col-title {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-left: -4px;
+}
+.archive-col-msgs {
+  width: 40px;
+  flex-shrink: 0;
+  text-align: left;
+  margin-left: -8px;
+}
+.archive-col-model {
+  width: 90px;
+  flex-shrink: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.archive-col-time {
+  width: 55px;
+  flex-shrink: 0;
+  text-align: right;
+}
+.archive-col-menu {
+  width: 24px;
+  flex-shrink: 0;
+}
+.archive-menu-btn {
+  background: transparent;
+  border: none;
+  color: var(--fg);
+  opacity: 0.3;
+  cursor: pointer;
+  padding: 4px;
+  line-height: 0;
+  border-radius: 4px;
+  transition: background .15s, opacity .15s;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-left: 4px;
+  vertical-align: middle;
+  height: 100%;
+}
+.archive-menu-btn svg {
+  display: block;
+  margin-top: -6px;
+}
+.archive-menu-btn:hover {
+  background: color-mix(in srgb, var(--fg) 10%, transparent);
+  opacity: 1;
+}
+
+/* ── Gallery (image library) ── */
+
+.gallery-modal-content {
+  width: min(820px, 94vw);
+  max-height: 92vh;
+  font-size: 12px;
+}
+/* Keep the Gallery frame anchored while tabs or bulk-selection controls
+   change the content height. Centering the frame made every shorter tab move
+   the whole window vertically. */
+@media (min-width: 769px) {
+  #calendar-modal,
+  #gallery-modal,
+  #tasks-modal,
+  #memory-modal,
+  #doclib-modal,
+  #compare-model-overlay,
+  #research-overlay,
+  #theme-modal,
+  #settings-modal,
+  #email-lib-modal,
+  #cookbook-modal {
+    align-items: flex-start;
+    padding-top: 4vh;
+    box-sizing: border-box;
+  }
+}
+/* While the Edit tab is active the editor needs a *definite* height so
+   the right-side tool panel (`.ge-right-panel { overflow-y:auto }`)
+   scrolls INTERNALLY. Without this the modal only has `max-height`, so
+   `.gallery-editor { height:100% }` can't resolve — the editor grows to
+   its full content height, overflows the modal body, and the modal-body
+   scrollbar that appears can't be wheel-scrolled because the editor's
+   `overscroll-behavior:contain` blocks the chain. Pinning a real height
+   here makes the panel bounded and scrollable as designed. Scoped to
+   the editor view (matched via the container's inline `display:flex`)
+   so the Photos/Albums views keep sizing to their content. */
+.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) {
+  height: 92vh;
+}
+/* The editor tab owns the modal body's scroll context. Keep the landing
+   chooser and the mounted editor inside the available sheet height so a long
+   saved-project list cannot push the launch controls below the viewport. */
+.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) > .modal-body {
+  display: flex;
+  min-height: 0;
+  overflow: hidden;
+}
+.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) #gallery-editor-container {
+  min-height: 0;
+  overflow: hidden;
+}
+/* Photo-detail view sizing (issue #314).
+   The detail view is rendered as a `position:absolute; inset:0` overlay
+   *inside* `.gallery-images-container`, painted over the photo grid. Because
+   it's absolutely positioned it can't contribute to the container's height —
+   the container (and therefore the overlay's `inset:0` box) collapses to the
+   height of the grid sitting behind it. When the library only has a few
+   photos that grid is short, so the detail view is crushed: the image is
+   clipped and the metadata sidebar (`overflow-y:auto`) is squeezed into a
+   tiny, internally-scrolling strip. (With a large library the grid is tall,
+   which is why it looked fine in the demo video but cramped for users with
+   few photos.)
+   Fix: when the detail view is open, hide the grid-view siblings and drop the
+   overlay into normal flow. The container — and the window, up to its 92vh
+   max-height — then sizes to the detail's own content (image + metadata), so
+   nothing is clipped or squeezed regardless of how many photos exist. Scoped
+   via the detail element's inline `display:flex` so the grid / albums views
+   keep sizing to their own content. Works on both desktop and the mobile
+   full-screen sheet. */
+#gallery-images-container:has(> #gallery-detail[style*="flex"]) > *:not(#gallery-detail) {
+  display: none !important;
+}
+#gallery-images-container:has(> #gallery-detail[style*="flex"]) > #gallery-detail {
+  position: static;
+}
+/* Containing block for the photo-detail overlay — keeps it inside the body
+   so it sits below the modal header and the tab strip instead of covering them. */
+.gallery-images-container {
+  position: relative;
+}
+.gallery-modal-content .modal-header h4 {
+  font-size: 1rem;
+}
+.gallery-stats {
+  font-size: 10px;
+  color: color-mix(in srgb, var(--fg) 50%, transparent);
+  margin-bottom: 4px;
+}
+.gallery-toolbar {
+  display: flex;
+  gap: 6px;
+  margin-bottom: 4px;
+  align-items: center;
+}
+.gallery-toolbar-break { display: none; }
+/* Keep the hint anchored while independently seating the input in the row. */
+.gallery-search-wrap { position: relative; flex: 1; min-width: 0; display: flex; top: 0; }
+.gallery-search {
+  position: relative;
+  top: 4px;
+  flex: 1;
+  width: 100%;
+  padding: 4px 8px;
+  padding-right: 78px;   /* room for the enter hint */
+  background: var(--bg);
+  color: var(--fg);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  font: inherit;
+  font-size: 12px;
+  outline: none;
+}
+.gallery-search-enter-hint {
+  position: absolute;
+  right: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 10px;
+  font-weight: 600;
+  color: var(--accent, var(--red));
+  pointer-events: none;
+  opacity: 0;
+  transition: opacity 0.12s;
+  white-space: nowrap;
+}
+/* Show the hint only once the user has typed something (placeholder gone). */
+.gallery-search:not(:placeholder-shown) ~ .gallery-search-enter-hint { opacity: 0.95; }
+.gallery-search:focus {
+  border-color: var(--red);
+}
+.gallery-model-filter,
+.gallery-sort {
+  padding: 4px 8px;
+  background: var(--bg);
+  border: 1px solid var(--border);
+  border-radius: 6px;
+  color: var(--fg);
+  font: inherit;
+  font-size: 11px;
+  cursor: pointer;
+  position: relative;
+  top: 4px;
+}
+.gallery-selection-bar {
+  margin-bottom: 4px;
+  position: relative;
+  top: 2px;
+  padding-right: 18px;
+}
+.gallery-tag-chips {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 4px;
+  margin-bottom: 4px;
+}
+.gallery-chip {
+  height: 28px;
+  box-sizing: border-box;
+  padding: 0 13px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+  border-radius: 14px;
+  font-size: 12px;
+  line-height: 1;
+  border: 1px solid var(--border);
+  background: none;
+  color: var(--fg);
+  cursor: pointer;
+  transition: border-color 0.15s, background 0.15s;
+}
+.gallery-chip:hover {
+  border-color: var(--red);
+}
+.gallery-chip.active {
+  background: color-mix(in srgb, var(--red) 15%, transparent);
+  border-color: color-mix(in srgb, var(--red) 40%, transparent);
+  color: var(--red);
+}
+/* Album chips row */
+.gallery-album-chips {
+  display: flex; gap: 4px; flex-wrap: wrap; padding: 0 0 3px;
+}
+.gallery-chip-fav { color: var(--red); }
+.gallery-chip-fav.active { background: color-mix(in srgb, var(--red) 15%, transparent); border-color: var(--red); }
+.gallery-chip-add { opacity: 0.5; font-size: 14px; padding: 2px 10px; }
+/* Active-album indicator — appears when the Photos grid is filtered to one album. */
+.gallery-chip-active-album,
+.gallery-active-filter-pill {
+  display: inline-flex; align-items: center; gap: 4px;
+  background: color-mix(in srgb, var(--red) 14%, transparent);
+  border-color: color-mix(in srgb, var(--red) 45%, transparent);
+  position: relative;
+  top: 6px;
+}
+.gallery-active-filter-pill {
+  background: color-mix(in srgb, var(--fg) 7%, transparent);
+  border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+  color: color-mix(in srgb, var(--fg) 86%, transparent);
+}
+.gallery-clear-all-filters {
+  height: 28px;
+  border-style: dashed;
+  opacity: 0.72;
+}
+.gallery-clear-all-filters:hover {
+  opacity: 1;
+  color: var(--red);
+}
+.gallery-chip-clear {
+  background: none;
+  border: none;
+  color: inherit;
+  opacity: 0.6;
+  cursor: pointer;
+  padding: 0;
+  margin-left: 1px;
+  width: 12px;
+  height: 12px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  line-height: 1;
+  position: relative;
+  top: -4px;
+}
+.gallery-chip-clear:hover { opacity: 1; }
+
+
+
+/* Favorite button on card */
+/* Video cards: