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(' ');
+ 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()} `);
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 so each
+ option can carry its own SVG icon. Themed to match .email-filter-btn. */
+.memory-sort-picker {
+ position: relative;
+ top: 0;
+ flex-shrink: 0;
+}
+.memory-sort-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: inherit;
+ font-size: 11px;
+ height: 24px;
+ padding: 0 8px;
+ cursor: pointer;
+ text-align: left;
+ transition: border-color 0.12s, background 0.12s;
+}
+.memory-sort-btn:hover { border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border)); }
+.memory-sort-btn:focus-visible {
+ outline: none;
+ border-color: var(--accent, var(--red));
+}
+.memory-sort-current {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ white-space: nowrap;
+}
+.memory-sort-icon-cur {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.memory-sort-icon-cur svg { width: 13px; height: 13px; }
+.memory-sort-caret {
+ flex-shrink: 0;
+ opacity: 0.6;
+ transition: transform 0.15s;
+}
+
+.memory-sort-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ min-width: 140px;
+ z-index: 100;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+ padding: 4px;
+}
+.memory-sort-item {
+ all: unset;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 6px 9px;
+ border-radius: 5px;
+ font-size: 12px;
+ cursor: pointer;
+ color: var(--fg);
+ box-sizing: border-box;
+}
+.memory-sort-item:hover,
+.memory-sort-item:focus-visible {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.memory-sort-item-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.memory-sort-item-icon svg { width: 13px; height: 13px; }
+.memory-sort-item-label {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Enabled/Disabled text label next to an admin toggle.
+ Matches the .memory-desc treatment (same color + 11px size) so it
+ visually belongs to the section description, not the switch. */
+.admin-toggle-state {
+ font-size: 10px;
+ line-height: 1.5;
+ font-weight: normal;
+ letter-spacing: 0.02em;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+}
+.admin-card > div:has(.admin-switch input) .admin-toggle-state::before {
+ content: "Disabled";
+}
+.admin-card > div:has(.admin-switch input:checked) .admin-toggle-state::before {
+ content: "Enabled";
+}
+/* Nudge the bulk-bar action buttons up 2px (and Memory's -2px left) to
+ align with the row baseline. Covers both the Memory bulk bar
+ (Cancel/Delete) and the Skills bulk bar (Cancel/Approve/Delete) — both
+ live inside #memory-modal. */
+#memory-modal .memory-bulk-bar #memory-bulk-cancel,
+#memory-modal .memory-bulk-bar #memory-bulk-delete {
+ position: relative;
+ top: -3px;
+ left: -2px;
+}
+#memory-modal .memory-bulk-bar #skills-bulk-cancel,
+#memory-modal .memory-bulk-bar #skills-bulk-publish,
+#memory-modal .memory-bulk-bar #skills-bulk-audit,
+#memory-modal .memory-bulk-bar #skills-bulk-delete-nonpassing,
+#memory-modal .memory-bulk-bar #skills-bulk-delete {
+ position: relative;
+ top: -2px;
+}
+#skills-bulk-bar #skills-bulk-delete-nonpassing {
+ margin-left: auto;
+}
+#skills-select-btn .memory-select-btn-icon {
+ position: relative;
+ top: -2px;
+}
+#doclib-research-bulk .memory-toolbar-btn {
+ position: relative;
+ top: 3px;
+ right: 16px;
+}
+/* Archive bulk buttons — nudge down 1px to match research. */
+#doclib-arc-bulk .memory-toolbar-btn {
+ position: relative;
+ top: 1px;
+}
+/* Same right-aligned layout for the other bulk bars — Chats, Documents,
+ Archive, Skills, Memories. Cancel's auto-margin pushes the action group
+ to the right; 8px of extra right padding seats it off the edge (matching
+ the research bar's 8px nudge). */
+#doclib-chats-bulk #doclib-chats-bulk-archive,
+#doclib-bulk-bar #doclib-bulk-archive,
+#doclib-arc-bulk #doclib-arc-bulk-restore,
+#email-lib-bulk #email-lib-bulk-actions,
+#tasks-bulk-bar #tasks-bulk-delete,
+#serve-bulk-bar #serve-bulk-delete,
+#gallery-bulk-bar #gallery-bulk-actions,
+#gallery-editor-drafts-bulk #gallery-editor-drafts-bulk-delete,
+#memory-modal .memory-bulk-bar #memory-bulk-delete,
+#memory-modal .memory-bulk-bar #skills-bulk-publish {
+ margin-left: auto;
+ position: relative;
+ top: -1px;
+}
+
+/* X-icon Cancel button used in every bulk-select bar (Esc target). The bare SVG
+ sits slightly too high vs. the adjacent text buttons — nudge it down 2px. */
+[id$="-bulk-cancel"] {
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ border-color: color-mix(in srgb, var(--red) 40%, transparent);
+ color: var(--red);
+}
+[id$="-bulk-cancel"]:hover {
+ background: color-mix(in srgb, var(--red) 24%, transparent);
+ border-color: var(--red);
+ color: var(--red);
+}
+[id$="-bulk-cancel"] svg {
+ width: 12px;
+ height: 12px;
+ position: relative;
+ top: 2px;
+}
+#doclib-chats-bulk,
+#doclib-bulk-bar,
+#doclib-arc-bulk,
+#email-lib-bulk,
+#tasks-bulk-bar,
+#serve-bulk-bar,
+#gallery-bulk-bar,
+#gallery-editor-drafts-bulk,
+#memory-modal .memory-bulk-bar {
+ padding-right: 18px;
+}
+#email-lib-bulk-delete.email-bulk-loading,
+#email-lib-bulk-actions.email-bulk-loading,
+#tasks-bulk-delete.tasks-bulk-loading {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ opacity: 0.9;
+ cursor: wait;
+}
+#email-lib-bulk-delete.email-bulk-loading .email-bulk-whirlpool,
+#email-lib-bulk-actions.email-bulk-loading .email-bulk-whirlpool,
+#tasks-bulk-delete.tasks-bulk-loading .tasks-bulk-whirlpool {
+ width: 12px;
+ height: 12px;
+ margin: 0;
+ position: relative;
+ top: -1px;
+}
+#email-lib-bulk-delete.email-bulk-loading .email-bulk-loading-label,
+#email-lib-bulk-actions.email-bulk-loading .email-bulk-loading-label,
+#tasks-bulk-delete.tasks-bulk-loading .tasks-bulk-loading-label {
+ position: relative;
+ top: 0;
+}
+/* Nudge the whole memory + skills bulk buttons (icon + label together) up. */
+#memory-modal #memory-bulk-bar .memory-toolbar-btn,
+#memory-modal #skills-bulk-bar .memory-toolbar-btn {
+ position: relative;
+ top: -2px;
+}
+
+.memory-bulk-check-all {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ cursor: pointer;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ font-size: 10px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ user-select: none;
+ position: relative;
+ top: 0;
+}
+.memory-bulk-check-all:hover {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+
+#memory-selected-count {
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+ font-size: 10px;
+ flex: 1;
+}
+
+/* Custom checkbox — toggle dot (shared by select-all and per-item) */
+.memory-select-cb,
+.memory-bulk-check-all input {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 6px !important;
+ height: 6px !important;
+ min-width: 6px;
+ min-height: 6px;
+ max-width: 6px;
+ max-height: 6px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: transparent;
+ cursor: pointer;
+ flex-shrink: 0;
+ margin: 0;
+ align-self: center;
+ position: relative;
+ box-sizing: content-box;
+ transition: all 0.15s;
+}
+
+.memory-select-cb:hover,
+.memory-bulk-check-all input:hover {
+ border-color: var(--red);
+}
+
+.memory-select-cb:checked,
+.memory-bulk-check-all input:checked {
+ background: var(--red);
+ border-color: var(--red);
+}
+
+.memory-count {
+ font-size: 11px;
+ color: var(--color-muted);
+}
+
+.memory-search-input {
+ height: 24px;
+ margin-top: 6px;
+ padding: 0 8px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.memory-search-input:focus {
+ outline: none;
+ border-color: var(--red);
+}
+
+.memory-list {
+ flex: 1;
+ min-height: 0; /* Required so flex:1 inside a flex parent can shrink rather than push its parent past 85vh */
+ overflow-y: auto;
+ overflow-x: hidden; /* Stop the synapse sweep from triggering a sideways scrollbar */
+ overscroll-behavior: contain;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.memory-item.task-paused {
+ opacity: 0.45 !important;
+ filter: saturate(0.55);
+ background: repeating-linear-gradient(
+ 45deg,
+ color-mix(in srgb, var(--fg) 2%, transparent),
+ color-mix(in srgb, var(--fg) 2%, transparent) 8px,
+ color-mix(in srgb, var(--fg) 5%, transparent) 8px,
+ color-mix(in srgb, var(--fg) 5%, transparent) 16px
+ ) !important;
+}
+.task-status-badge {
+ appearance: none;
+ -webkit-appearance: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ margin: 0;
+ height: 20px;
+ min-height: 20px;
+ padding: 0 8px;
+ border-radius: 999px;
+ flex-shrink: 0;
+ cursor: pointer;
+ border: 1px solid transparent;
+ line-height: 1;
+ transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease, filter 0.12s ease;
+ user-select: none;
+}
+.task-state-badge > svg,
+.task-state-badge > .task-state-label {
+ pointer-events: none;
+}
+.task-state-label {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ white-space: nowrap;
+}
+.task-state-label-mobile {
+ display: none;
+}
+.task-paused-badge {
+ color: var(--orange, #ffb86c);
+ background: color-mix(in srgb, var(--orange, #ffb86c) 22%, transparent);
+ border-color: color-mix(in srgb, var(--orange, #ffb86c) 35%, transparent);
+}
+.task-active-badge {
+ color: var(--green, #50fa7b);
+ background: color-mix(in srgb, var(--green, #50fa7b) 20%, transparent);
+ border-color: color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
+}
+.task-run-now-badge {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 34%, transparent);
+}
+.task-card-run-btn {
+ appearance: none;
+ height: 20px;
+ min-height: 0;
+ box-sizing: border-box;
+ position: relative;
+ top: -6px;
+}
+.task-state-badge svg {
+ position: relative;
+ top: 0;
+}
+.task-status-badge:hover {
+ filter: brightness(1.08) saturate(1.15);
+}
+.task-paused-badge:hover {
+ background: color-mix(in srgb, var(--orange, #ffb86c) 30%, transparent);
+ border-color: color-mix(in srgb, var(--orange, #ffb86c) 55%, transparent);
+}
+.task-active-badge:hover {
+ background: color-mix(in srgb, var(--green, #50fa7b) 28%, transparent);
+ border-color: color-mix(in srgb, var(--green, #50fa7b) 55%, transparent);
+}
+.task-run-now-badge:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 24%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 52%, transparent);
+}
+
+.task-builtin-badge {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.4px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
+ padding: 1px 6px;
+ border-radius: 8px;
+ flex-shrink: 0;
+ white-space: nowrap;
+}
+.task-builtin-badge.modified {
+ color: var(--orange, #ff9800);
+ border-color: color-mix(in srgb, var(--orange, #ff9800) 40%, transparent);
+ background: color-mix(in srgb, var(--orange, #ff9800) 12%, transparent);
+}
+.memory-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ max-height: 200px;
+ flex-shrink: 0; /* memory-list is a flex column; without this, items get squeezed to fit */
+ transition: all 0.15s;
+}
+
+.memory-item-title {
+ font-size: 12px;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.memory-item:hover,
+.doclib-card:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 16%, transparent);
+}
+/* Synapse pulse — a brief horizontal light sweep runs left → right across
+ each memory like a signal traversing a neural pathway. Per-item stagger
+ via nth-child + varied durations keeps the list shimmering rather than
+ pulsing in sync. */
+#memory-list .memory-item {
+ position: relative;
+ overflow: hidden;
+}
+#memory-list .memory-item::after {
+ /* Sweep highlight rides the border ring only — gradient-fill + mask cutout
+ keeps the bright pulse on the 1px stroke instead of washing the body. */
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: inherit;
+ padding: 1px;
+ pointer-events: none;
+ background: linear-gradient(
+ to right,
+ transparent 0%,
+ transparent calc(var(--sweep, -20%) - 8%),
+ color-mix(in srgb, var(--red) 85%, transparent) var(--sweep, -20%),
+ transparent calc(var(--sweep, -20%) + 8%),
+ transparent 100%
+ );
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ animation: memory-synapse-sweep 6.2s linear infinite;
+ animation-delay: 0.4s;
+}
+#memory-list .memory-item:nth-child(2n)::after { animation-duration: 7.4s; animation-delay: 1.6s; }
+#memory-list .memory-item:nth-child(3n)::after { animation-duration: 8.8s; animation-delay: 3.2s; }
+#memory-list .memory-item:nth-child(5n)::after { animation-duration: 9.3s; animation-delay: 4.7s; }
+#memory-list .memory-item:nth-child(7n)::after { animation-duration: 5.5s; animation-delay: 2.3s; }
+#memory-list .memory-item:hover::after { animation: none; opacity: 0; }
+@property --sweep {
+ syntax: '';
+ inherits: false;
+ initial-value: -20%;
+}
+@media (prefers-reduced-motion: reduce) {
+ #memory-list .memory-item::after { animation: none; opacity: 0; }
+}
+.memory-pinned:hover {
+ background: color-mix(in srgb, var(--red) 4%, transparent);
+ border-color: var(--border);
+ border-left-color: var(--red);
+}
+
+.memory-item-content {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.memory-item-text {
+ font-size: 11px;
+ line-height: 1.5;
+ word-break: break-word;
+ color: var(--fg);
+}
+
+.memory-item-edit-input {
+ flex: 1;
+ padding: 3px 5px;
+ border-radius: 4px;
+ border: 1px solid var(--red);
+ background: var(--bg);
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ min-width: 0;
+}
+
+
+/* Edit row: text input + category select side by side */
+.memory-edit-row {
+ display: flex;
+ gap: 4px;
+ flex: 1;
+ min-width: 0;
+}
+
+.memory-edit-cat-select {
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--red);
+ border-radius: 4px;
+ font-family: inherit;
+ font-size: 9px;
+ padding: 2px 8px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+
+/* The Add-memory category select uses the same semantic control as inline
+ editing, but needs the full-size add-row dimensions. */
+.memory-add-row .memory-edit-cat-select {
+ height: 28px;
+ min-width: 92px;
+ box-sizing: border-box;
+ border-color: var(--border);
+ border-radius: 6px;
+ padding: 2px 8px;
+ font-size: 12px;
+}
+
+
+.memory-item-editing {
+ border-color: color-mix(in srgb, var(--red) 40%, transparent);
+ background: color-mix(in srgb, var(--red) 3%, transparent);
+}
+
+.memory-menu-btn {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--color-muted);
+ font-size: 18px;
+ width: 24px;
+ height: 24px;
+ line-height: 24px;
+ padding: 0;
+ border-radius: 6px;
+ cursor: pointer;
+ flex-shrink: 0;
+ opacity: 0;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.memory-item:hover .memory-menu-btn {
+ opacity: 1;
+}
+
+.memory-menu-btn:hover {
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ border-color: var(--border);
+ color: var(--fg);
+}
+
+.memory-item-dropdown {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 4px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ min-width: auto;
+ width: max-content;
+}
+
+.memory-item-dropdown .dropdown-item-compact {
+ padding: 6px 8px;
+ font-size: 11px;
+ border-radius: 8px;
+ white-space: nowrap;
+}
+
+.memory-item-dropdown .dropdown-item-compact:hover {
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+}
+
+.memory-item-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+ margin-left: auto;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.memory-item:hover .memory-item-actions {
+ opacity: 1;
+}
+
+/* Skill rows show actions at a dim opacity by default so view/run/delete are
+ always discoverable, then brighten on hover. */
+.memory-item.skill-row .memory-item-actions {
+ opacity: 0.35;
+ position: relative;
+ top: -1px;
+}
+.memory-item.skill-row:hover .memory-item-actions {
+ opacity: 1;
+}
+
+.memory-item-btn {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--color-muted);
+ font-size: 11px;
+ height: 22px;
+ padding: 0 6px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.15s;
+ display: flex;
+ align-items: center;
+}
+@media (max-width: 768px) {
+ /* Nudge the ••• menu button up on mobile so it visually aligns with the
+ title row rather than sitting a hair below it. */
+ .memory-item-actions .memory-item-btn { transform: translateY(-3px); }
+ /* Email rows sit on a slightly different baseline (extra meta row /
+ nav-arrows cluster), so pull the menu button back down 1px. */
+ #email-lib-modal .memory-item-actions .memory-item-btn { transform: translateY(-2px); }
+ /* Base rule above hides .memory-item-actions until hover. Mobile has no
+ hover → the ⋮ button in cookbook serve / library cards was invisible
+ and effectively unclickable. Force-show on mobile. */
+ .memory-item-actions { opacity: 0.7 !important; }
+ .memory-item-actions .memory-item-btn {
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
+ }
+}
+
+/* Research-preview sub-sections — used by the research-tab expand pattern. */
+.doclib-research-section-label {
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ opacity: 0.55;
+ margin: 8px 0 4px;
+}
+.doclib-research-sources ol {
+ margin: 0;
+ padding-left: 18px;
+ font-size: 11px;
+ line-height: 1.5;
+}
+.doclib-research-sources a {
+ color: var(--accent, var(--red));
+ text-decoration: none;
+}
+.doclib-research-sources a:hover { text-decoration: underline; }
+.doclib-research-summary {
+ font-size: 11px;
+ line-height: 1.5;
+}
+.doclib-research-summary p { margin: 4px 0; }
+
+.memory-item-btn:hover {
+ color: var(--fg);
+ border-color: var(--border);
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+
+.memory-item-btn.delete:hover {
+ color: var(--color-error);
+ border-color: var(--color-error);
+}
+
+.memory-item-btn.save {
+ color: var(--red);
+}
+
+.memory-item-btn.save:hover {
+ border-color: var(--red);
+}
+
+.memory-item-btn.pin {
+ padding: 1px 4px;
+ opacity: 0.4;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.memory-item-btn.pin.active {
+ opacity: 1;
+}
+.memory-pin-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--fg);
+ opacity: 0.5;
+ transition: all 0.15s;
+}
+.memory-item-btn.pin.active .memory-pin-dot {
+ background: var(--red);
+ opacity: 1;
+}
+.memory-pinned {
+ border-left: 3px solid var(--red);
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--red) 4%, transparent);
+}
+.memory-pinned .memory-item-actions {
+ opacity: 1;
+}
+.memory-pinned .memory-item-actions .memory-item-btn:not(.pin) {
+ opacity: 0;
+}
+.memory-pinned:hover .memory-item-actions .memory-item-btn:not(.pin) {
+ opacity: 1;
+}
+
+/* Category filter chips */
+.memory-category-filters {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.memory-cat-chip {
+ background: none;
+ border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ font-size: 10px;
+ height: 22px;
+ padding: 0 8px;
+ display: inline-flex;
+ align-items: center;
+ border-radius: 10px;
+ cursor: pointer;
+ font-family: inherit;
+ transition: all 0.15s;
+ text-transform: lowercase;
+}
+
+.memory-cat-chip:hover {
+ border-color: var(--red);
+ color: var(--red);
+}
+
+.memory-cat-chip.active {
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ border-color: color-mix(in srgb, var(--red) 40%, transparent);
+ color: var(--red);
+}
+
+@media (max-width: 768px) {
+ .memory-category-filters:has(.memory-cat-chip),
+ .gallery-tag-chips,
+ .gallery-album-chips,
+ .gallery-ai-tags {
+ flex-wrap: nowrap !important;
+ overflow-x: auto;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ touch-action: pan-x;
+ }
+ .memory-category-filters:has(.memory-cat-chip)::-webkit-scrollbar,
+ .gallery-tag-chips::-webkit-scrollbar,
+ .gallery-album-chips::-webkit-scrollbar,
+ .gallery-ai-tags::-webkit-scrollbar {
+ display: none;
+ }
+ .memory-category-filters:has(.memory-cat-chip) .memory-cat-chip,
+ .gallery-tag-chips .gallery-chip,
+ .gallery-album-chips .gallery-chip,
+ .gallery-ai-tags .gallery-ai-chip {
+ flex: 0 0 auto;
+ }
+}
+
+/* Sort select — styled to match the .email-filter-btn look:
+ appearance reset + custom caret so the native chevron doesn't
+ collide with our icon/border treatment. */
+.memory-sort-select {
+ position: relative;
+ top: 3px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: inherit;
+ font-size: 11px;
+ height: 24px;
+ padding: 0 22px 0 8px;
+ cursor: pointer;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-image: url("data:image/svg+xml;utf8, ");
+ background-repeat: no-repeat;
+ background-position: right 7px center;
+ background-size: 10px 10px;
+ transition: border-color 0.12s, background 0.12s;
+}
+.memory-sort-select:hover {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
+}
+.memory-sort-select:focus {
+ outline: none;
+ border-color: var(--red);
+}
+
+/* Memory sort wrapper: positions the SVG icon over the left
+ edge of the native , mirroring the email filter btn. */
+.memory-sort-wrap {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ left: -2px;
+}
+.memory-sort-wrap .memory-sort-select {
+ padding-left: 26px;
+}
+.memory-sort-icon {
+ position: absolute;
+ left: 7px;
+ top: calc(50% + 1px);
+ transform: translateY(-50%);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ color: var(--accent, var(--red));
+ pointer-events: none;
+ z-index: 1;
+}
+.memory-sort-icon svg { width: 13px; height: 13px; }
+
+.email-folder-picker {
+ position: relative;
+ top: 0;
+}
+.email-folder-picker-icon {
+ position: absolute;
+ left: 8px;
+ top: calc(50% + 3px);
+ transform: translateY(-50%);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ color: var(--accent, var(--red));
+ pointer-events: none;
+ z-index: 1;
+}
+.email-folder-picker-icon svg {
+ width: 13px;
+ height: 13px;
+}
+
+/* Custom email filter picker (All / Unread / Favorites / …). Replaces the
+ native so options can carry SVG icons. The hidden source
+ is the value store and dispatches
+ 'change' on selection. */
+.email-filter-picker {
+ position: relative;
+ top: 3px;
+}
+/* Desktop nudge — sits 2px higher than the mobile default so it aligns
+ with the neighbouring memory-sort-select baseline. */
+@media (min-width: 769px) {
+ .email-filter-picker { top: 1px; }
+}
+.email-filter-btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: inherit;
+ font-size: 11px;
+ height: 24px;
+ padding: 0 8px 0 8px;
+ cursor: pointer;
+ text-align: left;
+ transition: border-color 0.12s, background 0.12s;
+ position: relative;
+ top: -4px;
+}
+.email-filter-btn:hover { border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border)); }
+.email-filter-btn:focus-visible {
+ outline: none;
+ border-color: var(--accent, var(--red));
+}
+.email-filter-current {
+ flex: 1;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-filter-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 13px;
+ height: 13px;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.email-filter-icon svg { width: 13px; height: 13px; }
+.email-filter-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-filter-caret {
+ flex-shrink: 0;
+ opacity: 0.6;
+ transition: transform 0.15s;
+}
+.email-filter-picker .email-filter-btn[aria-expanded="true"] .email-filter-caret {
+ transform: rotate(180deg);
+}
+
+.email-filter-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: auto;
+ z-index: 100;
+ width: max(100%, 190px);
+ max-height: 320px;
+ overflow-y: auto;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+.email-filter-menu[hidden] { display: none; }
+.email-filter-menu-title {
+ padding: 5px 9px 4px;
+ color: var(--fg-muted, var(--fg));
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.65;
+ white-space: nowrap;
+}
+@media (max-width: 768px) {
+ .email-filter-menu {
+ width: min(210px, calc(100vw - 24px));
+ }
+ .email-folder-menu {
+ width: min(230px, calc(100vw - 24px));
+ }
+}
+.email-filter-group {
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.6px;
+ text-transform: uppercase;
+ opacity: 0.55;
+ padding: 8px 9px 3px;
+}
+.email-filter-item {
+ all: unset;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 6px 9px;
+ border-radius: 5px;
+ font-size: 12px;
+ cursor: pointer;
+ color: var(--fg);
+ box-sizing: border-box;
+}
+.email-filter-item:hover,
+.email-filter-item:focus-visible {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.email-filter-item-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.email-filter-item-icon svg { width: 13px; height: 13px; }
+.email-filter-item-label {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Item metadata row */
+.memory-item-meta {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+/* Category badge on each item */
+.memory-cat-badge {
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--accent, var(--red));
+ text-transform: lowercase;
+}
+
+.memory-cat-identity {
+ background: color-mix(in srgb, var(--hl-function) 15%, transparent);
+ color: var(--hl-function);
+}
+.memory-cat-preference {
+ background: color-mix(in srgb, var(--hl-keyword) 15%, transparent);
+ color: var(--hl-keyword);
+}
+.memory-cat-contact {
+ background: color-mix(in srgb, #98c379 15%, transparent);
+ color: #98c379;
+}
+.memory-cat-project {
+ background: color-mix(in srgb, var(--hl-string) 15%, transparent);
+ color: var(--hl-string);
+}
+.memory-cat-goal {
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ color: var(--red);
+}
+.memory-cat-task {
+ background: color-mix(in srgb, #d19a66 15%, transparent);
+ color: #d19a66;
+}
+.memory-cat-pinned {
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ color: var(--red);
+}
+
+/* Source and time metadata */
+.memory-item-source,
+.memory-item-time,
+.memory-item-uses {
+ font-size: 9px;
+ color: color-mix(in srgb, var(--fg) 35%, transparent);
+}
+.memory-item-uses {
+ font-family: monospace;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+}
+
+
+/* Empty state */
+.memory-empty {
+ padding: 24px 16px;
+ color: color-mix(in srgb, var(--fg) 35%, transparent);
+ text-align: center;
+ font-size: 11px;
+ font-style: italic;
+}
+
+/* Suggestions area */
+.memory-suggestions {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ /* Bound the import-review list to the modal like the sibling .memory-list,
+ so a long list scrolls internally instead of overflowing the
+ overflow:hidden .admin-card — which clipped lower entries and their
+ save/discard controls with no usable scroll area. */
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ /* Small gutter so the scrollbar doesn't sit flush against the item cards. */
+ padding-right: 4px;
+}
+
+.memory-suggestions.hidden {
+ display: none;
+}
+
+.memory-suggestions-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ padding-bottom: 4px;
+ border-bottom: 1px solid var(--border);
+ /* Pin the title + save all/back controls to the top of the scrolling
+ review list so they stay reachable while the items scroll under them.
+ Opaque background masks items passing beneath. */
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background: var(--panel);
+}
+.memory-suggestions-actions,
+.memory-suggestion-actions {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ flex-shrink: 0;
+}
diff --git a/static/css/05-documents.css b/static/css/05-documents.css
new file mode 100644
index 000000000..2c79ef37f
--- /dev/null
+++ b/static/css/05-documents.css
@@ -0,0 +1,3199 @@
+/* 05-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.
+ */
+.memory-suggestions-actions {
+ position: relative;
+ left: -4px;
+}
+.memory-suggestions-actions .memory-item-btn,
+.memory-suggestion-actions .memory-item-btn {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: color-mix(in srgb, var(--border) 85%, transparent);
+}
+.memory-suggestions-actions .memory-item-btn.save,
+.memory-suggestion-actions .memory-item-btn.save {
+ background: color-mix(in srgb, var(--red) 9%, transparent);
+ border-color: color-mix(in srgb, var(--red) 28%, transparent);
+}
+
+.memory-suggestion-item {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 6px;
+ padding: 5px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--red) 3%, transparent);
+}
+
+/* Tidy animations */
+.memory-tidy-editing {
+ border-color: color-mix(in srgb, var(--hl-function) 40%, transparent);
+ background: color-mix(in srgb, var(--hl-function) 6%, transparent);
+ transition: all 0.3s;
+}
+
+.memory-tidy-text-old {
+ opacity: 0.4;
+ text-decoration: line-through;
+ transition: opacity 0.25s;
+}
+
+.memory-tidy-text-new {
+ color: var(--hl-function);
+ transition: color 0.4s;
+}
+
+.memory-tidy-removing {
+ text-decoration: line-through;
+ opacity: 0;
+ max-height: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+ border-color: transparent;
+ overflow: hidden;
+ transition: opacity 0.3s, max-height 0.4s 0.1s, padding 0.4s 0.1s, border-color 0.3s;
+}
+
+/* ===== DOCUMENT EDITOR (ARTIFACTS) PANEL ===== */
+
+/* Doc editor is a body-level sibling of chat-container */
+
+/* Divider between chat and doc editor */
+.doc-divider {
+ width: 1px;
+ flex-shrink: 0;
+ background: color-mix(in srgb, var(--fg) 11%, transparent);
+ cursor: col-resize;
+ transition: background 0.15s;
+ position: relative;
+ z-index: 10;
+}
+.doc-divider::before {
+ content: '';
+ position: absolute;
+ top: 0; bottom: 0;
+ left: -10px;
+ width: 21px;
+ cursor: col-resize;
+}
+.doc-divider:hover {
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+}
+/* Always-visible "›" handle on the drag divider — clickable to collapse the
+ panel, and signals the divider is interactive. */
+.doc-divider-collapse {
+ position: absolute;
+ top: 50%;
+ left: 2px;
+ transform: translateY(-50%);
+ width: 20px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ background: var(--panel);
+ color: var(--fg);
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 1;
+ opacity: 0.6;
+ cursor: pointer;
+ transition: opacity 0.1s, border-color 0.1s, left 0.08s ease;
+ z-index: 12;
+}
+.doc-divider.doc-divider-handle-idle:hover .doc-divider-collapse,
+.doc-divider.doc-divider-handle-idle:focus-within .doc-divider-collapse {
+ opacity: 0.92;
+ pointer-events: auto;
+}
+/* When in fullscreen mode (cursor outside the doc), the flap itself slides
+ to the OUTSIDE edge of the divider — visually belonging to the chat side
+ the cursor is on, not the doc. The left/right shift pairs with the glyph
+ rotation for a single coordinated transition. */
+.doc-divider-collapse[data-mode="fullscreen"] {
+ left: -22px;
+}
+.doc-divider-collapse:hover { opacity: 1 !important; border-color: var(--accent, var(--red)); }
+/* The same `›` glyph is in the markup; CSS rotates 180° for the left-pointing
+ (fullscreen) state. Smooth transition pairs with the chevron's slide. */
+.doc-divider-collapse > span {
+ display: inline-block;
+ transition: transform 0.08s ease, opacity 0.08s ease;
+}
+.doc-divider-collapse[data-mode="fullscreen"] > span {
+ transform: rotate(180deg);
+}
+/* Secondary "hide panel" X button in the divider — invisible until the pane
+ is fullscreen, then floats just below the unfullscreen chevron so the user
+ has a one-tap escape that minimises the pane instead of just exiting
+ fullscreen. */
+.doc-divider-hide {
+ position: absolute;
+ top: 50%;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ margin-top: 22px;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--panel);
+ color: var(--accent, var(--red));
+ cursor: pointer;
+ padding: 0;
+ z-index: 12;
+ opacity: 0.85;
+ transition: opacity 0.15s, border-color 0.15s;
+}
+.doc-divider-hide:hover {
+ opacity: 1;
+ border-color: var(--accent, var(--red));
+}
+
+/* Copy / Export split button — main click copies, the caret opens the export menu. */
+.doc-split-btn {
+ display: inline-flex;
+ align-items: stretch;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ overflow: hidden;
+ height: 22px;
+ flex-shrink: 0;
+}
+.doc-split-btn .doc-split-main,
+.doc-split-btn .doc-split-caret {
+ border: none !important;
+ border-radius: 0 !important;
+ height: 100% !important;
+ min-height: 0 !important;
+ /* Keep the element fully opaque so the divider line stays crisp; dim the
+ glyph via colour instead (the base .doc-action-icon-btn fades the whole
+ element to 0.3, which also hides the divider). */
+ opacity: 1 !important;
+ color: color-mix(in srgb, var(--fg) 55%, transparent) !important;
+ background: none !important;
+ transition: color 0.12s, background 0.12s;
+}
+.doc-split-btn:hover .doc-split-main,
+.doc-split-btn:hover .doc-split-caret { color: var(--fg) !important; }
+.doc-split-btn .doc-split-caret {
+ border-left: 1px solid var(--border) !important;
+ padding: 0 5px 0 7px !important;
+}
+.doc-split-btn .doc-split-main:hover,
+.doc-split-btn .doc-split-caret:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent) !important;
+ color: var(--fg) !important;
+}
+
+/* Editor pane — body-level flex sibling */
+.doc-editor-pane {
+ flex: 1;
+ min-width: 0;
+ max-width: 70vw;
+ container-type: inline-size;
+ container-name: docpane;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg);
+ border-left: 1px solid var(--border);
+ box-shadow: -10px 0 22px rgba(0, 0, 0, 0.16);
+ overflow: hidden;
+ height: 100%;
+ position: relative;
+ z-index: 1;
+ color-scheme: dark;
+ /* Desktop entrance is driven by openPanel() so the pane has one transform
+ owner. The mobile sheet has its own animation below. */
+ animation: none;
+ transform-origin: right center;
+ will-change: clip-path, opacity;
+}
+@media (prefers-reduced-motion: reduce) {
+ .doc-editor-pane,
+ .doc-editor-pane.doc-pane-leaving { animation: none; }
+}
+.doc-loading-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg);
+}
+
+/* ---- Tab bar ---- */
+.doc-tab-bar {
+ display: flex;
+ align-items: stretch;
+ background: var(--bg);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ height: 36px;
+}
+.doc-tab-scroll {
+ display: flex;
+ align-items: stretch;
+ flex: 1;
+ min-width: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ justify-content: flex-start;
+ /* Fade tabs into the bar's background at the edges (next to the scroll
+ arrows) so an overflowing tab dissolves instead of being hard-cut. The
+ fade is conditional — when we're at an edge there's nothing to fade to,
+ so the mask gradient becomes flat on that side (no shadow). */
+ -webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
+ mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
+}
+.doc-tab-arrow {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.55;
+ cursor: pointer;
+ font-size: 18px;
+ padding: 0 6px;
+ flex-shrink: 0;
+ transition: opacity 0.15s;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ position: relative;
+ top: -2px;
+}
+.doc-tab-arrow:hover {
+ opacity: 1;
+}
+/* Mobile swipe-down grab handle at the top of the doc sheet. */
+.doc-mobile-grabber { display: none; }
+@media (max-width: 768px) {
+ body.doc-view .doc-mobile-grabber {
+ display: block;
+ flex-shrink: 0;
+ height: 18px;
+ position: relative;
+ background: transparent;
+ background-color: transparent;
+ background-image: none;
+ touch-action: none;
+ cursor: grab;
+ }
+ body.doc-view .doc-mobile-grabber::before {
+ content: '';
+ position: absolute;
+ top: 7px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 36px;
+ height: 4px;
+ background: var(--fg);
+ opacity: 0.25;
+ border-radius: 2px;
+ }
+}
+
+.doc-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 0 10px;
+ font-family: inherit;
+ font-size: 11px;
+ color: var(--fg);
+ opacity: 0.4;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: opacity 0.1s, background 0.1s;
+ flex-shrink: 0;
+ border-right: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
+ position: relative;
+}
+.doc-tab:hover {
+ opacity: 0.7;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.doc-tab.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border-radius: 7px 7px 0 0;
+}
+.doc-tab-title {
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.doc-tab-title-input {
+ background: transparent;
+ border: 1px solid var(--fg);
+ border-radius: 2px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ padding: 0 4px;
+ height: 18px;
+ width: 120px;
+ max-width: 180px;
+ outline: none;
+}
+.doc-tab-lang {
+ font-size: 9px;
+ opacity: 0.5;
+}
+.doc-tab-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ color: var(--fg);
+ opacity: 0.4;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ box-sizing: border-box;
+ margin-left: 3px;
+ flex-shrink: 0;
+ align-self: center;
+ transition: opacity 0.12s, color 0.12s, background 0.12s, border-color 0.12s;
+}
+.doc-tab-close:hover {
+ opacity: 1;
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+/* Mobile-only footer (Close + Copy); hidden on desktop and in email mode. */
+.doc-mobile-footer { display: none; }
+.doc-tab-new {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.25;
+ cursor: pointer;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 0 10px;
+ transition: opacity 0.1s;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+.doc-tab-new:hover {
+ opacity: 0.7;
+}
+.doc-tab-play {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.55;
+ cursor: pointer;
+ padding: 0 2px;
+ font-size: 10px;
+ line-height: 1;
+ transition: opacity 0.1s, color 0.1s;
+ flex-shrink: 0;
+}
+.doc-tab-play:hover {
+ opacity: 1;
+ color: var(--green, #4ec970);
+}
+.doc-tab-play.active {
+ opacity: 1;
+ color: var(--green, #4ec970);
+}
+.doc-tab.dragging {
+ opacity: 0.3;
+}
+.doc-tab.drag-over {
+ border-left: 2px solid var(--fg);
+}
+
+/* ---- HTML preview iframe ---- */
+.doc-html-preview {
+ flex: 1;
+ width: 100%;
+ border: none;
+ background: #fff;
+}
+
+/* ---- Editor header ---- */
+.doc-editor-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 10px;
+ /* Moved to the bottom as a footer: flex order pushes it last in the pane
+ column, and the divider flips to the top edge. */
+ order: 99;
+ border-top: 1px solid var(--border);
+ flex-shrink: 0;
+ flex-wrap: nowrap;
+ min-height: 36px;
+ background: var(--bg);
+}
+/* Version now lives in the document tab, not the footer. */
+#doc-version-badge { display: none !important; }
+/* Language icon chip on doc tabs — sits between the version pill and title.
+ Hidden when empty so docs without a language don't get awkward spacing. */
+.doc-tab-lang {
+ display: inline-flex; align-items: center;
+ flex-shrink: 0;
+ align-self: center;
+}
+.doc-tab-lang:empty { display: none; }
+.doc-tab-lang svg { display: block; }
+
+.doc-tab-version {
+ font-size: 9px;
+ font-weight: 600;
+ padding: 1px 6px;
+ cursor: pointer;
+ flex-shrink: 0;
+ align-self: center;
+ line-height: 1.4;
+ /* Sits to the LEFT of the title now — space it off the title text. */
+ margin-right: 6px;
+ /* Accent pill so it's obvious the version is a clickable control. */
+ color: var(--accent-primary, var(--red));
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 45%, transparent);
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
+ border-radius: 9px;
+}
+.doc-tab-version:hover {
+ border-color: var(--accent-primary, var(--red));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 22%, transparent);
+}
+.doc-close-btn {
+ order: -1;
+ opacity: 0.5;
+ flex-shrink: 0;
+}
+.doc-close-btn:hover {
+ opacity: 1;
+}
+
+.doc-editor-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ justify-content: flex-end;
+}
+.doc-left .doc-editor-actions {
+ margin-left: 0;
+}
+
+.doc-version-badge {
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--red) 55%, transparent);
+ color: var(--red);
+ padding: 1px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1.4;
+ cursor: pointer;
+ user-select: none;
+ transition: background 0.1s, border-color 0.1s;
+ opacity: 0.9;
+}
+.doc-version-badge:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--red) 20%, transparent);
+ border-color: var(--red);
+}
+
+.doc-stream-indicator {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ color: var(--accent);
+ opacity: 0.9;
+ white-space: nowrap;
+ animation: doc-stream-pulse 1.5s ease-in-out infinite;
+}
+.doc-stream-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--accent);
+}
+
+/* In the doc footer the type picker sits next to the accent Copy/Export split —
+ match its 28px height and 6px radius so the right-hand controls line up. */
+.doc-actions-footer #doc-language-select {
+ height: 28px;
+ border-radius: 6px;
+ font-size: 11px;
+ top: 0;
+}
+/* Lang-type icon shown to the LEFT of the language select. Browsers won't
+ render SVG inside , so this surface the current selection's icon
+ externally. */
+#doc-language-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px; height: 18px;
+ color: var(--accent-primary, var(--red));
+ flex-shrink: 0;
+}
+#doc-language-icon:empty { display: none; }
+#doc-language-icon svg { display: block; }
+
+/* Visually hidden but available to assistive tech (screen readers, axe).
+ Use for content that should be announced/structural but not painted —
+ e.g. the persistent page . */
+.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
+ — s can't render SVG). Hidden select stays as the source of truth. */
+.doc-langpicker-native-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;
+ border: 0 !important;
+}
+.doc-langpicker-trigger {
+ display: inline-flex; align-items: center; gap: 6px;
+ height: 28px; padding: 0 8px 0 10px;
+ background: var(--bg); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px;
+ font-size: 11px; cursor: pointer;
+ transition: border-color 0.12s, background 0.12s;
+}
+.doc-langpicker-trigger:hover {
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 60%, var(--border));
+}
+.doc-langpicker-trigger.doc-langpicker-email-compact .doc-langpicker-label,
+.doc-langpicker-trigger.doc-langpicker-email-compact > svg:last-child {
+ display: none;
+}
+.doc-langpicker-trigger svg { display: block; flex-shrink: 0; }
+.doc-langpicker-label {
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ max-width: 90px;
+}
+.doc-langpicker-ico-blank {
+ display: inline-block; width: 14px; height: 14px; flex-shrink: 0;
+}
+.doc-langpicker-menu {
+ background: var(--bg);
+ border: 1px solid var(--border); border-radius: 8px;
+ padding: 4px;
+ box-shadow: 0 10px 28px rgba(0,0,0,0.32);
+ max-height: 60vh; overflow-y: auto;
+ z-index: 10000;
+ min-width: 160px;
+}
+.doc-langpicker-item {
+ display: flex; align-items: center; gap: 8px;
+ width: 100%;
+ padding: 6px 10px;
+ background: none; color: var(--fg);
+ border: none; border-radius: 5px;
+ font-size: 12px; text-align: left;
+ cursor: pointer;
+}
+.doc-langpicker-item:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.doc-langpicker-group {
+ padding: 6px 10px 3px;
+ color: var(--fg-muted, var(--fg));
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ opacity: 0.55;
+}
+.doc-langpicker-divider {
+ height: 1px;
+ margin: 4px 6px;
+ background: var(--border);
+ opacity: 0.65;
+}
+.doc-langpicker-item.is-selected {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 14%, transparent);
+ color: var(--accent-primary, var(--red));
+}
+.doc-langpicker-item svg { display: block; flex-shrink: 0; }
+.doc-langpicker-item .doc-langpicker-label { max-width: 220px; }
+
+.doc-language-select {
+ background-color: var(--bg);
+ background-image: url("data:image/svg+xml;utf8, ");
+ background-repeat: no-repeat;
+ background-position: right 4px center;
+ color: var(--fg);
+ color-scheme: dark;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ position: relative;
+ top: 0;
+ font-family: inherit;
+ font-size: 10px;
+ padding: 2px 20px 2px 8px;
+ height: 22px;
+ /* Fixed width so the right-anchored chevron doesn't shift when the selected
+ option's text width changes. */
+ width: 96px;
+ text-overflow: ellipsis;
+ opacity: 0.8;
+ cursor: pointer;
+ -moz-appearance: none;
+ appearance: none;
+}
+/* Light theme: tint the chevron with the light foreground instead of cyan. */
+:root.light .doc-language-select {
+ background-image: url("data:image/svg+xml;utf8, ");
+}
+/* New-tab "+" spins on hover, like the library sidebar "+". */
+.doc-tab-new svg { transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
+.doc-tab-new:hover svg { transform: rotate(180deg) scale(1.15); }
+
+.doc-action-btn {
+ background: none;
+ color: var(--fg);
+ border: none;
+ font-family: inherit;
+ font-size: 10px;
+ padding: 2px 8px;
+ height: 22px;
+ cursor: pointer;
+ opacity: 0.45;
+ transition: opacity 0.1s;
+}
+.doc-action-btn:hover {
+ opacity: 1;
+}
+.doc-action-icon-btn {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.3;
+ cursor: pointer;
+ padding: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.1s;
+}
+.doc-action-icon-btn:hover {
+ opacity: 1;
+}
+.doc-action-icon-btn:disabled {
+ opacity: 0.18;
+ cursor: default;
+ pointer-events: none;
+}
+#doc-undo-btn:not(:disabled),
+#doc-redo-btn:not(:disabled) {
+ opacity: 0.68;
+}
+.doc-fontsize-levels {
+ position: absolute;
+ right: 6px;
+ bottom: 3px;
+ display: inline-flex;
+ line-height: 1;
+ pointer-events: none;
+}
+.doc-fontsize-levels i {
+ font-style: normal;
+ font-weight: 700;
+ font-size: 7px;
+ /* Bare --accent is undefined in this codebase, was falling back to the
+ hardcoded blue #4a9eff — use the real theme accent. */
+ color: var(--accent-primary, var(--red));
+ opacity: 1;
+}
+.doc-fontsize-levels i.active { opacity: 1; }
+/* Collapsed buttons hidden in header, shown in overflow menu */
+.doc-collapsible-btn.doc-collapsed { display: none !important; }
+.doc-overflow-toggle:hover { opacity: 1; }
+.doc-overflow-menu {
+ display: none;
+ position: fixed;
+ z-index: 9999;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 4px 0;
+ min-width: 140px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+}
+.doc-overflow-menu.open { display: block; }
+.doc-overflow-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 5px 12px;
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: background 0.1s;
+}
+.doc-overflow-item:hover,
+.doc-overflow-item:focus-visible {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
+ outline: none;
+}
+.doc-overflow-item .rich-align-preview {
+ display: inline-block;
+ white-space: nowrap;
+}
+.doc-overflow-item:disabled {
+ opacity: 0.28;
+ cursor: default;
+}
+.doc-overflow-item.is-current {
+ color: var(--accent-primary, var(--red));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 9%, transparent);
+}
+.md-dd-current {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ margin-left: auto;
+ opacity: 0;
+}
+.doc-overflow-item.is-current .md-dd-current { opacity: 1; }
+.doc-overflow-item.rich-table-danger { color: var(--color-danger, #c0392b); }
+.doc-overflow-item svg { flex-shrink: 0; opacity: 0.5; }
+.doc-ai-writing-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ color: var(--accent-primary, var(--red));
+}
+.doc-ai-writing-btn svg:first-child { color: var(--accent-primary, var(--red)); opacity: 1; }
+.doc-ai-writing-whirlpool { position: relative; top: -4px; }
+.doc-ai-writing-menu { min-width: 206px; }
+.doc-ai-writing-menu-title {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 12px 6px;
+ color: var(--accent-primary, var(--red));
+ font-size: 10px;
+ font-weight: 600;
+ border-bottom: 1px solid var(--border);
+}
+.doc-ai-writing-menu-title svg { opacity: 1; }
+.doc-ai-writing-item-icon {
+ width: 16px;
+ flex: 0 0 16px;
+ text-align: center;
+ color: var(--accent-primary, var(--red));
+ font-size: 11px;
+ font-weight: 600;
+}
+
+/* Markdown Edit/Preview two-icon switch — segmented, styled to match the Copy
+ split button. The active half gets a static highlight (no sliding). */
+.md-view-toggle {
+ display: inline-flex;
+ align-items: center;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ overflow: hidden;
+ flex-shrink: 0;
+ height: 22px;
+}
+.md-view-toggle .md-view-opt {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ min-width: 26px;
+ width: auto;
+ height: 100%;
+ padding: 0 7px !important;
+ border: none !important;
+ border-radius: 0 !important;
+ background: none !important;
+ color: var(--fg);
+ opacity: 0.45;
+ cursor: pointer;
+ transition: opacity 0.12s, color 0.12s, background 0.12s;
+}
+.md-view-label {
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+}
+.md-view-toggle .md-view-opt:hover { opacity: 0.8; }
+.md-view-toggle .md-view-opt.active {
+ opacity: 1;
+ color: var(--fg);
+ background: var(--bg) !important;
+ /* "Punch" pop when a side becomes active (only fires on an actual switch —
+ re-applying .active without a change doesn't restart the animation). */
+ animation: md-view-punch 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+@keyframes md-view-punch {
+ 0% { transform: scale(0.8); }
+ 55% { transform: scale(1.18); }
+ 100% { transform: scale(1); }
+}
+@media (prefers-reduced-motion: reduce) {
+ .md-view-toggle .md-view-opt.active { animation: none; }
+}
+
+.doc-md-toolbar.md-preview-active {
+ border-bottom-color: var(--bg);
+}
+.doc-md-toolbar.md-write-active {
+ border-bottom-color: var(--sidebar-bg, var(--panel));
+}
+.doc-md-toolbar.md-preview-active #doc-md-view-toggle,
+.doc-md-toolbar.md-write-active #doc-md-view-toggle {
+ align-self: flex-end;
+ height: 28px;
+ margin-bottom: -6px;
+ border-radius: 7px 7px 0 0;
+ box-shadow: 0 -1px 0 var(--border), -1px 0 0 var(--border), 1px 0 0 var(--border);
+}
+.doc-md-toolbar.md-preview-active #doc-md-view-toggle {
+ background: var(--bg);
+ border-bottom-color: var(--bg);
+}
+.doc-md-toolbar.md-write-active #doc-md-view-toggle {
+ background: var(--sidebar-bg, var(--panel));
+ border-bottom-color: var(--sidebar-bg, var(--panel));
+}
+.doc-md-toolbar.md-preview-active #doc-md-view-toggle .md-view-opt.active {
+ background: var(--bg) !important;
+}
+.doc-md-toolbar.md-write-active #doc-md-view-toggle .md-view-opt.active {
+ background: var(--bg) !important;
+}
+.doc-md-preview.md-preview-active {
+ background: var(--bg);
+ border-top: 0;
+ margin-top: 0;
+}
+
+/* Write and Preview are separate tabs that sit out from the toolbar. */
+.md-view-toggle {
+ position: relative;
+ gap: 2px;
+ height: 28px;
+ border: 0;
+ border-radius: 0;
+ overflow: visible;
+ background: var(--bg) !important;
+ box-shadow: none !important;
+}
+/* The toolbar owns the boundary beneath the editor. A second full-width
+ baseline here would remain visible directly under the active tab. */
+.md-view-toggle::after { content: none; }
+.md-view-toggle .md-view-opt {
+ height: 28px;
+ border: 1px solid var(--border) !important;
+ border-bottom-color: transparent !important;
+ border-radius: 7px 7px 0 0 !important;
+ position: relative;
+ z-index: 2;
+}
+.md-view-toggle .md-view-opt.active {
+ z-index: 3;
+}
+.md-view-toggle .md-view-opt:first-child,
+#doc-render-view-toggle .md-view-opt:first-child {
+ background: var(--sidebar-bg, var(--panel)) !important;
+}
+.md-view-toggle .md-view-opt:last-child,
+#doc-render-view-toggle .md-view-opt:last-child {
+ background: var(--sidebar-bg, var(--panel)) !important;
+}
+
+/* Keep code's Edit/Run switch in the same visual language as Write/Preview:
+ Edit is attached to the sidebar-colored editor surface, while Run/Preview
+ is attached to the lighter content surface. */
+#doc-render-view-toggle .md-view-opt {
+ color: var(--fg);
+ border-color: var(--border) !important;
+}
+#doc-render-view-toggle .md-view-opt.active {
+ opacity: 1;
+ color: var(--fg);
+}
+
+/* Code/Run uses the same tab shape and baseline as Write/Preview. */
+.doc-md-toolbar #doc-render-view-toggle {
+ align-self: flex-end;
+ height: 28px;
+ margin-bottom: -6px;
+}
+.doc-md-toolbar #doc-render-view-toggle .md-view-opt:first-child {
+ background: var(--sidebar-bg, var(--panel)) !important;
+}
+.doc-md-toolbar #doc-render-view-toggle .md-view-opt:last-child {
+ background: var(--bg) !important;
+}
+
+/* Mobile: the doc/email footer controls (format toggle, export button, export
+ menu) were slimmer than the Send button, so the row looked ragged. Bump them
+ to match the Send button height (and give the export/overflow menu bigger,
+ touch-friendly rows). */
+@media (max-width: 768px) {
+ .md-view-toggle { height: 28px; }
+ .md-view-toggle .md-view-opt { min-width: 38px; padding: 0 8px !important; }
+ .doc-action-icon-btn { padding: 6px; }
+ .email-send-btn { height: 28px; }
+ /* The type/language picker was the slim one stuck at 22px — match it to the
+ rest of the row. */
+ .doc-language-select { height: 28px; font-size: 13px; padding: 2px 22px 2px 10px; }
+ .doc-overflow-item { font-size: 13px; padding: 9px 14px; }
+ .doc-overflow-item .overflow-icon svg,
+ .doc-overflow-item svg { width: 16px; height: 16px; }
+ .email-more-menu .dropdown-item-compact { padding: 9px 12px; font-size: 13px; }
+
+ /* The doc footer is tight once the run/preview toggle joins it, so on mobile
+ go icon-only for Close, Undo & Copy (their icons are clear).
+ font-size:0 drops the text node next to the SVG (the SVG has fixed w/h, so
+ it's unaffected). Slightly narrower type picker buys a little more room. */
+ #doc-actions-footer #doc-undo-btn span,
+ #doc-actions-footer #doc-footer-close-btn span,
+ #doc-email-actions #doc-email-discard-btn span { display: none; }
+ #doc-actions-footer #doc-undo-btn,
+ #doc-actions-footer #doc-redo-btn,
+ #doc-actions-footer #doc-footer-close-btn,
+ #doc-email-actions #doc-email-discard-btn { gap: 0; padding: 0 9px; }
+ #doc-actions-footer #doc-footer-copy-btn { gap: 0; padding: 0 13px; font-size: 0 !important; }
+ /* In reply mode the button carries a "Reply" label that should stay visible
+ (the icon-only treatment above is only for the plain Copy action). */
+ #doc-actions-footer #doc-footer-copy-btn[data-mode="reply"] { gap: 5px; padding: 0 13px; font-size: 12px; }
+ #doc-actions-footer #doc-language-select { width: 80px; }
+ #doc-actions-footer .doc-stats-btn { width: 28px; padding: 0 4px; }
+ #doc-actions-footer .doc-stats-unit { display: none; }
+ #doc-actions-footer .doc-stats-popover { right: -34px; }
+ #doc-actions-footer .doc-save-button-label { display: none; }
+}
+
+/* Very narrow document panes: keep the primary Save action visible while
+ collapsing secondary labels before allowing the footer to clip. This uses
+ the pane width, not the viewport width, because the editor can be docked. */
+@container docpane (max-width: 420px) {
+ #doc-actions-footer {
+ gap: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ overflow: hidden;
+ }
+ #doc-actions-footer > #doc-undo-btn,
+ #doc-actions-footer > #doc-redo-btn {
+ flex: 0 0 28px;
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+ #doc-actions-footer > #doc-undo-btn span,
+ #doc-actions-footer > #doc-footer-copy-btn,
+ #doc-actions-footer .doc-save-button-label,
+ #doc-actions-footer > #doc-stats .doc-stats-unit {
+ font-size: 0;
+ }
+ #doc-actions-footer > #doc-undo-btn span,
+ #doc-actions-footer .doc-save-button-label,
+ #doc-actions-footer > #doc-stats .doc-stats-unit {
+ display: none;
+ }
+ #doc-actions-footer > #doc-stats {
+ flex: 0 0 28px;
+ }
+ #doc-actions-footer #doc-copy-export-split {
+ flex: 0 0 auto;
+ }
+ #doc-actions-footer #doc-footer-copy-btn {
+ flex: 0 0 36px;
+ width: 36px;
+ min-width: 36px;
+ padding-left: 0;
+ padding-right: 0;
+ font-size: 0 !important;
+ align-items: center;
+ justify-content: center;
+ }
+ #doc-actions-footer #doc-footer-export-btn {
+ flex: 0 0 24px;
+ width: 24px;
+ min-width: 24px;
+ padding-left: 4px;
+ padding-right: 4px;
+ }
+ #doc-actions-footer #doc-copy-export-split {
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 50%, transparent);
+ border-radius: 6px;
+ overflow: hidden;
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 20%, transparent);
+ }
+ #doc-actions-footer #doc-copy-export-split .email-send-btn {
+ border-top: 0;
+ border-bottom: 0;
+ }
+ #doc-actions-footer #doc-copy-export-split #doc-footer-copy-btn {
+ border-left: 0;
+ }
+ #doc-actions-footer #doc-copy-export-split #doc-footer-export-btn {
+ border-right: 0;
+ }
+ #doc-actions-footer #doc-footer-export-btn svg {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+/* Markdown formatting toolbar */
+.doc-md-toolbar [data-md="unlink"] { display: none !important; }
+/* In code-mode the toolbar only hosts the view toggles + utility buttons
+ (font-size, diff). Hide markdown-only formatting controls — bold,
+ italic, headings, list, link, attach, code-dropdown, emoji, hr, and
+ the PDF-only buttons. The view toggles + fontsize + diff stay. */
+.doc-md-toolbar[data-mode="code"] [data-md],
+.doc-md-toolbar[data-mode="code"] .md-dd-toggle,
+.doc-md-toolbar[data-mode="code"] #md-toolbar-attach-btn,
+.doc-md-toolbar[data-mode="code"] #md-toolbar-emoji-slot,
+.doc-md-toolbar[data-mode="code"] .md-toolbar-pdf-only,
+.doc-md-toolbar[data-mode="code"] .md-toolbar-sep {
+ display: none !important;
+}
+.doc-md-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 1px;
+ padding: 2px 8px;
+ border-bottom: 1px solid var(--border);
+ background: var(--sidebar-bg, var(--panel));
+ flex-shrink: 0;
+ overflow: hidden;
+ height: 36px;
+ position: relative;
+ /* Same edge fade as the tab strip — toolbar buttons dissolve into the bar's
+ background at the edges instead of being hard-cut when they overflow. */
+ -webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
+ mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
+}
+.doc-md-toolbar button {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.85;
+ padding: 4px 7px;
+ font-family: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+ line-height: 1.3;
+ transition: opacity 0.1s;
+ min-height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.doc-md-toolbar button:hover {
+ opacity: 1;
+}
+.doc-md-toolbar button:disabled {
+ opacity: 0.28;
+ cursor: default;
+ pointer-events: none;
+}
+.doc-link-remove-btn {
+ color: var(--red, #dc2626);
+ border-color: color-mix(in srgb, var(--red, #dc2626) 45%, var(--border));
+}
+.doc-md-toolbar button:active {
+ opacity: 0.6;
+}
+/* Active formatting state — set by the email WYSIWYG sync. Mirrors the
+ selection's current marks (B/I/S, heading level, list) so the toolbar
+ acts as an indicator, not just a launcher. */
+.doc-md-toolbar button.is-active {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+}
+.doc-md-toolbar button.is-active:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+}
+.doc-md-toolbar button:focus:not(:focus-visible),
+.doc-tab-new:focus:not(:focus-visible),
+.doc-tab-arrow:focus:not(:focus-visible) {
+ outline: none;
+ background: none;
+}
+.doc-md-toolbar button:focus-visible,
+.doc-tab-new:focus-visible,
+.doc-tab-arrow:focus-visible {
+ outline: 1px solid var(--accent-primary, var(--red));
+ outline-offset: -1px;
+}
+/* Grouped formatting dropdown toggles (heading / code / list) */
+.md-dd-toggle { gap: 1px !important; }
+/* Tint only dropdown chevrons accent; the main tool icon keeps toolbar color. */
+.md-dd-toggle svg { opacity: 1; margin-left: 1px; flex-shrink: 0; transform: translateY(0); }
+.md-dd-toggle > svg:last-child { color: var(--accent-primary, var(--red, #4a9eff)); }
+.md-dd-ico {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ flex-shrink: 0;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 11px;
+ opacity: 0.7;
+}
+.md-toolbar-sep {
+ width: 1px;
+ height: 12px;
+ background: color-mix(in srgb, var(--border) 60%, transparent);
+ margin: 0 4px;
+ flex-shrink: 0;
+}
+.md-toolbar-items {
+ display: flex;
+ align-items: center;
+ gap: 1px;
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow-x: auto;
+ scrollbar-width: none;
+ scroll-behavior: auto;
+ -webkit-overflow-scrolling: touch;
+ touch-action: pan-x pan-y;
+}
+.md-toolbar-leading-controls {
+ display: flex;
+ align-items: center;
+ flex: 0 0 auto;
+}
+.md-toolbar-items::-webkit-scrollbar { display: none; }
+/* On a PDF, the annotation tools (text / checkmark / signature) are the primary
+ actions — pull them to the far left of the toolbar. `order` is visual only
+ (they're display:none on non-PDF docs), so it has no effect elsewhere. */
+.md-toolbar-items #doc-pdf-add-text-btn { order: -3; }
+.md-toolbar-items #doc-pdf-add-check-btn { order: -2; }
+.md-toolbar-items #doc-pdf-add-sign-btn { order: -1; }
+@media (min-width: 769px) {
+ .md-toolbar-items #doc-pdf-add-text-btn {
+ margin-left: 14px;
+ }
+}
+/* Stack the signature icon over a tiny "sign" caption so the tool reads
+ clearly (the icon alone is ambiguous). The JS toggles inline display
+ none/'' for PDF mode, so this flex display only applies when shown. */
+#doc-pdf-add-sign-btn {
+ flex-direction: column;
+ align-items: center;
+ gap: 0;
+ line-height: 1;
+}
+.doc-pdf-sign-label {
+ font-size: 6.5px;
+ letter-spacing: 0.3px;
+ margin-top: 0;
+ opacity: 1;
+ color: var(--accent, var(--red));
+}
+/* Edge scroll arrows — appear when the toolbar has more icons off-screen. */
+.md-scroll-arrow {
+ position: static;
+ flex: 0 0 28px;
+ align-self: stretch;
+ width: 28px;
+ display: flex;
+ align-items: center;
+ border: none;
+ cursor: pointer;
+ color: var(--fg);
+ padding: 0 !important;
+ min-height: 0 !important;
+ opacity: 1 !important;
+ z-index: 6;
+}
+.md-scroll-arrow svg { position: relative; top: 2px; opacity: 0.75; transition: opacity 0.1s; }
+.md-scroll-arrow:hover svg { opacity: 1; }
+@media (min-width: 769px) {
+ /* On desktop the arrow rides 2px lower than the toolbar baseline. */
+ .md-scroll-arrow svg { top: 0; }
+}
+.md-scroll-left {
+ justify-content: flex-start;
+ padding-left: 3px;
+ /* Mostly-solid toolbar bg so the icons behind the arrow are hidden, fading
+ to transparent only at the inner edge. */
+ background: linear-gradient(to right, var(--bg) 0%, var(--bg) 80%, transparent 100%);
+}
+.md-scroll-right {
+ justify-content: flex-end;
+ padding-right: 3px;
+ background: linear-gradient(to left, var(--bg) 0%, var(--bg) 80%, transparent 100%);
+}
+.md-toolbar-overflow-wrapper {
+ position: relative;
+ flex-shrink: 0;
+ margin-left: auto;
+}
+.md-toolbar-overflow-toggle {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--fg);
+ opacity: 0.4;
+ padding: 2px 4px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: opacity 0.1s;
+}
+.md-toolbar-overflow-toggle:hover {
+ opacity: 1;
+}
+.md-toolbar-overflow-menu {
+ display: none;
+ position: fixed;
+ z-index: 1000;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 4px;
+ min-width: 0;
+ width: max-content;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ backdrop-filter: blur(12px);
+}
+.md-toolbar-overflow-menu.open {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ max-width: 200px;
+}
+.md-toolbar-overflow-item {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--fg);
+ opacity: 0.5;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-family: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+ line-height: 1.3;
+ transition: opacity 0.1s, background 0.1s;
+}
+.md-toolbar-overflow-item:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: var(--border);
+}
+
+/* Find bar */
+.doc-find-bar {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+ padding: 4px 8px;
+ background: var(--panel, var(--bg));
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.doc-find-row,
+.doc-replace-row {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+}
+.doc-replace-row[hidden] { display: none; }
+.doc-find-input {
+ flex: 1;
+ min-width: 0;
+ padding: 3px 6px;
+ font-size: 12px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ outline: none;
+}
+.doc-find-input:focus { border-color: var(--accent); }
+.doc-find-count {
+ font-size: 11px;
+ opacity: 0.6;
+ white-space: nowrap;
+ min-width: 50px;
+ text-align: center;
+}
+.doc-find-nav, .doc-find-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ color: var(--fg);
+ cursor: pointer;
+ padding: 2px 5px;
+ font-size: 14px;
+ border-radius: 3px;
+ opacity: 0.7;
+}
+.doc-find-nav:hover, .doc-find-close:hover,
+.doc-find-nav.is-active { opacity: 1; background: color-mix(in srgb, var(--fg) 10%, transparent); }
+.doc-find-action {
+ min-height: 25px;
+ padding: 3px 8px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+.doc-find-action:hover,
+.doc-find-action:focus-visible {
+ border-color: var(--accent);
+ outline: none;
+}
+
+/* Live document outline. It is body-portaled so the toolbar's clipping and
+ edge mask cannot cut it off in the mobile full-screen editor. */
+.doc-outline-menu {
+ position: fixed;
+ box-sizing: border-box;
+ max-height: min(420px, calc(100vh - 24px));
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.3);
+ color: var(--fg);
+}
+.doc-outline-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 32px;
+ padding: 0 10px;
+ border-bottom: 1px solid var(--border);
+ font-size: 11px;
+ font-weight: 600;
+}
+.doc-outline-list {
+ overflow-y: auto;
+ padding: 4px;
+ overscroll-behavior: contain;
+}
+.doc-outline-item {
+ width: 100%;
+ min-height: 30px;
+ display: grid;
+ grid-template-columns: 24px minmax(0, 1fr);
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px 4px 7px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ text-align: left;
+ cursor: pointer;
+}
+.doc-outline-item:hover,
+.doc-outline-item:focus-visible {
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ outline: none;
+}
+.doc-outline-level {
+ opacity: 0.42;
+ font-size: 9px;
+ font-variant-numeric: tabular-nums;
+}
+.doc-outline-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.doc-outline-empty {
+ padding: 18px 12px;
+ text-align: center;
+ font-size: 11px;
+ opacity: 0.52;
+}
+
+@media (max-width: 768px) {
+ .doc-outline-menu { max-height: min(52vh, 420px); }
+ .doc-outline-item { min-height: 38px; font-size: 12px; }
+}
+
+/* Selection formatting stays close to the selected text. The controls are
+ symbols only and mirror the same commands as the full Rich Text toolbar. */
+.doc-rich-selection-toolbar {
+ position: fixed;
+ display: flex;
+ align-items: center;
+ gap: 1px;
+ padding: 3px;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.32);
+ color: var(--fg);
+ user-select: none;
+ overflow: visible;
+}
+.doc-rich-selection-toolbar button {
+ width: 30px;
+ height: 28px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ font-size: 12px;
+ cursor: pointer;
+ touch-action: manipulation;
+}
+.doc-rich-selection-toolbar button:nth-child(5) {
+ margin-left: 3px;
+ border-left: 1px solid var(--border);
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+.doc-rich-selection-toolbar button:hover,
+.doc-rich-selection-toolbar button:focus-visible,
+.doc-rich-selection-toolbar button.is-active {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ outline: none;
+}
+.doc-rich-selection-toolbar .doc-rich-selection-close {
+ position: absolute;
+ top: -9px;
+ right: -9px;
+ z-index: 2;
+ width: 18px;
+ height: 18px;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--panel, var(--bg));
+ color: var(--fg);
+ font: 700 15px/1 system-ui, sans-serif;
+ box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35);
+}
+.doc-rich-selection-toolbar .doc-rich-selection-close:hover,
+.doc-rich-selection-toolbar .doc-rich-selection-close:focus-visible {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, var(--panel, var(--bg)));
+ outline: none;
+}
+
+@media (max-width: 768px) {
+ .doc-rich-selection-toolbar { padding: 4px; }
+ .doc-rich-selection-toolbar button { width: 38px; height: 34px; font-size: 13px; }
+ .doc-rich-selection-toolbar .doc-rich-selection-close { width: 20px; height: 20px; top: -10px; right: -10px; }
+}
+
+.doc-rich-slash-menu {
+ position: fixed;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ max-height: min(420px, calc(100vh - 24px));
+ overflow: hidden;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.32);
+ color: var(--fg);
+}
+.doc-rich-slash-header {
+ min-height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 10px;
+ border-bottom: 1px solid var(--border);
+ font-size: 11px;
+ font-weight: 600;
+}
+.doc-rich-slash-list {
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: 4px;
+}
+.doc-rich-slash-item {
+ width: 100%;
+ min-height: 34px;
+ display: grid;
+ grid-template-columns: 30px minmax(0, 1fr);
+ align-items: center;
+ gap: 5px;
+ padding: 3px 8px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ text-align: left;
+ cursor: pointer;
+}
+.doc-rich-slash-item:hover,
+.doc-rich-slash-item:focus-visible,
+.doc-rich-slash-item.is-active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 13%, transparent);
+ outline: none;
+}
+.doc-rich-slash-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 24px;
+ color: var(--accent, var(--red));
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.82;
+}
+.doc-rich-slash-empty {
+ padding: 18px 12px;
+ text-align: center;
+ font-size: 11px;
+ opacity: 0.52;
+}
+
+@media (max-width: 768px) {
+ .doc-rich-slash-menu { max-height: min(54vh, 420px); }
+ .doc-rich-slash-item { min-height: 42px; font-size: 12px; }
+}
+
+/* Editor — highlighted overlay + transparent textarea.
+ Lock the editor wrap to a fixed text-column width, centered horizontally
+ in the doc pane. This eliminates wrap-related drift bugs as a class:
+ the textarea + overlay always have the same available width regardless
+ of viewport, sidebar state, dock state, or window resize. Whitespace on
+ either side reads like a writing-app column (Bear / iA Writer pattern).
+ container-type: inline-size lets us hide the line-number gutter via a
+ container query when the editor's own width is narrow (rather than
+ viewport-width, which would miss the case of a narrow side-docked
+ editor inside a wide window). */
+.doc-editor-wrap {
+ position: relative;
+ flex: 1;
+ min-height: 0;
+ /* Cap the column at ~820px outer (~760px content area after the 60px
+ of horizontal padding the textarea+overlay use for the gutter). */
+ max-width: 820px;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
+ overflow: hidden;
+ background: var(--bg);
+ container-type: inline-size;
+ container-name: doceditor;
+}
+
+/* When the editor itself is narrow, soft-wrap makes one logical line
+ span multiple visual rows but the gutter still shows ONE number per
+ logical line — so the gutter and the visible rows fall out of sync.
+ Hide the gutter (and reclaim the 48px of left padding it reserved)
+ below a threshold where the mismatch reads as a glitch. */
+@container doceditor (max-width: 360px) {
+ .doc-line-numbers { display: none !important; }
+ .doc-editor-textarea,
+ .doc-editor-highlight {
+ padding-left: 12px !important;
+ }
+}
+.doc-line-numbers {
+ position: absolute;
+ top: 0; left: 0; bottom: 0;
+ width: 36px;
+ padding: 10px 8px 10px 0;
+ margin: 0;
+ font-family: inherit;
+ font-size: 11px;
+ line-height: 1.45;
+ text-align: right;
+ color: var(--fg);
+ opacity: 0.18;
+ background: var(--bg);
+ overflow: hidden;
+ white-space: pre;
+ tab-size: 4;
+ font-variant-ligatures: none !important;
+ font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
+ font-kerning: none !important;
+ text-rendering: geometricPrecision !important;
+ z-index: 2;
+ pointer-events: none;
+ user-select: none;
+}
+.doc-line-number-content {
+ display: block;
+ will-change: transform;
+}
+.doc-line-number-row {
+ position: relative;
+ box-sizing: border-box;
+}
+.doc-line-number-label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 36px;
+ text-align: right;
+}
+.doc-line-number-measure {
+ position: absolute !important;
+ visibility: hidden !important;
+ pointer-events: none !important;
+ left: -9999px !important;
+ top: 0 !important;
+ height: 0 !important;
+ min-height: 0 !important;
+ max-height: none !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+ border: 0 !important;
+ resize: none !important;
+ box-sizing: content-box !important;
+ color: transparent !important;
+ background: transparent !important;
+}
+mark.doc-find-mark.current {
+ background: var(--accent) !important;
+ color: var(--bg) !important;
+ box-shadow: 0 0 0 2px var(--fg);
+ outline: 1px solid var(--fg);
+}
+
+/* Find-match overlay rects — drawn on top of the textarea, work in
+ every doc mode (markdown, email, plain) regardless of whether the
+ syntax-highlight overlay is shown. Translucent band so the underlying
+ text stays readable; current match gets a brighter solid band. */
+.doc-find-rect {
+ background: color-mix(in srgb, var(--accent) 25%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 65%, transparent);
+}
+.doc-find-rect.current {
+ background: color-mix(in srgb, var(--accent) 55%, transparent);
+ box-shadow: inset 0 0 0 2px var(--accent);
+}
+
+.doc-editor-highlight {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ margin: 0;
+ padding: 10px 12px 10px 48px;
+ font-family: inherit;
+ font-size: 11px !important;
+ line-height: 1.45 !important;
+ tab-size: 4;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow: hidden;
+ /* `scrollbar-gutter: stable` reserves scrollbar-track width in the
+ layout even when no scrollbar is shown. The textarea below has the
+ same setting — without this, the textarea would consume scrollbar
+ space the moment its content overflows vertically, shrinking its
+ content width and wrapping lines earlier than the overlay. The
+ visible-text-drift bug user reports as "after ~16 rows it wraps
+ even though there's space" was caused by exactly that. */
+ scrollbar-gutter: stable;
+ scrollbar-width: none;
+ pointer-events: none;
+ z-index: 0;
+ background: var(--bg) !important;
+ color: var(--hl-fg, var(--fg));
+ border: none;
+ /* Disable ligatures + kerning everywhere in the editor. Monospace fonts
+ like Fira Code form ligatures for `=>`, `!=`, `->`, `==` etc., but
+ hljs splits those pairs into separate s in the overlay, which
+ breaks the ligature on this side while the textarea still forms it.
+ Result: visible row drift whenever code contains those pairs. Pin
+ ligatures off in both layers (textarea below) so widths stay equal. */
+ font-variant-ligatures: none !important;
+ font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
+ font-kerning: none !important;
+ text-rendering: geometricPrecision !important;
+ box-sizing: border-box;
+}
+
+/* Document font size options */
+.doc-font-m .doc-editor-textarea,
+.doc-font-m .doc-editor-highlight,
+.doc-font-m .doc-line-numbers { font-size: 13px !important; }
+.doc-font-l .doc-editor-textarea,
+.doc-font-l .doc-editor-highlight,
+.doc-font-l .doc-line-numbers { font-size: 15px !important; }
+.doc-email-richbody.doc-font-m { font-size: 15px !important; }
+.doc-email-richbody.doc-font-l { font-size: 17px !important; }
+/* Markdown base text should match chat text color, not code color */
+.doc-editor-highlight .language-markdown {
+ color: var(--fg) !important;
+}
+.doc-editor-highlight code,
+.doc-editor-highlight code.hljs,
+.doc-editor-highlight .hljs {
+ font-family: inherit;
+ font-size: inherit !important;
+ line-height: inherit !important;
+ background: transparent !important;
+ padding: 0 !important;
+ margin: 0 !important;
+ border-radius: 0 !important;
+ overflow: hidden !important;
+ display: block;
+ pointer-events: none;
+}
+.doc-editor-textarea {
+ position: absolute;
+ top: 0; left: 0; right: 0; bottom: 0;
+ width: 100%;
+ height: 100% !important;
+ max-height: none !important;
+ min-height: 0 !important;
+ z-index: 1;
+ background: var(--bg) !important;
+ color: transparent !important;
+ color-scheme: dark;
+ caret-color: var(--fg);
+ border: none;
+ outline: none;
+ resize: none;
+ font-family: inherit;
+ /* Caret position only matches the underlying highlight if BOTH layers use
+ identical metrics — !important on font-size + line-height defends against
+ anything else in the cascade nudging the textarea but not the overlay. */
+ font-size: 11px !important;
+ line-height: 1.45 !important;
+ padding: 10px 12px 10px 48px;
+ overflow-y: scroll;
+ /* Pair with .doc-editor-highlight's scrollbar-gutter: stable so the
+ textarea's content width DOESN'T shrink the moment its scrollbar
+ appears (overflow-y: scroll keeps scrollbar permanent, gutter
+ reserves the space layout-wise). Without this, line wrap diverges
+ between textarea and overlay whenever content exceeds the visible
+ area — caret stays right, but typed text appears on a different row
+ than the caret. */
+ scrollbar-gutter: stable;
+ /* Show a real scrollbar for long documents. scrollbar-gutter above keeps
+ the text column stable so the gutter, textarea, and find overlay stay
+ metrically aligned while the scrollbar is present. */
+ scrollbar-width: thin;
+ scrollbar-color: color-mix(in srgb, var(--fg) 28%, transparent) transparent;
+ -webkit-overflow-scrolling: touch;
+ tab-size: 4;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ -moz-appearance: none;
+ appearance: none;
+ box-sizing: border-box;
+ /* Mirror the ligature/kerning lockdown from .doc-editor-highlight above.
+ Without this the textarea forms `=>`/`!=`/`->`/`==` as single-glyph
+ ligatures while the overlay can't (hljs splits the pair into
+ separate spans), so glyph widths diverge and the visible text drifts
+ down relative to the caret as code accumulates. */
+ font-variant-ligatures: none !important;
+ font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
+ font-kerning: none !important;
+ text-rendering: geometricPrecision !important;
+}
+.doc-editor-textarea::-webkit-scrollbar { width: 8px; }
+.doc-editor-textarea::-webkit-scrollbar-track { background: transparent; }
+.doc-editor-textarea::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--fg) 24%, transparent);
+ border-radius: 999px;
+}
+.doc-editor-textarea::-webkit-scrollbar-thumb:hover {
+ background: color-mix(in srgb, var(--fg) 36%, transparent);
+}
+.doc-editor-textarea:hover,
+.doc-editor-textarea:focus,
+.doc-editor-textarea:active {
+ background: var(--bg) !important;
+ /* Used to force `color: transparent` here so the hidden two-layer
+ textarea wouldn't bleed through on hover/focus. Now that the
+ textarea renders its OWN visible text (overlay is hidden), forcing
+ transparent here makes typed text disappear the moment the cursor
+ enters the page. Keep the visible fg color instead. */
+ color: var(--fg) !important;
+ outline: none !important;
+}
+.doc-editor-textarea::placeholder {
+ color: var(--fg);
+ opacity: 0.25;
+}
+/* Show real text when selecting so copy/paste is visible */
+.doc-editor-textarea::selection {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--accent, var(--red)) 48%, transparent);
+}
+
+/* ---- Selection indicator badge ---- */
+.doc-selection-badge {
+ font-size: 10px;
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+ border-radius: 4px;
+ padding: 1px 4px 1px 6px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 32px;
+ box-sizing: border-box;
+}
+.doc-selection-clear {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ color: var(--fg);
+ opacity: 0.4;
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ box-sizing: border-box;
+ transition: opacity 0.12s, color 0.12s, background 0.12s, border-color 0.12s;
+}
+.doc-selection-clear:hover {
+ opacity: 1;
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.doc-selection-chip-clear {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ margin-left: 3px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--panel, var(--bg));
+ color: var(--fg);
+ font: 700 15px/1 system-ui, sans-serif;
+ box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35);
+ cursor: pointer;
+}
+.doc-selection-chip-clear:hover,
+.doc-selection-chip-clear:focus-visible {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, var(--panel, var(--bg)));
+ outline: none;
+}
+.doc-selection-clear {
+ width: auto;
+ height: 18px;
+ margin-left: 3px;
+ padding: 0 7px;
+ border-radius: 999px;
+ font: 700 11px/1 system-ui, sans-serif;
+ opacity: 1;
+}
+.doc-edit-tag {
+ display: inline-flex;
+ align-items: center;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 32%, transparent);
+ color: var(--accent, var(--red));
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 0.68em;
+ line-height: 1;
+ opacity: 0.78;
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ border-radius: 999px;
+ padding: 1px 5px;
+ margin-right: 2px;
+ white-space: nowrap;
+ transition: opacity 0.12s, background 0.12s, border-color 0.12s;
+}
+.doc-edit-tag:hover,
+.doc-edit-tag:focus-visible {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+}
+.doc-edit-tag:focus-visible {
+ outline: 2px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+ outline-offset: 2px;
+}
+/* Attachment cards in user messages */
+.attach-cards {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 8px;
+}
+.attach-card {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 10px;
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ /* Same border as the chat bubbles. */
+ border: 1px solid var(--bubble-border, var(--border));
+ font-size: 12px;
+ transition: background 0.15s;
+}
+.attach-card[style*="cursor: pointer"]:hover,
+.attach-card[data-file-id]:hover {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.attach-card-icon {
+ flex-shrink: 0;
+ opacity: 0.6;
+}
+.attach-card-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 200px;
+}
+.attach-card-size {
+ opacity: 0.45;
+ font-size: 11px;
+ white-space: nowrap;
+}
+.history-attach-card {
+ cursor: default;
+ opacity: 0.82;
+}
+.history-attach-card .attach-card-size {
+ text-transform: lowercase;
+}
+/* Import prompt banner (above chatbar) */
+.import-prompt-banner {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ margin: 0 auto 6px;
+ max-width: 800px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
+ border-left: 3px solid var(--accent);
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--fg);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ backdrop-filter: blur(12px);
+ animation: modal-enter 0.15s ease-out;
+}
+.import-prompt-banner span { flex: 1; }
+.import-prompt-banner button {
+ padding: 3px 12px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: none;
+ color: var(--fg);
+ cursor: pointer;
+ font-size: 12px;
+ white-space: nowrap;
+}
+.import-prompt-banner button:hover {
+ border-color: var(--fg);
+}
+.import-prompt-dismiss {
+ border: none !important;
+ background: none !important;
+ opacity: 0.5;
+ font-size: 16px !important;
+ padding: 0 4px !important;
+}
+.import-prompt-dismiss:hover { opacity: 1; background: none !important; }
+.doc-selection-overlay {
+ position: absolute;
+ background: color-mix(in srgb, var(--red) 10%, transparent);
+ border-left: 2px solid color-mix(in srgb, var(--red) 50%, transparent);
+ pointer-events: none;
+ z-index: 2;
+ transition: top 0.05s;
+}
+.doc-selection-overlay-clear {
+ position: absolute;
+ top: 1px;
+ right: 3px;
+ width: 18px;
+ height: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--panel) 82%, transparent);
+ color: var(--fg);
+ font: 16px/1 inherit;
+ opacity: 0.7;
+ cursor: pointer;
+ pointer-events: auto;
+}
+.doc-selection-overlay-clear:hover,
+.doc-selection-overlay-clear:focus-visible {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, var(--panel));
+ outline: none;
+}
+.doc-selection-rich-clear {
+ position: fixed;
+ z-index: 10040;
+ width: 18px;
+ height: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--panel, var(--bg));
+ color: var(--fg);
+ font: 700 15px/1 system-ui, sans-serif;
+ opacity: 1;
+ cursor: pointer;
+ box-shadow: 0 2px 7px rgba(0, 0, 0, 0.35);
+}
+.doc-selection-rich-clear:hover,
+.doc-selection-rich-clear:focus-visible {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, var(--panel, var(--bg)));
+ outline: none;
+}
+
+/* ── Suggestion comments (Google Docs style) ── */
+.doc-suggestion-highlight {
+ position: absolute;
+ box-sizing: border-box;
+ background: color-mix(in srgb, var(--accent) 32%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 72%, transparent);
+ border-radius: 2px;
+ pointer-events: none;
+ z-index: 3;
+ transition: top 0.1s, opacity 0.15s;
+}
+/* Suggestion card — fixed next to editor, anchored to the change */
+.doc-suggestion-card {
+ position: fixed;
+ width: 250px;
+ background: var(--panel);
+ border: 1px solid var(--accent);
+ border-radius: 10px;
+ padding: 12px 12px 10px;
+ font-size: 12px;
+ box-shadow: 0 4px 20px rgba(0,0,0,0.25);
+ animation: suggestion-enter 0.25s ease-out;
+ z-index: 250;
+ overflow: visible;
+}
+.doc-suggestion-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 6px;
+}
+.doc-suggestion-nav {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.doc-suggestion-nav-btn {
+ background: none;
+ border: none;
+ color: var(--fg);
+ cursor: pointer;
+ font-size: 16px;
+ line-height: 1;
+ padding: 2px 4px;
+ opacity: 0.35;
+ transition: opacity 0.1s;
+}
+.doc-suggestion-nav-btn:hover { opacity: 1; }
+.doc-suggestion-close {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ margin: -2px -2px -2px 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 999px;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+ opacity: 1;
+ cursor: pointer;
+ font-size: 0;
+ line-height: 1;
+ flex-shrink: 0;
+ position: relative;
+}
+.doc-suggestion-close:hover,
+.doc-suggestion-close:focus-visible {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+ color: var(--fg);
+ outline: none;
+}
+.doc-suggestion-counter {
+ font-size: 10px;
+ opacity: 0.4;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+}
+/* Inline diff markers — injected into the code highlight element */
+.sugg-inline-del {
+ background: color-mix(in srgb, var(--red) 20%, transparent);
+ color: color-mix(in srgb, var(--red) 70%, var(--fg));
+ text-decoration: line-through;
+ text-decoration-thickness: 1px;
+ text-decoration-color: color-mix(in srgb, var(--red) 40%, transparent);
+ border-radius: 2px;
+ padding: 0 2px;
+}
+.sugg-inline-add {
+ background: color-mix(in srgb, var(--green) 25%, transparent);
+ color: var(--green);
+ border-radius: 2px;
+ padding: 0 2px;
+}
+/* ---- Diff mode ---- */
+.diff-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 12px;
+ background: color-mix(in srgb, var(--fg) 4%, var(--bg));
+ border-bottom: 1px solid var(--border);
+ font-size: 11px;
+ flex-shrink: 0;
+}
+.diff-toolbar-status {
+ opacity: 0.5;
+ font-size: 10px;
+ margin-right: auto;
+}
+.diff-toolbar-btn {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 10px;
+ padding: 3px 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-family: inherit;
+ transition: border-color 0.15s, background 0.15s;
+}
+.diff-toolbar-btn:hover {
+ border-color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+}
+.diff-toolbar-btn-accept { color: #58a6ff; border-color: color-mix(in srgb, #58a6ff 36%, transparent); }
+.diff-toolbar-btn-accept:hover { border-color: #58a6ff; background: color-mix(in srgb, #58a6ff 12%, transparent); }
+.diff-toolbar-btn-reject { color: #f2cc60; border-color: color-mix(in srgb, #f2cc60 36%, transparent); }
+.diff-toolbar-btn-reject:hover { border-color: #f2cc60; background: color-mix(in srgb, #f2cc60 12%, transparent); }
+.diff-line-del {
+ display: block;
+ background: color-mix(in srgb, #f2cc60 24%, transparent);
+ border-left: 3px solid #f2cc60;
+ color: #f2cc60;
+ padding-left: 4px;
+ margin-left: -7px;
+ text-decoration: line-through;
+}
+.diff-line-add {
+ display: block;
+ background: color-mix(in srgb, #58a6ff 24%, transparent);
+ border-left: 3px solid #58a6ff;
+ color: #58a6ff;
+ padding-left: 4px;
+ margin-left: -7px;
+}
+/* Inline diff summary (version history cards) */
+.diff-del {
+ color: #f2cc60;
+ background: color-mix(in srgb, #f2cc60 16%, transparent);
+ text-decoration: line-through;
+ border-radius: 2px;
+ padding: 0 2px;
+}
+.diff-add {
+ color: #58a6ff;
+ background: color-mix(in srgb, #58a6ff 16%, transparent);
+ font-weight: 600;
+ border-radius: 2px;
+ padding: 0 2px;
+}
+.diff-line-equal {
+ display: block;
+}
+.diff-chunk-resolved {
+ opacity: 0.3;
+ transition: opacity 0.3s;
+}
+.diff-chunk-actions {
+ position: absolute;
+ right: 8px;
+ top: 0;
+ display: flex;
+ gap: 4px;
+ z-index: 5;
+ pointer-events: auto;
+}
+/* Diff mode: textarea sits on top of the highlight where chunk buttons live.
+ Disable its pointer events so clicks reach the buttons in the layer below. */
+.doc-editor-wrap.diff-mode .doc-editor-textarea {
+ pointer-events: none;
+ color: transparent !important;
+ caret-color: transparent !important;
+}
+.doc-editor-wrap.diff-mode .doc-editor-highlight {
+ display: block !important;
+ pointer-events: auto;
+ z-index: 3;
+ color: var(--fg) !important;
+ background: var(--sidebar-bg, var(--panel)) !important;
+ overflow: hidden;
+}
+.doc-editor-wrap.diff-mode .doc-editor-highlight * {
+ color: inherit;
+}
+.doc-editor-wrap.diff-mode .diff-line-del {
+ color: #f2cc60 !important;
+}
+.doc-editor-wrap.diff-mode .diff-line-add {
+ color: #58a6ff !important;
+}
+.diff-chunk-btn {
+ width: 20px;
+ height: 20px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ line-height: 1;
+ transition: border-color 0.15s, background 0.15s;
+ opacity: 0.6;
+}
+.diff-chunk-btn:hover { opacity: 1; }
+.diff-chunk-btn-accept { color: #58a6ff; }
+.diff-chunk-btn-accept:hover { border-color: #58a6ff; background: color-mix(in srgb, #58a6ff 15%, transparent); }
+.diff-chunk-btn-reject { color: #f2cc60; }
+.diff-chunk-btn-reject:hover { border-color: #f2cc60; background: color-mix(in srgb, #f2cc60 15%, transparent); }
+
+.doc-suggestion-accept-all {
+ flex: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ padding: 5px 8px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 10px;
+ font-family: inherit;
+ background: transparent;
+ border: 1px solid var(--accent);
+ color: var(--accent);
+ transition: background 0.1s;
+}
+.doc-suggestion-accept-all:hover {
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
+}
+.doc-suggestion-reason {
+ opacity: 0.6;
+ margin-bottom: 6px;
+ font-size: 11px;
+ line-height: 1.4;
+}
+.doc-suggestion-diff {
+ background: var(--bg);
+ border-radius: 4px;
+ padding: 6px 8px;
+ font-family: var(--code-font, monospace);
+ font-size: 11px;
+ margin-bottom: 8px;
+ max-height: 100px;
+ overflow-y: auto;
+ word-break: break-word;
+}
+.doc-suggestion-del {
+ color: var(--red);
+ text-decoration: line-through;
+ opacity: 0.7;
+}
+.doc-suggestion-add {
+ color: var(--green);
+}
+.doc-suggestion-actions {
+ display: flex;
+ gap: 6px;
+}
+.doc-suggestion-accept,
+.doc-suggestion-dismiss {
+ flex: 1;
+ padding: 5px 8px;
+ border: none;
+ border-radius: 5px;
+ cursor: pointer;
+ font-size: 11px;
+ font-family: inherit;
+ font-weight: 500;
+ transition: background 0.1s;
+}
+.doc-suggestion-accept {
+ background: var(--accent, var(--red));
+ color: #fff;
+}
+.doc-suggestion-dismiss {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+}
+.doc-suggestion-dismiss:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+@keyframes suggestion-enter {
+ from { opacity: 0; transform: translateX(10px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+/* Mobile: suggestion card overlays top of editor (no room on side) */
+@media (max-width: 768px) {
+ .doc-suggestion-card {
+ right: 8px;
+ right: 8px;
+ width: auto;
+ top: 8px !important;
+ }
+ .doc-suggestion-card::before { display: none; }
+}
+
+/* ---- Streaming animation ---- */
+.doc-editor-textarea[readonly] {
+ caret-color: var(--red);
+}
+
+/* Diff overlay */
+.doc-diff-overlay {
+ position: absolute;
+ inset: 0;
+ background: var(--bg);
+ z-index: 5;
+ overflow-y: auto;
+ padding: 8px 12px;
+ font-family: 'Fira Code', monospace;
+ font-size: 0.85rem;
+ line-height: 1.5;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ border-radius: 6px;
+}
+.doc-diff-overlay.visible { opacity: 1; }
+.doc-diff-overlay.fading { opacity: 0; transition: opacity 0.4s ease; }
+.doc-diff-stats {
+ display: flex;
+ gap: 10px;
+ padding: 4px 0 8px;
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+.diff-stat-del { color: var(--warn); }
+.diff-stat-add { color: var(--green); }
+.doc-diff-line {
+ padding: 1px 8px;
+ border-radius: 4px;
+ white-space: pre-wrap;
+ word-break: break-all;
+}
+.doc-diff-line.same {
+ opacity: 0.4;
+}
+.doc-diff-line.del {
+ background: color-mix(in srgb, var(--warn) 15%, transparent);
+ color: var(--warn);
+ text-decoration: line-through;
+ text-decoration-color: color-mix(in srgb, var(--warn) 40%, transparent);
+}
+.doc-diff-line.add {
+ background: color-mix(in srgb, var(--green) 12%, transparent);
+ color: var(--green);
+}
+.doc-diff-sep {
+ text-align: center;
+ padding: 2px 0;
+ font-size: 0.7rem;
+ opacity: 0.3;
+ color: var(--fg);
+}
+
+/* Version history panel (slide-out) */
+.doc-version-panel {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 300px;
+ background: var(--bg);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ z-index: 200;
+ box-shadow: 4px 0 16px rgba(0,0,0,0.35);
+ animation: version-slide-in 0.2s ease-out;
+ /* Match the app modal aesthetic so nothing inherits the large body font. */
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 12px;
+ letter-spacing: -0.01em;
+}
+@keyframes version-slide-in {
+ from { opacity: 0; transform: translateX(-30px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+.doc-version-panel.hidden {
+ display: none;
+}
+@media (max-width: 768px) {
+ .doc-version-panel {
+ left: 0;
+ right: 0;
+ top: auto;
+ bottom: 0;
+ width: 100%;
+ height: 50vh;
+ border-right: none;
+ border-left: none;
+ border-top: 1px solid var(--border);
+ border-radius: 14px 14px 0 0;
+ box-shadow: 0 -4px 16px rgba(0,0,0,0.3);
+ /* It's a bottom sheet on mobile — slide UP from the bottom, not in from
+ the left (the desktop animation looked like a black box janking in). */
+ animation: version-slide-up 0.2s ease-out;
+ }
+}
+@keyframes version-slide-up {
+ from { opacity: 0; transform: translateY(30px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.doc-version-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--border);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--fg);
+}
+
+.doc-version-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+}
+
+.doc-version-item {
+ padding: 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-bottom: 6px;
+ cursor: pointer;
+ transition: all 0.15s;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.doc-version-item:hover {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: var(--fg);
+}
+
+.doc-version-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+
+.doc-version-num {
+ font-weight: 600;
+ font-size: 12px;
+ color: var(--fg);
+}
+.doc-version-source {
+ font-size: 10px;
+ color: var(--fg);
+ opacity: 0.5;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ padding: 1px 6px;
+ border-radius: 4px;
+}
+.doc-version-time {
+ font-size: 10px;
+ color: var(--fg);
+ opacity: 0.4;
+ margin-left: auto;
+}
+.doc-version-summary {
+ font-size: 11px;
+ color: var(--fg);
+ opacity: 0.5;
+ margin-bottom: 4px;
+}
+
+.doc-version-restore {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 10px;
+ padding: 2px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.doc-version-restore:hover {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: var(--fg);
+}
+/* "latest" badge */
+.doc-version-latest {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ padding: 1px 6px;
+ border-radius: 4px;
+}
+/* Diff preview lines — small and muted so they don't dominate the item. */
+.doc-version-diff {
+ font-size: 10px;
+ line-height: 1.5;
+ opacity: 0.8;
+ margin-top: 2px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ word-break: break-word;
+}
+.doc-version-diff .diff-del {
+ color: #f2cc60;
+ background: color-mix(in srgb, #f2cc60 16%, transparent);
+ opacity: 1;
+}
+.doc-version-diff .diff-add {
+ color: #58a6ff;
+ background: color-mix(in srgb, #58a6ff 16%, transparent);
+}
+
+/* Mobile: doc editor takes over full screen as toggle */
+@media (max-width: 768px) {
+ body.doc-view .doc-editor-pane {
+ /* Force its own full-screen window on mobile. !important on these so an
+ inline width/position left over from desktop drag-resize, or the base
+ desktop split layout (flex: 1; max-width: 70vw; border-left), can never
+ render it as a narrow side pane ("sidebar") on a phone. */
+ position: fixed !important;
+ inset: 0 !important;
+ top: 0 !important; right: 0 !important; bottom: 0 !important; left: 0 !important;
+ max-width: 100% !important;
+ width: 100% !important;
+ flex: none !important;
+ z-index: 170;
+ /* Stroke the top edge so the rounded corners read as a curved sheet edge. */
+ border: 1px solid var(--border);
+ border-bottom: none;
+ /* Rounded top corners like the other mobile sheet windows. */
+ border-radius: 14px 14px 0 0;
+ /* Slide up from the bottom (sheet), not in from the side, on mobile. */
+ animation: sheet-enter 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+ transform-origin: bottom center;
+ }
+ /* A document that was already open before refresh should be restored in
+ place, not replay the mobile sheet entrance and visibly jump upward. */
+ body.doc-view .doc-editor-pane.doc-pane-restoring {
+ animation: none;
+ transform: none;
+ }
+ body.doc-view .doc-divider {
+ display: none;
+ }
+ /* Hide chat behind doc panel on mobile */
+ body.doc-view .chat-container {
+ display: none;
+ }
+ /* Doc + email windows alternate: whichever was opened last is in front.
+ Default (a doc was opened last) → email windows sit BELOW the full-screen
+ doc pane (170) so the draft is on top. When the user re-opens an email
+ window (body.email-front, set by openEmailLibrary / reader open+restore) →
+ the email windows jump ABOVE the doc. Covers the email library AND any open
+ reader window (email-reader-); both are .modal at z-250 by default. */
+ body.doc-view #email-lib-modal,
+ body.doc-view .modal[id^="email-reader-"] { z-index: 150 !important; }
+ body.doc-view.email-front #email-lib-modal,
+ body.doc-view.email-front .modal[id^="email-reader-"] { z-index: 300 !important; }
+ body.doc-view:not(.email-front) .doc-editor-pane { z-index: 10010 !important; }
+ body.doc-view:not(.email-front) #email-lib-modal,
+ body.doc-view:not(.email-front) .modal[id^="email-reader-"] { z-index: 10000 !important; }
+ /* Hide new-session button and hamburger when doc editor is open on mobile */
+ body.doc-view .mobile-new-chat-btn,
+ /* Hide the global hamburger while Compare Mode is active — the compare
+ header has its own close button in the top-right, and overlapping the
+ two looks like a bug. The .compare-active class lives on the chat
+ container, not body, so use :has(). */
+body:has(.compare-active) .hamburger-btn { display: none !important; }
+/* Mobile: hide the sidebar hamburger when a document panel (or notes pane)
+ is open — those sheets cover the whole screen on mobile, so a floating
+ hamburger sticking out over them is just clutter / mis-tap bait. */
+@media (max-width: 768px) {
+ body.doc-view .hamburger-btn,
+ body:has(#notes-pane) .hamburger-btn { display: none !important; }
+}
+ /* Make room for hamburger button alongside the tab/header bars (fallback if shown) */
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-tab-bar,
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-editor-header,
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-md-toolbar {
+ padding-left: 44px;
+ }
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-tab-bar,
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-editor-header,
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-md-toolbar {
+ padding-right: 44px;
+ }
+ /* ── Tab bar — match header height, bigger touch targets ── */
+ .doc-tab-bar {
+ padding: 0;
+ height: 40px;
+ }
+ .doc-tab {
+ padding: 0 12px;
+ font-size: 13px;
+ }
+ /* Bigger × touch target on mobile. */
+ .doc-tab-close {
+ font-size: 22px;
+ width: 32px;
+ height: 32px;
+ padding: 0;
+ opacity: 0.5;
+ }
+ .doc-tab .doc-tab-menu-btn {
+ opacity: 0.4 !important;
+ padding: 4px 6px !important;
+ }
+ .doc-tab .doc-tab-menu-btn svg {
+ width: 14px !important;
+ height: 14px !important;
+ }
+ /* Footer is identical to desktop now, so the separate mobile Close/Copy
+ footer is dropped and the per-tab × stays (matching desktop). */
+ .doc-mobile-footer { display: none !important; }
+ .doc-tab-new {
+ font-size: 13px !important;
+ padding: 0 14px !important;
+ gap: 4px;
+ }
+ /* No hamburger padding — it's hidden in doc-view */
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-tab-bar,
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-editor-header,
+ body.doc-view.sidebar-collapsed.hamburger-left .doc-md-toolbar {
+ padding-left: 0 !important;
+ }
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-tab-bar,
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-editor-header,
+ body.doc-view.sidebar-collapsed.hamburger-right .doc-md-toolbar {
+ padding-right: 0 !important;
+ }
+ /* ── Header — identical layout to desktop (left-aligned), match tab height ── */
+ .doc-editor-header {
+ padding: 6px 12px 6px 8px;
+ gap: 6px;
+ min-height: 40px;
+ }
+ .doc-import-label,
+ #doc-version-badge {
+ display: none !important;
+ }
+ /* ── Markdown toolbar — same height as tab bar ── */
+ .doc-md-toolbar {
+ height: 40px !important;
+ padding: 4px 8px;
+ gap: 2px;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+ /* The toolbar's padding-left is forced to 0 by the hamburger rules, so give
+ the Edit/Preview toggle its own left margin to clear the screen edge +
+ the 18px mask fade. Margin isn't overridden by those !important paddings. */
+ .doc-md-toolbar .md-view-toggle { margin-left: 8px; }
+ .doc-md-toolbar button {
+ font-size: 13px !important;
+ padding: 6px 10px !important;
+ min-width: 34px;
+ min-height: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+ .md-toolbar-sep {
+ height: 18px;
+ margin: 0 4px;
+ }
+}
+
+/* Opacity slider in the theme tabs row — lets the user see the page behind
+ the modal while tweaking colors. JS toggles .hidden based on active tab. */
+.theme-opacity-wrap {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ margin-left: auto;
+ margin-right: 6px;
+ padding: 2px 9px;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ opacity: 0.65;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
+ height: 22px;
+ align-self: center;
+ /* Now a toggle, not a slider wrapper. */
+ cursor: pointer;
+ color: var(--fg);
+ font: inherit;
+ background: transparent;
+}
+.theme-opacity-wrap.hidden { display: none; }
+.theme-opacity-wrap:hover { opacity: 1; }
+/* The title h4 already carries margin-right:auto to group header controls
+ on the right. BOTH the Peek button AND the injected minimize button also
+ have margin-left:auto — and multiple competing auto-margins split the
+ free space, stranding Peek in the middle. Zero their left-autos here so
+ the h4 alone pushes Peek + minimize + close together, flush right. */
+.modal-header .theme-opacity-wrap,
+.modal-header .modal-minimize-btn { margin-left: 0 !important; }
+/* On = peeking through; highlight with the accent so the state is obvious. */
+.theme-opacity-wrap.active {
+ opacity: 1;
+ border-color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ color: var(--accent, var(--red));
+}
+.theme-opacity-label { font-size: 10px; font-weight: 600; letter-spacing: 0.02em; }
+.theme-opacity-wrap > svg { flex-shrink: 0; opacity: 0.7; }
+.theme-opacity-wrap input[type="range"] {
+ width: 92px;
+ height: 4px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: color-mix(in srgb, var(--fg) 18%, transparent);
+ border-radius: 2px;
+ outline: none;
+ margin: 0;
+}
+.theme-opacity-wrap input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--accent, var(--red));
+ border: none;
+ cursor: pointer;
+}
+.theme-opacity-wrap input[type="range"]::-moz-range-thumb {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--accent, var(--red));
+ border: none;
+ cursor: pointer;
+}
+
+/* ── Theme editor: hover-to-highlight zone ── */
+.theme-zone-highlight {
+ position: fixed;
+ pointer-events: none;
+ z-index: 9998;
+ border: 2px dashed var(--accent, var(--red));
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 40%, transparent),
+ 0 0 18px color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+ animation: theme-zone-pulse 1.4s ease-in-out infinite;
+}
+@keyframes theme-zone-pulse {
+ 0%, 100% { opacity: 0.95; }
+ 50% { opacity: 0.55; }
+}
+/* Highlight the color row itself too so the user has a strong visual link
+ between the input they're hovering and the zone overlay on the page. */
+#theme-tab-customize .color-row {
+ transition: background 0.15s, border-color 0.15s;
+ border-radius: 6px;
+}
+#theme-tab-customize .color-row:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+}
+
+/* "Auto-saved" pill that flashes after each color/font change. */
+.theme-autosaved-pill {
+ position: sticky;
+ bottom: 8px;
+ margin: 8px auto 0;
+ width: fit-content;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ background: color-mix(in srgb, var(--color-success, #4caf50) 18%, var(--bg));
+ color: var(--color-success, #4caf50);
+ border: 1px solid color-mix(in srgb, var(--color-success, #4caf50) 40%, transparent);
+ border-radius: 12px;
+ padding: 4px 10px;
+ font-size: 11px;
+ font-weight: 500;
+ opacity: 0;
+ transform: translateY(4px);
+ transition: opacity 0.2s ease-out, transform 0.2s ease-out;
+ pointer-events: none;
+ z-index: 5;
+}
+.theme-autosaved-pill.visible {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+/* ---- Doc fullscreen ---- */
+.doc-editor-pane.doc-fullscreen {
+ flex: 1;
+ max-width: 100%;
+ width: 100% !important;
+ border-left: none;
+}
+/* Keep the hamburger reachable in fullscreen — version history can still
+ hide it (the panel covers that area). */
+body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
+ display: none !important;
+}
+
+/* ---- Document run output ---- */
+.doc-run-output {
+ border-top: 2px solid var(--hl-function, #61afef);
+ background: var(--bg);
+ padding: 6px 12px 8px;
+ max-height: 200px;
+ overflow: auto;
+ font-family: 'Fira Code', 'Courier New', monospace;
+ font-size: 0.85em;
+ line-height: 1.5;
+ position: relative;
+}
+.doc-run-output .code-runner-pre { background: none !important; border: none !important; margin: 0; padding: 0; }
+.doc-run-output .code-runner-copy-inline {
+ right: 16px;
+ width: 28px;
+ height: 26px;
+ padding: 0;
+ align-items: center;
+ justify-content: center;
+}
+.doc-run-output .code-runner-error { color: var(--red); }
+.doc-run-output .code-runner-loading { font-style: italic; color: var(--red); }
+.doc-run-output .code-runner-empty { color: var(--fg-muted, var(--fg)); padding: 2px 0; }
+.doc-run-output .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; }
+.doc-run-output .code-runner-close:hover { opacity: 1; }
+.doc-run-pre { color: var(--fg); white-space: pre-wrap; word-break: break-word; margin: 0; }
+.doc-run-error { color: var(--red); white-space: pre-wrap; word-break: break-word; margin: 0; }
+
+/* ---- Markdown preview ---- */
+.doc-md-preview {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px 20px;
+ font-family: inherit;
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--fg);
+ background: var(--bg);
+}
+.doc-md-preview.doc-rich-preview {
+ background: #fff;
+ color: #111;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.6;
+ padding: 32px max(24px, calc((100% - 780px) / 2));
+}
+.doc-rich-preview > :not(.doc-preview-hover-edit) {
+ max-width: 780px;
+ margin-left: auto;
+ margin-right: auto;
+}
+.doc-rich-preview-empty {
+ position: relative;
+ left: -16px;
+ top: -8px;
+ color: #6b7280;
+ text-align: center;
+}
+.doc-preview-hover-edit {
+ position: sticky;
+ top: 0;
+ z-index: 4;
+ float: right;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ margin: 0 0 8px 12px;
+ padding: 5px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel);
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(-2px);
+ transition: opacity 0.12s, transform 0.12s, background 0.12s, border-color 0.12s;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
+}
+.doc-md-preview:hover .doc-preview-hover-edit,
+.doc-preview-hover-edit:focus-visible {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+}
+.doc-preview-hover-edit:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--panel));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+}
+
+/* DOCX visual preview — keep the Word document on a readable white sheet even
+ when the surrounding workspace uses a dark theme. */
+.doc-docx-preview {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+ padding: 28px 18px 42px;
+ background: #d9dce1;
+ color: #111827;
+}
+.doc-docx-paper {
+ width: min(816px, 100%);
+ min-height: 1056px;
+ box-sizing: border-box;
+ margin: 0 auto;
+ padding: 72px 76px;
+ background: #fff;
+ color: #111;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 15px;
+ line-height: 1.55;
+ box-shadow: 0 4px 18px rgba(0,0,0,.22);
+}
+.doc-docx-paper h1, .doc-docx-paper h2, .doc-docx-paper h3,
+.doc-docx-paper h4, .doc-docx-paper h5, .doc-docx-paper h6 {
+ color: #111;
+ line-height: 1.2;
+ margin: 1.2em 0 .55em;
+}
+.doc-docx-paper p { margin: 0 0 .8em; }
+.doc-docx-paper img { max-width: 100%; height: auto; }
+.doc-docx-paper table { width: 100%; border-collapse: collapse; margin: 1em 0; }
+.doc-docx-paper td, .doc-docx-paper th { border: 1px solid #c7cbd1; padding: 6px 8px; vertical-align: top; }
+.doc-docx-paper th { background: #f2f3f5; }
+.doc-docx-paper a { color: #1757a6; }
+.doc-docx-preview-loading, .doc-docx-preview-error {
+ max-width: 816px;
+ margin: 40px auto;
+ padding: 20px;
+ color: var(--fg);
+ text-align: center;
+}
+.doc-docx-preview-error { color: var(--color-error, #c2413d); }
+@media (max-width: 700px) {
+ .doc-docx-preview { padding: 12px 8px 28px; }
+ .doc-docx-paper { min-height: 0; padding: 34px 24px; font-size: 14px; }
+}
+.doc-md-preview pre {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 10px 12px;
+ overflow-x: auto;
+}
+.doc-md-preview code {
+ font-size: 0.95em;
+}
+.doc-md-preview p { margin: 0.6em 0; }
+.doc-md-preview h1, .doc-md-preview h2, .doc-md-preview h3 {
+ margin: 0.8em 0 0.4em;
+ color: var(--hl-string);
+}
+.doc-md-preview ul, .doc-md-preview ol {
+ margin-left: 20px;
+ margin-bottom: 0.6em;
+}
+.doc-md-preview blockquote {
+ border-left: 3px solid var(--border);
+ padding-left: 12px;
+ margin: 0.6em 0;
+ opacity: 0.8;
+}
+
+/* ---- Active state for doc action icon buttons ---- */
+.doc-action-icon-btn.active {
+ opacity: 1;
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+#doc-run-btn:hover {
+ color: var(--hl-function, #61afef);
+}
+
+/* ===== ADMIN PANEL (inside settings modal) ===== */
+
+/* Cards */
+.admin-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px;
+ margin-bottom: 10px;
+}
+/* When the integrations editor opens, the inner admin-card should match the
+ listed integration cards (subtle tint, same border) instead of reverting
+ to the solid-panel admin-card surface used elsewhere. */
+#unified-intg-form .admin-card,
+#integrations-form .admin-card {
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.admin-card h2 {
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: -0.03em;
+ margin-bottom: 8px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+}
+.admin-danger-card {
+ border-color: color-mix(in srgb, var(--color-error) 27%, transparent);
+}
+
+/* Toggle switch */
+.admin-toggle-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.admin-toggle-label {
+ font-size: 13px;
+ font-weight: 500;
+}
+.admin-toggle-sub {
+ /* 65% mix keeps this helper text above WCAG AA 4.5:1 on the dark panel
+ (50% only reached ~3.9:1). */
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ font-size: 11px;
+ margin-top: 2px;
+}
+/* Hide the native up/down spinners on the Max auto-skills number input. */
+#skill-max-input {
+ -moz-appearance: textfield;
+ appearance: textfield;
+}
+#skill-max-input::-webkit-inner-spin-button,
+#skill-max-input::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+.admin-switch {
+ position: relative;
+ width: 30px;
+ height: 16px;
+ flex-shrink: 0;
+ display: inline-block;
+}
+.admin-switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+.admin-slider {
+ position: absolute;
+ inset: 0;
+ background: color-mix(in srgb, var(--fg) 50%, transparent);
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.08s;
+}
diff --git a/static/css/06-admin-settings.css b/static/css/06-admin-settings.css
new file mode 100644
index 000000000..4c2e95a30
--- /dev/null
+++ b/static/css/06-admin-settings.css
@@ -0,0 +1,1426 @@
+/* 06-admin-settings - 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.
+ */
+.admin-slider::before {
+ content: '';
+ position: absolute;
+ left: 2px;
+ top: 2px;
+ width: 12px;
+ height: 12px;
+ background: var(--panel);
+ border-radius: 50%;
+ transition: transform 0.08s;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.25);
+}
+.admin-switch input:checked + .admin-slider {
+ background: var(--red);
+}
+.admin-switch input:checked + .admin-slider::before {
+ transform: translateX(14px);
+}
+
+/* User rows */
+.admin-user-row {
+ display: flex;
+ flex-direction: column;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ margin-bottom: 6px;
+ transition: border-color 0.15s;
+}
+.admin-user-row:hover {
+ border-color: color-mix(in srgb, var(--fg) 20%, var(--border));
+}
+.admin-user-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.admin-user-name { font-size: 13px; font-weight: 500; }
+/* Privilege panel */
+.admin-priv-panel {
+ max-height: 600px;
+ transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.2s ease;
+ overflow: hidden;
+}
+.admin-priv-panel.hidden {
+ max-height: 0 !important;
+ opacity: 0;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ margin-top: 0 !important;
+ border-top: none !important;
+}
+/* Privilege toggle rows */
+.admin-priv-panel [data-priv] {
+ accent-color: var(--accent, var(--red));
+}
+/* Section headers */
+.admin-priv-section {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ opacity: 0.35;
+ font-weight: 600;
+ margin: 10px 0 4px;
+}
+.admin-priv-section:first-child {
+ margin-top: 0;
+}
+/* Model checkbox list */
+.priv-models-list {
+ max-height: 150px;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px 6px;
+ background: var(--bg);
+ scrollbar-width: thin;
+}
+.priv-models-list::-webkit-scrollbar {
+ width: 4px;
+}
+.priv-models-list::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--fg) 15%, transparent);
+ border-radius: 2px;
+}
+.priv-models-list label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 3px 2px;
+ font-size: 11px;
+ cursor: pointer;
+ border-radius: 3px;
+ transition: background 0.1s;
+}
+.priv-models-list label:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.priv-models-list input[type="checkbox"] {
+ accent-color: var(--accent, var(--red));
+ margin: 0;
+ flex-shrink: 0;
+}
+/* All/None links */
+.priv-models-all,
+.priv-models-none {
+ font-size: 10px;
+ opacity: 0.5;
+ color: var(--fg);
+ text-decoration: none;
+ cursor: pointer;
+ transition: opacity 0.1s;
+}
+.priv-models-all:hover,
+.priv-models-none:hover {
+ opacity: 1;
+}
+
+/* MCP tool toggles panel */
+.mcp-tools-panel {
+ width: 100%;
+ padding: 8px 0 4px;
+ border-top: 1px solid var(--border);
+ margin-top: 8px;
+ max-height: 600px;
+ transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.2s ease;
+ overflow: hidden;
+}
+.mcp-tools-panel.hidden {
+ max-height: 0 !important;
+ opacity: 0;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ margin-top: 0 !important;
+}
+.mcp-tools-panel .mcp-tools-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 6px;
+}
+.mcp-tools-panel .mcp-tools-header span:first-child {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ opacity: 0.35;
+ font-weight: 600;
+}
+.mcp-tools-panel .mcp-tools-header a {
+ font-size: 10px;
+ opacity: 0.5;
+ color: var(--fg);
+ text-decoration: none;
+ cursor: pointer;
+ transition: opacity 0.1s;
+}
+.mcp-tools-panel .mcp-tools-header a:hover {
+ opacity: 1;
+}
+.mcp-tools-list {
+ max-height: 200px;
+ overflow-y: auto;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px 6px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.mcp-tools-list::-webkit-scrollbar {
+ width: 4px;
+}
+.mcp-tools-list::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--fg) 15%, transparent);
+ border-radius: 2px;
+}
+.mcp-tools-list label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 6px;
+ font-size: 11px;
+ cursor: pointer;
+ border-radius: 3px;
+ min-width: 0;
+}
+.mcp-tools-list label > span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+.mcp-tools-list label:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.mcp-tools-list input[type="checkbox"] {
+ accent-color: var(--accent, var(--red));
+ margin: 0;
+ flex-shrink: 0;
+}
+/* Dot-style toggle (mirrors .note-check-dot) for the Add Models row,
+ replacing the native checkbox while keeping it for click handling. */
+.adm-cb-hidden {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+.adm-check-dot {
+ /* Lock the dot to a fixed 12×12 box. The parent rule
+ `.mcp-tools-list label > span { flex: 1 }` was stretching this span
+ to fill the row when it didn't have its own flex override. */
+ flex: 0 0 12px !important;
+ width: 12px !important;
+ height: 12px;
+ min-width: 12px;
+ max-width: 12px;
+ box-sizing: border-box;
+ border-radius: 50%;
+ border: 1.5px solid color-mix(in srgb, var(--fg) 35%, transparent);
+ position: relative;
+ transition: background 0.2s, border-color 0.2s, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+.adm-model-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 28px;
+ padding: 2px 4px;
+ box-sizing: border-box;
+ width: 100%;
+}
+.adm-model-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1 1 auto;
+ min-width: 0;
+ cursor: pointer;
+}
+.adm-model-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+.adm-model-tools-col {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 auto;
+ margin-left: auto;
+}
+.adm-model-tools-label {
+ font-size: 10px;
+ opacity: 0.45;
+ flex-shrink: 0;
+ user-select: none;
+}
+.adm-model-tool-mode,
+.admin-tools-select {
+ height: 24px;
+ min-height: 24px;
+ line-height: 22px;
+ font-size: 11px;
+ padding: 0 18px 0 6px;
+ width: 124px;
+ max-width: 130px;
+ flex-shrink: 0;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background-color: var(--select-bg);
+ color: var(--select-fg);
+ box-sizing: border-box;
+ vertical-align: middle;
+}
+@media (max-width: 480px) {
+ .adm-model-tool-mode,
+ .admin-tools-select {
+ width: 105px;
+ max-width: 110px;
+ font-size: 10px;
+ padding: 0 14px 0 4px;
+ }
+}
+.adm-model-row:hover .adm-check-dot {
+ border-color: var(--accent, var(--red));
+ transform: scale(1.15);
+}
+.adm-cb-hidden:checked + .adm-check-dot {
+ background: var(--accent, var(--red));
+ border-color: var(--accent, var(--red));
+}
+/* Added Models bulk selection uses the shared dot, without a check glyph. */
+.adm-endpoint-select-dot {
+ flex-basis: 11px !important;
+ width: 11px !important;
+ height: 11px;
+ min-width: 11px;
+ max-width: 11px;
+}
+/* Disabled endpoints — dim the row but keep the toggle/delete buttons
+ at full opacity so the user can re-enable or remove without squinting. */
+.admin-user-row.admin-ep-disabled { opacity: 0.55; }
+
+
+.admin-user-row.adm-ep-just-added {
+ border-radius: 8px;
+ animation: adm-ep-just-added-glow 2.2s ease-out;
+}
+.admin-user-row.admin-ep-disabled .admin-btn-sm,
+.admin-user-row.admin-ep-disabled .admin-btn-delete,
+.admin-user-row.admin-ep-disabled .admin-badge-off { opacity: 1; }
+/* Local / API subsection labels inside the Endpoints card */
+.adm-ep-section-head {
+ display: flex;
+ align-items: center;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--fg);
+ opacity: 0.5;
+ margin-bottom: 4px;
+}
+
+/* Collapsible Add-Models subsections (API / Local) — the whole header row
+ acts as a toggle so a long cloud-API form can be tucked away when you
+ only want to paste a local URL. */
+.adm-section-toggle {
+ cursor: pointer;
+ user-select: none;
+ opacity: 0.8;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 9px;
+ margin-bottom: 6px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ transition: border-color 0.12s, background 0.12s, opacity 0.12s;
+}
+.adm-section-toggle:hover {
+ opacity: 1;
+ border-color: var(--red);
+ background: color-mix(in srgb, var(--red) 8%, transparent);
+}
+.adm-section-toggle .adm-section-caret { opacity: 0.6; }
+/* When expanded, square off the bottom so the header reads as attached to
+ the form it controls. */
+.adm-add-section:not(.collapsed) .adm-section-toggle {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ margin-bottom: 0;
+}
+.adm-section-caret {
+ margin-left: auto;
+ flex-shrink: 0;
+ transition: transform 0.18s ease;
+}
+.adm-quickstart-toggle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+ user-select: none;
+ opacity: 0.72;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px 8px;
+ font-size: 11px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.adm-quickstart-toggle:hover {
+ opacity: 1;
+ border-color: var(--red);
+}
+.adm-quickstart-body {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border: 1px solid var(--border);
+ border-top: 0;
+ border-radius: 0 0 6px 6px;
+ padding: 6px 8px;
+}
+
+/* Custom provider picker (logo + name) replacing the 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: fills the same slot as , plus a small play badge
+ so it's clear the thumbnail represents a video. */
+.gallery-card video {
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+ background: #000;
+ display: block;
+}
+.gallery-card-play {
+ position: absolute;
+ left: 50%; top: 50%;
+ transform: translate(-50%, -50%);
+ width: 36px; height: 36px;
+ display: flex; align-items: center; justify-content: center;
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ border-radius: 50%;
+ pointer-events: none;
+ z-index: 1;
+}
+.gallery-card-play svg { margin-left: 2px; }
+
+.gallery-fav-btn {
+ position: absolute; top: 0px; right: 4px; z-index: 2;
+ background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
+ width: 26px; height: 26px; font-size: 14px;
+ color: rgba(255,255,255,0.6); cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ opacity: 0; transition: opacity 0.15s;
+ padding: 0;
+}
+.gallery-fav-btn.gallery-fav-active { opacity: 1; color: var(--red); }
+
+.gallery-dl-btn {
+ position: absolute; top: 0px; left: 4px; z-index: 2;
+ background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
+ width: 26px; height: 26px;
+ color: rgba(255,255,255,0.75); cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ opacity: 0; transition: opacity 0.15s, background 0.15s;
+ padding: 0;
+}
+.gallery-dl-btn:hover { background: rgba(0,0,0,0.7); color: #fff; }
+/* In select mode the per-thumbnail hover buttons (favorite + download) just
+ get in the way of picking — hide them so the card is a clean select target.
+ `body.gallery-selecting` is the authoritative signal (one class for the
+ whole grid) — the per-card .gallery-card-selectable is kept as a backup
+ in case the body class is missed. */
+body.gallery-selecting .gallery-fav-btn,
+body.gallery-selecting .gallery-dl-btn,
+.gallery-card-selectable .gallery-fav-btn,
+.gallery-card-selectable .gallery-dl-btn {
+ display: none !important;
+}
+
+/* AI tag chips in detail view */
+.gallery-ai-tags { display: flex; gap: 4px; flex-wrap: wrap; }
+.gallery-ai-chip {
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
+ border-radius: 10px; padding: 3px 8px; font-size: 10px;
+ color: var(--fg); opacity: 0.8;
+ font: inherit; font-size: 10px;
+ cursor: pointer;
+ transition: background 0.12s, opacity 0.12s, border-color 0.12s;
+}
+.gallery-ai-chip:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
+}
+/* AI-generated tags: muted neutral with a tiny sparkle marker so the
+ user can tell at a glance these came from the model, not them. */
+.gallery-aitag-chip {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+.gallery-aitag-chip:hover {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 35%, transparent);
+}
+.gallery-aitag-mark {
+ font-size: 8px;
+ opacity: 0.55;
+ color: var(--accent, var(--red));
+}
+/* User-applied tags keep the accent-colored chip — they're the personal/
+ curated tags, so they get the strongest visual weight. */
+.gallery-user-chip {
+ font-weight: 600;
+}
+/* × to remove a user tag (appears inside the chip). */
+.gallery-tag-x {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 5px;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ font-size: 12px;
+ line-height: 1;
+ opacity: 0.55;
+ transition: opacity 0.12s, background 0.12s, color 0.12s;
+}
+.gallery-tag-x:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--red, #ff5555) 22%, transparent);
+ color: var(--red, #ff5555);
+}
+
+.gallery-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ align-content: start;
+ gap: 8px;
+ max-height: calc(92vh - 165px);
+ overflow-y: auto;
+ padding: 2px;
+ transition: border-color 0.15s, background 0.15s;
+}
+.gallery-grid.gallery-dragover {
+ border: 2px dashed var(--red);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--red) 5%, transparent);
+}
+.gallery-card {
+ position: relative;
+ aspect-ratio: 1;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
+}
+.gallery-card.gallery-card-focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 36%, transparent);
+}
+/* Upload-affordance tile pinned to the top-left of the Photos grid. Matches
+ the Upload album tile in the Albums tab — dashed border, centered icon. */
+.gallery-card-upload {
+ border-style: dashed;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ opacity: 0.75;
+ transition: opacity 0.12s, border-color 0.15s, transform 0.15s;
+}
+/* The "Start AI tag" button turns into a Cancel control during a tag run. */
+.gallery-tag-cancelling {
+ color: var(--red) !important;
+ border-color: color-mix(in srgb, var(--red) 45%, var(--border)) !important;
+}
+/* Skeleton/shimmer placeholder tiles shown while the first page of photos loads. */
+.gallery-card-skeleton {
+ cursor: default;
+ background: linear-gradient(100deg,
+ color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
+ color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
+ color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
+ background-size: 200% 100%;
+ animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
+}
+.gallery-card-skeleton:hover { transform: none; box-shadow: none; border-color: var(--border); }
+/* Chat attachment image: shimmer skeleton + centered whirlpool shown while a
+ sent photo uploads / its thumbnail loads (see chatRenderer buildAttachCards). */
+.attach-image-skeleton {
+ position: relative;
+ width: 160px; height: 120px;
+ border-radius: 6px;
+ display: flex; align-items: center; justify-content: center;
+ background: linear-gradient(100deg,
+ color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
+ color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
+ color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
+ background-size: 200% 100%;
+ animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
+}
+.attach-image-skeleton > .spinner-whirlpool { margin: 0 !important; }
+
+/* Corner "Aa" button on a chat photo thumbnail — opens the vision/OCR editor
+ so the user can correct what the vision model fed to the LLM. */
+.attach-image-preview { position: relative; }
+.attach-vision-model,
+.attach-image-name {
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.attach-vision-model {
+ font-size: 10px;
+ color: var(--accent, var(--red));
+ opacity: 0.85;
+ margin-top: 3px;
+}
+.attach-image-name {
+ font-size: 10px;
+ opacity: 0.5;
+ margin-top: 1px;
+}
+.attach-ocr-btn {
+ position: absolute; top: 4px; right: 4px;
+ z-index: 5; /* sit above msg-action overlays so the click lands here */
+ height: 22px;
+ background: rgba(0,0,0,0.55); color: #fff;
+ border: 1px solid rgba(255,255,255,0.2); border-radius: 4px;
+ padding: 0 8px 0 6px; cursor: pointer;
+ display: inline-flex; align-items: center; justify-content: center; gap: 5px;
+ font-size: 11px; font-weight: 500; line-height: 1;
+ opacity: 0.75; transition: opacity 0.15s;
+}
+.attach-ocr-btn:hover { opacity: 1; }
+.attach-ocr-btn svg { display: block; }
+/* On mobile keep just the (larger) edit icon — the "Caption" label crowds
+ the corner of the small chat photo. */
+@media (max-width: 768px) {
+ .attach-ocr-btn .attach-ocr-label { display: none; }
+ .attach-ocr-btn {
+ width: 28px; height: 28px;
+ padding: 0;
+ }
+ .attach-ocr-btn svg { width: 16px; height: 16px; }
+}
+
+/* Vision/OCR text editor modal — opened from the "Aa" button above. */
+.vision-editor-overlay {
+ position: fixed; inset: 0; z-index: 9999;
+ background: rgba(0,0,0,0.55);
+ display: flex; align-items: center; justify-content: center;
+ padding: 16px;
+}
+.vision-editor-panel {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 14px 16px;
+ width: min(560px, 92vw);
+ max-height: 88vh;
+ display: flex; flex-direction: column; gap: 8px;
+ box-shadow: 0 14px 40px rgba(0,0,0,0.4);
+}
+.vision-editor-title {
+ font-size: 13px; font-weight: 600; color: var(--fg);
+ display: flex; align-items: center; gap: 6px;
+}
+.vision-editor-desc { font-size: 11px; opacity: 0.6; line-height: 1.4; }
+.vision-editor-text {
+ width: 100%; min-height: 180px;
+ background: var(--panel); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px;
+ padding: 8px 10px;
+ font-family: inherit; font-size: 12px; line-height: 1.5;
+ resize: vertical; outline: none;
+}
+.vision-editor-text:focus { border-color: var(--accent-primary, var(--red)); }
+.vision-editor-hint { font-size: 10.5px; opacity: 0.55; line-height: 1.4; }
+.vision-editor-actions {
+ display: flex; gap: 8px; justify-content: flex-end;
+ margin-top: 4px;
+}
+.vision-editor-btn {
+ background: var(--panel); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px;
+ padding: 6px 14px; font-size: 12px; cursor: pointer;
+ transition: border-color 0.15s, background 0.15s;
+ display: inline-flex; align-items: center; gap: 5px;
+}
+.vision-editor-btn svg { display: block; }
+/* Optical nudge: the button text sits 1px higher than the SVG glyphs
+ alongside it. Drop the text down 1px so they line up. */
+.vision-editor-btn .vision-btn-label { position: relative; top: 1px; }
+.vision-editor-btn:hover { border-color: color-mix(in srgb, var(--fg) 40%, var(--border)); }
+.vision-editor-btn-primary {
+ background: var(--accent-primary, var(--red));
+ color: #fff; border-color: transparent;
+}
+.vision-editor-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
+
+/* Quick full-size lightbox shown when the user taps a chat photo thumbnail. */
+.attach-lightbox {
+ position: fixed; inset: 0; z-index: 9998;
+ background: rgba(0, 0, 0, 0.85);
+ display: flex; align-items: center; justify-content: center;
+ padding: 16px; cursor: zoom-out;
+ animation: attach-lightbox-fade 0.12s ease-out;
+}
+.attach-lightbox img {
+ max-width: 100%; max-height: 100%;
+ border-radius: 6px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ object-fit: contain;
+}
+.attach-lightbox-err {
+ position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
+ background: var(--bg); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px;
+ padding: 6px 10px; font-size: 12px;
+}
+@keyframes attach-lightbox-fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.gallery-card-upload:hover {
+ opacity: 1;
+ border-color: var(--red);
+ transform: translateY(-1px);
+}
+.gallery-card-upload-inner {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ color: var(--fg);
+}
+.gallery-card-upload-label {
+ font-size: 12px;
+ font-weight: 500;
+ opacity: 0.8;
+}
+.gallery-card:hover {
+ border-color: var(--red);
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--red) 15%, transparent);
+ transform: translateY(-1px);
+}
+.gallery-card-overlay {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 8px;
+ background: linear-gradient(180deg, rgba(0,0,0,0.42), transparent 34%, rgba(0,0,0,0.22));
+ color: #fff;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s;
+}
+.gallery-card-badges {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ justify-content: flex-end;
+ margin-top: auto;
+ margin-bottom: 34px;
+}
+.gallery-card-badge {
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: rgba(0, 0, 0, 0.55);
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1.1;
+}
+.gallery-card-badge-fav {
+ color: #ff5f6f;
+}
+.gallery-select-dot {
+ position: absolute; top: 6px; left: 6px; z-index: 2;
+ display: block;
+ width: 10px; height: 10px; border-radius: 50%; cursor: pointer;
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+ border: 1px solid color-mix(in srgb, var(--fg) 50%, transparent);
+ transition: background 0.15s;
+}
+.gallery-select-dot.selected { background: var(--red); border-color: var(--red); }
+.gallery-select-dot:hover { background: color-mix(in srgb, var(--red) 50%, transparent); }
+.gallery-select-btn {
+ padding: 9px 10px 11px; background: transparent; color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
+ font-size: 11px; font-family: inherit; opacity: 0.6; transition: all 0.15s;
+ margin-top: -4px !important;
+}
+.gallery-select-btn:hover { opacity: 1; }
+/* Match the library Select toggle's red tint (.memory-toolbar-btn.active).
+ Bumped contrast so the toggled state actually reads on mobile — the prior
+ 15% red on transparent was nearly invisible against the page background. */
+.gallery-select-btn.active {
+ background: color-mix(in srgb, var(--red) 28%, transparent);
+ color: var(--red);
+ border-color: var(--red);
+ font-weight: 600;
+ opacity: 1;
+}
+/* Toolbar action buttons sit 2px lower than the generic select-btn so
+ they baseline-align with the toolbar's selects/inputs instead of
+ floating above them. */
+.gallery-toolbar-action {
+ margin-top: 0 !important;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+.gallery-bulk-bar {
+ display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 12px;
+ justify-content: flex-end;
+}
+.gallery-bulk-bar.hidden { display: none; }
+.gallery-bulk-all {
+ display: inline-flex; align-items: center; gap: 4px;
+ font-size: 11px; opacity: 0.75; cursor: pointer; margin-right: auto;
+}
+.gallery-bulk-all input { accent-color: var(--accent, var(--red)); cursor: pointer; }
+.gallery-bulk-delete {
+ padding: 3px 10px; background: transparent; color: var(--red);
+ border: 1px solid var(--red); border-radius: 4px; cursor: pointer;
+ font-size: 11px; font-family: inherit;
+}
+.gallery-bulk-delete:hover { background: var(--red); color: #fff; }
+.gallery-bulk-cancel {
+ padding: 3px 10px; background: transparent; color: var(--fg);
+ border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
+ font-size: 11px; font-family: inherit; opacity: 0.6;
+}
+.gallery-bulk-cancel:hover { opacity: 1; }
+.gallery-card img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+.gallery-card-info {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 6px 8px;
+ background: linear-gradient(transparent, rgba(0,0,0,0.75));
+ color: #fff;
+}
+.gallery-card-prompt {
+ font-size: 10px;
+ line-height: 1.3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.gallery-card-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 2px;
+ font-size: 9px;
+ opacity: 0.8;
+}
+.gallery-card-model {
+ padding: 1px 5px;
+ border-radius: 6px;
+ background: rgba(255,255,255,0.15);
+}
+.gallery-empty {
+ text-align: center;
+ color: color-mix(in srgb, var(--fg) 35%, transparent);
+ padding: 32px 16px;
+ font-size: 12px;
+ font-style: italic;
+}
+.gallery-load-more {
+ display: block;
+ margin: 10px auto 0;
+ padding: 6px 16px;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ transition: border-color 0.15s, color 0.15s;
+}
+.gallery-load-more:hover {
+ border-color: var(--red);
+ color: var(--red);
+}
+
+/* Gallery detail overlay */
+.gallery-detail {
+ position: absolute;
+ inset: 0;
+ background: var(--panel);
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+}
+.gallery-detail-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px 4px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ /* Pin to top so a tall (portrait) photo can't cover the Back / ⋮
+ menu when the detail body grows past the visible area. */
+ position: sticky;
+ top: 0;
+ background: var(--panel);
+ /* Above the image's rotate/nav buttons (z-index:3) so they scroll cleanly
+ behind the header instead of colliding with it. */
+ z-index: 5;
+ min-height: 0;
+}
+.gallery-detail-title {
+ max-width: min(360px, 40vw);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+ font-weight: 600;
+ opacity: 0.82;
+ position: relative;
+ top: 1px;
+}
+.gallery-detail-back {
+ background: none;
+ border: none;
+ color: var(--fg);
+ cursor: pointer;
+ font: inherit;
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ position: relative;
+ top: -2px;
+}
+/* Slightly larger header icons (back/edit/heart/⋮) — override the inline 14px. */
+.gallery-detail-header svg { width: 16px; height: 16px; }
+.gallery-detail-back:hover {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.gallery-detail-action {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ transition: border-color 0.15s;
+}
+.gallery-detail-action:hover {
+ border-color: var(--fg);
+}
+.gallery-detail-action.danger {
+ color: var(--color-error, #e55);
+}
+/* Overflow menu — replaces the seven individual action buttons that used
+ to crowd the right side of the detail header. */
+.gallery-detail-menu-wrap { position: relative; }
+.gallery-detail-menu-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 26px;
+ padding: 0;
+ position: relative;
+ top: -2px;
+}
+.gallery-detail-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ right: 0;
+ min-width: 150px;
+ padding: 3px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+ z-index: 12;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+.gallery-detail-menu[hidden] { display: none; }
+.gallery-detail-menu .dropdown-item-compact {
+ width: 100%;
+ background: none;
+ border: none;
+ text-align: left;
+ font: inherit;
+ /* Tighter than the global default — the photo-detail menu has 8
+ items so every pixel of vertical padding adds up. */
+ padding: 4px 8px;
+ font-size: 11px;
+ gap: 8px;
+ line-height: 1.2;
+}
+.gallery-detail-menu .dropdown-item-compact .dropdown-icon,
+.gallery-detail-menu .dropdown-item-compact .dropdown-icon svg {
+ width: 12px;
+ height: 12px;
+}
+.gallery-detail-body {
+ display: flex;
+ gap: 16px;
+ padding: 12px;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+.gallery-detail-image {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+ min-height: 0;
+ height: 100%;
+ padding-inline: 56px;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+/* Shrink-wraps the actual image so overlay children (heart, face boxes)
+ anchor to the IMAGE bounds, not the wider letterboxed wrapper. */
+.gallery-detail-img-frame {
+ position: relative;
+ display: inline-flex;
+ max-width: 100%;
+ max-height: 100%;
+}
+.gallery-detail-nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(0, 0, 0, 0.45);
+ color: rgba(255, 255, 255, 0.85);
+ border: none;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s, background 0.15s;
+ z-index: 3;
+ padding: 0;
+}
+.gallery-detail-nav:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
+.gallery-detail-nav-prev { left: 8px; }
+.gallery-detail-nav-next { right: 8px; }
+
+/* Rotate buttons — same pattern as the prev/next arrows but pinned to the
+ top corners. Hover-revealed so they don't clutter the image. */
+.gallery-detail-rotate {
+ position: absolute;
+ top: 8px;
+ background: rgba(0, 0, 0, 0.45);
+ color: rgba(255, 255, 255, 0.85);
+ border: none;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s, background 0.15s;
+ z-index: 3;
+ padding: 0;
+}
+.gallery-detail-rotate:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
+.gallery-detail-rotate-ccw { left: 8px; }
+.gallery-detail-rotate-cw { right: 8px; }
+/* Inline heart that lives next to the "Date" label in the sidebar.
+ Tiny outline icon by default; fills red when favorited. */
+.gallery-detail-date-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 6px;
+}
+.gallery-detail-fav-inline {
+ background: none;
+ border: none;
+ padding: 0;
+ /* Nudge up 8px so the heart sits visually above the Date label
+ baseline rather than centred on it. */
+ margin-top: -8px;
+ border-radius: 4px;
+ color: var(--fg-muted);
+ opacity: 0.6;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+ transition: opacity 0.15s, color 0.15s;
+}
+/* Shrink the SVG so the heart matches the height of the small uppercase
+ Date label sitting next to it. */
+.gallery-detail-fav-inline svg {
+ width: 12px;
+ height: 12px;
+ display: block;
+}
+.gallery-detail-fav-inline:hover {
+ opacity: 1;
+}
+.gallery-detail-fav-inline.active {
+ opacity: 1;
+ color: var(--red);
+}
+.gallery-detail-sidebar {
+ width: 290px;
+ flex-shrink: 0;
+ /* Vertical scroll for long metadata, but never horizontal — overflow:auto
+ alone would let a wide child (e.g. long album name in the select) push
+ the sidebar into horizontal-scroll mode. */
+ overflow-x: hidden;
+ overflow-y: auto;
+ min-width: 0;
+}
+.gallery-detail-inspector-head {
+ padding-bottom: 10px;
+ margin-bottom: 10px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+}
+.gallery-detail-eyebrow {
+ font-size: 9px;
+ font-weight: 700;
+ color: var(--accent, var(--red));
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-bottom: 3px;
+}
+.gallery-detail-inspector-title {
+ font-size: 14px;
+ font-weight: 650;
+ line-height: 1.25;
+ word-break: break-word;
+}
+.gallery-detail-meta-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+ margin: 4px 0 12px;
+}
+.gallery-detail-meta-card {
+ min-width: 0;
+ border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ border-radius: 7px;
+ padding: 7px 8px;
+}
+.gallery-detail-meta-card span {
+ display: block;
+ font-size: 9px;
+ font-weight: 700;
+ color: color-mix(in srgb, var(--fg) 48%, transparent);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 3px;
+}
+.gallery-detail-meta-card strong {
+ display: block;
+ min-width: 0;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.25;
+ word-break: break-word;
+}
+.gallery-detail-inline-link {
+ appearance: none;
+ background: none;
+ border: none;
+ color: var(--accent, var(--red));
+ padding: 0;
+ font: inherit;
+ font-weight: inherit;
+ cursor: pointer;
+ text-align: left;
+}
+.gallery-detail-mini-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 7px;
+}
+.gallery-detail-mini-btn {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px 8px;
+ font: inherit;
+ font-size: 10.5px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+.gallery-detail-mini-btn:hover {
+ border-color: color-mix(in srgb, var(--fg) 42%, var(--border));
+}
+/* Constrain any child that could otherwise stretch the sidebar wider than
+ its declared width (selects with long option text, long URLs, etc.). */
+.gallery-detail-sidebar select,
+.gallery-detail-sidebar input,
+.gallery-detail-sidebar .gallery-detail-prompt {
+ max-width: 100%;
+ box-sizing: border-box;
+}
+.gallery-date-rel {
+ opacity: 0.55;
+ margin-left: 4px;
+ font-size: 11px;
+ font-style: italic;
+}
+.gallery-detail-section label {
+ display: block;
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.6;
+ margin-bottom: 3px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.gallery-detail-section div {
+ font-size: 12px;
+ word-break: break-word;
+}
+.gallery-detail-prompt {
+ white-space: pre-wrap;
+ line-height: 1.4;
+}
+.gallery-tag-input,
+.gallery-detail-name-input {
+ width: 100%;
+ padding: 5px 8px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 11px;
+ margin-bottom: 4px;
+}
+.gallery-tag-input:focus,
+.gallery-detail-name-input:focus {
+ border-color: var(--red);
+ outline: none;
+}
+/* ↵ enter hint inside the Add-a-tag field (accent), replacing the "press Enter"
+ placeholder text. */
+.gallery-tag-input-wrap { position: relative; }
+.gallery-tag-enter-hint {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-top: -2px; /* the input has 4px bottom margin; center on the box */
+ color: var(--accent, var(--red));
+ font-size: 13px;
+ line-height: 1;
+ pointer-events: none;
+ opacity: 0.8;
+}
+.gallery-detail-name-input { font-size: 13px; font-weight: 500; }
+/* ↵ enter hint on the image name field — accent, shown while editing to signal
+ that Enter saves. */
+.gallery-name-wrap { position: relative; }
+.gallery-name-enter {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-top: -2px;
+ color: var(--accent, var(--red));
+ opacity: 0.85;
+ pointer-events: none;
+ display: none;
+}
+.gallery-tag-save {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ font: inherit;
+ font-size: 10px;
+ padding: 3px 10px;
+ border-radius: 4px;
+ transition: border-color 0.15s, color 0.15s;
+}
+.gallery-tag-save:hover {
+ border-color: var(--red);
+ color: var(--red);
+}
+
+@media (max-width: 600px) {
+ .gallery-grid {
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ gap: 6px;
+ }
+ /* Single-row toolbar on mobile: search + Sources + Recent (sort) all share
+ one row. The search bar shrinks so the dropdowns fit beside it. */
+ .gallery-toolbar {
+ flex-wrap: nowrap;
+ gap: 4px;
+ }
+ .gallery-search-wrap {
+ flex: 1 1 0;
+ min-width: 0;
+ }
+ .gallery-search {
+ /* Drop the desktop right-padding reserved for the "enter to tag" hint —
+ hide the hint on mobile (below) so the input can use its full width. */
+ padding-right: 8px;
+ }
+ .gallery-search-enter-hint { display: none; }
+ /* Disable the row break so the filters stay inline with search. */
+ .gallery-toolbar-break { display: none; }
+ .gallery-model-filter,
+ .gallery-sort {
+ flex: 0 0 auto;
+ /* Cap each dropdown so search keeps a usable amount of width. */
+ max-width: 100px;
+ font-size: 11px;
+ padding: 4px 6px;
+ /* Keep the same vertical alignment as the desktop toolbar. */
+ position: relative;
+ top: 4px;
+ }
+ .gallery-select-btn {
+ padding: 6px 8px 8px;
+ font-size: 9px;
+ flex-shrink: 0;
+ /* Push Select to the far right of the single-row toolbar, regardless
+ of its DOM position (sits between search and the dropdowns). */
+ order: 99;
+ margin-left: auto;
+ }
+ /* Tabs scroll horizontally rather than wrapping when narrow. */
+ .gallery-tabs {
+ overflow-x: auto;
+ flex-wrap: nowrap;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .gallery-tabs::-webkit-scrollbar { display: none; }
+ .gallery-tab { flex-shrink: 0; }
+ /* Album chips also scroll horizontally so they don't stack. */
+ .gallery-album-chips {
+ overflow-x: auto;
+ flex-wrap: nowrap;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding-bottom: 4px;
+ }
+ .gallery-album-chips::-webkit-scrollbar { display: none; }
+ .gallery-chip { flex-shrink: 0; }
+ /* Tasks filter chips — same horizontal-swipe behaviour as the gallery
+ albums / library chips on mobile: one row, slide-to-see-more. */
+ .tasks-activity-filters {
+ overflow-x: auto;
+ flex-wrap: nowrap !important;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding-bottom: 4px;
+ }
+ .tasks-activity-filters::-webkit-scrollbar { display: none; }
+ .tasks-activity-filters > * { flex-shrink: 0; }
+ /* Detail view */
+ .gallery-detail-body {
+ flex-direction: column;
+ overflow: visible;
+ }
+ .gallery-detail-image {
+ flex: 0 0 auto;
+ width: 100%;
+ height: clamp(280px, 58vh, 520px);
+ padding-inline: 48px;
+ }
+ .gallery-detail-sidebar {
+ width: 100%;
+ }
+ .gallery-detail-title {
+ display: none;
+ }
+ .gallery-detail-meta-grid {
+ grid-template-columns: 1fr;
+ }
+ /* Album grid: smaller tile minimum on mobile. */
+ .gallery-albums-grid {
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 8px;
+ }
+}
+
+
+/* ── Scoreboard table ── */
+.scoreboard-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.88em;
+}
+.scoreboard-table th {
+ text-align: left;
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--border);
+ font-weight: 600;
+ font-size: 0.85em;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+.scoreboard-table th:not(:first-child) {
+ text-align: center;
+}
+.scoreboard-table td {
+ padding: 5px 10px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+ text-align: center;
+}
+.scoreboard-table td.scoreboard-model {
+ text-align: left;
+ font-weight: 500;
+ max-width: 200px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.scoreboard-table td.scoreboard-pct {
+ font-weight: 600;
+ color: var(--red);
+}
diff --git a/static/css/10-cookbook.css b/static/css/10-cookbook.css
new file mode 100644
index 000000000..8acded779
--- /dev/null
+++ b/static/css/10-cookbook.css
@@ -0,0 +1,4324 @@
+/* 10-cookbook - 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.
+ */
+.scoreboard-table tbody tr:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+
+#scoreboard-overlay svg {
+ color: var(--accent, var(--red));
+}
+
+/* ── Compare search results ── */
+.compare-search-results {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.compare-search-result {
+ padding: 6px 0;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
+}
+.compare-search-result:last-child {
+ border-bottom: none;
+}
+.search-result-title {
+ color: var(--color-accent);
+ text-decoration: none;
+ font-weight: 500;
+ font-size: 0.9em;
+ display: block;
+ line-height: 1.3;
+}
+.search-result-title:hover {
+ color: var(--color-link-hover);
+ text-decoration: underline;
+}
+.search-result-snippet {
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ font-size: 0.82em;
+ line-height: 1.4;
+ margin-top: 2px;
+}
+.search-result-url {
+ color: color-mix(in srgb, var(--fg) 35%, transparent);
+ font-size: 0.75em;
+ margin-top: 3px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+
+/* ── Cookbook Tool ── */
+
+/* ── Cookbook ── */
+.cookbook-serve-preset {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ margin-bottom: 3px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+.cookbook-serve-preset:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
+.cookbook-serve-preset-name {
+ font-size: 11px;
+ font-weight: 500;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cookbook-serve-preset-meta {
+ font-size: 9px;
+ font-family: 'Fira Code', monospace;
+ color: var(--fg-muted);
+ opacity: 0.5;
+}
+.cookbook-serve-preset-launch {
+ background: none;
+ border: none;
+ color: #50fa7b;
+ cursor: pointer;
+ font-size: 10px;
+ opacity: 0.6;
+ padding: 0 2px;
+ position: relative;
+ top: -4px;
+}
+.cookbook-serve-preset-launch:hover { opacity: 1; }
+.cookbook-serve-preset-rm {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ font-size: 10px;
+ opacity: 0.3;
+ padding: 0 2px;
+ position: relative;
+ top: -4px;
+}
+.cookbook-serve-preset-rm:hover { opacity: 0.8; color: var(--red); }
+.cookbook-body {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow-y: auto;
+ flex: 1;
+ min-height: 0;
+ scrollbar-width: thin;
+}
+#cookbook-modal .modal-content {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+#cookbook-modal .modal-header {
+ flex: 0 0 auto;
+}
+#cookbook-modal .cookbook-body {
+ min-height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+#cookbook-modal .cookbook-group {
+ min-height: 0;
+}
+#cookbook-modal .cookbook-group > .admin-card {
+ min-height: 0;
+ /* Let .cookbook-body be the SINGLE scroll surface. Nesting another
+ overflow:auto here trapped the wheel inside the cached-list when a
+ serve panel expanded — the page couldn't scroll past the panel's
+ bottom (Launch button got hidden). */
+ overflow: visible !important;
+}
+#cookbook-modal .cookbook-section-body {
+ min-height: 0;
+}
+.cookbook-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ flex: 1;
+ min-height: 0;
+}
+
+/* Cards — match admin-card */
+.cookbook-card {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.cookbook-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.cookbook-card-header-actions { display: flex; gap: 4px; }
+.cookbook-card-title { font-size: 13px; font-weight: 600; }
+.cookbook-card-desc {
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+
+/* Fields grid */
+.cookbook-fields {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 6px;
+ margin-top: 4px;
+}
+.cookbook-field-label {
+ display: flex;
+ flex-direction: column;
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ gap: 3px;
+}
+
+/* Inputs — match admin-add-form input */
+.cookbook-field-input {
+ padding: 5px 8px;
+ font-size: 12px;
+ font-family: inherit;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ outline: none;
+}
+.cookbook-field-input:focus { border-color: var(--red); }
+#hwfit-dl-server, #hwfit-usecase, #hwfit-server-select, #hwfit-cache-server, #serve-sort { height: 28px; width: 88px; }
+/* Serve tab — match Documents sizing, but leave room for the active "Cancel"
+ label and center it vertically so the descenders don't clip. */
+#hwfit-cache-select {
+ min-width: 58px;
+ height: 28px;
+ position: relative;
+ top: -3px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+}
+
+/* Command preview */
+.cookbook-cmd-preview {
+ margin: 4px 0 0;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: 'Fira Code', monospace;
+ font-size: 11px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: var(--fg);
+}
+
+/* Buttons — match admin-btn-sm */
+.cookbook-actions { display: flex; gap: 6px; margin-top: 2px; }
+.cookbook-btn {
+ padding: 4px 10px;
+ min-width: 54px;
+ text-align: center;
+ font-size: 11px;
+ font-family: inherit;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+ color: var(--fg);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.cookbook-btn:hover {
+ background: var(--border);
+ border-color: var(--accent);
+}
+.cookbook-btn:disabled { opacity: 0.5; cursor: not-allowed; }
+.cookbook-run-btn {
+ background: var(--accent);
+ color: var(--panel);
+ border-color: var(--accent);
+ font-weight: 600;
+}
+.cookbook-run-btn:hover { opacity: 0.85; }
+.cookbook-stop-btn {
+ background: var(--color-error);
+ border-color: var(--color-error);
+}
+
+/* Output */
+.cookbook-output-wrap {
+ position: relative;
+ margin: 0;
+}
+.cookbook-output-kill {
+ position: absolute;
+ top: 4px;
+ right: 34px;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ color: var(--fg-muted);
+ font-size: 16px;
+ line-height: 1;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity .15s, color .15s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cookbook-output-kill:hover { color: var(--color-error, var(--warn)); border-color: var(--border); }
+.cookbook-output-wrap .copy-code {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+}
+.cookbook-output-wrap:hover .copy-code { opacity: 0.7; }
+.cookbook-output-pre {
+ margin: 0;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: 'Fira Code', monospace;
+ font-size: 11px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ max-height: 180px;
+ overflow-y: auto;
+}
+.cookbook-output-error { color: var(--color-error); }
+
+/* Downloaded/cached models */
+.hwfit-cached-item {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ margin: 2px 0;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ cursor: default;
+ font-size: 12px;
+}
+.hwfit-cached-item .hwfit-serve-panel {
+ flex-basis: 100%;
+ margin-top: 4px;
+}
+.hwfit-cached-item:hover {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+.hwfit-cached-header {
+ font-size: 10px;
+ opacity: 0.4;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border: none !important;
+ background: none !important;
+ padding: 2px 8px;
+ cursor: default !important;
+}
+.hwfit-cached-header:hover { background: none !important; }
+.hwfit-cached-name {
+ font-weight: 600;
+ flex-shrink: 0;
+}
+.hwfit-cached-repo {
+ flex: 1;
+ color: var(--fg-muted);
+ font-size: 11px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.hwfit-cached-size,
+.hwfit-cached-files {
+ font-size: 10px;
+ color: var(--fg-muted);
+ white-space: nowrap;
+}
+.hwfit-cached-serve,
+.hwfit-cached-delete {
+ flex-shrink: 0;
+}
+.hwfit-cached-delete {
+ opacity: 0.4;
+ font-size: 10px;
+}
+.hwfit-cached-delete:hover {
+ opacity: 1;
+ color: var(--color-error, var(--warn));
+}
+.hwfit-cached-badge {
+ font-size: 9px;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-weight: 600;
+ white-space: nowrap;
+}
+.hwfit-cached-ready {
+ background: color-mix(in srgb, var(--color-success, #4caf50) 20%, transparent);
+ color: var(--color-success, #4caf50);
+}
+.hwfit-cached-dl {
+ background: color-mix(in srgb, var(--color-warning, #f0ad4e) 20%, transparent);
+ color: var(--color-warning, #f0ad4e);
+}
+
+/* Cookbook notification dot */
+#tool-cookbook-btn {
+ position: relative;
+}
+#rail-cookbook {
+ position: relative;
+}
+.cookbook-open-loading {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+ margin-left: 2px;
+ color: var(--accent, var(--red));
+ opacity: 0.9;
+}
+.cookbook-open-loading .ai-spinner,
+.cookbook-open-loading .ai-spinner-whirlpool {
+ display: inline-flex !important;
+ width: 14px;
+ height: 14px;
+ margin: 0 !important;
+ gap: 0 !important;
+}
+.cookbook-open-loading canvas {
+ display: block;
+}
+.cookbook-section-loading-wp,
+.cookbook-section-loading-wp canvas {
+ width: 22px !important;
+ height: 22px !important;
+ display: block !important;
+}
+#rail-cookbook .cookbook-open-loading {
+ position: absolute;
+ right: 2px;
+ top: -2px;
+ width: 12px;
+ height: 12px;
+ margin-left: 0;
+}
+/* The serve-list 'downloading' / 'download stalled' status text sits a hair
+ low against the rest of the meta line on mobile — nudge it up 2px. */
+@media (max-width: 768px) {
+ .cookbook-dl-status {
+ position: relative;
+ top: -2px;
+ }
+}
+.cookbook-notif-dot {
+ width: 6px;
+ height: 6px;
+ flex-shrink: 0;
+ border-radius: 50%;
+ background: var(--color-success, #4caf50);
+ /* Drives the breathing glow in the keyframes (matches email's dot). */
+ --notif-glow: var(--color-success, #4caf50);
+ animation: cookbook-notif-pulse 2s ease-in-out infinite;
+}
+.cookbook-notif-dot.cookbook-notif-error {
+ background: var(--color-error, #f44);
+ --notif-glow: var(--color-error, #f44);
+ position: relative;
+ left: -2px;
+ top: -1px;
+}
+.rail-notify-error { color: var(--color-error, #f44) !important; }
+.cookbook-tab-error-dot {
+ display: inline-block; width: 5px; height: 5px; border-radius: 50%;
+ background: var(--color-error, #f44); margin-left: 4px; vertical-align: middle;
+ animation: cookbook-notif-pulse 2s ease-in-out infinite;
+}
+@keyframes cookbook-notif-pulse {
+ 0%, 100% {
+ opacity: 1;
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--notif-glow, var(--accent, var(--red))) 60%, transparent);
+ }
+ 50% {
+ opacity: 0.85;
+ box-shadow: 0 0 6px 2px color-mix(in srgb, var(--notif-glow, var(--accent, var(--red))) 55%, transparent);
+ }
+}
+.cookbook-clear-btn {
+ font-size: 10px !important;
+ padding: 0 8px !important;
+ height: 22px !important;
+ border-radius: 6px !important;
+ border: 1px solid var(--border) !important;
+ color: color-mix(in srgb, var(--fg) 40%, transparent) !important;
+ opacity: 1 !important;
+ background: none !important;
+}
+.cookbook-clear-btn:hover {
+ color: var(--red) !important;
+ border-color: var(--red) !important;
+ background: color-mix(in srgb, var(--red) 8%, transparent) !important;
+}
+.cookbook-bulk-bar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ margin-bottom: 4px;
+ background: color-mix(in srgb, var(--red) 6%, transparent);
+ border: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
+ border-radius: 6px;
+ font-size: 11px;
+}
+.cookbook-bulk-bar.hidden { display: none; }
+.serve-select-cb {
+ width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; cursor: pointer;
+ background: var(--border); transition: background 0.15s;
+ align-self: center;
+ position: relative;
+ top: -2px;
+}
+#serve-bulk-bar {
+ position: relative;
+ top: -6px;
+}
+#serve-bulk-bar #serve-bulk-cancel {
+ top: -2px !important;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 28px;
+ height: 24px;
+ line-height: 1;
+}
+#serve-bulk-bar #serve-bulk-cancel svg {
+ top: 0;
+}
+.serve-select-cb.selected { background: var(--red); }
+.serve-select-cb:hover { background: color-mix(in srgb, var(--red) 50%, transparent); }
+#hwfit-cache-select.active {
+ background: var(--red);
+ color: #fff;
+ border-color: var(--red);
+}
+.cookbook-serve-dirs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+}
+.cookbook-serve-dir-pill {
+ font-size: 10px;
+ font-family: 'Fira Code', monospace;
+ padding: 2px 8px;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+ color: var(--fg-muted);
+ letter-spacing: 0.2px;
+}
+/* Status pills shown inline in a Serve-tab card title (next to the model
+ name) when the model has a live serve / download task. Shared base
+ class so "running" and "downloading" sit on the same row with the
+ same chrome; only color varies. */
+.cookbook-serve-running-pill,
+.cookbook-serve-downloading-pill {
+ display: inline-block;
+ margin-left: 6px;
+ padding: 1px 7px;
+ border-radius: 10px;
+ font-size: 9px;
+ font-weight: 500;
+ text-transform: lowercase;
+ letter-spacing: 0.3px;
+ vertical-align: 2px;
+ position: relative;
+ top: 1px; /* nudged down 2px from the old -1px so it sits
+ flush with the cap-height of the title text */
+}
+.cookbook-serve-running-pill {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+}
+.cookbook-serve-downloading-pill {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+ opacity: 0.85;
+}
+.cookbook-serve-downloading-pill.is-stalled {
+ /* Stalled downloads stay visible but read as warning, not progress. */
+ color: var(--fg-muted, #888);
+ background: color-mix(in srgb, var(--fg-muted, #888) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg-muted, #888) 30%, transparent);
+ opacity: 1;
+}
+.cookbook-serve-running-pill.is-clickable {
+ cursor: pointer;
+ transition: background 0.12s, border-color 0.12s;
+}
+.cookbook-serve-running-pill.is-clickable:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+}
+
+.cookbook-serve-dir-edit {
+ font-size: 9px;
+ color: var(--fg-muted);
+ opacity: 0.4;
+ cursor: pointer;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+.cookbook-serve-dir-edit:hover {
+ opacity: 0.7;
+ color: var(--accent, var(--red));
+}
+.cookbook-gpu-group {
+ display: flex;
+ gap: 3px;
+ margin-top: -4px;
+}
+.cookbook-gpu-btn {
+ width: 30px; height: 30px;
+ font-size: 12px;
+ font-family: 'Fira Code', monospace;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ opacity: 0.4;
+ transition: all 0.15s;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cookbook-gpu-btn:hover {
+ opacity: 0.7;
+ border-color: var(--fg-muted);
+}
+.cookbook-gpu-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 55%, transparent);
+ color: var(--fg);
+ font-weight: 600;
+}
+/* Probe annotation states (set by "Free?" button) */
+.cookbook-gpu-btn.gpu-free {
+ opacity: 1;
+ border-color: #4ade80;
+ color: #4ade80;
+}
+.cookbook-gpu-btn.gpu-busy {
+ opacity: 0.85;
+ border-color: color-mix(in srgb, var(--red) 70%, transparent);
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 8%, transparent);
+}
+.cookbook-gpu-btn.gpu-missing {
+ opacity: 0.2;
+ text-decoration: line-through;
+ cursor: not-allowed;
+}
+/* "Clear Server" lives in the actions row next to "Probe GPUs".
+ Inherits .cookbook-btn sizing; this rule just adds the destructive
+ red tint on hover so the "this kills processes" cue stays. */
+.cookbook-gpu-clear:hover:not(:disabled) {
+ color: var(--red);
+ border-color: color-mix(in srgb, var(--red) 60%, var(--border));
+ background: color-mix(in srgb, var(--red) 8%, transparent);
+}
+.cookbook-gpu-clear:disabled { opacity: 0.4; cursor: wait; }
+.cookbook-gpu-clear:disabled { opacity: 0.4; cursor: wait; }
+/* GPU probe popup — per-GPU process list with kill buttons */
+.cookbook-gpu-popup {
+ /* Fixed positioning (relative to viewport) so we never get pulled into
+ a scrolling/transform stacking context from an ancestor. Z-index has
+ to clear the cookbook modal (260) and the rest of the high-z UI
+ layers (themed-confirm and various overlays sit around 9000-10000). */
+ position: fixed;
+ z-index: 10010;
+ min-width: 280px;
+ max-width: 420px;
+ background: var(--panel, #1a1a1a);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
+ font-family: 'Fira Code', monospace;
+ font-size: 11px;
+ color: var(--fg);
+}
+.cookbook-gpu-popup-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ border-bottom: 1px solid var(--border);
+ font-weight: 600;
+}
+.cookbook-gpu-popup-stats {
+ flex: 1;
+ font-weight: 400;
+ color: var(--fg-muted);
+ font-size: 10px;
+}
+.cookbook-gpu-popup-close {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 4px;
+}
+.cookbook-gpu-popup-close:hover { color: var(--fg); }
+.cookbook-gpu-popup-body {
+ padding: 4px 0;
+ max-height: 320px;
+ overflow-y: auto;
+}
+.cookbook-gpu-popup-empty {
+ padding: 10px;
+ color: var(--fg-muted);
+ font-style: italic;
+}
+.cookbook-gpu-proc {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+}
+.cookbook-gpu-proc:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
+.cookbook-gpu-proc-info {
+ flex: 1;
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ min-width: 0;
+}
+.cookbook-gpu-proc-pid {
+ color: var(--fg-muted);
+ width: 56px;
+ flex-shrink: 0;
+}
+.cookbook-gpu-proc-name {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+.cookbook-gpu-proc-mem {
+ color: var(--fg-muted);
+ flex-shrink: 0;
+}
+.cookbook-gpu-proc-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+.cookbook-gpu-kill {
+ border: 1px solid color-mix(in srgb, var(--red) 60%, transparent);
+ background: none;
+ color: var(--red);
+ border-radius: 3px;
+ padding: 2px 8px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 10px;
+}
+.cookbook-gpu-kill[data-sig="KILL"] {
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+.cookbook-gpu-kill:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--red) 20%, transparent);
+}
+.cookbook-gpu-kill:disabled { opacity: 0.4; cursor: wait; }
+.cookbook-serve-title {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ min-width: 0;
+}
+.cookbook-serve-title-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cookbook-hf-link {
+ font-size: 9px;
+ text-decoration: none;
+ color: var(--fg-muted);
+ opacity: 0.5;
+ padding: 1px 5px;
+ border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ vertical-align: 1px;
+ letter-spacing: 0.3px;
+ font-weight: 600;
+ flex-shrink: 0;
+}
+.cookbook-hf-link:hover {
+ opacity: 0.8;
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+}
+/* In the Trending models list the HF pill is the row's main affordance —
+ tint it accent so it stands out next to the model name. */
+#cookbook-hf-latest-list .cookbook-hf-link {
+ color: var(--accent, var(--red));
+ opacity: 0.85;
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
+}
+
+/* Running tab sections */
+.cookbook-saved-section,
+.cookbook-serve-section,
+.cookbook-dl-section {
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-bottom: 6px;
+ overflow: hidden;
+}
+/* Divider between server-group sections in the Active tab so the
+ transition between e.g. local + remote-host server blocks reads
+ visually instead of running into each other. */
+.cookbook-group[data-backend-group="Running"] > [class*="cookbook-server-section-"] + [class*="cookbook-server-section-"] {
+ margin-top: 14px;
+ padding-top: 14px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
+}
+.cookbook-section-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 10px;
+ cursor: pointer;
+ user-select: none;
+ /* Persistent surface + border so it reads as a clickable bar instead of
+ blending into the panel background. */
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ transition: background 0.15s, border-color 0.15s;
+}
+.cookbook-section-header:hover {
+ background: color-mix(in srgb, var(--fg) 9%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 22%, transparent);
+}
+.cookbook-section-header.has-server-color {
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 45%, var(--border));
+ background: color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 4%, var(--bg));
+ box-shadow: inset 6px 0 0 var(--cookbook-server-accent, var(--cookbook-server-color));
+}
+.cookbook-section-header.has-server-color:hover {
+ background: color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 7%, var(--bg));
+ border-color: color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 60%, var(--border));
+}
+.cookbook-section-header.has-server-color .cookbook-section-title,
+.cookbook-section-header.has-server-color .cookbook-section-chevron {
+ color: var(--cookbook-server-accent, var(--cookbook-server-color));
+}
+.cookbook-section-header.has-server-color .cookbook-srv-status.ok {
+ background: var(--cookbook-server-accent, var(--cookbook-server-color));
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 18%, transparent),
+ 0 0 8px color-mix(in srgb, var(--cookbook-server-accent, var(--cookbook-server-color)) 55%, transparent);
+}
+.cookbook-section-header .cookbook-clear-btn {
+ margin-left: 0;
+ position: relative;
+ top: -3px;
+}
+@media (max-width: 768px) {
+ .cookbook-section-header .cookbook-clear-btn {
+ top: -3px;
+ }
+ .cookbook-task-check {
+ top: 4px;
+ }
+}
+/* "Stop all" sits just left of "Clear finished"; it carries the auto margin so
+ the pair is pushed together to the right of the section title. */
+.cookbook-section-header .cookbook-stop-all-btn {
+ margin-left: auto;
+ margin-right: 6px;
+ position: relative;
+ top: -3px;
+}
+.cookbook-stop-all-btn {
+ font-size: 10px !important;
+ padding: 0 8px !important;
+ height: 22px !important;
+ border-radius: 6px !important;
+ border: 1px solid var(--border) !important;
+ color: color-mix(in srgb, var(--fg) 40%, transparent) !important;
+ opacity: 1 !important;
+ background: none !important;
+}
+.cookbook-stop-all-btn:hover {
+ color: var(--warn, #f0ad4e) !important;
+ border-color: var(--warn, #f0ad4e) !important;
+ background: color-mix(in srgb, var(--warn, #f0ad4e) 8%, transparent) !important;
+}
+.cookbook-section-chevron {
+ flex-shrink: 0;
+ opacity: 0.6;
+ transition: transform 0.15s ease, opacity 0.15s ease;
+}
+.cookbook-section-body {
+ padding: 0;
+}
+.cookbook-saved-toggle-header {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--fg-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ margin-bottom: 4px;
+}
+.cookbook-saved-toggle:hover { color: var(--fg); }
+.cookbook-saved-item {
+ margin: 2px 0;
+ border-radius: 4px;
+ font-size: 11px;
+}
+.cookbook-saved-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 6px;
+ border-radius: 4px;
+ transition: background 0.08s;
+}
+.cookbook-saved-header:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.cookbook-saved-item .hwfit-serve-panel {
+ margin: 4px 0 4px 6px;
+}
+.cookbook-saved-host {
+ font-size: 10px;
+ color: var(--fg-muted);
+ font-family: 'Fira Code', monospace;
+}
+.cookbook-saved-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cookbook-saved-launch {
+ font-size: 10px;
+ padding: 2px 8px;
+}
+.cookbook-saved-del {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ opacity: 0.3;
+ cursor: pointer;
+ font-size: 11px;
+ padding: 0 2px;
+}
+.cookbook-saved-del:hover {
+ opacity: 1;
+ color: var(--color-error, var(--warn));
+}
+
+/* Serve config panel */
+.hwfit-serve-panel {
+ margin-top: 6px;
+ padding: 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+.cookbook-serve-slots {
+ display: flex;
+ gap: 3px;
+ justify-content: flex-end;
+ margin-bottom: 4px;
+}
+/* When the Save split sits inside Row 1 (next to GPUs), align it with the
+ input baseline (the row's grid cells stretch top-down; without this the
+ Save buttons sit above the GPU button group). */
+.hwfit-serve-row .cookbook-serve-slots {
+ align-self: end;
+ margin-bottom: 4px;
+}
+.cookbook-slot-btn {
+ min-width: 22px; height: 28px;
+ padding: 0 6px;
+ font-size: 10px; font-weight: 600;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: none;
+ white-space: nowrap;
+ max-width: 80px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--fg-muted);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.5;
+ transition: all 0.15s;
+}
+.cookbook-slot-btn:hover { opacity: 0.9; border-color: var(--fg-muted); }
+/* Saved-configs split button: "Save" + dropdown-arrow joined into one control,
+ but each segment keeps its own style — Save uses the filled accent
+ "Done"-button look; the arrow stays a subtle outlined menu trigger. */
+.cookbook-saved-split { gap: 0; }
+.cookbook-saved-save,
+.cookbook-saved-arrow { max-width: none; }
+.cookbook-saved-save {
+ padding: 0 10px;
+ gap: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ border-color: var(--border);
+ font-weight: 600;
+ opacity: 1;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+.cookbook-saved-save:hover {
+ background: var(--border);
+ border-color: var(--accent);
+ opacity: 1;
+}
+.cookbook-saved-arrow {
+ padding: 0 6px;
+ background: var(--bg);
+ color: var(--fg);
+ border-color: var(--border);
+ opacity: 1;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ border-left: none;
+}
+.cookbook-saved-menu .cookbook-saved-favorite {
+ border-left: 3px solid var(--red);
+ background: color-mix(in srgb, var(--red) 4%, transparent);
+}
+.cookbook-saved-fav-btn {
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background: none;
+ color: var(--fg-muted);
+ opacity: 0.55;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+.cookbook-saved-fav-btn:hover,
+.cookbook-saved-fav-btn.active {
+ color: var(--red);
+ opacity: 1;
+ background: color-mix(in srgb, var(--red) 10%, transparent);
+}
+.cookbook-saved-fav-badge {
+ flex-shrink: 0;
+ background: transparent;
+ padding: 0;
+}
+.cookbook-serve-favorite-model {
+ border-left-color: var(--red) !important;
+ background: color-mix(in srgb, var(--red) 4%, transparent);
+}
+.cookbook-serve-fav-badge {
+ display: inline-flex;
+ align-items: center;
+ vertical-align: 1px;
+ margin-left: 5px;
+ margin-right: 2px;
+ background: transparent;
+ padding: 0;
+}
+.cookbook-slot-saved { background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--accent); }
+.cookbook-slot-saved:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
+.cookbook-slot-btn.active { opacity: 1; background: var(--accent); color: #fff; border-color: var(--accent);
+}
+.cookbook-slot-wrap {
+ position: relative;
+ display: inline-flex;
+}
+.cookbook-slot-del {
+ position: absolute;
+ top: -5px;
+ right: -5px;
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: var(--bg, #1a1a1a);
+ color: var(--fg-muted);
+ font-size: 12px;
+ line-height: 1;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s, color 0.15s, border-color 0.15s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.cookbook-slot-del:hover {
+ color: #f44;
+ border-color: #f44;
+}
+.cookbook-card-backend { display: none; }
+/* Dependencies row — aligned tags */
+.cookbook-dep-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel);
+}
+/* Let the deps list fill the available window height instead of being
+ capped at the default .doclib-grid 400-px box. */
+[data-backend-group="Dependencies"] #cookbook-deps-list {
+ flex: 1;
+ max-height: none;
+}
+
+/* Settings tab: stack the HF token + Servers cards as distinct blocks
+ (matches the Download tab's block-per-section layout). */
+.cookbook-settings-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ flex: 1;
+ /* Scroll the whole settings panel so the Servers card can grow to hold every
+ server (it used to be a cramped internal scroll box that clipped them). */
+ overflow-y: auto;
+ min-height: 0;
+}
+.cookbook-settings-stack.hidden { display: none; }
+.cookbook-dep-row.cookbook-dep-blocked { opacity: 0.4; }
+.cookbook-dep-info { flex: 1; min-width: 0; }
+.cookbook-dep-section {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ margin: 12px 2px 4px;
+}
+.cookbook-dep-section-title {
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+}
+.cookbook-dep-section-note {
+ font-size: 10px;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.cookbook-dep-subgroup {
+ margin: 6px 0 8px 12px;
+ padding: 7px 8px 8px;
+ border-left: 2px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ border-radius: 0 6px 6px 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.cookbook-dep-subgroup-title {
+ display: flex;
+ align-items: baseline;
+ gap: 7px;
+ margin: 0 1px 1px;
+ font-size: 10.5px;
+ font-weight: 700;
+ color: color-mix(in srgb, var(--fg) 78%, transparent);
+}
+.cookbook-dep-subgroup-title em {
+ font-size: 9.5px;
+ font-style: normal;
+ font-weight: 500;
+ color: color-mix(in srgb, var(--fg) 48%, transparent);
+}
+.cookbook-dep-tag {
+ font-size: 9px;
+ padding: 0 8px;
+ border-radius: 4px;
+ white-space: nowrap;
+ text-align: center;
+ min-width: 62px;
+ box-sizing: border-box;
+ line-height: 1;
+ /* Explicit height so the tags (Install / Installed) are the EXACT
+ same height as the sibling tags — Firefox gives buttons a taller
+ native box otherwise. Mirrors the server-row tag rule (height:24px). */
+ height: 24px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.cookbook-dep-target {
+ border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.cookbook-dep-cat {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+}
+/* Rebuild tag — same look as the LLM category tag, sits to its left. */
+.cookbook-dep-rebuild {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ color: color-mix(in srgb, var(--fg) 75%, transparent);
+ border: 1px solid color-mix(in srgb, var(--fg) 20%, transparent);
+ cursor: pointer;
+ font-family: inherit;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ position: relative;
+ top: -2px;
+}
+.cookbook-dep-reinstall {
+ top: -3px;
+}
+.cookbook-dep-rebuild:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+}
+.cookbook-dep-installed {
+ background: color-mix(in srgb, var(--green, #50fa7b) 18%, transparent);
+ color: var(--green, #50fa7b);
+ border: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
+ /* Match Install + Installed ▾ so all three variants align in mixed rows. */
+ width: 87.7px;
+ min-width: 87.7px;
+ padding: 0 10px;
+ box-sizing: border-box;
+}
+.cookbook-dep-na {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
+ cursor: help;
+ /* Match other dep tag widths so N/A rows line up with Install / Installed. */
+ min-width: 75.85px;
+ padding: 0 10px;
+ box-sizing: border-box;
+}
+.cookbook-dep-install {
+ background: var(--accent, var(--red));
+ color: #fff;
+ border: none;
+ cursor: pointer;
+ font-family: inherit;
+ font-weight: 500;
+ position: relative;
+ top: -3px;
+ width: 87.7px;
+ min-width: 87.7px;
+ padding: 0 10px;
+ /* Strip the native button box so it's the same height as the sibling tags
+ (Firefox renders taller otherwise); height comes from .cookbook-dep-tag. */
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+.cookbook-dep-build-deps {
+ width: auto;
+ min-width: 87.7px;
+ max-width: 100%;
+ white-space: normal;
+ overflow-wrap: anywhere;
+ line-height: 1.15;
+ text-align: center;
+ height: auto;
+ min-height: 24px;
+}
+/* The install state is the terminal control in a dependency row. Keep it on
+ the far edge even when optional rebuild/category controls are present. */
+.cookbook-dep-row > .cookbook-dep-installed,
+.cookbook-dep-row > .cookbook-dep-na,
+.cookbook-dep-row > .cookbook-dep-install:not(.cookbook-dep-build-deps) {
+ margin-left: auto;
+}
+/* Center the "?" glyph inside the help chip. Without text-align it sits 0.5px
+ left of true center because of the character's natural baseline offset. */
+.hwfit-help-chip {
+ text-align: center;
+ padding-left: 0.5px;
+}
+.cookbook-dep-install:hover { opacity: 0.85; }
+/* Installed split button: "Installed" label + separator + ▾ caret; clicking it
+ opens the actions menu (Update). Replaces the old ⋮ button. */
+.cookbook-dep-installed-btn {
+ padding: 0;
+ width: 87.7px;
+ min-width: 87.7px;
+ cursor: pointer;
+ font-family: inherit;
+ overflow: hidden;
+ position: relative;
+ top: -3px;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+}
+.cookbook-dep-installed-btn .cookbook-dep-caret {
+ display: inline-flex;
+ align-items: center;
+ align-self: stretch;
+ padding: 0 5px;
+ font-size: 14px;
+ line-height: 1;
+ position: relative;
+ top: 1px;
+ left: 4px;
+ opacity: 0.85;
+ border-left: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
+}
+.hwfit-serve-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
+ gap: 6px;
+ margin-bottom: 6px;
+}
+/* Tighter min on the core-knobs row only so Dtype / TP / GPU Mem / Max
+ Seqs fit on one line without crushing Row 1's GPU button strip or
+ the Backend picker. */
+.hwfit-serve-row-core {
+ grid-template-columns: repeat(auto-fill, minmax(78px, 1fr));
+}
+/* Save / saved-config split aligned to the right end of Row 1 and
+ nudged up 4px so it sits flush with the rest of the row. */
+.hwfit-serve-row .cookbook-serve-slots {
+ justify-self: end;
+ position: relative;
+ top: -4px;
+}
+/* The Settings label wraps the Save split — right-align its title text
+ so the word "Settings" sits above the right-aligned Save button
+ instead of floating off to the left of its grid cell. 8px right
+ padding now (16px → 8px) per follow-up tweak, brings the title back
+ over the actual Save button instead of overshooting it. */
+.hwfit-serve-row label:has(> .cookbook-serve-slots) {
+ grid-column: -2 / -1;
+ justify-self: end;
+ text-align: right;
+ padding-right: 0;
+}
+.hwfit-serve-row label:has(> .cookbook-serve-slots) > span {
+ display: inline-block;
+ position: relative;
+ left: -33px;
+}
+.hwfit-serve-topline {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 8px;
+ margin: 0 0 6px;
+}
+.hwfit-serve-topline .hwfit-serve-runtime-text {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.hwfit-serve-preset-row {
+ display: flex;
+ justify-content: flex-end;
+ margin: 0;
+ flex: 0 0 auto;
+ position: relative;
+ top: -6px;
+}
+.hwfit-serve-row label {
+ font-size: 10px;
+ color: var(--fg-muted);
+ white-space: nowrap;
+ letter-spacing: 0.3px;
+}
+.hwfit-serve-row-core .hwfit-context-label {
+ grid-column: span 2;
+ width: auto;
+ min-width: 0;
+ max-width: none;
+ justify-self: start;
+}
+.hwfit-context-control {
+ display: block;
+ position: relative;
+ align-items: center;
+ margin-top: 2px;
+ width: 100px;
+}
+.hwfit-context-control .hwfit-sf[data-field="ctx"] {
+ min-width: 0;
+ width: 100%;
+ padding-right: 40px;
+}
+.hwfit-serve-row-core label:has(.hwfit-sf[data-field="max_seqs"]) {
+ position: relative;
+ left: -66px;
+}
+.hwfit-serve-row-core label:has(.hwfit-sf[data-field="gpu_mem"]) {
+ position: relative;
+ left: -64px;
+}
+.hwfit-serve-row-core .hwfit-gpus-label {
+ position: relative;
+ left: -70px;
+}
+.hwfit-context-calc-btn {
+ position: absolute;
+ right: 3px;
+ top: -4px;
+ width: 34px;
+ height: 28px;
+ min-width: 34px;
+ padding: 0;
+ font-size: 10px;
+ line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ overflow: hidden;
+ white-space: nowrap;
+}
+.hwfit-context-calc-btn .spinner-whirlpool {
+ width: 12px !important;
+ height: 12px !important;
+ margin: 0 auto !important;
+}
+.hwfit-context-calc-btn .ai-spinner-whirlpool {
+ width: 12px !important;
+ height: 12px !important;
+}
+@media (min-width: 769px) {
+ [data-llama-mode-toggle].mode-toggle.mode-toggle-three .mode-toggle-btn > span {
+ top: -6px !important;
+ }
+ .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;
+ }
+}
+@media (max-width: 768px) {
+ .hwfit-serve-row-core .hwfit-context-label {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .hwfit-serve-row-core label:has(.hwfit-sf[data-field="max_seqs"]),
+ .hwfit-serve-row-core label:has(.hwfit-sf[data-field="gpu_mem"]) {
+ left: 0;
+ }
+ details.hwfit-serve-advanced label:has(.hwfit-sf[data-field="vllm_env_preset"]),
+ details.hwfit-serve-advanced label:has(.hwfit-sf[data-field="extra_env"]) {
+ grid-column: 1 / -1 !important;
+ }
+ .hwfit-context-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ width: auto;
+ }
+ .hwfit-context-control .hwfit-sf[data-field="ctx"] {
+ width: 100px;
+ padding-right: 6px;
+ }
+ .hwfit-context-calc-btn {
+ position: relative;
+ top: -2px;
+ left: 2px;
+ right: auto;
+ flex: 0 0 34px;
+ width: 34px;
+ min-width: 34px;
+ }
+}
+.hwfit-auto-ctx-note {
+ display: block;
+ margin-top: 3px;
+ font-size: 9px;
+ opacity: 0.6;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Ctx slider — ported from origin/main. Sits in the Scan/Download toolbar
+ next to the quant dropdown. Drives _ctxValue() in cookbook-hwfit.js. */
+.hwfit-ctx-control {
+ height: 28px;
+ min-width: 134px;
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ font-size: 11px; /* slightly smaller than .cookbook-field-input so Context reads quieter next to Engine/Quant */
+}
+.hwfit-ctx-control span {
+ /* Match Quant/Engine select label style: no uppercase, no letter-spacing. */
+ text-transform: none;
+ letter-spacing: 0;
+ opacity: 0.9;
+}
+.hwfit-ctx-control > span:first-child {
+ font-size: 9.5px;
+}
+/* Editor-style slider (same look as the gallery editor sliders): thin pill
+ rail that fattens on interaction, circular red thumb that grows on hover. */
+.hwfit-ctx-control input[type="range"] {
+ width: 64px;
+ min-width: 64px;
+ height: 4px;
+ padding: 0;
+ border: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ /* Hard-coded grey so the rail is GUARANTEED visible regardless of theme —
+ every theme-derived color we tried (--fg-muted, --border, accent-bg mix)
+ kept blending into the panel background on at least one theme. */
+ background: rgba(150, 150, 150, 0.65);
+ border-radius: 999px;
+ accent-color: var(--red);
+ cursor: pointer;
+ transition: height 0.15s ease, background 0.15s ease;
+}
+.hwfit-ctx-control input[type="range"]:hover,
+.hwfit-ctx-control input[type="range"]:focus,
+.hwfit-ctx-control input[type="range"]:active {
+ background: var(--fg);
+}
+.hwfit-ctx-control input[type="range"]:hover,
+.hwfit-ctx-control input[type="range"]:focus,
+.hwfit-ctx-control input[type="range"]:active {
+ height: 10px;
+}
+.hwfit-ctx-control input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.hwfit-ctx-control input[type="range"]::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.hwfit-ctx-control input[type="range"]:hover::-webkit-slider-thumb,
+.hwfit-ctx-control input[type="range"]:focus::-webkit-slider-thumb,
+.hwfit-ctx-control input[type="range"]:active::-webkit-slider-thumb {
+ width: 18px;
+ height: 18px;
+}
+.hwfit-ctx-control input[type="range"]:hover::-moz-range-thumb,
+.hwfit-ctx-control input[type="range"]:focus::-moz-range-thumb,
+.hwfit-ctx-control input[type="range"]:active::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+}
+.hwfit-ctx-control output {
+ min-width: 28px;
+ text-align: right;
+ color: var(--fg);
+ font-weight: 600;
+}
+.hwfit-sf {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ padding: 0 6px;
+ height: 28px;
+ box-sizing: border-box;
+}
+.hwfit-sf[data-field="backend"],
+.hwfit-sf[data-field="dtype"],
+.hwfit-sf[data-field="tp"] {
+ height: 32px;
+ box-sizing: border-box;
+ width: 100%;
+}
+.hwfit-sf:focus {
+ border-color: var(--accent, var(--red));
+ outline: none;
+}
+.hwfit-sf[type="text"] { width: 100%; }
+.hwfit-sf.hwfit-sf-wide { width: 100%; }
+.hwfit-serve-checks {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 6px;
+}
+.hwfit-sf-cb {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: var(--fg-muted);
+ cursor: pointer;
+ padding: 3px 0;
+}
+.hwfit-sf-cb:hover { color: var(--fg); }
+/* Speculative method + tokens controls — render inline beside the checkbox */
+.hwfit-spec-group .hwfit-spec-method,
+.hwfit-spec-group .hwfit-spec-tokens {
+ height: 20px;
+ padding: 0 4px;
+ font-size: 11px;
+ /* Match the generic .hwfit-sf serve controls: inherit font + 4px radius
+ so the Speculative method/token widgets read as the same control
+ family as the rest of the panel (they were 'Fira Code'/3px before). */
+ font-family: inherit;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+}
+.hwfit-spec-group .hwfit-spec-method { min-width: 110px; }
+.hwfit-spec-group .hwfit-spec-tokens { width: 44px; text-align: center; }
+/* The vLLM Speculative input was a bare with the
+ native browser up/down spinner showing. Hide the spinner so it
+ matches the cleaner MTP-spec input next to it. */
+.hwfit-spec-tokens-bare {
+ -moz-appearance: textfield;
+ appearance: textfield;
+ /* Tighten + center now that the spinner column is gone — without this
+ the input still reserves the old 44px and the number floats with a
+ dead gutter on the right. !important beats the inline width=44px
+ that ships in the markup. */
+ width: 30px !important;
+ padding-right: 4px !important;
+ text-align: center;
+}
+/* Themed step buttons replacing the native number-input spinner. */
+.hwfit-numstep {
+ display: inline-flex;
+ align-items: stretch;
+ height: 20px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ overflow: hidden;
+}
+.hwfit-numstep:focus-within { border-color: var(--accent, var(--red)); }
+.hwfit-numstep .hwfit-spec-tokens {
+ border: none !important;
+ border-radius: 0 !important;
+ width: 38px;
+ height: 100%;
+ background: transparent;
+ -moz-appearance: textfield;
+ appearance: textfield;
+ margin: 0;
+}
+.hwfit-numstep-btn {
+ width: 16px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border: none;
+ color: var(--accent, var(--red));
+ font-family: inherit;
+ font-size: 15px;
+ font-weight: 700;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0;
+ opacity: 1;
+ transition: background 0.12s, opacity 0.12s, color 0.12s;
+}
+.hwfit-numstep-btn:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ color: var(--accent, var(--red));
+}
+.hwfit-numstep-btn:active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+}
+.hwfit-numstep-btn[data-step="-1"] { border-right: 1px solid var(--border); }
+.hwfit-numstep-btn[data-step="1"] { border-left: 1px solid var(--border); }
+/* Custom toggle switch */
+.hwfit-sf-cb input[type="checkbox"] {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 28px;
+ height: 14px;
+ border-radius: 7px;
+ background: color-mix(in srgb, var(--fg) 15%, transparent);
+ position: relative;
+ cursor: pointer;
+ margin: 0;
+ flex-shrink: 0;
+ transition: background 0.2s;
+}
+.hwfit-sf-cb input[type="checkbox"]::after {
+ content: '';
+ position: absolute;
+ top: 1px;
+ left: 2px;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #000;
+ transition: transform 0.2s, background 0.2s;
+}
+.hwfit-sf-cb input[type="checkbox"]:checked {
+ background: var(--accent, var(--red));
+}
+.hwfit-sf-cb input[type="checkbox"]:checked::after {
+ transform: translateX(14px);
+ background: #fff;
+}
+.hwfit-serve-extra label {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ font-size: 10px;
+ color: var(--fg-muted);
+ letter-spacing: 0.3px;
+}
+.hwfit-serve-extra .hwfit-sf {
+ width: 100%;
+}
+.hwfit-serve-cmd-summary {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ list-style: none;
+ cursor: pointer;
+ user-select: none;
+ padding: 6px 10px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ color: var(--fg-muted);
+ font-size: 10px;
+ letter-spacing: 0.3px;
+}
+.hwfit-serve-cmd {
+ margin: 0;
+ padding: 8px 10px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-top: none;
+ border-radius: 4px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
+ font-size: 10px;
+ white-space: pre;
+ word-break: normal;
+ width: 100%;
+ box-sizing: border-box;
+ resize: none;
+ color: var(--fg);
+ line-height: 1.5;
+ min-height: 112px;
+ overflow: auto;
+}
+.hwfit-serve-actions {
+ display: flex;
+ gap: 6px;
+ margin-top: 4px;
+ align-items: center;
+}
+.hwfit-serve-actions .cookbook-btn {
+ padding: 5px 14px;
+ font-size: 11px;
+}
+.hwfit-serve-actions-spacer { flex: 1 1 auto; }
+.hwfit-serve-actions .cookbook-serve-slots {
+ margin: 0;
+ align-self: stretch;
+}
+.hwfit-serve-launch {
+ background: var(--accent-primary, var(--red));
+ color: #fff;
+ border: 1px solid var(--accent-primary, var(--red));
+ border-radius: 4px;
+ font-weight: 700;
+}
+.hwfit-serve-launch:hover {
+ opacity: 0.9;
+}
+
+/* Task header ⋮ menu button */
+.cookbook-task-save-btn {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ opacity: 0.3;
+ padding: 2px;
+ flex-shrink: 0;
+ transition: opacity 0.15s;
+ position: relative;
+ top: -4px;
+ transform: scale(1.15);
+}
+.cookbook-task-save-btn:hover { opacity: 0.8; }
+.cookbook-task-edit-btn {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ cursor: pointer;
+ opacity: 0.3;
+ padding: 2px;
+ flex-shrink: 0;
+ transition: opacity 0.15s;
+ position: relative;
+ top: -4px;
+ transform: scale(1.15);
+}
+.cookbook-task-edit-btn:hover { opacity: 0.8; }
+.cookbook-task-menu-btn {
+ background: none;
+ border: 1px solid transparent;
+ color: var(--fg-muted);
+ font-size: 16px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ opacity: 0;
+ border-radius: 4px;
+ position: relative;
+ top: -3px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.15s;
+ flex-shrink: 0;
+}
+.cookbook-task:hover .cookbook-task-menu-btn { opacity: 0.6; }
+.cookbook-task-menu-btn:hover {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ border-color: var(--border);
+ color: var(--fg);
+}
+.cookbook-task-menu-btn.cookbook-menu-active {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--fg) 9%, transparent);
+ border-color: var(--border);
+ color: var(--fg);
+}
+@media (max-width: 768px) {
+ .cookbook-task .cookbook-task-menu-btn {
+ opacity: 0.72;
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
+ top: -3px;
+ }
+ .cookbook-task .cookbook-task-menu-btn:active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 9%, transparent);
+ border-color: var(--border);
+ }
+}
+
+/* Launch-command textarea wrapper — Copy pill floats at the top-right
+ corner of the field (chat run-output pattern). */
+.hwfit-serve-cmd-wrap {
+ position: relative;
+}
+.hwfit-serve-cmd-title {
+ margin: 2px 0 3px;
+ font-size: 10px;
+ color: var(--fg-muted);
+ letter-spacing: 0.3px;
+}
+.hwfit-serve-cmd-details[open] > .hwfit-serve-cmd-summary {
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 48%, var(--border));
+ box-shadow: 0 0 12px color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+}
+.hwfit-serve-copy-inline {
+ position: absolute;
+ top: 4px; right: 4px;
+ z-index: 2;
+ width: 26px !important;
+ height: 26px !important;
+ min-width: 26px !important;
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ transition: opacity 0.12s, color 0.12s;
+}
+.hwfit-serve-copy-inline:hover { opacity: 1; color: var(--accent-primary, var(--red)); }
+.hwfit-serve-copy-inline.copied { opacity: 1; color: var(--color-save-green, #4caf50); }
+.hwfit-serve-copy-inline svg { display: block; }
+
+/* Split button: Clear Server (main) + ^ arrow (more actions). The two halves
+ are visually joined — left button square on its right edge, arrow square
+ on its left edge — so they read as one widget. */
+.cookbook-gpu-split { display: inline-flex; flex-shrink: 0; }
+.cookbook-gpu-split .cookbook-gpu-split-arrow svg { display: block; }
+
+.cookbook-task-dropdown {
+ /* Must sit above the cookbook modal (whose auto-stack z-index starts at
+ 300 and climbs whenever the modal is brought to the front). 1000 was
+ not enough — the dropdown rendered behind the modal on mobile. */
+ z-index: 10000;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 4px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ min-width: auto;
+ width: max-content;
+}
+.cookbook-task-dropdown .dropdown-item-compact {
+ padding: 6px 10px;
+ font-size: 12px;
+ cursor: pointer;
+ border-radius: 4px;
+ white-space: nowrap;
+}
+.cookbook-task-dropdown .dropdown-item-compact:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+}
+.cookbook-dropdown-danger:hover {
+ color: var(--red) !important;
+}
+.cookbook-task-retry:hover { color: var(--color-warning, #f0ad4e); }
+.cookbook-task-kill:hover { color: var(--color-error, var(--warn)); }
+
+/* Serve "Edit command" modal */
+.cookbook-edit-overlay {
+ position: fixed; inset: 0; z-index: 10000;
+ background: rgba(0,0,0,0.5);
+ display: flex; align-items: center; justify-content: center;
+}
+.cookbook-edit-modal {
+ width: min(720px, 92vw);
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
+ display: flex; flex-direction: column; gap: 10px;
+}
+.cookbook-edit-title {
+ font-size: 13px; font-weight: 600; color: var(--fg);
+}
+.cookbook-edit-textarea {
+ width: 100%;
+ min-height: 140px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: ui-monospace, Menlo, Consolas, monospace;
+ font-size: 12px;
+ padding: 8px;
+ outline: none;
+ resize: vertical;
+ white-space: pre;
+ overflow: auto;
+}
+.cookbook-edit-textarea:focus { border-color: var(--accent, var(--fg)); }
+.cookbook-edit-actions {
+ display: flex; gap: 8px; justify-content: flex-end;
+}
+.cookbook-edit-save {
+ background: color-mix(in srgb, var(--accent) 20%, transparent) !important;
+ border-color: var(--accent) !important;
+}
+
+/* Running tasks */
+/* Each running task is a real card with a left accent stripe coloured by
+ status, instead of an invisible divider against bg. */
+.cookbook-task {
+ margin: 0 0 8px;
+ border: 1px solid var(--border);
+ border-left: 3px solid color-mix(in srgb, var(--fg) 30%, transparent);
+ border-radius: 8px;
+ overflow: hidden;
+ max-width: 100%;
+ min-width: 0;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ transition: border-color 0.15s, background 0.15s;
+}
+.cookbook-task:hover {
+ border-color: color-mix(in srgb, var(--fg) 25%, var(--border));
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+}
+/* Status-driven left stripe via :has() — graceful fallback to neutral. */
+.cookbook-task:has(.cookbook-task-running) { border-left-color: var(--green, #50fa7b); }
+.cookbook-task:has(.cookbook-task-downloading) { border-left-color: var(--color-accent, #00aaff); }
+.cookbook-task:has(.cookbook-task-done) { border-left-color: var(--green, #50fa7b); }
+.cookbook-task:has(.cookbook-task-error) { border-left-color: var(--color-error, var(--warn, #f87171)); }
+.cookbook-task:has(.cookbook-task-queued) { border-left-color: var(--color-warning, #f0ad4e); }
+
+/* Serve crashed / unreachable — full red frame so a dead server in the
+ Running tab is obvious at a glance (mirrors the endpoint health dot). */
+.cookbook-task.cookbook-task-unreachable,
+.cookbook-task[data-type="serve"][data-status="error"],
+.cookbook-task[data-type="serve"][data-status="crashed"] {
+ border-color: var(--color-error, #f44);
+ border-left-color: var(--color-error, #f44);
+ background: color-mix(in srgb, var(--color-error, #f44) 8%, var(--bg));
+}
+
+.cookbook-task-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 10px;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ font-size: 12px;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
+ /* Pin the row so flex parents + Firefox mobile can't squeeze its height to 0,
+ which hides the type pill + model name and leaves only the sub-line +
+ output visible. */
+ flex-shrink: 0;
+ min-height: 32px;
+}
+.cookbook-task-type {
+ text-transform: uppercase;
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ flex-shrink: 0;
+ width: 67.3px;
+ text-align: center;
+}
+.cookbook-task-type[data-type="serve"] {
+ background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
+ color: var(--green, #50fa7b);
+}
+.cookbook-dl-add-server {
+ position: relative;
+ top: -3px;
+}
+.cookbook-task-type[data-type="download"] {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ color: var(--fg-muted);
+}
+/* Finished state — overrides the per-type colors so a completed download or
+ serve task shows the same green FINISHED chip. */
+.cookbook-task-type.cookbook-task-type-done {
+ background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
+ color: var(--green, #50fa7b);
+}
+.cookbook-task-backend {
+ width: 67.3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ opacity: 0.5;
+}
+.cookbook-task-name {
+ flex: 1;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.cookbook-task-wave {
+ font-size: 11px;
+ letter-spacing: -1px;
+ color: #fff;
+ /* Gentle pulse on top of the cycling bars so a running task reads as clearly
+ alive — especially when the card is collapsed and the log isn't visible. */
+ animation: cookbook-wave-pulse 1.3s ease-in-out infinite;
+}
+.cookbook-task-indicator {
+ min-width: 24px;
+ text-align: center;
+ flex-shrink: 0;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ /* The done/clear pill inside is wider than 24px and would otherwise
+ overflow into the edit/save buttons that sit immediately before it on
+ a serve task. Push it right so it stops crashing into them on desktop. */
+ margin-left: 8px;
+}
+.cookbook-task-check {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ position: relative;
+ top: 0;
+ cursor: pointer;
+ padding: 1px 6px 1px 4px;
+ border-radius: 9px;
+ color: var(--red, #ff5555);
+ transition: background 0.15s;
+}
+.cookbook-task-check svg { flex-shrink: 0; }
+.cookbook-task-check:hover { background: color-mix(in srgb, var(--red, #ff5555) 18%, transparent); }
+/* Terminal task clear pill. */
+.cookbook-task-done-label,
+.cookbook-task-clear-label {
+ font-size: 9px;
+ line-height: 1;
+ text-transform: lowercase;
+}
+.cookbook-task-done-label { color: var(--red, #ff5555); }
+.cookbook-task-clear-label { display: none; }
+.cookbook-task-check-ico { display: none; }
+.cookbook-task-clear-ico { display: inline; }
+.cookbook-task[data-status="done"] .cookbook-task-check {
+ color: var(--green, #50fa7b);
+}
+.cookbook-task[data-status="done"] .cookbook-task-check:hover {
+ background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
+}
+.cookbook-task[data-status="done"] .cookbook-task-done-label {
+ color: var(--green, #50fa7b);
+}
+@media (max-width: 820px) {
+ .cookbook-task-check {
+ top: 2px;
+ }
+}
+.cookbook-task-start-now {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ position: relative;
+ top: -4px;
+ cursor: pointer;
+ /* Tightened vertical padding so the hover-background isn't disproportionately
+ tall vs the icon+label. */
+ padding: 0 6px 0 4px;
+ height: 14px;
+ border: 0;
+ border-radius: 7px;
+ background: transparent;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 9px;
+ line-height: 1;
+ text-transform: lowercase;
+ white-space: nowrap;
+ transition: background 0.15s;
+}
+.cookbook-task-start-now svg {
+ flex-shrink: 0;
+ position: relative;
+ top: 0;
+}
+.cookbook-task-start-now:hover {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+/* "Serve" button on a finished download — green pill matching the "running" /
+ finished badge (it sits next to the green FINISHED chip + check). */
+.cookbook-task-serve-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 1px 6px 1px 4px;
+ border: 0;
+ border-radius: 9px;
+ line-height: 1;
+ flex-shrink: 0;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 9px;
+ text-transform: lowercase;
+ background: transparent;
+ color: var(--green, #50fa7b);
+ position: relative;
+ top: -2px;
+ margin-right: 2px;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ transition: background 0.15s;
+}
+.cookbook-task-serve-btn svg { flex-shrink: 0; }
+.cookbook-task-serve-btn:hover { background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent); }
+.cookbook-task-sub {
+ padding: 1px 10px 4px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.cookbook-task-session {
+ font-size: 9px;
+ color: var(--fg-muted);
+ font-family: 'Fira Code', monospace;
+ opacity: 0.35;
+ letter-spacing: 0.3px;
+}
+.cookbook-task-server {
+ font-size: 9px;
+ color: var(--fg-muted);
+ font-family: 'Fira Code', monospace;
+ opacity: 0.4;
+}
+.cookbook-task-uptime {
+ font-size: 9px;
+ color: var(--fg-muted);
+ font-family: 'Fira Code', monospace;
+ opacity: 0.4;
+}
+.cookbook-task-status {
+ font-size: 9px;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-weight: 600;
+ text-align: left;
+ flex-shrink: 0;
+ line-height: 16px;
+}
+.cookbook-task-running { background: color-mix(in srgb, var(--green, #50fa7b) 20%, transparent); color: var(--green, #50fa7b); }
+.cookbook-task-downloading { background: color-mix(in srgb, var(--color-accent, #00aaff) 20%, transparent); color: var(--color-accent, #00aaff); }
+/* Stopping: same pill treatment as "running" but orange. */
+.cookbook-task-stopping { background: color-mix(in srgb, var(--orange, #ffb86c) 22%, transparent); color: var(--orange, #ffb86c); }
+.cookbook-task-done { background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
+.cookbook-task-error { background: color-mix(in srgb, var(--color-error, var(--warn)) 20%, transparent); color: var(--color-error, var(--warn)); }
+/* Crashed: same filled-pill treatment as "running" but red, with a red border. */
+.cookbook-task-crashed { background: color-mix(in srgb, var(--red, #ff5555) 16%, transparent); color: var(--red, #ff5555); border: 1px solid var(--red, #ff5555); padding: 0 5px; }
+.cookbook-task-stopped { background: color-mix(in srgb, var(--color-warning, #f0ad4e) 22%, transparent); color: var(--color-warning, #f0ad4e); }
+.cookbook-task-queued { background: color-mix(in srgb, var(--color-warning, #f0ad4e) 15%, transparent); color: var(--color-warning, #f0ad4e); }
+/* Stopped / crashed servers get an orange surround instead of the heavier
+ red — communicates "this isn't running, here's the last command you used,
+ relaunch when ready" without screaming "error". Color stays normal so the
+ command text remains easy to copy. */
+.cookbook-task[data-status="stopped"],
+.cookbook-task[data-status="error"],
+.cookbook-task[data-status="crashed"] {
+ border-color: color-mix(in srgb, var(--color-warning, #f0ad4e) 55%, var(--border));
+ background: color-mix(in srgb, var(--color-warning, #f0ad4e) 6%, var(--bg));
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-warning, #f0ad4e) 18%, transparent);
+}
+.cookbook-task[data-status="stopped"] .cookbook-output-pre,
+.cookbook-task[data-status="error"] .cookbook-output-pre,
+.cookbook-task[data-status="crashed"] .cookbook-output-pre {
+ border-top-color: color-mix(in srgb, var(--color-warning, #f0ad4e) 35%, var(--border));
+}
+.cookbook-task-retry,
+.cookbook-task .cookbook-output-wrap { margin: 0; }
+.cookbook-task .cookbook-output-pre {
+ border-radius: 0;
+ border-top: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
+ max-height: 150px;
+ max-width: 100%;
+ overflow-x: hidden;
+ box-sizing: border-box;
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+}
+.cookbook-task .cookbook-output-pre:empty {
+ display: none;
+}
+.cookbook-task-collapsed {
+ display: none !important;
+}
+.cookbook-task[data-type="serve"] .cookbook-task-header {
+ margin: 4px 4px 0;
+ border-radius: 6px;
+}
+.cookbook-task[data-type="serve"] .cookbook-output-wrap {
+ margin: 0 4px 4px;
+}
+.cookbook-task[data-type="serve"] .cookbook-output-pre {
+ border-radius: 6px;
+}
+
+/* Env bar — match admin-card */
+.cookbook-env-bar {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px;
+}
+.cookbook-env-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.cookbook-env-row .cookbook-field-input {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* Tabs — match library tabs */
+.cookbook-tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 8px;
+}
+/* Mobile: every modal tab strip should swipe horizontally instead of wrapping
+ or getting cut off. Hide the scrollbar (still pannable via touch / drag). */
+@media (max-width: 768px) {
+ /* Server settings actions need a less fiddly touch target on phones. */
+ #cookbook-server-add.cal-add-btn-text {
+ height: 37px;
+ border-radius: 18.5px;
+ }
+ .cookbook-server-check-btn,
+ .cookbook-server-key-btn,
+ .cookbook-server-cancel-btn,
+ .cookbook-server-rm-btn,
+ .cookbook-server-save-btn {
+ height: 31px !important;
+ min-height: 31px;
+ border-radius: 7px;
+ }
+
+ .cookbook-official-filter {
+ flex: 0 0 auto;
+ }
+
+ .cookbook-dep-row {
+ align-items: flex-start;
+ flex-wrap: wrap;
+ }
+ .cookbook-dep-row > .cookbook-dep-info {
+ flex: 1 1 100%;
+ }
+ .cookbook-dep-build-deps {
+ flex: 0 1 auto;
+ max-width: 100%;
+ margin-left: auto;
+ order: 20;
+ }
+ .cookbook-dep-row > .cookbook-dep-installed,
+ .cookbook-dep-row > .cookbook-dep-installed-btn,
+ .cookbook-dep-row > .cookbook-dep-na,
+ .cookbook-dep-row > .cookbook-dep-install:not(.cookbook-dep-build-deps) {
+ margin-left: auto;
+ }
+
+ .cookbook-tabs,
+ .memory-tabs,
+ .admin-tabs,
+ .lib-tabs,
+ .gallery-tabs,
+ .preset-tabs {
+ flex-wrap: nowrap !important;
+ overflow-x: auto !important;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior-x: contain;
+ scrollbar-width: none;
+ }
+ .cookbook-tabs::-webkit-scrollbar,
+ .memory-tabs::-webkit-scrollbar,
+ .admin-tabs::-webkit-scrollbar,
+ .lib-tabs::-webkit-scrollbar,
+ .gallery-tabs::-webkit-scrollbar,
+ .preset-tabs::-webkit-scrollbar {
+ display: none;
+ }
+ .cookbook-tabs > *,
+ .memory-tabs > *,
+ .admin-tabs > *,
+ .lib-tabs > *,
+ .gallery-tabs > *,
+ .preset-tabs > * {
+ flex-shrink: 0;
+ }
+}
+.cookbook-tab {
+ /* `background: none` isn't enough on Windows — Chrome/Edge fall back to the
+ OS native button background (dark gray / black under dark mode) when
+ no explicit color + appearance reset is applied. Force transparent +
+ appearance:none so the tab inherits the modal's panel color. */
+ appearance: none;
+ -webkit-appearance: none;
+ background: transparent;
+ background-color: transparent;
+ padding: 6px 14px;
+ font-size: 12px;
+ font-family: inherit;
+ color: var(--fg-muted);
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ transition: color 0.1s, border-color 0.1s;
+ white-space: nowrap;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+.cookbook-tab:hover {
+ color: var(--fg);
+}
+.cookbook-tab.active {
+ color: var(--accent, var(--red));
+ border-bottom-color: var(--accent, var(--red));
+}
+/* Mobile: hide tab icons so labels don't wrap to a new row */
+@media (max-width: 640px) {
+ .cookbook-tab svg {
+ display: none;
+ }
+ .cookbook-tab {
+ padding: 6px 10px;
+ font-size: 13px;
+ }
+ /* Mobile cookbook text: match the calendar/library modal scale. iOS keeps
+ its no-zoom safeguard below; Android does not need inflated controls. */
+ .cookbook-body {
+ font-size: 13px;
+ }
+ .cookbook-body h2 { font-size: 14px; }
+ .cookbook-body .memory-desc,
+ .cookbook-body .doclib-desc { font-size: 12px; }
+ .cookbook-body .memory-item-title { font-size: 13px; }
+ .cookbook-body .memory-item-meta { font-size: 11px !important; }
+ .cookbook-body .hwfit-sf,
+ .cookbook-body .cookbook-field-input,
+ .cookbook-body .cookbook-dl-repo { font-size: 13px; }
+ .cookbook-body .memory-toolbar-btn { font-size: 12px; }
+}
+
+/* Mobile cookbook sizing — kept in line with calendar/library modals. */
+@media (max-width: 768px) {
+ .hwfit-serve-row-core .hwfit-context-label {
+ grid-column: 1 / -1;
+ }
+ /* The Speculative control (checkbox + method dropdown + token stepper)
+ is too wide for a phone — the stepper ran off the right edge of the
+ modal. Let the group wrap onto its own line, take full width, and
+ shrink the method dropdown so the +/− stepper stays on-screen. */
+ .hwfit-spec-group {
+ flex-wrap: wrap;
+ flex-basis: 100%;
+ column-gap: 4px;
+ row-gap: 4px;
+ }
+ .hwfit-spec-group .hwfit-spec-method {
+ min-width: 0;
+ flex: 0 1 82px;
+ max-width: 82px;
+ }
+ .hwfit-numstep { flex: 0 0 auto; }
+ .hwfit-spec-group .hwfit-help-chip-inline {
+ flex: 0 0 auto;
+ margin-left: 2px !important;
+ }
+ .cookbook-card-title { font-size: 13px; }
+ .cookbook-card-desc { font-size: 12px; }
+ .cookbook-field-label { font-size: 12px; }
+ /* All cookbook controls share one compact field scale. */
+ .cookbook-field-input { font-size: 13px; padding: 6px 8px; }
+ select.cookbook-field-input { font-size: 13px !important; padding: 5px 8px; }
+ /* Repo input + model search read oversized at 16px; bring their text and
+ placeholder hints down to match the rest of the cookbook. */
+ .cookbook-dl-repo, .hwfit-search { font-size: 13px !important; }
+ .cookbook-cmd-preview { font-size: 12px; padding: 8px 10px; }
+ .cookbook-btn { font-size: 12px; padding: 6px 12px; }
+ .cookbook-tab { font-size: 13px; }
+ .cookbook-task-header { font-size: 13px; padding: 6px 10px; }
+ .cookbook-task-name { font-size: 13px; }
+ .cookbook-task-sub { font-size: 11px; }
+ .cookbook-task-status {
+ font-size: 10px;
+ /* Vertical alignment of the phase/loading badge against the task title. */
+ position: relative;
+ top: 2px;
+ }
+ /* Keep the ascii wave aligned with the badge (both 2px down). */
+ .cookbook-task-wave {
+ position: relative;
+ top: 2px;
+ }
+ /* Session id (serve-…) + uptime sub-line down 1px. */
+ .cookbook-task-session,
+ .cookbook-task-uptime {
+ position: relative;
+ top: 1px;
+ }
+ /* Rest of the header row down 2px to match the status badge + wave: the
+ model title, the serve/download type tag, and the edit/save/menu icons. */
+ .cookbook-task-type,
+ .cookbook-task-name,
+ .cookbook-task-edit-btn,
+ .cookbook-task-save-btn {
+ position: relative;
+ top: 2px;
+ }
+ /* The ⋮ menu sits higher than the rest of the row. */
+ .cookbook-task-menu-btn {
+ position: relative;
+ top: -2px;
+ /* Mobile has no hover — the base rule's opacity:0 left the ⋮ button
+ invisible AND effectively unclickable. Always show + give a proper
+ touch target. */
+ opacity: 0.7 !important;
+ width: 32px !important;
+ height: 32px !important;
+ font-size: 18px !important;
+ }
+ /* The copied-confirmation checkmark sits 2px higher than the copy icon. */
+ .cookbook-output-copy.copied svg {
+ position: relative;
+ top: 2px;
+ }
+ .cookbook-section-title { font-size: 12px; }
+ .cookbook-settings-label { font-size: 12px; }
+ .cookbook-settings-hint { font-size: 11px; }
+ .cookbook-settings-input { font-size: 13px; }
+ /* Settings stack is flex:1 + overflow:hidden with no inner scroll, so on a
+ short mobile viewport its lower half gets clipped. Let it scroll. */
+ .cookbook-settings-stack { overflow-y: auto !important; -webkit-overflow-scrolling: touch; }
+ /* The Servers card is flex:1 + its own overflow-y:auto — a nested scroll that
+ collapses and crops its list inside the now-scrolling stack. Let both cards
+ take natural height and scroll the whole stack as one instead. */
+ .cookbook-settings-stack > .admin-card {
+ flex: 0 0 auto !important;
+ overflow: visible !important;
+ }
+ .cookbook-dl-repo { font-size: 13px !important; }
+ .cookbook-dl-btn { font-size: 14px; }
+ .cookbook-serve-preset-name { font-size: 14px; }
+ .cookbook-serve-preset-meta { font-size: 12px; }
+ .cookbook-saved-name { font-size: 14px; }
+ .cookbook-saved-host { font-size: 12px; }
+ .cookbook-slot-btn { font-size: 13px; }
+ /* The serve-panel "Save" split button reads larger than the surrounding
+ controls (it's also bold) — knock it down so it matches the row. */
+ .cookbook-saved-save,
+ .cookbook-saved-arrow { font-size: 11px; }
+ .cookbook-output-pre { font-size: 12px; }
+ .cookbook-dep-tag { font-size: 12px; }
+ .cookbook-checkbox-label { font-size: 13px; }
+ .cookbook-gpu-btn { font-size: 13px; }
+}
+
+/* Slider — range + text value */
+.cookbook-slider-wrap {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.cookbook-slider {
+ flex: 1;
+ min-width: 0;
+ height: 6px;
+ margin: 8px 0;
+ padding: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ background: linear-gradient(to right, var(--accent, var(--red)) 0%, var(--accent, var(--red)) 50%, color-mix(in srgb, var(--fg) 12%, transparent) 50%);
+ border-radius: 4px;
+ outline: none;
+ cursor: pointer;
+ transition: opacity 0.1s;
+}
+.cookbook-slider-value {
+ width: 64px !important;
+ flex-shrink: 0;
+ text-align: center;
+ font-size: 11px;
+}
+
+/* Toggle switches — match admin-switch */
+.cookbook-checkbox-row {
+ grid-column: 1 / -1;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 14px;
+ padding: 4px 0;
+}
+.cookbook-checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ cursor: pointer;
+ user-select: none;
+}
+.cookbook-checkbox-label:hover { color: var(--fg); }
+.cookbook-checkbox {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 30px;
+ height: 16px;
+ background: color-mix(in srgb, var(--fg) 15%, transparent);
+ border-radius: 8px;
+ position: relative;
+ cursor: pointer;
+ transition: background 0.08s;
+ flex-shrink: 0;
+}
+.cookbook-checkbox::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--panel);
+ box-shadow: 0 1px 2px rgba(0,0,0,0.25);
+ transition: transform 0.08s;
+}
+.cookbook-checkbox:checked {
+ background: var(--red);
+}
+
+/* GPU toggle grid */
+.cookbook-gpu-row {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 8px;
+}
+.cookbook-gpu-label {
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ margin-right: 4px;
+ white-space: nowrap;
+}
+.cookbook-gpu-btn {
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ font-size: 11px;
+ font-family: inherit;
+ font-weight: 600;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+ color: color-mix(in srgb, var(--fg) 40%, transparent);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.cookbook-gpu-btn:hover {
+ color: var(--fg);
+ border-color: var(--red);
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.cookbook-gpu-btn.active {
+ background: var(--red);
+ color: var(--panel);
+ border-color: var(--red);
+}
+
+/* Instance / Save buttons */
+.cookbook-add-instance-btn,
+.cookbook-save-btn {
+ background: transparent;
+ border: 1px dashed color-mix(in srgb, var(--accent) 50%, transparent);
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ font-size: 11px;
+}
+.cookbook-add-instance-btn:hover,
+.cookbook-save-btn:hover {
+ border-color: var(--accent);
+ color: var(--fg);
+}
+.cookbook-remove-instance-btn {
+ background: transparent;
+ border: none;
+ color: color-mix(in srgb, var(--fg) 40%, transparent);
+ font-size: 12px;
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+}
+.cookbook-remove-instance-btn:hover { color: var(--red); }
+
+/* Preset chips */
+/* Saved card styling */
+.cookbook-saved-card .cookbook-card-desc {
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.cookbook-delete-preset-btn {
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+ font-size: 12px;
+}
+.cookbook-delete-preset-btn:hover {
+ color: var(--red);
+}
+
+
+/* Kill button */
+.cookbook-kill-btn {
+ border-color: var(--color-error);
+ color: var(--color-error);
+ background: transparent;
+}
+.cookbook-kill-btn:hover {
+ background: var(--color-error);
+ color: var(--panel);
+ border-color: var(--color-error);
+}
+
+/* Error diagnosis banner */
+.cookbook-diagnosis {
+ margin-top: 6px;
+ padding: 10px 12px;
+ background: color-mix(in srgb, var(--color-error) 8%, transparent);
+ border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
+ border-radius: 6px;
+ /* The diagnosis body can carry traceback fragments and long unbroken
+ paths (e.g. /home/.../snapshots//.gguf). Without these,
+ a single long token pushes the card wider than the cookbook modal,
+ scrolling the row right and clipping the action buttons. */
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+.cookbook-diagnosis pre,
+.cookbook-diagnosis code {
+ white-space: pre-wrap;
+ word-break: break-word;
+ overflow-wrap: anywhere;
+ max-width: 100%;
+}
+.cookbook-diag-header {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ position: relative;
+ top: -4px;
+ margin-bottom: -4px;
+}
+.cookbook-diag-fold {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0;
+ min-height: 0;
+ border: 0;
+ background: transparent;
+ color: var(--color-error);
+ font: inherit;
+ font-size: 11px;
+ font-weight: 700;
+ cursor: pointer;
+ margin-right: auto;
+}
+.cookbook-diag-fold:hover {
+ background: transparent;
+ color: var(--color-error);
+ opacity: 0.85;
+}
+.cookbook-diag-chevron {
+ display: inline-block;
+ width: 10px;
+ font-size: 10px;
+}
+.cookbook-diag-copy {
+ border: 0;
+ background: transparent;
+ color: var(--fg-muted);
+ padding: 0 2px;
+ width: 18px;
+ height: 18px;
+ min-height: 18px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.cookbook-diag-copy:hover {
+ background: transparent;
+ color: var(--fg);
+}
+.cookbook-diag-copy.copied {
+ color: var(--green, #50fa7b);
+}
+.cookbook-diag-copy svg {
+ display: block;
+}
+.cookbook-diag-dismiss {
+ border: 0;
+ background: transparent;
+ color: var(--fg-muted);
+ padding: 0;
+ width: 16px;
+ height: 18px;
+ min-height: 18px;
+ line-height: 16px;
+ font-size: 13px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ top: -2px;
+}
+.cookbook-diag-dismiss:hover {
+ background: transparent;
+ color: var(--color-error);
+}
+.cookbook-diag-message {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-error);
+ margin-bottom: 4px;
+ margin-left: 2px;
+ user-select: text;
+}
+.cookbook-diag-suggestion {
+ font-size: 11px;
+ line-height: 1.35;
+ color: var(--fg-muted);
+ margin-bottom: 8px;
+ margin-left: 2px;
+ user-select: text;
+}
+.cookbook-diag-fixes {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+.cookbook-diag-actions {
+ position: relative;
+ display: inline-flex;
+}
+.cookbook-diag-action-trigger {
+ font-size: 11px;
+ padding: 4px 10px;
+ min-height: 24px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--color-error) 40%, transparent);
+ color: var(--fg);
+}
+.cookbook-diag-action-trigger:hover {
+ border-color: var(--color-error);
+ background: color-mix(in srgb, var(--color-error) 12%, transparent);
+}
+.cookbook-diag-menu {
+ position: absolute;
+ left: 0;
+ top: calc(100% + 4px);
+ min-width: 180px;
+ z-index: 80;
+}
+.cookbook-diag-menu button {
+ width: 100%;
+ justify-content: flex-start;
+ text-align: left;
+ white-space: nowrap;
+}
+.cookbook-diag-btn {
+ font-size: 11px;
+ padding: 4px 10px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--color-error) 40%, transparent);
+ color: var(--fg);
+}
+.cookbook-diag-btn:hover {
+ border-color: var(--color-error);
+ background: color-mix(in srgb, var(--color-error) 12%, transparent);
+}
+/* Icons on the left of diagnosis action buttons (Retry / Copy / Edit / etc.). */
+.cookbook-diag-btn,
+.cookbook-diag-menu button {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.cookbook-diag-btn-ico {
+ flex-shrink: 0;
+ opacity: 0.9;
+}
+
+/* ── What Fits? (hardware model fitting tab in cookbook) ── */
+.cookbook-group.hidden { display: none !important; }
+
+/* Section titles */
+.cookbook-section-title {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--fg);
+ opacity: 0.7;
+ margin: 10px 0 4px;
+}
+.cookbook-section-title:first-child { margin-top: 0; }
+
+/* Download input */
+.cookbook-dl-input {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 4px;
+ align-items: stretch;
+}
+.cookbook-dl-repo {
+ flex: 1;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: 'Fira Code', monospace;
+ font-size: 12px;
+ padding: 5px 8px;
+}
+.cookbook-dl-repo:focus {
+ border-color: var(--accent, var(--red));
+ outline: none;
+}
+.cookbook-dl-repo::placeholder {
+ color: var(--fg-muted);
+ opacity: 0.5;
+ font-family: inherit;
+}
+/* Brief border+glow flash when an Ollama row in the hwfit list autofills the
+ Download input — helps the user see what landed when the input is offscreen
+ or above a tall list. */
+.cookbook-dl-repo.cookbook-dl-flash {
+ border-color: var(--red) !important;
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--red) 25%, transparent) !important;
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+.cookbook-dl-btn {
+ background: var(--accent, var(--red));
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ padding: 0 14px;
+ height: 28px;
+ font-size: 11px;
+ font-family: inherit;
+ cursor: pointer;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ margin-top: -4px;
+}
+.cookbook-dl-btn:hover {
+ opacity: 0.9;
+}
+.cookbook-dl-gguf-row {
+ margin-top: -1px;
+ gap: 5px;
+ align-items: center;
+ justify-content: flex-end;
+ font-size: 11px;
+ position: relative;
+ top: -2px;
+}
+.cookbook-dl-gguf-label {
+ opacity: 0.65;
+ flex-shrink: 0;
+}
+#cookbook-dl-gguf-quant {
+ height: 28px;
+ min-width: 118px;
+ flex: 0 0 auto;
+}
+#cookbook-dl-gguf-note {
+ opacity: 0.55;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 240px;
+}
+
+/* HF link in search panel */
+.hwfit-panel-hf-link {
+ font-size: 10px;
+ color: var(--red);
+ text-decoration: none;
+ margin-left: auto;
+ padding: 2px 6px;
+ border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
+ border-radius: 3px;
+ transition: all 0.15s;
+}
+.hwfit-panel-hf-link:hover {
+ color: #fff;
+ background: var(--red);
+ border-color: var(--red);
+}
+
+.cookbook-server-toggle {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ user-select: none;
+ list-style: none;
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-size: 11px;
+ color: var(--fg-muted);
+ transition: background 0.1s, color 0.1s;
+}
+.cookbook-server-toggle:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
+.cookbook-server-toggle svg {
+ opacity: 0.5;
+ transition: transform 0.2s;
+}
+.cookbook-server-details[open] .cookbook-server-toggle svg {
+ transform: rotate(45deg);
+}
+.cookbook-settings-content {
+ padding: 6px 0;
+}
+.cookbook-settings-row {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ margin-bottom: 8px;
+}
+.cookbook-settings-label {
+ font-size: 10px;
+ color: var(--fg-muted);
+}
+.cookbook-settings-hint {
+ opacity: 0.5;
+ font-weight: 400;
+}
+.cookbook-settings-input {
+ width: 100%;
+ font-size: 11px;
+ padding: 4px 6px;
+}
+.cookbook-settings-subtitle {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--fg-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ margin: 8px 0 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.cookbook-server-add {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ color: var(--fg-muted);
+ font-size: 13px;
+ width: 16px;
+ height: 16px;
+ position: relative;
+ top: -3px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ line-height: 0;
+ padding-bottom: 3px;
+ line-height: 16px;
+ text-align: center;
+ transition: color 0.1s, border-color 0.1s;
+}
+.cookbook-server-add:hover {
+ color: var(--fg);
+ border-color: var(--fg);
+}
+.cookbook-server-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+}
+.cookbook-srv-status {
+ flex: 0 0 auto;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #777;
+ transition: background 0.2s, box-shadow 0.2s, transform 0.15s;
+ cursor: pointer;
+}
+.cookbook-srv-status.testing {
+ background: var(--color-warning, #f0ad4e);
+ animation: cookbook-srv-pulse 0.9s ease-in-out infinite;
+}
+.cookbook-srv-status.ok {
+ background: var(--cookbook-server-color, var(--color-success, #50fa7b));
+ /* Soft outer ring + breathing halo so active servers glow like LEDs. */
+ box-shadow:
+ 0 0 0 2px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 25%, transparent),
+ 0 0 10px 2px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 55%, transparent);
+ animation: cookbook-srv-glow-ok 2.4s ease-in-out infinite;
+}
+.cookbook-srv-status.fail {
+ background: var(--red, #e06c75);
+ box-shadow:
+ 0 0 0 2px color-mix(in srgb, var(--red, #e06c75) 25%, transparent),
+ 0 0 10px 2px color-mix(in srgb, var(--red, #e06c75) 55%, transparent);
+}
+@keyframes cookbook-srv-glow-ok {
+ 0%, 100% {
+ box-shadow:
+ 0 0 0 2px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 22%, transparent),
+ 0 0 8px 1px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 45%, transparent);
+ }
+ 50% {
+ box-shadow:
+ 0 0 0 3px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 38%, transparent),
+ 0 0 14px 3px color-mix(in srgb, var(--cookbook-server-color, var(--color-success, #50fa7b)) 75%, transparent);
+ }
+}
+.cookbook-server-row input.hwfit-sf,
+.cookbook-server-row select.hwfit-sf {
+ font-size: 11px;
+ padding: 0 6px;
+ margin: 0;
+ height: 24px;
+ min-height: 24px;
+ max-height: 24px;
+ line-height: 24px;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font-family: inherit;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+.cookbook-server-row select.hwfit-sf {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 6px center;
+ padding-right: 18px;
+}
+/* Each server is its own card block (mirrors the Running tab's task cards):
+ full border + accent left-rail + rounded corners + subtle fill, spaced. */
+.cookbook-server-entry {
+ position: relative;
+ margin-bottom: 8px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--cookbook-server-color, color-mix(in srgb, var(--fg) 30%, transparent));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ box-sizing: border-box;
+}
+.cookbook-server-entry:last-child { margin-bottom: 0; }
+.cookbook-server-row .cookbook-srv-color-wrap {
+ width: 28px;
+ flex-shrink: 0;
+ flex-grow: 0;
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ color: var(--cookbook-server-color, var(--fg-muted));
+}
+.cookbook-server-row .cookbook-srv-color-dot {
+ position: static;
+ transform: none;
+ pointer-events: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: currentColor;
+ opacity: 0.55;
+ z-index: 1;
+}
+.cookbook-server-row .cookbook-srv-color-wrap.has-color .cookbook-srv-color-dot {
+ opacity: 1;
+}
+.cookbook-server-row .cookbook-srv-color-btn {
+ width: 26px;
+ height: 26px;
+ padding: 0 !important;
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ border-radius: 50%;
+ top: -2px;
+}
+.cookbook-server-row .cookbook-srv-color-btn:hover {
+ background: color-mix(in srgb, var(--cookbook-server-color, var(--fg)) 14%, transparent);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cookbook-server-color, var(--fg)) 18%, transparent);
+}
+.cookbook-server-row .cookbook-srv-color-caret {
+ position: absolute;
+ right: 6px;
+ top: 50%;
+ transform: translateY(-50%);
+ opacity: 0.65;
+}
+.cookbook-server-row .cookbook-srv-color-wrap.has-color .cookbook-srv-color-btn {
+ color: var(--fg);
+ border-color: transparent;
+ background-color: transparent;
+ box-shadow: none;
+}
+.cookbook-server-row .cookbook-srv-color-wrap.has-color .cookbook-srv-color-dot {
+ background: var(--cookbook-server-color, currentColor);
+}
+.cookbook-server-row .cookbook-srv-color-wrap.has-color .cookbook-srv-color-caret {
+ color: var(--fg-muted);
+}
+.cookbook-srv-color-menu {
+ position: absolute;
+ z-index: 5000;
+ top: calc(100% + 4px);
+ left: 0;
+ width: 138px;
+ padding: 4px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+ box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent);
+}
+.cookbook-srv-color-menu.hidden {
+ display: none;
+}
+.cookbook-srv-color-item {
+ width: 100%;
+ height: 24px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--swatch-color, var(--fg)) 10%, transparent);
+ color: var(--fg);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 7px;
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ text-align: left;
+}
+.cookbook-srv-color-item:hover,
+.cookbook-srv-color-item.active {
+ border-color: color-mix(in srgb, var(--swatch-color, var(--accent, var(--red))) 55%, var(--border));
+ background: color-mix(in srgb, var(--swatch-color, var(--accent, var(--red))) 18%, var(--bg));
+}
+.cookbook-srv-color-item-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--swatch-color, var(--fg-muted));
+ border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ flex-shrink: 0;
+}
+/* Normalize every control on a server row to the same 24-px height — with
+ many servers the small per-control height drift was very visible. */
+.cookbook-server-row > * {
+ height: 24px;
+ box-sizing: border-box;
+ flex-shrink: 0;
+}
+.cookbook-server-row > input.hwfit-sf,
+.cookbook-server-row > select.hwfit-sf,
+.cookbook-server-row > button,
+.cookbook-server-row .cookbook-srv-actions > button {
+ height: 24px;
+ line-height: 22px;
+ padding: 0 8px;
+ font-size: 11px;
+ box-sizing: border-box;
+}
+.cookbook-server-row .cookbook-srv-actions > .close-btn { width: 24px; padding: 0; line-height: 22px; }
+.cookbook-server-row .cookbook-dep-tag { display: inline-flex; align-items: center; height: 24px; padding: 0 6px; line-height: 1; }
+.cookbook-server-rm {
+ width: 22px;
+ height: 22px;
+ font-size: 10px;
+ opacity: 0.55;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: opacity 0.12s, background 0.12s, color 0.12s, border-color 0.12s;
+}
+.cookbook-server-rm:hover {
+ opacity: 1;
+ background: var(--red);
+ color: #fff;
+ border-color: var(--red);
+}
+/* Labelled "Delete server" variant (lives in the Model Directory row) — override
+ the 22x22 icon box with a normal text button. */
+.cookbook-server-rm-btn {
+ width: auto;
+ height: auto;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-family: inherit;
+ background: none;
+ color: var(--red);
+ border-color: color-mix(in srgb, var(--red) 40%, var(--border));
+ white-space: nowrap;
+ position: relative;
+ top: -3px;
+}
+/* The "+" glyph in the Servers-header "+ Add" button, nudged up 1px (scoped so
+ the shared calendar +New pill is unaffected). */
+#cookbook-server-add .cal-add-plus { position: relative; top: -1px; }
+/* Save button on a new server entry — same shape as Delete, accent-colored;
+ turns green once saved. */
+.cookbook-server-save-btn {
+ width: auto;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-family: inherit;
+ cursor: pointer;
+ background: none;
+ color: var(--accent, var(--red));
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
+ border-radius: 4px;
+ white-space: nowrap;
+}
+.cookbook-server-save-btn:hover { background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent); }
+/* Connection controls use the same compact typography as the server's
+ Delete and Save actions, despite living in the title row. */
+.cookbook-server-check-btn,
+.cookbook-server-key-btn {
+ padding: 2px 8px;
+ font-size: 10px;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1;
+ white-space: nowrap;
+}
+/* Cancel (discard a new server) — same shape/size as Save, muted. */
+.cookbook-server-cancel-btn {
+ width: auto;
+ padding: 2px 8px;
+ font-size: 10px;
+ font-family: inherit;
+ cursor: pointer;
+ background: none;
+ color: var(--fg-muted, var(--fg));
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ white-space: nowrap;
+}
+.cookbook-server-cancel-btn:hover { color: var(--fg); border-color: var(--fg); }
+.cookbook-server-save-btn.saved {
+ color: var(--green, #50fa7b);
+ border-color: color-mix(in srgb, var(--green, #50fa7b) 45%, var(--border));
+ cursor: default;
+}
+.cookbook-path-row {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 3px;
+ align-items: center;
+}
+.cookbook-path-input {
+ flex: 1;
+}
+.cookbook-path-add {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 3px;
+ color: var(--fg-muted);
+ font-size: 12px;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ line-height: 1;
+ transition: color 0.1s, border-color 0.1s;
+}
+.cookbook-path-add:hover {
+ color: var(--fg);
+ border-color: var(--fg);
+}
+
+/* Server selector in search toolbar */
+.hwfit-server-select {
+ min-width: 70px;
+}
+/* GPU toggle buttons */
+.hwfit-gpu-toggles {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ position: relative;
+ top: -3px;
+}
+.hwfit-gpu-btn {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg-muted);
+ font-size: 10px;
+ padding: 0 5px;
+ cursor: pointer;
+ min-width: 20px;
+ height: 28px;
+ box-sizing: border-box;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
+ font-family: inherit;
+}
+.hwfit-gpu-btn:hover {
+ color: var(--fg);
+ border-color: var(--fg);
+}
+.hwfit-gpu-btn.active {
+ background: var(--accent, var(--red));
+ color: #fff;
+ border-color: var(--accent, var(--red));
+}
+/* Pool selector for heterogeneous GPU boxes — sits left of the RAM/GPU buttons */
+.hwfit-gpu-group {
+ background: var(--bg-elev, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-size: 10px;
+ height: 28px;
+ padding: 0 4px;
+ cursor: pointer;
+ box-sizing: border-box;
+ font-family: inherit;
+ max-width: 190px;
+}
+.hwfit-gpu-group:hover { border-color: var(--fg); }
+@keyframes cookbookCmdFlash {
+ 0% { box-shadow: 0 0 0 0 var(--accent, var(--red)); border-color: var(--accent, var(--red)); }
+ 30% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 40%, transparent); border-color: var(--accent, var(--red)); }
+ 100% { box-shadow: 0 0 0 0 transparent; }
+}
+/* "Confirmed working" tick on a saved serve config (auto-saved once its endpoint registered) */
+.cookbook-saved-confirmed {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+}
+.hwfit-container { display: flex; flex-direction: column; gap: 8px; }
+.hwfit-toolbar {
+ display: flex; gap: 4px; align-items: center; flex-wrap: wrap;
+}
+.hwfit-toolbar .spinner-whirlpool canvas {
+ position: relative;
+ top: -2px;
+}
+#hwfit-cache-scan .spinner-whirlpool canvas {
+ position: relative;
+ top: -2px;
+}
+.hwfit-toolbar select,
+.hwfit-toolbar input {
+ height: 28px;
+ padding: 0 6px;
+ font-size: 11px;
+ font-family: inherit;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+.hwfit-toolbar select:focus,
+.hwfit-toolbar input:focus { outline: none; border-color: var(--red); }
+.hwfit-toolbar .hwfit-usecase-btn {
+ min-width: 0;
+ width: 108px;
+ flex-shrink: 0;
+ font-size: 10px;
+ top: -3px;
+}
+.hwfit-toolbar .hwfit-quant { min-width: 0; width: 78px; flex-shrink: 0; font-size: 10px; }
+.hwfit-toolbar .hwfit-engine-btn { min-width: 0; width: 116px; flex-shrink: 0; font-size: 10px; }
+.hwfit-advanced-panel {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+ margin-top: 6px;
+ padding: 7px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.hwfit-advanced-panel.hidden { display: none; }
+.hwfit-advanced-panel .hwfit-engine-btn {
+ top: -3px;
+ width: 116px;
+ min-width: 116px;
+ box-sizing: border-box;
+ font-size: 11px;
+}
+.hwfit-advanced-panel .hwfit-quant {
+ width: 78px;
+ min-width: 78px;
+ height: 28px;
+ box-sizing: border-box;
+ padding: 0 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ position: relative;
+ top: 0;
+}
+.hwfit-advanced-panel .hwfit-quant option {
+ background: var(--panel, var(--bg));
+ color: var(--fg);
+}
+.hwfit-advanced-btn.active {
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+
+/* Dot inside the Fit column header — click to toggle the fit-only filter
+ (off = show too-tight rows; on = hide them). */
+.hwfit-fit-dot {
+ display: inline-block;
+ margin-right: 4px;
+ font-size: 8px;
+ line-height: 1;
+ color: color-mix(in srgb, var(--fg) 35%, transparent);
+ cursor: pointer;
+ vertical-align: middle;
+ position: relative;
+ top: -1px; /* nudge 1px up so the small dot sits centered with the "Fit" caps */
+ transition: color 0.12s ease, text-shadow 0.12s ease;
+}
+/* Quant suffix appended to model names when the storage format isn't in the
+ repo id — e.g. "(FP4-MoE-Mixed)" after DeepSeek-V4-Flash. Muted to read as
+ metadata, not part of the name. */
+.hwfit-name-quant {
+ font-size: 0.78em;
+ opacity: 0.55;
+ font-weight: 400;
+ margin-left: 4px;
+}
+.hwfit-fit-dot:hover { color: var(--accent, var(--red)); }
+.hwfit-fit-dot.active {
+ color: var(--green, #50fa7b);
+ text-shadow: 0 0 4px color-mix(in srgb, var(--green, #50fa7b) 55%, transparent);
+}
+.hwfit-help-chip {
+ width: 14px;
+ height: 14px;
+ flex: 0 0 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 1;
+ cursor: help;
+ position: relative;
+ top: -1px;
+ margin-left: -1px;
+}
+.hwfit-help-chip:hover {
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--fg) 45%, transparent);
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.hwfit-help-chip-inline {
+ margin-left: -2px;
+ margin-right: 0;
+ top: 0; /* parent rule sets top:-1px; nudge inline variant 1px lower */
+}
+/* Quant select + inline ? wrapper — the ? sits inside the dropdown's bordered
+ box, anchored on the right just left of the chevron. */
+.hwfit-quant-wrap, .hwfit-engine-wrap, .hwfit-usecase-wrap {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+.hwfit-engine-btn,
+.hwfit-usecase-btn {
+ height: 28px;
+ padding: 0 30px 0 7px;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--fg);
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ cursor: pointer;
+ text-align: left;
+ position: relative;
+ top: -7px;
+}
+.hwfit-engine-btn-icon,
+.hwfit-engine-item-icon,
+.hwfit-usecase-btn-icon,
+.hwfit-usecase-item-icon {
+ width: 14px;
+ height: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.hwfit-engine-btn-label,
+.hwfit-usecase-btn-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.hwfit-engine-caret,
+.hwfit-usecase-caret {
+ position: absolute;
+ right: 6px;
+ opacity: 0.55;
+ pointer-events: none;
+}
+.hwfit-engine-menu,
+.hwfit-usecase-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ z-index: 120;
+ padding: 4px;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 6px 20px rgba(0,0,0,0.22);
+}
+.hwfit-engine-item,
+.hwfit-usecase-item {
+ width: 100%;
+ min-height: 26px;
+ padding: 5px 7px;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ color: var(--fg);
+ background: transparent;
+ border: 0;
+ border-radius: 5px;
+ font: inherit;
+ font-size: 11px;
+ text-align: left;
+ cursor: pointer;
+ box-sizing: border-box;
+}
+.hwfit-engine-item:hover,
+.hwfit-engine-item.active,
+.hwfit-usecase-item:hover,
+.hwfit-usecase-item.active {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.hwfit-engine-item-label,
+.hwfit-usecase-item-label {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.hwfit-quant-wrap .hwfit-quant-help,
+.hwfit-engine-wrap .hwfit-engine-help {
+ position: absolute;
+ right: 20px; /* sits just left of the native select chevron */
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: auto;
+ margin: 0;
+}
+.hwfit-ctx-control {
+ height: 28px;
+ min-width: 134px;
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg-muted);
+ background: var(--bg);
+ font-size: 10px;
+ box-sizing: border-box;
+}
+.hwfit-ctx-control span {
+ /* Match Quant/Engine select label style: no uppercase, no letter-spacing. */
+ text-transform: none;
+ letter-spacing: 0;
+ opacity: 0.9;
+}
+.hwfit-ctx-control input[type="range"] {
+ width: 64px;
+ min-width: 64px;
+ height: 4px;
+ padding: 0;
+ border: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ /* Hardcoded grey rail — was background:transparent here, which was the
+ LATER-in-cascade override that kept making the rail invisible. */
+ background: rgba(150, 150, 150, 0.65) !important;
+ border-radius: 999px;
+ accent-color: var(--accent, var(--red));
+}
+.hwfit-ctx-control output {
+ min-width: 28px;
+ text-align: right;
+ color: var(--fg);
+ font-weight: 600;
+}
+.hwfit-server-toggle { flex-shrink: 0; font-size: 10px !important; padding: 3px 8px !important; white-space: nowrap; }
+.hwfit-env-row { gap: 6px; flex-wrap: wrap; }
+.hwfit-hw {
+ display: flex; flex-wrap: wrap; column-gap: 4px; row-gap: 7px; padding: 4px 0;
+}
+.hwfit-hw-chip {
+ font-size: 10px; padding: 0 8px; border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--fg); opacity: 0.7; white-space: nowrap;
+ border: 0;
+ font-family: inherit;
+ line-height: 1;
+ height: 17px;
+ box-sizing: border-box;
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ /* Cap chip width so a long label (e.g. heterogeneous GPU group
+ "1× RTX 4090 + 1× RTX 3060") wraps to the next row instead of
+ overflowing the modal. Full text stays in the tooltip. */
+ max-width: 100%;
+}
+.hwfit-hw-chip-toggle {
+ /* Allow the chip body to truncate with an ellipsis when the chip
+ itself is capped at its container's width. Without this, the
+ toggle button keeps its intrinsic width and pushes the × button
+ off-screen on narrow viewports. */
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.hwfit-hw-chip button,
+.hwfit-hw-chip-dismiss,
+.hwfit-hw-chip-manual,
+.hwfit-hw-chip-toggle,
+.hwfit-hw-chip-x {
+ appearance: none;
+ -webkit-appearance: none;
+ background: none;
+ border: 0;
+ color: inherit;
+ font: inherit;
+ padding: 0;
+ /* Inherit the chip's line-height so a slightly larger × glyph
+ doesn't push past the 17px chip height (which was clipping the
+ text vertically in the multi-button layout). */
+ line-height: inherit;
+}
+.hwfit-chip-x {
+ display: inline-block;
+ transform: translateY(-1px);
+}
+.hwfit-hw-chip-dismiss:hover { opacity: 1; }
+/* Two-part chip: text body toggles dim on click, × removes the chip.
+ Visual minimal — just a slightly bigger × so it's easier to hit. */
+.hwfit-hw-chip-toggle {
+ cursor: pointer;
+ /* `!important` because the earlier `.hwfit-hw-chip button { font:
+ inherit }` reset (same specificity, defined later in source)
+ was winning over the plain rule and leaving the manual chip's
+ button at the browser-default ~13px. Forcing it locally so the
+ manual chip's text matches every other chip's text. */
+ font-size: 10px !important;
+ line-height: 1 !important;
+ transform: translateY(-3px);
+}
+.hwfit-hw-chip-x {
+ cursor: pointer;
+ /* `!important` for the same cascade reason as the toggle button —
+ the `.hwfit-hw-chip button { font: inherit }` reset was leaving
+ the regular chips' × inheriting the chip's 10px while the
+ manual chip's × landed on the browser default (~13px), making
+ manual look bigger. Lock every × at 13px. */
+ font-size: 11px !important;
+ line-height: 1 !important;
+ transform: translateY(-3px);
+}
+.hwfit-hw-chip-x:hover {
+ opacity: 0.75;
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+}
+.hwfit-hw-chip-off {
+ /* Ghosted: drop the pill background so it reads as "off" rather than
+ "selected but faded". Lower text opacity matches the muted feel. */
+ background: transparent !important;
+ opacity: 0.4;
+}
+.hwfit-hw-chip-manual {
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ color: var(--accent, var(--red));
+ opacity: 1;
+ cursor: pointer;
+ font-family: inherit;
+ margin-inline: 3px;
+ padding-left: 7px;
+ padding-right: 7px;
+}
+.hwfit-hw-chip-manual:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+}
+.hwfit-hw-manual-btn {
+ min-width: 42px;
+}
+.hwfit-manual-panel {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+ width: 100%;
+ margin-top: 6px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ box-sizing: border-box;
+}
+.hwfit-manual-panel.hidden { display: none; }
+.hwfit-manual-panel label {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 9px;
+ color: var(--fg-muted);
+}
+.hwfit-manual-panel select,
+.hwfit-manual-panel input,
+.hwfit-manual-panel button {
+ height: 24px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 10px;
+ padding: 0 6px;
+ box-sizing: border-box;
+}
+.hwfit-manual-panel input { width: 72px; }
+.hwfit-manual-panel button { cursor: pointer; }
+.hwfit-manual-panel button svg {
+ flex: 0 0 auto;
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+}
+.hwfit-manual-panel .hwfit-hw-manual-clear svg,
+.hwfit-manual-panel .hwfit-hw-manual-save svg {
+ position: relative;
+ top: 2px;
+ color: var(--accent, var(--red));
+}
+.hwfit-hw-manual-btn svg,
+.hwfit-manual-panel .hwfit-hw-manual-clear svg {
+ color: var(--accent, var(--red));
+}
+.hwfit-manual-panel .hwfit-hw-manual-clear > span,
+.hwfit-manual-panel .hwfit-hw-manual-save > span {
+ position: relative;
+ top: -2px;
+}
+.hwfit-manual-panel .hwfit-hw-manual-save {
+ margin-left: auto;
+}
+.hwfit-manual-panel button:hover { border-color: var(--fg); }
+/* GPU driver error (e.g. NVML version mismatch) — stands out from the muted
+ chips and hints there's a real problem, with the full message on hover. */
+.hwfit-hw-chip-error {
+ background: color-mix(in srgb, var(--red) 16%, transparent);
+ color: var(--red); opacity: 1; cursor: help;
+ border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
+}
+.hwfit-list {
+ max-height: 52vh; overflow-y: auto; display: flex; flex-direction: column; gap: 2px;
+}
+.hwfit-loading {
+ display: flex; align-items: center; justify-content: center;
+ color: var(--fg-muted); padding: 16px 0; font-size: 12px;
+}
+.hwfit-hw-visibility-warning {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ text-align: left;
+ margin-top: 8px;
+}
+.hwfit-hw-visibility-warning-title {
+ font-weight: 600;
+}
+.hwfit-hw-visibility-warning-body {
+ opacity: 0.78;
+ line-height: 1.45;
+}
+.hwfit-hw-visibility-warning-actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+.hwfit-row {
+ display: flex; align-items: center; gap: 6px; padding: 5px 8px;
+ border-radius: 6px; cursor: pointer; font-size: 11px;
+ transition: background 0.1s;
+}
+.hwfit-row:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }
+/* Scan/Download model rows enter like the other library lists. Keep the
+ column header static and only replay this when the list had no model rows,
+ so ordinary filtering and sorting remain instant. */
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:not(.hwfit-header) {
+ animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
+}
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(2) { animation-delay: 0.04s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(3) { animation-delay: 0.08s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(4) { animation-delay: 0.12s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(5) { animation-delay: 0.16s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(6) { animation-delay: 0.20s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(7) { animation-delay: 0.24s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(8) { animation-delay: 0.28s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(9) { animation-delay: 0.32s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(10) { animation-delay: 0.36s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(11) { animation-delay: 0.40s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(12) { animation-delay: 0.44s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(13) { animation-delay: 0.48s; }
+.hwfit-list.cookbook-hwfit-models-just-loaded > .hwfit-row:nth-child(n+14) { animation-delay: 0.48s; }
+/* Already-downloaded rows: dim slightly so attention falls on undownloaded
+ options. The green dot stays bright as the "this is local" cue. */
+.hwfit-row:has(.hwfit-dl-dot) { opacity: 0.55; }
+.hwfit-header {
+ cursor: default; position: sticky; top: 0; z-index: 1;
+ background: var(--panel); border-bottom: 1px solid var(--border);
+ padding: 4px 8px; font-weight: 600;
+}
+.hwfit-header:hover { background: var(--panel); }
+.hwfit-header .hwfit-col { font-size: 9px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.3px; }
+.hwfit-sortable:hover { color: var(--fg) !important; }
+.hwfit-sort-active { color: var(--red) !important; }
+.hwfit-col {
+ flex-shrink: 0; white-space: nowrap; text-align: left;
+ font-size: 9px; color: var(--fg-muted);
+}
+.hwfit-fit { width: 52px; font-weight: 700; text-transform: uppercase; }
+.hwfit-name {
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
+ font-weight: 500; font-size: 11px; color: var(--fg);
+}
+.hwfit-c-score { width: 40px; font-weight: 700; font-size: 11px; color: var(--fg); }
+.hwfit-moe {
+ display: inline-block; padding: 0 4px; border-radius: 4px; margin-left: 4px;
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ color: var(--red); font-weight: 600; font-size: 8px;
+}
+.hwfit-sort, .hwfit-quant { width: auto; min-width: 70px; flex-shrink: 0; }
+.hwfit-row-active { background: color-mix(in srgb, var(--red) 8%, transparent); }
+
+/* ── Inline action panel (expands below a model row) ── */
+.hwfit-action-panel {
+ border: 1px solid var(--border); border-left: 3px solid var(--red);
+ border-radius: 0 6px 6px 6px; background: var(--panel);
+ padding: 10px 12px; margin: 2px 0 6px; font-size: 11px;
+ display: flex; flex-direction: column; gap: 8px;
+ animation: hwfit-panel-in 0.15s ease-out;
+}
+.hwfit-panel-header {
+ display: flex; align-items: center; gap: 8px; min-width: 0;
+}
+.hwfit-panel-model {
+ font-size: 11px; font-weight: 600; color: var(--fg);
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
+}
+.hwfit-panel-badge {
+ font-size: 9px; padding: 2px 8px; border-radius: 10px;
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+ color: var(--red); font-weight: 600; flex-shrink: 0;
+}
+.hwfit-panel-fields {
+ display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
+}
+.hwfit-panel-fields label, .hwfit-adv-grid label {
+ display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--fg-muted); white-space: nowrap;
+}
+.hwfit-panel-fields input, .hwfit-panel-fields select,
+.hwfit-adv-grid input, .hwfit-adv-grid select {
+ background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
+ color: var(--fg); font-size: 11px; padding: 3px 6px; font-family: inherit;
+ outline: none; transition: border-color 0.15s;
+}
+.hwfit-panel-fields input:focus, .hwfit-adv-grid input:focus,
+.hwfit-panel-fields select:focus, .hwfit-adv-grid select:focus {
+ border-color: var(--red);
+}
+.hwfit-panel-fields input[type="text"] { width: 60px; }
+.hwfit-adv-grid input[type="text"] { width: 80px; }
+.hwfit-adv-grid {
+ display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 6px 0;
+}
+.hwfit-cb input[type="checkbox"] { margin: 0 2px 0 0; }
+.hwfit-panel-advanced, .hwfit-panel-settings {
+ font-size: 10px;
+}
+.hwfit-panel-advanced summary, .hwfit-panel-settings summary {
+ cursor: pointer; color: var(--fg-muted); font-size: 10px;
+ user-select: none; padding: 2px 0;
+}
+.hwfit-panel-advanced summary:hover, .hwfit-panel-settings summary:hover { color: var(--fg); }
+.hwfit-panel-cmd {
+ font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
+ font-size: 10px; padding: 6px 8px; border-radius: 4px;
+ background: var(--bg); border: 1px solid var(--border);
+ white-space: pre-wrap; word-break: break-all; max-height: 60px; overflow-y: auto;
+ color: var(--fg-muted);
+}
+.hwfit-panel-actions {
+ display: flex; gap: 4px; flex-wrap: wrap;
+}
+.hwfit-panel-note {
+ font-size: 10px;
+ line-height: 1.35;
+ color: var(--fg-muted);
+ background: color-mix(in srgb, var(--yellow, #f1fa8c) 8%, transparent);
+ border: 1px solid color-mix(in srgb, var(--yellow, #f1fa8c) 18%, var(--border));
+ border-radius: 4px;
+ padding: 5px 7px;
+}
+
+/* ── Saved presets ── */
+.hwfit-preset {
+ display: flex; align-items: center; gap: 8px; padding: 6px 10px;
+ border: 1px solid var(--border); border-radius: 6px; font-size: 11px;
+ transition: border-color 0.15s;
+}
+.hwfit-preset:hover { border-color: var(--red); }
+.hwfit-preset-name { font-weight: 600; color: var(--fg); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.hwfit-preset-model { font-size: 9px; color: var(--fg-muted); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.hwfit-preset-backend { font-size: 9px; padding: 1px 6px; border-radius: 8px; background: color-mix(in srgb, var(--fg) 8%, transparent); color: var(--fg-muted); }
+
+@media (max-width: 600px) {
+ .hwfit-c-ctx, .hwfit-c-speed, .hwfit-c-mode { display: none; }
+ .hwfit-panel-fields { flex-direction: column; align-items: stretch; }
+ .hwfit-panel-fields input[type="text"] { width: 100%; }
+ .hwfit-preset-model { display: none; }
+}
+
+/* ===== Settings Modal Layout ===== */
+.settings-modal-content {
+ width: min(1040px, 94vw);
+ max-height: 85vh;
+ padding: 0;
+ container-type: inline-size;
+ container-name: settings-modal;
+}
+
+/* Issue #208 — anchor the Settings window to the TOP of the chat area instead
+ of vertically centering it. The base .modal uses `align-items:center`, so a
+ centered window grows and shrinks around its own midpoint when you switch
+ between tabs whose content differs in height (Add Models vs. Shortcuts,
+ etc.). That makes the in-modal nav rail — and the whole window — appear to
+ jump up and down between pages. Pinning the top edge keeps the nav rail and
+ surrounding layout visually stable; the panel only ever grows downward.
+ Desktop only: on mobile the panel is a full-height bottom sheet that is
+ already top-stable, and a margin there would push it past the viewport. The
+ drag/dock code clears this margin (sets inline margin:0) the moment a window
+ is dragged, so moving the window still works exactly as before. */
+@media (min-width: 769px) {
+ #settings-modal { align-items: flex-start; }
+ #settings-modal .settings-modal-content { margin-top: 7vh; }
+}
+
+.settings-modal-content .modal-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border);
+}
+
+.settings-layout {
+ display: flex;
+ min-height: 400px;
+ max-height: calc(85vh - 60px);
+}
+
+.settings-modal-content[style*="height"],
+#settings-modal.modal-right-docked .settings-modal-content {
+ overflow: hidden;
+}
+
+.settings-modal-content[style*="height"] .settings-layout,
+#settings-modal.modal-right-docked .settings-layout {
+ flex: 1 1 0;
+ min-height: 0;
+ max-height: none;
+}
+
+.settings-modal-content[style*="height"] .settings-panels,
+#settings-modal.modal-right-docked .settings-panels,
+.settings-modal-content[style*="height"] .settings-sidebar,
+#settings-modal.modal-right-docked .settings-sidebar {
+ min-height: 0;
+}
+
+.settings-sidebar {
+ position: relative;
+ width: var(--settings-sidebar-width, 220px);
+ flex: 0 0 var(--settings-sidebar-width, 220px);
+ min-width: 0;
+ border-right: 1px solid var(--border);
+ padding: 8px;
+ transition: width 0.16s ease, flex-basis 0.16s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ background: color-mix(in srgb, var(--fg) 2%, transparent);
+}
+
+.settings-sidebar-divider { height: 1px; background: var(--border); margin: 8px 12px; }
+.settings-sidebar-label {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+ opacity: 0.35;
+ padding: 4px 12px 2px;
+}
+.settings-nav-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ border: none;
+ background: none;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ font-family: inherit;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 0.1s;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.settings-nav-item:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--fg);
+}
+
+.settings-nav-item.active {
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+ color: var(--red);
+}
+
+.settings-nav-item svg {
+ flex-shrink: 0;
+ opacity: 0.7;
+}
+
+.settings-nav-item.active svg {
+ opacity: 1;
+}
+
+.settings-sidebar-divider { height: 1px; background: var(--border); margin: 8px 12px; }
+
+/* Keyboard shortcuts */
+.shortcut-category {
+ font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
+ opacity: 0.4; padding: 8px 0 4px; margin-top: 4px;
+}
+.shortcut-category:first-child { margin-top: 0; padding-top: 0; }
+.shortcut-row {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 5px 0; border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
+}
+.shortcut-row:last-child { border-bottom: none; }
+.shortcut-row.shortcut-conflict { background: color-mix(in srgb, var(--warn) 6%, transparent); border-radius: 4px; padding: 5px 6px; }
+.shortcut-label { font-size: 12px; display: flex; align-items: center; gap: 6px; }
+.shortcut-icon { display: inline-flex; opacity: 0.5; flex-shrink: 0; }
+.shortcut-warn {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 14px; height: 14px; border-radius: 50%; background: var(--warn); color: #fff;
+ font-size: 9px; font-weight: 700; margin-left: 4px;
+}
+.shortcut-controls { display: flex; align-items: center; gap: 4px; }
+/* Inline hint shown while rebinding a shortcut ("press a key" → "↵ Enter
+ to save"). Subtle so it doesn't compete with the key caps. */
+.shortcut-hint {
+ font-size: 10px;
+ opacity: 0.6;
+ color: var(--accent, var(--red));
+ white-space: nowrap;
+ margin-right: 2px;
+}
+.shortcut-hint[hidden] { display: none; }
+.shortcut-key {
+ font-family: inherit; font-size: 0; padding: 2px 4px;
+ background: transparent; border: none; border-radius: 4px;
+ color: var(--fg); cursor: pointer; display: flex; align-items: center; gap: 2px;
+ transition: all 0.15s;
+}
+.shortcut-key:hover { background: color-mix(in srgb, var(--accent, #cc6a3a) 8%, transparent); }
+/* Unbound shortcut — show a dashed "Set" placeholder instead of keycaps. */
+.shortcut-key-unset { font-size: 10px; }
+.shortcut-unset {
+ font-size: 10px;
+ padding: 2px 8px;
+ border: 1px dashed var(--border);
+ border-radius: 4px;
+ color: color-mix(in srgb, var(--fg) 45%, transparent);
+}
+.shortcut-key-unset:hover .shortcut-unset { border-color: var(--accent, var(--red)); color: var(--fg); }
+.shortcut-key kbd {
+ display: inline-block; font-family: inherit; font-size: 10px;
+ padding: 2px 6px; min-width: 20px; text-align: center;
+ /* Highlight kbd chips in the theme accent so they stand out from
+ normal text and clearly mark "this is a key you press". */
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg));
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border));
+ color: var(--accent, var(--red));
+ border-radius: 3px;
+ box-shadow: 0 1px 0 color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
+ line-height: 1.4;
+ font-weight: 600;
+}
+.shortcut-key.listening {
+ background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, transparent);
+ animation: shortcut-pulse 1s infinite;
+}
+.shortcut-key.listening kbd { border-color: var(--accent, #cc6a3a); }
+.shortcut-action-btn {
+ width: 24px; height: 24px; border-radius: 4px; border: 1px solid var(--border);
+ background: transparent; color: var(--fg); cursor: pointer; font-size: 13px;
+ display: flex; align-items: center; justify-content: center; transition: all 0.15s;
+}
+.shortcut-action-btn:not(.is-reset):hover { border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, var(--bg)); }
+.shortcut-action-btn.is-reset { opacity: 0.55; border-color: transparent; background: transparent; }
+.shortcut-action-btn.is-reset:hover,
+.shortcut-action-btn.is-reset:focus,
+.shortcut-action-btn.is-reset:active { opacity: 0.55; background: transparent; border-color: transparent; }
+@keyframes shortcut-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.6; }
+}
+
+.settings-panels {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px 20px;
+ min-width: 0;
+}
+.settings-appearance-panel:not(.hidden) {
+ display: flex;
+}
+.settings-appearance-panel > .admin-card:nth-of-type(1) { order: 3; }
+.settings-appearance-panel > .admin-card:nth-of-type(2) { order: 1; }
+.settings-appearance-panel > .admin-card:nth-of-type(3) { order: 2; }
+.settings-appearance-panel > .admin-card:nth-of-type(n+4) { order: 4; }
+
+/* Mobile: stack tabs on top */
+@media (max-width: 600px) {
+ .settings-layout {
+ flex-direction: column;
+ }
+ .settings-sidebar {
+ width: auto;
+ flex-direction: row;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+ padding: 6px;
+ }
+ .settings-nav-item {
+ padding: 6px 10px;
+ font-size: 11px;
+ }
+}
diff --git a/static/css/11-tasks.css b/static/css/11-tasks.css
new file mode 100644
index 000000000..1ff602fa3
--- /dev/null
+++ b/static/css/11-tasks.css
@@ -0,0 +1,1972 @@
+/* 11-tasks - 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.
+ */
+/* Snapped/narrow Settings window: move the tab rail to the top. Viewport media
+ alone misses desktop right-half snapping, where the modal is narrow but the
+ browser window is not. */
+@container settings-modal (max-width: 620px) {
+ .settings-layout {
+ flex-direction: column;
+ }
+ .settings-sidebar {
+ width: auto;
+ max-height: none;
+ flex-direction: row;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+ overflow-y: hidden;
+ padding: 6px;
+ scrollbar-width: none;
+ }
+ .settings-sidebar::-webkit-scrollbar {
+ display: none;
+ }
+ .settings-sidebar-divider,
+ .settings-sidebar-label {
+ display: none;
+ }
+ .settings-nav-item {
+ flex: 0 0 auto;
+ padding: 6px 10px;
+ font-size: 11px;
+ }
+ .settings-panels {
+ padding: 12px 14px;
+ }
+}
+
+/* ── Entrance Animations ── */
+
+/* Welcome name — left-to-right wipe with a touch of horizontal stretch.
+ Fires on initial render via the .welcome-name rule; restarts on Nobody⇄
+ Odysseus toggle via JS reflow trick. */
+.welcome-name {
+ transform-origin: left center;
+ animation: welcome-name-reveal 0.55s cubic-bezier(0.34, 1.32, 0.55, 1) both;
+}
+/* Hold the welcome-screen entrance animations until the app has finished its
+ initial load (fonts loaded + layout settled). Running them mid-load made the
+ splash jump/flicker as the page reflowed ("haywire"). Until body.welcome-ready
+ is set by JS, the splash sits in its final, static state (no flash, since the
+ non-animated state IS the resting state); adding the class then plays the
+ entrance once, cleanly. */
+body:not(.welcome-ready) #welcome-screen,
+body:not(.welcome-ready) .welcome-name {
+ animation: none !important;
+}
+/* Hold the splash invisible (the entrance's start state) until ready, so when
+ the animation is released it fades/wipes in smoothly instead of flashing
+ from fully-visible to the animation's hidden first frame. */
+body:not(.welcome-ready) #welcome-screen {
+ opacity: 0;
+}
+@keyframes welcome-name-reveal {
+ from {
+ clip-path: inset(0 100% 0 0);
+ transform: scaleX(0.96);
+ opacity: 0.5;
+ }
+ to {
+ clip-path: inset(0 0 0 0);
+ transform: scaleX(1);
+ opacity: 1;
+ }
+}
+
+@keyframes welcome-enter {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -45%);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%);
+ }
+}
+
+@keyframes msg-enter {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes picker-roll-up {
+ from {
+ opacity: 0;
+ transform: scaleY(0.4) translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: scaleY(1) translateY(0);
+ }
+}
+@keyframes picker-roll-down {
+ from {
+ opacity: 1;
+ transform: scaleY(1) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: scaleY(0.4) translateY(8px);
+ }
+}
+@keyframes model-picker-refresh-spin {
+ to { transform: rotate(360deg); }
+}
+
+@keyframes modal-enter {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+@keyframes modal-exit {
+ from {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.97) translateY(6px);
+ }
+}
+.modal-content.modal-closing {
+ animation: modal-exit 0.18s ease-in both;
+}
+
+#cookbook-modal .modal-content.cookbook-modal-entering {
+ animation: cookbook-modal-enter 0.28s cubic-bezier(0.22, 1.35, 0.36, 1) both;
+}
+/* Mobile: real bottom-sheet slide-up — the existing 12px translate is
+ too subtle on a phone, the modal effectively just appeared. */
+@media (max-width: 768px) {
+ #cookbook-modal .modal-content.cookbook-modal-entering {
+ animation: cookbook-modal-enter-mobile 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
+ }
+}
+#cookbook-modal .modal-content:not(.cookbook-modal-entering):not(.modal-closing) {
+ animation: none;
+}
+#cookbook-modal .modal-content.modal-closing {
+ animation: modal-exit 0.18s cubic-bezier(0.4, 0, 1, 1) both;
+}
+
+/* Per-token streaming fade — new words materialize */
+.token-new {
+ animation: token-fade 0.4s ease-out both;
+}
+
+@keyframes token-fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+/* Disable entrance animations when loading chat history (bulk render) */
+.chat-history.no-animate .msg {
+ animation: none;
+}
+
+/* Prefer reduced motion */
+@media (prefers-reduced-motion: reduce) {
+ .msg,
+ .modal-content,
+ #welcome-screen,
+ .toast,
+ .token-new {
+ animation: none !important;
+ transition: none !important;
+ }
+}
+
+@media (min-width: 769px) {
+ #tool-tasks-btn #assistant-notif-dot.sidebar-notif-dot,
+ #tool-tasks-btn.task-failure-pending::after,
+ #tool-tasks-btn.task-completion-pending::after {
+ transform: translateX(-2px);
+ }
+}
+.tasks-modal-content { max-width: 600px; width: min(600px, 92vw); background: var(--bg); font-size: 12px; }
+.task-completed-preview-row {
+ cursor: default;
+ margin-bottom: 6px;
+ padding: 8px 10px 10px;
+ max-height: none;
+ overflow: visible;
+}
+.task-completed-preview-head {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ min-height: 20px;
+ font-size: 11px;
+}
+.task-completed-chat-preview {
+ padding: 0 0 4px;
+}
+.task-completed-chat-preview .doclib-chat-preview-messages {
+ max-height: 220px;
+ margin-top: 7px;
+}
+.task-completed-chat-preview .doclib-chat-bubble {
+ max-width: 92%;
+}
+.task-completed-chat-preview .doclib-chat-preview-actions {
+ top: 2px;
+ margin-top: 4px;
+}
+.task-completed-preview-row .task-log-time {
+ margin-left: 8px;
+}
+
+/* Tasks tabs reuse the .memory-tab look. The Brain window's tab bar is
+ full-bleed (its underline spans the whole modal width). The Tasks bar is a
+ direct child of .modal-content (padding:10px), so cancel that padding with
+ negative side margins to span edge-to-edge, then re-inset the tabs by 10px
+ so they line up with the rest of the modal content — matching the Brain bar. */
+.tasks-modal-content .tasks-tabs {
+ margin: -2px -10px 8px;
+ padding: 0 10px;
+}
+
+/* Activity log — compact by default. Click the row to expand body+actions. */
+.task-log-row {
+ --cat-hue: 220;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 4px 10px;
+ margin-bottom: 3px;
+ background: color-mix(in srgb, var(--fg) 2%, transparent);
+ position: relative;
+ cursor: pointer;
+ transition: padding .12s ease;
+}
+.task-log-row:hover {
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.task-log-row.task-log-row-error {
+ border-color: color-mix(in srgb, var(--red, #f87171) 35%, var(--border));
+ background: color-mix(in srgb, var(--red, #f87171) 4%, transparent);
+}
+.task-log-row.task-log-row-error:hover {
+ background: color-mix(in srgb, var(--red, #f87171) 7%, transparent);
+}
+.task-log-row.expanded { padding: 8px 10px 6px; }
+.task-log-row-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 11px;
+ margin-bottom: 0;
+}
+.task-log-row.expanded .task-log-row-head { margin-bottom: 4px; }
+/* Collapsed: body + footer hidden. Expanded: visible. */
+.task-log-row:not(.expanded):not(.is-skipped) .task-log-row-body,
+.task-log-row:not(.expanded):not(.is-skipped) .task-log-row-actions,
+.task-log-row:not(.expanded):not(.is-skipped) .task-log-prompt {
+ display: none;
+}
+.task-log-name {
+ font-weight: 600;
+ /* Pull saturation from the per-row hue but mix with the foreground so the
+ title still reads in dark mode. Lightness stays adaptive. */
+ color: hsl(var(--cat-hue) 60% 60%);
+}
+.task-log-failed-tag {
+ color: var(--red, #f87171);
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 1;
+ white-space: nowrap;
+ margin-left: -2px;
+}
+.task-log-task-icon {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 0 auto;
+ margin-right: -3px;
+}
+.task-log-row.task-log-row-error .task-log-name,
+.task-log-row.task-log-row-error .task-log-task-icon {
+ color: var(--red, #f87171);
+}
+.task-log-task-icon svg {
+ top: 0 !important;
+ opacity: 0.46 !important;
+}
+.task-log-row-head .task-ai-mark {
+ top: 0;
+ margin-left: -4px;
+}
+.task-log-repeat {
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 1;
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ border-radius: 999px;
+ padding: 1px 0;
+ white-space: nowrap;
+}
+@media (prefers-color-scheme: light) {
+ .task-log-name { color: hsl(var(--cat-hue) 65% 38%); }
+}
+/* Per-account prefix in fan-out results — e.g. "[Default] No recent emails"
+ becomes a compact accent chip + plain message. Makes multi-account activity
+ rows readable instead of a bracket soup. */
+/* Running / queued status line in the Activity tab — whirlpool + label. */
+.task-log-running {
+ display: inline-flex;
+ align-items: center;
+ gap: 0;
+ font-size: 11px;
+ opacity: 0.75;
+}
+.task-log-running-label { font-style: normal; }
+
+/* New right-side placement: "Running " sits where the timestamp
+ normally would, on the head row's right edge. */
+.task-log-running-inline {
+ display: inline-flex;
+ align-items: center;
+ gap: 0;
+ font-size: 11px;
+ opacity: 0.75;
+}
+.task-log-force-run {
+ appearance: none;
+ box-sizing: border-box;
+ border: 0;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ box-shadow: none;
+ color: inherit;
+ opacity: .82;
+ margin-left: 7px;
+ padding: 1px 6px 1px 4px;
+ height: 18px;
+ min-height: 16px;
+ max-height: 18px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ font-family: inherit;
+ font-size: 10px;
+ line-height: 1;
+ cursor: pointer;
+ position: relative;
+ top: -3px;
+}
+.task-log-force-run svg,
+.task-log-stop svg {
+ display: block;
+ flex-shrink: 0;
+ transform: translateY(1px);
+}
+.task-log-force-run:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
+ color: var(--green, #50fa7b);
+}
+.task-log-stop {
+ border: 0;
+ background: transparent;
+ color: inherit;
+ opacity: .72;
+ padding: 0;
+ margin-left: 6px;
+ width: 12px;
+ height: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ position: relative;
+ top: -3px;
+}
+.task-log-stop:hover {
+ opacity: 1;
+ color: var(--red, #f87171);
+}
+
+/* Slim single-line row for skipped (noop) runs — body/actions stripped, font
+ shrunk, opacity dropped. Distinguishes "task ran but had nothing to do"
+ from a "real" entry without flooding the feed visually. */
+.task-log-row.is-skipped {
+ padding: 4px 8px;
+ opacity: 0.45;
+ font-size: 11px;
+ background: transparent;
+}
+.task-log-row.is-skipped .task-log-row-head { padding: 0; }
+.task-log-row.is-skipped .task-log-name { font-weight: 500; }
+.task-log-row.is-skipped .task-log-skipped-reason {
+ flex: 1 1 auto;
+ margin-left: 6px;
+ font-style: italic;
+ opacity: 0.85;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+.task-log-row.is-skipped:hover { opacity: 0.7; }
+
+.task-log-account-tag {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ padding: 1px 7px;
+ margin-right: 4px;
+ border-radius: 10px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+ vertical-align: 1px;
+}
+.task-log-status {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+}
+.task-log-status-ok { background: #4ade80; box-shadow: 0 0 6px #4ade80, 0 0 3px #4ade80; }
+.task-log-status-error { background: var(--red, #f87171); box-shadow: 0 0 6px var(--red, #f87171), 0 0 3px var(--red, #f87171); }
+.task-log-status-info { background: color-mix(in srgb, var(--fg) 25%, transparent); }
+.task-log-status-queued { background: #fbbf24; box-shadow: 0 0 0 2px color-mix(in srgb, #fbbf24 30%, transparent); }
+.task-log-status-running { background: #60a5fa; animation: task-log-pulse 1.4s ease-in-out infinite; }
+.task-log-status-skipped { background: color-mix(in srgb, var(--fg) 20%, transparent); }
+.task-log-status-aborted { background: color-mix(in srgb, var(--fg) 30%, transparent); border: 1px dashed color-mix(in srgb, var(--fg) 50%, transparent); }
+.task-log-row-body {
+ font-size: 12px;
+ line-height: 1.5;
+ color: color-mix(in srgb, var(--fg) 85%, transparent);
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+.task-log-row-body p { margin: 0 0 6px 0; }
+.task-log-row-body p:last-child { margin-bottom: 0; }
+.task-log-row-body pre {
+ margin: 4px 0;
+ padding: 6px 8px;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border-radius: 4px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 11px;
+ max-height: 180px;
+ overflow: auto;
+}
+.task-log-row-body code {
+ font-size: 11px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ padding: 1px 4px;
+ border-radius: 3px;
+}
+.task-log-row-body ul, .task-log-row-body ol {
+ margin: 4px 0;
+ padding-left: 20px;
+}
+/* Markdown headings inside a task result — without these, ## headings
+ render at browser-default huge sizes and blow out the cramped row. */
+.task-log-row-body h1,
+.task-log-row-body h2,
+.task-log-row-body h3,
+.task-log-row-body h4 {
+ margin: 10px 0 4px;
+ line-height: 1.3;
+ font-weight: 650;
+ color: var(--fg);
+}
+.task-log-row-body h1 { font-size: 14px; }
+.task-log-row-body h2 { font-size: 13px; }
+.task-log-row-body h3,
+.task-log-row-body h4 { font-size: 12px; opacity: 0.9; }
+.task-log-row-body h1:first-child,
+.task-log-row-body h2:first-child,
+.task-log-row-body h3:first-child { margin-top: 0; }
+.task-log-row-body blockquote {
+ margin: 4px 0;
+ padding: 2px 0 2px 10px;
+ border-left: 2px solid color-mix(in srgb, var(--fg) 20%, transparent);
+ opacity: 0.85;
+}
+.task-log-row-body a { color: var(--accent, #60a5fa); text-decoration: none; }
+.task-log-row-body a:hover { text-decoration: underline; }
+.task-log-row-body strong { font-weight: 650; color: var(--fg); }
+/* Email-summary tables — render compactly instead of default browser ugliness. */
+.task-log-row-body table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 6px 0;
+ font-size: 11px;
+}
+.task-log-row-body th,
+.task-log-row-body td {
+ border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+ padding: 3px 6px;
+ text-align: left;
+ vertical-align: top;
+}
+.task-log-row-body th {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ font-weight: 600;
+}
+.task-log-row-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-top: 4px;
+}
+.task-log-open-chat,
+.task-log-open-report,
+.task-log-copy,
+.task-log-clear-cache,
+.task-log-run-again,
+.task-log-force-run-action {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ background: none;
+ border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
+ border-radius: 5px;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ font-family: inherit;
+ font-size: 10px;
+ padding: 1px 6px;
+ cursor: pointer;
+ transition: background .15s, color .15s, border-color .15s;
+ line-height: 1.4;
+}
+.task-log-open-chat:hover,
+.task-log-open-report:hover,
+.task-log-copy:hover,
+.task-log-clear-cache:hover,
+.task-log-run-again:hover,
+.task-log-force-run-action:hover {
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--fg) 30%, transparent);
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.task-log-row-actions > .task-log-open-chat,
+.task-log-row-actions > .task-log-copy {
+ margin-left: auto;
+}
+.task-log-clear-cache svg {
+ position: relative;
+ top: 0;
+}
+/* Activity filter chips — toggle-out model: ON by default (solid),
+ click to toggle OFF (dimmed + strikethrough) to hide that group. */
+.tasks-af-chip {
+ font-size: 11px;
+ padding: 3px 10px;
+ border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ color: var(--fg);
+ cursor: pointer;
+ font-family: inherit;
+ transition: opacity .12s, background .12s, border-color .12s;
+}
+.tasks-af-chip:hover { border-color: color-mix(in srgb, var(--fg) 32%, transparent); }
+.tasks-af-chip.off {
+ opacity: 0.4;
+ text-decoration: line-through;
+ background: transparent;
+}
+.tasks-af-chip.active {
+ border-color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ opacity: 1;
+}
+.tasks-af-chip-error { border-color: color-mix(in srgb, var(--red, #f87171) 45%, transparent); }
+.tasks-af-chip-error:not(.off) { color: var(--red, #f87171); }
+
+/* "Open in Deep Research" is now a regular clickable chat link (a
+ `#research-` markdown anchor in the assistant's message), rendered as
+ an a.chat-link. Nudge it 4px right so it sits slightly inset. */
+a.chat-link[href^="#research-"] {
+ margin-left: 4px;
+}
+.task-log-row.is-long .task-log-row-body {
+ max-height: 8.5em;
+ overflow: hidden;
+ position: relative;
+ mask-image: linear-gradient(to bottom, #000 70%, transparent);
+ -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
+}
+.task-log-row.is-long.expanded .task-log-row-body {
+ max-height: none;
+ overflow: visible;
+ mask-image: none;
+ -webkit-mask-image: none;
+}
+.task-log-row-toggle {
+ margin-top: 6px;
+ background: none;
+ border: none;
+ font-family: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ padding: 2px 0;
+ color: transparent; /* hide raw "Show more" text */
+}
+.task-log-row:not(.expanded) .task-log-row-toggle::before { content: 'Show more'; }
+.task-log-row.expanded .task-log-row-toggle::before { content: 'Show less'; }
+.task-log-prompt {
+ margin-top: 6px;
+ font-size: 11px;
+}
+.task-log-prompt summary {
+ cursor: pointer;
+ opacity: 0.5;
+ user-select: none;
+}
+.task-log-prompt summary:hover { opacity: 0.8; }
+.task-log-prompt pre {
+ margin: 4px 0 0;
+ padding: 6px 8px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ border-radius: 4px;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 11px;
+ opacity: 0.75;
+ max-height: 200px;
+ overflow: auto;
+}
+
+/* Tasks is the shared theme reference for procedural cards. Keep the tint
+ together so Skills cannot drift when theme colors change. Hover and
+ selection remain owned by the shared card state rules above. */
+.task-card,
+.skill-card {
+ background: color-mix(in srgb, var(--fg) 2%, transparent);
+}
+.task-card {
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 7px 8px;
+ margin-bottom: 0;
+ min-height: 46px;
+ box-sizing: border-box;
+ align-items: center;
+}
+/* When expanded, height grows to fit the detail panel. */
+.task-card.expanded { align-items: flex-start; }
+.task-card .memory-item-title {
+ position: relative;
+ top: -2px;
+}
+/* Always show the per-card ⋮ kebab on task cards (the default opacity:0 +
+ hover-reveal is unreliable on touch and easy to miss on desktop).
+ pointer-events / z-index belt-and-braces against any sibling overlay,
+ margin reset undoes the global `.modal-body button { margin-top:6px }`
+ that was punting the hit-target down. */
+.task-card .memory-item-actions {
+ opacity: 0.55;
+ pointer-events: auto;
+ position: relative;
+ z-index: 5;
+}
+.task-card:hover .memory-item-actions,
+.task-card .memory-item-actions:hover { opacity: 1; }
+.task-card .memory-item-actions .memory-item-btn {
+ margin: 0 !important;
+ pointer-events: auto;
+ position: relative;
+ z-index: 5;
+ width: 28px;
+ min-width: 28px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+/* Make the SVG inside the kebab transparent to pointer events so the click
+ always lands on the BUTTON itself (some browsers / nested SVG events lose
+ the click when hitting the inner glyph). */
+.task-card .memory-item-actions .memory-item-btn svg { pointer-events: none; }
+.task-ai-mark {
+ flex: 0 0 auto;
+ color: var(--accent, var(--red));
+ opacity: 0.78;
+}
+/* Per-card select checkbox rides up to the title line. The "All" checkbox is
+ #tasks-select-all (not .memory-select-cb), so it stays put. */
+.task-card .memory-select-cb { position: relative; }
+.task-card.task-card-deleting {
+ opacity: 0.68;
+ pointer-events: none;
+ position: relative;
+}
+.task-card-delete-busy {
+ position: absolute;
+ right: 11px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ height: 22px;
+ padding: 0 8px;
+ border-radius: 999px;
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, var(--panel, #111));
+ border: 1px solid color-mix(in srgb, var(--red) 24%, transparent);
+ font-size: 10px;
+ font-weight: 600;
+ line-height: 1;
+ z-index: 7;
+}
+.task-card-delete-busy-spin {
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ border: 1.5px solid color-mix(in srgb, currentColor 28%, transparent);
+ border-top-color: currentColor;
+ animation: spin 0.8s linear infinite;
+}
+#tasks-pause-all-btn { position: relative; top: -2px; }
+#tasks-pause-all-btn:has(input:disabled) { opacity: 0.4; }
+/* Bigger ⋮ dropdown on mobile (its buttons carry inline styles → !important). */
+@media (max-width: 768px) {
+ .task-card .task-status-badge {
+ touch-action: manipulation;
+ -webkit-tap-highlight-color: transparent;
+ }
+ .task-card .task-state-badge svg,
+ .task-card .task-card-run-btn svg {
+ width: 10px;
+ height: 10px;
+ }
+}
+
+.task-card-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.task-card-name {
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.task-card-schedule {
+ font-size: 10.5px;
+ opacity: 0.5;
+ margin-left: auto;
+}
+
+.task-card-output {
+ font-size: 10px;
+ opacity: 0.45;
+}
+
+.task-card-prompt {
+ font-size: 11px;
+ opacity: 0.55;
+ margin: 4px 0;
+ line-height: 1.4;
+}
+
+.task-card-meta {
+ font-size: 10.5px;
+ opacity: 0.45;
+ margin-bottom: 6px;
+}
+
+.task-card-actions {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.task-btn {
+ font-family: inherit;
+ font-size: 11px;
+ padding: 4px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: none;
+ color: var(--fg);
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.15s, background 0.15s;
+}
+.task-btn:hover { opacity: 1; }
+.task-btn:disabled { opacity: 0.3; cursor: not-allowed; }
+
+.task-btn-primary {
+ background: var(--red);
+ color: #fff;
+ border-color: transparent;
+ opacity: 1;
+}
+.task-btn-primary:hover { opacity: 0.85; }
+
+.task-btn-danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 30%, transparent); }
+.task-btn-danger:hover { opacity: 1; border-color: var(--red); }
+
+.tasks-clock {
+ font-size: 10px;
+ opacity: 0.35;
+ text-align: center;
+ padding: 6px 0 8px;
+}
+
+.task-preset-grid {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;
+}
+.task-preset-card {
+ display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
+ padding: 12px; border: 1px solid var(--border); border-radius: 8px;
+ background: var(--bg); cursor: pointer; transition: all 0.15s; text-align: left;
+ color: var(--fg);
+}
+.task-preset-card:hover {
+ border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 8%, var(--bg));
+}
+.task-preset-icon { font-size: 20px; margin-bottom: 2px; }
+.task-preset-label { font-size: 12px; font-weight: 600; }
+.task-preset-desc { font-size: 10px; opacity: 0.5; line-height: 1.3; }
+
+.task-form { display: flex; flex-direction: column; gap: 4px; }
+.task-form-label {
+ font-size: 11px;
+ opacity: 0.55;
+ font-weight: 500;
+ display: block;
+ /* Consistent rhythm: 8px above each label (section gap), 4px below to the
+ field — same spacing for Name/Prompt/Trigger/Output/Model/Chain. */
+ margin: 8px 0 4px;
+}
+.task-form-input {
+ font-family: inherit;
+ font-size: 11px;
+ padding: 6px 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ transition: border-color 0.15s;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+}
+.task-form-input:focus {
+ outline: none;
+ border-color: var(--red);
+}
+.task-form-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 8px; }
+
+.task-form-notif-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 9px 0 3px;
+ cursor: pointer;
+ color: var(--fg);
+}
+.task-form-notif-toggle input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+.task-form-notif-switch {
+ width: 30px;
+ height: 16px;
+ border-radius: 999px;
+ border: 1px solid var(--border);
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ position: relative;
+ flex: 0 0 auto;
+ transition: background 0.15s, border-color 0.15s;
+}
+.task-form-notif-toggle input:checked + .task-form-notif-switch {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 58%, transparent);
+ background: color-mix(in srgb, var(--accent, var(--red)) 20%, transparent);
+}
+.task-form-notif-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ font-size: 11px;
+ font-weight: 500;
+}
+.task-form-notif-copy span:last-child {
+ font-size: 10px;
+ font-weight: 400;
+ opacity: 0.52;
+}
+
+.task-form-toggle {
+ display: flex; gap: 4px;
+}
+.task-toggle-btn {
+ flex: 1; padding: 4px 8px; font-size: 10px; font-family: inherit;
+ border: 1px solid var(--border); border-radius: 6px; background: transparent;
+ color: var(--fg); opacity: 0.4; cursor: pointer; transition: all 0.15s;
+}
+.task-toggle-btn:hover { opacity: 0.7; border-color: var(--fg); }
+.task-toggle-btn.active {
+ opacity: 1; border-color: var(--red); color: var(--red);
+ background: color-mix(in srgb, var(--red) 10%, transparent);
+}
+
+.task-time-picker, .task-date-picker {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.task-time-select, .task-date-select {
+ width: auto;
+ min-width: 48px;
+ font-size: 11px;
+}
+.task-time-sep { opacity: 0.4; font-size: 11px; }
+
+.task-history-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.task-runs-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; }
+.task-run-item {
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px 10px;
+}
+.task-run-item-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 11px;
+}
+.task-run-time { margin-left: auto; font-size: 10px; opacity: 0.45; }
+.task-run-result {
+ font-size: 11px;
+ opacity: 0.6;
+ margin-top: 4px;
+ line-height: 1.4;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+/* ── Settings Form Utilities ── */
+
+.settings-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.settings-col {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.settings-label {
+ font-size: 12px;
+ min-width: 70px;
+}
+.settings-select {
+ flex: 1;
+ /* Allow the select to shrink below its longest option's width inside a flex
+ row — without this a long model name (e.g. the Vision picker) overflowed
+ the card off-screen on mobile. */
+ min-width: 0;
+ max-width: 100%;
+ padding: 5px 8px;
+ font-family: inherit;
+ font-size: 12px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ transition: border-color 0.15s;
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: textfield;
+ outline: none;
+}
+input.settings-select::-webkit-outer-spin-button,
+input.settings-select::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+input.settings-select::placeholder { color: color-mix(in srgb, var(--fg) 35%, transparent); }
+.settings-select:focus {
+ outline: none;
+ border-color: var(--red);
+}
+
+/* Deep Research uses long labels and previously fixed-width fields. Keep
+ every control constrained by the card at intermediate desktop widths too. */
+.admin-card:has(#set-researchSearch) .settings-row { min-width: 0; }
+.admin-card:has(#set-researchSearch) .settings-row > .settings-label { flex: 0 0 112px; min-width: 0; }
+#set-researchSearch,
+#set-researchMaxTokens,
+#set-researchRunTimeout,
+.settings-row div:has(> #set-researchExtractTimeout),
+.settings-row div:has(> #set-researchExtractConcurrency) {
+ width: auto !important;
+ max-width: 100% !important;
+ min-width: 0 !important;
+ flex: 1 1 0 !important;
+}
+
+@media (max-width: 768px) {
+ /* Deep Research settings contain desktop-sized inline widths. On mobile,
+ let each control claim the card width instead of escaping the admin block. */
+ .settings-row:has(#set-researchSearch),
+ .settings-row:has(#set-researchMaxTokens),
+ .settings-row:has(#set-researchExtractTimeout),
+ .settings-row:has(#set-researchExtractConcurrency),
+ .settings-row:has(#set-researchRunTimeout) {
+ flex-wrap: wrap;
+ align-items: stretch;
+ }
+ .settings-row:has(#set-researchSearch) .settings-label,
+ .settings-row:has(#set-researchMaxTokens) .settings-label,
+ .settings-row:has(#set-researchExtractTimeout) .settings-label,
+ .settings-row:has(#set-researchExtractConcurrency) .settings-label,
+ .settings-row:has(#set-researchRunTimeout) .settings-label {
+ flex: 1 1 auto;
+ }
+ #set-researchSearch,
+ #set-researchMaxTokens,
+ #set-researchRunTimeout,
+ .settings-row div:has(> #set-researchExtractTimeout),
+ .settings-row div:has(> #set-researchExtractConcurrency) {
+ width: 100% !important;
+ max-width: 100% !important;
+ flex: 1 1 100% !important;
+ margin-left: 0 !important;
+ }
+}
+
+/* Default-chat fallback chain editor. Each row mirrors the primary
+ endpoint/model selectors, indented under them to read as a chain. */
+.settings-fallbacks {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.settings-fallbacks:not(:empty) { margin-top: 2px; }
+.settings-fallback-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding-left: 12px;
+ border-left: 2px solid color-mix(in srgb, var(--fg) 12%, transparent);
+ max-width: 100%;
+ box-sizing: border-box;
+}
+.settings-fallback-num {
+ font-size: 11px;
+ opacity: 0.4;
+ min-width: 14px;
+ text-align: right;
+}
+.settings-fallback-row .settings-select {
+ width: 0;
+ min-width: 0;
+ min-inline-size: 0;
+ box-sizing: border-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.settings-fallback-row .settings-select:first-of-type {
+ flex: 0 1 128px;
+ width: 128px;
+ max-width: 34%;
+}
+.settings-fallback-row .settings-select:nth-of-type(2) {
+ flex: 1 1 0;
+ width: 0;
+ max-width: 100%;
+}
+details.hwfit-serve-advanced > .hwfit-serve-row,
+details.hwfit-serve-advanced > .hwfit-serve-checks,
+details.hwfit-serve-advanced > .hwfit-serve-cmd-wrap,
+details.hwfit-serve-advanced > .hwfit-serve-extra {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+/* Inside Advanced, tighten the per-row grid gap and the label →
+ select gap so rows of single-line dropdowns don't visually sit
+ on top of half a row of empty space. Outside Advanced (Core,
+ etc.) the original ~6px breathing room stays. */
+details.hwfit-serve-advanced > .hwfit-serve-row {
+ gap: 4px;
+ row-gap: 4px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-row.hwfit-backend-llamacpp,
+details.hwfit-serve-advanced > .hwfit-serve-checks.hwfit-backend-llamacpp {
+ margin-top: -20px;
+ position: relative;
+ left: 4px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-row.hwfit-backend-llamacpp {
+ column-gap: 8px;
+ row-gap: 2px;
+}
+details.hwfit-serve-advanced > .hwfit-llama-placement-row {
+ margin-top: -6px !important;
+}
+details.hwfit-serve-advanced > .hwfit-llama-memory-row {
+ margin-top: -20px !important;
+}
+.cookbook-llama-cpu-mode details.hwfit-serve-advanced > .hwfit-llama-memory-row {
+ margin-top: -8px !important;
+}
+details.hwfit-serve-advanced > .hwfit-llama-batch-row,
+details.hwfit-serve-advanced > .hwfit-llama-monitor-row,
+details.hwfit-serve-advanced > .hwfit-llama-checks-row {
+ margin-top: -18px !important;
+}
+details.hwfit-serve-advanced > .hwfit-diff-settings-row {
+ row-gap: 2px;
+}
+details.hwfit-serve-advanced > .hwfit-diff-checks-row,
+details.hwfit-serve-advanced > .hwfit-diff-harmonize-row {
+ margin-top: -10px !important;
+}
+details.hwfit-serve-advanced > .hwfit-diff-checks-row {
+ row-gap: 2px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-row.hwfit-vram-monitor {
+ gap: 4px !important;
+ grid-template-columns: max-content minmax(0, 1fr);
+ white-space: nowrap;
+}
+details.hwfit-serve-advanced .hwfit-sf[data-field="vllm_block_size"] {
+ width: calc(100% - 6px);
+}
+details.hwfit-serve-advanced .hwfit-sf[data-field="vllm_kv_cache_dtype"] {
+ width: 60px;
+ min-width: 60px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-checks {
+ gap: 4px;
+ row-gap: 4px;
+}
+/* Pull the vLLM/SGLang checks row, Extra args, and the trailing
+ model-specific (Speculative) checks row up tight against the row
+ above — the previous 4px gap plus per-row baseline padding left a
+ ~8px gap that read as too airy in the Advanced fold. */
+details.hwfit-serve-advanced > .hwfit-serve-checks.hwfit-backend-vllm,
+details.hwfit-serve-advanced > .hwfit-serve-checks.hwfit-backend-sglang,
+details.hwfit-serve-advanced > .hwfit-serve-extra {
+ margin-top: -8px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-extra {
+ margin-top: -18px;
+}
+details.hwfit-serve-advanced > .hwfit-serve-row:last-of-type,
+details.hwfit-serve-advanced > .hwfit-serve-checks:last-of-type {
+ margin-bottom: 0;
+}
+
+.settings-fallback-remove {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+ cursor: pointer;
+ transition: border-color 0.12s, color 0.12s, background 0.12s;
+}
+.settings-fallback-remove:hover {
+ border-color: var(--red);
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 10%, transparent);
+}
+.settings-fallback-add {
+ align-self: flex-start;
+ margin-top: 2px;
+ font-size: 11px;
+ padding: 3px 9px;
+ border: 1px dashed var(--border);
+ border-radius: 6px;
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ cursor: pointer;
+ transition: border-color 0.12s, color 0.12s;
+}
+.settings-fallback-add:hover {
+ border-color: var(--red);
+ color: var(--red);
+}
+.settings-input {
+ flex: 1;
+ padding: 5px 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ transition: border-color 0.15s;
+}
+.settings-input:focus {
+ outline: none;
+ border-color: var(--red);
+}
+/* ── Contacts manager (Settings → Integrations → CardDAV) ── */
+.contacts-add-row {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 8px;
+ align-items: center;
+}
+.contacts-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ max-height: 360px;
+ overflow-y: auto;
+}
+.contact-row {
+ padding: 6px 4px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
+}
+.contact-row:last-child { border-bottom: none; }
+.contact-row-edit { margin-top: 4px; }
+
+/* ── Sort Dropdown ── */
+
+.sort-dropdown {
+ position: absolute;
+ right: 0;
+ top: 100%;
+ z-index: 1000;
+ min-width: 120px !important;
+ width: max-content;
+ padding: 4px !important;
+ margin-top: 4px;
+ background: var(--panel) !important;
+ border: 1px solid var(--border) !important;
+ border-radius: 8px !important;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4) !important;
+ backdrop-filter: none !important;
+}
+.sort-dropdown-item {
+ cursor: pointer;
+ padding: 6px 8px 6px 6px !important;
+ font-size: 11px !important;
+ border-radius: 6px !important;
+ border-bottom: none !important;
+ white-space: nowrap;
+ transition: background 0.1s;
+}
+.sort-dropdown-item:hover {
+ background: color-mix(in srgb, var(--accent) 10%, transparent) !important;
+}
+.sort-dropdown-item.ai-sort-option {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
+}
+.sort-dropdown-item.ai-sort-option .auto-sort-icon {
+ color: var(--accent, var(--red));
+ display: inline-flex;
+ align-items: center;
+}
+#auto-sort-sessions-btn {
+ transform: translateX(-4px);
+}
+.sort-dropdown-item.ai-sort-option:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent) !important;
+}
+.sort-dropdown-sep {
+ border-top: 1px solid var(--border);
+ margin-top: 2px;
+ padding-top: 6px;
+}
+
+/* Mobile: bigger taps for the chats sort/funnel menu and the select-mode
+ bulk-action bar. Also drop the "Rearrange" item — it's a touch-finicky
+ feature better left to desktop. */
+@media (max-width: 768px) {
+ /* Nudge the funnel/sort button right on mobile so it doesn't hug the
+ chevron next to it. */
+ #session-sort-btn { transform: translateX(13px); }
+ #session-sort-dropdown.sort-dropdown {
+ min-width: 200px !important;
+ padding: 6px !important;
+ }
+ #session-sort-dropdown .sort-dropdown-item {
+ padding: 12px 14px !important;
+ font-size: 14px !important;
+ }
+ #session-rearrange-toggle { display: none !important; }
+
+ /* Select-mode bar — make Archive / Delete / Cancel buttons + the
+ Select-all toggle finger-sized. */
+ .session-bulk-bar {
+ padding: 8px 10px;
+ font-size: 14px;
+ gap: 10px;
+ }
+ .session-bulk-btn {
+ padding: 10px;
+ min-width: 44px;
+ min-height: 44px;
+ }
+ .session-bulk-btn svg { width: 20px; height: 20px; }
+ #session-select-all-dot { font-size: 22px !important; padding: 6px; }
+ #session-select-all-label { font-size: 14px !important; padding: 4px; }
+}
+
+
+/* ── Admin: Built-in tools ── */
+.admin-tool-category {
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-bottom: 6px;
+ overflow: hidden;
+ transition: border-color 0.15s;
+}
+.admin-tool-category:hover {
+ border-color: color-mix(in srgb, var(--fg) 20%, var(--border));
+}
+.admin-tool-cat-header {
+ font-size: 12px;
+ font-weight: 500;
+ padding: 8px 10px;
+ transition: background 0.15s;
+}
+.admin-tool-cat-header:hover {
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.admin-tool-cat-body {
+ border-top: 1px solid var(--border);
+ padding: 4px 0;
+}
+.admin-tool-cat-body.hidden {
+ display: none;
+}
+.admin-tool-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 10px;
+ padding-right: 28px; /* align toggles with header toggle (chevron 12px + gap 6px + padding) */
+ transition: background 0.08s;
+}
+.admin-tool-row:hover {
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.admin-tool-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+.admin-tool-name {
+ font-size: 11px;
+ font-weight: 500;
+}
+.admin-tool-desc {
+ font-size: 10px;
+ opacity: 0.4;
+}
+.admin-tool-ctx {
+ font-size: 9px;
+ opacity: 0.3;
+ flex-shrink: 0;
+ min-width: 35px;
+ text-align: right;
+ font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
+}
+
+/* Cookbook serve param hints */
+.hwfit-hint {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--fg);
+ font-size: 8px;
+ font-weight: 600;
+ opacity: 0.35;
+ cursor: help;
+ margin-left: 2px;
+ vertical-align: middle;
+ transition: opacity 0.1s;
+}
+.hwfit-hint:hover {
+ opacity: 0.8;
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
+}
+.hwfit-dl-dot {
+ color: var(--green);
+ font-size: 8px;
+ margin-left: 4px;
+ opacity: 0.7;
+}
+.hwfit-parser-tag {
+ font-size: 9px;
+ opacity: 0.4;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ padding: 1px 5px;
+ border-radius: 3px;
+ margin-left: 4px;
+ font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
+}
+
+/* Cookbook output copy — show only on hover */
+.cookbook-output-wrap .cookbook-output-copy {
+ opacity: 0;
+ transition: opacity 0.1s;
+}
+
+/* Cached model menu button */
+.hwfit-cached-menu-btn {
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-size: 16px;
+ cursor: pointer;
+ opacity: 0;
+ padding: 2px 4px;
+ border-radius: 4px;
+ transition: opacity 0.1s;
+ flex-shrink: 0;
+ position: relative;
+ top: -2px;
+}
+.memory-item:hover .hwfit-cached-menu-btn,
+.hwfit-cached-item:hover .hwfit-cached-menu-btn { opacity: 0.4; }
+.hwfit-cached-menu-btn:hover { opacity: 1 !important; }
+.hwfit-cached-menu-btn svg { pointer-events: none; }
+@media (max-width: 768px) {
+ #cookbook-modal .hwfit-cached-menu-btn {
+ opacity: 0.72 !important;
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
+ padding: 0;
+ justify-content: center;
+ top: -4px;
+ }
+ #cookbook-modal .hwfit-cached-menu-btn:active {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--fg) 9%, transparent);
+ }
+}
+
+/* Quick run button */
+.cookbook-run-btn {
+ background: var(--accent, var(--red)) !important;
+ color: var(--panel) !important;
+ border-color: var(--accent, var(--red)) !important;
+ font-weight: 600;
+}
+.cookbook-run-btn:hover {
+ opacity: 0.9;
+}
+#hwfit-cache-scan {
+ position: relative;
+ top: 1px;
+ width: 26px;
+ height: 24px;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+#hwfit-cache-scan.spinning svg {
+ animation: modelPickerRefreshSpin 0.75s linear infinite;
+}
+#serve-search {
+ height: 28px;
+ min-height: 28px;
+ font-family: inherit;
+ font-size: 11px;
+ background-image: url("data:image/svg+xml;utf8, ");
+ background-repeat: no-repeat;
+ background-position: 9px center;
+ padding-left: 28px;
+}
+.cookbook-sort-select-wrap {
+ position: relative;
+ display: inline-flex;
+ flex-shrink: 0;
+ align-items: center;
+}
+.cookbook-sort-select-wrap > svg {
+ position: absolute;
+ left: 9px;
+ z-index: 1;
+ pointer-events: none;
+ opacity: 0.65;
+ transform: translateY(4px);
+}
+
+/* Cookbook model directory tags */
+.cookbook-modeldir-tag {
+ font-size: 11px;
+ padding: 3px 8px;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--fg);
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-family: 'Berkeley Mono', 'SF Mono', monospace;
+}
+.cookbook-modeldir-default {
+ opacity: 0.5;
+}
+.cookbook-server-bottom-actions {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 4px;
+ margin-top: 5px;
+}
+.cookbook-server-bottom-actions > button {
+ height: 22px;
+ box-sizing: border-box;
+ display: inline-flex;
+ align-items: center;
+}
+/* Default-server radio-check in a Settings server title (same look as the
+ model-dir download target). Active = accent-tinted. */
+.cookbook-srv-default {
+ cursor: pointer;
+ opacity: 0.35;
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ line-height: 1;
+ transition: opacity 0.12s, color 0.12s;
+}
+.cookbook-srv-default:hover { opacity: 0.8; }
+.cookbook-srv-default.active { opacity: 1; color: var(--accent, var(--red)); }
+.cookbook-srv-default-label { font-size: 10px; font-weight: 600; letter-spacing: 0.02em; }
+.cookbook-srv-default-icon {
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+}
+.cookbook-srv-default-icon svg {
+ width: 13px;
+ height: 13px;
+}
+.cookbook-srv-default-icon,
+.cookbook-srv-default-label {
+ position: relative;
+ top: 2px;
+}
+.email-settings-default,
+.email-settings-default .cookbook-srv-default-icon,
+.email-settings-default .cookbook-srv-default-label {
+ position: relative;
+ top: -2px;
+}
+/* Download-target toggle inside a model-dir tag */
+.cookbook-modeldir-dl {
+ cursor: pointer;
+ opacity: 0.35;
+ display: inline-flex;
+ align-items: center;
+ line-height: 0;
+ transition: opacity 0.12s, color 0.12s;
+}
+.cookbook-modeldir-dl:hover { opacity: 0.8; }
+.cookbook-modeldir-dl.active { opacity: 1; color: var(--accent, var(--red)); }
+/* The tag currently flagged as the download target — clearly highlighted so
+ it's obvious where downloads land. */
+.cookbook-modeldir-target {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ font-weight: 600;
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 60%, transparent);
+}
+.cookbook-modeldir-rm {
+ cursor: pointer;
+ opacity: 0.4;
+ font-size: 10px;
+}
+.cookbook-modeldir-rm:hover {
+ opacity: 1;
+}
+.cookbook-modeldir-add {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-size: 10px;
+ font-weight: 500;
+ height: 19.5px;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ position: relative;
+ top: -3px;
+ padding: 0 9px;
+ transition: border-color 0.12s, background 0.12s;
+}
+.cookbook-modeldir-add:hover { border-color: var(--accent, var(--red)); background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent); }
+
+/* Cookbook serve optimizations */
+.hwfit-serve-opts {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 4px 0;
+}
+.hwfit-apply-opts {
+ font-size: 11px !important;
+ min-width: auto !important;
+ white-space: nowrap;
+}
+.hwfit-opts-desc {
+ font-size: 9px;
+ opacity: 0.4;
+ font-style: italic;
+}
+
+/* Library modal tabs */
+.lib-tabs,
+.admin-tabs {
+ display: flex;
+ gap: 0;
+ margin-bottom: 8px;
+ border-bottom: 1px solid var(--border);
+}
+.lib-tab,
+.admin-tab {
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--fg-muted);
+ font-size: 12px;
+ font-family: inherit;
+ padding: 6px 14px;
+ cursor: pointer;
+ transition: color 0.1s, border-color 0.1s;
+}
+.lib-tab:hover,
+.admin-tab:hover { color: var(--fg); }
+.lib-tab.active,
+.admin-tab.active {
+ color: var(--accent, var(--red));
+ border-bottom-color: var(--accent, var(--red));
+}
+
+/* Cookbook tab count badge */
+.cookbook-tab-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 16px;
+ height: 16px;
+ padding: 0 4px;
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 20%, transparent);
+ color: var(--accent, var(--red));
+ font-size: 9px;
+ font-weight: 700;
+ margin-left: 4px;
+ line-height: 1;
+}
+
+/* ── Generate Visual Report button (deep research) ── */
+.view-report-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 12px;
+ padding: 8px 16px;
+ border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--red) 10%, transparent);
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 0.88em;
+ font-weight: 500;
+ transition: background 0.15s ease;
+ cursor: pointer;
+}
+.view-report-btn:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--red) 18%, transparent);
+}
+.view-report-btn svg {
+ flex-shrink: 0;
+}
+.report-btn-wrap {
+ margin-top: 12px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.view-report-btn.chat-about-btn {
+ border-color: color-mix(in srgb, var(--fg) 22%, transparent);
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+.view-report-btn.chat-about-btn:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.view-report-btn:disabled {
+ opacity: 0.6;
+ cursor: progress;
+}
+.report-spinner {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.88em;
+ color: var(--fg);
+ opacity: 0.7;
+}
+.report-spinner-text {
+ font-weight: 500;
+}
+.report-spinner-dots::after {
+ content: '';
+ animation: report-dots 1.4s steps(4, end) infinite;
+}
+@keyframes report-dots {
+ 0% { content: ''; }
+ 25% { content: '.'; }
+ 50% { content: '..'; }
+ 75% { content: '...'; }
+ 100% { content: ''; }
+}
+
+/* ── Continue research hint ────────────────────────── */
+.continue-research-wrap {
+ margin-top: 8px;
+}
+.continue-research-hint {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.78em;
+ color: var(--fg-dim, var(--fg));
+ opacity: 0.5;
+ padding: 4px 0;
+}
+.continue-research-hint svg {
+ flex-shrink: 0;
+ opacity: 0.6;
+}
+
+/* ── Research reconnect & timer ────────────────────── */
+.msg.research-reconnect {
+ border-left: 3px solid var(--red);
+ background: color-mix(in srgb, var(--red) 5%, transparent);
+}
+.research-timer {
+ font-size: 0.8em;
+ opacity: 0.6;
+ margin-top: 4px;
+ font-family: monospace;
+ color: var(--fg-dim, var(--fg));
+}
+
+/* ── Research synapse visualization ───────────────────────────────
+ Live SVG graph of an in-flight deep-research run. The query is the
+ central node; sub-questions branch off per round; sources are leaf
+ nodes that pop in as they're captured. Pulses indicate activity. */
+.research-synapse {
+ margin: 6px 0 4px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 24%, var(--border));
+ border-radius: 7px;
+ background:
+ linear-gradient(color-mix(in srgb, var(--fg) 3%, transparent) 1px, transparent 1px),
+ linear-gradient(90deg, color-mix(in srgb, var(--fg) 3%, transparent) 1px, transparent 1px),
+ radial-gradient(ellipse at 50% 48%, color-mix(in srgb, var(--accent, var(--red)) 13%, transparent) 0%, transparent 62%),
+ color-mix(in srgb, var(--panel) 68%, var(--bg));
+ background-size: 18px 18px, 18px 18px, auto, auto;
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--fg) 5%, transparent);
+ overflow: hidden;
+}
+.research-synapse .rs-stage {
+ height: 400px;
+ position: relative;
+}
+.research-synapse .rs-meta-header {
+ position: absolute;
+ z-index: 4;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-top: 0;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+ padding: 5px 8px 6px;
+ background: color-mix(in srgb, var(--panel) 76%, transparent);
+}
+.research-synapse-ascii-filter > .rs-stage > svg {
+ display: none;
+}
+.research-synapse-ascii-filter .rs-ascii-overlay {
+ position: absolute;
+ z-index: 3;
+ inset: 28px 10px 10px;
+ margin: 0;
+ overflow: hidden;
+ pointer-events: none;
+ color: color-mix(in srgb, var(--accent, var(--red)) 76%, var(--fg));
+ font: 600 10px/1.35 ui-monospace, SFMono-Regular, Consolas, monospace;
+ white-space: pre;
+ text-align: center;
+ opacity: 1;
+ text-shadow: 0 0 7px color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+ mix-blend-mode: screen;
+}
+.research-synapse-compact .rs-meta { padding: 5px 8px 6px; font-size: 10px; }
+.research-synapse-compact .rs-label-sub { font-size: 8px; }
+.research-synapse .rs-stage-head {
+ position: absolute;
+ z-index: 2;
+ top: 7px;
+ left: 9px;
+ right: 9px;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: var(--fg-dim, var(--fg));
+ font: 600 8px/1 ui-monospace, "JetBrains Mono", monospace;
+ text-transform: uppercase;
+ pointer-events: none;
+}
+.research-synapse .rs-live-dot {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: var(--accent, var(--red));
+ box-shadow: 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 75%, transparent);
+ animation: rs-live 1.8s ease-in-out infinite;
+}
+.research-synapse .rs-phase-chip {
+ margin-left: auto;
+ padding: 3px 5px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 25%, var(--border));
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--bg));
+ color: var(--accent, var(--red));
+}
+.research-synapse svg {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+.research-synapse .rs-edge {
+ stroke: color-mix(in srgb, var(--accent, var(--red)) 32%, var(--border));
+ stroke-width: 1.35;
+ fill: none;
+ opacity: 0.72;
+}
+.research-synapse .rs-node {
+ fill: var(--bg);
+ stroke: var(--accent, var(--red));
+ stroke-width: 1.5;
+ transition: all 0.3s ease;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+.research-synapse .rs-source-index {
+ fill: var(--fg);
+ font: 650 4.5px/1 ui-monospace, "JetBrains Mono", monospace;
+ pointer-events: none;
+ opacity: 0.88;
+}
+.research-synapse .rs-node-expanded { animation: rs-hub-expand 0.75s ease-out; }
+.research-synapse .rs-node-new {
+ animation: rs-pop 0.6s ease-out;
+}
+.research-synapse .rs-pulse {
+ fill: var(--accent, var(--red));
+ opacity: 0;
+ animation: rs-pulse 2.6s ease-out infinite;
+ transform-box: fill-box;
+ transform-origin: center;
+}
+.research-synapse .rs-label {
+ fill: var(--fg);
+ font-size: 10px;
+ font-family: ui-monospace, "JetBrains Mono", monospace;
+ pointer-events: none;
+ opacity: 0.85;
+}
+.research-synapse .rs-label-sub {
+ font-size: 9px;
+ opacity: 0.7;
+}
+.research-synapse .rs-meta {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto auto auto;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 10px 8px;
+ font-family: ui-monospace, "JetBrains Mono", monospace;
+ font-size: 11px;
+ color: var(--fg-dim, var(--fg));
+ opacity: 0.92;
+ border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+ background: color-mix(in srgb, var(--panel) 76%, transparent);
+}
+.research-synapse .rs-meta .rs-status {
+ display: none;
+ color: var(--accent, var(--red));
+ font-weight: 600;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.research-synapse .rs-meta b {
+ color: var(--fg);
+ font-weight: 600;
+}
+.research-synapse.rs-complete .rs-meta .rs-status { color: var(--color-success, #4caf50); }
+.research-synapse.rs-error .rs-meta .rs-status { color: var(--red); }
diff --git a/static/css/12-gallery.css b/static/css/12-gallery.css
new file mode 100644
index 000000000..16f47e863
--- /dev/null
+++ b/static/css/12-gallery.css
@@ -0,0 +1,839 @@
+/* 12-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.
+ */
+.research-synapse.rs-phase-reading .rs-phase-chip,
+.research-synapse.rs-phase-analyzing .rs-phase-chip { color: var(--gold, #d4a72c); }
+.research-synapse.rs-phase-writing .rs-phase-chip { color: var(--color-success, #4caf50); }
+
+@media (prefers-reduced-motion: reduce) {
+ .research-synapse .rs-live-dot,
+ .research-synapse .rs-pulse,
+ .research-synapse .rs-node-new,
+ .research-synapse .rs-node-expanded,
+ .research-synapse .rs-edge-firing { animation: none !important; }
+}
+
+/* ── Raw findings items (inside sources-style box) ── */
+.finding-item {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.finding-item .source-link {
+ border-radius: 6px 6px 0 0;
+}
+.finding-summary {
+ padding: 6px 8px 8px;
+ font-size: 0.82em;
+ line-height: 1.5;
+ color: var(--fg-dim, var(--fg));
+ opacity: 0.85;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
+ margin-bottom: 4px;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.finding-item:last-child .finding-summary {
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+/* ═══════════════════════════════════════════════════════════
+ Gallery Editor
+ ═══════════════════════════════════════════════════════════ */
+
+/* Tab bar */
+.gallery-tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border);
+ padding: 0 16px;
+ background: var(--panel);
+}
+.gallery-tab {
+ padding: 8px 18px;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: var(--fg);
+ opacity: 0.6;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 500;
+ transition: opacity 0.15s, border-color 0.15s;
+}
+.gallery-tab:hover { opacity: 0.85; }
+.gallery-tab.active {
+ opacity: 1;
+ border-bottom-color: var(--red);
+}
+
+/* Narrow modal tab strips should stay on one row. Resized docked windows can
+ be much narrower than the viewport, so this cannot live only in mobile media
+ queries. */
+.cookbook-tabs,
+.memory-tabs,
+.admin-tabs,
+.lib-tabs,
+.gallery-tabs,
+.preset-tabs {
+ flex-wrap: nowrap !important;
+ overflow-x: auto !important;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior-x: contain;
+ scrollbar-width: none;
+}
+.cookbook-tabs::-webkit-scrollbar,
+.memory-tabs::-webkit-scrollbar,
+.admin-tabs::-webkit-scrollbar,
+.lib-tabs::-webkit-scrollbar,
+.gallery-tabs::-webkit-scrollbar,
+.preset-tabs::-webkit-scrollbar {
+ display: none;
+}
+.cookbook-tabs > *,
+.memory-tabs > *,
+.admin-tabs > *,
+.lib-tabs > *,
+.gallery-tabs > *,
+.preset-tabs > * {
+ flex: 0 0 auto;
+}
+.cookbook-tab,
+.memory-tab,
+.admin-tab,
+.lib-tab,
+.gallery-tab,
+.preset-tab {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ white-space: nowrap;
+ line-height: 1;
+}
+.gallery-tab {
+ gap: 6px;
+}
+
+@container (max-width: 360px) {
+ .cookbook-tab:has(svg),
+ .memory-tab:has(svg),
+ .admin-tab:has(svg),
+ .lib-tab:has(svg),
+ .gallery-tab:has(svg),
+ .preset-tab:has(svg) {
+ width: 34px;
+ min-width: 34px;
+ padding-left: 0;
+ padding-right: 0;
+ font-size: 0;
+ }
+
+ .cookbook-tab:has(svg) svg,
+ .memory-tab:has(svg) svg,
+ .admin-tab:has(svg) svg,
+ .lib-tab:has(svg) svg,
+ .gallery-tab:has(svg) svg,
+ .preset-tab:has(svg) svg {
+ width: 14px;
+ height: 14px;
+ margin-right: 0 !important;
+ vertical-align: middle !important;
+ }
+
+ .memory-tab:has(svg) .memory-count,
+ .gallery-tab:has(svg) .gallery-tab-label,
+ .gallery-tab:has(svg) .gallery-tab-close,
+ .cookbook-tab:has(svg) .cookbook-tab-count,
+ .preset-tab:has(svg) .preset-count {
+ display: none !important;
+ }
+}
+/* Icon + label layout inside each tab. */
+.gallery-tab {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+.gallery-tab-icon {
+ display: inline-flex;
+ align-items: center;
+ opacity: 0.85;
+}
+.gallery-tab.active .gallery-tab-icon { opacity: 1; }
+/* Close × on the Edit tab — appears on hover. */
+.gallery-tab-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ margin-left: 2px;
+ border-radius: 4px;
+ opacity: 0;
+ transition: opacity 0.12s, background 0.12s;
+ /* Use a slightly larger glyph at a larger line-height so the × sits
+ visually centered — the U+00D7 character has uneven em-box metrics. */
+ font-size: 16px;
+ line-height: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ padding-bottom: 2px;
+ box-sizing: border-box;
+}
+.gallery-tab:hover .gallery-tab-close,
+.gallery-tab.active .gallery-tab-close { opacity: 0.65; }
+/* Hide the close × entirely on the Edit tab when there's no edit open. */
+.gallery-tab[data-tab="editor"]:not(.has-edit) .gallery-tab-close { display: none !important; }
+.gallery-tab-close:hover { opacity: 1 !important; background: color-mix(in srgb, var(--red) 25%, transparent); color: var(--red); }
+/* Inline rename input shown when the Edit tab is double-clicked. */
+.gallery-tab-rename-input {
+ background: var(--bg);
+ border: 1px solid var(--red);
+ border-radius: 3px;
+ color: var(--fg);
+ font: inherit;
+ font-size: 13px;
+ padding: 1px 4px;
+ width: 140px;
+ outline: none;
+}
+
+/* Albums tab — grid of album cards with cover thumbnails. */
+.gallery-albums-container {
+ padding: 12px 4px;
+ max-height: calc(92vh - 120px);
+ overflow-y: auto;
+ border: 2px dashed transparent;
+ border-radius: 8px;
+ transition: border-color 0.15s, background 0.15s;
+}
+.gallery-settings-container {
+ padding: 8px 4px;
+ max-height: calc(92vh - 120px);
+ overflow-y: auto;
+}
+.gallery-albums-container.gallery-dragover {
+ border-color: var(--red);
+ background: color-mix(in srgb, var(--red) 5%, transparent);
+}
+.gallery-albums-empty {
+ display: flex; flex-direction: column; align-items: center; gap: 12px;
+ padding: 48px 16px;
+ opacity: 0.7;
+ font-size: 13px;
+}
+.gallery-albums-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ gap: 12px;
+}
+.gallery-album-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ cursor: pointer;
+ transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
+}
+.gallery-album-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--red);
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.gallery-album-menu-btn {
+ position: absolute;
+ top: 6px; right: 6px;
+ width: 24px; height: 24px;
+ display: flex; align-items: center; justify-content: center;
+ background: color-mix(in srgb, var(--bg) 70%, transparent);
+ color: var(--fg);
+ border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+ border-radius: 6px;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.12s, background 0.12s;
+ z-index: 2;
+ backdrop-filter: blur(4px);
+ /* Plain "…" text — the previous SVG was rendering as a flat black
+ block on some themes when its currentColor didn't contrast. */
+ font-size: 16px;
+ line-height: 1;
+ font-weight: 600;
+ padding: 0 0 4px;
+}
+.gallery-album-card:hover .gallery-album-menu-btn,
+.gallery-album-menu-btn:focus-visible { opacity: 1; }
+.gallery-album-menu-btn:hover { background: var(--bg); }
+.gallery-album-menu-pop {
+ position: absolute;
+ top: 34px; right: 6px;
+ min-width: 140px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+ display: flex; flex-direction: column;
+ padding: 4px;
+ z-index: 3;
+}
+.gallery-album-menu-pop[hidden] { display: none; }
+.gallery-album-cover {
+ aspect-ratio: 4 / 3;
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+ display: flex; align-items: center; justify-content: center;
+ overflow: hidden;
+}
+.gallery-album-placeholder {
+ opacity: 0.4;
+ font-size: 40px;
+ display: flex; align-items: center; justify-content: center;
+ width: 100%; height: 100%;
+}
+.gallery-album-info {
+ padding: 8px 10px;
+ display: flex; flex-direction: column; gap: 2px;
+}
+.gallery-album-name {
+ font-size: 13px;
+ font-weight: 500;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+}
+.gallery-album-count {
+ font-size: 11px;
+ opacity: 0.55;
+}
+.gallery-album-card-add {
+ border-style: dashed;
+ opacity: 0.7;
+}
+.gallery-album-card-add:hover { opacity: 1; }
+.gallery-album-action-tile {
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+}
+
+/* Edit-tab empty state — shown before any image is loaded. */
+.gallery-editor-landing {
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
+ gap: 10px;
+ padding: 28px 16px;
+ flex: 1;
+ text-align: center;
+ color: var(--fg);
+}
+.gallery-editor-landing h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+.gallery-editor-heading {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+.gallery-editor-heading > svg { opacity: 0.6; flex: 0 0 auto; }
+/* "vision model" link in the AI-tagging description → opens Settings. */
+.ge-vision-link {
+ color: var(--accent, var(--red));
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ cursor: pointer;
+}
+.ge-vision-link:hover { opacity: 0.8; }
+.ge-alpha-tag {
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ vertical-align: middle;
+ padding: 1px 5px;
+ border-radius: 4px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
+ position: relative;
+ top: -1px;
+}
+.gallery-editor-landing-actions {
+ display: flex; gap: 10px;
+ margin-top: 8px;
+}
+/* Bigger primary action buttons specifically inside the editor landing —
+ the gallery-wide .gallery-select-btn is a compact toolbar style which is
+ too small for the empty-state hero. */
+.gallery-editor-landing-actions .gallery-select-btn {
+ padding: 7px 22px 17px;
+ font-size: 13px;
+ opacity: 0.85;
+}
+/* Visible template picker: every canvas ratio is inspectable without opening
+ a native menu, and its SVG outline previews the resulting canvas shape. */
+.gallery-editor-template-picker {
+ width: min(760px, 96%);
+ margin-top: 10px;
+}
+.gallery-editor-template-picker h4 {
+ margin: 0 0 7px;
+ font-size: 11px;
+ opacity: 0.55;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.gallery-editor-template-options {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(105px, 1fr));
+ gap: 6px;
+}
+.gallery-editor-template-option {
+ height: 72px;
+ min-width: 0;
+ padding: 6px 7px;
+ display: grid;
+ grid-template-rows: 28px auto auto;
+ justify-items: center;
+ align-items: center;
+ background: var(--panel);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ overflow: hidden;
+}
+.gallery-editor-template-option svg { color: var(--accent, var(--red)); }
+.gallery-editor-template-option:hover,
+.gallery-editor-template-option:focus-visible {
+ outline: none;
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 7%, var(--panel));
+}
+.gallery-editor-template-option span {
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 600;
+}
+.gallery-editor-template-option small {
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 9px;
+ opacity: 0.52;
+}
+@media (max-width: 620px) {
+ .gallery-editor-landing {
+ justify-content: flex-start;
+ gap: 7px;
+ padding: 18px 12px;
+ overflow-y: auto;
+ }
+ .gallery-editor-landing-actions {
+ width: 100%;
+ gap: 6px;
+ justify-content: center;
+ }
+ .gallery-editor-landing-actions .gallery-select-btn {
+ flex: 1 1 0;
+ min-width: 0;
+ max-width: 180px;
+ padding: 6px 8px 10px;
+ font-size: 11px;
+ }
+ .gallery-editor-template-picker {
+ width: 100%;
+ margin-top: 4px;
+ }
+ .gallery-editor-template-options {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ /* The common square, widescreen, portrait, and Instagram presets are
+ enough to start on a phone. Keep the print and 4K presets on desktop. */
+ .gallery-editor-template-option:nth-child(n + 5) {
+ display: none;
+ }
+}
+/* Saved-drafts grid on the editor landing. Surfaces every persisted
+ in-progress project so the user can resume without re-opening the
+ original photo. Each card: thumbnail + name + last-saved hint + ×. */
+.gallery-editor-drafts {
+ width: min(720px, 92%);
+ margin: 28px auto 0; /* centered */
+ padding-top: 18px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.gallery-editor-drafts {
+ position: relative;
+}
+.gallery-editor-drafts-loading {
+ position: absolute;
+ inset: 30px 0 0 0;
+ z-index: 4;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--panel);
+ border-radius: 6px;
+ min-height: 80px;
+}
+.gallery-editor-drafts-header {
+ display: flex;
+ align-items: center;
+ justify-content: center; /* center the title / search / select */
+ gap: 8px;
+ margin-bottom: 8px;
+}
+.gallery-editor-drafts-title {
+ margin: 0;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ opacity: 0.55;
+ font-weight: 600;
+ text-align: left;
+ flex-shrink: 0;
+}
+.gallery-editor-drafts-search {
+ flex: 1 1 auto;
+ min-width: 0;
+ max-width: none;
+ height: 26px; /* same thickness as the Select button */
+ box-sizing: border-box;
+ padding: 0 8px;
+ font-size: 12px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+}
+.gallery-editor-drafts-search:focus { outline: none; border-color: var(--red); }
+/* The shared .gallery-select-btn has a -4px top margin + tall asymmetric padding
+ meant for the main gallery toolbar; in this header it makes the button a
+ different height / offset from the search box. Normalize so it lines up. */
+.gallery-editor-drafts-header .gallery-select-btn {
+ margin-top: 0 !important;
+ height: 26px; /* match the search bar */
+ box-sizing: border-box;
+ padding: 0 10px;
+ font-size: 12px;
+ line-height: 1;
+ flex-shrink: 0;
+}
+
+.cookbook-official-filter {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: 100%;
+ color: var(--fg);
+ font-size: 10px;
+ opacity: 0.72;
+ white-space: nowrap;
+ position: relative;
+ top: 1px;
+}
+.cookbook-official-filter > span:first-child {
+ position: relative;
+ top: 1px;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 9px;
+}
+.cookbook-official-filter .toggle {
+ width: 26px;
+ height: 14px;
+}
+.cookbook-official-filter .toggle .slider:before {
+ width: 10px;
+ height: 10px;
+ left: 2px;
+ bottom: 2px;
+}
+.cookbook-official-filter .toggle input:checked + .slider:before {
+ transform: translateX(12px);
+}
+#gallery-editor-drafts-select { margin-left: auto; flex-shrink: 0; }
+#gallery-editor-drafts-select.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+}
+/* Graceful exit when a project card is deleted — fade + shrink before re-render. */
+.gallery-editor-draft-card.gallery-draft-removing {
+ opacity: 0;
+ transform: scale(0.92);
+ transition: opacity 0.24s ease, transform 0.24s ease;
+ pointer-events: none;
+}
+/* Draft select dot uses the standard small .gallery-select-dot look (same as the
+ photo grid) — no oversized white-ring checkbox. */
+.gallery-editor-drafts-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ gap: 10px;
+}
+.gallery-drafts-just-opened > .gallery-editor-draft-card {
+ animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
+}
+.gallery-drafts-just-opened > :nth-child(1) { animation-delay: 0.02s; }
+.gallery-drafts-just-opened > :nth-child(2) { animation-delay: 0.04s; }
+.gallery-drafts-just-opened > :nth-child(3) { animation-delay: 0.06s; }
+.gallery-drafts-just-opened > :nth-child(4) { animation-delay: 0.08s; }
+.gallery-drafts-just-opened > :nth-child(5) { animation-delay: 0.10s; }
+.gallery-drafts-just-opened > :nth-child(6) { animation-delay: 0.12s; }
+.gallery-drafts-just-opened > :nth-child(7) { animation-delay: 0.14s; }
+.gallery-drafts-just-opened > :nth-child(8) { animation-delay: 0.16s; }
+.gallery-drafts-just-opened > :nth-child(9) { animation-delay: 0.18s; }
+.gallery-drafts-just-opened > :nth-child(10) { animation-delay: 0.20s; }
+.gallery-drafts-just-opened > :nth-child(11) { animation-delay: 0.22s; }
+.gallery-drafts-just-opened > :nth-child(12) { animation-delay: 0.24s; }
+.gallery-drafts-just-opened > :nth-child(13) { animation-delay: 0.26s; }
+.gallery-drafts-just-opened > :nth-child(14) { animation-delay: 0.28s; }
+.gallery-drafts-just-opened > :nth-child(15) { animation-delay: 0.30s; }
+.gallery-drafts-just-opened > :nth-child(16) { animation-delay: 0.32s; }
+.gallery-drafts-just-opened > :nth-child(17) { animation-delay: 0.34s; }
+.gallery-drafts-just-opened > :nth-child(18) { animation-delay: 0.36s; }
+.gallery-drafts-just-opened > :nth-child(19) { animation-delay: 0.38s; }
+.gallery-drafts-just-opened > :nth-child(20) { animation-delay: 0.40s; }
+.gallery-drafts-just-opened > :nth-child(n+21) { animation-delay: 0.42s; }
+.gallery-drafts-view-toggle {
+ display: inline-flex;
+ position: relative;
+ top: 0;
+ height: 24px;
+ flex: 0 0 auto;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: var(--bg);
+}
+.gallery-drafts-view-toggle button {
+ width: 25px;
+ height: 22px;
+ margin: 0;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ border-right: 1px solid var(--border);
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+ cursor: pointer;
+ line-height: 0;
+}
+.gallery-drafts-view-toggle button:last-child { border-right: 0; }
+.gallery-drafts-view-toggle button:hover { color: var(--fg); }
+.gallery-drafts-view-toggle button.active {
+ height: 22px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+.gallery-editor-drafts-grid.list-view .gallery-editor-draft-thumb {
+ width: 44px;
+ height: 44px;
+ aspect-ratio: auto;
+}
+.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete {
+ position: static;
+ opacity: 0.55;
+ background: transparent;
+ color: var(--fg);
+}
+.gallery-editor-draft-delete-trash { display: none; }
+.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete-trash {
+ display: block;
+ width: 18px;
+ height: 18px;
+ position: relative;
+ top: -2px;
+}
+.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete:hover {
+ opacity: 1;
+ color: var(--color-error, #f44);
+ background: color-mix(in srgb, var(--color-error, #f44) 10%, transparent);
+}
+.gallery-editor-draft-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 6px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+ cursor: pointer;
+ transition: border-color 0.15s, background 0.15s, transform 0.15s;
+ text-align: left;
+}
+.gallery-editor-draft-card:hover {
+ border-color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--panel) 90%, transparent);
+}
+.gallery-editor-draft-thumb {
+ width: 100%;
+ aspect-ratio: 1;
+ object-fit: cover;
+ border-radius: 4px;
+ background: var(--bg);
+ display: block;
+}
+.gallery-editor-draft-thumb-empty {
+ background: repeating-linear-gradient(
+ 45deg,
+ color-mix(in srgb, var(--fg) 4%, transparent),
+ color-mix(in srgb, var(--fg) 4%, transparent) 6px,
+ transparent 6px,
+ transparent 12px
+ );
+}
+.gallery-editor-draft-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+.gallery-editor-draft-name {
+ font-size: 12px;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.gallery-editor-draft-meta {
+ font-size: 10px;
+ opacity: 0.55;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.gallery-editor-draft-delete {
+ position: absolute;
+ top: -2px;
+ right: 8px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: none;
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ font-size: 14px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.12s, background 0.12s;
+}
+.gallery-editor-draft-delete {
+ padding-bottom: 2px;
+}
+.gallery-editor-draft-delete:hover { background: var(--red); }
+/* On very narrow phones, give project search its own row instead of
+ compressing the title and view controls into an overflowing header. */
+@media (max-width: 360px) {
+ .gallery-editor-drafts-header {
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ gap: 6px;
+ }
+ .gallery-editor-drafts-title {
+ flex: 1 1 auto;
+ }
+ .gallery-editor-drafts-search {
+ order: 10;
+ flex: 1 1 100%;
+ width: 100%;
+ max-width: none;
+ }
+ #gallery-editor-drafts-select {
+ margin-left: 0;
+ }
+}
+/* Editor layout */
+.gallery-editor {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ height: 100%;
+ overflow: hidden;
+ /* Prevent the editor's own scroll/touch from chaining out to the
+ gallery modal or the page body — critical on mobile so touching
+ the canvas doesn't slide the whole page. */
+ overscroll-behavior: contain;
+ touch-action: pan-y;
+}
+/* Pin the topbar to the top of the editor so its action buttons remain
+ visible if the gallery modal body itself ends up scrolling. */
+.ge-topbar { position: sticky; top: 0; z-index: 5; }
+
+/* Top bar */
+.ge-topbar {
+ display: flex;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: thin;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 10px;
+ background: var(--panel);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ gap: 8px;
+}
+.ge-topbar-left, .ge-topbar-right {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 4px;
+}
+.ge-topbar-right > * {
+ flex-shrink: 0;
+}
+.ge-topbar .dropdown[popover] {
+ position: fixed;
+ margin: 0;
+ bottom: auto;
+ max-height: calc(100vh - 80px);
+ overflow-y: auto;
+}
+.ge-draft-status {
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: 92px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: color-mix(in srgb, var(--fg) 52%, transparent);
+ font-size: 10px;
+ line-height: 1;
+ transition: color 0.15s, opacity 0.15s;
+}
diff --git a/static/css/13-image-editor.css b/static/css/13-image-editor.css
new file mode 100644
index 000000000..d759ed7d3
--- /dev/null
+++ b/static/css/13-image-editor.css
@@ -0,0 +1,4823 @@
+/* 13-image-editor - 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.
+ */
+.ge-draft-status[data-state="pending"],
+.ge-draft-status[data-state="saving"] { color: color-mix(in srgb, var(--accent, var(--red)) 75%, var(--fg)); }
+.ge-draft-status[data-state="saved"] { color: color-mix(in srgb, var(--color-success, #4ade80) 78%, var(--fg)); }
+.ge-draft-status[data-state="error"] { color: var(--color-error, #ef4444); }
+/* "ALPHA" badge — flags the editor as in-development. */
+.ge-alpha-badge {
+ flex-shrink: 0;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ line-height: 1;
+ padding: 3px 6px;
+ margin-right: 4px;
+ border-radius: 4px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
+ text-transform: uppercase;
+ user-select: none;
+ cursor: default;
+}
+@media (max-width: 768px) {
+ /* Keep the toolbar tight on mobile — badge stays but shrinks. */
+ .ge-alpha-badge { font-size: 8px; padding: 2px 5px; margin-right: 2px; }
+}
+.ge-diffusion-status {
+ font-size: 11px;
+ cursor: pointer;
+ padding: 2px 8px;
+ border-radius: 4px;
+ transition: all 0.15s;
+ white-space: nowrap;
+}
+.ge-diffusion-status.online {
+ color: var(--color-success, #4ade80);
+}
+.ge-diffusion-status.offline {
+ color: var(--color-error, #ef4444);
+ opacity: 0.7;
+}
+.ge-diffusion-status.offline:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--color-error) 10%, transparent);
+}
+.ge-topbar-fill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+.ge-topbar-fill:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+ border-color: var(--accent, var(--red));
+}
+/* Mask-color swatch in the topbar — small label + circular swatch so
+ the user can pick a contrasting overlay colour from anywhere. */
+.ge-topbar-mask-color-wrap {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+}
+.ge-topbar-mask-color-label {
+ font-size: 10px;
+ opacity: 0.6;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.ge-topbar-mask-color {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 18px;
+}
+.ge-topbar-mask-color.cp-swatch-input {
+ width: 18px;
+ height: 18px;
+ flex: 0 0 18px;
+ border-radius: 50%;
+ padding: 0;
+}
+.ge-topbar-sep {
+ width: 1px;
+ height: 16px;
+ background: var(--border);
+ margin: 0 4px;
+}
+
+/* Editor body (toolbar + canvas + panel) */
+.ge-editor-body {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ position: relative;
+}
+.gallery-editor-container {
+ flex: 1;
+ min-height: 0;
+ display: none;
+}
+
+.ge-toolbar {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ /* Asymmetric horizontal padding shifts the whole column 4 px left —
+ buttons, hover backgrounds, active highlight pills, and section
+ separators all move together. */
+ padding: 8px 8px 8px 0;
+ background: var(--panel);
+ border-right: 1px solid var(--border);
+ width: 64px;
+ flex-shrink: 0;
+ min-height: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ scrollbar-width: none;
+}
+.ge-toolbar::-webkit-scrollbar { display: none; }
+.ge-tool-sep {
+ font-size: 8px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--color-muted);
+ text-align: center;
+ padding: 8px 0 4px;
+ border-top: 1px solid var(--border);
+ margin-top: 4px;
+ opacity: 0.6;
+ flex-shrink: 0;
+}
+/* All tool buttons share a fixed height so the column reads as a clean
+ grid. Long labels (e.g. "Remove BG") truncate with ellipsis instead of
+ wrapping to two lines and making one button taller than the others. */
+.ge-tool-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ padding: 0 2px;
+ height: 42px;
+ flex-shrink: 0;
+ border: none;
+ background: none;
+ color: var(--fg);
+ opacity: 0.6;
+ cursor: pointer;
+ border-radius: 6px;
+ transition: background 0.15s, opacity 0.15s;
+}
+.ge-tool-btn { position: relative; }
+/* Hover background is now a pseudo so we can shift it 2 px left
+ independently from the button's own bounds (which contain the icon
+ and label at their already-shifted positions). */
+.ge-tool-btn:hover { opacity: 0.85; }
+.ge-tool-btn.active {
+ opacity: 1;
+ color: var(--red);
+ background: none;
+ /* Padding stays the same as inactive buttons so the icon stays
+ horizontally aligned with its neighbors. The taller highlight is
+ drawn by ::before so it can extend up/down without pushing the
+ button or shifting the icon. */
+}
+/* Tool button contents stay above the active highlight pseudo. Also
+ shifted an additional 2 px left so the icon + label sit further inside
+ the highlight pill (which still anchors to the button bounds). */
+.ge-tool-btn > * { position: relative; z-index: 1; left: -2px; }
+.ge-tool-icon { font-size: 18px; line-height: 1; }
+.ge-tool-label { font-size: 10px; line-height: 1.25; }
+/* AI badge — same ✦ glyph the Enhance tool uses, pinned to the top-
+ left of any tool button marked `ai: true`. Same color as the icon
+ itself (inherits foreground) so it reads as part of the tool, not
+ a flag. */
+.ge-tool-ai {
+ position: absolute !important;
+ top: 1px;
+ z-index: 2;
+ color: inherit;
+ opacity: 0.7;
+ pointer-events: none;
+ font-size: 11px;
+ line-height: 1;
+ font-weight: 700;
+ left: 3px !important;
+}
+.ge-tool-btn.is-ai:hover .ge-tool-ai { opacity: 0.95; }
+.ge-tool-btn.is-ai.active .ge-tool-ai { opacity: 1; }
+/* Inline ✦ marker used on AI action buttons (Generate, Remove, etc.)
+ so they read as AI-backed at a glance — same glyph as .ge-tool-ai
+ in the toolbar, just sitting inline next to the label. */
+.ge-btn-ai-mark {
+ display: inline-block;
+ font-size: 11px;
+ line-height: 1;
+ font-weight: 700;
+ opacity: 0.75;
+ margin-right: 1px;
+}
+
+/* Per-tool clear-selection badge — shows a tiny X in the top-right of
+ the Lasso / Wand button when each holds a selection. Click clears
+ the selection without switching tools (handler stops propagation). */
+.ge-tool-clear {
+ position: absolute !important;
+ top: 3px;
+ right: 3px;
+ width: 14px;
+ height: 14px;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ color: #fff;
+ background: var(--red);
+ cursor: pointer;
+ opacity: 0.95;
+ z-index: 2;
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 70%, transparent),
+ 0 1px 4px color-mix(in srgb, var(--red) 50%, transparent);
+ transition: opacity 0.12s, transform 0.12s, box-shadow 0.12s;
+ left: auto !important;
+}
+.ge-tool-clear:hover {
+ opacity: 1;
+ transform: scale(1.15);
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 80%, transparent),
+ 0 2px 6px color-mix(in srgb, var(--red) 60%, transparent);
+}
+
+/* Aspect-ratio placeholder shown while a draft is loading. */
+.ge-canvas-placeholder {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border: 1px dashed color-mix(in srgb, var(--fg) 20%, transparent);
+ border-radius: 4px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1;
+}
+/* Canvas area (center) */
+.ge-canvas-area {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: auto;
+ background: var(--bg);
+ position: relative;
+ min-width: 0;
+ /* Stop touch interactions inside the editor from dragging the whole
+ page (or the gallery modal). Touches on the canvas are owned by the
+ editor's drawing logic — no native pan/zoom. */
+ overscroll-behavior: contain;
+}
+.ge-ruler {
+ position: absolute;
+ z-index: 16;
+ touch-action: none;
+ user-select: none;
+}
+.ge-ruler-horizontal {
+ top: 0;
+ left: 18px;
+ right: 0;
+ width: calc(100% - 18px);
+ height: 18px;
+ cursor: row-resize;
+}
+.ge-ruler-vertical {
+ top: 18px;
+ bottom: 0;
+ left: 0;
+ width: 18px;
+ height: calc(100% - 18px);
+ cursor: col-resize;
+}
+.ge-ruler-corner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 17;
+ width: 18px;
+ height: 18px;
+ background: var(--panel);
+ border-right: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ box-sizing: border-box;
+}
+.ge-wand-loading {
+ position: absolute;
+ inset: 0;
+ z-index: 25;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--bg) 30%, transparent);
+ pointer-events: none;
+}
+.ge-ai-command {
+ position: absolute;
+ left: 50%;
+ bottom: 14px;
+ z-index: 18;
+ width: min(430px, calc(100% - 28px));
+ transform: translateX(-50%);
+ padding: 7px;
+ box-sizing: border-box;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel, var(--bg)) 94%, transparent);
+ box-shadow: 0 8px 22px color-mix(in srgb, #000 26%, transparent);
+ backdrop-filter: blur(8px);
+ transition: bottom 0.16s ease, opacity 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
+}
+.ge-ai-command-toggle {
+ width: 100%;
+ height: 28px;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ border: 0;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--accent, var(--red));
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+}
+.ge-ai-command-toggle-caret {
+ opacity: 0.7;
+ flex-shrink: 0;
+}
+.ge-ai-command-head {
+ display: flex;
+ align-items: center;
+ min-height: 24px;
+ padding: 0 2px 5px;
+ color: var(--fg);
+}
+.ge-ai-command-title {
+ margin-left: 5px;
+ font-size: 11px;
+ font-weight: 650;
+}
+.ge-ai-command-scope {
+ margin-left: 7px;
+ padding-left: 7px;
+ border-left: 1px solid var(--border);
+ color: var(--fg-muted);
+ font-size: 9px;
+ line-height: 1;
+ text-transform: uppercase;
+}
+.ge-ai-command-collapsed {
+ bottom: -16px;
+ width: auto;
+ min-width: 102px;
+ padding: 2px 9px;
+ opacity: 0.32;
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 18%, var(--border));
+ box-shadow: 0 3px 10px color-mix(in srgb, #000 16%, transparent);
+}
+.ge-canvas-area:hover .ge-ai-command-collapsed,
+.ge-ai-command-collapsed:hover,
+.ge-ai-command-collapsed:focus-within {
+ bottom: 10px;
+ opacity: 0.94;
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
+ box-shadow: 0 8px 22px color-mix(in srgb, #000 24%, transparent);
+}
+.ge-ai-command-form {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.ge-ai-command-input {
+ flex: 1 1 auto;
+ min-width: 0;
+ height: 32px;
+ box-sizing: border-box;
+ padding: 0 9px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--bg) 76%, #000 24%);
+ color: var(--fg);
+ font: inherit;
+ font-size: 12px;
+}
+.ge-ai-command-input:focus {
+ outline: none;
+ border-color: var(--accent, var(--red));
+}
+.ge-ai-command-clear {
+ position: relative;
+ top: -2px;
+ width: 26px;
+ height: 32px;
+ flex: 0 0 26px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: -38px;
+ border: 0;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--fg-muted);
+ cursor: pointer;
+ padding: 0;
+ z-index: 1;
+}
+.ge-ai-command-clear:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.ge-ai-command-run {
+ position: relative;
+ top: -2px;
+ 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;
+ flex: 0 0 auto;
+ transition: background 0.25s, opacity 0.12s;
+ overflow: hidden;
+}
+.ge-ai-command-run:hover {
+ background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white));
+}
+.ge-ai-command-busy .ge-ai-command-run {
+ opacity: 0.58;
+ cursor: wait;
+}
+.ge-ai-command-busy .ge-ai-command-run svg {
+ display: none;
+}
+.ge-ai-command-close {
+ width: 28px;
+ height: 32px;
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
+ border-radius: 8px;
+ background: transparent;
+ color: var(--fg);
+ opacity: 0.52;
+ cursor: pointer;
+ padding: 0;
+ margin-left: auto;
+}
+.ge-ai-command-close:hover {
+ opacity: 0.9;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.ge-ai-command-suggestions {
+ position: absolute;
+ left: 7px;
+ right: 7px;
+ bottom: calc(100% - 2px);
+ z-index: 2;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ max-height: 236px;
+ overflow: auto;
+ padding: 5px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 34%, var(--border));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel, var(--bg)) 97%, #000 3%);
+ box-shadow: 0 10px 28px color-mix(in srgb, #000 28%, transparent);
+}
+.ge-ai-command-suggestion {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 38px minmax(82px, auto) minmax(0, 1fr);
+ align-items: center;
+ gap: 8px;
+ min-height: 28px;
+ padding: 4px 7px;
+ border: 0;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--fg);
+ text-align: left;
+ cursor: pointer;
+ font: inherit;
+}
+.ge-ai-command-suggestion:hover,
+.ge-ai-command-suggestion.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+}
+.ge-ai-command-suggestion-kind {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--accent, var(--red));
+ font-size: 8px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+.ge-ai-command-suggestion-main {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 11px;
+ font-weight: 650;
+}
+.ge-ai-command-suggestion-hint {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--fg-muted);
+ font-size: 10px;
+}
+.ge-ai-command-status {
+ margin-top: 4px;
+ font-size: 10px;
+ line-height: 1.3;
+ color: var(--fg-muted);
+}
+.ge-ai-command-status[data-kind="running"] {
+ color: var(--accent, var(--red));
+}
+.ge-ai-command-status[data-kind="done"] {
+ color: var(--green, #4caf50);
+}
+.ge-ai-command-status[data-kind="error"] {
+ color: var(--red, #e5484d);
+}
+/* Loading overlay shown while a large image is decoding — centered
+ whirlpool + "Loading" caption over a translucent dim. */
+.ge-loading-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--bg) 70%, transparent);
+ pointer-events: none;
+}
+/* Full-editor cover (project load): above the toolbar/panel and near-opaque so
+ the top toolbar/old content doesn't peek through while loading. */
+.ge-loading-overlay-full {
+ z-index: 200;
+ background: color-mix(in srgb, var(--bg) 92%, transparent);
+ pointer-events: auto;
+}
+.ge-loading-overlay .ge-loading-inner {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 16px;
+ border-radius: 10px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.ge-loading-overlay .ge-loading-text {
+ font-size: 12px;
+ opacity: 0.8;
+}
+
+/* Shortcuts cheatsheet (toggled by `?` or the keyboard icon in the top bar). */
+#ge-shortcuts-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 50;
+ background: color-mix(in srgb, var(--bg) 70%, transparent);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+}
+#ge-shortcuts-overlay[hidden] { display: none; }
+.ge-shortcuts-card {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 14px 18px 12px;
+ box-shadow: 0 8px 30px color-mix(in srgb, var(--fg) 25%, transparent);
+ width: min(720px, 92vw);
+ max-height: 86vh;
+ overflow-y: auto;
+ color: var(--fg);
+}
+.ge-shortcuts-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ font-weight: 600;
+ font-size: 13px;
+}
+.ge-shortcuts-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 14px 24px;
+}
+@media (min-width: 720px) {
+ .ge-shortcuts-grid { grid-template-columns: repeat(4, 1fr); }
+}
+.ge-shortcuts-col h5 {
+ margin: 0 0 6px;
+ font-size: 11px;
+ opacity: 0.6;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ font-weight: 600;
+}
+.ge-shortcuts-foot {
+ margin-top: 12px;
+ font-size: 11px;
+ opacity: 0.55;
+ text-align: center;
+}
+.ge-main-canvas {
+ /* Cursor is managed entirely by JS so each tool can show its own
+ icon (move arrow, circle overlay, etc.) without CSS fighting it. */
+ image-rendering: pixelated;
+ box-shadow: 0 2px 12px rgba(0,0,0,0.3);
+ background: repeating-conic-gradient(#808080 0% 25%, #a0a0a0 0% 50%) 50% / 16px 16px;
+}
+
+/* Transform-overlay canvas — sits over the main canvas with extra
+ margin so resize / rotation handles can render OUTSIDE the image
+ bounds. Pointer events disabled so it doesn't intercept clicks. */
+.ge-transform-overlay {
+ position: absolute;
+ image-rendering: pixelated;
+ pointer-events: none;
+ z-index: 5;
+}
+.ge-selection-overlay {
+ position: absolute;
+ image-rendering: pixelated;
+ pointer-events: none;
+ z-index: 4;
+}
+.ge-quick-mask-bar {
+ position: absolute;
+ top: 10px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 18;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 32px;
+ padding: 4px 5px 4px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel);
+ box-shadow: 0 4px 16px rgba(0,0,0,0.24);
+ color: var(--fg);
+ font-size: 11px;
+ white-space: nowrap;
+}
+.ge-quick-mask-label { font-weight: 600; }
+.ge-quick-mask-tools { display: flex; }
+.ge-quick-mask-tool,
+.ge-quick-mask-done {
+ min-height: 24px;
+ padding: 2px 8px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+.ge-quick-mask-tool:first-child { border-radius: 4px 0 0 4px; }
+.ge-quick-mask-tool:last-child { border-radius: 0 4px 4px 0; border-left: 0; }
+.ge-quick-mask-tool.active { background: var(--accent); color: #fff; border-color: var(--accent); }
+.ge-quick-mask-done { border-radius: 4px; }
+.ge-marquee-constraint-row {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ gap: 5px 7px;
+ font-size: 10px;
+}
+.ge-marquee-constraint-row > select {
+ min-width: 0;
+ height: 26px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-marquee-dimensions {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 26px;
+ gap: 5px;
+ align-items: center;
+}
+.ge-marquee-dimensions label {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ gap: 4px;
+}
+.ge-marquee-dimensions input {
+ width: 100%;
+ min-width: 0;
+ height: 26px;
+ padding: 2px 5px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-marquee-dimensions .ge-icon-btn { width: 26px; height: 26px; }
+
+/* Right panel */
+.ge-right-panel {
+ width: 250px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ border-left: 1px solid var(--border);
+ background: var(--panel);
+ /* The whole panel scrolls so every tool control is reachable; the
+ layers-actions row uses position:sticky to stay pinned at the
+ bottom of the viewport while scrolling. */
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+
+/* Controls */
+.ge-controls {
+ flex: 0 0 auto;
+ padding: 10px 10px 6px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.ge-layer-geometry-section {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+#ge-layer-geometry-name {
+ margin-left: auto;
+ min-width: 0;
+ max-width: 110px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: var(--color-muted);
+ font-size: 9px;
+ font-weight: 400;
+ text-transform: none;
+}
+.ge-layer-geometry-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 5px;
+}
+.ge-layer-geometry-grid label {
+ display: grid;
+ grid-template-columns: 12px minmax(0, 1fr);
+ align-items: center;
+ gap: 2px;
+ min-width: 0;
+ color: var(--color-muted);
+ font-size: 9px;
+}
+.ge-layer-geometry-grid input {
+ width: 100%;
+ height: 26px;
+ box-sizing: border-box;
+ min-width: 0;
+ padding: 3px 4px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+ text-align: right;
+ appearance: textfield;
+}
+.ge-layer-geometry-grid input:focus { outline: none; border-color: var(--red); }
+.ge-layer-geometry-grid input[readonly] { opacity: 0.58; cursor: default; }
+@media (max-width: 700px) {
+ .ge-layer-geometry-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
+}
+/* Brush controls (Color + Size) — give the two rows breathing room
+ between them, since neither sits inside a separator-bordered section. */
+#ge-brush-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 9px;
+}
+.ge-brush-pressure-row { justify-content: space-between; gap: 6px; }
+.ge-pressure-option { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; opacity: 0.7; }
+.ge-pressure-option .toggle-switch { width: 24px; height: 14px; }
+.ge-pressure-option .toggle-slider::before { width: 10px; height: 10px; }
+.ge-pressure-option .toggle-switch input:checked + .toggle-slider::before { transform: translateX(10px); }
+.ge-brush-preset-row select { flex: 1 1 auto; min-width: 0; }
+.ge-brush-preset-row .ge-btn { flex: 0 0 auto; }
+.ge-text-section,
+.ge-shape-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+}
+.ge-shape-gradient-extra-stops {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.ge-gradient-extra-stop {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
+ gap: 6px;
+ align-items: end;
+}
+.ge-gradient-stop-remove {
+ min-width: 28px;
+ height: 28px;
+ padding: 0 6px;
+}
+.ge-text-section textarea,
+.ge-text-section select,
+.ge-text-section input[type="number"],
+.ge-shape-section input[type="number"] {
+ box-sizing: border-box;
+ min-width: 0;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-text-section textarea {
+ width: 100%;
+ min-height: 58px;
+ resize: vertical;
+ padding: 6px 7px;
+ line-height: 1.35;
+}
+.ge-text-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(56px, 0.42fr) auto;
+ gap: 6px;
+ align-items: end;
+}
+.ge-text-field {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ min-width: 0;
+ font-size: 9px;
+ color: var(--color-muted);
+}
+.ge-text-field select,
+.ge-text-field input { width: 100%; height: 28px; padding: 3px 6px; }
+.ge-text-format-row { gap: 4px; }
+.ge-text-toggle {
+ width: auto;
+ min-width: 24px;
+ flex: 1 1 24px;
+ height: 28px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ cursor: pointer;
+}
+.ge-text-toggle:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); }
+.ge-text-toggle.active {
+ color: var(--red);
+ border-color: color-mix(in srgb, var(--red) 55%, var(--border));
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+.ge-text-format-sep { width: 1px; height: 20px; background: var(--border); margin: 0 2px; }
+.ge-shape-types { display: flex; gap: 4px; }
+.ge-shape-sides-field { max-width: 90px; }
+.ge-direct-text-editor {
+ position: fixed;
+ z-index: 10020;
+ box-sizing: border-box;
+ padding: 4px;
+ overflow: hidden;
+ resize: both;
+ border: 1px dashed var(--red);
+ outline: 1px solid color-mix(in srgb, var(--red) 24%, transparent);
+ border-radius: 2px;
+ background: color-mix(in srgb, var(--bg) 8%, transparent);
+ caret-color: var(--red);
+ transform-origin: top left;
+ box-shadow: none;
+}
+
+/* Floating Transform popup — horizontal layout, draggable from
+ anywhere on the popup (matches the FX / adjust popups). Defaults
+ to the right side of the editor (over the layers panel). */
+/* Match the .ge-adj-popup layout convention: icon + title + [_][×]
+ header bar, then the body. Drag from the header (same as FX popups). */
+.ge-transform-popup {
+ position: absolute;
+ z-index: 10020;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0 0 8px;
+ background: color-mix(in srgb, var(--panel) 98%, var(--bg));
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+ backdrop-filter: blur(6px);
+ user-select: none;
+}
+.ge-transform-popup-head {
+ /* Inherits .ge-adj-head styling — flex row, icon + title + buttons,
+ bottom border, grab cursor — with the title nudged down 2px for
+ visual balance and explicit horizontal padding so the icon doesn't
+ hug the left edge. */
+ display: flex !important;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ padding-top: 10px;
+ touch-action: none;
+ cursor: grab;
+}
+.ge-transform-popup-head:active { cursor: grabbing; }
+.ge-transform-popup-head .ge-head-btns { margin-left: auto !important; }
+.ge-transform-popup-head .ge-adj-title { position: relative; top: 2px; }
+/* Icon nudged 4px lower than the title baseline so it reads as the
+ row's anchor rather than floating above it. */
+.ge-transform-popup-head .ge-adj-icon { position: relative; top: 4px; }
+/* Mobile-only: shift the head content UP to match the rest of the
+ editor's mobile popup styling. */
+@media (max-width: 820px) {
+ .ge-transform-popup-head .ge-adj-title { top: -2px; }
+ .ge-transform-popup-head .ge-adj-icon { top: 0; }
+}
+/* Make sure the head-buttons cluster sits hard-right with a small
+ gap, matching the FX-popup convention. */
+.ge-transform-popup-head .ge-head-btns { margin-left: auto; }
+.ge-transform-min-hint {
+ display: inline-flex;
+ align-items: center;
+ opacity: 0.45;
+ margin-right: -2px;
+ line-height: 1;
+ color: var(--fg-muted);
+}
+/* Small "Merge" text label next to each merge / flatten button so the
+ icons read at a glance instead of needing the title-tooltip. */
+.ge-merge-label {
+ font-size: 10px;
+ margin-left: 4px;
+ opacity: 0.75;
+}
+.ge-transform-popup-body {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+}
+.ge-transform-popup-hint { padding: 0 12px; }
+.ge-transform-field {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ position: relative;
+}
+.ge-transform-field label {
+ font-size: 11px;
+ font-weight: 600;
+ opacity: 0.65;
+ min-width: 12px;
+ text-align: right;
+}
+/* Hide the native browser spin-buttons entirely — we render our own
+ themed ▲/▼ via .ge-transform-spin (see below). */
+.ge-transform-popup-input {
+ width: 76px;
+ height: 24px;
+ padding: 0 18px 0 6px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ cursor: text;
+ -moz-appearance: textfield;
+ box-sizing: border-box;
+}
+/* Rotation field reserves extra room on the right for both the °
+ suffix AND the custom spinner so they don't sit on top of each other.
+ The suffix renders inside that reserved zone. */
+.ge-transform-popup-input-rot { width: 76px; padding-right: 30px; }
+.ge-transform-popup-input:focus { outline: none; border-color: var(--red); }
+/* Custom themed spinner — two slim chevron buttons stacked to the right
+ of the input. Tight stack so the pair reads as one control instead of
+ two floating arrows. */
+.ge-transform-spin {
+ position: absolute;
+ right: 3px;
+ top: 50%;
+ /* Nudged up 4px relative to the input's vertical center. */
+ transform: translateY(calc(-50% - 4px));
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0;
+ height: 18px;
+}
+.ge-transform-spin button {
+ flex: 1 1 0;
+ width: 12px;
+ min-height: 0;
+ padding: 0;
+ background: transparent;
+ color: var(--fg-muted);
+ border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
+ font-size: 7px;
+ line-height: 1;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
+}
+.ge-transform-spin button:first-child { border-radius: 3px 3px 0 0; border-bottom: 0; }
+.ge-transform-spin button:last-child { border-radius: 0 0 3px 3px; }
+.ge-transform-spin button:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+}
+.ge-transform-spin button:active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+}
+/* Position the ° suffix to the LEFT of the spinner so they don't
+ overlap. The rotation input's larger padding leaves room. */
+.ge-transform-popup-suffix {
+ position: absolute;
+ right: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 10px;
+ opacity: 0.55;
+ pointer-events: none;
+}
+.ge-transform-aspect-btn {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg-muted);
+ cursor: pointer;
+ /* 2px smaller than the W/H input height so it reads as a chip-style
+ toggle rather than another form field. Pulled up 2px so it tucks
+ between W and H rather than sitting on their baseline. */
+ height: 22px;
+ width: 22px;
+ padding: 0;
+ position: relative;
+ top: -3px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.5;
+ transition: opacity 0.12s, color 0.12s, border-color 0.12s;
+}
+.ge-transform-aspect-btn:hover { opacity: 1; }
+.ge-transform-aspect-btn.active {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
+}
+/* Quick-action cluster (flip H, flip V, rotate 90°) — sits between the
+ rotation input and the Apply button. Each button matches the input
+ height and uses the editor's accent palette on hover. */
+.ge-transform-quick {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ padding: 0 4px;
+ /* Pulled up 2px so the flip/rotate icons sit slightly higher than the
+ W/H/rotation inputs, matching the visual weight of the head row. */
+ position: relative;
+ top: -2px;
+ border-left: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+}
+.ge-transform-quick-btn {
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ background: none;
+ color: var(--fg-muted);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ transition: opacity 0.12s, color 0.12s, border-color 0.12s, background 0.12s;
+}
+.ge-transform-quick-btn:hover {
+ opacity: 1;
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+.ge-transform-quick-btn:active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
+}
+.ge-transform-popup-hint {
+ font-size: 10px;
+ opacity: 0.5;
+ margin: 0;
+ line-height: 1.3;
+}
+.ge-transform-preserve-field {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 0 12px;
+ font-size: 10px;
+ color: var(--fg-muted);
+ cursor: pointer;
+}
+.ge-transform-preserve-field input {
+ accent-color: var(--accent, var(--red));
+ margin: 0;
+}
+
+/* Floating Inpaint prompt — pops next to the user's last brush stroke
+ so they can type a description and Generate without diverting to
+ the side panel. */
+.ge-inpaint-popup {
+ position: fixed;
+ z-index: 280;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
+}
+.ge-inpaint-popup-input {
+ width: 240px;
+ padding: 6px 8px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 12px;
+ position: relative;
+ top: 0;
+}
+.ge-inpaint-popup-input:focus { outline: none; border-color: var(--red); }
+.ge-inpaint-popup-run {
+ padding: 6px 12px;
+ font-size: 12px;
+ font-weight: 600;
+ position: relative;
+ top: 1px;
+}
+.ge-inpaint-popup-close {
+ background: none;
+ border: none;
+ color: var(--fg-muted);
+ font-size: 16px;
+ line-height: 1;
+ padding: 2px 4px;
+ margin-left: 2px;
+ cursor: pointer;
+ position: relative;
+ top: 0;
+ opacity: 0.7;
+ transition: opacity 0.15s, color 0.15s;
+}
+.ge-inpaint-popup-close:hover { opacity: 1; color: var(--fg); }
+/* Loading state: the popup's panel chrome (bg/border/shadow) gets out
+ of the way so only the whirlpool floats over the canvas. */
+.ge-inpaint-popup.ge-inpaint-popup-loading {
+ background: transparent;
+ border-color: transparent;
+ box-shadow: none;
+ padding: 0;
+ min-width: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+.ge-controls input[type="range"] {
+ width: 100%;
+ box-sizing: border-box;
+ margin: 0;
+ display: block;
+}
+.ge-control-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ color: var(--fg);
+}
+.ge-control-row label {
+ flex-shrink: 0;
+ opacity: 0.7;
+ min-width: 36px;
+}
+.ge-color-picker {
+ width: 24px;
+ height: 24px;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ padding: 0;
+ cursor: pointer;
+ background: none;
+ overflow: hidden;
+}
+/* attachColorPicker swaps the native for a styled
+ text input with the same `.ge-color-picker` class plus
+ `.cp-swatch-input`. Force the 24×24 circular swatch back on it so
+ it doesn't render as a wide text box stretched across the row. */
+.ge-color-picker.cp-swatch-input {
+ width: 24px;
+ height: 24px;
+ flex: 0 0 24px;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ padding: 0;
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+ /* Hide the (read-only) text caret + selection so the element reads
+ as a swatch, not a text field. */
+ color: transparent;
+ text-shadow: none;
+ caret-color: transparent;
+ font-size: 0;
+ user-select: none;
+}
+.ge-color-picker.cp-swatch-input:focus {
+ outline: 1px solid var(--red);
+ outline-offset: 1px;
+}
+.ge-size-slider {
+ flex: 1 1 auto;
+ min-width: 0;
+ height: 8px;
+ accent-color: var(--red);
+ -webkit-appearance: none;
+ appearance: none;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+ margin: 0;
+}
+.ge-size-label { font-size: 10px; opacity: 0.5; min-width: 28px; text-align: right; }
+
+/* Eraser controls: tighter rows + a small brush preview on the left of
+ each label so the user can see what each slider affects. The three
+ previews share the same dot but each leans on a different rendering
+ trick (alpha / scatter / blur) to mirror its slider's effect. */
+.ge-eraser-row {
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px;
+ padding: 2px 0;
+ /* Lets the trailing value span pin to the row's right edge so it
+ stays visible when the slider expands left over the label. */
+ position: relative;
+}
+/* Inline value chip next to the label text. (Previously absolutely-
+ positioned over the slider track to ride the expand animation, which
+ caused overlap now that the dynamic-slider behavior is off for these.) */
+.ge-eraser-row label > span[id$="-label"] {
+ margin-left: 4px;
+ padding: 0 4px;
+ border-radius: 3px;
+ cursor: text;
+ font-size: 10px;
+ opacity: 0.7;
+ font-variant-numeric: tabular-nums;
+}
+/* When the value chip is pulled out of its label and placed AFTER the
+ slider (see DOM transform in galleryEditor.js init), it sits on the
+ row's right edge and naturally shortens the slider's flex space. */
+.ge-eraser-row > span[id$="-label"].ge-slider-value {
+ flex: 0 0 auto;
+ margin: 0 0 0 6px;
+ padding: 0 4px;
+ font-size: 10px;
+ opacity: 0.7;
+ font-variant-numeric: tabular-nums;
+ min-width: 34px;
+ text-align: right;
+ cursor: text;
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+/* Per-tool model selector row (lives at the top of each AI tool's
+ section in the side panel). Mirrors the inpaint Model row layout. */
+.ge-tool-model-row {
+ display: flex !important;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 6px;
+ min-width: 0;
+}
+.ge-tool-model-row > label {
+ font-size: 11px;
+ opacity: 0.6;
+ flex: 0 0 auto;
+}
+.ge-tool-model {
+ flex: 1 1 0;
+ min-width: 0;
+ font-size: 10px;
+ padding: 2px 4px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+}
+
+/* Section label inside a dropdown — small uppercase header. */
+.dropdown-section-label {
+ font-size: 9px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ opacity: 0.5;
+ font-weight: 600;
+ padding: 6px 8px 2px;
+}
+.dropdown-section-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 4px 0;
+ opacity: 0.6;
+}
+
+/* Inline numeric editor that replaces the value chip on click. */
+.ge-slider-edit {
+ position: absolute;
+ z-index: 11;
+ font: inherit;
+ font-size: 10px;
+ font-variant-numeric: tabular-nums;
+ padding: 1px 4px;
+ border: 1px solid var(--red);
+ border-radius: 3px;
+ background: var(--bg);
+ color: var(--fg);
+ outline: none;
+ text-align: center;
+}
+
+/* Merge dropdown — position is set by JS each time it opens (relative to
+ the button's bounding rect) because the parent .ge-layers has
+ overflow:hidden which would otherwise clip the popup. */
+.ge-merge-menu {
+ position: fixed;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 3px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
+ z-index: 50;
+ display: flex;
+ flex-direction: column;
+ min-width: 140px;
+}
+.ge-merge-menu .dropdown-item-compact {
+ background: none;
+ border: none;
+ width: 100%;
+ text-align: left;
+ padding: 5px 8px;
+ font-size: 11px;
+ gap: 8px;
+ font: inherit;
+ font-size: 11px;
+}
+
+/* Thin separator inside a tool section. */
+.ge-section-divider {
+ border: 0;
+ border-top: 1px solid var(--border);
+ margin: 10px -2px;
+ opacity: 0.6;
+}
+
+/* Small explanatory paragraph under AI tool section labels. */
+.ge-section-hint {
+ font-size: 10.5px;
+ line-height: 1.45;
+ opacity: 0.55;
+ margin: 0 0 8px;
+}
+/* Clone-tool hint swaps between "Alt-click" (desktop) and "Double-tap"
+ (mobile) so the on-screen instruction matches the actual gesture. */
+.ge-clone-hint-mobile { display: none; }
+@media (max-width: 820px) {
+ .ge-clone-hint-desktop { display: none; }
+ .ge-clone-hint-mobile { display: inline; }
+}
+/* Section title + adjacent "?" affordance — collapses the long
+ explanatory paragraph into a hoverable tooltip so the side panel
+ stays compact. */
+.ge-section-title-with-help {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+/* Drop the source-whitespace text node between the title text and the
+ ? span so the chip hugs the title with no extra gap. */
+.ge-section-title-with-help .ge-section-help { margin-left: 0; }
+.ge-section-help {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ color: var(--fg-muted);
+ font-size: 9px;
+ font-weight: 700;
+ cursor: help;
+ user-select: none;
+ transition: background 0.12s, color 0.12s;
+}
+.ge-section-help:hover,
+.ge-section-help:focus-visible {
+ background: color-mix(in srgb, var(--fg) 24%, transparent);
+ color: var(--fg);
+ outline: none;
+}
+
+/* Lightweight section title — used inside the inpaint panel to group
+ the mask actions (Eye / Invert / Clear) under a "Selection" header. */
+.ge-section-title {
+ margin: 10px 0 4px;
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ opacity: 0.5;
+ font-weight: 600;
+}
+/* Selection-row buttons stay compact so eye + invert + clear fit on
+ one line beside each other. */
+.ge-inpaint-mask-row {
+ display: flex !important;
+ gap: 4px;
+ align-items: center;
+}
+.ge-inpaint-mask-row .ge-btn {
+ flex: 0 0 auto;
+}
+.ge-inpaint-mask-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ opacity: 0.5;
+ font-weight: 600;
+ margin-right: 4px;
+}
+
+/* Fixed label column so the three sliders start at the same x and have
+ the same length when unused. Without this, "Flow" (4 chars) leaves
+ more room than "Opacity" (7 chars). */
+.ge-eraser-row label {
+ font-size: 10px;
+ opacity: 0.55;
+ flex: 0 0 78px;
+ white-space: nowrap;
+ display: inline-flex;
+ justify-content: space-between;
+ gap: 4px;
+}
+/* Fixed-size slider track for all side-panel sliders — no dynamic
+ width/height expansion on drag. Only the thumb grows on interaction
+ (see ::-webkit-slider-thumb / ::-moz-range-thumb below). */
+.ge-eraser-row input[type="range"] {
+ flex: 1 1 auto;
+ min-width: 0;
+ height: 8px;
+ accent-color: var(--red);
+ -webkit-appearance: none;
+ appearance: none;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+ margin: 0;
+ position: relative;
+ z-index: 2;
+}
+.ge-eraser-row input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
+}
+.ge-eraser-row input[type="range"]:hover::-webkit-slider-thumb,
+.ge-eraser-row input[type="range"].is-using::-webkit-slider-thumb,
+.ge-eraser-row input[type="range"]:active::-webkit-slider-thumb {
+ width: 16px; height: 16px;
+ transition: width 0.12s ease 0s, height 0.12s ease 0s;
+}
+.ge-eraser-row input[type="range"]::-moz-range-thumb {
+ width: 9px; height: 9px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
+}
+.ge-eraser-row input[type="range"]:hover::-moz-range-thumb,
+.ge-eraser-row input[type="range"].is-using::-moz-range-thumb,
+.ge-eraser-row input[type="range"]:active::-moz-range-thumb {
+ width: 16px; height: 16px;
+ transition: width 0.12s ease 0s, height 0.12s ease 0s;
+}
+
+/* Help "?" icon next to a section title. Subtle by default, hover
+ brightens. Tooltip via native `title` attribute. */
+.ge-section-help {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ margin-left: 4px;
+ font-size: 9px;
+ font-weight: 600;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ color: var(--fg-muted);
+ background: none;
+ cursor: help;
+ vertical-align: middle;
+ position: relative;
+ top: -1px;
+ opacity: 0.7;
+ transition: opacity 0.15s, color 0.15s, border-color 0.15s;
+}
+.ge-section-help:hover { opacity: 1; color: var(--fg); border-color: var(--fg-muted); }
+
+/* Layer-FX popup — floating window bound to one layer. */
+.ge-fx-popup {
+ position: fixed;
+ z-index: 300;
+ width: 320px;
+ max-height: 80vh;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 12px 32px rgba(0,0,0,0.45);
+ display: flex;
+ flex-direction: column;
+ font-size: 11px;
+ color: var(--fg);
+}
+.ge-fx-popup-head {
+ display: flex; align-items: center; justify-content: space-between;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border);
+ font-weight: 600;
+}
+.ge-fx-popup-title { font-size: 12px; }
+.ge-fx-popup-close {
+ background: none; border: none; color: var(--fg-muted);
+ font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px;
+}
+.ge-fx-popup-close:hover { color: var(--fg); }
+.ge-fx-popup-body { padding: 8px 10px; overflow-y: auto; }
+.ge-fx-popup-foot {
+ display: flex; gap: 6px; padding: 6px 10px;
+ border-top: 1px solid var(--border);
+}
+.ge-fx-group-title {
+ font-size: 10px; font-weight: 600; opacity: 0.6;
+ text-transform: uppercase; letter-spacing: 0.5px;
+ margin: 8px 0 4px;
+}
+.ge-fx-group-title:first-child { margin-top: 0; }
+.ge-fx-cb-tone {
+ font-size: 10px; opacity: 0.55; font-style: italic;
+ margin: 4px 0 2px;
+}
+.ge-fx-row {
+ display: flex; align-items: center; gap: 6px;
+ padding: 2px 0;
+}
+.ge-fx-row-label {
+ font-size: 10px; opacity: 0.7;
+ flex: 0 0 110px; white-space: nowrap;
+}
+.ge-fx-row input[type="range"] {
+ flex: 1 1 auto; min-width: 0;
+ height: 6px; accent-color: var(--red);
+ -webkit-appearance: none; appearance: none;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+}
+.ge-fx-row input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none; appearance: none;
+ width: 11px; height: 11px; border-radius: 50%;
+ background: var(--red); border: none; cursor: pointer;
+}
+.ge-fx-row input[type="range"]::-moz-range-thumb {
+ width: 11px; height: 11px; border-radius: 50%;
+ background: var(--red); border: none; cursor: pointer;
+}
+.ge-fx-row-value {
+ font-size: 10px; opacity: 0.7; font-variant-numeric: tabular-nums;
+ flex: 0 0 40px; text-align: right;
+}
+
+/* Layer-row FX button — tinted when the layer has any non-identity FX. */
+.ge-layer-fx-btn {
+ opacity: 0.55;
+}
+.ge-layer-fx-btn:hover { opacity: 0.9; }
+.ge-layer-fx-btn.active {
+ opacity: 1;
+ color: var(--accent, var(--red));
+}
+/* Add-mask button on each layer row. Subtle by default; lights up red
+ when a lasso/wand selection is live so the user instantly sees that
+ clicking it will bake that selection into a mask on this layer. */
+.ge-layer-mask-btn {
+ opacity: 0.45;
+}
+.ge-layer-mask-btn:hover { opacity: 0.9; }
+.ge-layer-mask-btn.from-selection {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+}
+
+/* Shared frosted-glass style for FX menu + adjustment popups. */
+.ge-frosted {
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+ backdrop-filter: blur(14px) saturate(140%);
+ -webkit-backdrop-filter: blur(14px) saturate(140%);
+ border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
+ box-shadow: 0 12px 40px rgba(0,0,0,0.55);
+}
+
+/* FX dropdown menu from the layer-row fx icon. */
+.ge-fx-menu {
+ position: fixed;
+ z-index: 305;
+ min-width: 200px;
+ padding: 4px;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.ge-fx-menu-item {
+ background: none;
+ border: none;
+ text-align: left;
+ padding: 7px 10px;
+ font-size: 12px;
+ color: var(--fg);
+ border-radius: 5px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.ge-fx-menu-icon {
+ display: inline-flex;
+ align-items: center;
+ color: var(--fg-muted);
+ flex-shrink: 0;
+}
+.ge-fx-menu-item:hover {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.ge-fx-menu-item:hover .ge-fx-menu-icon { color: var(--fg); }
+
+.ge-add-layer-menu {
+ position: fixed;
+ z-index: 10004;
+ width: 218px;
+ padding: 5px;
+ border-radius: 7px;
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+}
+.ge-add-layer-menu button {
+ height: 30px;
+ padding: 0 9px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ text-align: left;
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+}
+.ge-add-layer-menu button:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
+.ge-add-layer-symbol {
+ width: 16px;
+ display: inline-flex;
+ justify-content: center;
+ color: var(--accent, var(--red));
+ font-size: 13px;
+}
+.ge-add-layer-divider { height: 1px; margin: 4px 5px; background: color-mix(in srgb, var(--fg) 10%, transparent); }
+
+/* Per-type adjustment popup. */
+.ge-adj-popup {
+ position: fixed;
+ z-index: 305;
+ width: 320px;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ font-size: 11px;
+ color: var(--fg);
+}
+.ge-adj-popup .ge-adj-head {
+ display: flex; align-items: center; gap: 6px;
+ padding: 8px 12px;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+ cursor: grab;
+ user-select: none;
+}
+.ge-adj-popup .ge-head-btns { margin-left: auto; }
+.ge-adj-icon {
+ display: inline-flex;
+ align-items: center;
+ color: var(--fg-muted);
+ flex-shrink: 0;
+}
+.ge-adj-title { font-weight: 600; font-size: 12px; flex: 1 1 auto; }
+.ge-adj-min, .ge-adj-close, .ge-history-close {
+ background: none; border: none; color: var(--fg-muted);
+ font-size: 16px; line-height: 1; cursor: pointer; padding: 0 4px;
+ flex-shrink: 0;
+}
+.ge-history-close:hover { color: var(--fg); }
+/* Mobile-only: shift head icons/buttons up and enlarge the minimise
+ glyph so it reads as a window-minimise affordance, not a centered
+ minus. Desktop keeps the original tight head styling. */
+@media (max-width: 820px) {
+ .ge-adj-icon {
+ position: relative;
+ top: -4px;
+ }
+ .ge-adj-title {
+ position: relative;
+ top: -2px;
+ }
+ .ge-adj-min, .ge-adj-close, .ge-history-close {
+ position: relative;
+ top: -4px;
+ }
+ .ge-adj-min,
+ .ge-adj-popup .ge-adj-min,
+ .ge-history-head .ge-adj-min,
+ .ge-transform-popup-head .ge-adj-min {
+ font-size: 22px !important;
+ font-weight: 600 !important;
+ padding: 0 8px !important;
+ line-height: 0.6 !important;
+ position: relative;
+ top: 4px;
+ }
+ .ge-adj-head .ge-transform-aspect-btn {
+ position: relative;
+ top: -4px;
+ }
+}
+.ge-adj-min:hover, .ge-adj-close:hover { color: var(--fg); }
+
+/* Floating dock at bottom-right for minimised FX popups. */
+#ge-fx-dock {
+ position: fixed;
+ left: 50%;
+ transform: translateX(-50%);
+ bottom: 12px;
+ z-index: 304;
+ display: flex;
+ flex-direction: row;
+ gap: 6px;
+ align-items: center;
+ pointer-events: none;
+ max-width: calc(100vw - 24px);
+ flex-wrap: wrap;
+ justify-content: center;
+}
+.ge-fx-dock-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 8px;
+ border-radius: 6px;
+ font-size: 11px;
+ color: var(--fg);
+ cursor: pointer;
+ pointer-events: auto;
+ max-width: 220px;
+ white-space: nowrap;
+ overflow: hidden;
+}
+.ge-fx-dock-chip:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.ge-fx-dock-icon { display: inline-flex; align-items: center; }
+.ge-fx-dock-close {
+ margin-left: 2px; opacity: 0.55; padding: 0 2px;
+ font-size: 14px; line-height: 1;
+}
+.ge-fx-dock-close:hover { opacity: 1; color: var(--red); }
+
+/* Sub-layer row icon in the layer panel. */
+.ge-adj-sub-name {
+ display: inline-flex !important;
+ align-items: center;
+ gap: 4px;
+}
+.ge-adj-sub-icon {
+ display: inline-flex; align-items: center;
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.ge-adj-sub-icon svg { width: 11px; height: 11px; }
+
+/* History panel — labeled timeline of edits. */
+#ge-history-panel {
+ position: fixed;
+ z-index: 10004;
+ width: 240px;
+ max-height: 60vh;
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ color: var(--fg);
+ font-size: 11px;
+}
+.ge-history-head {
+ display: flex; align-items: center; gap: 6px;
+ padding: 8px 10px;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+ cursor: grab;
+ user-select: none;
+}
+.ge-history-title { font-weight: 600; font-size: 12px; flex: 1 1 auto; }
+.ge-history-close {
+ background: none; border: none; color: var(--fg-muted);
+ font-size: 16px; line-height: 1; cursor: pointer; padding: 0 4px;
+ flex-shrink: 0;
+}
+.ge-history-close:hover { color: var(--fg); }
+.ge-history-list { overflow-y: auto; padding: 4px 0; }
+.ge-history-row {
+ display: flex; align-items: center; gap: 8px;
+ width: 100%;
+ padding: 5px 10px;
+ background: none; border: none;
+ text-align: left; cursor: pointer;
+ color: var(--fg); font-size: 11px;
+}
+.ge-history-row:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
+.ge-history-row.current { background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
+.ge-history-row.current .ge-history-row-dot { background: var(--accent, var(--red)); }
+.ge-history-row.future { opacity: 0.45; }
+.ge-history-row-dot {
+ width: 6px; height: 6px; border-radius: 50%;
+ background: var(--fg-muted);
+ flex-shrink: 0;
+}
+.ge-history-row-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ge-history-row-time { font-size: 9px; opacity: 0.55; flex-shrink: 0; }
+
+/* Tight button group in popup heads — min + close sit immediately next
+ to each other with no inherited flex gap, pushed to the right edge. */
+.ge-head-btns {
+ display: inline-flex;
+ align-items: center;
+ gap: 0;
+ margin-left: auto;
+ flex-shrink: 0;
+}
+.ge-head-btns > button {
+ width: 26px;
+ height: 26px;
+ padding: 0 !important;
+ margin: 0;
+ font-size: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+}
+.ge-head-btns > button:hover {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+
+/* Detail-header heart toggle — sits next to the icon Edit button. */
+.gallery-detail-fav-header { opacity: 0.7; transition: opacity 0.15s, color 0.15s; }
+.gallery-detail-fav-header:hover { opacity: 1; }
+.gallery-detail-fav-header.active { color: var(--red); opacity: 1; }
+
+/* Canvas loading overlay — covers the canvas area while a blocking op
+ (rotation, big resize, etc.) runs. */
+.ge-canvas-loading {
+ position: absolute;
+ inset: 0;
+ z-index: 50;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ border-radius: 8px;
+ pointer-events: auto;
+}
+.ge-canvas-loading-spinner {
+ width: 32px; height: 32px;
+ border: 3px solid color-mix(in srgb, var(--fg) 25%, transparent);
+ border-top-color: var(--accent, var(--red));
+ border-radius: 50%;
+ animation: ge-canvas-spin 0.8s linear infinite;
+}
+.ge-canvas-loading-msg { font-size: 12px; opacity: 0.85; }
+
+/* Missing-dependency notice in tool sections (Bg Remove etc.). */
+.ge-dep-notice {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
+ border-radius: 6px;
+ margin: 4px 0 8px;
+ font-size: 11px;
+}
+.ge-dep-notice-text { flex: 1 1 auto; line-height: 1.4; }
+.ge-dep-notice-text strong { display: block; margin-bottom: 2px; }
+.ge-dep-notice-text code {
+ font-family: 'Fira Code', monospace;
+ font-size: 10px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.ge-dep-notice .ge-btn { flex-shrink: 0; }
+
+/* Brief highlight on a Cookbook deps row arrived-at via deep-link. */
+.cookbook-pkg-flash { animation: cookbook-pkg-flash 1.4s ease-out; }
+@keyframes cookbook-pkg-flash {
+ 0% { background: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent); }
+ 100% { background: transparent; }
+}
+
+/* Flash highlight when an anchor-link ([Name](#task-/#skill-/#research-))
+ opens a panel and focuses a specific item. */
+@keyframes anchor-item-flash {
+ 0% { background: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent); }
+ 60% { background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
+ 100% { background: transparent; }
+}
+.task-card-flash,
+.skill-row-flash,
+.research-card-flash {
+ animation: anchor-item-flash 2s ease-out;
+ border-radius: 6px;
+}
+
+.memory-item-focus {
+ animation: anchor-item-flash 2.2s ease-out;
+ outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+ outline-offset: 2px;
+ border-radius: 6px;
+}
+
+/* Preview dot icons for the Blur sub-menu — visual cue for each blur
+ type (Gaussian = soft round, Zoom = radial streaks). */
+.ge-blur-icon {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ display: inline-block;
+ flex-shrink: 0;
+ background: var(--fg);
+}
+.ge-blur-icon.ge-blur-gaussian {
+ background: radial-gradient(circle, var(--fg) 0%, var(--fg) 25%, transparent 75%);
+ filter: blur(1.5px);
+}
+.ge-blur-icon.ge-blur-zoom {
+ background: radial-gradient(circle, var(--fg) 0%, var(--fg) 20%, transparent 60%);
+ box-shadow:
+ 0 -5px 0 -3px var(--fg),
+ 0 5px 0 -3px var(--fg),
+ -5px 0 0 -3px var(--fg),
+ 5px 0 0 -3px var(--fg),
+ -4px -4px 0 -3px var(--fg),
+ 4px -4px 0 -3px var(--fg),
+ -4px 4px 0 -3px var(--fg),
+ 4px 4px 0 -3px var(--fg);
+}
+
+/* Inline-swatch color picker — small square that fits next to a title. */
+.ge-inline-swatch {
+ width: 16px !important;
+ height: 16px !important;
+ padding: 0 !important;
+ border: 1px solid var(--border) !important;
+ border-radius: 3px !important;
+ cursor: pointer;
+ flex-shrink: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ background: none;
+}
+
+/* Drop overlay over the gallery editor while a file drag is in flight. */
+.ge-drop-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 200;
+ pointer-events: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border: 3px dashed color-mix(in srgb, var(--accent, var(--red)) 80%, transparent);
+ border-radius: 8px;
+}
+.ge-drop-overlay-msg {
+ padding: 12px 20px;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--fg);
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+ backdrop-filter: blur(10px);
+ border-radius: 8px;
+}
+
+/* Small buttons that show an SVG icon + text label side-by-side. */
+.ge-btn-iconlabel {
+ display: inline-flex !important;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+}
+.ge-btn-iconlabel svg {
+ flex-shrink: 0;
+ opacity: 0.85;
+}
+.ge-btn-iconlabel:hover svg { opacity: 1; }
+
+.ge-adj-body {
+ padding: 8px 12px;
+ max-height: 60vh;
+ overflow-y: auto;
+ overflow-x: hidden;
+}
+.ge-adj-preset-field {
+ display: grid;
+ grid-template-columns: 46px 1fr;
+ align-items: center;
+ gap: 8px;
+ margin: 8px 12px 0;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ font-size: 10px;
+}
+.ge-adj-preset-select {
+ min-width: 0;
+ width: 100%;
+ padding: 5px 7px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--panel) 90%, transparent);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-adj-preset-select:focus { outline: none; border-color: var(--accent, var(--red)); }
+.ge-adj-popup { box-sizing: border-box; overflow: hidden; }
+.ge-adj-foot {
+ display: flex; gap: 6px; justify-content: flex-end;
+ padding: 8px 12px;
+ border-top: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.ge-adj-foot-spacer { flex: 1 1 auto; }
+.ge-adj-compare-btn.is-active {
+ color: var(--accent, var(--red));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 65%, transparent);
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+.ge-adj-channel-field {
+ display: grid;
+ grid-template-columns: 64px 1fr;
+ align-items: center;
+ gap: 8px;
+ margin: 0 0 8px;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ font-size: 10px;
+}
+.ge-adj-channel-select,
+.ge-adj-selective-range {
+ width: 100%;
+ min-width: 0;
+ height: 27px;
+ padding: 0 25px 0 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--panel);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-gradient-map-preview {
+ height: 24px;
+ margin-bottom: 9px;
+ border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ border-radius: 4px;
+ background: linear-gradient(90deg, var(--ge-gradient-start), var(--ge-gradient-end));
+}
+.ge-gradient-color-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+.ge-gradient-color-row label {
+ display: grid;
+ grid-template-columns: 1fr 30px;
+ align-items: center;
+ gap: 6px;
+ font-size: 10px;
+ opacity: .75;
+}
+.ge-gradient-color-row input[type="color"] {
+ width: 30px;
+ height: 24px;
+ padding: 1px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: transparent;
+}
+.ge-adj-toggle-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 28px;
+ font-size: 10px;
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+}
+.ge-adj-toggle-row input { accent-color: var(--red); }
+.ge-curves-wrap {
+ width: 100%;
+ aspect-ratio: 14 / 9;
+ border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
+ background: #0b0d10;
+ overflow: hidden;
+}
+.ge-curves-canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
+.ge-adj-help { margin: 7px 0 0; font-size: 9px; line-height: 1.35; opacity: .5; }
+.ge-adjustment-layer-item .ge-layer-adjustment-marker { color: var(--accent, var(--red)); }
+.ge-adjustment-layer-item .ge-layer-adjustment-marker svg { width: 12px; height: 12px; }
+/* Mobile-only: oversize the foot Cancel/Apply buttons to match the
+ Transform popup. Desktop keeps the original ge-btn-sm sizing. */
+@media (max-width: 820px) {
+ .ge-adj-foot .ge-adj-cancel-btn,
+ .ge-adj-foot .ge-adj-apply-btn {
+ padding: 8px 16px !important;
+ font-size: 13px !important;
+ min-width: 80px;
+ text-align: center;
+ }
+}
+.ge-adj-row {
+ display: flex; align-items: center; gap: 3px;
+ padding: 0;
+ margin: 0;
+ min-width: 0;
+ line-height: 1;
+}
+.ge-adj-row > label {
+ flex: 0 0 96px;
+ font-size: 10px;
+ opacity: 0.7;
+}
+.ge-adj-row input[type="range"] {
+ flex: 1 1 auto;
+ min-width: 0;
+ height: 6px;
+ accent-color: var(--red);
+ -webkit-appearance: none; appearance: none;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+}
+/* Mobile-only: fixed-size sliders + taller rows so the thumb growing
+ on active doesn't push the row height around. */
+@media (max-width: 820px) {
+ .ge-adj-row {
+ min-height: 28px;
+ }
+ /* Let the slider consume the space left after the label and value chip.
+ A fixed 160px track overflows the 320px phone sheet once the popup's
+ horizontal padding and controls are included. */
+ .ge-adj-row > label {
+ flex: 0 0 clamp(72px, 26vw, 96px);
+ min-width: 0;
+ }
+ .ge-adj-row input[type="range"] {
+ flex: 1 1 auto;
+ width: auto;
+ min-width: 56px;
+ max-width: none;
+ }
+}
+@media (max-width: 360px) {
+ /* Give Gradient Map's color labels and swatches a usable tap width on
+ narrow phones; the popup remains scrollable if the extra row is needed. */
+ .ge-gradient-color-row {
+ grid-template-columns: 1fr;
+ gap: 6px;
+ }
+}
+.ge-adj-row input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none; appearance: none;
+ width: 12px; height: 12px;
+ border-radius: 50%;
+ background: var(--red);
+ cursor: pointer; border: none;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.ge-adj-row input[type="range"]:hover::-webkit-slider-thumb,
+.ge-adj-row input[type="range"]:active::-webkit-slider-thumb {
+ width: 18px; height: 18px;
+}
+.ge-adj-row input[type="range"]::-moz-range-thumb {
+ width: 12px; height: 12px;
+ border-radius: 50%; background: var(--red); border: none; cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.ge-adj-row input[type="range"]:hover::-moz-range-thumb,
+.ge-adj-row input[type="range"]:active::-moz-range-thumb {
+ width: 18px; height: 18px;
+}
+/* Value chip — fixed-width column, text aligned LEFT so the chip's
+ left edge always sits immediately after the slider regardless of
+ value length. Without a fixed width Hue's "180 °" made its slider
+ shorter than Saturation's "100" since slider was flex-1. */
+.ge-adj-value {
+ flex: 0 0 34px;
+ margin-left: 0;
+ padding: 0 2px;
+ font-size: 10px; opacity: 0.7; text-align: left;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ cursor: text;
+}
+.ge-adj-value:hover { opacity: 1; }
+.ge-adj-cb-tone {
+ font-size: 10px; opacity: 0.55; font-style: italic;
+ margin: 2px 0 0;
+ line-height: 1;
+}
+.ge-adj-cb-row {
+ padding: 0;
+ gap: 3px;
+}
+.ge-adj-cb-tone-select {
+ width: 100%;
+ padding: 6px 8px;
+ font-size: 12px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: color-mix(in srgb, var(--panel) 90%, transparent);
+ color: var(--fg);
+}
+.ge-adj-cb-tone-select:focus { outline: none; border-color: var(--red); }
+.ge-adj-hist-details[open] > summary::before {
+ content: '▾ ';
+}
+.ge-adj-hist-details .ge-adj-hist-wrap {
+ margin-top: 4px;
+}
+.ge-adj-histogram {
+ display: block;
+ width: 100%;
+ height: 80px;
+ border-radius: 4px;
+ background: rgba(0,0,0,0.25);
+}
+.ge-adj-hist-wrap {
+ position: relative;
+ margin-bottom: 14px;
+}
+.ge-adj-clip-status {
+ margin-top: 3px;
+ color: var(--fg-muted);
+ font-size: 10px;
+ line-height: 1.25;
+ font-variant-numeric: tabular-nums;
+}
+.ge-adj-hist-handles {
+ position: absolute;
+ left: 0; right: 0;
+ bottom: -10px;
+ height: 12px;
+ pointer-events: none;
+}
+.ge-adj-hist-handle {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ cursor: ew-resize;
+ pointer-events: auto;
+ border-style: solid;
+ border-width: 0 6px 10px 6px;
+ border-color: transparent transparent var(--fg-muted) transparent;
+ filter: drop-shadow(0 1px 1px rgba(0,0,0,0.4));
+}
+.ge-adj-hist-handle:hover { border-bottom-color: var(--fg); }
+.ge-adj-hist-handle.hist-h-black { border-bottom-color: #111; }
+.ge-adj-hist-handle.hist-h-white { border-bottom-color: #fff; }
+.ge-adj-hist-handle.hist-h-gamma { border-bottom-color: var(--accent, var(--red)); }
+/* Per-slider revert button. */
+.ge-adj-revert {
+ background: none; border: none;
+ color: var(--fg-muted); cursor: pointer;
+ padding: 2px 4px; margin-left: 2px;
+ opacity: 0.55;
+ display: inline-flex; align-items: center; justify-content: center;
+ transition: opacity 0.15s, color 0.15s;
+}
+.ge-adj-revert:hover { opacity: 1; color: var(--fg); }
+
+/* Adjustment sub-layer row in the layer panel — indented under parent. */
+.ge-adj-sub-item {
+ padding-left: 18px;
+ opacity: 0.85;
+ border-left: 2px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+ margin-left: 6px;
+ font-size: 10.5px;
+}
+.ge-adj-sub-item .ge-layer-name {
+ opacity: 0.85;
+ font-style: italic;
+}
+/* Mask sub-layer rows. Same indent as adj sub-layers but the
+ highlight bar uses fg-muted instead of accent — these aren't FX
+ effects, just selection-state. When activated (the click target
+ for paint / inpaint / generate), border + name go full strength. */
+.ge-mask-sub-item {
+ border-left-color: color-mix(in srgb, var(--fg) 30%, transparent);
+}
+.ge-mask-sub-item.active {
+ border-left-color: var(--red);
+ opacity: 1;
+}
+.ge-clone-source-status {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 24px;
+ margin: 2px 0 5px;
+ padding: 0 6px;
+ color: var(--fg);
+ opacity: 0.68;
+ font-size: 10px;
+}
+.ge-clone-source-status span { flex: 1; min-width: 0; }
+.ge-clone-source-status .ge-icon-btn { margin-left: auto; }
+.ge-mask-inline-thumb {
+ width: 34px;
+ height: 26px;
+ flex: 0 0 34px;
+ opacity: 0.78;
+}
+.ge-eraser-preview {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: var(--fg);
+ flex-shrink: 0;
+ display: inline-block;
+ position: relative;
+}
+.ge-actions {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+}
+.ge-btn {
+ padding: 4px 8px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font-size: 11px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+.ge-btn:hover { background: color-mix(in srgb, var(--fg) 10%, var(--bg)); }
+.ge-btn.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, var(--bg));
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+}
+.ge-btn-sm { padding: 3px 6px; font-size: 10px; }
+/* Busy state on AI tool buttons — whirlpool + verb text side by side. */
+.ge-btn-processing {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+}
+.ge-btn-busy-label { font-size: inherit; line-height: 1; }
+/* Mask-vis eye button is icon-only — strip the inherited .ge-btn
+ background / border so it reads as a plain affordance, matching
+ the layer-row eye toggles. */
+.ge-mask-vis-btn {
+ background: none !important;
+ border: none !important;
+ padding: 2px 4px !important;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.5;
+ transition: opacity 0.12s;
+}
+.ge-mask-vis-btn:hover { opacity: 0.85; background: none !important; }
+.ge-mask-vis-btn.visible { opacity: 0.9; }
+/* Paint/Erase segmented toggle in the Inpaint Brush section. */
+.ge-inpaint-mode-btn {
+ font-size: 11px;
+ padding: 4px 8px;
+ opacity: 0.6;
+ border: 1px solid var(--border);
+}
+.ge-inpaint-mode-btn:hover { opacity: 0.85; }
+.ge-inpaint-mode-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+/* Wand New / + Add / − Subtract segmented toggle. */
+.ge-wand-mode-btn {
+ flex: 1 1 0;
+ font-size: 11px;
+ padding: 4px 6px;
+ opacity: 0.6;
+ border: 1px solid var(--border);
+}
+.ge-wand-mode-btn:hover { opacity: 0.85; }
+.ge-wand-mode-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+.ge-wand-live-btn {
+ flex: 0 0 auto;
+ opacity: 0.6;
+ min-width: 46px;
+ height: 22px;
+ padding: 0 10px !important;
+ border-radius: 999px !important;
+ font-size: 10px;
+ line-height: 1;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s;
+}
+.ge-wand-live-btn:hover { opacity: 0.85; }
+.ge-wand-live-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+/* Top-bar buttons (undo/redo, zoom +/-, Fit, 1:1, Resize, Edge, Import,
+ Download, Save) — visually their glyphs sit too low compared to the
+ surrounding labels, so shift contents up 2 px via asymmetric padding. */
+/* Visually the buttons sit lower than the surrounding text labels on the
+ topbar. Keep the box padding symmetric but lift the whole box 2 px so it
+ centers against the adjacent label baselines. */
+.ge-topbar .ge-btn,
+.ge-topbar .ge-btn-sm,
+.ge-topbar select,
+.ge-topbar input { position: relative; top: -2px; }
+/* The tiny "Gen" / "Inpaint" inline labels now look too high relative to
+ the shifted-up controls — nudge them down 2 px to re-align. */
+.ge-topbar span[style*="font-size:9px"] { position: relative; top: 2px; }
+/* The Gen and Inpaint model selects — must visually sit on the same line as
+ the small "Gen" / "Inpaint" labels (which are at top:2px). Use !important
+ so this beats the generic `.ge-topbar select` rule above no matter what
+ element-vs-class specificity quirks. */
+.ge-topbar .ge-ai-model { position: relative !important; top: 1px !important; }
+/* The ◢ glyph baseline reads lower than the surrounding text — shift it
+ up 1 px so it visually centers with the "Edge" label. */
+.ge-edge-glyph { position: relative; top: -1px; }
+/* Flex-line-break helper used in the mobile transform popup. Hidden by
+ default so desktop keeps its single-row layout. */
+.ge-row-break { display: none; }
+/* Stacked button — glyph on top, small uppercase label below. Currently
+ used for Undo/Redo so users get an obvious "UNDO" / "REDO" affordance
+ label without losing the compact icon. */
+.ge-stacked-btn {
+ display: inline-flex !important;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1px;
+ padding: 2px 8px !important;
+ line-height: 1;
+}
+.ge-stacked-btn .ge-stacked-glyph svg { display: block; }
+.ge-stacked-btn .ge-stacked-caret {
+ margin-left: 2px;
+ font-size: 7px;
+ line-height: 1;
+ opacity: 0.55;
+}
+.ge-stacked-btn .ge-stacked-label {
+ font-size: 8px;
+ letter-spacing: 0.06em;
+ opacity: 0.65;
+ font-weight: 600;
+ position: relative;
+ top: 2px;
+}
+/* Zoom % indicator — magnifier glyph on top, percentage label below.
+ Matches the Undo/Redo stacked-button layout so the topbar reads as
+ one consistent row of stacked icons. */
+.ge-zoom-stack {
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1px;
+ line-height: 1;
+ padding: 0 4px;
+}
+.ge-zoom-stack .ge-zoom-glyph {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ /* Glyph nudged 1 px right (was 2 px left). */
+ position: relative;
+ left: -1px;
+}
+.ge-zoom-stack .ge-zoom-glyph svg {
+ width: 16px;
+ height: 16px;
+}
+.ge-zoom-stack .ge-zoom-label {
+ font-size: 8px;
+ letter-spacing: 0.06em;
+ opacity: 0.65;
+ font-weight: 600;
+ position: relative;
+ top: 4px;
+ left: 1px;
+}
+/* + Add sits 1 px below the title baseline to match the surrounding
+ header buttons after the recent header restructure. */
+.ge-layers-header #ge-add-layer { position: relative; top: 1px; }
+.ge-layer-blend-row {
+ display: grid;
+ grid-template-columns: 38px minmax(0, 1fr);
+ align-items: center;
+ gap: 6px;
+ padding: 4px 7px;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
+ font-size: 10px;
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+}
+.ge-layer-blend-row select {
+ width: 100%;
+ min-width: 0;
+ height: 25px;
+ padding: 0 24px 0 7px;
+ border-radius: 4px;
+ font-size: 10px;
+}
+.ge-layer-selection-bar {
+ min-height: 30px;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 3px 10px;
+ border-bottom: 1px solid var(--border);
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--bg-secondary));
+ color: var(--fg);
+ font-size: 10px;
+}
+.ge-layer-selection-bar > span { flex: 1; opacity: 0.72; }
+.ge-layer-selection-bar .ge-icon-btn {
+ width: 24px;
+ height: 24px;
+ min-width: 24px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ opacity: 0.72;
+}
+.ge-layer-selection-bar .ge-icon-btn.danger:hover { color: var(--red); }
+/* Layer-row buttons in the right panel — the unicode glyphs (◉, ⤢) have
+ irregular baseline metrics so the box reads as too low; shift up 2 px. */
+.ge-layer-vis,
+.ge-layer-btn { position: relative; top: -2px; }
+/* Delete (×) button glyph baseline reads even lower than the visibility
+ icon — push it up an extra 2 px so it visually centers. */
+.ge-layer-btn.danger { top: -4px; }
+/* Save / Save as new / Download dropdown anchored to the primary Save
+ button in the topbar. */
+.ge-save-wrap {
+ position: relative;
+ display: inline-block;
+ flex-shrink: 0;
+}
+.ge-save-menu {
+ position: fixed;
+ min-width: 160px;
+ padding: 4px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+ z-index: 10005;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.ge-save-state-icon { display: none; }
+.ge-save-label { white-space: nowrap; min-width: 0; }
+.ge-save-state-saved { color: var(--fg); }
+.ge-save-menu .dropdown-item-compact {
+ width: 100%;
+ background: none;
+ border: none;
+ text-align: left;
+ font: inherit;
+}
+
+/* Resize and Edge popups in the topbar — same anchored-dropdown pattern as
+ the Save menu. Resize lists preset canvas sizes; Edge is a tiny form.
+ Image + Filter menus reuse the same chrome. */
+.ge-resize-wrap,
+.ge-edge-wrap,
+.ge-view-wrap,
+.ge-image-wrap,
+.ge-selection-wrap,
+.ge-filter-wrap { position: relative; display: inline-block; }
+.ge-resize-menu,
+.ge-edge-menu,
+.ge-view-menu,
+.ge-image-menu,
+.ge-selection-menu,
+.ge-filter-menu {
+ position: absolute;
+ top: calc(100% + 2px);
+ right: 0;
+ padding: 4px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+ z-index: 12;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+/* Mobile: escape the topbar's stacking context (z 5) so the dropdown
+ lands above the bottom-sheet panels (z 50 / 60). Switch to fixed and
+ pin under the topbar. */
+@media (max-width: 820px) {
+ .ge-resize-menu,
+ .ge-edge-menu,
+ .ge-view-menu,
+ .ge-image-menu,
+ .ge-selection-menu,
+ .ge-filter-menu {
+ position: fixed;
+ top: 44px;
+ right: 8px;
+ z-index: 500;
+ max-width: calc(100vw - 16px);
+ }
+}
+.ge-resize-menu { min-width: 200px; }
+.ge-edge-menu { min-width: 220px; padding: 10px; gap: 6px; }
+.ge-view-menu { min-width: 174px; }
+.ge-image-menu { min-width: 180px; }
+.ge-selection-menu { min-width: 224px; }
+.ge-filter-menu { min-width: 180px; }
+.ge-resize-menu .dropdown-item-compact,
+.ge-view-menu .dropdown-item-compact,
+.ge-image-menu .dropdown-item-compact,
+.ge-selection-menu .dropdown-item-compact,
+.ge-filter-menu .dropdown-item-compact {
+ width: 100%;
+ background: none;
+ border: none;
+ text-align: left;
+ font: inherit;
+}
+.ge-view-toggle .ge-view-check { opacity: 0; }
+.ge-view-toggle.active .ge-view-check { opacity: 1; color: var(--red); }
+.ge-view-grid-size {
+ display: grid;
+ grid-template-columns: 1fr 52px auto;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 8px;
+ font-size: 10px;
+ color: var(--color-muted);
+}
+.ge-view-grid-size input {
+ width: 52px;
+ height: 24px;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+ text-align: right;
+}
+/* Sub-menu label inside the Filter / Image dropdown — small uppercase
+ header so "Blur" / "Selection" reads as a category, not an action. */
+.ge-filter-submenu-label {
+ font-size: 9px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ opacity: 0.45;
+ font-weight: 600;
+ padding: 4px 8px 2px;
+ pointer-events: none;
+}
+/* Disabled dropdown items (e.g. Fill when there's no selection) read
+ as muted + non-interactive. */
+.ge-image-menu .dropdown-item-compact:disabled,
+.ge-selection-menu .dropdown-item-compact:disabled,
+.ge-filter-menu .dropdown-item-compact:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+.ge-selection-save-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 28px;
+ gap: 5px;
+ padding: 3px 4px;
+}
+.ge-selection-save-row input {
+ min-width: 0;
+ height: 27px;
+ padding: 3px 7px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-saved-selection-list { display: flex; flex-direction: column; gap: 1px; }
+.ge-saved-selection-row { display: grid; grid-template-columns: minmax(0, 1fr) 26px; align-items: center; }
+.ge-saved-selection-load,
+.ge-saved-selection-delete {
+ min-width: 0;
+ height: 26px;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+.ge-saved-selection-load { padding: 3px 8px; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
+.ge-saved-selection-delete { opacity: 0.45; font-size: 15px; }
+.ge-saved-selection-load:hover,
+.ge-saved-selection-delete:hover { background: var(--hover); opacity: 1; }
+.ge-saved-selection-empty { padding: 4px 8px 6px; font-size: 10px; opacity: 0.45; }
+/* Filter live-preview modal — translucent backdrop over the editor
+ with a frosted parameter panel anchored top-right. The active layer
+ re-renders live as the user drags the sliders. */
+.ge-filter-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 14;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-end;
+ padding: 60px 20px 20px;
+ background: rgba(0, 0, 0, 0.18);
+ pointer-events: auto;
+}
+.ge-filter-modal {
+ width: 240px;
+ background: color-mix(in srgb, var(--panel) 95%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(10px);
+ padding: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.ge-filter-modal-head {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ opacity: 0.7;
+}
+.ge-filter-row {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.ge-filter-row label {
+ font-size: 10px;
+ opacity: 0.7;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.ge-filter-row-value {
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+ opacity: 0.85;
+}
+.ge-filter-row input[type="range"] {
+ width: 100%;
+}
+.ge-filter-row input[type="color"] {
+ width: 100%;
+ height: 24px;
+ padding: 1px;
+ border: 1px solid var(--border, #333);
+ border-radius: 4px;
+ background: transparent;
+ cursor: pointer;
+}
+.ge-filter-modal-actions {
+ display: flex;
+ gap: 6px;
+ justify-content: flex-end;
+ margin-top: 4px;
+}
+
+/* Controlled image export. */
+.ge-export-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10020;
+ display: grid;
+ place-items: center;
+ padding: 16px;
+ background: rgba(0, 0, 0, 0.56);
+}
+.ge-export-dialog {
+ width: min(720px, calc(100vw - 32px));
+ max-height: calc(100vh - 32px);
+ overflow: auto;
+ background: var(--panel);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 18px 54px rgba(0, 0, 0, 0.48);
+}
+.ge-export-head,
+.ge-export-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 14px;
+}
+.ge-export-head { border-bottom: 1px solid var(--border); }
+.ge-export-head h2 {
+ margin: 0;
+ font-size: 13px;
+ letter-spacing: 0;
+}
+.ge-export-close {
+ border: 0;
+ background: transparent;
+ color: var(--fg);
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ font-size: 20px;
+ line-height: 1;
+ cursor: pointer;
+ opacity: 0.6;
+}
+.ge-export-close:hover { opacity: 1; }
+.ge-export-body {
+ display: grid;
+ grid-template-columns: minmax(260px, 1.25fr) minmax(220px, 0.75fr);
+ gap: 14px;
+ padding: 14px;
+}
+.ge-export-preview-wrap {
+ min-height: 300px;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background-color: #22262d;
+ background-image:
+ linear-gradient(45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
+ linear-gradient(-45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
+ linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.06) 75%),
+ linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.06) 75%);
+ background-size: 20px 20px;
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0;
+}
+.ge-export-preview {
+ display: block;
+ max-width: calc(100% - 24px);
+ max-height: 420px;
+ box-shadow: 0 4px 18px rgba(0,0,0,0.38);
+}
+.ge-export-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.ge-export-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+ font-size: 10px;
+ opacity: 0.78;
+}
+.ge-export-field > span {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.ge-export-field input[type="text"],
+.ge-export-field input[type="number"] {
+ width: 100%;
+ box-sizing: border-box;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 7px 8px;
+ font: inherit;
+ font-size: 11px;
+}
+.ge-export-field input:focus { outline: none; border-color: var(--red); }
+.ge-export-format {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ overflow: hidden;
+}
+.ge-export-format button {
+ min-height: 30px;
+ border: 0;
+ border-right: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+ font-size: 10px;
+ cursor: pointer;
+}
+.ge-export-format button:last-child { border-right: 0; }
+.ge-export-format button.active {
+ background: color-mix(in srgb, var(--red) 18%, var(--bg));
+ color: var(--red);
+}
+.ge-export-dimensions {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr);
+ gap: 5px;
+ align-items: end;
+}
+.ge-export-link {
+ width: 28px;
+ height: 29px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--bg);
+ color: var(--fg);
+ opacity: 0.45;
+ cursor: pointer;
+}
+.ge-export-link.active { opacity: 1; color: var(--red); border-color: var(--red); }
+.ge-export-quality input { width: 100%; accent-color: var(--red); }
+.ge-export-surface-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ border-top: 1px solid var(--border);
+ padding-top: 10px;
+}
+.ge-export-toggle,
+.ge-export-matte {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 10px;
+}
+.ge-export-matte.disabled { opacity: 0.35; pointer-events: none; }
+.ge-export-footer {
+ border-top: 1px solid var(--border);
+ font-size: 10px;
+}
+@media (max-width: 700px) {
+ .ge-export-overlay { padding: 8px; place-items: end center; }
+ .ge-export-dialog { width: 100%; max-height: calc(100dvh - 16px); }
+ .ge-export-body { grid-template-columns: 1fr; padding: 10px; }
+ .ge-export-preview-wrap { min-height: 190px; max-height: 260px; }
+ .ge-export-preview { max-height: 230px; }
+}
+.ge-menu-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 4px 0;
+}
+.ge-edge-form {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.ge-edge-label {
+ font-size: 11px;
+ opacity: 0.65;
+}
+.ge-edge-input {
+ padding: 6px 8px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 12px;
+}
+.ge-edge-input:focus { outline: none; border-color: var(--red); }
+.ge-edge-actions {
+ display: flex;
+ gap: 6px;
+}
+.ge-edge-actions .ge-btn { flex: 1; }
+.ge-edge-hint {
+ margin: 0;
+ font-size: 10px;
+ opacity: 0.5;
+ line-height: 1.4;
+}
+
+/* Styled "New canvas" prompt — replaces the native browser prompt(). */
+.ge-canvas-prompt {
+ max-width: 430px;
+ width: 90vw;
+}
+.ge-canvas-prompt .modal-header h4 { margin: 0; font-size: 14px; }
+.ge-canvas-prompt-row {
+ display: flex;
+ align-items: flex-end;
+ gap: 10px;
+ justify-content: center;
+ margin: 6px 0 2px;
+}
+.ge-canvas-prompt-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex: 1;
+ min-width: 0;
+}
+.ge-canvas-prompt-field > span {
+ font-size: 11px;
+ opacity: 0.65;
+}
+.ge-canvas-prompt-field input {
+ width: 100%;
+ padding: 8px 10px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font: inherit;
+ font-size: 14px;
+}
+.ge-canvas-prompt-field input:focus { outline: none; border-color: var(--red); }
+.ge-canvas-prompt-x {
+ font-size: 18px;
+ opacity: 0.4;
+ align-self: center;
+ padding-bottom: 8px;
+ position: relative;
+ top: 8px;
+}
+.ge-canvas-prompt-hint {
+ margin: 0 auto 0 0;
+ font-size: 11px;
+ opacity: 0.5;
+ line-height: 1.35;
+ text-align: left;
+}
+.ge-canvas-prompt-section-label {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ margin: 2px 0 7px;
+ font-size: 10px;
+ font-weight: 650;
+ letter-spacing: .04em;
+ text-transform: uppercase;
+ opacity: .62;
+}
+.ge-canvas-units-field {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin: 8px 0 2px;
+ font-size: 11px;
+ opacity: .75;
+}
+.ge-canvas-control-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ min-width: 0;
+}
+.ge-canvas-prompt-icon,
+.ge-canvas-preset-icon {
+ flex: 0 0 auto;
+ opacity: .68;
+}
+.ge-canvas-units-field select,
+.ge-canvas-interpolation-field select {
+ min-width: 112px;
+ padding: 5px 7px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font: inherit;
+}
+.ge-canvas-anchor-label {
+ margin: 14px 0 6px;
+ font-size: 10px;
+ font-weight: 650;
+ letter-spacing: .04em;
+ text-transform: uppercase;
+ opacity: .62;
+ text-align: center;
+}
+.ge-canvas-anchor-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 22px);
+ gap: 5px;
+ justify-content: center;
+}
+.ge-canvas-anchor {
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ cursor: pointer;
+ position: relative;
+}
+.ge-canvas-anchor:hover,
+.ge-canvas-anchor.active {
+ border-color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+.ge-canvas-resize-options {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 12px;
+ margin-top: 12px;
+}
+.ge-canvas-lock-option {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 11px;
+ opacity: .72;
+ cursor: pointer;
+}
+.ge-canvas-interpolation-field {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ opacity: .72;
+}
+.ge-canvas-interpolation-field select {
+ min-width: 92px;
+ padding: 4px 6px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.ge-canvas-preset-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+}
+.ge-canvas-preset {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 3px;
+ min-width: 0;
+ padding: 8px 9px;
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ color: var(--fg);
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+.ge-canvas-preset:hover,
+.ge-canvas-preset.active {
+ border-color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+.ge-canvas-preset span,
+.ge-canvas-preset strong {
+ display: block;
+ width: 100%;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.ge-canvas-preset span { font-size: 11px; font-weight: 600; }
+.ge-canvas-preset strong { font-size: 10px; font-weight: 500; opacity: .62; }
+.ge-canvas-prompt-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border);
+ margin-top: 8px;
+}
+
+.ge-btn-primary {
+ background: var(--red);
+ color: #fff;
+ border-color: var(--red);
+ font-weight: 600;
+}
+.ge-btn-primary:hover { background: color-mix(in srgb, var(--red) 85%, #000); }
+.ge-zoom-label { font-size: 10px; opacity: 0.5; }
+
+/* Nudge whirlpool spinners 1px down so they sit on the text baseline
+ when placed inside action buttons (Generate, Harmonize, etc). */
+.ge-btn .ai-spinner-whirlpool,
+.ge-btn .spinner-whirlpool,
+button .ai-spinner-whirlpool,
+button .spinner-whirlpool {
+ position: relative;
+ top: 1px;
+}
+
+/* Layers panel */
+.ge-layers {
+ /* Natural height now that the whole right panel scrolls — flex:1
+ would collapse the list inside a scrolling parent. */
+ flex: 0 0 auto;
+ display: flex;
+ flex-direction: column;
+}
+/* Vertical drag handle that sits on the LEFT edge of the right panel.
+ Drag horizontally to widen / narrow the entire side panel. Slim
+ by default; brighter on hover so the user notices it's interactive. */
+.ge-panel-resize {
+ position: absolute;
+ left: -3px;
+ top: 0;
+ bottom: 0;
+ width: 6px;
+ cursor: ew-resize;
+ background: transparent;
+ z-index: 5;
+ transition: background 0.12s;
+}
+.ge-panel-resize:hover,
+.ge-panel-resize:active {
+ background: color-mix(in srgb, var(--red) 30%, transparent);
+}
+/* The right panel needs `position: relative` so the handle (absolute)
+ anchors to it instead of the viewport. */
+.ge-right-panel {
+ position: relative;
+}
+
+/* Floating value bubble that pops above the slider thumb while
+ dragging. Solves the "I can't see the number" problem when the
+ slider's opaque plate covers the row's right-edge value chip. */
+.ge-slider-bubble {
+ /* Fixed positioning so the bubble escapes any overflow:hidden/auto
+ on the slider's ancestor chain (the layers list clips, and used
+ to swallow the bubble entirely). JS sets viewport-relative
+ left/top; the translate centers and lifts the bubble above the
+ slider thumb. */
+ position: fixed;
+ transform: translate(-50%, -100%);
+ z-index: 10000;
+ padding: 5px 10px;
+ background: var(--red);
+ color: #fff;
+ font-size: 13px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ border-radius: 6px;
+ box-shadow: 0 3px 10px color-mix(in srgb, var(--red) 40%, transparent);
+ pointer-events: none;
+ white-space: nowrap;
+ opacity: 0;
+ transition: opacity 0.1s;
+}
+.ge-slider-bubble.visible { opacity: 1; }
+
+/* Floating thumbnail shown when hovering a layer row. */
+.ge-layer-thumb {
+ position: fixed;
+ z-index: 60;
+ padding: 4px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 6px 18px color-mix(in srgb, var(--fg) 30%, transparent);
+ pointer-events: none;
+ display: none;
+}
+.ge-layers-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 10px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--fg);
+ opacity: 0.8;
+ border-bottom: 1px solid var(--border);
+ position: relative;
+}
+.ge-layers-title { flex: 1; }
+.ge-layers-grab { display: none; } /* mobile only */
+.ge-layers-merge-wrap { position: relative; display: inline-flex; }
+.ge-layers-merge-wrap #ge-layers-merge-btn { position: relative; top: 0; }
+.ge-layers-merge-menu {
+ /* Fixed positioning so the menu escapes the layers panel's overflow
+ clipping (which made it look transparent). JS sets exact coords
+ via getBoundingClientRect on the trigger button at open time. */
+ position: fixed;
+ z-index: 200;
+ min-width: 160px;
+ padding: 4px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ pointer-events: auto;
+}
+.ge-layers-merge-menu .dropdown-item-compact {
+ width: 100%;
+ background: none;
+ border: none;
+ text-align: left;
+ font: inherit;
+}
+.ge-layers-actions {
+ display: flex;
+ gap: 4px;
+ padding: 6px 8px;
+ border-top: 1px solid var(--border);
+ flex-wrap: wrap;
+ /* Pin the action row to the bottom of the right panel's scroll
+ viewport. Without this, when the tool-controls section is tall
+ enough to overflow the panel, the merge buttons get pushed below
+ the fold and require scrolling to reach. */
+ position: sticky;
+ bottom: 0;
+ background: var(--panel);
+ z-index: 4;
+ /* Centered cluster instead of stretching the buttons to fill the
+ panel width. */
+ justify-content: center;
+}
+/* Merge / flatten buttons sit content-sized so the cluster reads as a
+ centered group of three, not three stretched bars across the panel. */
+.ge-layers-actions .ge-btn {
+ flex: 0 0 auto;
+ min-width: 0;
+ padding: 4px 8px;
+ font-size: 10px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ white-space: nowrap;
+}
+.ge-layers-actions .ge-icon-btn {
+ /* Icon-only buttons stay narrow — no risk of label overflow. */
+ flex: 0 0 auto;
+ padding: 4px 8px;
+}
+.ge-layers-actions .ge-btn svg {
+ flex-shrink: 0;
+ margin-right: 0 !important;
+}
+.ge-layers-list {
+ /* Cap the list so a huge layer stack scrolls within its own box
+ rather than pushing the whole panel arbitrarily tall. */
+ max-height: 320px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding: 0;
+}
+.ge-layer-item {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 5px 8px;
+ cursor: pointer;
+ border-left: 2px solid transparent;
+ transition: background 0.1s;
+ font-size: 11px;
+ color: var(--fg);
+}
+.ge-layer-item:focus-visible,
+.ge-layer-group-row:focus-visible {
+ outline: 1px solid var(--accent, var(--red));
+ outline-offset: -1px;
+}
+@media (min-width: 701px) {
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) {
+ display: grid;
+ grid-template-columns: 12px 22px 34px minmax(0, 1fr) 24px;
+ row-gap: 5px;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-drag {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-vis {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-inline-thumb {
+ grid-column: 3;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name,
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name-input {
+ grid-column: 4;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-lock-btn {
+ grid-column: 5;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ width: 100%;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-controls {
+ grid-column: 1 / -1;
+ grid-row: 3;
+ justify-content: flex-end;
+ min-width: 0;
+ }
+}
+.ge-layer-inline-thumb {
+ display: block;
+ width: 34px;
+ height: 26px;
+ flex: 0 0 34px;
+ object-fit: contain;
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ border-radius: 3px;
+ background: #303030;
+ image-rendering: auto;
+}
+/* Row-hover tint removed — kept the layer row's background stable so
+ it doesn't shift visually as the user reaches for the opacity slider. */
+.ge-layer-item.active {
+ background: color-mix(in srgb, var(--red) 8%, transparent);
+ border-left-color: var(--red);
+}
+/* "Active parent" — the layer is selected, but a mask sub-layer below
+ it is the actual paint target. Soft hint so the user can see which
+ parent owns the active mask without competing with the mask's
+ strong red highlight. */
+.ge-layer-item.active-parent {
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+ border-left-color: color-mix(in srgb, var(--fg) 30%, transparent);
+}
+.ge-layer-item.selected:not(.active):not(.active-parent) {
+ background: color-mix(in srgb, var(--accent, var(--red)) 7%, transparent);
+ box-shadow: inset 2px 0 0 color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
+}
+.ge-layer-group-row {
+ min-height: 36px;
+ display: grid;
+ grid-template-columns: 12px 18px 24px 34px minmax(0, 1fr) 24px;
+ grid-template-rows: auto auto auto;
+ align-items: center;
+ gap: 3px;
+ padding: 3px 7px 3px calc(7px + var(--group-depth, 0) * 14px);
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
+ background: color-mix(in srgb, var(--bg-secondary) 78%, var(--bg));
+ color: var(--fg);
+ width: 100%;
+ box-sizing: border-box;
+}
+.ge-layer-group-row .ge-layer-group-drag { grid-column: 1; grid-row: 1; }
+.ge-layer-group-row .ge-layer-group-toggle { grid-column: 2; grid-row: 1; }
+.ge-layer-group-row .ge-layer-vis { grid-column: 3; grid-row: 1; }
+.ge-layer-group-row .ge-group-inline-thumb { grid-column: 4; grid-row: 1; }
+.ge-layer-group-row .ge-layer-group-name { grid-column: 5; grid-row: 1; }
+.ge-layer-group-row .ge-layer-lock-btn { grid-column: 6; grid-row: 1; }
+.ge-layer-group-row .ge-layer-opacity-row {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ display: flex;
+ width: 100%;
+ min-width: 0;
+}
+.ge-layer-group-row .ge-layer-opacity-row .ge-layer-opacity {
+ flex: 1 1 auto;
+ width: 100%;
+ min-width: 0;
+}
+.ge-layer-opacity-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.ge-layer-opacity-row .ge-layer-opacity { flex: 1 1 auto; width: auto; }
+.ge-layer-group-row .ge-layer-controls {
+ grid-column: 1 / -1;
+ grid-row: 3;
+ justify-content: flex-end;
+}
+.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ width: 100%;
+}
+.ge-layer-group-drag { touch-action: none; }
+.ge-layer-group-row.group-dragging {
+ opacity: 0.62;
+ outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 65%, transparent);
+}
+.ge-group-drop-line {
+ position: absolute;
+ z-index: 20;
+ left: 6px;
+ right: 6px;
+ height: 2px;
+ pointer-events: none;
+ background: var(--accent, var(--red));
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--bg) 65%, transparent);
+}
+.ge-layer-group-row.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--bg-secondary));
+ box-shadow: inset 2px 0 0 var(--accent, var(--red));
+}
+.ge-layer-group-toggle {
+ width: 18px;
+ height: 22px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ opacity: 0.62;
+}
+.ge-layer-group-toggle svg { transition: transform 120ms ease; }
+.ge-layer-group-toggle.expanded svg { transform: rotate(90deg); }
+.ge-layer-group-name {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ overflow: hidden;
+ font-size: 11px;
+ font-weight: 600;
+}
+.ge-layer-group-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.ge-group-mask-sub-item { padding-left: calc(20px + var(--group-mask-depth, 1) * 14px); }
+.ge-layer-vis {
+ border: none;
+ background: none;
+ color: var(--fg);
+ cursor: pointer;
+ padding: 2px 4px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.4;
+ transition: opacity 0.12s;
+}
+.ge-layer-vis:hover { opacity: 0.8; }
+.ge-layer-vis.visible { opacity: 0.9; }
+.ge-layer-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ min-width: 0;
+}
+.ge-layer-clipped-marker {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 0 auto;
+ color: var(--accent, var(--red));
+ opacity: 0.82;
+}
+.ge-layer-name-input {
+ flex: 1;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 11px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ min-width: 0;
+}
+/* All sliders inside the image editor share the same visual language as
+ the layer opacity slider — rounded pill, dim track, red thumb. Track
+ AND thumb grow when interacting for finer tweaking. */
+.gallery-editor-container input[type="range"] {
+ -webkit-appearance: none;
+ appearance: none;
+ height: 4px;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+ accent-color: var(--red);
+ cursor: pointer;
+ transition: height 0.15s ease;
+}
+.gallery-editor-container input[type="range"]:hover,
+.gallery-editor-container input[type="range"]:focus,
+.gallery-editor-container input[type="range"]:active {
+ height: 10px;
+}
+.gallery-editor-container input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.gallery-editor-container input[type="range"]::-moz-range-thumb {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--red);
+ border: none;
+ cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.gallery-editor-container input[type="range"]:hover::-webkit-slider-thumb,
+.gallery-editor-container input[type="range"]:focus::-webkit-slider-thumb,
+.gallery-editor-container input[type="range"]:active::-webkit-slider-thumb {
+ width: 18px;
+ height: 18px;
+}
+.gallery-editor-container input[type="range"]:hover::-moz-range-thumb,
+.gallery-editor-container input[type="range"]:focus::-moz-range-thumb,
+.gallery-editor-container input[type="range"]:active::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+}
+
+/* Layer-row opacity slider — fixed track, only thumb grows on
+ interaction. Takes a bit of row space at rest, but no jumpy expand
+ animation. */
+.ge-layer-opacity {
+ width: 70px;
+ height: 6px;
+ accent-color: var(--red);
+ flex-shrink: 0;
+ -webkit-appearance: none;
+ appearance: none;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+ position: relative;
+ top: 0;
+}
+.ge-layer-opacity:hover::-webkit-slider-thumb,
+.ge-layer-opacity:active::-webkit-slider-thumb,
+.ge-layer-opacity.dragging::-webkit-slider-thumb {
+ width: 18px;
+ height: 18px;
+}
+.ge-layer-opacity:hover::-moz-range-thumb,
+.ge-layer-opacity:active::-moz-range-thumb,
+.ge-layer-opacity.dragging::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+}
+.ge-layer-controls {
+ display: flex;
+ gap: 2px;
+ flex-shrink: 0;
+}
+.ge-layer-controls .ge-mask-inspect-btn.active {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+}
+.ge-mask-properties {
+ position: relative;
+ flex: 0 0 30px;
+}
+.ge-mask-properties > summary:hover,
+.ge-mask-properties[open] > summary {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+.ge-mask-properties-panel {
+ position: fixed;
+ z-index: 9999;
+ right: auto;
+ top: 0;
+ width: 190px;
+ padding: 8px;
+ display: grid;
+ gap: 7px;
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+ border-radius: 5px;
+ background: var(--bg-secondary, var(--bg));
+ box-shadow: 0 8px 24px rgba(0,0,0,.28);
+}
+.ge-mask-property-row {
+ display: grid;
+ grid-template-columns: 52px minmax(0, 1fr) 35px;
+ align-items: center;
+ gap: 6px;
+ font-size: 10px;
+}
+.ge-mask-property-row input { min-width: 0; width: 100%; }
+.ge-mask-apply-btn {
+ width: 100%;
+ min-height: 26px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ color: var(--fg);
+ cursor: pointer;
+ font-size: 10px;
+}
+.ge-mask-apply-btn:hover { background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent); }
+.ge-layer-tools {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 2px;
+ min-height: 34px;
+ padding: 3px 7px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ background: color-mix(in srgb, var(--bg-secondary) 72%, var(--bg));
+}
+.ge-layer-tools .ge-layer-controls {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ width: 100%;
+}
+.ge-eyedropper-live {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ min-height: 28px;
+ padding: 3px 6px;
+ border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
+ border-radius: 5px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.ge-eyedropper-loupe {
+ width: 42px;
+ height: 42px;
+ flex: 0 0 42px;
+ border-radius: 4px;
+ image-rendering: pixelated;
+ background: var(--bg);
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 22%, transparent);
+}
+.ge-eyedropper-live-swatch {
+ width: 20px;
+ height: 20px;
+ flex: 0 0 20px;
+ border-radius: 4px;
+ background: transparent;
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 22%, transparent);
+}
+.ge-eyedropper-live-values {
+ display: grid;
+ gap: 1px;
+ min-width: 0;
+ font-size: 10px;
+ line-height: 1.25;
+}
+.ge-eyedropper-live-values > span { opacity: .62; font-variant-numeric: tabular-nums; }
+.ge-eyedropper-live-swatch.empty {
+ background: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--fg) 18%, transparent) 4px 6px);
+}
+.ge-eyedropper-live code {
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ opacity: .75;
+}
+/* Left-edge drag handle on each layer row. */
+.ge-layer-drag {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 12px;
+ margin-right: 2px;
+ opacity: 0.4;
+ cursor: grab;
+ touch-action: none;
+ user-select: none;
+ flex-shrink: 0;
+}
+.ge-layer-drag:hover { opacity: 0.85; }
+/* While dragSort lifts the item via absolute positioning, the placeholder
+ slot shows where it'll land — give it a subtle red outline so users see
+ the target clearly. */
+.ge-layers-list .drag-placeholder {
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+ border: 1px dashed color-mix(in srgb, var(--red) 50%, transparent);
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+.ge-layer-item.dragging {
+ background: var(--panel);
+ box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 4px;
+}
+.ge-layer-btn {
+ border: none;
+ background: none;
+ color: var(--fg);
+ cursor: pointer;
+ font-size: 12px;
+ padding: 0 2px;
+ opacity: 0.4;
+ transition: opacity 0.1s;
+}
+.ge-layer-btn:hover { opacity: 0.8; }
+.ge-layer-btn.active { color: var(--accent, var(--red)); opacity: 0.95; }
+.ge-layer-btn:disabled { cursor: default; opacity: 0.16; }
+.ge-layer-btn.danger:hover { color: var(--red); opacity: 1; }
+
+.ge-layer-lock-menu {
+ position: fixed;
+ z-index: 10004;
+ width: 176px;
+ padding: 4px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel, var(--bg));
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
+}
+.ge-layer-lock-option {
+ width: 100%;
+ height: 30px;
+ display: grid;
+ grid-template-columns: 20px minmax(0, 1fr);
+ align-items: center;
+ gap: 6px;
+ padding: 0 8px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ text-align: left;
+ cursor: pointer;
+}
+.ge-layer-lock-option:hover,
+.ge-layer-lock-option:focus-visible {
+ outline: none;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.ge-layer-align-menu { width: 218px; }
+.ge-layer-lock-check {
+ width: 16px;
+ height: 16px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid color-mix(in srgb, var(--fg) 28%, transparent);
+ border-radius: 3px;
+ color: var(--accent, var(--red));
+ font-size: 11px;
+ line-height: 1;
+}
+@media (max-width: 700px) {
+ .ge-layer-lock-menu { width: auto; }
+ .ge-layer-lock-option { height: 44px; font-size: 13px; }
+}
+
+/* Crop apply button */
+.ge-crop-apply {
+ position: absolute;
+ z-index: 10;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 6px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+}
+.ge-crop-apply input[type="number"] {
+ width: 56px;
+ padding: 3px 4px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font: inherit;
+ font-size: 11px;
+ -moz-appearance: textfield;
+}
+.ge-crop-x { font-size: 11px; opacity: 0.5; }
+.ge-crop-apply-btn {
+ padding: 4px 10px;
+ background: var(--red);
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ position: relative;
+ top: -2px;
+}
+.ge-crop-apply-btn:hover { background: color-mix(in srgb, var(--red) 85%, #000); }
+
+/* Text input floating */
+.ge-text-input {
+ background: var(--bg);
+ border: 1px solid var(--red);
+ color: var(--fg);
+ padding: 4px 8px;
+ font-size: 14px;
+ border-radius: 4px;
+ min-width: 120px;
+}
+.ge-brush-cursor {
+ position: fixed; pointer-events: none; z-index: 2147483647;
+ border: 1.5px solid #fff; border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.72);
+ display: none;
+ box-sizing: border-box;
+}
+.ge-inpaint-section {
+ padding: 8px 0; border-top: 1px solid var(--border); margin-top: 4px;
+}
+.ge-inpaint-popover-head {
+ display: none;
+}
+@media (min-width: 821px) {
+ .ge-controls.ge-inpaint-popover-host {
+ padding: 0;
+ border-bottom: 0;
+ gap: 0;
+ }
+ .ge-inpaint-section.ge-inpaint-popover {
+ position: fixed;
+ z-index: 10004;
+ width: 330px;
+ max-height: calc(100vh - 24px);
+ overflow-y: auto;
+ padding: 10px;
+ margin: 0;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--panel);
+ box-shadow: 0 12px 36px rgba(0, 0, 0, 0.42);
+ }
+ .ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin: -10px -10px 8px;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border);
+ cursor: grab;
+ user-select: none;
+ }
+ .ge-inpaint-section.ge-inpaint-popover > .ge-section-title-with-help:not(.ge-inpaint-popover-title) {
+ display: none;
+ }
+ .ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-title {
+ margin: 0;
+ flex: 1 1 auto;
+ }
+ .ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-close {
+ flex: 0 0 auto;
+ position: relative;
+ top: -8px;
+ border: none;
+ background: none;
+ color: var(--fg-muted);
+ font-size: 18px;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0 4px;
+ border-radius: 4px;
+ transition: color 0.14s ease, background-color 0.14s ease;
+ }
+ .ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-close:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ }
+}
+.ge-inpaint-model-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.ge-inpaint-model-row label {
+ flex: 0 0 auto;
+ font-size: 10px;
+ opacity: 0.65;
+}
+.ge-inpaint-model-row #ge-ai-inpaint {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-size: 10px;
+ padding: 4px 6px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+}
+.ge-inpaint-prompt {
+ width: 100%; padding: 6px 8px; background: var(--bg); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 4px; font-size: 12px;
+ font-family: inherit; margin-top: 4px; box-sizing: border-box;
+}
+.ge-inpaint-prompt:focus { border-color: var(--red); outline: none; }
+
+/* Responsive: stack on narrow screens */
+@media (max-width: 700px) {
+ /* The editor body holds the sidebar | canvas | layers panel; on
+ mobile we stack them as toolbar (top) → canvas (middle) → layers
+ (bottom). Without this the body stays a flex row and the canvas
+ ends up zero-width because the sidebar takes the full width. */
+ .ge-editor-body {
+ flex-direction: column;
+ }
+ /* Brush/Size-style rows: stack the label above its slider so the
+ slider gets a full row instead of getting smashed against the
+ panel's right edge. Doesn't touch .ge-eraser-row which has its own
+ value-chip-on-the-right layout. */
+ .ge-control-row:not(.ge-eraser-row):has(input[type="range"]) {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 2px;
+ }
+ .ge-control-row:not(.ge-eraser-row):has(input[type="range"]) > input[type="range"] {
+ width: 100%;
+ }
+ /* More breathing room around lasso/wand sliders on mobile so they
+ don't hug the right edge of the controls panel. */
+ #ge-lasso-section .ge-eraser-row,
+ #ge-wand-section .ge-eraser-row {
+ padding-right: 12px;
+ margin-top: 6px;
+ }
+ /* Lasso action buttons (Clear / Copy / To Mask / Bg Remove): pin to
+ the right edge of the row instead of left-aligned. */
+ #ge-lasso-section .ge-actions,
+ #ge-wand-section .ge-actions {
+ justify-content: flex-end;
+ }
+ /* Universal thumb-grow on active for every slider on mobile. */
+ .ge-controls input[type="range"]:active::-webkit-slider-thumb,
+ .ge-controls input[type="range"].is-using::-webkit-slider-thumb {
+ width: 22px;
+ height: 22px;
+ }
+ .ge-controls input[type="range"]:active::-moz-range-thumb,
+ .ge-controls input[type="range"].is-using::-moz-range-thumb {
+ width: 22px;
+ height: 22px;
+ }
+ .ge-controls input[type="range"]::-webkit-slider-thumb,
+ .ge-controls input[type="range"]::-moz-range-thumb {
+ transition: width 0.12s ease, height 0.12s ease;
+ }
+ .ge-toolbar {
+ flex-direction: row;
+ width: 100%;
+ height: auto;
+ padding: 4px 0;
+ border-right: none;
+ border-bottom: 1px solid var(--border);
+ overflow-x: auto;
+ overflow-y: hidden;
+ flex-shrink: 0;
+ gap: 0;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .ge-toolbar::-webkit-scrollbar { display: none; }
+ /* Section labels in the toolbar were vertical breaks — turn them
+ into thin vertical dividers when the column flows horizontally. */
+ .ge-tool-sep {
+ border-top: none;
+ border-left: 1px solid var(--border);
+ margin: 0 4px;
+ padding: 0;
+ width: 1px;
+ height: 24px;
+ text-indent: -9999px;
+ overflow: hidden;
+ flex-shrink: 0;
+ }
+ .ge-tool-btn {
+ flex-shrink: 0;
+ width: 48px;
+ }
+ /* Canvas is the main thing — give it as much vertical space as
+ the editor body can spare. min-height keeps it visible even when
+ the right panel is at its max. */
+ .ge-canvas-area {
+ flex: 1 1 auto;
+ min-height: 40vh;
+ }
+ /* Right panel = layers list. Docked as a bottom sheet with a peek
+ state: collapsed shows the header strip (and the active layer
+ row peeks through), expanded reveals all layers + actions. The
+ user swipes up to expand, down to collapse — the active layer is
+ never fully hidden. */
+ .ge-right-panel {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ max-height: 70vh;
+ height: 70vh;
+ background: var(--panel);
+ border-left: none;
+ border-top: 1px solid var(--border);
+ border-radius: 12px 12px 0 0;
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
+ z-index: 50;
+ flex-shrink: 0;
+ /* Peek height grows with the layer count up to a cap. JS sets the
+ `--peek-height` custom property from _renderLayerPanel(); default
+ fallback is enough for the header + one row. */
+ transform: translateY(calc(100% - var(--peek-height, 110px)));
+ transition: transform 0.22s ease-out;
+ }
+ .ge-right-panel.expanded {
+ transform: translateY(0);
+ }
+ /* Minimized: panel slides almost fully off-screen — only a thin
+ handle strip remains visible so the user can swipe back up. */
+ .ge-right-panel.minimized {
+ transform: translateY(calc(100% - 24px));
+ }
+ .ge-layers-grab {
+ display: block !important;
+ position: absolute;
+ top: 6px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40px;
+ height: 4px;
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+ border-radius: 2px;
+ }
+ .ge-layers-header {
+ padding-top: 16px;
+ position: relative;
+ }
+ /* In peek state, the layers list takes the full panel height (~70vh)
+ and never overflows so its own scrollbar doesn't activate. Constrain
+ the list to the visible peek area so it scrolls properly when there
+ are more layers than the peek can show. */
+ .ge-right-panel:not(.expanded) .ge-layers-list {
+ max-height: calc(var(--peek-height, 110px) - 52px);
+ -webkit-overflow-scrolling: touch;
+ }
+ /* Mobile layer-row buttons — bump hit targets up so fingers can
+ reliably tap visibility / fx / delete. */
+ .ge-layer-vis,
+ .ge-layer-btn,
+ .ge-layer-fx-btn {
+ width: 40px !important;
+ height: 40px !important;
+ font-size: 18px !important;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .ge-layer-btn svg,
+ .ge-layer-fx-btn svg {
+ width: 18px;
+ height: 18px;
+ }
+ .ge-layers-list {
+ overflow-x: clip;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) {
+ display: grid;
+ grid-template-columns: 12px 40px 34px minmax(0, 1fr) 40px;
+ grid-template-rows: 40px auto 40px;
+ align-items: center;
+ row-gap: 3px;
+ }
+ .ge-layers-list > .ge-layer-group-row {
+ grid-template-columns: 12px 40px 40px 34px minmax(0, 1fr) 40px;
+ grid-template-rows: 40px auto 40px;
+ align-items: center;
+ row-gap: 3px;
+ }
+ .ge-layer-group-row .ge-layer-group-drag { grid-column: 1; grid-row: 1; }
+ .ge-layer-group-row .ge-layer-group-toggle { grid-column: 2; grid-row: 1; }
+ .ge-layer-group-row .ge-layer-vis { grid-column: 3; grid-row: 1; }
+ .ge-layer-group-row .ge-group-inline-thumb { grid-column: 4; grid-row: 1; }
+ .ge-layer-group-row .ge-layer-group-name { grid-column: 5; grid-row: 1; }
+ .ge-layer-group-row .ge-layer-lock-btn { grid-column: 6; grid-row: 1; }
+ .ge-layer-group-row .ge-layer-opacity-row {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ margin-left: 52px;
+ width: auto;
+ }
+ .ge-layer-group-row .ge-layer-controls {
+ grid-column: 1 / -1;
+ grid-row: 3;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-drag {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-vis {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-inline-thumb {
+ grid-column: 3;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name,
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name-input {
+ grid-column: 4;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-lock-btn {
+ grid-column: 5;
+ grid-row: 1;
+ }
+ .ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ display: flex;
+ width: auto;
+ min-width: 0;
+ margin-left: 52px;
+ }
+ .ge-mask-sub-item {
+ display: grid;
+ grid-template-columns: 40px 34px minmax(80px, 1fr) auto;
+ grid-template-rows: 40px;
+ align-items: center;
+ padding-left: 8px;
+ }
+ .ge-mask-sub-item > .ge-layer-vis { grid-column: 1; grid-row: 1; }
+ .ge-mask-sub-item > .ge-mask-inline-thumb { grid-column: 2; grid-row: 1; }
+ .ge-mask-sub-item > .ge-layer-name {
+ grid-column: 3;
+ grid-row: 1;
+ display: flex !important;
+ min-width: 0;
+ }
+ .ge-mask-sub-item > .ge-layer-controls {
+ grid-column: 4;
+ grid-row: 1;
+ min-width: 0;
+ justify-content: flex-start;
+ }
+ .ge-mask-sub-item > .ge-layer-controls .ge-layer-btn {
+ width: 30px !important;
+ min-width: 30px !important;
+ height: 30px !important;
+ font-size: 14px !important;
+ }
+ /* Layer opacity slider stays at its expanded size at all times on
+ mobile — there's room for it and the resize-on-hover felt jittery
+ when scrolling. */
+ .ge-layer-opacity,
+ .ge-layer-opacity:hover,
+ .ge-layer-opacity:active,
+ .ge-layer-opacity.dragging {
+ width: 160px !important;
+ height: 10px !important;
+ }
+ /* Transform / adjust popups dock at the TOP of the viewport on
+ mobile so they overlay the topbar instead of eating canvas space.
+ Fixed positioning escapes any transformed ancestor stacking. */
+ .ge-transform-popup,
+ .ge-adj-popup,
+ .ge-fx-popup {
+ position: fixed !important;
+ left: 8px !important;
+ right: 8px !important;
+ top: 32px !important;
+ bottom: auto !important;
+ max-width: calc(100vw - 16px) !important;
+ width: auto !important;
+ min-width: 0 !important;
+ min-height: 240px !important;
+ max-height: calc(100vh - 40px);
+ overflow-y: auto;
+ z-index: 10003 !important;
+ }
+ .ge-fx-menu {
+ position: fixed !important;
+ left: 8px !important;
+ right: 8px !important;
+ top: auto !important;
+ bottom: calc(env(safe-area-inset-bottom, 0px) + 8px) !important;
+ width: auto !important;
+ min-width: 0 !important;
+ max-width: calc(100vw - 16px) !important;
+ z-index: 10002 !important;
+ }
+ /* Hide both the minimise and × close buttons in the header on
+ mobile — a clearly labelled Cancel button next to Apply in the
+ body is more discoverable than tiny header icons. */
+ .ge-transform-popup #ge-transform-min,
+ .ge-transform-popup #ge-transform-cancel {
+ display: none !important;
+ }
+ /* Wrap the transform body into precision rows on mobile:
+ Row 1: X + Y
+ Row 2: W + H
+ Row 3: ↻ rotate + quick actions + Apply/Cancel */
+ .ge-transform-popup-body {
+ flex-wrap: wrap !important;
+ align-items: center;
+ row-gap: 8px;
+ column-gap: 6px;
+ }
+ .ge-transform-popup-body #ge-transform-rot {
+ width: 3.5ch !important;
+ min-width: 0 !important;
+ }
+ /* 3-row layout, enforced by explicit `.ge-row-break` elements in the
+ DOM (the only reliable way to force a flex-wrap line break).
+ Row 1: W input + H input
+ Row 2: aspect lock + ↻ rotate input
+ Row 3: flip-h, flip-v, rot-90 + Cancel + Apply */
+ .ge-transform-popup-body .ge-row-break {
+ display: block;
+ flex-basis: 100%;
+ height: 0;
+ width: 100%;
+ }
+ /* Flip buttons slightly bigger so they're easier to tap. */
+ .ge-transform-quick-btn {
+ padding: 8px 10px !important;
+ min-width: 40px;
+ min-height: 40px;
+ }
+ .ge-transform-quick-btn svg {
+ width: 18px;
+ height: 18px;
+ }
+ /* Cancel + Apply share the right side of row 2, same size, paired. */
+ .ge-transform-popup-body #ge-transform-cancel-btn {
+ margin-left: auto;
+ }
+ .ge-transform-popup-body #ge-transform-cancel-btn,
+ .ge-transform-popup-body #ge-transform-apply {
+ padding: 8px 16px !important;
+ font-size: 13px !important;
+ min-width: 80px;
+ text-align: center;
+ }
+ /* Stepper-style number control on mobile:
+ [ − ] [ input ] [ + ]
+ Stack reverses so the − sits visually on the LEFT of the input, +
+ on the RIGHT. Big finger-sized squares, big +/- glyphs, both
+ buttons support tap-and-hold auto-repeat (JS) for fast scrubbing. */
+ .ge-transform-field {
+ display: inline-flex !important;
+ align-items: stretch;
+ gap: 0;
+ }
+ .ge-transform-field > label {
+ display: inline-flex;
+ align-items: center;
+ padding-right: 6px;
+ font-weight: 600;
+ }
+ .ge-transform-field:not(:has(#ge-transform-rot)) > label {
+ position: relative;
+ top: -1px;
+ }
+ .ge-transform-spin {
+ display: inline-flex !important;
+ flex-direction: row !important;
+ /* Use grid-order to swap positions of [−][+]: the − should sit
+ BEFORE the input, the + AFTER it. Achieve this by absolute
+ order — first child styled differently, second styled
+ differently, then we move the whole .ge-transform-spin's first
+ button visually before the input via reverse-flex. */
+ order: 0;
+ width: auto !important;
+ }
+ .ge-transform-field {
+ flex-direction: row;
+ }
+ /* Re-flow: label + [-] + input + [+]
+ We split the .ge-transform-spin's two buttons across the field's
+ flex line using order: order:0 for −, order:2 for input, order:3
+ for +. The spin itself gets `display: contents` so its
+ children can be reordered independently against the input. */
+ .ge-transform-spin {
+ display: contents !important;
+ }
+ .ge-transform-field > input.ge-transform-popup-input {
+ order: 2;
+ text-align: center;
+ }
+ .ge-transform-spin button[data-spin="down"] {
+ order: 1;
+ }
+ .ge-transform-spin button[data-spin="up"] {
+ order: 3;
+ }
+ /* The buttons themselves — chunky, square, identical in size. Reset
+ the base `:first-child` rule (which strips the bottom border for
+ the old stacked layout) so both buttons render with the same
+ visual box in the new row layout. */
+ .ge-transform-spin button {
+ box-sizing: border-box !important;
+ width: 36px !important;
+ height: 36px !important;
+ min-width: 36px;
+ min-height: 36px;
+ padding: 0 0 0 0 !important;
+ font-size: 18px !important;
+ font-weight: 700;
+ line-height: 1 !important;
+ border: 1px solid var(--border) !important;
+ border-radius: 6px !important;
+ flex-shrink: 0;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ top: -4px;
+ }
+ .ge-transform-spin button:first-child,
+ .ge-transform-spin button:last-child {
+ border-radius: 6px !important;
+ border-bottom: 1px solid var(--border) !important;
+ }
+ /* Graphic-editor adjustment buttons use text glyphs that sit visually low —
+ lift them to match the +/- button alignment. */
+ .ge-adj-close,
+ .ge-adj-min {
+ padding-top: 0 !important;
+ padding-bottom: 8px !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ }
+ .ge-transform-spin button[data-spin="down"] { margin-right: 4px; }
+ .ge-transform-spin button[data-spin="up"] { margin-left: 4px; }
+ /* The input between them — generous tap area and clear typography. */
+ .ge-transform-field > input.ge-transform-popup-input {
+ height: 36px !important;
+ padding: 0 6px !important;
+ font-size: 14px !important;
+ min-width: 56px;
+ position: relative;
+ top: 2px;
+ }
+ /* Rotate input — compact so it fits on row 2 alongside the flip
+ quick buttons and the Cancel + Apply pair. */
+ .ge-transform-field > #ge-transform-rot.ge-transform-popup-input {
+ min-width: 0;
+ width: 64px !important;
+ }
+ /* Suffix (°) sits right after the rotate input but before the + button. */
+ .ge-transform-popup-suffix { order: 3; align-self: center; padding: 0 2px; }
+ /* Slider thumb grows only while actively dragged — keeps the resting
+ row compact, thumb pops larger when the user grabs it. */
+ .gallery-editor-container input[type="range"]:active::-webkit-slider-thumb,
+ .ge-layer-opacity:active::-webkit-slider-thumb,
+ .ge-layer-opacity.dragging::-webkit-slider-thumb {
+ width: 22px !important;
+ height: 22px !important;
+ }
+ .gallery-editor-container input[type="range"]:active::-moz-range-thumb,
+ .ge-layer-opacity:active::-moz-range-thumb,
+ .ge-layer-opacity.dragging::-moz-range-thumb {
+ width: 22px !important;
+ height: 22px !important;
+ }
+ /* Slider value bubble on mobile — bigger text/padding for
+ readability. Position is JS-driven via `position: fixed` from the
+ base rule, so don't override top/bottom here — `bottom: 100%`
+ with fixed positioning was pushing the bubble off-screen. */
+ .ge-slider-bubble {
+ font-size: 14px !important;
+ padding: 5px 10px !important;
+ }
+ /* Editor topbar — scrolls horizontally on narrow viewports. Buttons
+ fill more vertical+horizontal space so each is a comfortable
+ finger-tap target instead of feeling cramped. */
+ .ge-topbar {
+ overflow-x: auto;
+ overflow-y: hidden;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ gap: 6px;
+ padding: 6px 8px;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .ge-topbar::-webkit-scrollbar { display: none; }
+ .ge-topbar-left,
+ .ge-topbar-right {
+ flex-shrink: 0;
+ gap: 6px;
+ }
+ .ge-alpha-badge {
+ display: none;
+ }
+ #ge-undo {
+ position: sticky !important;
+ left: 8px;
+ background: var(--panel);
+ z-index: 8;
+ box-shadow: 8px 0 10px -10px color-mix(in srgb, var(--fg) 55%, transparent);
+ }
+ .ge-topbar .ge-btn,
+ .ge-topbar .ge-btn-sm {
+ padding: 7px 11px !important;
+ font-size: 13px !important;
+ min-height: 34px;
+ line-height: 1;
+ top: 0 !important;
+ }
+ .ge-topbar .ge-btn svg,
+ .ge-topbar .ge-btn-sm svg {
+ width: 16px;
+ height: 16px;
+ }
+ .ge-topbar select,
+ .ge-topbar input,
+ .ge-topbar .ge-ai-model {
+ padding: 6px 8px !important;
+ font-size: 12px !important;
+ min-height: 32px;
+ top: 0 !important;
+ }
+ .ge-topbar .ge-canvas-size {
+ font-size: 12px !important;
+ padding: 0 6px;
+ line-height: 34px;
+ }
+ #ge-history-panel {
+ left: 0 !important;
+ right: 0 !important;
+ top: auto !important;
+ bottom: 0 !important;
+ width: auto !important;
+ max-height: min(60dvh, 420px);
+ border-radius: 12px 12px 0 0 !important;
+ z-index: 10001 !important;
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
+ }
+ #ge-history-panel .ge-history-head { cursor: default; }
+ #ge-shortcuts-btn {
+ display: none !important;
+ }
+ /* Topbar popups that need real interaction (Resize sliders, Edge
+ form, Save with sub-items) become bottom-sheet slide-ups on
+ mobile so there's room to interact. Image and Filter are tight
+ single-column dropdowns — they stay anchored to their button. */
+ .ge-resize-menu,
+ .ge-edge-menu,
+ .ge-save-menu {
+ position: fixed !important;
+ left: 0 !important;
+ right: 0 !important;
+ top: auto !important;
+ bottom: 0 !important;
+ min-width: 0 !important;
+ max-width: 100vw !important;
+ width: auto;
+ padding: 18px 16px 24px !important;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 12px 12px 0 0 !important;
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
+ z-index: 10005;
+ animation: ge-controls-slide-up 0.2s ease-out;
+ }
+ /* Image / Filter stay as anchored dropdowns; just cap width
+ so they don't run off-screen on narrow viewports. */
+ .ge-image-menu,
+ .ge-selection-menu,
+ .ge-filter-menu {
+ max-width: calc(100vw - 16px);
+ }
+ .ge-resize-menu::before,
+ .ge-edge-menu::before,
+ .ge-save-menu::before {
+ content: '';
+ position: absolute;
+ top: 6px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40px;
+ height: 4px;
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+ border-radius: 2px;
+ }
+ /* The Edge form needs a bit of breathing room when it spans the
+ viewport; the W/H inputs in the crop apply pop should stay compact. */
+ .ge-edge-menu { padding: 12px; }
+ .ge-canvas-prompt {
+ width: calc(100vw - 16px) !important;
+ max-width: none !important;
+ }
+ .ge-canvas-prompt-footer {
+ flex-wrap: wrap;
+ }
+ .ge-canvas-prompt-footer .ge-canvas-prompt-hint {
+ flex: 1 0 100%;
+ margin-bottom: 2px;
+ }
+ /* Inpaint floating prompt — keep it inside the canvas area on
+ mobile so it doesn't run off the right edge. */
+ .ge-inpaint-popup {
+ max-width: calc(100vw - 16px);
+ }
+ .ge-inpaint-popup-input { width: 160px; }
+
+ /* Tool-specific controls (brush size/color, eraser opacity/flow,
+ lasso feather, inpaint prompt panel, etc.) lift out of the right
+ panel and dock as a slide-up bottom sheet so they have actual room
+ to breathe on a phone. A short grab-handle visually separates it
+ from the canvas. */
+ .ge-controls {
+ position: fixed !important;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 60;
+ border-bottom: none;
+ border-top: 1px solid var(--border);
+ background: var(--panel);
+ border-radius: 12px 12px 0 0;
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
+ max-height: 60vh;
+ overflow-y: auto;
+ padding: 18px 16px 24px;
+ gap: 12px;
+ /* Slide-up entrance animation when the controls become visible. */
+ animation: ge-controls-slide-up 0.18s ease-out;
+ }
+ .ge-controls::before {
+ content: '';
+ position: absolute;
+ top: 6px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40px;
+ height: 4px;
+ background: color-mix(in srgb, var(--fg) 30%, transparent);
+ border-radius: 2px;
+ }
+ @keyframes ge-controls-slide-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+ }
+ /* User swiped the sheet down — keep tool active but hide its
+ controls. Re-tapping the same tool button toggles back. */
+ .ge-controls.dismissed {
+ transform: translateY(100%);
+ pointer-events: none;
+ /* Smooth slide-out matching the slide-in duration. */
+ animation: none;
+ transition: transform 0.18s ease-in;
+ }
+}
+
+/* ── Group Chat ────────────────────────────────────── */
+.msg-group .role {
+ font-weight: 600;
+}
+#group-toggle-btn.active,
+.overflow-menu-item#overflow-group-btn.active {
+ color: var(--red);
+ background: color-mix(in srgb, var(--red) 12%, transparent);
+}
+/* Group model picker — match app theme */
+#group-model-picker .modal-content {
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+}
+#group-model-picker .modal-header {
+ border-bottom: 1px solid var(--border);
+}
+#group-model-picker .memory-item {
+ border-radius: 6px;
+ transition: background 0.1s;
+}
+#group-model-picker .memory-item:hover {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
diff --git a/static/css/14-email.css b/static/css/14-email.css
new file mode 100644
index 000000000..56d1df6df
--- /dev/null
+++ b/static/css/14-email.css
@@ -0,0 +1,4729 @@
+/* 14-email - 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.
+ */
+#group-model-picker input[type="checkbox"] {
+ accent-color: var(--accent, var(--red));
+}
+#group-model-picker input[type="radio"] {
+ accent-color: var(--accent, var(--red));
+}
+#group-model-picker .btn-primary {
+ background: var(--accent, var(--red));
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-weight: 500;
+}
+#group-model-picker .btn-primary:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+#group-model-picker .btn-primary:hover:not(:disabled) {
+ filter: brightness(1.1);
+}
+
+/* ── Email document type ── */
+.doc-email-header {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 10px 12px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg);
+ flex-shrink: 0;
+}
+.email-field {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.email-field label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--fg);
+ opacity: 0.5;
+ min-width: 50px;
+ text-align: right;
+}
+.email-field input {
+ flex: 1;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 5px 8px;
+ font-size: 13px;
+ font-family: inherit;
+ color: var(--fg);
+ outline: none;
+}
+.email-field input:focus {
+ border-color: var(--accent, #4a9eff);
+}
+.email-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-end;
+ margin-top: 2px;
+}
+.email-draft-btn {
+ background: transparent;
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 6px 14px;
+ font-size: 12px;
+ cursor: pointer;
+ font-family: inherit;
+}
+.email-draft-btn:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
+.email-draft-btn:disabled { opacity: 0.5; cursor: not-allowed; }
+
+/* ── Email inbox list ── */
+.email-body { border-top: 1px solid var(--border); }
+.email-folder-bar { padding: 4px 8px; border-bottom: 1px solid var(--border); }
+.email-folder-select {
+ background: transparent; border: 1px solid var(--border); border-radius: 4px;
+ color: var(--fg); font-size: 11px; padding: 2px 6px; cursor: pointer; width: 100%; height: 26px;
+}
+.email-item {
+ display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px;
+ cursor: pointer; border-bottom: 1px solid var(--border); position: relative; transition: background 0.1s;
+}
+.email-item:hover { background: var(--hover-bg, rgba(255,255,255,0.03)); }
+.email-item-spam:hover { opacity: 0.75; }
+.email-tag-spam {
+ display: inline-flex; align-items: center; gap: 4px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
+ color: var(--accent, var(--red));
+ border-radius: 10px; padding: 1px 6px; font-size: 9px; font-weight: 600;
+ text-transform: lowercase; letter-spacing: 0.3px;
+}
+.email-spam-unflag {
+ background: none; border: none; color: inherit; cursor: pointer;
+ padding: 0 2px; font-size: 10px; line-height: 1; opacity: 0.6;
+}
+.email-spam-unflag:hover { opacity: 1; color: var(--fg); }
+/* Unread state is now a subtle icon, not full styling */
+.email-done-check {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 16px; height: 16px; border-radius: 50%; background: transparent;
+ border: 1.5px solid var(--border); color: transparent;
+ flex-shrink: 0; cursor: pointer; transition: all 0.15s;
+ margin-right: 4px;
+}
+.email-done-check:hover { border-color: var(--accent, #4a9eff); }
+.email-done-check.active { background: var(--accent, #4a9eff); border-color: var(--accent, #4a9eff); color: #fff; }
+.email-done-check.active svg { display: block; }
+.email-done-check svg { display: none; }
+
+
+
+
+.email-delete-busy > :not(.email-delete-overlay) {
+ opacity: 0.42;
+ transition: opacity 0.12s ease;
+}
+
+.email-delete-overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 8;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: inherit;
+ background: color-mix(in srgb, var(--panel, var(--bg)) 82%, transparent);
+ pointer-events: none;
+}
+
+.email-delete-overlay .spinner-whirlpool,
+.email-delete-overlay .ai-spinner-whirlpool {
+ margin: 0 !important;
+}
+
+.doclib-card.email-card-removing {
+ pointer-events: none !important;
+ max-height: 0 !important;
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ border-color: transparent !important;
+ opacity: 0;
+ transform: translateX(-10px) scale(0.985);
+ transition:
+ opacity 0.18s ease,
+ transform 0.22s ease,
+ max-height 0.23s ease,
+ padding 0.23s ease,
+ margin 0.23s ease,
+ border-color 0.18s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .doclib-card.email-card-removing {
+ transition: opacity 0.08s ease;
+ transform: none;
+ }
+}
+
+/* Library card done check — always visible, subtle icon next to title */
+.email-card-done {
+ display: inline-flex; align-items: center; justify-content: center;
+ flex-shrink: 0; cursor: pointer;
+ transition: opacity 0.15s, color 0.15s;
+ opacity: 0.15; color: var(--fg);
+}
+.email-card-titlerow .memory-item-title {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.email-card-titlerow { position: relative; min-width: 0; }
+.email-card-status {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 7px;
+ flex: 0 0 auto;
+ min-width: max-content;
+ margin-left: auto;
+ white-space: nowrap;
+}
+.email-card-status > * {
+ flex: 0 0 auto;
+}
+.email-card-status + .email-card-nav-arrows {
+ margin-left: 6px;
+}
+.email-card-status .email-card-nav-arrows {
+ margin-left: 2px;
+}
+.email-card-attachment {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 13px;
+ width: 13px;
+ height: 18px;
+}
+/* Hover preview: bright accent when unchecked so the user sees a check coming.
+ Once active, keep the exact same color on hover so the done state does not
+ visually flip while the pointer is still over it. */
+.email-card-done:not(.active):hover {
+ opacity: 0.75 !important;
+ color: var(--accent-primary, var(--red));
+}
+.email-card-done.active { opacity: 0.95; color: var(--accent-primary, var(--red)); }
+.email-card-done.active:hover {
+ opacity: 0.95 !important;
+ color: var(--accent-primary, var(--red)) !important;
+}
+.email-card-favorite {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 18px;
+ width: 18px;
+ height: 18px;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: var(--fg);
+ cursor: pointer;
+ position: relative;
+ top: -3px;
+ opacity: 0.18;
+ transition: opacity 0.15s, color 0.15s, transform 0.15s;
+}
+.email-card-favorite:hover,
+.email-card-favorite:focus-visible {
+ opacity: 0.8;
+ color: var(--accent-primary, var(--red));
+}
+.email-card-favorite.active {
+ opacity: 0.9;
+ color: var(--accent-primary, var(--red));
+}
+@keyframes check-pop {
+ 0% { transform: scale(1); }
+ 40% { transform: scale(1.7) rotate(-10deg); }
+ 70% { transform: scale(1.1) rotate(4deg); }
+ 100% { transform: scale(1) rotate(0); }
+}
+@keyframes check-unpop {
+ 0% { transform: scale(1); opacity: 0.95; }
+ 35% { transform: scale(0.45) rotate(8deg); opacity: 0.05; }
+ 65% { transform: scale(0.9); opacity: 0.2; }
+ 100% { transform: scale(1); }
+}
+
+/* Expanded email preview in library — override .memory-item row layout */
+.doclib-card.email-card-expanded.memory-item {
+ cursor: default !important;
+ background: var(--bg) !important;
+ border: 1px solid var(--border) !important;
+ display: flex !important;
+ flex-direction: column !important;
+ align-items: stretch !important;
+ /* Fill the available grid height instead of the old hardcoded 70vh —
+ the modal already caps height (90dvh on mobile, auto on desktop), so
+ the card just needs to flex into whatever remains. */
+ flex: 1 1 auto !important;
+ height: 100% !important;
+ min-height: 0 !important;
+ max-height: none !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ gap: 0 !important;
+}
+.doclib-card.email-card-expanded.memory-item > div:first-child {
+ /* The summary content (subject, date) - acts as the title bar. Left padding
+ trimmed 14→6px to shift the title 8px left total. */
+ padding: 2px 4px 10px !important; /* top trimmed to lift the header text up */
+ margin-top: -14px !important; /* pull the whole bar up into the freed space */
+ border-bottom: 1px solid var(--border);
+ flex: 0 0 auto !important;
+ width: 100%;
+}
+/* Keep the per-card '...' actions menu visible while the email card is
+ expanded — we used to hide it and surface a duplicate menu button in
+ the expanded header, but that was redundant. The bottom menu now
+ carries the actions in both states. */
+/* When expanded inline, show the subject a touch larger than the 12px card
+ title (close to the new-tab window header) without being oversized. */
+.doclib-card.email-card-expanded .memory-item-title {
+ font-size: 14px;
+ font-weight: 600;
+}
+/* The sender name is redundant once expanded (the From: row shows it just
+ below), so drop it and keep only the date — which then sits left-aligned
+ directly under the subject. Nudge the date 1px right to line up exactly with
+ the subject text, and up closer to it. */
+.doclib-card.email-card-expanded .email-meta-sender,
+.doclib-card.email-card-expanded .email-meta-sep {
+ display: none;
+}
+.doclib-card.email-card-expanded .memory-item-meta {
+ margin-top: -9px !important;
+}
+.doclib-card.email-card-expanded .email-meta-date {
+ margin-left: 3px;
+}
+.email-card-reader {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ font-size: 12px;
+}
+.email-card-reader-loading {
+ background: color-mix(in srgb, var(--bg) 86%, #000 14%);
+ border-top: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
+ min-height: 240px;
+}
+.email-card-reader-error {
+ background: color-mix(in srgb, var(--bg) 86%, #000 14%);
+ border-top: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
+}
+.email-reader-load-error {
+ display: flex;
+ min-height: 180px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 9px;
+ padding: 24px 16px;
+ text-align: center;
+ color: var(--fg);
+}
+.email-reader-load-error-title {
+ font-size: 13px;
+ font-weight: 700;
+}
+.email-reader-load-error-msg {
+ max-width: min(460px, 92%);
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ font-size: 11px;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+}
+.email-skeleton-line,
+.email-skeleton-dot {
+ display: block;
+ border-radius: 999px;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--fg) 12%, transparent) 0%,
+ color-mix(in srgb, var(--fg) 22%, transparent) 45%,
+ color-mix(in srgb, var(--fg) 12%, transparent) 90%
+ );
+ background-size: 220% 100%;
+ animation: email-skeleton-shimmer 1.35s ease-in-out infinite;
+}
+.email-skeleton-dot {
+ width: 9px;
+ height: 9px;
+ flex: 0 0 auto;
+}
+.email-list-skeleton {
+ position: absolute;
+ inset: 0;
+ padding: 10px 7px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ pointer-events: none;
+}
+.email-skeleton-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-height: 48px;
+ padding: 8px 10px;
+ border: 1px solid color-mix(in srgb, var(--border) 76%, transparent);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+}
+.email-skeleton-lines {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
+.email-reader-skeleton {
+ display: flex;
+ flex-direction: column;
+ min-height: 100%;
+ flex: 1;
+}
+.email-reader-skeleton-header {
+ min-height: 60px;
+ padding: 12px 14px;
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ border-bottom: 1px solid var(--border);
+ background: color-mix(in srgb, var(--panel) 72%, transparent);
+}
+.email-reader-skeleton-header .chip { width: 132px; height: 18px; }
+.email-reader-skeleton-header .chip.short { width: 78px; }
+.email-reader-skeleton-header .action {
+ width: 38px;
+ height: 28px;
+ margin-left: auto;
+ border-radius: 6px;
+}
+.email-reader-skeleton-header .action + .action { margin-left: 0; }
+.email-reader-skeleton-atts {
+ display: flex;
+ gap: 7px;
+ padding: 8px 14px;
+ border-bottom: 1px solid var(--border);
+}
+.email-reader-skeleton-atts .attachment { width: 126px; height: 20px; border-radius: 6px; }
+.email-reader-skeleton-atts .attachment.short { width: 88px; }
+.email-reader-skeleton-body {
+ display: flex;
+ flex-direction: column;
+ gap: 11px;
+ padding: 16px 14px;
+ min-height: 340px;
+ flex: 1;
+}
+.email-reader-skeleton-body .body { width: 88%; height: 9px; border-radius: 6px; }
+.email-reader-skeleton-body .body.wide { width: 100%; }
+.email-reader-skeleton-body .body.medium { width: 94%; }
+.email-reader-skeleton-body .body.short { width: 62%; }
+.email-reader-skeleton-body .body.gap {
+ margin-top: 12px;
+ width: 78%;
+}
+.email-search-progress-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ min-height: 30px;
+ padding: 6px 10px;
+ font-size: 11px;
+ opacity: 0.65;
+}
+.email-search-progress-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--accent-primary, var(--red));
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-primary, var(--red)) 35%, transparent);
+ animation: email-search-progress-pulse 1.2s ease-in-out infinite;
+}
+@media (prefers-reduced-motion: reduce) {
+ .email-skeleton-line,
+ .email-skeleton-dot,
+ .email-inline-image-skeleton,
+ .email-inline-image-placeholder.is-loading .email-inline-image-icon,
+ .email-inline-image-frame.is-loading,
+ .email-search-progress-dot {
+ animation: none;
+ }
+}
+/* Per-email unread dot in the expanded reader's title row. Background color
+ stays inline (per-sender hue from _senderColor), but the dot gets a soft
+ breathing glow + a 4px vertical nudge so it reads as centered against
+ the row instead of riding the baseline. */
+.email-card-unread-dot {
+ position: relative;
+ top: 0;
+ animation: email-card-unread-breathe 2.2s ease-in-out infinite;
+}
+.email-reader-header {
+ display: flex;
+ flex-direction: row;
+ /* Top-align so when the user expands To/Cc, the action cluster
+ stays on the From line above instead of drifting to the middle. */
+ align-items: flex-start;
+ gap: 12px;
+ /* Taller header so the 44px-tall absolute-positioned action
+ cluster inside the From row has vertical breathing room. */
+ padding: 8px 14px;
+ min-height: 60px;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg);
+ flex-shrink: 0;
+ /* Containing block for the absolute action cluster. */
+ position: relative;
+ /* Container so the action-cluster wrap breakpoints below react to
+ the reader's own width instead of the page viewport. */
+ container-type: inline-size;
+ container-name: emailreader;
+}
+.email-reader-header > .email-reader-actions {
+ margin-left: auto;
+ flex-shrink: 0;
+}
+/* Meta block stacks the From / To / Cc rows; each row keeps label
+ tight to its chip(s) on the same line. The recipient-chips inside
+ scroll horizontally so long lists slide under the floating action
+ cluster on the right. */
+.email-reader-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+ /* Positioning context for the absolute action cluster. */
+ position: relative;
+}
+.email-reader-meta .recipient-chips {
+ display: inline-flex;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ gap: 4px;
+ max-width: 100%;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+.email-reader-meta .recipient-chips::-webkit-scrollbar { display: none; }
+.email-reader-meta-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.email-reader-meta-row.email-reader-meta-from {
+ position: relative;
+ /* Never wrap "From:" off the chip onto its own line — the chip
+ span has overflow-x scroll built in, so it shrinks/scrolls
+ instead of jumping to a new row. */
+ flex-wrap: nowrap;
+ min-width: 0;
+ /* Reserve space on the right for the 1-row cluster in wide mode. */
+ padding-right: 320px;
+}
+.email-reader-meta > .email-reader-meta-details {
+ min-width: 0;
+ padding-right: 320px;
+}
+.email-reader-meta > .email-reader-actions-inline {
+ position: absolute;
+ top: 1px;
+ right: 0;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: flex-end;
+ gap: 4px;
+ z-index: 2;
+ /* Sit visually above the From baseline like the lifted overlay. */
+ margin-top: -9px;
+}
+/* Recipient chip span keeps at least ~4 characters visible (60px)
+ before the overflow-scroll kicks in, so the sender / first
+ recipient name is always partly readable. */
+.email-reader-meta .recipient-chips {
+ min-width: 60px;
+}
+/* Medium reader widths: cap the cluster at 3 icons per row so it
+ never wraps past 2 rows. min-width forces the cluster to reserve
+ enough room for 3 icons in a row — without it, grid col 2 could
+ shrink below the cap and the cluster fanned out into 4+ short
+ rows. */
+@container emailreader (max-width: 450px) {
+ .email-reader-meta > .email-reader-actions-inline {
+ /* 3 icons (48px each) + 2 gaps (4px each) = 152px exact. Adding
+ 6px slack so subpixel rounding can't tip a row from 3 to 2
+ icons and fan the cluster into a 3rd row. */
+ min-width: 158px;
+ max-width: 158px;
+ /* Solid background so the email body content behind the cluster
+ doesn't peek through the gaps between icons or the slack on
+ the right edge when the cluster's wrapped to 2 rows. */
+ background: var(--bg);
+ }
+ .email-reader-meta > .email-reader-actions-inline .memory-toolbar-btn.reader-icon-btn {
+ width: 48px;
+ min-width: 48px;
+ max-width: 48px;
+ height: 40px;
+ padding-left: 0;
+ padding-right: 0;
+ gap: 2px;
+ }
+ .email-reader-meta > .email-reader-actions-inline .reader-btn-label {
+ display: block;
+ max-width: 44px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 8px;
+ line-height: 1;
+ }
+ /* Tighten the right-side padding now that the cluster is 158px
+ wide (instead of the ~308 wide 1-row layout). */
+ .email-reader-meta-row.email-reader-meta-from,
+ .email-reader-meta > .email-reader-meta-details {
+ padding-right: 170px;
+ }
+ /* Cluster wrapped to 2 rows (44px + 4 gap + 44px = 92px) sits
+ absolute above the From baseline. Set min-height on the meta
+ (a descendant of the container, more reliable than targeting
+ the container itself) so the header grows enough to contain
+ the cluster + 4px of breathing room between the cluster's
+ bottom and the header bottom. */
+ .email-reader-meta {
+ min-height: 96px;
+ }
+}
+/* Very narrow reader: once the chip span has shrunk to its ~4
+ character min, the cluster snaps to absolute overlay with a soft
+ shadow so it visually floats above the truncated chip. */
+@container emailreader (max-width: 380px) {
+ /* Pane too narrow even for 158px of right-side reservation — kill
+ the padding so chips use the full width, and let the cluster
+ overlay via z-index + shadow. */
+ .email-reader-meta-row.email-reader-meta-from,
+ .email-reader-meta > .email-reader-meta-details {
+ padding-right: 0;
+ }
+ .email-reader-meta > .email-reader-actions-inline {
+ /* Keep the same -7px lift as the medium-width state so the
+ cluster doesn't slide ~4px DOWN when it transitions into
+ overlay mode. */
+ /* margin-top stays inherited at -7px from the base rule. */
+ /* 158px content + 22px padding-left so the inner row still fits
+ 3 icons cleanly. */
+ min-width: 180px;
+ max-width: 180px;
+ background: linear-gradient(to right, transparent 0, var(--bg) 18px);
+ padding-left: 22px;
+ /* Soft shadow on the left edge so the overlap reads as the
+ cluster "floating over" the chip. */
+ box-shadow: -6px 0 12px -6px rgba(0, 0, 0, 0.25);
+ }
+}
+/* Mobile: drop the box-shadow AND the left-edge gradient fade —
+ either can read as a shadow under the icons on phone screens.
+ Also lift the cluster 1px more. !important needed because the
+ @container query overlay rule has the same selector specificity. */
+@media (max-width: 768px) {
+ .memory-toolbar-btn[data-mode="ai-reply-fast"] {
+ height: 30px !important;
+ min-height: 30px !important;
+ }
+ .email-reader-meta > .email-reader-actions-inline {
+ box-shadow: none !important;
+ background: none !important;
+ padding-left: 0 !important;
+ margin-top: -10px;
+ }
+}
+/* Docked panes used to flow the cluster under the meta; now they
+ share the same absolute-top-right + wrap behaviour as undocked.
+ No docked-specific overrides — the container queries below pick
+ the right cluster size and overlay state from the reader width. */
+/* Gmail-style chevron — collapsed view shows only From; the chevron
+ reveals the To/Cc details inline below. */
+.email-reader-meta-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ margin-left: -3px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: inherit;
+ opacity: 0.55;
+ cursor: pointer;
+ border-radius: 4px;
+ flex-shrink: 0;
+}
+.email-reader-meta-toggle:hover { opacity: 1; background: color-mix(in srgb, currentColor 10%, transparent); }
+.email-reader-meta-toggle svg { transition: transform 0.15s ease; }
+.email-reader-meta-toggle.open svg { transform: rotate(180deg); }
+.email-reader-meta-toggle.email-reader-cc-toggle {
+ width: auto;
+ min-width: 18px;
+ gap: 3px;
+ margin-left: 1px;
+ padding: 0 5px;
+ color: var(--accent, var(--red));
+ font-family: inherit;
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1;
+}
+.email-reader-meta-details {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+/* When expanded, To/Cc details live in the header flow. A floating popover
+ can overlap the folded attachment strip directly beneath the header. */
+.email-reader-meta > .email-reader-meta-details:not([hidden]) {
+ position: relative;
+ top: auto;
+ left: auto;
+ right: auto;
+ margin-top: 4px;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ padding: 0;
+ box-shadow: none;
+ z-index: 1;
+}
+/* Reserve a fixed label width so From: / To: / Cc: all align and
+ their chips start at the same x. */
+.email-reader-meta-row strong { opacity: 0.5; font-weight: 600; flex-shrink: 0; min-width: 36px; }
+.email-reader-meta-row > span {
+ white-space: nowrap;
+ min-width: 0;
+}
+/* Recipient chips */
+.recipient-chips {
+ display: inline-flex !important; flex-wrap: wrap; gap: 4px;
+ white-space: normal !important; overflow: visible !important;
+ text-overflow: clip !important;
+}
+.recipient-chip {
+ display: inline-flex; align-items: center;
+ gap: 5px;
+ padding: 1px 8px; font-size: 10px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ color: var(--fg);
+ white-space: nowrap;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, max-width 0.2s;
+ max-width: 220px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.recipient-chip-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.recipient-chip-copy {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ top: -2px;
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: none;
+ background: none;
+ color: inherit;
+ opacity: 0.55;
+ cursor: pointer;
+ flex: 0 0 auto;
+}
+.recipient-chip-copy:hover,
+.recipient-chip-copy.copied {
+ opacity: 1;
+ color: var(--accent-primary, var(--red));
+}
+.recipient-chip-copy[hidden] {
+ display: none !important;
+}
+.recipient-chip:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 40%, transparent);
+}
+.recipient-chip.expanded {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 15%, transparent);
+ border-color: var(--accent-primary, var(--red));
+ max-width: 500px;
+}
+.recipient-chip.popover-open {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 15%, transparent);
+ border-color: var(--accent-primary, var(--red));
+}
+.recipient-chip-popover {
+ position: fixed;
+ z-index: 12050;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ max-width: min(420px, calc(100vw - 20px));
+ padding: 8px 10px;
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 45%, var(--border));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--bg) 96%, var(--fg) 4%);
+ color: var(--fg);
+ box-shadow: 0 14px 34px rgba(0, 0, 0, 0.42);
+ font-size: 12px;
+ line-height: 1.35;
+ pointer-events: auto;
+}
+.recipient-chip-popover-main {
+ min-width: 0;
+}
+.recipient-chip-popover-name {
+ font-weight: 650;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.recipient-chip-popover-detail {
+ overflow-wrap: anywhere;
+ opacity: 0.86;
+}
+.recipient-chip-popover-copy {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ color: inherit;
+ cursor: pointer;
+}
+.recipient-chip-popover-copy:hover,
+.recipient-chip-popover-copy.copied {
+ color: var(--accent-primary, var(--red));
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 55%, transparent);
+}
+@container docpane (max-width: 460px) {
+ .email-reader-header {
+ flex-direction: column;
+ gap: 6px;
+ }
+ .email-reader-header > .email-reader-actions {
+ /* On narrow widths fall back to in-flow so it doesn't overlap
+ the meta entirely. */
+ position: static;
+ align-self: flex-end;
+ background: none;
+ padding-left: 0;
+ }
+ .email-reader-meta-row:not(.email-reader-meta-from) {
+ padding-right: 0;
+ }
+ .email-reader-meta-row strong {
+ min-width: 0;
+ }
+ .recipient-chip {
+ max-width: 100%;
+ }
+}
+/* Mobile: long recipient lists (To/Cc with many addresses) shouldn't wrap to
+ N rows and push the body down. Keep them on one row, horizontally scrollable,
+ no scrollbar chrome. */
+@media (max-width: 768px) {
+ .recipient-chips {
+ flex-wrap: nowrap !important;
+ overflow-x: auto !important;
+ overflow-y: hidden !important;
+ scrollbar-width: none;
+ -webkit-overflow-scrolling: touch;
+ }
+ .recipient-chips::-webkit-scrollbar { display: none; }
+ .recipient-chip { flex-shrink: 0; }
+}
+.email-reader-actions {
+ display: flex; flex-direction: column; gap: 4px; align-items: flex-end;
+ flex-shrink: 0;
+ margin-top: -4px;
+}
+/* Two stacked rows: Reply/Reply-all/Forward on the From line, with
+ Summary/AI-reply/More on a row below. */
+.email-reader-actions-row {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 4px;
+}
+.email-reader-atts {
+ display: flex; flex-wrap: wrap; gap: 6px;
+ padding: 8px 14px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.email-reader-body {
+ font-size: 12px;
+ line-height: 1.55;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-wrap: anywhere;
+ flex: 1;
+ overflow-y: auto;
+ padding: 14px;
+ min-height: 0;
+ background: var(--panel);
+ box-shadow: inset 0 1px 0 color-mix(in srgb, var(--border) 65%, transparent);
+ /* Use the OS colored emoji font for incoming mail content */
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
+ font-variant-emoji: emoji;
+}
+.email-reader-body.html-body img { max-width: 100%; height: auto; }
+.email-reader-body.html-body [style*="max-width:600px"],
+.email-reader-body.html-body [style*="max-width: 600px"],
+.email-reader-body.html-body [style*="width:600px"],
+.email-reader-body.html-body [style*="width: 600px"],
+.email-reader-body.html-body table[width="600"] {
+ max-width: none !important;
+ width: 100% !important;
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+}
+.email-reader-body.html-body [align="center"] {
+ margin-left: 0 !important;
+ margin-right: 0 !important;
+}
+.email-reader-body .email-inline-image-placeholder {
+ display: inline-flex;
+ align-items: center;
+ gap: 9px;
+ max-width: min(100%, 360px);
+ min-height: 58px;
+ margin: 6px 0;
+ padding: 8px 10px;
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel) 72%, transparent);
+ color: var(--fg);
+ vertical-align: middle;
+}
+.email-inline-image-skeleton {
+ width: 58px;
+ height: 42px;
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--fg) 7%, transparent) 0%,
+ color-mix(in srgb, var(--fg) 13%, transparent) 45%,
+ color-mix(in srgb, var(--fg) 7%, transparent) 90%
+ );
+ background-size: 220% 100%;
+ animation: email-skeleton-shimmer 1.35s ease-in-out infinite;
+}
+.email-inline-image-icon {
+ opacity: 0.52;
+ display: inline-flex;
+}
+.email-inline-image-placeholder.is-loading .email-inline-image-icon svg {
+ display: none;
+}
+.email-inline-image-info {
+ display: inline-flex;
+ flex-direction: column;
+ min-width: 0;
+ line-height: 1.25;
+}
+.email-inline-image-title {
+ font-size: 11px;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-inline-image-sub {
+ font-size: 10px;
+ opacity: 0.58;
+}
+.email-inline-image-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ margin-left: auto;
+ flex: 0 0 auto;
+}
+.email-inline-image-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transform: translateY(-2px);
+ height: 24px;
+ line-height: 1;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ color: inherit;
+ font: inherit;
+ font-size: 10px;
+ text-decoration: none;
+ cursor: pointer;
+}
+.email-inline-image-btn:hover {
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 55%, var(--border));
+ color: var(--accent-primary, var(--red));
+}
+.email-inline-image-download-btn {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+}
+.email-inline-image-loaded {
+ max-width: 100%;
+ height: auto;
+ border-radius: 6px;
+}
+.email-inline-image-frame {
+ display: inline-flex;
+ max-width: 100%;
+ min-width: min(360px, 100%);
+ min-height: 90px;
+ margin: 6px 0;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--fg) 7%, transparent) 0%,
+ color-mix(in srgb, var(--fg) 13%, transparent) 45%,
+ color-mix(in srgb, var(--fg) 7%, transparent) 90%
+ );
+ background-size: 220% 100%;
+}
+.email-inline-image-frame .email-inline-image-loaded {
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+.email-inline-image-load-all {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ max-width: 100%;
+ margin: -4px 0 6px;
+ padding: 7px 9px;
+ border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--panel) 72%, transparent);
+ font-size: 11px;
+}
+.email-inline-image-load-all span {
+ opacity: 0.68;
+ margin-right: auto;
+}
+.email-inline-image-load-all button {
+ transform: translateY(-4px);
+ height: 24px;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ color: inherit;
+ font: inherit;
+ font-size: 10px;
+ cursor: pointer;
+}
+.email-inline-image-load-all button:hover {
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 55%, var(--border));
+ color: var(--accent-primary, var(--red));
+}
+@media (max-width: 520px) {
+ .email-reader-body .email-inline-image-placeholder {
+ display: flex;
+ width: 100%;
+ }
+ .email-inline-image-skeleton {
+ width: 48px;
+ }
+ .email-inline-image-actions {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
+.email-reader-body.html-body table { max-width: 100%; border-collapse: collapse; }
+/* Gmail Drive chips arrive with inline width/height/border/padding from
+ Gmail's composer (border:1px solid #ddd, white-ish bg assumed). On a
+ dark theme that reads as a near-invisible white box. Override with a
+ theme-aware chip surface so the filename + icon stay visible. */
+.email-reader-body .gmail_chip,
+.email-reader-body .gmail_drive_chip {
+ width: auto !important;
+ max-width: 100% !important;
+ height: auto !important;
+ max-height: none !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ gap: 6px;
+ padding: 6px 10px !important;
+ margin: 6px 0 !important;
+ border: 1px solid var(--border) !important;
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.email-reader-body .gmail_chip:hover,
+.email-reader-body .gmail_drive_chip:hover {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border)) !important;
+ background: color-mix(in srgb, var(--accent, var(--red)) 6%, transparent);
+}
+.email-reader-body .gmail_chip a,
+.email-reader-body .gmail_drive_chip a {
+ color: var(--accent, var(--red)) !important;
+ display: inline-flex !important;
+ align-items: center;
+ gap: 6px;
+ max-width: 100% !important;
+}
+.email-reader-body .gmail_chip a > span,
+.email-reader-body .gmail_drive_chip a > span {
+ color: var(--fg) !important;
+ font-size: 12px;
+}
+.email-reader-body .gmail_chip img,
+.email-reader-body .gmail_drive_chip img {
+ width: 16px !important;
+ height: 16px !important;
+ padding-right: 0 !important;
+ filter: none;
+}
+
+/* Force any phone/sms/date/address auto-link to inherit the body color so
+ numbers don't render as bright-blue browser-default text inside email
+ content. format-detection in turns off detection on iOS, but some
+ mail clients ship pre-wrapped tel: links — those still need taming. */
+.email-reader-body a[href^="tel:"],
+.email-reader-body a[href^="sms:"],
+.email-reader-body a[x-apple-data-detectors],
+.email-bubble-body a[href^="tel:"],
+.email-bubble-body a[href^="sms:"],
+.email-bubble-body a[x-apple-data-detectors],
+.email-thread-turn-body a[href^="tel:"],
+.email-thread-turn-body a[href^="sms:"],
+.email-thread-turn-body a[x-apple-data-detectors] {
+ color: inherit !important;
+ text-decoration: none !important;
+ pointer-events: auto;
+}
+.email-reader-body a,
+.email-bubble-body a,
+.email-thread-turn-body a {
+ color: var(--accent-primary, var(--red));
+ text-decoration: underline;
+ overflow-wrap: anywhere;
+ word-break: normal;
+ cursor: pointer;
+}
+.email-reader-body a:hover,
+.email-bubble-body a:hover,
+.email-thread-turn-body a:hover { opacity: 0.8; }
+/* Summary block — same band chrome as Attachments / Signature / Earlier-
+ thread (no accent tint, no rounded corners, no leading star icon). The
+ only difference is the label, so all collapsible sections in the email
+ reader look uniform. */
+.email-summary-panel {
+ margin: 0 0 12px 0;
+ padding: 10px 12px;
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 8%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 30%, transparent);
+ border-radius: 6px;
+}
+.email-summary-header {
+ display: flex; align-items: center; gap: 5px;
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
+ color: var(--accent-primary, var(--red)); opacity: 0.85;
+ margin-bottom: 6px; letter-spacing: 0.4px;
+}
+.email-summary-content {
+ font-size: 12px; line-height: 1.5; color: var(--fg); white-space: pre-wrap;
+}
+/* Click-to-fold: tap the summary header to collapse/expand. The chevron
+ flips when collapsed, the content hides + the panel's bottom margin
+ tightens so a folded summary doesn't take up vertical space. */
+.email-summary-toggle { cursor: pointer; user-select: none; }
+.email-summary-panel.collapsed { padding-bottom: 6px; margin-bottom: 6px; }
+.email-translation-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 26px;
+}
+.email-translation-busy {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+}
+.email-translation-spinner {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+}
+.email-translation-loading-text {
+ opacity: 0.68;
+ font-size: 12px;
+}
+.email-translation-panel .email-translation-icon {
+ color: var(--accent-primary, var(--red));
+}
+
+/* Foldable attachments — same fold-on-click UX as the summary panel. */
+.email-reader-atts-wrap {
+ display: flex; flex-direction: column;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+.email-reader-atts-header {
+ display: flex; align-items: center; gap: 5px;
+ padding: 6px 14px;
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
+ color: var(--fg); opacity: 0.7;
+ cursor: pointer; user-select: none;
+ letter-spacing: 0.4px;
+}
+.email-attachments-download-all {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ height: 22px;
+ padding: 0 8px;
+ border-radius: 999px;
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 35%, transparent);
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 9%, transparent);
+ color: var(--accent-primary, var(--red));
+ font: inherit;
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: none;
+ letter-spacing: 0;
+ cursor: pointer;
+ flex: 0 0 auto;
+}
+.email-attachments-download-all:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 60%, transparent);
+}
+.email-attachments-download-all .spinner-whirlpool,
+.email-attachments-download-all .ai-spinner-whirlpool {
+ width: 12px !important;
+ height: 12px !important;
+ margin: 0 !important;
+}
+.email-reader-atts-wrap > .email-reader-atts {
+ border-bottom: none !important;
+}
+.email-reader-atts-hidden-note {
+ padding: 0 14px 6px;
+ font-size: 10px;
+ color: var(--fg-muted);
+ opacity: 0.65;
+}
+
+/* Quote fold = neutral full-width band (matches attachments header). */
+.email-quote-fold {
+ margin: 0 -14px;
+ padding: 0;
+ border: 0;
+ border-top: 1px solid var(--border) !important;
+ border-radius: 0;
+ outline: 0 !important;
+ box-shadow: none !important;
+ /* Neutralise any rich-mail HTML that injects a yellow / colored bg
+ or border onto blockquote ancestors. */
+ background: transparent !important;
+}
+/* Same neutralisation for any element nested inside the fold (the inner
+ blockquote, paragraphs, divs from the original message). Yellow outlines
+ in Outlook-style mails almost always come from inline border-color or
+ box-shadow on these. */
+.email-quote-fold *,
+.email-quote-fold *::before,
+.email-quote-fold *::after {
+ outline-color: transparent !important;
+ box-shadow: none !important;
+}
+.email-quote-fold blockquote,
+.email-quote-fold blockquote[type="cite"],
+.email-quote-fold table,
+.email-quote-fold div {
+ border-color: var(--border) !important;
+ /* Override the .msg blockquote rule that sets a colored left border via
+ var(--hl-function) — that's where the "yellow" outline was coming from. */
+ border-left-color: var(--border) !important;
+ border-radius: 0 !important;
+ background: transparent !important;
+}
+/* JS marks the last .email-quote-fold in each email body with this class,
+ so it's the only one that gets rounded bottom corners. Avoids :has()
+ browser-support quirks. */
+.email-quote-fold.last-fold {
+ border-radius: 0 0 8px 8px;
+ overflow: hidden;
+}
+/* ── Threaded reply turns (recursive parser) ── */
+.email-thread-turn-body {
+ padding: 8px 14px 4px;
+ font-size: 12px;
+ line-height: 1.45;
+}
+/* Nested turns indent slightly + get their own card outline so the
+ reply hierarchy is obvious at a glance. */
+.email-thread-turn-body .email-thread-turn {
+ margin: 8px 0 4px;
+ margin-left: 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 2%, transparent);
+}
+/* The original blockquote's left stripe is redundant when each turn is
+ already a card — drop it inside thread bodies. */
+.email-thread-turn-body blockquote {
+ border-left: none !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: transparent !important;
+}
+/* Flatten the inner blockquote: no rounded corners, no yellow tint
+ inherited from rich-mail HTML. Subtle left border + neutral bg. */
+.email-quote-fold blockquote {
+ margin: 0;
+ padding: 8px 12px;
+ border: 0;
+ border-left: 2px solid var(--border);
+ border-radius: 0 !important;
+ background: transparent !important;
+ color: inherit;
+}
+/* "From … · Date …" chip appended to the summary line */
+.email-fold-summary-meta {
+ margin-left: 6px;
+ font-size: 10px;
+ font-weight: 500;
+ text-transform: none;
+ letter-spacing: 0;
+ opacity: 0.55;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+/* Sender name is the primary affordance — slightly larger and not all-caps so
+ "From: Sam" feels like a click target on the person, not a section label. */
+.email-fold-summary-name {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: none;
+ letter-spacing: 0.1px;
+ color: var(--fg);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+}
+.email-quote-fold .email-fold-summary {
+ display: flex; align-items: center; gap: 5px;
+ padding: 6px 14px;
+ cursor: pointer; list-style: none;
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
+ letter-spacing: 0.4px;
+ /* Force neutral page colors regardless of any rich-mail HTML wrapping us */
+ color: var(--fg) !important;
+ background: transparent !important;
+ background-color: transparent !important;
+ text-shadow: none !important;
+ font-style: normal !important;
+ opacity: 0.7;
+ user-select: none;
+ border-radius: 0;
+}
+.email-quote-fold .email-fold-summary > * {
+ /* Prevent inline-color attrs on summary children (icon SVGs, meta span)
+ from picking up a forced color from ancestor email HTML. */
+ color: inherit !important;
+ background: transparent !important;
+}
+.email-quote-fold .email-fold-summary { list-style: none; list-style-type: none; }
+.email-quote-fold[open] > *:not(summary) {
+ padding: 8px 14px 12px 14px;
+ border-top: 1px solid var(--border);
+}
+
+/* Signature fold = clean full-width band like the attachments header. No
+ accent overlay, no rounded corners — neutral chrome so it doesn't look
+ like the AI Summary panel. */
+.email-sig-fold {
+ margin: 8px 0 0;
+ padding: 0;
+ border: 0;
+ border-top: 1px dashed color-mix(in srgb, var(--fg) 18%, transparent);
+ background: transparent;
+ border-radius: 0;
+}
+.email-sig-fold .email-fold-summary {
+ display: flex; align-items: center; gap: 5px;
+ padding: 4px 0;
+ cursor: pointer;
+ list-style: none; list-style-type: none;
+ font-size: 9.5px; font-weight: 600; text-transform: uppercase;
+ letter-spacing: 0.4px;
+ color: var(--fg); opacity: 0.55;
+ user-select: none;
+}
+.email-sig-fold[open] > *:not(summary) {
+ padding: 6px 0 0;
+ border-top: 0;
+ font-size: 11px; line-height: 1.45;
+ color: color-mix(in srgb, var(--fg) 75%, transparent);
+ white-space: pre-wrap;
+}
+
+/* ── Chat-bubble layout for email threads ──
+ Each parsed turn becomes a bubble. The active account's outgoing
+ replies align right (mine); everyone else aligns left (theirs).
+ Bubbles read top→bottom oldest→newest, like a chat. */
+.email-bubbles {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 4px 0 8px;
+ margin: 0 -2px;
+}
+.email-bubble-row {
+ display: flex;
+ align-items: flex-end;
+ gap: 8px;
+ width: 100%;
+ min-width: 0;
+}
+.email-bubble {
+ flex: 0 1 auto;
+ max-width: 90%;
+ min-width: 0;
+ padding: 8px 12px;
+ border: 1px solid var(--border);
+ background: var(--panel);
+ font-size: 12.5px;
+ line-height: 1.5;
+ word-wrap: break-word;
+ overflow-wrap: anywhere;
+ overflow: hidden;
+}
+.email-bubble-mine .email-bubble {
+ background: color-mix(in srgb, var(--bubble-accent, var(--fg)) 10%, var(--bg));
+ border-color: color-mix(in srgb, var(--bubble-accent, var(--border)) 35%, var(--border));
+ border-radius: 14px 14px 4px 14px;
+}
+.email-bubble-theirs .email-bubble {
+ background: color-mix(in srgb, var(--bubble-accent, var(--fg)) 5%, var(--panel));
+ border-color: color-mix(in srgb, var(--bubble-accent, var(--border)) 25%, var(--border));
+ border-radius: 14px 14px 14px 4px;
+}
+.email-bubble-head {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ font-size: 10.5px;
+ margin-bottom: 4px;
+ opacity: 0.7;
+ min-width: 0;
+}
+.email-bubble-author {
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 50ch;
+}
+.email-bubble-date {
+ opacity: 0.7;
+ font-weight: 400;
+ white-space: nowrap;
+}
+.email-bubble-body {
+ white-space: normal;
+ /* Reader-controlled typography — override sender's inline styles AND
+ attribute-based sizing (, , , h1..h6). Weight
+ and italics still pass through for emphasis. */
+ font-family: inherit;
+ font-size: var(--email-body-size, 13.5px);
+ line-height: 1.5;
+}
+/* Heading tags carry browser-default font-size multipliers that bypass
+ `font-size: inherit` when an inline style was set; pin them too. */
+.email-bubble-body h1,
+.email-bubble-body h2,
+.email-bubble-body h3,
+.email-bubble-body h4,
+.email-bubble-body h5,
+.email-bubble-body h6,
+.email-bubble-body big,
+.email-bubble-body small,
+.email-bubble-body font {
+ font-size: inherit !important;
+ line-height: inherit !important;
+}
+/* Sender HTML often wraps content in a fixed-width
+ or
. Override so content fills the reader's available
+ width — otherwise resizing the email window doesn't reflow anything. */
+.email-bubble-body,
+.email-bubble-body * {
+ max-width: 100% !important;
+}
+.email-bubble-body div[style*="width"],
+.email-bubble-body table[width],
+.email-bubble-body table {
+ width: 100% !important;
+ box-sizing: border-box !important;
+}
+.email-bubble-body blockquote {
+ border-left: 2px solid var(--border) !important;
+ margin: 4px 0 !important;
+ padding: 4px 10px !important;
+ background: transparent !important;
+ border-radius: 0 !important;
+ color: inherit;
+}
+.email-bubble-avatar {
+ flex-shrink: 0;
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ /* Per-sender color set inline (`style="background:hsl(...)"`); this
+ fallback only kicks in when JS hasn't assigned one. */
+ background: color-mix(in srgb, var(--fg) 14%, transparent);
+ color: #fff;
+ text-shadow: 0 1px 0 rgba(0, 0, 0, 0.22);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 9.5px;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+ user-select: none;
+}
+/* Sig folds inside a bubble: tone down — no full-bleed band, since the
+ bubble already provides the chrome. */
+.email-bubble-body .email-sig-fold {
+ margin: 8px 0 0;
+ border-top: 1px dashed var(--border);
+}
+.email-bubble-body .email-sig-fold[open] > *:not(summary),
+.email-bubble-body .email-quote-fold[open] > *:not(summary) {
+ padding: 6px 0 0;
+ border-top: 0;
+}
+@media (max-width: 600px) {
+ .email-bubble { max-width: 96%; }
+ .email-bubble-avatar { display: none; }
+}
+
+/* ── Schedule Send modal ── */
+.schedule-send-body {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 16px 18px 12px;
+}
+.schedule-send-label {
+ font-size: 11px;
+ font-weight: 500;
+ letter-spacing: 0.3px;
+ text-transform: uppercase;
+ color: var(--fg);
+ opacity: 0.55;
+ margin-top: 6px;
+}
+.schedule-send-label:first-child { margin-top: 0; }
+.schedule-send-presets {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ margin-bottom: 4px;
+}
+.schedule-send-presets .memory-toolbar-btn {
+ padding: 5px 10px 11px;
+ font-size: 12px;
+ justify-content: center;
+ line-height: 1;
+}
+.schedule-send-confirm svg {
+ width: 12px;
+ height: 12px;
+ vertical-align: -1px;
+ margin-right: 6px;
+}
+.schedule-send-datetime {
+ font-size: 13px;
+ padding: 8px 10px;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ outline: none;
+ font-family: inherit;
+ width: 100%;
+ box-sizing: border-box;
+}
+.schedule-send-datetime:focus {
+ border-color: var(--hl-function);
+}
+.schedule-send-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 18px 14px;
+ border-top: 1px solid var(--border);
+ margin-top: 4px;
+}
+.schedule-send-confirm {
+ background: var(--accent-primary, var(--red)) !important;
+ color: #fff !important;
+ border-color: var(--accent-primary, var(--red)) !important;
+}
+.schedule-send-confirm:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 85%, white) !important;
+}
+
+/* Senders embed inline colors in their HTML that clash with the app's
+ theme (black text in dark mode, white backgrounds, yellow highlights,
+ etc.). Force theme colors on every descendant of the rendered body so
+ emails inherit the user's chosen palette regardless of what the
+ sender's mail client emits. */
+.email-reader-body *,
+.email-bubble-body * {
+ color: var(--fg) !important;
+ -webkit-text-fill-color: currentColor !important;
+ background-color: transparent !important;
+ background-image: none !important;
+}
+.email-reader-body a,
+.email-bubble-body a,
+.email-thread-turn-body a {
+ color: var(--hl-function) !important;
+}
+.email-reader-body a[href^="tel:"],
+.email-reader-body a[href^="sms:"],
+.email-reader-body a[href^="x-apple-data-detectors:"],
+.email-reader-body a[x-apple-data-detectors],
+.email-bubble-body a[href^="tel:"],
+.email-bubble-body a[href^="sms:"],
+.email-bubble-body a[href^="x-apple-data-detectors:"],
+.email-bubble-body a[x-apple-data-detectors],
+.email-thread-turn-body a[href^="tel:"],
+.email-thread-turn-body a[href^="sms:"],
+.email-thread-turn-body a[href^="x-apple-data-detectors:"],
+.email-thread-turn-body a[x-apple-data-detectors] {
+ color: inherit !important;
+ -webkit-text-fill-color: currentColor !important;
+ text-decoration: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ -webkit-background-clip: border-box !important;
+ background-clip: border-box !important;
+}
+
+/* Prefer local/system emoji fonts; avoid remote font providers. */
+.doc-editor-textarea, .doc-editor-highlight, #doc-editor-code,
+.doc-md-preview, .doc-csv-preview {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', monospace;
+ font-variant-emoji: text;
+}
+/* Preview panes claim the editor's space so the action footer stays pinned
+ at the bottom of the pane even when the rendered content is short
+ (shorter table, single-paragraph markdown, etc). */
+.doc-csv-preview,
+.doc-md-preview {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: auto;
+}
+
+/* Email documents override: use colored system emoji in compose. */
+.doc-editor-textarea.email-mode,
+#doc-editor-code.email-mode,
+#doc-editor-code.email-mode .doc-editor-highlight,
+#doc-editor-code.email-mode .doc-editor-textarea {
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla" !important;
+ font-variant-emoji: emoji;
+}
+
+/* Single-layer rendering for ALL documents (not just email): the highlight
+ overlay ( with hljs spans) and the transparent textarea use two
+ different rendering paths internally — the textarea uses the browser's
+ native form line-breaker, the overlay uses CSS line-breaking — and they
+ can never be guaranteed to wrap byte-identical no matter how many CSS
+ properties are pinned. Pick one source of truth: make the textarea its
+ own visible text, hide the overlay. Trade-off: no syntax highlighting in
+ the live editor (rendered markdown preview / chat output still has it).
+ The caret is now glued to the typed text by definition since both are
+ rendered by the same element. */
+.doc-editor-highlight,
+#doc-editor-code.email-mode .doc-editor-highlight {
+ display: none !important;
+}
+/* Find bar is open: float the highlight overlay ON TOP of the
+ textarea with transparent background + transparent text, so only
+ the solid spans are visible. pointer-events
+ off so clicks still go through to the textarea below. */
+body.doc-find-active .doc-editor-highlight,
+body.doc-find-active #doc-editor-code.email-mode .doc-editor-highlight {
+ display: block !important;
+ z-index: 5 !important;
+ background: transparent !important;
+ pointer-events: none;
+}
+body.doc-find-active .doc-editor-highlight,
+body.doc-find-active .doc-editor-highlight * {
+ color: transparent !important;
+ background: transparent !important;
+}
+.doc-editor-textarea,
+#doc-editor-code.email-mode .doc-editor-textarea {
+ color: var(--fg) !important;
+ caret-color: var(--fg) !important;
+ z-index: 1;
+}
+
+/* Emoji picker (monochrome icon picker) */
+.emoji-picker-btn {
+ background: none; border: none; color: var(--fg); opacity: 0.35;
+ padding: 4px 7px; cursor: pointer; min-height: 28px;
+ display: inline-flex; align-items: center; justify-content: center;
+ transition: opacity 0.1s;
+}
+.emoji-picker-btn:hover { opacity: 1; }
+.emoji-picker {
+ width: min(300px, 92vw); max-height: min(300px, 55vh);
+ background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ display: flex; flex-direction: column; overflow: hidden;
+}
+.emoji-picker-search {
+ padding: 8px 12px; background: var(--panel); border: none;
+ border-bottom: 1px solid var(--border); color: var(--fg);
+ font-size: 12px; font-family: inherit; outline: none;
+}
+.emoji-picker-groups { overflow-y: auto; padding: 4px 0; flex: 1; }
+.emoji-picker-group { padding: 0 6px 4px 6px; }
+.emoji-picker-group-name {
+ font-size: 10px; font-weight: 600; opacity: 0.5;
+ text-transform: uppercase; letter-spacing: 0.5px;
+ padding: 4px 4px 2px 4px;
+}
+.emoji-picker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 1px; }
+.emoji-picker-item {
+ background: none; border: none; cursor: pointer;
+ padding: 4px; border-radius: 4px;
+ display: flex; align-items: center; justify-content: center;
+ color: var(--fg); transition: background 0.1s;
+}
+.emoji-picker-item:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }
+.emoji-picker-item svg { width: 16px; height: 16px; }
+
+.email-avatar {
+ width: 28px; height: 28px; border-radius: 50%; background: var(--accent, #4a9eff);
+ color: #fff; display: flex; align-items: center; justify-content: center;
+ font-size: 12px; font-weight: 600; flex-shrink: 0; margin-top: 1px;
+}
+.email-item-content { flex: 1; min-width: 0; overflow: hidden; }
+.email-item-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
+.email-sender { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.email-filter-chip {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 6px;
+ margin: 4px 8px 6px;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 11px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
+ color: var(--fg);
+}
+.email-filter-chip-clear {
+ background: none;
+ border: none;
+ color: inherit;
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 1;
+ padding: 0 2px;
+ opacity: 0.6;
+}
+.email-date { font-size: 10px; opacity: 0.5; white-space: nowrap; flex-shrink: 0; }
+.email-subject { font-size: 11px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
+.email-sent-chip,
+.email-folder-chip {
+ display: inline-flex;
+ align-items: center;
+ height: 14px;
+ padding: 0 5px;
+ margin-right: 5px;
+ border-radius: 999px;
+ font-size: 8px;
+ line-height: 1;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
+ vertical-align: 1px;
+}
+.email-folder-chip {
+ position: relative;
+ top: 4px;
+ color: color-mix(in srgb, var(--fg) 75%, transparent);
+ background: rgba(127, 127, 127, 0.13);
+ border-color: rgba(127, 127, 127, 0.24);
+}
+.email-meta-date-group {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ white-space: nowrap;
+}
+.email-meta-date-group .email-folder-chip {
+ margin-right: 0;
+}
+.email-card-expanded .email-sent-chip,
+.email-card-reader .email-sent-chip {
+ position: relative;
+ top: 4px;
+}
+.email-tags { display: inline-flex; align-items: center; flex-wrap: nowrap; gap: 3px; margin-left: 6px; vertical-align: middle; position: relative; top: -2px; }
+.email-card-tags {
+ flex: 0 0 auto;
+ min-width: 0;
+ max-width: 48%;
+ position: relative;
+ top: -4px;
+}
+.email-card-expanded .email-tags,
+.email-card-reader .email-tags { top: -4px; }
+.doclib-card-expanded .email-card-tags { top: -4px; }
+.email-card-expanded .email-card-titlerow .email-tag,
+.email-card-expanded .email-card-titlerow .email-tags-more {
+ transform: translateY(-2px);
+}
+@media (max-width: 768px) {
+ .email-card-expanded .email-card-titlerow .email-tag,
+ .email-card-expanded .email-card-titlerow .email-tags-more {
+ transform: translateY(2px);
+ }
+}
+.email-card-tags.email-tags-expanded {
+ position: absolute;
+ z-index: 4;
+ right: calc(var(--email-status-width, 44px) + 6px);
+ top: -5px;
+ max-width: min(58%, 260px);
+ padding: 4px 5px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel, var(--bg));
+ box-shadow: 0 5px 14px color-mix(in srgb, #000 20%, transparent);
+}
+.email-tags-more {
+ appearance: none;
+ border: 0;
+ box-sizing: border-box;
+ border-radius: 999px;
+ background: rgba(127, 127, 127, 0.16);
+ color: var(--fg);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 1px;
+ height: 13px !important;
+ min-height: 0 !important;
+ min-width: 23px;
+ padding: 1px 4px;
+ font-family: inherit;
+ font-size: 9px;
+ line-height: 1;
+ margin: 0;
+ opacity: 0.68;
+ position: relative;
+ top: 0;
+ vertical-align: middle;
+}
+.email-tags-more svg { display: block; flex: 0 0 auto; }
+.email-tags-expanded .email-tags-more svg { transform: rotate(180deg); }
+.email-tags-toggle-inline {
+ position: absolute;
+ right: 94px;
+ top: 50%;
+ transform: translateY(calc(-50% - 5px));
+ height: 20px !important;
+ min-width: 34px;
+ padding: 2px 6px !important;
+ font-size: 10px !important;
+ line-height: 1 !important;
+ opacity: 0.62;
+ border-radius: 999px !important;
+}
+.email-tags-toggle-inline.active {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 36%, transparent);
+}
+.email-tags-toggle-inline.email-tags-empty {
+ color: var(--warning, #d6b35a);
+ background: color-mix(in srgb, var(--warning, #d6b35a) 12%, transparent);
+ border-color: color-mix(in srgb, var(--warning, #d6b35a) 34%, transparent);
+}
+.email-tag {
+ display: inline-block;
+ font-size: 9px;
+ line-height: 1;
+ padding: 2px 5px;
+ border-radius: 8px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.3px;
+ background: rgba(127, 127, 127, 0.18);
+ color: var(--fg);
+}
+.email-tag-clickable {
+ appearance: none;
+ border: 0;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ font-family: inherit;
+ font-size: 9px;
+ font-weight: 500;
+ height: 13px !important;
+ min-height: 0 !important;
+ line-height: 1;
+ margin: 0;
+ padding: 2px 5px !important;
+ text-transform: uppercase;
+ vertical-align: middle;
+ flex: 0 0 auto;
+}
+.email-tag-work { background: rgba(96, 165, 250, 0.22); color: #60a5fa; }
+.email-tag-personal { background: rgba(74, 222, 128, 0.22); color: #4ade80; }
+.email-tag-finance { background: rgba(250, 204, 21, 0.22); color: #facc15; }
+.email-tag-bills { background: rgba(251, 146, 60, 0.22); color: #fb923c; }
+.email-tag-receipt { background: rgba(251, 146, 60, 0.22); color: #fb923c; }
+.email-tag-travel { background: rgba(167, 139, 250, 0.22); color: #a78bfa; }
+.email-tag-newsletter { background: rgba(148, 163, 184, 0.22); color: #94a3b8; }
+.email-tag-promo,
+.email-tag-marketing { background: rgba(244, 114, 182, 0.22); color: #f472b6; }
+.email-tag-notification { background: rgba(148, 163, 184, 0.22); color: #94a3b8; }
+.email-tag-security { background: rgba(248, 113, 113, 0.22); color: #f87171; }
+.email-tag-urgent { background: color-mix(in srgb, var(--color-error, #e06c75) 25%, transparent); color: var(--color-error, #e06c75); font-weight: 600; }
+.email-tag-reply-soon { background: rgba(240, 173, 78, 0.22); color: #f0ad4e; }
+.email-tag-social { background: rgba(56, 189, 248, 0.22); color: #38bdf8; }
+.email-tag-shopping { background: rgba(236, 72, 153, 0.22); color: #ec4899; }
+.email-tag-calendar { background: rgba(167, 139, 250, 0.22); color: #a78bfa; }
+.email-menu-wrap { position: relative; flex-shrink: 0; }
+.email-menu-btn {
+ background: none; border: none; color: var(--fg); opacity: 0; cursor: pointer;
+ padding: 4px; transition: opacity 0.15s;
+ position: relative; top: 0px;
+}
+.email-dropdown {
+ position: absolute; right: 0; top: 100%; z-index: 9999;
+ min-width: 120px; background: var(--bg); border: 1px solid var(--border);
+ border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); padding: 4px 0;
+}
+.email-loading { padding: 16px; text-align: center; font-size: 12px; opacity: 0.5; }
+.email-load-more { padding: 8px; text-align: center; }
+.email-load-more-btn {
+ background: transparent; border: 1px solid var(--border); border-radius: 4px;
+ color: var(--fg); padding: 4px 12px; font-size: 11px; cursor: pointer;
+}
+.email-load-more-btn:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
+.email-spinner {
+ display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.3);
+ border-top-color: #fff; border-radius: 50%; animation: email-spin 0.6s linear infinite;
+ vertical-align: middle; margin-right: 4px;
+}
+
+/* Email section doesn't collapse — hide the auto-injected chevron */
+#email-section .section-collapse-btn { display: none !important; }
+
+#email-section:hover #email-compose-btn {
+ opacity: 0.7 !important;
+}
+#email-compose-btn:hover {
+ opacity: 1 !important;
+ background: none !important;
+}
+#email-compose-btn svg {
+ width: 11px;
+ height: 11px;
+ transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+#email-compose-btn .list-item-plus-label {
+ top: calc(50% - 0.5px);
+ color: var(--fg);
+ font-size: 10px;
+}
+#email-compose-btn:hover svg {
+ transform: rotate(180deg) scale(1.15);
+}
+
+/* Library row "+" — identical to the email compose "+": hidden until the row
+ is hovered, no accent box, and the same rotate-in animation. */
+#library-new-doc-btn,
+#notes-new-note-btn {
+ opacity: 0.4 !important;
+ transition: opacity 0.15s ease;
+}
+#tool-library-btn:hover #library-new-doc-btn,
+#tool-notes-btn:hover #notes-new-note-btn {
+ opacity: 0.7 !important;
+}
+#library-new-doc-btn:hover,
+#notes-new-note-btn:hover {
+ opacity: 1 !important;
+ background: none !important;
+}
+#library-new-doc-btn svg,
+#notes-new-note-btn svg {
+ transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+#library-new-doc-btn .list-item-plus-label,
+#notes-new-note-btn .list-item-plus-label {
+ top: calc(50% + 1px);
+}
+#library-new-doc-btn:hover svg,
+#notes-new-note-btn:hover svg {
+ transform: rotate(180deg) scale(1.15);
+}
+
+
+/* MD toolbar attach button — matches other toolbar buttons */
+.md-toolbar-attach-btn {
+ background: none; border: none; color: var(--fg); opacity: 0.35;
+ padding: 4px 7px; cursor: pointer; min-height: 28px;
+ display: inline-flex; align-items: center; justify-content: center;
+ transition: opacity 0.1s;
+}
+.md-toolbar-attach-btn:hover { opacity: 1; }
+
+/* Email reader icon buttons — vertical icon + label stack. */
+.memory-toolbar-btn.reader-icon-btn {
+ width: auto;
+ height: 44px;
+ /* padding: 4px 2px; */
+ position: relative;
+ top: 1px;
+ display: inline-flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ flex: 0 0 auto;
+}
+.memory-toolbar-btn.reader-icon-btn svg { width: 18px; height: 18px; }
+.memory-toolbar-btn.reader-icon-btn[data-act="reply"] svg {
+ color: inherit;
+ stroke: currentColor;
+}
+.memory-toolbar-btn.reader-icon-btn.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
+ color: var(--accent, var(--red));
+}
+.memory-toolbar-btn.reader-icon-btn[data-act="more"] {
+ color: inherit;
+}
+.memory-toolbar-btn.reader-icon-btn[data-act="more"] svg { fill: currentColor; }
+.memory-toolbar-btn.reader-icon-btn.reader-more-active {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 24%, transparent);
+ color: inherit;
+}
+/* Cookbook Serve dropdown triggers (the cached-model kebab + the saved-config
+ arrow) — accent-colored to signal they open a menu, with a filled tint when
+ their dropdown is open. Matches the email reader More button pattern. */
+.hwfit-cached-menu-btn,
+.cookbook-saved-arrow {
+ color: var(--accent, var(--red));
+ /* Base .memory-item-btn is flex with align-items but no justify-content,
+ so the kebab SVG sat left-aligned inside the button instead of being
+ horizontally centered. Pin it center. */
+ justify-content: center;
+}
+.hwfit-cached-menu-btn.cookbook-menu-active,
+.cookbook-saved-arrow.cookbook-menu-active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
+ color: var(--accent, var(--red));
+}
+
+/* Card action menus share one hit target and baseline. Keep this late in the
+ cascade because modal buttons and Cookbook Serve add their own offsets. */
+.memory-item-actions > .memory-item-btn,
+.memory-item-actions .hwfit-cached-menu-btn {
+ box-sizing: border-box;
+ width: 28px;
+ min-width: 28px;
+ height: 22px;
+ min-height: 22px;
+ margin: 0 !important;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ color: var(--color-muted);
+ font-size: 11px;
+ display: inline-flex;
+ align-items: center;
+ align-self: center;
+ justify-content: center;
+ position: relative;
+ top: 0 !important;
+ transform: none !important;
+ line-height: 1;
+ transition: background 0.15s, border-color 0.15s, color 0.15s, opacity 0.15s;
+}
+.memory-item-actions .hwfit-cached-menu-btn:hover {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 24%, transparent);
+ color: var(--fg);
+}
+.memory-item-actions .hwfit-cached-menu-btn {
+ transform: translateY(2px) !important;
+}
+
+@media (min-width: 769px) {
+ .task-card .memory-item-actions {
+ top: 6px;
+ }
+ .task-card .memory-item-actions > .memory-item-btn {
+ transform: none !important;
+ }
+}
+
+/* All kebab menus use the same muted tone as the expand/collapse chevron. */
+:is(
+ .memory-menu-btn,
+ .memory-item-actions .memory-item-btn:not(.save):not(.pin):not(.delete):not(.doclib-card-action-btn),
+ .hwfit-cached-menu-btn,
+ .cookbook-task-menu-btn,
+ .skill-kebab-btn,
+ .archive-menu-btn,
+ .doc-tab-menu-btn,
+ .email-menu-btn
+) {
+ color: var(--color-muted, var(--fg)) !important;
+}
+:is(
+ .memory-menu-btn,
+ .memory-item-actions .memory-item-btn:not(.save):not(.pin):not(.delete):not(.doclib-card-action-btn),
+ .hwfit-cached-menu-btn,
+ .cookbook-task-menu-btn,
+ .skill-kebab-btn,
+ .archive-menu-btn,
+ .doc-tab-menu-btn,
+ .email-menu-btn
+) svg {
+ color: inherit;
+ fill: currentColor;
+}
+/* Card kebabs are lightweight menu affordances, not bordered buttons. The
+ old base hover rule gave only some library cards a boxed outline while
+ chats, tasks, and cookbook stayed borderless. */
+:is(
+ .memory-menu-btn,
+ .memory-item-actions .memory-item-btn:not(.save):not(.pin):not(.delete):not(.doclib-card-action-btn),
+ .hwfit-cached-menu-btn,
+ .cookbook-task-menu-btn,
+ .skill-kebab-btn,
+ .archive-menu-btn,
+ .doc-tab-menu-btn,
+ .email-menu-btn
+):hover {
+ background: none !important;
+ border-color: transparent !important;
+ color: var(--fg) !important;
+ opacity: 1;
+}
+/* Skills used a separate, larger kebab with a different hover treatment.
+ Match every other card action trigger. */
+.skill-kebab-btn {
+ box-sizing: border-box;
+ width: 28px;
+ min-width: 28px;
+ height: 22px;
+ min-height: 22px;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ color: var(--color-muted) !important;
+ opacity: 0.55;
+ top: -2px;
+}
+@media (max-width: 768px) {
+ .memory-item-actions > .memory-item-btn,
+ .memory-item-actions .hwfit-cached-menu-btn {
+ width: 40px;
+ min-width: 40px;
+ height: 40px;
+ min-height: 40px;
+ }
+ /* Card menus all need the same comfortable tap target. These components
+ previously escaped the shared action-button sizing through more-specific
+ desktop selectors. */
+ .memory-menu-btn,
+ .skill-kebab-btn,
+ .task-card .memory-item-actions .memory-item-btn {
+ width: 40px !important;
+ min-width: 40px !important;
+ height: 40px !important;
+ min-height: 40px !important;
+ }
+ .memory-menu-btn { opacity: 0.7; }
+ /* Document cards seat their actions on a slightly higher title baseline
+ than chats/research cards. Bring only that kebab down 4px. */
+ #doclib-modal .doclib-card[data-doc-id] > .doclib-document-header .memory-item-actions .memory-item-btn {
+ transform: translateY(-2px) !important;
+ }
+}
+/* Mobile: center the kebab/menu dropdowns in the viewport so they don't
+ slide left/right as the window width changes (they were positioned via
+ inline `right: ...px` relative to the kebab button). Stable position is
+ easier to land on by touch. The inline `top:` set by the menu code is
+ preserved; we only override the horizontal axis. */
+@media (max-width: 768px) {
+ .email-card-dropdown,
+ .cookbook-saved-menu,
+ .cookbook-dep-menu {
+ left: 50% !important;
+ right: auto !important;
+ transform: translateX(-50%);
+ }
+}
+/* Mobile-only Cancel item appended to dropdowns (kebab/More menus). On
+ desktop outside-click closes them cleanly; on touch that's fiddly, so
+ give an explicit Cancel row. Visually separated from the action items
+ above by a top divider + slight muted color. */
+.dropdown-cancel-mobile { display: none; }
+@media (max-width: 768px) {
+ .dropdown-cancel-mobile {
+ display: flex;
+ border-top: 1px solid var(--border);
+ margin-top: 4px;
+ padding-top: 8px;
+ opacity: 0.85;
+ }
+}
+/* Launch-command Copy button morphs into Cancel once Launch has been clicked. */
+.hwfit-serve-copy.hwfit-serve-cancel {
+ border-color: var(--color-danger, #e06c75) !important;
+ color: var(--color-danger, #e06c75) !important;
+}
+.hwfit-serve-copy.hwfit-serve-cancel:hover {
+ background: color-mix(in srgb, var(--color-danger, #e06c75) 14%, transparent);
+}
+
+/* One visual contract for card kebab menus. These menus are built by several
+ modules, and native button defaults previously made identical rows render
+ with different fonts, heights, and hover movement. */
+:is(
+ .session-dropdown-menu,
+ .email-card-dropdown,
+ .memory-item-dropdown,
+ .task-dropdown,
+ .skill-kebab-menu,
+ .doclib-card-dropdown,
+ .gallery-detail-menu
+) {
+ box-sizing: border-box;
+ padding: 4px !important;
+ color: var(--fg);
+ background: var(--panel, var(--bg)) !important;
+ border: 1px solid var(--border) !important;
+ border-radius: 8px !important;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3) !important;
+ font-family: inherit !important;
+ font-size: 11px !important;
+ line-height: 1.2;
+}
+:is(
+ .session-dropdown-menu,
+ .email-card-dropdown,
+ .memory-item-dropdown,
+ .task-dropdown,
+ .skill-kebab-menu,
+ .doclib-card-dropdown,
+ .gallery-detail-menu
+) > .dropdown-item-compact {
+ box-sizing: border-box;
+ width: 100%;
+ min-height: 28px;
+ padding: 6px 8px;
+ gap: 10px;
+ border: 0;
+ border-radius: 6px;
+ color: var(--fg);
+ background: transparent;
+ font: inherit !important;
+ line-height: 1.2;
+ text-align: left;
+ white-space: nowrap;
+}
+:is(
+ .session-dropdown-menu,
+ .email-card-dropdown,
+ .memory-item-dropdown,
+ .task-dropdown,
+ .skill-kebab-menu,
+ .doclib-card-dropdown,
+ .gallery-detail-menu
+) > .dropdown-item-compact:hover:not(:disabled) {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ transform: none;
+}
+@media (max-width: 768px) {
+ :is(
+ .session-dropdown-menu,
+ .email-card-dropdown,
+ .memory-item-dropdown,
+ .task-dropdown,
+ .skill-kebab-menu,
+ .doclib-card-dropdown,
+ .gallery-detail-menu
+ ) > .dropdown-item-compact {
+ min-height: 44px;
+ padding: 12px !important;
+ gap: 10px !important;
+ font-size: 14px !important;
+ }
+}
+/* Email modal title unread badge — small accent pill next to "Email". */
+.email-lib-unread-badge {
+ display: inline-block;
+ font-size: 0.55em;
+ font-weight: 700;
+ background: var(--accent-primary, var(--red));
+ color: var(--bg, #1a1a1a);
+ border-radius: 9px;
+ padding: 1px 7px;
+ margin-left: 8px;
+ vertical-align: 2px;
+ line-height: 1.3;
+ letter-spacing: 0.02em;
+ cursor: pointer;
+}
+.email-lib-unread-badge.email-lib-away-badge {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ color: var(--accent, var(--red));
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, transparent);
+ padding: 0 7px;
+}
+.email-lib-auto-reply-badge {
+ display: inline-block;
+ margin-left: 7px;
+ padding: 1px 7px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 48%, var(--border));
+ border-radius: 9px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
+ color: var(--accent, var(--red));
+ font-size: 0.55em;
+ font-weight: 700;
+ line-height: 1.3;
+ letter-spacing: 0.01em;
+ vertical-align: 2px;
+ cursor: pointer;
+}
+.email-lib-auto-reply-status-dot {
+ width: 6px;
+ height: 6px;
+ margin-left: 6px;
+ border-radius: 50%;
+ background: var(--accent, var(--red));
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent),
+ 0 0 8px color-mix(in srgb, var(--accent, var(--red)) 72%, transparent);
+ vertical-align: 2px;
+}
+.modal-header h4 > .email-lib-auto-reply-badge {
+ font-size: 0.65em;
+ white-space: nowrap;
+ vertical-align: 1px;
+}
+#email-lib-modal.email-lib-unread-active .doclib-modal-content {
+ box-shadow:
+ 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 22%, transparent),
+ 0 0 18px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent),
+ var(--shadow-lg, 0 18px 50px rgba(0, 0, 0, 0.35));
+}
+#email-lib-modal .email-lib-header-actions .minimize-btn {
+ position: relative;
+ left: 6px;
+}
+/* Per-card prev/next nav — only visible on the currently expanded card,
+ sits at the right of the title row next to the done check. */
+.email-card-nav-arrows {
+ display: none;
+ margin-left: auto;
+ gap: 2px;
+ flex-shrink: 0;
+ align-items: center;
+ position: relative;
+ top: -3px;
+ left: 19px;
+}
+.doclib-card.email-card-expanded .email-card-nav-arrows {
+ display: inline-flex;
+}
+.email-card-header-menu {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ border: none;
+ border-radius: 6px;
+ background: none;
+ color: var(--fg);
+ opacity: 0.65;
+ cursor: pointer;
+ position: relative;
+ top: -3px;
+ transition: opacity 0.12s, background 0.12s;
+}
+.doclib-card.email-card-expanded .email-card-header-menu {
+ display: inline-flex;
+}
+.email-card-header-menu:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.email-card-nav-btn {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.65;
+ cursor: pointer;
+ padding: 6px 8px;
+ border-radius: 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.12s, background 0.12s;
+}
+.email-card-nav-btn:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+
+/* ── Email document editor ── */
+.doc-email-header {
+ display: flex; flex-direction: column; gap: 6px; padding: 10px 12px;
+ border-bottom: 1px solid var(--border); background: var(--bg); flex-shrink: 0;
+}
+.doc-email-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 0;
+}
+.doc-email-collapse-btn {
+ width: 100%;
+ min-height: 24px;
+ display: none;
+ align-items: center;
+ gap: 7px;
+ padding: 2px 4px 3px;
+ border: none;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+ opacity: 0.72;
+ text-align: left;
+}
+.doc-email-collapse-btn:hover { opacity: 1; color: var(--accent, var(--red)); }
+.doc-email-collapse-btn svg {
+ flex-shrink: 0;
+ opacity: 0.65;
+ transition: transform 0.14s ease;
+}
+.doc-email-collapse-summary {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ opacity: 0.74;
+}
+.doc-email-header.doc-email-header-collapsed .doc-email-collapse-btn svg {
+ transform: rotate(180deg);
+}
+.email-field { display: flex; align-items: center; gap: 8px; position: relative; }
+.email-field label { font-size: 11px; font-weight: 600; color: var(--fg); opacity: 0.5; min-width: 50px; text-align: right; flex-shrink: 0; }
+.email-field input {
+ flex: 1; width: 100%; background: transparent; border: 1px solid var(--border); border-radius: 4px;
+ padding: 5px 8px; font-size: 13px; font-family: inherit; color: var(--fg); outline: none;
+ min-width: 0;
+}
+.email-field input:focus { border-color: var(--accent, #4a9eff); }
+@container docpane (max-width: 460px) {
+ .doc-email-header .email-field {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 3px;
+ align-items: stretch;
+ }
+ .doc-email-header .email-field label {
+ min-width: 0;
+ text-align: left;
+ }
+ .doc-email-header .email-field input {
+ width: 100%;
+ }
+}
+/* Cc toggle and attach button are absolute so they don't steal width
+ from the To input. Anchored to the input's vertical center + 4px
+ nudge so the label sits where the user expects it. */
+.email-field .email-cc-toggle {
+ position: absolute; right: 6px; top: calc(50% + 1px); transform: translateY(-50%);
+ z-index: 2;
+}
+/* Accent-tinted prefix label (To / Cc / Bcc / Subject) sitting at
+ the left edge of each input. Replaces both the old tag
+ and the all-placeholder accent. */
+.email-field .email-field-prefix {
+ position: absolute;
+ left: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--accent-primary, var(--red));
+ pointer-events: none;
+ z-index: 1;
+ line-height: 1;
+}
+.email-field input { padding-left: 44px; }
+.email-field #doc-email-subject { padding-left: 64px; }
+/* X close button inside Cc / Bcc fields. */
+.email-field .email-cc-close {
+ position: absolute; right: 6px; top: calc(50% + 1px); transform: translateY(-50%);
+ background: none; border: none; color: var(--fg);
+ opacity: 0.4; cursor: pointer;
+ width: 22px; height: 22px;
+ display: inline-flex; align-items: center; justify-content: center;
+ border-radius: 4px;
+ padding: 0;
+ z-index: 2;
+}
+.email-field .email-cc-close:hover {
+ opacity: 1;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, currentColor 10%, transparent);
+}
+.email-field input { padding-right: 60px; }
+.email-field #doc-email-cc, .email-field #doc-email-bcc { padding-right: 32px; }
+.email-field #doc-email-subject { padding-right: 8px; }
+
+.doc-email-actions {
+ display: flex; gap: 8px; justify-content: flex-end; padding: 10px 14px;
+ border-top: 1px solid var(--border); background: var(--bg); flex-shrink: 0;
+ align-items: center;
+ position: relative;
+ z-index: 30;
+ pointer-events: auto;
+}
+#doc-actions-footer .email-send-split {
+ margin-left: auto;
+}
+.doc-save-state-icon { display: none; flex: 0 0 auto; }
+.doc-save-button[data-save-state="dirty"] { color: var(--accent, var(--red)); }
+.doc-save-button[data-save-state="saving"] { color: var(--fg-muted, var(--fg)); }
+.doc-save-button[data-save-state="saved"] { color: var(--fg); }
+.doc-save-button[data-save-state="error"] { color: var(--color-danger, #e06c75); }
+.doc-stats-wrap {
+ position: relative;
+ margin-left: 0;
+ flex: 0 0 auto;
+}
+.doc-stats-btn {
+ height: 28px;
+ border: 0;
+ border-radius: 4px;
+ padding: 0 7px;
+ background: transparent;
+ color: var(--fg-muted, var(--fg));
+ font: inherit;
+ font-size: 10px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+.doc-stats-btn:hover,
+.doc-stats-btn[aria-expanded="true"] {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+}
+.doc-stats-btn.doc-stats-selected {
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+.doc-stats-popover {
+ position: fixed;
+ right: auto;
+ bottom: auto;
+ width: 190px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel, var(--bg));
+ box-shadow: 0 8px 22px rgba(0, 0, 0, 0.24);
+ color: var(--fg);
+ font-size: 11px;
+}
+.doc-stats-scope {
+ padding-bottom: 6px;
+ margin-bottom: 5px;
+ border-bottom: 1px solid var(--border);
+ color: var(--fg-muted, var(--fg));
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+.doc-stats-row {
+ min-height: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+/* WYSIWYG email body — what the recipient sees, edited in place. */
+.doc-email-richbody {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 12px 16px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--fg);
+ background: var(--bg);
+ outline: none;
+ white-space: pre-wrap;
+ text-align: left;
+ word-wrap: break-word;
+}
+.doc-email-richbody.richtext-mode {
+ background: var(--bg);
+}
+.doc-rich-empty-import {
+ position: absolute;
+ top: 90px;
+ right: 18px;
+ bottom: auto;
+ left: auto;
+ z-index: 2;
+ display: flex;
+ pointer-events: none;
+}
+.doc-rich-empty-import .doc-rich-empty-import-btn {
+ position: static;
+ float: none;
+ margin: 0;
+ opacity: 1;
+ pointer-events: auto;
+ transform: none;
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist {
+ list-style: none;
+ padding-left: 1.8em;
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist > li {
+ position: relative;
+ min-height: 1.6em;
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist > li::before {
+ content: "";
+ position: absolute;
+ left: -1.55em;
+ top: 0.3em;
+ width: 0.95em;
+ height: 0.95em;
+ box-sizing: border-box;
+ border: 1.5px solid color-mix(in srgb, var(--fg) 48%, transparent);
+ border-radius: 3px;
+ background: var(--sidebar-bg, var(--panel));
+ cursor: pointer;
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist > li[data-checked="true"]::before {
+ border-color: var(--accent, var(--red));
+ background: var(--accent, var(--red));
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist > li[data-checked="true"]::after {
+ content: "";
+ position: absolute;
+ left: -1.27em;
+ top: 0.44em;
+ width: 0.28em;
+ height: 0.5em;
+ border: solid #fff;
+ border-width: 0 1.5px 1.5px 0;
+ transform: rotate(45deg);
+ pointer-events: none;
+}
+.doc-email-richbody.richtext-mode ul.rich-checklist > li[data-checked="true"] {
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ text-decoration: line-through;
+}
+.doc-email-richbody.richtext-mode hr {
+ border: 0;
+ border-top: 1px solid var(--border);
+ margin: 1.25em 0;
+}
+.doc-email-richbody.richtext-mode hr.richtext-page-break {
+ height: 8px;
+ border-top: 1px dashed color-mix(in srgb, var(--fg) 42%, transparent);
+ border-bottom: 1px dashed color-mix(in srgb, var(--fg) 42%, transparent);
+ margin: 1.6em 0;
+ opacity: 0.8;
+}
+.doc-email-richbody.richtext-mode table {
+ width: 100%;
+ margin: 12px 0;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+.doc-email-richbody.richtext-mode th,
+.doc-email-richbody.richtext-mode td {
+ min-width: 48px;
+ padding: 7px 8px;
+ border: 1px solid var(--border);
+ vertical-align: top;
+ overflow-wrap: anywhere;
+}
+.doc-email-richbody.richtext-mode th {
+ background: color-mix(in srgb, var(--sidebar-bg, var(--panel)) 85%, var(--fg) 15%);
+ font-weight: 600;
+}
+.doc-email-richbody.richtext-mode code,
+.doc-email-richbody.richtext-mode span[style*="OdysseusInlineCode"] {
+ padding: 0.12em 0.35em;
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.9em;
+ white-space: break-spaces;
+}
+.doc-email-richbody.richtext-mode pre {
+ margin: 1em 0;
+ padding: 12px 14px;
+ overflow-x: auto;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--sidebar-bg, var(--panel)) 82%, #000 18%) !important;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ font-size: 0.9em;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ tab-size: 2;
+}
+
+::highlight(doc-find-results) {
+ color: inherit;
+ background-color: color-mix(in srgb, #facc15 48%, transparent);
+}
+
+::highlight(doc-find-current) {
+ color: inherit;
+ background-color: color-mix(in srgb, var(--accent, #ef4444) 68%, #facc15);
+}
+
+::highlight(doc-ai-selections) {
+ color: inherit;
+ background-color: color-mix(in srgb, var(--accent, #ef4444) 30%, transparent);
+}
+.doc-email-richbody img.richtext-image {
+ display: block;
+ margin: 12px 0;
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+ cursor: pointer;
+}
+.doc-email-richbody figure.richtext-image {
+ margin: 12px 0;
+ max-width: 100%;
+}
+.doc-email-richbody figure.richtext-image:not(:has(img)) { display: none; }
+.doc-email-richbody figure.richtext-image img.richtext-image {
+ display: block;
+ margin-top: 0;
+ margin-bottom: 0;
+ max-width: 100%;
+ height: auto;
+ border-radius: 4px;
+}
+.doc-email-richbody figure.richtext-image:has(img.richtext-image-size-60) .richtext-image-caption { width: 60%; }
+.doc-email-richbody figure.richtext-image:has(img.richtext-image-size-35) .richtext-image-caption { width: 35%; }
+.doc-email-richbody figure.richtext-image:has(img.richtext-image-align-left) .richtext-image-caption { margin-left: 0; margin-right: auto; }
+.doc-email-richbody figure.richtext-image:has(img.richtext-image-align-center) .richtext-image-caption { margin-left: auto; margin-right: auto; }
+.doc-email-richbody figure.richtext-image:has(img.richtext-image-align-right) .richtext-image-caption { margin-left: auto; margin-right: 0; }
+.doc-email-richbody img.richtext-image-size-100 { width: 100%; }
+.doc-email-richbody img.richtext-image-size-60 { width: 60%; }
+.doc-email-richbody img.richtext-image-size-35 { width: 35%; }
+.doc-email-richbody img.richtext-image-align-left { margin-left: 0; margin-right: auto; }
+.doc-email-richbody img.richtext-image-align-center { margin-left: auto; margin-right: auto; }
+.doc-email-richbody img.richtext-image-align-right { margin-left: auto; margin-right: 0; }
+.doc-email-richbody img.richtext-image[data-editor-image-selected] {
+ outline: 2px solid var(--accent, var(--red));
+ outline-offset: 3px;
+}
+.doc-rich-image-toolbar {
+ position: fixed;
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+ padding: 2px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: var(--bg);
+ color: var(--fg);
+ box-shadow: 0 3px 12px color-mix(in srgb, var(--fg) 18%, transparent);
+ z-index: 10032;
+}
+.doc-rich-image-toolbar button {
+ width: 24px;
+ height: 24px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 0;
+ border-radius: 3px;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+.doc-rich-image-toolbar button:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ color: var(--accent, var(--red));
+}
+.doc-rich-image-toolbar .doc-rich-image-remove:hover {
+ color: var(--accent, var(--red));
+}
+.doc-rich-image-source-menu {
+ position: fixed;
+ width: 260px;
+ min-width: 260px;
+ padding: 5px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--bg);
+ color: var(--fg);
+ box-shadow: 0 4px 16px color-mix(in srgb, var(--fg) 20%, transparent);
+ z-index: 10033;
+ font-size: 11px;
+}
+.doc-rich-image-source-title {
+ padding: 5px 7px 4px;
+ color: var(--fg);
+ font-weight: 700;
+ opacity: .75;
+}
+.doc-rich-image-source-menu > button,
+.doc-rich-gallery-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 7px 8px;
+ border: 0;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+.doc-rich-image-source-menu > button[data-image-source="computer"] {
+ padding-left: 9px;
+}
+.doc-rich-image-source-menu > button:hover,
+.doc-rich-gallery-item:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ color: var(--accent, var(--red));
+}
+.doc-rich-gallery-list {
+ display: grid;
+ gap: 2px;
+ max-height: 300px;
+ overflow-y: auto;
+}
+.doc-rich-gallery-item {
+ min-height: 44px;
+ min-width: 0;
+}
+.doc-rich-gallery-item > span:last-child {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.doc-rich-gallery-thumb {
+ width: 40px;
+ height: 40px;
+ flex: 0 0 40px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background-position: center;
+ background-size: cover;
+ background-repeat: no-repeat;
+}
+.rich-color-letter {
+ border-bottom: 2px solid var(--accent, var(--red));
+ font-weight: 700;
+ line-height: 14px;
+}
+.rich-color-swatch {
+ width: 13px !important;
+ height: 13px !important;
+ border: 1px solid color-mix(in srgb, var(--fg) 35%, transparent);
+ border-radius: 50%;
+}
+.rich-highlight-swatch { border-radius: 50%; }
+.rich-color-swatch.is-transparent {
+ background:
+ linear-gradient(135deg, transparent 43%, var(--red) 44%, var(--red) 56%, transparent 57%),
+ var(--bg);
+}
+.rich-color-palette-menu {
+ min-width: min(292px, calc(100vw - 16px));
+}
+.rich-color-palette-menu .rich-color-reset {
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+ margin-bottom: 3px;
+ padding-bottom: 7px;
+}
+.rich-color-palette-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(126px, 1fr));
+ gap: 0 4px;
+ padding: 0 4px;
+}
+.rich-color-palette-group {
+ min-width: 0;
+}
+.rich-color-palette-label {
+ padding: 4px 8px 3px;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+.rich-color-palette-group .doc-overflow-item {
+ min-width: 0;
+ padding-inline: 8px;
+}
+.rich-toolbar-color-custom {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-top: 3px;
+ padding: 7px 9px 4px 10px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+ color: var(--fg);
+ font-size: 11px;
+}
+.rich-toolbar-color-input.cp-swatch-input {
+ width: 22px;
+ height: 22px;
+ flex: 0 0 22px;
+ padding: 0;
+ border: 1px solid color-mix(in srgb, var(--fg) 28%, var(--border));
+ border-radius: 50%;
+ cursor: pointer;
+}
+.rich-toolbar-color-input.cp-swatch-input:focus-visible {
+ outline: 2px solid var(--accent, var(--red));
+ outline-offset: 2px;
+}
+
+.rich-toolbar-slider-row {
+ display: grid;
+ grid-template-columns: auto minmax(104px, 1fr) 42px;
+ align-items: center;
+ gap: 9px;
+ min-width: 220px;
+ padding: 10px;
+ color: var(--fg);
+ font-size: 11px;
+}
+.rich-toolbar-range {
+ width: 100%;
+ min-width: 0;
+ height: 8px;
+ margin: 0;
+ border: 0;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ accent-color: var(--red);
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+}
+.rich-toolbar-range::-webkit-slider-thumb {
+ width: 9px;
+ height: 9px;
+ border: 0;
+ border-radius: 50%;
+ background: var(--red);
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.rich-toolbar-range:hover::-webkit-slider-thumb,
+.rich-toolbar-range:focus::-webkit-slider-thumb,
+.rich-toolbar-range:active::-webkit-slider-thumb {
+ width: 16px;
+ height: 16px;
+}
+.rich-toolbar-range::-moz-range-thumb {
+ width: 9px;
+ height: 9px;
+ border: 0;
+ border-radius: 50%;
+ background: var(--red);
+ cursor: pointer;
+ transition: width 0.12s ease, height 0.12s ease;
+}
+.rich-toolbar-range:hover::-moz-range-thumb,
+.rich-toolbar-range:focus::-moz-range-thumb,
+.rich-toolbar-range:active::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+}
+.rich-toolbar-range:focus-visible {
+ outline: 2px solid var(--accent, var(--red));
+ outline-offset: 4px;
+}
+.rich-toolbar-slider-value {
+ width: 42px;
+ box-sizing: border-box;
+ padding: 2px 3px;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background: transparent;
+ text-align: right;
+ color: color-mix(in srgb, var(--fg) 68%, transparent);
+ font-variant-numeric: tabular-nums;
+ -webkit-appearance: textfield;
+ appearance: textfield;
+}
+.rich-toolbar-slider-value::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+.rich-toolbar-slider-value:focus {
+ border-color: var(--accent, var(--red));
+ outline: none;
+ color: var(--fg);
+}
+
+.rich-script-icon,
+.rich-font-icon,
+.rich-font-size-icon {
+ display: inline-block;
+ line-height: 1;
+ font-size: 11px;
+}
+.rich-highlight-icon {
+ display: block;
+ flex-shrink: 0;
+ color: var(--accent-primary, var(--red));
+}
+
+.rich-script-icon sup,
+.rich-script-icon sub {
+ font-size: 7px;
+ line-height: 0;
+}
+
+.rich-font-icon {
+ font-family: Georgia, serif;
+ font-size: 12px;
+}
+
+.rich-font-size-icon {
+ min-width: 16px;
+ font-size: 10px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+ text-align: center;
+}
+.rich-align-center { text-align: center; }
+.rich-align-right { text-align: right; }
+.doc-email-richbody .email-quoted-history {
+ margin-top: 14px;
+ padding: 10px 0 10px 12px;
+ border-left: 3px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ font-size: 0.92em;
+ line-height: 1.45;
+ white-space: pre-wrap;
+ user-select: text;
+ pointer-events: none;
+}
+.doc-email-richbody h1, .doc-email-richbody h2, .doc-email-richbody h3,
+.doc-email-richbody h4, .doc-email-richbody h5, .doc-email-richbody h6 { margin: 0.4em 0; }
+.email-more-menu {
+ position: absolute; bottom: 100%; right: 0; margin-bottom: 6px;
+ min-width: 160px; background: var(--bg); border: 1px solid var(--border);
+ border-radius: 6px; box-shadow: 0 -4px 12px rgba(0,0,0,0.2); padding: 4px;
+ z-index: 100;
+}
+.email-send-btn {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 20%, transparent);
+ color: var(--accent-primary, var(--red));
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 50%, transparent);
+ border-radius: 6px;
+ padding: 0 14px; height: 28px; font-size: 11px; font-weight: 600; cursor: pointer;
+ font-family: inherit; transition: all 0.15s; white-space: nowrap;
+ display: inline-flex; align-items: center; gap: 6px;
+}
+/* Split send button: [ Send │ ▾ ] — the caret drops the send-options menu UP. */
+.email-send-split { position: relative; display: inline-flex; }
+.email-send-split .email-send-main {
+ border-top-right-radius: 0; border-bottom-right-radius: 0; border-right: none;
+}
+.email-send-split .email-send-caret {
+ border-top-left-radius: 0; border-bottom-left-radius: 0;
+ padding: 0 8px; gap: 0;
+ /* its left border is the single divider between the two halves */
+ border-left: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 50%, transparent);
+}
+.email-send-split .email-send-caret svg { transition: transform 0.15s ease; }
+.email-send-split .email-send-caret[aria-expanded="true"] svg { transform: none; }
+.email-send-btn svg { flex-shrink: 0; }
+.email-send-btn:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 30%, transparent);
+ border-color: var(--accent-primary, var(--red));
+}
+.email-draft-btn {
+ background: none; border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ border-radius: 6px;
+ padding: 0 12px; height: 28px; font-size: 11px; cursor: pointer;
+ font-family: inherit; transition: all 0.15s; white-space: nowrap;
+}
+.email-draft-btn:hover { border-color: var(--fg); color: var(--fg); }
+.email-discard-btn {
+ background: transparent;
+ /* Dim the label text via color-mix instead of `opacity:0.6` on the whole
+ button — opacity multiplies through to the SVG and washes out the accent
+ X glyph. Per-element color dimming keeps the X at full accent strength. */
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ border: 1px solid var(--border); border-radius: 6px;
+ padding: 6px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
+}
+/* The ✕ glyph itself reads in accent. */
+.email-discard-btn svg { color: var(--accent-primary, var(--red)); opacity: 1; }
+.email-discard-btn:hover { opacity: 1; color: var(--red, #e55); border-color: var(--red, #e55); }
+
+/* Compose email "more" menu button — icon only, matches draft btn height */
+#doc-email-more-btn {
+ background: none; border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ border-radius: 6px;
+ width: 28px; height: 28px; padding: 0;
+ cursor: pointer; font-family: inherit; transition: all 0.15s;
+ display: inline-flex; align-items: center; justify-content: center;
+}
+#doc-email-more-btn:hover {
+ border-color: var(--fg); color: var(--fg);
+}
+.email-more-wrap { display: inline-flex; }
+.email-attachments {
+ display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0 0 58px;
+}
+.email-attachment-chip {
+ display: inline-flex; align-items: center; gap: 4px;
+ padding: 4px 8px; font-size: 11px; background: var(--hover-bg, rgba(255,255,255,0.05));
+ border: 1px solid var(--border); border-radius: 12px; color: var(--fg);
+ text-decoration: none; cursor: pointer; transition: background 0.15s, border-color 0.15s;
+ max-width: 200px; min-width: 0;
+}
+.email-attachment-chip:hover {
+ background: color-mix(in srgb, var(--accent) 15%, transparent);
+ border-color: var(--accent);
+}
+.email-attachment-chip.is-expanded {
+ max-width: 90vw;
+}
+.email-attachment-chip > span:not(.att-size):not(.email-attachment-open):not(.email-attachment-download):not(.email-attachment-calendar-open) {
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+ flex: 1 1 auto; min-width: 0;
+}
+.email-attachment-chip.is-expanded > span:not(.att-size):not(.email-attachment-open):not(.email-attachment-download):not(.email-attachment-calendar-open) {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-attachment-chip-related {
+ border-color: color-mix(in srgb, var(--accent) 30%, var(--border));
+}
+/* "Open in editor" launch icon — same prominent style on desktop AND mobile
+ (was 24px / dim / no border on desktop, easy to miss). Accent-tinted
+ background + border makes it read as a real action. */
+.email-attachment-open,
+.email-attachment-download,
+.email-attachment-calendar-open,
+.cal-event-email-source {
+ display: inline-flex; align-items: center; gap: 4px;
+ height: 22px; padding: 0 9px; border-radius: 999px;
+ margin-left: 6px; flex: 0 0 auto;
+ min-width: 52px; justify-content: center;
+ font-size: 10px; font-weight: 500; letter-spacing: 0.02em;
+ color: var(--accent-primary, var(--red));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 40%, transparent);
+ cursor: pointer; transition: background 0.1s, border-color 0.1s, color 0.1s;
+}
+.email-attachment-open {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+ margin-left: 4px;
+}
+.email-attachment-open svg,
+.email-attachment-open > svg,
+.email-attachment-download svg,
+.email-attachment-download > svg,
+.email-attachment-calendar-open svg,
+.email-attachment-calendar-open > svg,
+.cal-event-email-source svg,
+.cal-event-email-source > svg {
+ width: 12px; height: 12px;
+ opacity: 0.9;
+}
+.email-attachment-open:hover,
+.email-attachment-download:hover,
+.email-attachment-calendar-open:hover,
+.cal-event-email-source:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 22%, transparent);
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 70%, transparent);
+}
+.email-attachment-download {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+ margin-left: 4px;
+}
+.email-attachment-calendar-open {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+ margin-left: 4px;
+}
+.cal-event-email-source {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+ margin-left: 4px;
+}
+.email-attachment-open-label { line-height: 1; }
+.email-attachment-chip.is-expanded .email-attachment-open,
+.email-attachment-chip.is-expanded .email-attachment-download,
+.email-attachment-chip.is-expanded .email-attachment-calendar-open {
+ width: 24px;
+ min-width: 24px;
+ padding: 0;
+ margin-left: 4px;
+}
+.email-attachment-chip.is-expanded .email-attachment-open-label {
+ display: none;
+}
+@media (max-width: 768px) {
+ .doc-email-collapse-btn {
+ background: inherit;
+ }
+ /* Mobile: keep the pill but ensure a comfortable touch target. */
+ .email-attachment-open {
+ height: 26px; padding: 0 10px;
+ min-height: 26px !important;
+ min-width: 58px;
+ }
+ .email-attachments,
+ .email-compose-atts {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding-left: 0;
+ padding-bottom: 2px;
+ }
+ .email-attachments::-webkit-scrollbar,
+ .email-compose-atts::-webkit-scrollbar {
+ display: none;
+ }
+ /* Attachment chip body — modest minimum height so the open icon sits
+ neatly without dominating. */
+ .email-attachment-chip,
+ .email-compose-chip {
+ flex: 0 0 auto;
+ padding: 6px 8px !important;
+ min-height: 36px !important;
+ }
+ .email-attachment-chip.is-expanded {
+ flex-wrap: nowrap;
+ align-items: center;
+ max-width: min(92vw, 360px);
+ }
+ .email-attachment-chip.is-expanded > span:not(.att-size):not(.email-attachment-open):not(.email-attachment-download):not(.email-attachment-calendar-open) {
+ flex: 1 1 auto;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.25;
+ min-width: 0;
+ }
+ .email-attachment-chip.is-expanded .att-size {
+ margin-left: 0;
+ }
+ .email-attachment-chip.is-expanded .email-attachment-open,
+ .email-attachment-chip.is-expanded .email-attachment-download,
+ .email-attachment-chip.is-expanded .email-attachment-calendar-open {
+ width: 28px;
+ min-width: 28px;
+ padding: 0;
+ margin-left: 4px;
+ }
+ .email-compose-chip .compose-chip-name {
+ max-width: 190px;
+ }
+}
+
+/* Compose attachment chips (when sending new email) */
+.email-compose-atts {
+ display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 6px;
+ padding: 6px 0 0 0;
+}
+.email-compose-chip {
+ display: inline-flex; align-items: center; gap: 4px;
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: min(240px, 100%);
+ height: 24px; min-height: 24px; box-sizing: border-box;
+ padding: 2px 3px 2px 7px; font-size: 11px;
+ background: var(--hover-bg, rgba(255,255,255,0.05));
+ border: 1px solid var(--border); border-radius: 12px; color: var(--fg);
+}
+.email-compose-chip .compose-chip-name { max-width: 180px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.email-compose-chip .compose-chip-remove {
+ background: none; border: none; color: var(--fg); opacity: 0.5;
+ height: 18px; font-size: 14px; line-height: 18px; padding: 0 3px; cursor: pointer;
+ flex-shrink: 0;
+}
+.email-compose-chip .compose-chip-remove:hover { opacity: 1; color: var(--red, #e55); }
+
+.pdf-annotation-line-value {
+ width: 48px;
+ height: 22px;
+ box-sizing: border-box;
+ padding: 0 5px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ outline: none;
+ background: var(--bg-elev, var(--panel));
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 1;
+ text-align: right;
+}
+.pdf-annotation-line-value:hover { border-color: color-mix(in srgb, var(--fg) 38%, var(--border)); }
+.pdf-annotation-line-value:focus { border-color: var(--accent, var(--red)); }
+
+.email-odysseus-attach-menu {
+ position: fixed;
+ z-index: 10050;
+ width: 300px;
+ max-width: calc(100vw - 16px);
+ max-height: min(420px, calc(100vh - 16px));
+ padding: 7px;
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ background: var(--bg);
+ color: var(--fg);
+ box-shadow: 0 12px 30px rgba(0,0,0,0.28);
+ overflow: hidden;
+}
+.email-odysseus-attach-local {
+ width: 100%;
+ height: 30px;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 0 9px;
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 35%, var(--border));
+ border-radius: 5px;
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 9%, transparent);
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+}
+.email-odysseus-attach-local:hover {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 16%, transparent);
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 55%, var(--border));
+}
+.email-odysseus-attach-tabs {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 5px;
+ margin: 7px 0;
+}
+.email-odysseus-attach-tabs button {
+ height: 26px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: transparent;
+ color: var(--fg);
+ opacity: 0.72;
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+}
+.email-odysseus-attach-tabs button.active {
+ opacity: 1;
+ color: var(--accent-primary, var(--red));
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 45%, var(--border));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 8%, transparent);
+}
+.email-odysseus-attach-tabs button svg {
+ flex-shrink: 0;
+}
+.email-odysseus-attach-search-wrap {
+ height: 28px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin: -1px 0 7px;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ color: color-mix(in srgb, var(--fg) 48%, transparent);
+}
+.email-odysseus-attach-search-wrap:focus-within {
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 45%, var(--border));
+ color: var(--accent-primary, var(--red));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 6%, transparent);
+}
+.email-odysseus-attach-search {
+ min-width: 0;
+ flex: 1;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ color: var(--fg);
+ font: inherit;
+ font-size: 11px;
+}
+.email-odysseus-attach-search::placeholder {
+ color: color-mix(in srgb, var(--fg) 38%, transparent);
+}
+.email-odysseus-attach-list {
+ max-height: 320px;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.email-odysseus-attach-row {
+ width: 100%;
+ min-height: 38px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 7px;
+ border: 1px solid transparent;
+ border-radius: 5px;
+ background: transparent;
+ color: var(--fg);
+ text-align: left;
+ font: inherit;
+ cursor: pointer;
+}
+.email-odysseus-attach-row:hover {
+ background: var(--hover-bg, rgba(255,255,255,0.06));
+ border-color: var(--border);
+}
+.email-odysseus-attach-row.is-selected {
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 10%, transparent);
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 45%, var(--border));
+}
+.email-odysseus-attach-dot {
+ width: 8px;
+ height: 8px;
+ flex: 0 0 auto;
+ display: inline-block;
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.email-odysseus-attach-row.is-selected .email-odysseus-attach-dot {
+ border-color: var(--accent-primary, var(--red));
+ background: var(--accent-primary, var(--red));
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-primary, var(--red)) 18%, transparent);
+}
+.email-odysseus-attach-icon,
+.email-odysseus-attach-thumb {
+ width: 28px;
+ height: 28px;
+ flex: 0 0 auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ overflow: hidden;
+}
+.email-odysseus-attach-main {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.email-odysseus-attach-title,
+.email-odysseus-attach-meta {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-odysseus-attach-title {
+ font-size: 11px;
+ line-height: 1.2;
+}
+.email-odysseus-attach-meta {
+ font-size: 10px;
+ line-height: 1.15;
+ opacity: 0.52;
+}
+.email-odysseus-attach-empty {
+ padding: 14px 8px;
+ color: var(--muted, #888);
+ font-size: 11px;
+ text-align: center;
+}
+.email-odysseus-attach-actions {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 2px;
+ padding-top: 5px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 75%, transparent);
+ background: var(--bg);
+ position: sticky;
+ bottom: 0;
+}
+.email-odysseus-attach-count {
+ flex: 1;
+ min-width: 0;
+ color: var(--muted, #888);
+ font-size: 11px;
+}
+.email-odysseus-attach-selected {
+ height: 28px;
+ min-width: 72px;
+ padding: 0 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ border-radius: 999px;
+ border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 45%, var(--border));
+ background: var(--accent-primary, var(--red));
+ color: #fff;
+ font: inherit;
+ font-size: 11px;
+ cursor: pointer;
+}
+.email-odysseus-attach-selected svg {
+ flex-shrink: 0;
+}
+
+.email-cc-toggle {
+ background: none; border: none; color: var(--fg);
+ opacity: 0.4; font-size: 11px; cursor: pointer;
+ padding: 4px 8px; font-family: inherit;
+}
+.email-cc-toggle:hover {
+ opacity: 1;
+ color: var(--accent, #4a9eff);
+ background: none !important;
+}
+
+@media (max-width: 768px) {
+ .doc-email-collapse-btn {
+ display: flex;
+ }
+}
+
+.email-autocomplete {
+ position: absolute; top: 100%; left: 58px; right: 0; z-index: 1000;
+ background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2); max-height: 240px; overflow-y: auto;
+ margin-top: 2px;
+}
+.contact-suggestion {
+ display: flex; justify-content: space-between; gap: 8px;
+ padding: 6px 10px; font-size: 12px; cursor: pointer;
+ border-bottom: 1px solid var(--border);
+}
+.contact-suggestion:last-child { border-bottom: none; }
+.contact-suggestion:hover,
+.contact-suggestion.active,
+.contact-suggestion[aria-selected="true"] {
+ background: color-mix(in srgb, var(--accent-primary, var(--accent, var(--red))) 15%, transparent);
+ outline: 1px solid color-mix(in srgb, var(--accent-primary, var(--accent, var(--red))) 40%, transparent);
+ outline-offset: -1px;
+}
+.contact-suggestion .contact-name { font-weight: 600; color: var(--fg); }
+.contact-suggestion .contact-email { opacity: 0.6; font-size: 11px; }
+.contact-suggestion-status {
+ padding: 7px 10px;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ font-size: 11px;
+ cursor: default;
+}
+.contact-suggestion-status.is-loading { opacity: 0.72; }
+.contact-suggestion-status.is-unavailable {
+ color: color-mix(in srgb, var(--red) 82%, var(--fg));
+}
+.email-ai-reply-btn {
+ background: none;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0 12px; height: 28px; font-size: 11px; cursor: pointer;
+ font-family: inherit; transition: all 0.15s;
+ display: inline-flex; align-items: center; white-space: nowrap;
+}
+.email-ai-reply-btn:hover {
+ border-color: var(--accent, #4a9eff);
+ color: var(--accent, #4a9eff);
+}
+/* Align select/new buttons in popup library toolbar */
+#email-lib-select-btn, #email-lib-refresh-btn, #email-lib-settings-btn, #email-lib-compose-btn { position: relative; top: -4px; }
+/* Select + Refresh sit slightly lower than Compose on desktop. */
+#email-lib-select-btn, #email-lib-refresh-btn, #email-lib-settings-btn { top: -2px; }
+
+#email-lib-modal.email-settings-mode .email-accounts-row,
+#email-lib-modal.email-settings-mode .admin-card > .memory-toolbar,
+#email-lib-modal.email-settings-mode #email-lib-bulk,
+#email-lib-modal.email-settings-mode #email-lib-grid,
+#email-lib-modal.email-settings-mode #email-lib-sync-status,
+#email-lib-modal.email-settings-mode #email-lib-fab {
+ display: none !important;
+}
+#email-lib-settings-page[hidden] {
+ display: none !important;
+}
+#email-lib-modal.email-settings-mode #email-lib-settings-page {
+ display: flex;
+}
+.email-settings-page {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ min-width: 0;
+ flex-direction: column;
+ gap: 6px;
+ overflow: hidden;
+ padding: 2px 0 8px;
+ width: 100%;
+ box-sizing: border-box;
+ -webkit-overflow-scrolling: touch;
+}
+.settings-email-preferences-card {
+ display: contents;
+}
+.settings-email-preferences-card .email-settings-section {
+ margin: 0 0 10px;
+ padding: 14px 12px 12px;
+ background: var(--panel);
+}
+.settings-email-preferences-card .email-settings-section:last-child {
+ margin-bottom: 0;
+}
+.settings-email-preferences-card .email-settings-style-section {
+ margin-bottom: 4px;
+}
+.settings-email-default-card #settings-email-default-host {
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+ margin-left: auto;
+}
+.settings-email-default-card .settings-email-default-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+.settings-email-default-card .settings-email-default-title > span {
+ min-width: 0;
+}
+.settings-email-default-card .settings-email-default-title > span > svg {
+ color: var(--accent, var(--red));
+}
+.settings-email-default-card .email-settings-account-picker {
+ margin-left: 0 !important;
+}
+.settings-email-default-card .email-settings-support-copy {
+ position: relative;
+ top: 6px;
+}
+.settings-email-default-spacer {
+ height: 12px;
+}
+.settings-email-default-card .cookbook-srv-default-label {
+ position: relative;
+ top: 2px;
+}
+.settings-email-default-card .cookbook-srv-default-icon > svg {
+ position: relative;
+ top: 7px;
+}
+.settings-email-settings-legacy {
+ display: none;
+}
+
+/* Keep the standalone Email Settings cards aligned with the expandable
+ preference cards below them. */
+[data-settings-panel="email"] > .admin-card {
+ padding-top: 14px;
+}
+.email-settings-page-host {
+ display: flex;
+ flex: 0 0 auto;
+ min-height: 0;
+ padding: 0;
+ overflow: visible;
+}
+.settings-email-preferences-card > .email-settings-page-host { width: 100%; }
+.email-settings-page-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ min-height: 32px;
+ padding-bottom: 2px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
+}
+.email-settings-account-picker {
+ min-width: 0;
+ max-width: 100%;
+}
+.email-settings-account-picker select {
+ min-width: 0;
+ max-width: 100%;
+}
+.email-settings-back {
+ position: relative;
+ top: -2px;
+}
+.email-settings-header-back {
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+ border: 1px solid transparent;
+ width: auto;
+ height: 24px;
+ padding: 0 8px 0 6px;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ cursor: pointer;
+ border-radius: 999px;
+ flex-shrink: 0;
+ position: relative;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+}
+.email-settings-header-back:hover {
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 12%, transparent);
+ color: var(--fg);
+}
+.email-settings-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+.email-settings-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1 1 auto;
+ width: 100%;
+ min-width: 0;
+ font-size: 12px;
+ font-weight: 700;
+}
+.email-settings-title svg {
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.email-settings-close {
+ width: 24px;
+ height: 24px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ cursor: pointer;
+ font-size: 14px;
+ line-height: 1;
+}
+.email-settings-close:hover {
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
+}
+.email-settings-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 auto;
+ gap: 6px;
+ padding-top: 4px;
+ box-sizing: border-box;
+ min-width: 0;
+ min-height: 0;
+ overflow-y: auto;
+ overflow-x: hidden;
+ overscroll-behavior: contain;
+}
+.email-settings-loading,
+.email-settings-error {
+ font-size: 12px;
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ padding: 8px 2px;
+}
+.email-settings-loading {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+}
+.email-settings-loading-css-whirlpool {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ box-sizing: border-box;
+ border: 1.5px solid color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
+ border-top-color: var(--accent, var(--red));
+ border-right-color: color-mix(in srgb, var(--accent, var(--red)) 72%, transparent);
+ border-radius: 50%;
+ animation: whirlpool-spin 0.72s linear infinite;
+ flex: 0 0 auto;
+}
+.email-settings-loading-spinner {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ color: var(--accent, var(--red));
+}
+.email-settings-section {
+ display: flex;
+ flex-direction: column;
+ flex: 0 0 auto;
+ gap: 7px;
+ margin: 0;
+ padding: 14px 8px 8px;
+ min-width: 0;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ transition: opacity 0.15s ease, background 0.15s ease;
+}
+.email-settings-section.is-disabled {
+ opacity: 0.58;
+ background: color-mix(in srgb, var(--fg) 2%, transparent);
+}
+.email-settings-section-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.email-settings-section-head > div:first-child {
+ min-width: 0;
+ flex: 1 1 auto;
+}
+.email-settings-section-head > .email-settings-enabled-control,
+.email-settings-section-head > button {
+ margin-left: auto;
+}
+.email-settings-section-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ width: 100%;
+ min-width: 0;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+.email-settings-section-toggle:hover,
+.email-settings-section-toggle:active {
+ background: transparent;
+}
+.email-settings-section-toggle-copy {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ min-width: 0;
+ flex: 1 1 auto;
+}
+.email-settings-section-toggle > .email-settings-enabled-control {
+ margin-left: auto;
+ flex: 0 0 auto;
+}
+.email-settings-section-chevron {
+ flex: 0 0 auto;
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ transition: transform .14s ease, color .14s ease;
+}
+.email-settings-section-toggle:hover .email-settings-section-chevron,
+.email-settings-section-toggle:focus-visible .email-settings-section-chevron {
+ color: var(--accent, var(--red));
+}
+.email-settings-section-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 14px;
+ line-height: 1;
+ font-weight: 600;
+ letter-spacing: 0;
+ margin: 0;
+ padding: 0;
+}
+.email-settings-section-title svg {
+ color: var(--accent, var(--red));
+ opacity: 0.6;
+ flex-shrink: 0;
+}
+.email-settings-account-scope {
+ margin-left: 4px;
+ font-size: 11px;
+ font-weight: 500;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+}
+.email-settings-section-body {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ min-width: 0;
+ width: 100%;
+}
+.email-settings-section[open] > .email-settings-section-body {
+ min-height: 0;
+ flex: 0 0 auto;
+ height: auto;
+ max-height: none;
+ overflow: visible;
+ overscroll-behavior: contain;
+ padding: 0 4px 4px 0;
+ scrollbar-width: thin;
+}
+.email-settings-section[open] {
+ min-height: 0;
+ flex: 0 0 auto;
+}
+.email-settings-auto-reply-section[open],
+.email-settings-cleanup-section[open]:has(.email-unsub-card) {
+ flex: 1 1 auto;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr);
+ align-content: stretch;
+}
+.email-settings-auto-reply-section[open] > .email-settings-section-body,
+.email-settings-cleanup-section[open]:has(.email-unsub-card) > .email-settings-section-body {
+ flex: 0 0 auto;
+ height: 100%;
+ max-height: 100%;
+ overflow-y: auto;
+}
+.email-settings-section[open] > .email-settings-section-body > * {
+ flex: 0 0 auto;
+}
+.email-settings-section-body-head {
+ display: flex;
+ justify-content: flex-end;
+}
+.email-settings-display-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.email-settings-display-title {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.email-settings-display-title > svg {
+ flex: 0 0 auto;
+ color: var(--accent, var(--red));
+}
+.email-settings-display-title-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ position: relative;
+ top: 10px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+}
+.email-settings-display-title-row .email-settings-section-title {
+ flex: 1 1 auto;
+ width: auto;
+ padding-bottom: 0;
+ border-bottom: 0;
+}
+.email-settings-style-section .email-settings-section-title {
+ display: flex;
+ width: 100%;
+ position: relative;
+ top: 0;
+ padding-bottom: 8px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+}
+.email-settings-style-section .email-settings-section-head,
+.email-settings-style-section .email-settings-section-body {
+ position: static;
+}
+.email-settings-style-section .email-settings-section-head { margin-top: 8px; }
+.email-settings-style-section .email-settings-section-body { margin-top: 12px; }
+.email-settings-style-section .email-settings-section-desc {
+ margin-top: 16px;
+ top: 0;
+}
+.email-settings-auto-reply-section .email-settings-section-title,
+.email-settings-cleanup-section .email-settings-section-title {
+ display: flex;
+ width: 100%;
+ padding-bottom: 6px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+}
+.email-settings-cleanup-section .email-settings-section-title {
+ position: relative;
+ top: 10px;
+}
+.email-settings-auto-reply-section .email-settings-section-desc,
+.email-settings-cleanup-section .email-settings-section-desc {
+ margin-top: 10px;
+}
+.email-settings-display-section .email-settings-section-desc,
+.email-settings-cleanup-section .email-settings-section-desc,
+.email-settings-inline-images-section .email-settings-section-desc {
+ margin-top: 12px;
+ top: 12px;
+}
+.email-settings-auto-reply-title-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ position: relative;
+ top: 10px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
+}
+.email-settings-auto-reply-section .email-settings-section-head {
+ margin-top: 8px;
+}
+.email-settings-auto-reply-section .email-settings-section-body {
+ margin-top: 8px;
+}
+.email-settings-auto-reply-section .email-settings-section-desc {
+ margin-top: 12px;
+ top: 0;
+}
+.email-settings-display-section .email-settings-section-desc {
+ margin-top: 10px;
+ top: 10px;
+}
+.email-settings-auto-reply-section .email-settings-section-body {
+ margin-top: 14px;
+}
+.email-settings-auto-reply-title-row .email-settings-section-title {
+ flex: 1 1 auto;
+ width: auto;
+ padding-bottom: 0;
+ border-bottom: 0;
+}
+.email-settings-support-copy {
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+ font-size: 11px;
+ line-height: 1.3;
+}
+.email-settings-clean-btn > svg {
+ color: var(--accent, var(--red));
+}
+.email-settings-clean-btn {
+ height: 32px;
+}
+.email-unsub-rescan-btn {
+ height: 32px;
+}
+.email-settings-clean-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 7px;
+ min-width: 0;
+ margin-left: auto;
+}
+.email-settings-clean-status {
+ display: none;
+ font-size: 10.5px !important;
+ flex: 0 1 auto;
+ width: auto !important;
+ max-width: min(52vw, 360px);
+ min-width: 0;
+ overflow: hidden;
+ text-align: right;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ position: relative;
+ top: 2px;
+}
+.email-settings-clean-status.is-busy {
+ order: 0;
+ align-self: center;
+ width: auto !important;
+ max-width: 100%;
+ margin-right: auto;
+ text-align: left;
+}
+.email-settings-clean-btn {
+ flex: 0 0 auto;
+}
+.email-settings-default .cookbook-srv-default-icon > svg {
+ position: relative;
+ top: 3px;
+}
+.email-unsub-auto-safe-btn > svg {
+ position: relative;
+ top: 2px;
+}
+.email-unsub-auto-safe-btn {
+ height: 29px;
+}
+.email-unsub-status,
+.email-unsub-panel-status {
+ position: relative;
+ top: 2px;
+}
+.email-unsub-panel-status {
+ flex: 1 1 220px;
+ min-width: 0;
+ margin-right: auto;
+ overflow: hidden;
+ color: color-mix(in srgb, var(--fg) 68%, transparent);
+ font-size: 11px;
+ line-height: 1.3;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.email-unsub-panel-status.is-error {
+ color: var(--red, #e57373);
+}
+.email-unsub-card .email-tag {
+ 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)) 10%, transparent);
+}
+.email-unsub-duplicate-badge {
+ position: relative;
+ top: 2px;
+}
+.email-unsub-accent-icon {
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.email-unsub-card {
+ position: relative;
+ transition: opacity .16s ease;
+}
+.email-unsub-card-details {
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
+.email-unsub-card-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ cursor: pointer;
+}
+.email-unsub-card-toggle:hover,
+.email-unsub-card-toggle:focus-visible {
+ color: var(--accent, var(--red));
+}
+.email-unsub-card-toggle > svg {
+ transition: transform .14s ease;
+}
+.email-unsub-done-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+}
+.email-unsub-done-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ flex: 0 0 auto;
+ padding: 3px 6px;
+ position: relative;
+ top: -6px;
+}
+.email-unsub-card-details .email-unsub-done-btn {
+ top: 0;
+}
+.email-unsub-done-btn > svg {
+ color: var(--accent, var(--red));
+}
+.email-unsub-ignore-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ position: absolute;
+ top: 5px;
+ right: 6px;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+}
+.email-unsub-card-details .email-unsub-ignore-btn {
+ position: static;
+}
+.email-unsub-ignore-btn:hover {
+ color: var(--accent, var(--red));
+}
+.email-unsub-remaining-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ flex-wrap: wrap;
+}
+.email-unsub-remaining-actions button {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ position: relative;
+ top: -2px;
+}
+.email-unsub-remaining-actions svg {
+ flex: 0 0 auto;
+}
+.email-unsub-link-btn {
+ position: relative;
+ top: 2px;
+}
+.email-unsub-status.is-error {
+ color: var(--accent, var(--red)) !important;
+}
+.email-settings-section-desc {
+ position: relative;
+ top: 8px;
+ margin-top: 4px;
+ font-size: 10.5px;
+ line-height: 1.25;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+}
+.email-settings-inline-images-section .email-settings-section-desc {
+ margin-bottom: 4px;
+}
+.email-settings-inline-images-spacer {
+ height: 12px;
+}
+.email-settings-display-spacer {
+ height: 10px;
+}
+.email-settings-display-section .email-settings-section-desc {
+ margin-top: 10px;
+ top: 10px;
+}
+.email-settings-display-section,
+.email-settings-inline-images-section {
+ padding-bottom: 18px;
+}
+.email-settings-cleanup-section .email-settings-section-desc {
+ margin-top: 14px;
+ top: 14px;
+}
+.email-settings-auto-reply-section .email-settings-section-desc {
+ margin-top: 12px;
+ top: 10px;
+}
+.email-settings-toggle,
+.email-settings-check {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ font-size: 12px;
+ line-height: 1.25;
+}
+.email-settings-toggle {
+ flex-shrink: 0;
+}
+.email-settings-enabled-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ flex-shrink: 0;
+}
+.email-settings-enabled-state {
+ font-size: 10px;
+ font-weight: 700;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.email-settings-section.is-enabled .email-settings-enabled-state {
+ color: var(--accent, var(--red));
+}
+.email-settings-section.is-disabled .email-settings-enabled-state {
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.email-settings-display-section.is-enabled .email-settings-enabled-state {
+ color: var(--accent, var(--red));
+}
+.email-settings-check {
+ min-height: 24px;
+ padding: 3px 0;
+}
+.email-settings-check > span:first-child {
+ min-width: 0;
+ margin-right: auto;
+}
+.email-settings-check-spacer {
+ flex: 1;
+}
+.email-settings-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 7px;
+ min-width: 0;
+}
+.email-settings-grid label,
+.email-settings-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
+}
+.email-settings-grid span,
+.email-settings-field > span {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0;
+ color: color-mix(in srgb, var(--fg) 56%, transparent);
+}
+.email-settings-page input[type="date"],
+.email-settings-page input[type="datetime-local"],
+.email-settings-page select,
+.email-settings-page textarea {
+ width: 100%;
+ min-width: 0;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--panel, var(--bg));
+ color: var(--fg);
+ font: inherit;
+ font-size: 12px;
+}
+.email-settings-page input[type="date"],
+.email-settings-page input[type="datetime-local"],
+.email-settings-page select {
+ height: 28px;
+ padding: 0 7px;
+}
+.email-settings-page textarea {
+ resize: vertical;
+ min-height: 72px;
+ padding: 7px;
+ line-height: 1.35;
+}
+.email-settings-static-value {
+ min-height: 28px;
+ display: flex;
+ align-items: center;
+ padding: 0 7px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--panel, var(--bg)) 74%, transparent);
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+ font-size: 12px;
+}
+.email-settings-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 2px;
+}
+
+/* Email Settings actions use the same quiet outlined treatment everywhere:
+ the Settings links, the modal settings page, and unsubscribe actions. */
+[data-settings-panel="email"] #set-email-open-integrations,
+[data-settings-panel="email"] #set-email-open-tasks,
+[data-settings-panel="email"] .email-settings-section button.memory-toolbar-btn,
+#email-lib-settings-page .email-settings-section button.memory-toolbar-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-family: inherit;
+ font-weight: 400;
+}
+[data-settings-panel="email"] #set-email-open-integrations:hover,
+[data-settings-panel="email"] #set-email-open-tasks:hover,
+[data-settings-panel="email"] .email-settings-section button.memory-toolbar-btn:hover,
+#email-lib-settings-page .email-settings-section button.memory-toolbar-btn:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: var(--fg);
+ color: var(--fg);
+}
+[data-settings-panel="email"] #set-email-open-integrations > svg,
+[data-settings-panel="email"] #set-email-open-tasks > svg {
+ color: var(--accent, var(--red));
+ opacity: 1 !important;
+}
+[data-settings-panel="email"] .email-style-settings-extract,
+[data-settings-panel="email"] .email-style-settings-save,
+#email-lib-settings-page .email-style-settings-extract,
+#email-lib-settings-page .email-style-settings-save {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-weight: 400;
+}
+[data-settings-panel="email"] .email-style-settings-extract:hover,
+[data-settings-panel="email"] .email-style-settings-save:hover,
+#email-lib-settings-page .email-style-settings-extract:hover,
+#email-lib-settings-page .email-style-settings-save:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: var(--fg);
+ color: var(--fg);
+}
+.email-settings-status,
+.email-style-settings-status {
+ margin-right: auto;
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+}
+.email-settings-status.is-success,
+.email-style-settings-status.is-success {
+ color: var(--color-success, #4caf50);
+ position: relative;
+ top: 2px;
+}
+.email-settings-status.is-error,
+.email-style-settings-status.is-error {
+ color: var(--accent, var(--red));
+ position: relative;
+ top: 2px;
+}
+.email-settings-save,
+.email-style-settings-save {
+ height: 28px;
+ min-width: 64px;
+ justify-content: center;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.email-settings-save {
+ height: 32px;
+}
+.email-style-settings-extract {
+ height: 32px;
+}
+.email-style-settings-save {
+ height: 32px;
+}
+.email-settings-save svg,
+.email-style-settings-save svg {
+ color: var(--accent, var(--red));
+ flex-shrink: 0;
+}
+.email-style-settings-extract svg {
+ color: var(--accent, var(--red));
+ position: relative;
+ top: 2px;
+}
+.email-settings-task-setting {
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+}
+.email-settings-inline-link {
+ appearance: none;
+ border: 0;
+ padding: 0;
+ background: transparent;
+ color: var(--accent, var(--red));
+ font: inherit;
+ text-decoration: underline;
+ cursor: pointer;
+}
+.email-style-extract-help {
+ width: 18px;
+ height: 18px;
+ min-width: 18px;
+ padding: 0;
+ border-radius: 999px;
+ border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ color: color-mix(in srgb, var(--fg) 68%, transparent);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font: inherit;
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1;
+ cursor: help;
+}
+.email-style-extract-help:hover {
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 38%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+}
+@media (max-width: 420px) {
+ .email-settings-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+
+/* ── Notes ── */
+/* Notes panel — body-level flex sibling of chat-container */
+body.notes-view .chat-container { flex: 1; min-width: 0; }
+
+/* Mobile: notes panel takes over full screen */
+@media (max-width: 768px) {
+ body.notes-view .notes-pane {
+ position: fixed;
+ inset: 0;
+ max-width: 100%;
+ width: 100% !important;
+ /* Desktop sets height: min(80vh, 820px) which wins over inset:0's bottom:0
+ and leaves a gap at the bottom of the viewport. Force full height here. */
+ height: 100dvh !important;
+ max-height: 100dvh !important;
+ z-index: 170;
+ /* Bottom-sheet treatment like the document editor: stroked, rounded top,
+ slides up from the bottom. */
+ border: 1px solid var(--border);
+ border-bottom: none;
+ border-radius: 14px 14px 0 0;
+ /* Pane itself never scrolls — its inner .notes-pane-body is the scroller.
+ Two nested scrollers caused the body's flex:1 to lose its height bound
+ on Firefox mobile and the last row got clipped off-screen. */
+ overflow: hidden;
+ animation: sheet-enter 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+ transform-origin: bottom center;
+ }
+ /* Required for the flex:1 body to actually constrain to remaining pane
+ height instead of expanding to its content (default min-height:auto on
+ flex items). Without this both grid and list views overflow the viewport
+ bottom on phones. */
+ body.notes-view .notes-pane-body {
+ min-height: 0;
+ -webkit-overflow-scrolling: touch;
+ }
+ body.notes-view #notes-divider {
+ display: none;
+ }
+ body.notes-view .notes-mobile-grabber {
+ display: block;
+ flex-shrink: 0;
+ height: 18px;
+ position: relative;
+ background: var(--bg);
+ touch-action: none;
+ cursor: grab;
+ }
+ body.notes-view .notes-mobile-grabber::before {
+ content: '';
+ position: absolute;
+ top: 7px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 36px;
+ height: 4px;
+ background: var(--fg);
+ opacity: 0.25;
+ border-radius: 2px;
+ }
+ body.notes-view .chat-container {
+ display: none;
+ }
+ body.notes-view .mobile-new-chat-btn,
+ body.notes-view .hamburger-btn {
+ display: none !important;
+ }
+}
+/* ── Mobile notes UX ────────────────────────────────────
+ Tiles become read-only previews on touch ≤768px wide.
+ Tap opens a fullscreen overlay where editing happens; long-press
+ toggles drag-to-reorder. Wired up in static/js/notes.js. */
+body.notes-mobile-mode .note-card-corner,
+body.notes-mobile-mode .note-card-corner-edit,
+body.notes-mobile-mode .note-card-corner-archive,
+body.notes-mobile-mode .note-card-corner-copy,
+body.notes-mobile-mode .note-card-corner-trash,
+body.notes-mobile-mode .note-card-corner-unarchive,
+body.notes-mobile-mode .note-card-corner-done,
+body.notes-mobile-mode .note-card-edit-corner,
+body.notes-mobile-mode .note-card-done,
+body.notes-mobile-mode .note-card-actions,
+body.notes-mobile-mode .note-cl-quickadd,
+body.notes-mobile-mode .note-card .note-checkbox-rm,
+/* Panel-fullscreen toggle is redundant on mobile — the panel always
+ fills the viewport already. */
+body.notes-mobile-mode #notes-fullscreen-toggle {
+ display: none !important;
+}
+
+/* Header toggle icons (archive view, list/grid switch) — defaults are
+ tuned for the dense desktop header; on mobile bump them slightly so
+ they read as tap targets without dominating the header. */
+body.notes-mobile-mode #notes-archive-toggle,
+body.notes-mobile-mode #notes-view-toggle {
+ width: 30px;
+ height: 30px;
+ padding: 6px;
+}
+body.notes-mobile-mode #notes-archive-toggle svg,
+body.notes-mobile-mode #notes-view-toggle svg {
+ width: 17px;
+ height: 17px;
+}
+/* The whole card is the tap target on mobile — bigger feedback */
+body.notes-mobile-mode .note-card {
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ transition: transform 0.12s ease, box-shadow 0.12s ease;
+}
+
+/* Drag-to-reorder mode — desaturate the grid and sweep a subtle
+ shimmer across every tile so it's obvious you're in rearrange mode
+ (the Google Keep "active edit" cue). Also disable scroll inside the
+ panel so the drag works smoothly. */
+body.notes-drag-mode .notes-pane-body {
+ overflow: hidden;
+ touch-action: none;
+}
diff --git a/static/css/15-notes.css b/static/css/15-notes.css
new file mode 100644
index 000000000..3c4103273
--- /dev/null
+++ b/static/css/15-notes.css
@@ -0,0 +1,4062 @@
+/* 15-notes - 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.
+ */
+body.notes-drag-mode .note-card {
+ cursor: grab;
+ transform: scale(0.985);
+ filter: saturate(0.7);
+ opacity: 0.92;
+ position: relative;
+ overflow: hidden;
+ transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.3, 1),
+ filter 0.18s ease,
+ opacity 0.18s ease;
+}
+@media (max-width: 768px) {
+ body.notes-drag-mode .note-card {
+ filter: none;
+ opacity: 0.96;
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
+ }
+ body.notes-drag-mode .note-card::after {
+ display: none;
+ }
+}
+/* Placeholder — same footprint as the lifted card, dotted outline so
+ the drop target is visible while the card follows the finger. */
+.note-card-placeholder {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border: 2px dashed color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 8px;
+ box-sizing: border-box;
+ flex-shrink: 0;
+ transition: width 0.18s ease, height 0.18s ease;
+}
+
+
+/* ── Fullscreen single-note edit overlay ──────────────── */
+.note-fullscreen-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 10500;
+ background: var(--bg);
+ display: flex;
+ flex-direction: column;
+ opacity: 0;
+ /* Bigger zoom range so the transition reads as a real "tile expands
+ into a page" motion. transform-origin is set per-tile by JS. */
+ transform: scale(0.45);
+ transform-origin: center center;
+ transition: opacity 0.28s ease, transform 0.32s cubic-bezier(0.18, 0.74, 0.24, 1.06);
+ will-change: transform, opacity;
+}
+.note-fullscreen-overlay.open {
+ opacity: 1;
+ transform: scale(1);
+}
+.note-fullscreen-overlay.closing {
+ opacity: 0;
+ transform: scale(0.7);
+ transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.4, 0, 0.6, 1);
+}
+.note-fullscreen-header {
+ flex-shrink: 0;
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ border-bottom: 1px solid var(--border);
+}
+.note-fullscreen-actions {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+/* Compact the relocated Archive/Delete buttons so two of them fit
+ alongside the back chevron without crowding. */
+.note-fullscreen-actions .note-form-text-btn {
+ padding: 8px 12px;
+ font-size: 12px;
+ border-radius: 8px;
+}
+.note-fullscreen-actions .note-form-text-btn svg {
+ width: 14px;
+ height: 14px;
+ margin-right: 4px;
+}
+.note-fullscreen-back {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 40px; height: 40px;
+ background: none;
+ border: none;
+ color: var(--fg);
+ border-radius: 8px;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+}
+.note-fullscreen-back:active {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.note-fullscreen-body {
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 8px 14px 24px;
+}
+/* The reused .note-form inside the fullscreen overlay should breathe —
+ it was originally sized to slot into a grid cell. */
+.note-fullscreen-body .note-form {
+ max-width: 720px;
+ margin: 0 auto;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+}
+
+/* Bigger touch targets inside the fullscreen overlay. The default form
+ is tuned for the grid-card context (tiny icons fit a 200px wide
+ tile); on a phone-sized full-bleed page we want chunky thumb-sized
+ controls. */
+.note-fullscreen-back svg {
+ width: 28px;
+ height: 28px;
+}
+.note-fullscreen-overlay .note-form-type-pill svg {
+ width: 16px;
+ height: 16px;
+}
+.note-fullscreen-overlay .note-cl-row {
+ padding: 8px 0;
+ gap: 8px;
+ align-items: center;
+}
+.note-fullscreen-overlay .note-cl-grip {
+ font-size: 22px;
+ line-height: 1;
+ padding: 6px 8px;
+ opacity: 0.5;
+ cursor: grab;
+ touch-action: none;
+ user-select: none;
+ /* Pull 4px toward the left edge of the row. */
+ margin-left: -4px;
+}
+.note-fullscreen-overlay .note-cl-dot {
+ width: 16px;
+ height: 16px;
+ border-width: 2px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ position: relative;
+ /* Generous invisible tap target around the visible dot */
+}
+/* Filled checkmark glyph when the row is marked done — gives a clear
+ visual cue beyond the colored fill. Nudged up 2px so it sits
+ centered in the smaller dot. */
+.note-fullscreen-overlay .note-cl-row.done .note-cl-dot::after {
+ content: '';
+ position: absolute;
+ left: 2px; top: 1px;
+ width: 8px; height: 4px;
+ border-left: 2px solid #fff;
+ border-bottom: 2px solid #fff;
+ transform: rotate(-45deg);
+}
+.note-fullscreen-overlay .note-cl-rm {
+ width: 40px;
+ height: 40px;
+ font-size: 28px;
+ line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ /* Nudge 4px further toward the right edge of the row. */
+ margin-right: -4px;
+}
+/* Lifted row + placeholder while dragging a checklist item. Mirrors
+ the card-drag look so the two interactions feel consistent. */
+.note-cl-row-dragging {
+ box-shadow: 0 12px 28px rgba(0,0,0,0.4);
+ background: var(--bg);
+ border-radius: 8px;
+ opacity: 0.98;
+}
+.note-cl-row-placeholder {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border: 2px dashed color-mix(in srgb, var(--fg) 22%, transparent);
+ border-radius: 6px;
+ margin: 4px 0;
+ box-sizing: border-box;
+ transition: height 0.16s ease;
+}
+.note-fullscreen-overlay .note-form-text-btn {
+ padding: 10px 16px;
+ font-size: 14px;
+}
+.note-fullscreen-overlay .note-form-text-btn svg {
+ width: 16px;
+ height: 16px;
+}
+/* + Add and the camera/photo button share a row inside checklists, so
+ they're both within thumb reach when editing items. The whole row
+ reads as a single dashed "tap to add" surface — tapping ANYWHERE
+ on it triggers + Add (the photo button stays its own target). */
+.note-fullscreen-overlay .note-cl-add-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-top: 10px;
+ padding: 4px 8px 4px 4px;
+ border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: background 0.12s ease, border-color 0.12s ease;
+}
+.note-fullscreen-overlay .note-cl-add-row:active {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 36%, transparent);
+}
+.note-fullscreen-overlay .note-cl-add-row .note-cl-add {
+ flex: 1;
+ justify-content: flex-start;
+ background: transparent;
+ border: none;
+}
+.note-fullscreen-overlay .note-cl-add-row .note-form-photo-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ width: auto;
+ height: 36px;
+ padding: 0 12px 0 10px;
+ border-radius: 8px;
+ /* No border / no background — the surrounding +Add row provides the
+ bordered container, the camera is just an icon inside it. */
+ border: none;
+ background: transparent;
+}
+
+/* Read-mode overlay shown over the textarea for plain notes. Renders
+ linkified content so URLs are tappable. Click anywhere off a link
+ to flip to edit mode (focuses the underlying textarea). */
+.note-form-content-reader {
+ font-size: 15px;
+ line-height: 1.55;
+ padding: 10px 6px;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ color: var(--fg);
+ cursor: text;
+ min-height: 40px;
+}
+
+/* Per-row read mode for todo items — replaces the bare with a
+ clickable linkified span when the value contains a URL. */
+.note-fullscreen-overlay .note-cl-text-reader {
+ flex: 1;
+ padding: 7px 6px;
+ font-size: 14px;
+ line-height: 1.4;
+ min-height: 32px;
+ display: inline-flex;
+ align-items: center;
+ word-break: break-word;
+ color: var(--fg);
+ cursor: text;
+}
+.note-fullscreen-overlay .note-cl-row.done .note-cl-text-reader {
+ opacity: 0.5;
+ text-decoration: line-through;
+}
+.note-fullscreen-overlay .note-form-icon-btn svg {
+ width: 16px;
+ height: 16px;
+}
+
+
+/* Tags input is JS-relocated into the bottom actions row, pinned
+ left of Cancel/Update. Style it as a dashed pill so it reads as
+ metadata rather than another control. */
+.note-fullscreen-overlay .note-form-actions-group .note-form-label {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-size: 13px;
+ padding: 8px 10px;
+ margin: 0 auto 0 0;
+ background: transparent;
+ border: 1px dashed color-mix(in srgb, var(--fg) 16%, transparent);
+ border-radius: 6px;
+ text-align: left;
+}
+
+.note-fullscreen-overlay .note-color-picker { order: 1; }
+.note-fullscreen-overlay .note-form-type-seg {
+ order: 3;
+ margin-left: auto; /* shove right */
+ /* The overlay enlarges the pills (padding 10px), but the base seg is only
+ 28px tall with overflow:hidden — which clipped them. Grow the track so
+ the bigger touch targets fit. */
+ height: 40px;
+ border-radius: 12px;
+}
+.note-fullscreen-overlay .note-form-actions-group {
+ order: 4;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 6px;
+}
+
+.notes-mobile-grabber { display: none; }
+/* Notes panel: mounted like a window, docked to the right on desktop. */
+.notes-pane-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ pointer-events: none;
+ animation: notes-backdrop-fade-in 180ms ease both;
+}
+.notes-pane-backdrop .notes-pane {
+ pointer-events: auto;
+}
+.notes-pane-backdrop:has(.notes-pane.modal-right-docked),
+.notes-pane-backdrop:has(.notes-pane.modal-left-docked) {
+ background: transparent;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+ pointer-events: none;
+}
+.notes-pane-backdrop:has(.notes-pane.modal-right-docked) .notes-pane,
+.notes-pane-backdrop:has(.notes-pane.modal-left-docked) .notes-pane {
+ pointer-events: auto;
+}
+.notes-pane-backdrop:has(.notes-pane.notes-window-fullscreen) {
+ background: transparent;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+}
+.notes-pane {
+ display: flex;
+ flex-direction: column;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow: hidden;
+ width: min(880px, 92vw);
+ height: min(80vh, 820px);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
+ box-sizing: border-box;
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ letter-spacing: -0.015em;
+ /* Smooth open: scale up + fade in from the centre. */
+ animation: notes-pane-enter 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
+ transform-origin: center center;
+ will-change: transform, opacity;
+}
+.notes-pane.modal-right-docked {
+ border-left: 1px solid var(--border);
+ border-radius: 0;
+ box-shadow: -4px 0 14px rgba(0, 0, 0, 0.18);
+ transition: none !important;
+}
+.notes-pane.modal-left-docked {
+ border-right: 1px solid var(--border);
+ border-radius: 0;
+ box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18);
+ transition: none !important;
+}
+.notes-pane-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ justify-content: space-between;
+ padding: 0 0 6px;
+ margin: 10px 10px 0;
+ border-bottom: 1px solid var(--border);
+ background: inherit;
+ flex-shrink: 0;
+ flex-wrap: nowrap;
+ min-height: 30px;
+ overflow: hidden;
+}
+
+/* Archive mode — sepia tint over the panel + a header strip so it's clearly
+ a different view. Active reminders sort + glow rules don't apply here. */
+.notes-pane-archive {
+ background: color-mix(in srgb, #b48a4a 6%, var(--bg));
+}
+.notes-pane-archive .notes-pane-header {
+ background: color-mix(in srgb, #b48a4a 12%, var(--bg));
+ border-bottom-color: color-mix(in srgb, #b48a4a 35%, var(--border));
+}
+.notes-pane-archive .notes-pane-body {
+ background: color-mix(in srgb, #b48a4a 4%, var(--bg));
+}
+/* Smooth the archive↔active swap so the colour tint eases in/out instead
+ of snapping. Covers the pane, its header, and its body. */
+.notes-pane,
+.notes-pane .notes-pane-header,
+.notes-pane .notes-pane-body {
+ transition: background 0.28s ease, border-color 0.28s ease;
+}
+.notes-pane-archive .notes-pane-title::after {
+ content: 'Archive';
+ margin-left: 8px;
+ padding: 2px 8px 2px 22px;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 1px;
+ text-transform: uppercase;
+ color: #b48a4a;
+ background-color: color-mix(in srgb, #b48a4a 18%, transparent);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b48a4a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='5' rx='1'/%3E%3Cpath d='M4 8v11a2 2 0 002 2h12a2 2 0 002-2V8'/%3E%3Cpath d='M10 12h4'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: 6px center;
+ background-size: 11px 11px;
+ border: 1px solid color-mix(in srgb, #b48a4a 45%, transparent);
+ border-radius: 10px;
+ vertical-align: middle;
+}
+.notes-pane-archive .note-card {
+ background: color-mix(in srgb, #b48a4a 8%, var(--panel));
+ border-color: color-mix(in srgb, #b48a4a 25%, var(--border));
+}
+.notes-pane-header .doc-action-icon-btn {
+ flex-shrink: 0;
+}
+.notes-pane-header .notes-pane-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 0;
+ flex-shrink: 1;
+}
+.notes-pane-footer {
+ padding: 6px 8px;
+ border-top: 1px solid var(--border);
+ background: var(--panel);
+ flex-shrink: 0;
+ display: flex;
+ gap: 6px;
+}
+.notes-new-btn {
+ background: none;
+ border: 1px dashed color-mix(in srgb, var(--accent) 40%, transparent);
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+ border-radius: 6px;
+ padding: 6px 0;
+ flex: 1;
+ cursor: pointer;
+ font-size: 11px;
+ font-family: inherit;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ transition: border-color 0.15s, color 0.15s;
+}
+.notes-new-btn:hover {
+ border-color: var(--accent);
+ color: var(--fg);
+}
+.notes-pane-title {
+ /* Match the other tool headers (.modal-header h4). */
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ font-size: 1rem;
+ font-weight: 600;
+ letter-spacing: -0.03em;
+ color: var(--red);
+ white-space: nowrap;
+ height: 24px;
+ line-height: 22px;
+ padding: 0 6px;
+ margin: 6px 0 0 0;
+ display: inline-block;
+ vertical-align: middle;
+ /* Optical nudge — sits 1px above its row baseline on desktop, more on mobile
+ where the surrounding controls are taller. Overridden in the mobile
+ media block below. */
+ position: relative;
+ top: -1px;
+ box-sizing: border-box;
+}
+@media (max-width: 768px) {
+ .notes-pane-title { top: -4px; }
+ .notes-pane-title svg {
+ position: relative;
+ top: -2px;
+ }
+ /* Mobile header is tight — keep Archive / Toggle View icon-only. */
+ .notes-header-btn-label { display: none; }
+ .notes-header-text-btn { width: 32px !important; padding: 0 !important; }
+}
+.notes-header-btn-label {
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 1;
+ letter-spacing: 0.2px;
+ white-space: nowrap;
+}
+.notes-header-text-btn {
+ width: auto !important;
+ padding: 0 8px !important;
+}
+.notes-pane-body {
+ flex: 1;
+ overflow-y: auto;
+ scroll-behavior: smooth;
+ padding: 10px 8px 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ scrollbar-width: thin;
+}
+.notes-pane-body > .doclib-chip-scroll-frame:has(> .notes-labels-bar) {
+ /* The shared Library wrapper grows to fill available flex space. Notes
+ needs this to remain a content-height toolbar or it can create a huge
+ blank gap above the cards after filtering/re-rendering. */
+ flex: 0 0 auto;
+ width: 100%;
+}
+
+.note-card.note-card-sliding-out {
+ transform: translateX(120%);
+ opacity: 0;
+ transition: transform 0.32s ease-in, opacity 0.32s ease-in;
+ pointer-events: none;
+}
+
+/* ⋯ corner menu button (replaced the copy corner button) */
+.note-card-corner-menu {
+ background: none;
+ border: none;
+ color: var(--fg);
+ cursor: pointer;
+ padding: 2px;
+ opacity: 0.5;
+ transition: opacity 0.15s;
+ display: inline-flex;
+}
+.note-card-corner-menu:hover { opacity: 0.9; }
+/* Dropdown spawned by the ⋯ menu */
+.note-corner-menu-dropdown {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ padding: 4px;
+ min-width: 168px;
+ font-size: 12px;
+ animation: ncm-pop 0.12s ease-out both;
+}
+@keyframes ncm-pop { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
+.note-corner-menu-dropdown .ncm-item {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ width: 100%;
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ text-align: left;
+ padding: 8px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+}
+.note-corner-menu-dropdown .ncm-item:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.note-checkbox-agent {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ margin: 0 1px;
+ border: 0;
+ background: transparent;
+ color: var(--accent, var(--red));
+ box-shadow: none;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.12s, color 0.12s;
+}
+.note-checkbox-agent:hover {
+ background: transparent;
+ opacity: 1 !important;
+}
+.note-checkbox-agent.is-agent-stream-complete {
+ color: #50fa7b;
+ opacity: 0.9;
+}
+.note-checkbox-agent svg {
+ display: block;
+}
+
+.note-card {
+ /* Same tint that .doclib-card uses so a default (uncolored) note
+ visually separates from the panel background it sits on. */
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ /* Extra right padding so the corner buttons (pin / edit / copy / done)
+ never collide with the title, and a min-height so an empty/short
+ note is still tall enough to fit that row of corner controls. */
+ padding: 10px 14px;
+ padding-right: 30px;
+ min-height: 64px;
+ cursor: default;
+ transition: background 0.15s;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ /* Keep drawings/images clipped to the card so they can't spill outside it —
+ but allow a small overhang (the pin bubble sits at right:-8px and would
+ otherwise be cropped). overflow-clip-margin extends the clip-box without
+ letting full images bleed past. */
+ overflow: clip;
+ overflow-clip-margin: 14px;
+ /* Don't shrink inside the flex-column list pane — otherwise cards squish to
+ fit (clipped/too short) and the container never overflows, so it won't
+ scroll. Keeping natural height makes the list overflow → scrollable. */
+ flex-shrink: 0;
+}
+.note-card:hover:not([class*="note-color-"]) {
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+}
+.note-card { position: relative; }
+.note-card-image { max-width: 100%; }
+
+/* Mobile: bigger, more legible note cards + a sticky quick-add bar so you can
+ add a to-do without scrolling back up; and keep media inside the card. */
+@media (max-width: 768px) {
+ .note-card { padding: 14px 16px; padding-right: 34px; min-height: 84px; font-size: 15px; }
+ .note-card-image { max-height: 260px; max-width: 100%; object-fit: cover; }
+ /* Sticky create-todo bar — pin it to the top of the scrolling list pane.
+ Safe now that the cards no longer shrink (the list actually overflows). */
+ .notes-quick-add {
+ position: sticky;
+ top: -8px;
+ z-index: 12;
+ flex-shrink: 0;
+ margin-top: 0;
+ box-shadow: 0 -8px 0 8px var(--panel), 0 8px 0 0 var(--panel);
+ }
+}
+.note-card-pinned {
+ border-top: 2px solid color-mix(in srgb, var(--fg) 30%, transparent);
+}
+.note-card.note-card-reminder-fired {
+ animation: note-reminder-pulse 1.2s ease-in-out 2;
+}
+/* Sticky outside glow — applied to the exact note whose reminder fired.
+ It stays until the user interacts with that card. */
+.note-card.note-card-reminder-fired-sticky {
+ position: relative;
+ outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 36%, transparent);
+ outline-offset: 2px;
+ animation: note-reminder-glow 1.6s ease-in-out infinite;
+}
+
+/* Hover-only buttons (Google Keep style) */
+.note-card-select,
+.note-card-pin {
+ position: absolute;
+ top: -8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 50%;
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg);
+ opacity: 0.7;
+ z-index: 2;
+ transition: opacity 0.15s, transform 0.15s, background 0.15s;
+}
+/* Pencil + checkmark glyphs both have empty space along the top of their
+ 24×24 viewBox, so flex-centering leaves them sitting visually high.
+ Nudge the inner SVG down a touch to compensate. */
+.note-card-edit-corner svg,
+.note-card-done svg {
+ position: relative;
+ top: 0.5px;
+}
+.note-card-edit-corner {
+ position: absolute;
+ /* Both Edit + Done at top-right: Edit on the left (right:40px),
+ Done on the right (right:6px). */
+ top: 6px; right: 40px;
+ width: 28px; height: 28px;
+ /* Fully invisible at rest. Materializes (with a solid panel fill blocking
+ any text underneath) only when the card is hovered or the button is
+ focused. Same rule on touch — opacity stays 0 until the card is tapped
+ to expose actions. */
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ cursor: pointer; padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg); opacity: 0;
+ touch-action: manipulation;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+ z-index: 2;
+}
+.note-card:hover .note-card-edit-corner {
+ opacity: 0.85;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.note-card-edit-corner:hover,
+.note-card-edit-corner:focus-visible {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--accent) 18%, var(--panel)) !important;
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border)) !important;
+}
+@media (hover: none) {
+ .note-card-edit-corner {
+ opacity: 0.7;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ }
+}
+/* Copy corner — bottom-right counterpart to the edit/done corner buttons.
+ Round, subtle, hover-reveal on desktop and always faintly visible on
+ touch so mobile users can tap it without needing hover. */
+.note-card-copy-corner {
+ position: absolute;
+ bottom: 6px;
+ right: 6px;
+ top: auto;
+ width: 26px;
+ height: 26px;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg);
+ opacity: 0;
+ touch-action: manipulation;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+ z-index: 2;
+}
+.note-card:hover .note-card-copy-corner {
+ opacity: 0.7;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.note-card-copy-corner:hover,
+.note-card-copy-corner:focus-visible {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, var(--panel)) !important;
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border)) !important;
+}
+@media (hover: none) {
+ .note-card-copy-corner {
+ opacity: 0.6;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ }
+}
+/* Unarchive corner reuses the pencil's spot in archive view. */
+.note-card-unarchive-corner:hover,
+.note-card-unarchive-corner:focus-visible {
+ background: color-mix(in srgb, var(--accent) 22%, transparent) !important;
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border)) !important;
+ color: var(--accent);
+}
+
+/* "Done" pill in the bottom-right of every active note card. Visible-only on
+ hover (and always on touch) so it's clearly readable but doesn't clutter
+ the grid at rest. Distinct from the edit pencil so it's obvious what it
+ does — green checkmark + label, not just an icon. */
+/* Matches .note-card-edit-corner footprint (22×22 circle), just anchored
+ to the bottom-right corner instead of top-right. */
+.note-card-done {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ width: 28px;
+ height: 28px;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 50%;
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--green, #98c379);
+ opacity: 0;
+ touch-action: manipulation;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+ z-index: 2;
+}
+.note-card:hover .note-card-done {
+ opacity: 0.85;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+.note-card-done:hover,
+.note-card-done:focus-visible {
+ opacity: 1 !important;
+ background: color-mix(in srgb, var(--green, #98c379) 22%, var(--panel)) !important;
+ border-color: color-mix(in srgb, var(--green, #98c379) 50%, var(--border)) !important;
+}
+@media (hover: none) {
+ .note-card-done {
+ opacity: 0.7;
+ background: var(--panel);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ }
+}
+
+/* Copy corner — bottom-right of the card. Same opaque-fill pattern as
+ trash/unarchive so the underlying text/image never bleeds through. */
+.note-card-corner-copy {
+ position: absolute;
+ bottom: 6px;
+ right: 6px;
+ width: 28px;
+ height: 28px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ border-radius: 50%;
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg);
+ visibility: hidden;
+ touch-action: manipulation;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+ z-index: 2;
+}
+.note-card-corner-copy svg { position: relative; top: 1px; opacity: 0.85; transition: opacity 0.15s; }
+.note-card-corner-copy:hover,
+.note-card-corner-copy:focus-visible {
+ background: color-mix(in srgb, var(--accent) 18%, var(--panel));
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
+ color: var(--accent);
+}
+.note-card-corner-copy:hover svg { opacity: 1; }
+@media (hover: none) {
+ .note-card-corner-copy { visibility: visible; }
+ .note-card-corner-copy svg { opacity: 0.7; }
+}
+
+/* ── Archive view corners — trash (top-left) + unarchive (top-right) ── */
+/* Background fill stays fully opaque (var(--panel)) so the note text or
+ image behind doesn't bleed through. Visibility is gated by `visibility`
+ + a faded ICON instead of `opacity` on the whole button. */
+.note-card-corner-trash,
+.note-card-corner-unarchive {
+ position: absolute;
+ top: 6px;
+ width: 28px;
+ height: 28px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ border-radius: 50%;
+ cursor: pointer;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--fg);
+ visibility: hidden;
+ touch-action: manipulation;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+ z-index: 2;
+}
+.note-card-corner-trash svg,
+.note-card-corner-unarchive svg { opacity: 0.85; transition: opacity 0.15s; }
+.note-card-corner-trash:hover,
+.note-card-corner-trash:focus-visible {
+ background: color-mix(in srgb, var(--red) 18%, var(--panel));
+ border-color: color-mix(in srgb, var(--red) 50%, var(--border));
+ color: var(--red);
+}
+.note-card-corner-trash:hover svg,
+.note-card-corner-unarchive:hover svg { opacity: 1; }
+.note-card-corner-unarchive:hover,
+.note-card-corner-unarchive:focus-visible {
+ background: color-mix(in srgb, var(--accent) 18%, var(--panel));
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
+ color: var(--accent);
+}
+@media (hover: none) {
+ .note-card-corner-trash,
+ .note-card-corner-unarchive { visibility: visible; }
+ .note-card-corner-trash svg,
+ .note-card-corner-unarchive svg { opacity: 0.7; }
+}
+
+/* ─── Auto-AI ✨ chip on note cards ─────────────────────────────────── */
+.note-card-ai-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ margin-left: 6px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 500;
+ font-family: inherit;
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
+ color: var(--accent, var(--red));
+ cursor: pointer;
+ transition: background 0.12s, transform 0.12s, box-shadow 0.12s;
+ vertical-align: middle;
+}
+.note-card-ai-chip:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
+ transform: translateY(-1px);
+ box-shadow: 0 2px 6px color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
+}
+.note-card-ai-chip svg {
+ flex-shrink: 0;
+ animation: note-ai-shine 2.4s ease-in-out infinite;
+}
+.note-card-ai-solving {
+ cursor: default;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+}
+.note-card-ai-solving svg { animation: spin 0.9s linear infinite; filter: none; }
+.note-card-ai-done { background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent); }
+.note-card-ai-done svg { animation: none; }
+#notes-auto-ai-toggle.active { color: var(--accent, var(--red)); }
+
+/* ─── AI improve button (note form header) ───────────────────────────── */
+.note-form-improve-btn {
+ width: 38px !important;
+ height: 38px !important;
+ color: var(--accent, var(--red));
+}
+.note-form-improve-btn svg {
+ width: 22px !important;
+ height: 22px !important;
+ animation: note-ai-shine 2.4s ease-in-out infinite;
+}
+.note-form-improve-btn.busy svg {
+ animation: spin 0.9s linear infinite;
+ filter: none;
+}
+
+.note-card:hover .note-card-select,
+.note-card:hover .note-card-pin,
+.note-card-selected .note-card-select,
+.note-card-pinned .note-card-pin {
+ display: flex;
+}
+.note-card-select:hover, .note-card-pin:hover {
+ opacity: 1;
+ transform: scale(1.15);
+ background: color-mix(in srgb, var(--accent) 15%, var(--bg));
+}
+.note-card-pin.active {
+ display: flex;
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--bg));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
+ color: var(--accent, var(--red));
+ opacity: 1;
+}
+.note-card-selected .note-card-select {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+ opacity: 1;
+}
+
+/* Select-mode checkbox on cards (mirrors docs library) */
+.note-card-cb {
+ position: absolute;
+ top: 6px;
+ left: 6px;
+ z-index: 3;
+ margin: 0;
+}
+.note-card-selectmode {
+ padding-left: 28px;
+ cursor: pointer;
+}
+.note-card-selectmode:hover { background: color-mix(in srgb, var(--fg) 3%, transparent) !important; }
+
+@media (max-width: 768px) {
+ body.notes-mobile-mode .note-card-selectmode {
+ animation: notes-select-jiggle 0.34s cubic-bezier(0.42, 0, 0.58, 1) infinite;
+ transform-origin: 50% 50%;
+ }
+ body.notes-mobile-mode .note-card-selectmode:nth-child(2n) {
+ animation-delay: -0.08s;
+ }
+ /* Keep the subtle edit-mode cue only on Notes and Gallery. */
+ :is(
+ body.gallery-selecting .gallery-card
+ ) {
+ animation: notes-select-jiggle 0.34s cubic-bezier(0.42, 0, 0.58, 1) infinite;
+ transform-origin: 50% 50%;
+ }
+ :is(
+ body.gallery-selecting .gallery-card:nth-child(2n)
+ ) { animation-delay: -0.11s; }
+}
+@media (prefers-reduced-motion: reduce) {
+ body.notes-mobile-mode .note-card-selectmode,
+ body.gallery-selecting .gallery-card { animation: none; }
+}
+
+/* Bottom action row — hidden everywhere. The archive/delete actions and the
+ color picker now live inside the edit form (click the pencil corner). */
+.note-card-actions { display: none; }
+.note-agent-menu {
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ padding: 4px 0;
+ min-width: 140px;
+ font-size: 12px;
+}
+.note-card-colors {
+ display: flex;
+ gap: 3px;
+}
+/* Re-asserted AFTER the base rule above so the mobile-hide actually
+ wins the cascade — earlier @media (hover:none) block was being
+ overridden because the unconditional display:flex rule came later. */
+@media (hover: none) {
+ .note-card-colors { display: none; }
+}
+.note-card-color-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ cursor: pointer;
+ border: 1.5px solid color-mix(in srgb, var(--fg) 20%, transparent);
+ transition: transform 0.15s, border-color 0.15s;
+}
+.note-card-color-dot:hover {
+ transform: scale(1.25);
+ border-color: var(--fg);
+}
+.note-card-color-dot.active {
+ border-color: var(--fg);
+ border-width: 2px;
+}
+.note-card-action {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 3px;
+ color: var(--fg);
+ opacity: 0.4;
+ display: flex;
+ align-items: center;
+ border-radius: 3px;
+ transition: opacity 0.15s, background 0.15s;
+}
+
+/* Note image */
+.note-card-image {
+ width: 100%;
+ max-height: 180px;
+ object-fit: cover;
+ border-radius: 4px;
+ margin: 4px 0;
+ display: block;
+}
+.note-form-image-wrap {
+ position: relative;
+ margin: 4px 0;
+}
+.note-form-image {
+ width: 100%;
+ max-height: 200px;
+ object-fit: cover;
+ border-radius: 4px;
+ display: block;
+ /* Images are draggable=true by default; that catches the mousedown
+ and beats the X button to the click. Disabling drag at the CSS
+ level (the -webkit-user-drag prop) keeps the X button reliably
+ clickable even if the JS forgets to set draggable="false" on the
+ element. Unprefixed user-drag was never standardized — Firefox
+ drops it with a warning. */
+ -webkit-user-drag: none;
+ user-select: none;
+}
+.note-form-image-rm {
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ z-index: 5;
+ background: rgba(0,0,0,0.65);
+ border: none;
+ color: #fff;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 20px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.note-form-photo-btn {
+ flex: 0 0 auto;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ border-radius: 4px;
+ width: auto;
+ min-width: 32px;
+ height: 32px;
+ padding: 0 6px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ opacity: 1;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s;
+}
+.note-form-photo-btn:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
+
+/* Quick-add bar (always at top) */
+.notes-quick-add {
+ display: flex;
+ align-items: center;
+ transform: translateY(-2px);
+ gap: 4px;
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, var(--border));
+ border-radius: 6px;
+ /* Toggle pill sits at the very left of the bar — no inset padding. The
+ pill's own border provides visual breathing room from the wrapper edge. */
+ padding: 4px 6px 4px 4px;
+ margin-top: 2px;
+ margin-bottom: 8px;
+ transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
+ /* Subtle idle glow so the field reads as "you can type here" without
+ the user having to click it first. Fades out the moment the user
+ hovers or focuses it. */
+ animation: notes-quick-pulse 2.8s ease-in-out infinite;
+}
+.notes-quick-add:hover {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ animation: none;
+}
+.notes-quick-add:focus-within {
+ border-color: var(--accent, var(--red));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
+ background: color-mix(in srgb, var(--fg) 3%, var(--panel));
+ animation: none;
+}
+.notes-quick-input {
+ flex: 1;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--fg);
+ caret-color: var(--accent, var(--red));
+ font-family: inherit;
+ font-size: 12px;
+ padding: 6px 0;
+}
+.notes-quick-icon {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.45;
+ cursor: pointer;
+ padding: 5px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ transition: opacity 0.15s, background 0.15s;
+}
+.notes-quick-icon:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+/* 2-pill Note/Todo toggle in the quick-add bar — same look as the form's
+ type-seg but slimmer to fit the compact row. */
+.notes-quick-type-seg {
+ display: inline-flex;
+ height: 28px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ position: relative;
+ flex-shrink: 0;
+ /* Negative order pushes the toggle BEFORE the wrapper's ::before caret
+ hint, so the blinking | sits between the toggle and the input rather
+ than at the far-left edge. */
+ order: -1;
+}
+.notes-quick-type-seg::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 50%;
+ height: 100%;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ border-radius: 6px;
+ transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
+ z-index: 0;
+}
+.notes-quick-type-pill {
+ background: none;
+ border: none;
+ color: color-mix(in srgb, var(--fg) 40%, transparent);
+ cursor: pointer;
+ padding: 0 11px;
+ font-family: inherit;
+ transition: color 0.2s;
+ white-space: nowrap;
+ height: 100%;
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ z-index: 1;
+}
+.notes-quick-type-pill svg {
+ width: 16px;
+ height: 16px;
+}
+.notes-quick-type-pill:not(.active):hover {
+ color: color-mix(in srgb, var(--fg) 65%, transparent);
+}
+.notes-quick-type-pill.active,
+.notes-quick-type-pill.active:hover,
+.notes-quick-type-pill.active:focus {
+ color: var(--fg);
+ cursor: default;
+}
+/* Mobile: thumb-sized Note/Todo toggle. The 22px desktop pill is too
+ small to hit reliably on a phone. */
+@media (max-width: 768px) {
+ .notes-quick-type-seg {
+ height: 36px;
+ border-radius: 10px;
+ }
+ .notes-quick-type-seg::before {
+ border-radius: 9px;
+ }
+ .notes-quick-type-pill {
+ padding: 0 14px;
+ }
+ .notes-quick-type-pill svg {
+ width: 17px;
+ height: 17px;
+ }
+ /* Match the input + photo button heights to the bigger toggle so the
+ row reads as one consistent bar. */
+ .notes-quick-input {
+ height: 36px;
+ font-size: 15px;
+ }
+ .notes-quick-icon {
+ padding: 9px;
+ }
+ .notes-quick-icon svg {
+ width: 18px;
+ height: 18px;
+ }
+}
+.notes-empty-msg {
+ text-align: center;
+ /* 0.4 dropped this empty-state text to ~2.8:1; 0.65 keeps it readable
+ (WCAG AA) while staying visibly secondary. */
+ opacity: 0.65;
+ padding: 30px 20px;
+ font-size: 11px;
+}
+
+/* Loading skeleton */
+.notes-skeleton {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 4px;
+}
+.notes-skeleton-card {
+ height: 72px;
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ background: linear-gradient(
+ 90deg,
+ color-mix(in srgb, var(--fg) 4%, transparent) 0%,
+ color-mix(in srgb, var(--fg) 10%, transparent) 50%,
+ color-mix(in srgb, var(--fg) 4%, transparent) 100%
+ );
+ background-size: 200% 100%;
+ animation: notes-skeleton-shimmer 1.4s ease-in-out infinite;
+}
+
+/* Type toggle (note vs todo) */
+.note-form-type-row {
+ display: flex;
+ gap: 4px;
+ margin-top: -2px;
+}
+/* Reminder bell button */
+/* Mobile-only: bell icon in the note editor is accent-coloured so it pops as
+ the primary "set a reminder" affordance. The Archive button is hidden — the
+ Update (✓) button morphs into an Archive action when the user opens a note
+ and clicks without making any edits (see notes.js `archive-mode` toggle). */
+@media (max-width: 768px) {
+ .note-form-remind-btn { color: var(--accent, var(--red)) !important; }
+ .note-form-remind-btn > svg { color: var(--accent, var(--red)); }
+ .note-form-archive-btn { display: none !important; }
+ .notes-pane #notes-archive-toggle { display: none !important; }
+ .note-form-save.archive-mode {
+ color: var(--accent, var(--red)) !important;
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent) !important;
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent) !important;
+ }
+}
+.note-form-remind-btn {
+ flex: 0 0 auto;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ border-radius: 4px;
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 1;
+ position: relative;
+ top: -2px;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
+}
+.note-form-remind-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
+.note-form-remind-btn.has-date { color: var(--accent); border-color: var(--accent); }
+
+/* Calendar event form + notes form: jingle the little bell when the user
+ picks a reminder. Anchored near the top of the bell so the swing reads
+ as the bell clanging side-to-side. */
+.cal-remind-bell,
+.note-form-remind-btn > svg {
+ transform-origin: 50% 4px;
+ transition: transform 0.1s ease;
+}
+.cal-remind-bell.jingling,
+.note-form-remind-btn > svg.jingling {
+ animation: cal-bell-jingle 0.65s cubic-bezier(0.36, 0, 0.66, -0.56) both;
+}
+@media (prefers-reduced-motion: reduce) {
+ .cal-remind-bell.jingling,
+ .note-form-remind-btn > svg.jingling { animation: none; }
+}
+
+/* Reminder tag inside form */
+.note-form-reminder-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+.note-reminder-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
+ color: var(--accent);
+ border: none;
+ border-radius: 12px;
+ padding: 4px 4px 4px 9px;
+ font-size: 10px;
+ font-family: inherit;
+ cursor: pointer;
+ font-weight: 500;
+}
+.note-reminder-tag:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
+.note-reminder-tag-x {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--accent) 25%, transparent);
+ font-size: 12px;
+ line-height: 1;
+ position: relative;
+ top: -2px;
+}
+.note-reminder-tag-x:hover { background: var(--red); color: #fff; }
+
+/* Reminder tag on card */
+.note-card-reminder {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: color-mix(in srgb, var(--accent) 12%, transparent);
+ color: var(--accent);
+ font-size: 9px;
+ font-weight: 500;
+ padding: 2px 7px;
+ border-radius: 10px;
+ align-self: flex-start;
+ margin-top: 2px;
+}
+.note-card-reminder.overdue {
+ background: color-mix(in srgb, var(--red) 18%, transparent);
+ color: var(--red);
+}
+
+/* Reminder dropdown menu */
+.note-reminder-menu {
+ position: fixed;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 6px 24px rgba(0,0,0,0.25);
+ /* Above the fullscreen note overlay (z-index: 10500) — otherwise the
+ reminder picker opens hidden behind it on mobile and reads as
+ "broken". */
+ z-index: 12000;
+ min-width: 220px;
+ padding: 6px 0;
+ font-family: inherit;
+ font-size: 12px;
+}
+.note-reminder-menu-title {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ opacity: 0.5;
+ padding: 6px 14px 4px;
+ letter-spacing: 0.5px;
+}
+.note-reminder-menu-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ padding: 7px 14px;
+ cursor: pointer;
+ text-align: left;
+ transition: background 0.1s;
+}
+.note-reminder-menu-item:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }
+.note-reminder-menu-item.active { color: var(--accent); }
+.note-reminder-menu-sub {
+ font-size: 10px;
+ opacity: 0.5;
+ margin-left: 12px;
+}
+.note-reminder-menu-check { color: var(--accent); font-size: 12px; }
+.note-reminder-menu-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 4px 0;
+}
+.note-reminder-menu-picker {
+ padding: 6px 14px 8px;
+}
+.note-reminder-date-input {
+ width: 100%;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ padding: 6px 8px;
+ outline: none;
+ color-scheme: dark light;
+ box-sizing: border-box;
+}
+.note-reminder-date-input:focus { border-color: var(--accent); }
+.note-reminder-menu-confirm {
+ font-weight: 600;
+ color: var(--accent);
+ justify-content: center;
+}
+.note-reminder-menu-confirm.disabled,
+.note-reminder-menu-confirm[disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ color: var(--fg);
+}
+.note-reminder-menu-arrow {
+ opacity: 0.5;
+ margin-left: 8px;
+ font-size: 14px;
+ line-height: 1;
+}
+.note-reminder-menu-back {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ width: 100%;
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ opacity: 0.7;
+ padding: 6px 12px;
+ cursor: pointer;
+ text-align: left;
+ transition: opacity 0.1s, background 0.1s;
+}
+.note-reminder-menu-back:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 6%, transparent); }
+.note-reminder-menu-arrow-back {
+ font-size: 14px;
+ line-height: 1;
+}
+.note-reminder-menu-sublabel {
+ font-size: 10px;
+ opacity: 0.5;
+ padding: 6px 14px 2px;
+ letter-spacing: 0.3px;
+}
+.note-reminder-weekday-row {
+ display: flex;
+ gap: 4px;
+ padding: 4px 12px 8px;
+ flex-wrap: wrap;
+}
+.note-reminder-day-chip {
+ flex: 1 1 0;
+ min-width: 24px;
+ height: 28px;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s, border-color 0.1s, color 0.1s;
+ padding: 0;
+}
+.note-reminder-day-chip:hover {
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
+}
+.note-reminder-day-chip.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+}
+
+.note-form-type-btn,
+.note-form-type-toggle {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ border-radius: 4px;
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 1;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+}
+.note-form-type-toggle:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
+.note-form-type-toggle[data-type="todo"] { color: var(--accent); }
+
+/* Note | Todo | Draw segmented toggle — mirrors .mode-toggle (agent/chat) */
+.note-form-type-seg {
+ display: inline-flex;
+ height: 32px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
+ flex-shrink: 0;
+}
+.note-form-type-pill {
+ background: none;
+ border: none;
+ color: color-mix(in srgb, var(--fg) 40%, transparent);
+ cursor: pointer;
+ padding: 0 12px;
+ font-size: 13px;
+ font-weight: 500;
+ font-family: inherit;
+ transition: color 0.2s;
+ white-space: nowrap;
+ height: 100%;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ position: relative;
+ z-index: 1;
+}
+.note-form-type-pill:not(.active):hover { color: color-mix(in srgb, var(--fg) 60%, transparent); }
+.note-form-type-pill.active,
+.note-form-type-pill.active:hover,
+.note-form-type-pill.active:focus {
+ color: var(--fg);
+ cursor: default;
+}
+@media (max-width: 480px) {
+ .note-form-type-pill span { display: none; }
+ .note-form-type-pill { padding: 0 8px; }
+}
+
+.note-card-action:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+.note-card-delete:hover { color: var(--red); }
+
+/* Tag/label */
+.note-card-label {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 2px;
+}
+.note-card-label-chip {
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+ color: var(--accent);
+ border-radius: 999px;
+ padding: 2px 6px;
+ font: inherit;
+ font-size: 9px;
+ font-weight: 600;
+ line-height: 1.2;
+ cursor: pointer;
+}
+.note-card-label-chip:hover {
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
+}
+
+/* Search bar */
+.notes-search-bar {
+ padding: 6px 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.notes-select-trigger {
+ position: relative;
+ top: 3px;
+ flex-shrink: 0;
+ background: var(--bg);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-family: inherit;
+ font-size: 11px;
+ height: 30px;
+ padding: 0 12px;
+ cursor: pointer;
+ opacity: 0.75;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
+}
+.notes-select-trigger:hover { opacity: 1; border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
+.notes-select-trigger.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ color: var(--accent);
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
+}
+.notes-search-bar .memory-search-input {
+ flex: 1;
+ min-width: 0;
+ height: 30px;
+ min-height: 30px;
+ font-size: 11px;
+ padding: 0 10px 0 28px;
+ background-color: var(--bg);
+ background-image: url("data:image/svg+xml;utf8, ");
+ background-repeat: no-repeat;
+ background-position: 9px center;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ font-family: inherit;
+ outline: none;
+ transition: border-color 0.15s, background-color 0.15s;
+}
+.notes-search-bar .memory-search-input:focus {
+ border-color: var(--red);
+}
+
+/* Grid view: true CSS Grid plus JS-computed row spans, so cards masonry-pack
+ vertically without the jumpy rebalancing of CSS columns. */
+.notes-pane.notes-view-grid .notes-pane-body {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
+ grid-auto-rows: 4px;
+ grid-auto-flow: dense;
+ gap: 0 8px;
+ padding: 10px 8px 8px;
+ overflow-x: hidden;
+ align-items: start;
+ align-content: start;
+}
+.notes-pane.notes-view-grid .note-card {
+ margin: 0 0 8px;
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.notes-pane.notes-view-grid .note-form,
+.notes-pane.notes-view-grid .notes-quick-add {
+ grid-column: 1 / -1;
+ margin-bottom: 0;
+ grid-row-end: span 16;
+}
+/* Edit form in grid view: span the full row and size naturally — same shape
+ as the new-note form. The earlier max-width: 380px + max-height: 220px
+ inner-scroll combo read as a tiny popup floating in a wider panel and
+ didn't actually fit the form's content. */
+.notes-pane.notes-view-grid .note-form {
+ width: 100%;
+ max-width: none;
+ margin-left: 0;
+ margin-right: 0;
+ box-sizing: border-box;
+}
+/* Pinned section break: the first unpinned card jumps to column 1, leaving
+ any leftover cell in the pinned row empty. Reads as a visual divider
+ between pinned and unpinned without needing a separator element. */
+.notes-pane.notes-view-grid .note-card-pinned + .note-card:not(.note-card-pinned) {
+ grid-column-start: 1;
+}
+/* Mobile: 2-lane masonry via the CSS-Grid row-span trick. Each card spans
+ N tiny rows (4px each) based on its measured height; the grid auto-places
+ cards into the two columns so left/right lanes flow independently — no
+ row alignment between them. JS in notes.js sets `grid-row-end: span N`
+ on every card after render. Pure CSS multi-column was tried but its
+ `column-span: all` interaction with sticky positioning broke clicks on
+ the quick-add bar (Firefox mobile). */
+@media (max-width: 768px) {
+ .notes-pane.notes-view-grid .notes-pane-body {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ grid-auto-rows: 4px;
+ grid-auto-flow: dense;
+ gap: 0 8px;
+ align-content: start;
+ }
+ .notes-pane.notes-view-grid .note-card {
+ margin: -8px 0 6px;
+ transform: translateY(-12px);
+ /* grid-row-end set inline by JS (_applyMasonry) */
+ }
+ /* Full-width quick-add + edit form. Quick-add stays sticky at top so
+ the user can always reach it; it's NOT inside the masonry pack. */
+ .notes-pane.notes-view-grid .notes-label-toggle,
+ .notes-pane.notes-view-grid .notes-labels-bar,
+ .notes-pane.notes-view-grid .doclib-chip-scroll-frame:has(> .notes-labels-bar),
+ .notes-pane.notes-view-grid .notes-quick-add,
+ .notes-pane.notes-view-grid .note-form {
+ grid-column: 1 / -1;
+ }
+ .notes-pane.notes-view-grid .notes-label-toggle { grid-row: span 8; }
+ .notes-pane.notes-view-grid .notes-labels-bar { grid-row: span 16; }
+ .notes-pane.notes-view-grid .doclib-chip-scroll-frame:has(> .notes-labels-bar) { grid-row: span 16; }
+ .notes-pane.notes-view-grid .notes-quick-add { grid-row: span 12; }
+ .notes-pane.notes-view-grid .note-form {
+ width: 100%;
+ max-width: none;
+ margin-left: 0;
+ margin-right: 0;
+ box-sizing: border-box;
+ grid-row: auto / span 64;
+ }
+ .notes-pane.notes-view-grid .note-form:has(.note-form-type-seg.is-draw) {
+ grid-row: auto / span 152;
+ }
+ .notes-pane.notes-view-grid .note-form.note-form-new {
+ transform: translateY(-28px);
+ }
+ .notes-pane.notes-view-grid .notes-labels-bar {
+ padding-top: 0;
+ margin-top: -6px;
+ margin-bottom: 0;
+ }
+ .notes-pane.notes-view-grid .notes-quick-add {
+ margin-top: -32px;
+ margin-bottom: 4px;
+ }
+ /* Pinned-section break: first unpinned card jumps to column 1. */
+ .notes-pane.notes-view-grid .note-card-pinned + .note-card:not(.note-card-pinned) {
+ grid-column-start: 1;
+ }
+}
+
+/* Label filter bar */
+.notes-label-toggle {
+ align-self: flex-start;
+ margin: -6px 8px 2px;
+ top: 0;
+}
+.notes-search-bar .notes-label-toggle {
+ align-self: center;
+ flex: 0 0 auto;
+ height: 30px;
+ margin: 0;
+ top: 4px;
+ padding: 0 10px;
+ border-radius: 6px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+.notes-pane-body .doclib-chip-scroll-arrow.right {
+ top: calc(50% - 6px) !important;
+}
+.doclib-chip-scroll-frame:has(> #tasks-filter-chips) .doclib-chip-scroll-arrow {
+ top: calc(50% - 9px) !important;
+}
+
+@media (max-width: 768px) {
+ #doclib-modal .doclib-lang-chips {
+ position: relative;
+ top: -2px;
+ }
+ #doclib-chats-tidy-btn,
+ #doclib-chats-select-btn,
+ #doclib-arc-select-btn,
+ #doclib-research-tidy-btn,
+ #doclib-research-select-btn,
+ #doclib-tidy-btn,
+ #doclib-select-btn {
+ height: 30px !important;
+ min-height: 30px !important;
+ box-sizing: border-box;
+ }
+}
+
+/* Keep filter chips consistent across Skills, Tasks, and the document
+ library. Documents use the shared compact chip treatment as the baseline. */
+.skills-summary-chip,
+.memory-cat-chip,
+.doclib-chip {
+ 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;
+ font-family: inherit;
+ text-transform: none;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+}
+.skills-summary-chip:hover,
+.memory-cat-chip:hover,
+.doclib-chip:hover {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+}
+.skills-summary-chip.active,
+.memory-cat-chip.active,
+.doclib-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);
+}
+.notes-labels-bar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 0 8px 0;
+ margin-top: -6px;
+ margin-bottom: -2px;
+}
+.notes-label-chip {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ font-size: 10px;
+ padding: 3px 10px;
+ border-radius: 10px;
+ cursor: pointer;
+ font-family: inherit;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
+}
+.notes-label-chip:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
+}
+.notes-label-chip.active {
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ color: var(--accent);
+ border-color: color-mix(in srgb, var(--accent) 60%, transparent);
+ font-weight: 600;
+}
+.notes-label-chip-reminders {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+.notes-label-chip-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 14px;
+ height: 14px;
+ padding: 0 4px;
+ border-radius: 7px;
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ color: inherit;
+ font-size: 9px;
+ font-weight: 600;
+ margin-left: 3px;
+}
+.notes-label-chip-reminders.active .notes-label-chip-count {
+ background: color-mix(in srgb, var(--accent) 30%, transparent);
+ color: var(--accent);
+}
+.notes-label-chip-reminders.active.negated {
+ background: color-mix(in srgb, var(--red, #e55) 14%, transparent);
+ color: var(--red, #e55);
+ border-color: color-mix(in srgb, var(--red, #e55) 50%, transparent);
+}
+.notes-label-chip-reminders.active.negated .notes-label-chip-count {
+ background: color-mix(in srgb, var(--red, #e55) 25%, transparent);
+ color: var(--red, #e55);
+}
+.notes-label-clear-past {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ color: var(--red, #e55);
+ border-color: color-mix(in srgb, var(--red, #e55) 45%, var(--border));
+}
+.notes-label-clear-past:hover {
+ color: var(--red, #e55);
+ background: color-mix(in srgb, var(--red, #e55) 12%, transparent);
+ border-color: color-mix(in srgb, var(--red, #e55) 65%, var(--border));
+}
+/* Today + Goals filter chips — share styling, tint with a warm accent so
+ they read as "long-term work" alongside the cool reminder chip. */
+.notes-label-chip-today,
+.notes-label-chip-goals {
+ display: inline-flex;
+ align-items: center;
+ gap: 2px;
+}
+.notes-label-chip-today.active,
+.notes-label-chip-goals.active {
+ background: color-mix(in srgb, var(--accent-warm) 22%, transparent);
+ color: var(--accent-warm);
+ border-color: color-mix(in srgb, var(--accent-warm) 60%, transparent);
+}
+.notes-label-chip-today.active .notes-label-chip-count,
+.notes-label-chip-goals.active .notes-label-chip-count {
+ background: color-mix(in srgb, var(--accent-warm) 30%, transparent);
+ color: var(--accent-warm);
+}
+
+/* ── Goal cards ──────────────────────────────────────────────────────── */
+/* A goal note is structurally a checklist (note_type='goal') but visually
+ marked with a small Goal pill in the corner + a slightly warmer accent
+ so users can spot it at a glance among regular todos. */
+.note-card-goal {
+ border-left: 3px solid color-mix(in srgb, var(--accent-warm) 80%, transparent);
+}
+.note-goal-pill {
+ position: absolute;
+ top: 6px;
+ left: 8px;
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 2px 7px 2px 5px;
+ background: color-mix(in srgb, var(--accent-warm) 18%, transparent);
+ color: var(--accent-warm);
+ border-radius: 999px;
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ line-height: 1;
+ z-index: 2;
+}
+/* Description blurb above the checklist on goal cards */
+.note-goal-desc {
+ font-size: 12px;
+ opacity: 0.7;
+ margin: 4px 0 6px;
+ white-space: pre-wrap;
+ line-height: 1.4;
+}
+
+/* ── Goal form (Break-down editor) ───────────────────────────────────── */
+.note-form-goal {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.note-form-goal-desc {
+ width: 100%;
+ box-sizing: border-box;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 13px;
+ padding: 8px 10px;
+ resize: vertical;
+ min-height: 60px;
+}
+.note-form-goal-desc:focus {
+ outline: none;
+ border-color: color-mix(in srgb, var(--accent-warm) 60%, var(--border));
+}
+.note-form-goal-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.note-form-goal-ai {
+ display: inline-flex;
+ align-items: center;
+ background: color-mix(in srgb, var(--accent-warm) 16%, transparent);
+ color: var(--accent-warm);
+ border: 1px solid color-mix(in srgb, var(--accent-warm) 45%, transparent);
+ border-radius: 6px;
+ padding: 5px 10px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+}
+.note-form-goal-ai:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--accent-warm) 26%, transparent);
+ border-color: color-mix(in srgb, var(--accent-warm) 65%, transparent);
+}
+.note-form-goal-hint {
+ font-size: 11px;
+ opacity: 0.55;
+}
+
+/* ── Today view ──────────────────────────────────────────────────────── */
+.notes-today-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin: 4px 0 12px;
+}
+.notes-today-header {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: var(--accent-warm);
+ padding: 6px 10px 4px;
+}
+.notes-today-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.notes-today-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-left: 3px solid color-mix(in srgb, var(--accent-warm) 70%, transparent);
+ border-radius: 8px;
+ transition: opacity 0.2s, transform 0.2s;
+}
+.notes-today-row .note-check-dot {
+ flex-shrink: 0;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ border: 2px solid color-mix(in srgb, var(--fg) 40%, transparent);
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s;
+}
+.notes-today-row .note-check-dot:hover {
+ border-color: var(--accent-warm);
+ background: color-mix(in srgb, var(--accent-warm) 20%, transparent);
+}
+.notes-today-row.done .note-check-dot {
+ background: var(--accent-warm);
+ border-color: var(--accent-warm);
+}
+.notes-today-text {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.notes-today-title {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--accent-warm);
+ letter-spacing: 0.03em;
+ cursor: pointer;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.notes-today-step {
+ font-size: 14px;
+ color: var(--fg);
+ line-height: 1.35;
+}
+.notes-today-progress {
+ flex-shrink: 0;
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.6;
+ font-variant-numeric: tabular-nums;
+}
+.notes-empty {
+ text-align: center;
+ padding: 32px 16px;
+ font-size: 13px;
+ opacity: 0.55;
+ font-style: italic;
+}
+
+/* Bulk action bar */
+.notes-bulk-info {
+ font-size: 10px;
+ opacity: 0.6;
+ margin-left: 4px;
+}
+
+/* Form label input */
+.note-form-label {
+ background: transparent;
+ border: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
+ color: var(--fg);
+ font-size: 11px;
+ padding: 3px 6px;
+ border-radius: 4px;
+ outline: none;
+ width: 80px;
+ font-family: inherit;
+ opacity: 0.6;
+ transition: opacity 0.15s, border-color 0.15s;
+}
+.note-form-label:hover, .note-form-label:focus { opacity: 1; border-color: var(--border); }
+@keyframes noteLabelFlash {
+ 0% { background: color-mix(in srgb, var(--accent) 35%, transparent); }
+ 100% { background: transparent; }
+}
+
+.note-card-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 6px;
+}
+.note-card-title {
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.note-content-preview {
+ font-size: 12px;
+ opacity: 0.6;
+ line-height: 1.4;
+ /* Roomy preview: up to ~14 lines. Content is also sliced to 600 chars
+ client-side, so cards still stay reasonable in the list. */
+ max-height: 240px;
+ overflow: hidden;
+ cursor: pointer;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.note-pin-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 2px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+}
+.note-pin-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ border: 1.5px solid color-mix(in srgb, var(--fg) 25%, transparent);
+ transition: all 0.15s;
+}
+.note-pin-btn:hover .note-pin-dot { border-color: var(--fg); }
+.note-pin-btn.active .note-pin-dot {
+ background: var(--accent);
+ border-color: var(--accent);
+}
+.note-x-btn {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 2px;
+ flex-shrink: 0;
+ color: var(--fg);
+ opacity: 0.25;
+ display: flex;
+ align-items: center;
+ transition: opacity 0.15s, color 0.15s;
+}
+.note-x-btn:hover {
+ opacity: 0.9;
+ color: var(--red);
+}
+/* Inline due date next to title */
+.note-due-inline {
+ font-size: 9px;
+ padding: 1px 6px;
+ border-radius: 3px;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+ opacity: 0.7;
+ white-space: nowrap;
+ flex-shrink: 0;
+ align-self: center;
+}
+.note-due-inline.note-due-overdue {
+ background: color-mix(in srgb, var(--red) 18%, transparent);
+ color: var(--red);
+ opacity: 1;
+ font-weight: 600;
+}
+.note-card:active { cursor: grabbing; }
+/* Dragged card = "drop preview" — it already sits at the swap-target slot,
+ so making it the most visible card on screen tells the user exactly where
+ the note will land on release. */
+.note-card.dragging {
+ opacity: 0.92;
+ cursor: grabbing;
+ transform: scale(1.03) rotate(-0.6deg);
+ box-shadow: 0 14px 32px rgba(0,0,0,0.4), 0 0 0 2px var(--accent, var(--red));
+ border-color: var(--accent, var(--red)) !important;
+ background: color-mix(in srgb, var(--accent, var(--red)) 6%, var(--panel)) !important;
+ z-index: 10;
+ position: relative;
+ /* Let elementFromPoint see THROUGH the dragged card so the swap detector
+ can find the sibling underneath the finger. Without this, in single-row
+ (list view) the dragged card follows the finger and forever occludes
+ anything else, so no swap ever fires. The body-level touchmove listener
+ still receives events regardless. */
+ pointer-events: none;
+}
+.notes-pane-body.drag-active .note-card:not(.dragging) {
+ transition: transform 0.22s cubic-bezier(0.34, 1.2, 0.64, 1), border-color 0.15s, opacity 0.15s;
+ opacity: 0.78;
+}
+.notes-pane-body.drag-active .note-card:not(.dragging):hover {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
+ opacity: 1;
+}
+
+/* Checklist drag grip */
+.note-cl-grip {
+ cursor: grab;
+ opacity: 0.2;
+ font-size: 9px;
+ letter-spacing: -2px;
+ user-select: none;
+ flex-shrink: 0;
+ width: 14px;
+ height: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 0.15s;
+}
+.note-cl-row.dragging {
+ opacity: 0.4;
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
+ border-radius: 4px;
+}
+.note-cl-row.drop-before {
+ box-shadow: 0 -2px 0 0 var(--accent);
+}
+.note-cl-row.drop-after {
+ box-shadow: 0 2px 0 0 var(--accent);
+}
+
+.note-card-footer {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 2px;
+}
+
+.note-delete-btn {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.2;
+ cursor: pointer;
+ padding: 2px;
+ transition: opacity 0.15s;
+}
+.note-delete-btn:hover { opacity: 0.8; color: var(--red); }
+.note-archive-btn {
+ background: none; border: none; color: var(--fg); opacity: 0.2; cursor: pointer; padding: 2px; transition: opacity 0.15s;
+}
+
+/* Checklist preview — cap height so cards with 30 items don't push the
+ grid layout into oblivion; the inner list scrolls inside the card. */
+.note-checklist-preview {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ min-height: 18px;
+ max-height: 240px;
+ overflow-y: auto;
+ /* Let wheel/touch scrolling continue into the Notes pane when this nested
+ checklist is already at its top or bottom. */
+ overscroll-behavior-y: auto;
+ /* Card has padding-right:30px reserved for the top-corner edit/done/pin
+ buttons — but those sit above the checklist, not next to it. Pull the
+ checklist back into that reserved gutter so todo text can extend
+ further right. The copy button (bottom-right, 28×28) only appears on
+ hover; leave just enough room (last row) so it doesn't hide the final
+ item's tail. */
+ margin-right: -22px;
+ padding-right: 4px;
+}
+.note-cl-quickadd {
+ /* In flow now (was position:absolute), rendered AFTER the reminder badge
+ so the "+ Add item" input sits underneath the reminder instead of
+ overlapping it. Slight right inset keeps clear of the bottom-right
+ copy button on hover. */
+ display: block;
+ padding-top: 2px;
+ padding-right: 32px;
+ margin-top: 2px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s;
+}
+.note-cl-quickadd-input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
+ color: var(--fg);
+ font-size: 11px;
+ font-family: inherit;
+ padding: 4px 2px 2px;
+ outline: none;
+ opacity: 0.6;
+ transition: opacity 0.15s, border-color 0.15s;
+}
+.note-cl-quickadd-input:focus { opacity: 1; border-top-color: var(--border); }
+.note-checkbox {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 11px;
+ cursor: pointer;
+ /* Vertical padding stays 0 so short single-line rows pack tightly.
+ Left padding bumped so the dot's hover-scale (1.15x) doesn't clip
+ against the card edge. */
+ padding: 0 4px 0 8px;
+ line-height: 1.25;
+ border-radius: 4px;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+}
+.note-checkbox:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.note-check-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
+.note-link {
+ color: var(--accent-primary, var(--red));
+ text-decoration: underline;
+ word-break: break-all;
+}
+.note-link:hover { opacity: 0.8; }
+.note-checkbox-edit, .note-checkbox-rm {
+ flex: 0 0 auto;
+ background: transparent;
+ border: none;
+ color: var(--fg);
+ opacity: 0;
+ cursor: pointer;
+ padding: 2px;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 2px;
+ transition: opacity 0.12s, background 0.12s, color 0.12s;
+}
+.note-checkbox-rm:hover { opacity: 1 !important; color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
+.note-checkbox-edit:hover { opacity: 1 !important; color: var(--accent, var(--blue)); background: color-mix(in srgb, var(--accent, var(--blue)) 12%, transparent); }
+.note-check-text[contenteditable="true"] {
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ outline: 1px solid var(--accent, var(--blue));
+ border-radius: 2px;
+ cursor: text;
+ padding: 0 2px;
+ margin: 0 -2px;
+}
+.note-check-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ border: 1.75px solid color-mix(in srgb, var(--fg) 35%, transparent);
+ /* Solid panel fill so the dot reads as a tappable target even when an
+ image or colored card background sits behind it. */
+ background: var(--panel);
+ flex-shrink: 0;
+ position: relative;
+ /* Anchor the hover/active scale to the LEFT edge so the dot grows
+ rightward only. Default center origin pushed the scaled dot off the
+ left of the card (the parent .note-checklist-preview has overflow:auto
+ and clips horizontally as a side-effect of overflow-y:auto). */
+ transform-origin: left center;
+ transition: background 0.2s, border-color 0.2s, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+.notes-select-mode .note-checkbox:hover { background: transparent; }
+.notes-select-mode .note-checkbox:hover .note-check-dot { transform: none; border-color: color-mix(in srgb, var(--fg) 35%, transparent); }
+.note-checkbox:hover .note-check-dot {
+ border-color: var(--accent);
+ transform: scale(1.15);
+}
+.note-checkbox.done .note-check-dot {
+ background: var(--accent);
+ border-color: var(--accent);
+ animation: note-check-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
+}
+.note-check-text {
+ transition: opacity 0.25s;
+ line-height: 1.3;
+ padding-right: 4px;
+ word-break: break-word;
+}
+
+/* Due date badge */
+.note-due-badge {
+ font-size: 10px;
+ padding: 1px 6px;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+ color: var(--fg);
+ opacity: 0.6;
+}
+.note-due-overdue {
+ background: color-mix(in srgb, var(--red) 20%, transparent);
+ color: var(--red);
+ opacity: 1;
+ font-weight: 600;
+}
+
+/* Color classes — theme-aware via color-mix with --panel */
+.note-color-red { background: color-mix(in srgb, var(--red) 18%, var(--panel)); border-color: color-mix(in srgb, var(--red) 30%, var(--border)); }
+.note-color-orange { background: color-mix(in srgb, #d19a66 18%, var(--panel)); border-color: color-mix(in srgb, #d19a66 30%, var(--border)); }
+.note-color-yellow { background: color-mix(in srgb, var(--hl-string) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-string) 30%, var(--border)); }
+.note-color-green { background: color-mix(in srgb, #98c379 18%, var(--panel)); border-color: color-mix(in srgb, #98c379 30%, var(--border)); }
+.note-color-blue { background: color-mix(in srgb, var(--hl-function) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-function) 30%, var(--border)); }
+.note-color-purple { background: color-mix(in srgb, var(--hl-keyword) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-keyword) 30%, var(--border)); }
+
+
+/* Standard notes use a small identity palette when no color was selected.
+ The variation is deliberately limited to the card tint, top edge, and
+ title accent so the notes still read as one collection. */
+.note-card.note-standard {
+ border-top-width: 3px;
+ background: color-mix(in srgb, var(--note-accent) 7%, var(--panel));
+ border-color: color-mix(in srgb, var(--note-accent) 24%, var(--border));
+ box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--note-accent) 72%, transparent);
+}
+.note-card.note-standard:hover {
+ background: color-mix(in srgb, var(--note-accent) 11%, var(--panel));
+}
+.note-card.note-standard .note-card-title {
+ color: color-mix(in srgb, var(--note-accent) 82%, var(--fg));
+}
+.note-card.note-standard .note-card-label-chip {
+ border-color: color-mix(in srgb, var(--note-accent) 38%, var(--border));
+ background: color-mix(in srgb, var(--note-accent) 10%, transparent);
+ color: color-mix(in srgb, var(--note-accent) 86%, var(--fg));
+}
+
+/* Color picker dots */
+.note-color-picker {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+}
+.note-color-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid transparent;
+ transition: border-color 0.15s, transform 0.15s;
+ flex-shrink: 0;
+}
+.note-color-dot:hover { transform: scale(1.15); }
+.note-color-dot.active {
+ border-color: var(--fg);
+}
+
+/* Note form */
+.note-form {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 12px 14px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 10px;
+ transition: background 0.2s, border-color 0.2s;
+ /* Container so the action buttons can collapse to icons when the note
+ card is narrow (see .note-form-collapsible). */
+ container-type: inline-size;
+ container-name: noteform;
+}
+@media (max-width: 768px) {
+ .notes-pane:not(.notes-view-grid) .note-form.note-form-new {
+ transform: translateY(6px);
+ }
+}
+/* Label sits after the icon in each action button */
+.note-form-text-btn .nft-label { margin-left: 5px; }
+/* Never let the action buttons spill outside the card */
+.note-form-actions-group { flex-wrap: wrap; row-gap: 6px; }
+/* When the note card is narrow, collapse Archive / Delete / Cancel to
+ icon-only (their text label hides; tooltip via title= remains). The
+ Save/Update button is NOT collapsible — it always keeps its label and
+ stays last. */
+@container noteform (max-width: 360px) {
+ .note-form-collapsible .nft-label { display: none; }
+ .note-form-collapsible { padding-left: 9px; padding-right: 9px; }
+}
+.note-form.note-color-red { background: color-mix(in srgb, var(--red) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--red) 30%, var(--border)) !important; }
+.note-form.note-color-orange { background: color-mix(in srgb, #d19a66 18%, var(--panel)) !important; border-color: color-mix(in srgb, #d19a66 30%, var(--border)) !important; }
+.note-form.note-color-yellow { background: color-mix(in srgb, var(--hl-string) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-string) 30%, var(--border)) !important; }
+.note-form.note-color-green { background: color-mix(in srgb, #98c379 18%, var(--panel)) !important; border-color: color-mix(in srgb, #98c379 30%, var(--border)) !important; }
+.note-form.note-color-blue { background: color-mix(in srgb, var(--hl-function) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-function) 30%, var(--border)) !important; }
+.note-form.note-color-purple { background: color-mix(in srgb, var(--hl-keyword) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-keyword) 30%, var(--border)) !important; }
+.note-form-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding-right: 0;
+}
+.note-form-header .note-form-remind-btn {
+ margin-right: -7px; /* align bell center with X-button column on todo rows */
+ position: relative;
+ top: -2px;
+}
+.note-form-title {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 14px;
+ font-weight: 600;
+ padding: 4px 0;
+ outline: none;
+}
+.note-form-title:focus { border-color: var(--fg); }
+.note-form-header { position: relative; }
+.note-form-header .note-form-due {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
+ border: 0;
+ padding: 0;
+ margin: 0;
+ right: 0;
+ bottom: 0;
+}
+.note-form-due-btn {
+ flex: 0 0 auto;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ border-radius: 4px;
+ width: 26px;
+ height: 26px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ opacity: 0.85;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s;
+}
+.note-form-due-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
+.note-form-due-btn.has-date {
+ opacity: 1;
+ border-style: solid;
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
+}
+
+.note-type-toggle {
+ display: flex;
+ gap: 4px;
+}
+.note-type-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 11px;
+ padding: 3px 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ opacity: 0.5;
+ transition: opacity 0.15s, background 0.15s;
+}
+.note-type-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent);
+}
+
+.note-form-content {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 12px;
+ padding: 8px;
+ border-radius: 4px;
+ resize: vertical;
+ /* Roomier default so editing a note isn't cramped; JS auto-grow takes
+ it further as you type. */
+ min-height: 120px;
+ line-height: 1.5;
+ font-family: inherit;
+ outline: none;
+}
+.note-form-content:focus { border-color: var(--fg); }
+
+.note-form-meta {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.note-form-meta .note-color-picker { gap: 3px; }
+.note-form-actions-group {
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ /* Take the full width of the meta row so Archive/Delete can sit on the
+ LEFT side and the spacer pushes Cancel + Update to the right edge. */
+ flex: 1 1 auto;
+ flex-shrink: 0;
+ flex-wrap: nowrap;
+}
+.note-form-actions-spacer { flex: 1 1 auto; }
+/* Unified text+icon action button — used for Archive, Delete, Cancel, Update
+ so all 4 read as a matched set. Matches the existing .note-form-save
+ sizing/padding via the rules at ~25023. */
+.note-form-text-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border: 1px solid var(--border);
+ color: var(--fg);
+ border-radius: 6px;
+ padding: 5px 12px;
+ font-size: 12px;
+ font-family: inherit;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.12s, border-color 0.12s, color 0.12s;
+ white-space: nowrap;
+}
+.note-form-text-btn:hover {
+ background: color-mix(in srgb, var(--fg) 14%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 28%, var(--border));
+}
+.note-form-text-btn.note-form-save {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+.note-form-text-btn.note-form-save:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
+}
+.note-form-text-btn.danger:hover {
+ background: color-mix(in srgb, var(--red) 14%, transparent);
+ border-color: color-mix(in srgb, var(--red) 45%, var(--border));
+ color: var(--red);
+}
+.note-form-icon-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ border-radius: 4px;
+ width: 38px;
+ height: 38px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.65;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
+}
+/* Force the inner icons to render at the inline-attribute size — some other
+ stylesheets clamp `svg { width: ... }` globally and that shrinks them. */
+.note-form-icon-btn svg { width: 31px !important; height: 31px !important; }
+.note-form-icon-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
+.note-form-delete-btn:hover { color: var(--red); border-color: color-mix(in srgb, var(--red) 50%, var(--border)); background: color-mix(in srgb, var(--red) 8%, transparent); }
+
+/* Draw mode — canvas + small toolbar */
+.note-form-draw-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ position: relative;
+}
+/* X overlay on the canvas — appears only when a photo is loaded as the
+ background (via _wireCanvas). Clicking it restores the theme surface. */
+.note-form-draw-bg-rm {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ z-index: 3;
+ width: 28px;
+ height: 28px;
+ border: none;
+ border-radius: 50%;
+ background: rgba(0, 0, 0, 0.6);
+ color: #fff;
+ font-size: 18px;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.note-form-draw-bg-rm:hover { background: rgba(0, 0, 0, 0.8); }
+/* Draw mode hides the note's background-color picker and the standalone
+ image preview — they don't make sense alongside a canvas. Uses :has() so
+ it kicks in whenever the type-seg flips to .is-draw, in addition to the
+ JS that sets display:none. */
+.note-form:has(.note-form-type-seg.is-draw) .note-color-picker,
+.note-form:has(.note-form-type-seg.is-draw) .note-form-image-wrap {
+ display: none !important;
+}
+.note-form-canvas {
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ touch-action: none;
+ max-width: 100%;
+ cursor: crosshair;
+}
+.note-form-draw-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.note-form-draw-tool {
+ display: inline-flex;
+ align-items: center;
+ height: 26px;
+}
+/* Color picker rendered as a flat circular swatch — the native chrome around
+ would clash with the rest of the toolbar. */
+.note-form-draw-color {
+ appearance: none;
+ -webkit-appearance: none;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: 1px solid color-mix(in srgb, var(--fg) 25%, transparent);
+ background: none;
+ cursor: pointer;
+ padding: 0;
+ overflow: hidden;
+ box-sizing: border-box;
+ flex: 0 0 24px;
+}
+.note-form-draw-size-wrap { width: 110px; display: inline-flex; align-items: center; }
+.note-form-photo-plus {
+ font-size: 16px;
+ line-height: 1;
+ font-weight: 600;
+ opacity: 0.8;
+}
+/* Mirrors .gallery-editor-container input[type=range] — slim pill track that
+ grows on interaction, red circular thumb. */
+.note-form-draw-size {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 4px;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ border-radius: 999px;
+ accent-color: var(--red);
+ cursor: pointer;
+ transition: height 0.15s ease;
+}
+/* Brush | Eraser segmented toggle — sliding pill, same recipe as Note/Todo. */
+.note-form-draw-be {
+ display: inline-flex;
+ height: 32px;
+ /* Accent-colored box around the whole control so it's instantly readable
+ as "switch is on", and the sliding pill inside indicates which side. */
+ border: 2px solid var(--accent);
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
+ flex-shrink: 0;
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.note-form-draw-be-btn {
+ background: none;
+ border: none;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+ cursor: pointer;
+ padding: 0 8px;
+ width: 34px;
+ height: 100%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ z-index: 1;
+ transition: color 0.18s;
+}
+.note-form-draw-be-btn:hover { color: var(--fg); }
+/* Active side picks up the theme accent so brush vs eraser is unmistakable. */
+.note-form-draw-be-btn.active { color: var(--accent-primary, var(--red)); }
+
+.note-form-draw-text,
+.note-form-draw-line,
+.note-form-draw-circle,
+.note-form-draw-undo {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ border-radius: 4px;
+ height: 32px;
+ width: 34px;
+ font-family: inherit;
+ font-size: 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.7;
+ position: relative;
+ transition: opacity 0.15s, border-color 0.15s, background 0.15s;
+}
+.note-form-draw-text { font-weight: 700; font-family: Georgia, serif; font-size: 16px; }
+.note-form-draw-undo.has-undo {
+ opacity: 1;
+ color: var(--accent-primary, var(--red));
+ border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 55%, var(--border));
+ background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
+}
+.note-form-draw-undo:disabled {
+ opacity: 0.35;
+ cursor: default;
+}
+/* Size badge in the bottom-right of T — empty until a size is chosen. */
+.note-form-draw-text-badge {
+ position: absolute;
+ right: 3px;
+ bottom: 1px;
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 8px;
+ line-height: 1;
+ letter-spacing: 0.3px;
+ /* Bare --accent is undefined here, was rendering invisible — use the
+ defined accent so the S/M/L size badge actually shows in theme colour. */
+ color: var(--accent-primary, var(--red));
+}
+.note-form-draw-shape-badge {
+ position: absolute;
+ right: 3px;
+ bottom: 1px;
+ font-family: inherit;
+ font-weight: 700;
+ font-size: 8px;
+ line-height: 1;
+ letter-spacing: 0.3px;
+ color: var(--accent-primary, var(--red));
+}
+.note-form-draw-text:hover,
+.note-form-draw-line:hover,
+.note-form-draw-circle:hover,
+.note-form-draw-undo:hover {
+ opacity: 1;
+ border-color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
+}
+.note-form-draw-text.active,
+.note-form-draw-line.active,
+.note-form-draw-circle.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent) 18%, transparent);
+ color: var(--accent);
+ border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
+}
+.note-form-due {
+ background: transparent;
+ border: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
+ color: var(--fg);
+ font-size: 11px;
+ padding: 3px 6px;
+ border-radius: 4px;
+ outline: none;
+ opacity: 0.4;
+ transition: opacity 0.15s, border-color 0.15s;
+}
+.note-form-due:hover, .note-form-due:focus { opacity: 0.9; border-color: var(--border); }
+
+.note-form-actions {
+ display: flex;
+ gap: 6px;
+ justify-content: flex-end;
+}
+.note-form-save,
+.note-form-cancel,
+.note-form-archive {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 11px;
+ padding: 4px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+.note-form-save { background: color-mix(in srgb, var(--fg) 12%, transparent); font-weight: 600; }
+.note-form-save:hover { background: color-mix(in srgb, var(--fg) 20%, transparent); }
+.note-form-cancel:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
+.note-form-archive:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
+
+/* Checklist inputs in form */
+.note-checklist-inputs {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.note-cl-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.note-cl-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ border: 1.5px solid color-mix(in srgb, var(--fg) 30%, transparent);
+ flex-shrink: 0;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.note-cl-dot:hover { border-color: var(--fg); transform: scale(1.2); }
+.note-cl-row.done .note-cl-dot {
+ background: var(--accent);
+ border-color: var(--accent);
+}
+.note-cl-row.done .note-cl-text {
+ opacity: 0.4;
+ background: linear-gradient(currentColor, currentColor) no-repeat;
+ background-size: 0 1px;
+ background-position: 0 calc(50% - 1px);
+ animation: cl-strike 0.32s ease-out forwards;
+ transition: opacity 0.2s ease;
+}
+/* Draws the strikethrough line left-to-right when the row is marked
+ done, instead of snapping it in full-width on the same frame. */
+@keyframes cl-strike {
+ to { background-size: 100% 1px; }
+}
+.note-cl-text {
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid var(--border);
+ color: var(--fg);
+ font-size: 12px;
+ padding: 3px 0 3px 3px;
+ flex: 1 1 auto;
+ width: auto;
+ min-width: 0;
+ outline: none;
+}
+.note-cl-text:focus { border-color: var(--fg); }
+.note-cl-rm {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.3;
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0;
+ width: 24px;
+ height: 14px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ line-height: 1;
+}
+.note-cl-rm:hover { opacity: 0.8; color: var(--red); }
+.note-cl-add {
+ background: none;
+ border: none;
+ color: var(--fg);
+ opacity: 0.4;
+ cursor: pointer;
+ font-size: 11px;
+ padding: 4px 0;
+ text-align: left;
+}
+
+/* ── Calendar ── */
+.cal-modal-content { width:min(680px, 94vw); max-height:88vh; overflow-x:hidden; position:relative; }
+#calendar-modal .cal-modal-content { animation: none; }
+/* Was overflow-y:auto with grid + day-detail flowing naturally. Now uses a
+ true splitter layout: the body is a flex column that doesn't scroll
+ itself; the grid takes the remaining space and scrolls if needed; the
+ day-detail has an explicit height (driven by the splitter drag via
+ --cal-detail-h) and shrinks the grid visually as it grows. */
+#cal-body { display:flex; flex-direction:column; gap:8px; overflow:hidden; padding:0; min-height:0; }
+#cal-body > .cal-grid { flex: 1 1 auto; min-height: 120px; overflow-y: auto; overflow-x: hidden; }
+@media (max-width: 768px) {
+ /* Let the calendar pane shrink to nothing on mobile so the day-detail
+ splitter can be dragged all the way to the top, hiding the calendar
+ entirely. Without min-height:0 the grid (or the week-wrap below)
+ refuses to shrink past its built-in floor. */
+ #cal-body > .cal-grid,
+ #cal-body > .cal-wk-wrap {
+ min-height: 0;
+ max-height: none;
+ }
+ /* Let the week grid fill the calendar pane and scroll its hours internally
+ instead of overflowing #cal-body (which clips it and feels cramped). */
+ #cal-body > .cal-wk-wrap {
+ flex: 1 1 auto;
+ overflow: auto;
+ }
+}
+.cal-toolbar { display:flex; align-items:center; gap:6px; margin-bottom:7px; flex-wrap:wrap; line-height:1; max-width:100%; row-gap:6px; }
+
+/* Quick-add bar: natural-language event creation. Sits directly under
+ the toolbar; focused with `Q`; opens the bespoke form pre-filled. */
+.cal-quickadd-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 0 0 8px;
+ padding: 1px 10px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ transition: border-color 0.15s, background 0.15s;
+ position: relative;
+}
+/* Two-tone placeholder hint: "Quick add" in accent, the example in grey, both
+ at one low opacity. Overlays the (empty) input; hidden once the user types. */
+.cal-quickadd-hint {
+ position: absolute;
+ left: 10px;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ color: var(--fg);
+ opacity: 0.5;
+ pointer-events: none;
+ white-space: nowrap;
+ overflow: hidden;
+}
+.cal-quickadd-hint .qa-hint-accent { color: var(--accent, var(--red)); }
+/* ↵ enter glyph in the hint — makes it obvious the field submits on Enter. */
+.cal-quickadd-hint .qa-hint-enter { margin-left: auto; flex: 0 0 auto; color: var(--accent, var(--red)); opacity: 0.8; }
+/* Cycling example text fades out before swap, then fades back in. */
+.cal-quickadd-hint .qa-hint-example {
+ display: block;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-style: italic;
+ transition: opacity 0.18s ease;
+}
+.cal-quickadd-hint .qa-hint-example.qa-hint-fade { opacity: 0; }
+.cal-title-hint {
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(calc(-50% - 2px));
+ display: none;
+ align-items: center;
+ gap: 7px;
+ font-size: 18px;
+ line-height: 1.2;
+ white-space: nowrap;
+ pointer-events: none;
+ color: color-mix(in srgb, var(--fg) 42%, transparent);
+}
+.cal-hero-title:placeholder-shown ~ .cal-title-hint { display: inline-flex; }
+.cal-title-hint .cal-title-enter-ico { color: var(--accent, var(--red)); flex-shrink: 0; }
+/* Pure-CSS toggle: hide the hint as soon as the field has real text (no
+ per-keystroke JS, so typing never triggers a re-render / focus loss). */
+.cal-quickadd-input:not(:placeholder-shown) ~ .cal-quickadd-hint { display: none; }
+.cal-quickadd-row:focus-within {
+ border-color: color-mix(in srgb, var(--accent, var(--fg)) 55%, var(--border));
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+}
+.cal-quickadd-icon {
+ color: color-mix(in srgb, var(--accent, var(--fg)) 80%, transparent);
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ opacity: 0.7;
+}
+.cal-quickadd-input {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 12px;
+ height: 22px;
+ padding: 0;
+}
+.cal-quickadd-status {
+ font-size: 10px;
+ opacity: 0.55;
+ font-variant-numeric: tabular-nums;
+}
+.cal-toolbar > *, .cal-toolbar-nav > *, .cal-toolbar-right > * { vertical-align:middle; }
+.cal-toolbar-nav { display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; }
+.cal-toolbar-right {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 6px;
+ flex: 1 1 100%;
+ width: 100%;
+ margin-left: 0;
+ margin-top: 6px;
+ flex-wrap: wrap;
+}
+.cal-title { font-size:13px; font-weight:600; white-space:nowrap; height:24px; line-height:30px; padding:0 6px; display:inline-block; vertical-align:middle; box-sizing:border-box; }
+.cal-toolbar-clock {
+ margin-left: 8px;
+ color: var(--accent, var(--red));
+ font-size: 11px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
+button.cal-nav { background:color-mix(in srgb, var(--fg) 6%, transparent); border:1px solid var(--border); color:var(--fg); border-radius:5px; padding:0 8px; height:24px; line-height:22px; cursor:pointer; font-size:11px; font-family:inherit; box-sizing:border-box; vertical-align:middle; }
+button.cal-nav:hover { background:color-mix(in srgb, var(--fg) 12%, transparent); }
+button.cal-nav.cal-toolbar-arrow {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ padding: 0;
+ border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
+ border-radius: 50%;
+ 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: 1;
+ opacity: 0.84;
+}
+.cal-toolbar-arrow-glyph {
+ position: relative;
+ top: -1px;
+ line-height: 1;
+}
+@media (max-width: 768px) {
+ button.cal-nav.cal-toolbar-arrow { display: inline-flex !important; }
+ .cal-toolbar-nav {
+ flex: 1 1 100%;
+ width: 100%;
+ min-width: 0;
+ }
+ #cal-next { margin-left: auto; }
+}
+.cal-week-start-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0;
+ width: max-content;
+ padding: 0;
+ height: 24px;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ background: color-mix(in srgb, var(--fg) 4%, transparent);
+}
+.cal-week-start-btn {
+ box-sizing: border-box;
+ height: 22px !important;
+ min-height: 22px !important;
+ margin-top: 0 !important;
+ padding: 0 7px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ background: transparent;
+ color: color-mix(in srgb, var(--fg) 68%, transparent);
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 10px;
+ line-height: 20px;
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
+}
+.cal-settings-actions {
+ display: flex;
+ justify-content: flex-end;
+ width: 100%;
+}
+#cal-settings-panel .cal-week-start-toggle {
+ display: flex;
+ margin-left: auto;
+}
+.cal-settings-sync-status {
+ position: relative;
+ top: 2px;
+ font-size: 11px;
+ opacity: 0.6;
+}
+.cal-settings-sync-status.is-error {
+ color: var(--red);
+ opacity: 1;
+}
+.cal-week-start-btn:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+}
+.cal-week-start-btn.active,
+.cal-week-start-btn.active:hover {
+ color: var(--fg);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
+}
+.cal-side-nav {
+ position: absolute;
+ top: var(--cal-side-nav-y, 50%);
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
+ 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);
+ cursor: pointer;
+ z-index: 6;
+ opacity: 0;
+ pointer-events: none;
+ font-family: inherit;
+ font-size: 24px;
+ line-height: 28px;
+ padding: 0 0 3px;
+ transition: opacity 0.12s ease, background 0.12s ease, transform 0.12s ease;
+}
+.cal-modal-content:hover .cal-side-nav,
+.cal-side-nav:focus-visible {
+ opacity: 0.84;
+ pointer-events: auto;
+}
+.cal-side-nav:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--bg) 76%, var(--fg) 24%);
+ transform: translateY(-50%) scale(1.04);
+}
+button.cal-today-btn { font-size:10px; opacity:0.5; }
+button.cal-add-btn { background:var(--accent); color:#fff; border:none; border-radius:50%; width:24px; height:24px; line-height:22px; font-size:18px; cursor:pointer; flex-shrink:0; padding:0; box-sizing:border-box; font-family:inherit; vertical-align:middle; text-align:center; }
+button.cal-add-btn.cal-add-btn-text {
+ width:auto; min-width:0;
+ background: color-mix(in srgb, var(--fg) 8%, transparent);
+ color: var(--fg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 0 10px 0 6px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ height: 24px;
+ line-height: 1;
+ margin-top: 0;
+ transition: background 0.15s, border-color 0.15s;
+}
+button.cal-add-btn.cal-add-btn-text:hover {
+ background: color-mix(in srgb, var(--fg) 14%, transparent);
+ border-color: var(--accent);
+ opacity: 1;
+}
+.cal-add-plus {
+ font-size: 16px;
+ line-height: 1;
+ color: var(--accent, var(--red));
+ font-weight: 500;
+ display: inline-block;
+ transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
+ padding-bottom: 2px;
+}
+button.cal-add-btn.cal-add-btn-text:hover .cal-add-plus {
+ transform: rotate(180deg);
+}
+.cal-add-label { font-size:11px; font-weight:600; line-height:1; opacity:0.85; }
+/* Small variant of the toolbar's +New button. Inherits the .cal-add-btn-text
+ styles (pill with "+" + "New" + spring-rotate on the +), so the
+ animation is identical. Just shrunk a bit. */
+button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm {
+ height: 20px;
+ border-radius: 10px;
+ /* At rest: tight, equal padding around the "+" so it sits centred
+ with no trailing gap. On hover the right pad and the flex gap grow
+ to make room for the revealed "New". */
+ padding: 0 5px;
+ gap: 0;
+ transition: padding 0.25s ease, gap 0.25s ease, background 0.15s, border-color 0.15s;
+}
+/* Don't touch .cal-add-plus on the small variant — the toolbar +New is
+ the reference and we want the same rotation centre / motion. The label
+ is shrunk and hidden until hover so the pill is compact at rest. */
+button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-label {
+ font-size: 10px;
+ max-width: 0;
+ margin-left: 0;
+ opacity: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ transition: max-width 0.25s ease, opacity 0.18s ease, margin-left 0.25s ease;
+}
+button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm:hover .cal-add-label {
+ max-width: 40px;
+ margin-left: 3px;
+ opacity: 0.85;
+}
+.cal-add-btn:hover { opacity:0.85; }
+
+/* Mobile: bump the +New pill and the +/- zoom buttons up to a tap-friendly
+ size so they're easy to hit with a thumb. */
+@media (max-width: 768px) {
+ /* Toolbar +New (relocated by JS into the quickadd row on mobile) */
+ button.cal-add-btn.cal-add-btn-text {
+ height: 36px;
+ border-radius: 18px;
+ padding: 0 14px 0 12px;
+ gap: 6px;
+ flex-shrink: 0;
+ }
+ /* Day-detail +New: just a "+" on mobile — drop the label entirely. */
+ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ padding: 0;
+ gap: 0;
+ justify-content: center;
+ }
+ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-label {
+ display: none;
+ }
+ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-plus {
+ font-size: 18px;
+ padding-bottom: 0;
+ }
+ .cal-add-plus { font-size: 22px; padding-bottom: 1px; }
+ .cal-add-label { font-size: 13px; }
+ .cal-wk-zoom {
+ width: 36px;
+ height: 36px;
+ font-size: 20px;
+ border-radius: 8px;
+ opacity: 0.85;
+ }
+ /* The +New button sits as a sibling of the quickadd row inside a flex
+ wrapper, so it's clearly outside the input's bordered box. The row
+ itself keeps its own styling. */
+ .cal-quickadd-wrap {
+ display: flex;
+ align-items: stretch;
+ gap: 8px;
+ margin: 0 0 8px;
+ }
+ .cal-quickadd-wrap > #cal-quickadd-row {
+ flex: 1;
+ min-width: 0;
+ margin: 0;
+ }
+ .cal-quickadd-wrap > .cal-add-btn-text {
+ align-self: stretch;
+ flex-shrink: 0;
+ }
+ /* Nudge the "+" glyph up one pixel on the round day-detail button so it
+ sits visually centred inside the circle. */
+ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-plus {
+ transform: translateY(-1px);
+ }
+ /* Event form: bigger Create / Cancel buttons so they're easy to tap. */
+ .cal-form-actions button.cal-btn {
+ height: 44px;
+ padding: 0 20px;
+ font-size: 14px;
+ border-radius: 8px;
+ min-width: 96px;
+ }
+ /* Left-align the day/month + tags row in the day-detail panel and
+ the calendar/category chip row so they sit flush against the
+ left edge on a narrow screen. */
+ .cal-detail-header {
+ justify-content: flex-start;
+ gap: 12px;
+ }
+ .cal-filters {
+ justify-content: flex-start;
+ }
+}
+
+/* Refresh button spin — driven by JS toggling .cal-syncing on the button.
+ transform-box + transform-origin keep the rotation pivoted at the SVG's
+ own centre so it spins in place instead of orbiting / drifting up. */
+#cal-sync svg,
+#email-lib-refresh-btn svg { display: block; transform-box: fill-box; transform-origin: 50% 50%; }
+/* Brief checkmark flash after a refresh completes. Lasts ~900ms in JS;
+ the small bounce + accent color give a clear "done" cue. */
+#cal-sync.cal-sync-done svg,
+#email-lib-refresh-btn.email-lib-refresh-done svg {
+ color: var(--accent, #2ea043);
+ animation: refresh-check-pop 0.32s ease-out;
+}
+@keyframes refresh-check-pop {
+ 0% { transform: scale(0.55); opacity: 0.4; }
+ 60% { transform: scale(1.18); opacity: 1; }
+ 100% { transform: scale(1); opacity: 1; }
+}
+#cal-sync.cal-syncing svg,
+#email-lib-refresh-btn.email-lib-refreshing svg { animation: spin 0.6s linear infinite; }
+.email-undone-toggle.active,
+.email-attach-toggle.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+/* Hide the gallery-style reader-btn labels on desktop — only the mobile
+ @media block re-displays them under each icon. Desktop keeps compact
+ icon-only reader buttons. */
+.reader-btn-label {
+ display: inline-block;
+ font-size: 9px;
+ font-weight: 500;
+ line-height: 1;
+ letter-spacing: 0.02em;
+ white-space: nowrap;
+ opacity: 0.85;
+}
+
+/* Inline attachment-filter toggle nested inside the search input. The
+ input has padding-right:34px set inline so its text doesn't slide
+ under the button. */
+.email-search-wrap { display: flex; align-items: center; }
+/* Keep the email search input and the adjacent "Select" button identical in
+ height regardless of base-vs-mobile overrides — pin both explicitly. */
+.email-search-row .memory-search-input {
+ height: 32px;
+ min-height: 32px;
+ box-sizing: border-box;
+}
+.email-search-row .email-search-select-btn {
+ height: 32px;
+ min-height: 32px;
+ box-sizing: border-box;
+ margin-top: 6px;
+ flex-shrink: 0;
+}
+/* Select moved into the dropdown row — dock it to the right and match the
+ selects' vertical nudge (.memory-sort-select has top:3px). */
+.memory-category-filters .email-filter-select-btn,
+.memory-category-filters .email-filter-refresh-btn {
+ position: relative;
+ top: 3px;
+ flex-shrink: 0;
+}
+.memory-category-filters .email-filter-refresh-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 8px;
+}
+.memory-category-filters .email-reminders-clear-btn {
+ position: relative;
+ top: -2px;
+ flex-shrink: 0;
+}
+.email-attach-toggle-inline {
+ position: absolute !important;
+ right: 4px !important;
+ top: 50%;
+ transform: translateY(calc(-50% - 5px));
+ width: 26px !important;
+ height: 26px !important;
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ border-radius: 6px !important;
+ background: transparent !important;
+ border: none !important;
+ opacity: 0.55;
+}
+.email-attach-toggle-inline:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
+}
+.email-attach-toggle-inline.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
+ color: var(--accent, var(--red)) !important;
+}
+/* Inline undone-toggle nested inside the search input, sits LEFT of the
+ attach toggle. Same visual treatment. */
+.email-undone-toggle-inline {
+ position: absolute !important;
+ right: 34px !important;
+ top: 50%;
+ transform: translateY(calc(-50% - 3px));
+ width: 26px !important;
+ height: 26px !important;
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ border-radius: 6px !important;
+ background: transparent !important;
+ border: none !important;
+ opacity: 0.55;
+}
+.email-reminder-toggle-inline {
+ position: absolute !important;
+ right: 64px !important;
+ top: 50%;
+ transform: translateY(calc(-50% - 3px));
+ width: 26px !important;
+ height: 26px !important;
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ border-radius: 6px !important;
+ background: transparent !important;
+ border: none !important;
+ opacity: 0.55;
+}
+.email-reminder-toggle-inline:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
+}
+.email-reminder-toggle-inline.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
+ color: var(--accent, var(--red)) !important;
+}
+.email-undone-toggle-inline:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
+}
+.email-undone-toggle-inline.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
+ color: var(--accent, var(--red)) !important;
+}
+/* Round (circular) toggles in the search bar, matching the app's default icon
+ buttons. Always fully visible (not dimmed until hover). */
+.email-attach-toggle-inline,
+.email-undone-toggle-inline,
+.email-reminder-toggle-inline { border-radius: 50% !important; opacity: 1 !important; }
+.email-search-options-btn {
+ position: absolute !important;
+ right: 5px !important;
+ top: 50% !important;
+ transform: translateY(calc(-50% - 5px));
+ width: 26px !important;
+ height: 26px !important;
+ min-width: 26px !important;
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ border-radius: 999px !important;
+ background: transparent !important;
+ border: 0 !important;
+ color: inherit !important;
+ opacity: 0.7;
+ z-index: 2;
+}
+.email-search-options-btn:hover,
+.email-search-options-btn.active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent) !important;
+ color: var(--accent, var(--red)) !important;
+}
+.email-search-options-menu {
+ position: absolute;
+ top: calc(100% + 2px);
+ right: 0;
+ z-index: 75;
+ min-width: 140px;
+ padding: 4px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--panel, var(--bg));
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+}
+.email-search-options-title {
+ padding: 5px 9px 4px;
+ color: var(--fg-muted, var(--fg));
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.65;
+ white-space: nowrap;
+}
+.email-search-options-menu button {
+ width: 100%;
+ border: 0;
+ background: transparent;
+ color: var(--fg);
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ height: auto;
+ min-height: 0;
+ padding: 6px 8px;
+ border-radius: 8px;
+ font-family: inherit;
+ font-size: 11px;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: normal;
+ text-align: left;
+ cursor: pointer;
+}
+.email-search-options-menu button:hover,
+.email-search-options-menu button.active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ color: var(--accent, var(--red));
+}
+.email-search-option-icon {
+ width: 14px;
+ height: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ opacity: 0.5;
+}
+.email-search-option-icon svg { width: 14px; height: 14px; }
+.email-search-date-panel {
+ width: 220px;
+ padding: 6px 7px 5px;
+ border-top: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
+}
+.email-search-date-panel input {
+ width: 100%;
+ height: 26px;
+ box-sizing: border-box;
+ padding: 2px 6px;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ outline: none;
+ background: var(--bg);
+ color: var(--fg);
+ font: inherit;
+}
+.email-search-date-panel input:focus { border-color: var(--accent, var(--red)); }
+.email-search-date-actions { display: flex; justify-content: flex-end; gap: 5px; }
+.email-search-options-menu .email-search-date-actions [data-email-date-apply] {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+}
+@media (max-width: 768px) {
+ .email-search-options-menu button {
+ min-height: 44px;
+ padding: 12px 12px !important;
+ font-size: 14px !important;
+ gap: 10px !important;
+ }
+ .email-search-options-menu .email-search-option-icon {
+ width: 18px;
+ height: 18px;
+ }
+ .email-search-options-menu .email-search-option-icon svg {
+ width: 16px;
+ height: 16px;
+ }
+}
+/* Mobile: enlarge the icons inside the inline search-bar toggles
+ (done / attachment / reminders) — buttons themselves stay the same,
+ only the SVG glyph scales up so it's tappable + visible. */
+@media (max-width: 768px) {
+ .email-filter-btn {
+ height: 30px;
+ min-height: 30px;
+ font-size: 12px;
+ top: -6px;
+ padding: 0 9px;
+ }
+ .email-folder-btn {
+ top: -4px;
+ }
+
+ #email-lib-folder,
+ .memory-category-filters .email-filter-select-btn,
+ .memory-category-filters .email-filter-refresh-btn,
+ .memory-category-filters .email-filter-settings-btn {
+ height: 30px !important;
+ min-height: 30px !important;
+ box-sizing: border-box;
+ }
+
+ .memory-category-filters .email-filter-refresh-btn,
+ .memory-category-filters .email-filter-settings-btn {
+ width: 30px;
+ padding: 0 !important;
+ }
+ .memory-category-filters .email-filter-settings-btn > svg {
+ width: 15px;
+ height: 15px;
+ }
+
+ .email-search-row .memory-search-input {
+ height: 38px;
+ min-height: 38px;
+ }
+
+ #email-lib-chip-bar {
+ padding-right: 42px !important;
+ padding-left: 30px !important;
+ gap: 5px !important;
+ }
+
+ .email-lib-chip-bar-icon {
+ width: 14px !important;
+ height: 14px !important;
+ left: 9px !important;
+ }
+
+ #email-lib-search {
+ font-size: 14px !important;
+ min-width: 88px !important;
+ top: 0 !important;
+ }
+
+ #email-lib-search::placeholder {
+ font-size: 14px !important;
+ }
+
+ #email-lib-chip-bar.has-email-lib-pills #email-lib-search {
+ min-width: 24px !important;
+ flex: 1 1 24px !important;
+ }
+
+ .email-lib-pill {
+ min-height: 24px !important;
+ height: 24px !important;
+ line-height: 24px !important;
+ font-size: 12px !important;
+ padding: 0 6px 0 8px !important;
+ max-width: 180px !important;
+ }
+
+ .email-lib-pill svg {
+ width: 14px !important;
+ height: 14px !important;
+ }
+
+ .email-lib-pill-x {
+ font-size: 13px !important;
+ top: -4.5px !important;
+ }
+
+ .email-lib-filter-pill {
+ min-height: 24px !important;
+ height: 24px !important;
+ line-height: 24px !important;
+ gap: 4px !important;
+ padding: 0 6px 0 8px !important;
+ }
+
+ .email-lib-filter-pill .email-lib-pill-icon {
+ width: 17px !important;
+ height: 17px !important;
+ }
+
+ .email-lib-filter-pill svg {
+ width: 18px !important;
+ height: 18px !important;
+ }
+
+ .email-lib-filter-pill .email-lib-pill-x {
+ font-size: 13px !important;
+ top: -4.5px !important;
+ }
+
+ .email-tags-toggle-inline {
+ right: 6px;
+ height: 28px !important;
+ min-width: 42px;
+ padding: 0 8px !important;
+ font-size: 11px !important;
+ transform: translateY(calc(-50% - 3px));
+ }
+
+ .email-attach-toggle-inline {
+ right: 56px !important;
+ width: 32px !important;
+ height: 32px !important;
+ transform: translateY(calc(-50% - 3px));
+ }
+
+ .email-undone-toggle-inline {
+ right: 92px !important;
+ width: 32px !important;
+ height: 32px !important;
+ transform: translateY(calc(-50% - 3px));
+ }
+
+ .email-reminder-toggle-inline {
+ right: 128px !important;
+ width: 32px !important;
+ height: 32px !important;
+ transform: translateY(calc(-50% - 3px));
+ }
+
+ .email-attach-toggle-inline svg,
+ .email-undone-toggle-inline svg,
+ .email-reminder-toggle-inline svg,
+ .email-filter-refresh-btn svg {
+ width: 16px !important;
+ height: 16px !important;
+ }
+}
+.email-accounts-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 0 6px;
+}
+.email-accounts-row .doclib-desc { flex-shrink: 0; }
+/* Only the direct-child compose button gets pushed right; nested chips
+ inside #email-lib-accounts pack to the left as normal flex items. */
+.email-accounts-row > .memory-toolbar-btn { flex-shrink: 0; margin-left: auto; }
+.email-account-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.email-account-chip.email-account-has-unread {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ color: color-mix(in srgb, var(--accent, var(--red)) 86%, var(--fg));
+}
+.email-account-unread-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 999px;
+ background: var(--accent, var(--red));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
+ flex: 0 0 auto;
+}
+.email-account-chip-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.email-account-unread-count {
+ font-size: 9px;
+ line-height: 1;
+ font-weight: 700;
+ color: var(--accent, var(--red));
+ opacity: 0.9;
+ position: relative;
+ top: 0;
+}
+.email-account-away-label {
+ color: var(--accent, var(--red));
+ font-size: 9px;
+ font-weight: 700;
+ line-height: 1;
+ flex: 0 0 auto;
+}
+.email-accounts-loading-whirlpool {
+ width: 14px;
+ height: 14px;
+ margin: 3px 4px 0 1px;
+ display: inline-flex;
+ flex: 0 0 auto;
+}
+.email-accounts-loading-label {
+ font-size: 10px;
+ opacity: 0.55;
+ position: relative;
+ top: 2px;
+ white-space: nowrap;
+}
+.email-loading-with-label {
+ /* Fill the email list grid so the spinner lands in the middle of the
+ email window rather than the top of the empty list section. */
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ text-align: center;
+ pointer-events: none;
+}
+.date-section-header {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 8px 9px 4px;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0;
+ line-height: 1.2;
+ text-transform: uppercase;
+ user-select: none;
+}
+.date-section-header::after {
+ content: '';
+ flex: 1 1 auto;
+ height: 1px;
+ background: color-mix(in srgb, var(--border) 72%, transparent);
+}
+#session-list .date-section-header {
+ padding-left: 10px;
+ padding-right: 8px;
+ cursor: pointer;
+}
+#session-list .date-section-header.collapsed {
+ color: color-mix(in srgb, var(--fg) 36%, transparent);
+}
+#session-list .date-section-header.collapsed::after {
+ opacity: 0.45;
+}
+#session-list .date-section-header:focus-visible {
+ outline: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
+ outline-offset: 2px;
+ border-radius: 5px;
+}
+/* Bootstrap row shown while the session list hydrates, and on load failure.
+ Reads as a normal list row but is not selectable. */
+.session-list-bootstrap {
+ cursor: default;
+ pointer-events: none;
+}
+#email-lib-grid .date-section-header {
+ padding: 10px 5px 3px;
+}
+#email-lib-modal .email-lib-sync-status {
+ flex: 0 0 auto;
+ min-height: 14px;
+ padding: 1px 4px 0;
+ font-size: 10.5px;
+ line-height: 1.2;
+ text-align: center;
+ opacity: 0.58;
+ color: var(--muted-text, currentColor);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: relative;
+ top: 4px;
+}
diff --git a/static/css/16-calendar.css b/static/css/16-calendar.css
new file mode 100644
index 000000000..d4112f705
--- /dev/null
+++ b/static/css/16-calendar.css
@@ -0,0 +1,1175 @@
+/* 16-calendar - 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.
+ */
+@media (max-width: 768px) {
+ #email-lib-modal .email-lib-sync-status {
+ min-height: 13px;
+ padding-right: 5px;
+ font-size: 10px;
+ }
+}
+/* Nudge the subject + sender/date meta line 4px right inside each
+ email card so they don't sit flush against the read/unread/done
+ markers on the left. Includes #email-lib-modal selector to
+ override the expanded card's `padding: 4px 0 6px` shorthand
+ which was zeroing out the title's padding-left. */
+#email-lib-grid .email-card-titlerow,
+#email-lib-grid .memory-item-meta,
+#email-lib-modal .doclib-card-expanded .email-card-titlerow {
+ padding-left: 4px;
+}
+.email-loading-label {
+ font-size: 11px;
+ opacity: 0.6;
+}
+
+/* Refresh button now lives top-right in the modal header next to the close X.
+ Borderless (matches the close X), and a fixed square box so the spin and the
+ refresh→checkmark icon swap never change its size or nudge neighbours. */
+.email-lib-header-actions #email-lib-refresh-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 auto;
+ border: none;
+ background: none;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+}
+/* Bump the icon up on mobile for a bigger visual — but keep the button BOX at
+ 24px so it never exceeds the header's natural height (the title/close set
+ that). A taller box would grow the sticky header and push the list down. */
+@media (max-width: 768px) {
+ .email-lib-header-actions #email-lib-refresh-btn svg {
+ width: 17px;
+ height: 17px;
+ }
+}
+.cal-view-toggle {
+ display: inline-flex;
+ align-items: stretch;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ overflow: hidden;
+ vertical-align: middle;
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+ line-height: 1;
+}
+.cal-view-btn + .cal-view-btn { border-left: 1px solid var(--border); }
+.cal-view-btn:hover { opacity: 0.75; }
+.cal-view-btn.active {
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ color: var(--accent, var(--red));
+ opacity: 1;
+ font-weight: 600;
+}
+/* Toolbar holds only the toggle button; the chip row renders below the
+ toolbar on its own line and wraps freely. */
+.cal-filters {
+ display:flex;
+ gap:6px;
+ margin:6px 0 8px;
+ flex-wrap:wrap;
+ padding:6px 8px;
+ border:1px solid color-mix(in srgb, var(--fg) 8%, transparent);
+ border-radius:8px;
+ background:
+ linear-gradient(180deg, color-mix(in srgb, var(--fg) 4%, transparent), transparent),
+ color-mix(in srgb, var(--fg) 2.5%, var(--bg));
+ box-shadow:inset 0 1px 0 color-mix(in srgb, #fff 4%, transparent);
+}
+/* Round the native color-input swatch in calendar settings. The outer
+ wraps a colored fill drawn by the browser; we need to round both
+ the wrapper and the fill so it actually appears circular. */
+.cal-s-color { border-radius: 50%; overflow: hidden; }
+.cal-filter-item { display:inline-flex; align-items:center; gap:4px; cursor:pointer; font-size:10px; padding:2px 8px; line-height:1.4; border-radius:10px; background:color-mix(in srgb, var(--bg) 72%, transparent); border:1px solid color-mix(in srgb, var(--fg) 12%, transparent); transition:opacity 0.15s, background 0.15s, border-color 0.15s, transform 0.15s; }
+.cal-filter-item:hover { background:color-mix(in srgb, var(--fg) 10%, transparent); }
+.cal-filter-item.cal-filter-important { font-weight:700; color:#e5a33a; border-color:color-mix(in srgb, #e5a33a 40%, transparent); }
+/* Match the toolbar's other buttons (cal-nav, cal-view-btn, cal-add-btn-text):
+ 24px tall, 11px font, 5px corners. */
+.cal-filter-toggle {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: color-mix(in srgb, var(--fg) 6%, transparent);
+ border: 1px solid var(--border);
+ color: var(--fg);
+ cursor: pointer;
+ font-size: 11px;
+ font-family: inherit;
+ font-weight: 500;
+ padding: 0 10px;
+ height: 24px;
+ line-height: 1;
+ border-radius: 5px;
+ box-sizing: border-box;
+ vertical-align: middle;
+ opacity: 0.65;
+ position: relative;
+ top: -3px;
+}
+.cal-filter-toggle:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 12%, transparent); }
+.cal-filter-toggle-glyph {
+ display: inline-block;
+ position: relative;
+ top: 0;
+ margin-right: 3px;
+ color: var(--accent, var(--red));
+}
+.cal-filter-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
+/* The grid is now a flex column: one row of weekday headers + six
+ .cal-week-row rows. Each row holds the 7 day cells in its own
+ internal grid plus the absolute-positioned multi-day overlays.
+ --bars on a week-row is the number of multi-day events overlapping
+ it; cells in that row use it to reserve top padding so the bar
+ overlays don't cover the day numbers or single-event rows. */
+.cal-grid { display:flex; flex-direction:column; gap:1px; background:color-mix(in srgb, var(--fg) 8%, transparent); border-radius:6px; overflow:hidden; }
+.cal-week-headers { display:grid; grid-template-columns:repeat(7,1fr); gap:1px; }
+.cal-week-row { position:relative; display:grid; grid-template-columns:repeat(7,1fr); gap:1px; }
+.cal-weekday { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); text-align:center; font-size:10px; font-weight:600; opacity:0.4; padding:5px 0; }
+.cal-day {
+ background:
+ linear-gradient(180deg, color-mix(in srgb, var(--fg) 2%, transparent), transparent 34px),
+ var(--bg);
+ min-height:78px;
+ padding:3px;
+ cursor:pointer;
+ position:relative;
+ transition:background 0.12s, box-shadow 0.12s;
+ overflow:hidden;
+}
+.cal-day:hover { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); }
+.cal-day.cal-today {
+ box-shadow: inset 0 0 0 2px var(--accent, var(--red));
+ background: var(--bg);
+ border-radius: 8px;
+}
+.cal-day.cal-selected:not(.cal-today) .cal-day-num {
+ color: var(--accent, var(--fg));
+ opacity: 1;
+ font-weight: 700;
+}
+.cal-day.cal-drag-over { background:color-mix(in srgb, var(--accent) 18%, var(--bg)); }
+.cal-day.cal-range-select { background:color-mix(in srgb, var(--accent, var(--red)) 25%, var(--bg)); box-shadow:inset 0 0 0 1px var(--accent, var(--red)); }
+.cal-day-num { font-size:10px; font-weight:600; display:block; margin-bottom:1px; opacity:0.7; line-height:1.2; }
+.cal-dots { display:flex; flex-direction:row; align-items:center; gap:2px; flex-wrap:nowrap; margin-bottom:1px; height:6px; }
+.cal-dot { width:5px; height:5px; border-radius:50%; display:inline-block; cursor:grab; flex-shrink:0; }
+.cal-dot-more { font-size:7px; opacity:0.4; }
+.cal-event-row {
+ display:flex;
+ align-items:center;
+ gap:4px;
+ padding:2px 4px;
+ border-radius:4px;
+ cursor:grab;
+ line-height:1.2;
+ margin-bottom:2px;
+ min-height:17px;
+ background:color-mix(in srgb, var(--fg) 5%, transparent);
+ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 7%, transparent);
+ animation:cal-event-enter 0.16s ease-out both;
+ transition:background 0.15s, box-shadow 0.15s, transform 0.15s;
+}
+.cal-event-row:hover { background:color-mix(in srgb, var(--fg) 10%, transparent); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 14%, transparent); transform:translateY(-1px); }
+.cal-event-row-dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; box-shadow:0 0 0 1px color-mix(in srgb, var(--bg) 80%, transparent); }
+.cal-event-row-time { font-size:10px; opacity:0.72; font-variant-numeric:tabular-nums; flex-shrink:0; font-weight:600; }
+.cal-event-row-name { font-size:10px; opacity:0.96; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1; min-width:0; font-weight:600; }
+.cal-event-reminder {
+ display: inline-flex;
+ width: 12px;
+ height: 12px;
+ margin-left: 5px;
+ vertical-align: -2px;
+ color: color-mix(in srgb, var(--fg) 58%, transparent);
+ opacity: 0.9;
+ flex: 0 0 auto;
+}
+.cal-event-reminder svg {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+.cal-event-row .cal-event-reminder {
+ width: 10px;
+ height: 10px;
+ margin-left: 4px;
+}
+.cal-event-source {
+ display:inline-flex;
+ align-items:center;
+ justify-content:center;
+ width:12px;
+ height:12px;
+ margin-left:5px;
+ border-radius:50%;
+ font-size:7px;
+ font-weight:800;
+ line-height:1;
+ color:var(--cal-event-fg, var(--fg));
+ background:color-mix(in srgb, currentColor 16%, transparent);
+ box-shadow:inset 0 0 0 1px color-mix(in srgb, currentColor 20%, transparent);
+ vertical-align:1px;
+}
+.cal-event-source svg {
+ width: 10px;
+ height: 10px;
+ display: block;
+}
+.cal-event-row .cal-event-source {
+ width:11px;
+ height:11px;
+ font-size:7px;
+ margin-left:4px;
+ color:color-mix(in srgb, var(--fg) 72%, transparent);
+}
+.cal-multiday .cal-event-reminder,
+.cal-wk-allday-event .cal-event-reminder {
+ color: var(--cal-event-fg, currentColor);
+ opacity: 0.88;
+}
+.cal-multiday .cal-event-source,
+.cal-wk-allday-event .cal-event-source {
+ color: var(--cal-event-fg, currentColor);
+}
+.cal-event-more { font-size:9px; opacity:0.55; padding-left:6px; }
+/* Multi-day bar — positioned as an absolute overlay inside its
+ .cal-week-row so it spans uninterrupted across every column it
+ covers. --col is the starting column (0–6), --span is the number
+ of days it covers within the row, --slot stacks parallel bars. */
+.cal-multiday {
+ position: absolute;
+ /* Fractional offsets let timed events that cross midnight render at
+ true proportional width. start-frac shifts the left edge into the
+ first day; end-frac trims the right edge inside the last day.
+ All-day events default to (0, 1) and still fill whole columns. */
+ left: calc((var(--col, 0) + var(--start-frac, 0)) * (100% / 7));
+ width: calc(
+ (var(--span, 1) - var(--start-frac, 0) - (1 - var(--end-frac, 1)))
+ * (100% / 7)
+ );
+ top: calc(2px + var(--slot, 0) * 12px);
+ height: 12px;
+ font-size: 9px;
+ line-height: 12px;
+ padding: 0 5px;
+ border-radius: 4px;
+ color: var(--cal-event-fg, #fff);
+ font-weight: 700;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: grab;
+ opacity: 0.98;
+ z-index: 2;
+ pointer-events: auto;
+ box-shadow: 0 1px 2px color-mix(in srgb, #000 18%, transparent);
+}
+/* Splitter between the month/week grid and the day-tasks panel — drag
+ vertically to give the day panel more room. Height clamped by the
+ `--cal-detail-h` variable set on #cal-body by the splitter drag JS. */
+.cal-splitter {
+ height: 8px;
+ margin: 6px -10px 0;
+ cursor: row-resize;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ user-select: none;
+ touch-action: none;
+}
+/* Mobile: fat hitbox so the splitter is easy to grab with a thumb,
+ without changing how the grip itself looks. */
+@media (max-width: 768px) {
+ .cal-splitter {
+ height: 24px;
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+ .cal-splitter-grip {
+ width: 56px;
+ height: 4px;
+ }
+}
+.cal-splitter-grip {
+ width: 42px;
+ height: 3px;
+ border-radius: 2px;
+ background: color-mix(in srgb, var(--fg) 25%, transparent);
+ transition: background 0.12s;
+}
+.cal-splitter:hover .cal-splitter-grip,
+.cal-splitter-dragging .cal-splitter-grip {
+ background: var(--accent, var(--red));
+}
+.cal-day-detail {
+ margin-top: 4px;
+ border-top: 1px solid var(--border);
+ padding-top: 8px;
+ height: var(--cal-detail-h, 240px);
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ flex-shrink: 0;
+}
+.cal-detail-header { display:flex; justify-content:space-between; align-items:center; font-size:12px; font-weight:600; margin-bottom:6px; padding-right:8px; }
+.cal-empty { font-size:11px; opacity:0.3; padding:4px 0; }
+.cal-event-item { display:flex; gap:9px; padding:8px 9px; border-radius:6px; cursor:pointer; align-items:flex-start; background:color-mix(in srgb, var(--fg) 3.5%, transparent); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent); margin-bottom:4px; }
+.cal-event-item:hover { background:color-mix(in srgb, var(--fg) 8%, transparent); box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent); }
+.cal-event-dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; margin-top:4px; box-shadow:0 0 0 1px color-mix(in srgb, var(--bg) 80%, transparent); }
+.cal-event-info { flex:1; min-width:0; }
+.cal-event-name { font-size:13px; font-weight:700; line-height:1.25; color:color-mix(in srgb, var(--fg) 96%, transparent); overflow-wrap:anywhere; }
+.cal-event-tag {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: 500;
+ padding: 1px 6px;
+ border-radius: 8px;
+ border: 1px solid;
+ margin-left: 6px;
+ vertical-align: 1px;
+ opacity: 0.85;
+ white-space: nowrap;
+}
+.cal-event-time { font-size:11px; opacity:0.68; line-height:1.35; margin-top:2px; font-weight:500; }
+.cal-event-loc { font-size:11px; opacity:0.58; line-height:1.35; margin-top:1px; }
+/* ── Week view: hour grid ──────────────────────────────────────────
+ Vertical hour rail on the left, 7 day columns on the right with their
+ own all-day strip and an absolute-positioned hour grid below it. */
+.cal-wk-wrap {
+ display: flex;
+ flex-direction: row;
+ gap: 0;
+ background:
+ linear-gradient(180deg, color-mix(in srgb, var(--fg) 3%, transparent), transparent 120px),
+ var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ /* The wrap itself scrolls so the hour rail and day columns move
+ together; column headers and the rail spacer are sticky-pinned. */
+ overflow: auto;
+ max-height: calc(100vh - 220px);
+ min-height: 360px;
+ position: relative;
+}
+.cal-wk-now-below-hint {
+ position: absolute;
+ height: 2px;
+ border-radius: 2px 2px 0 0;
+ background: var(--accent, var(--red));
+ pointer-events: none;
+ z-index: 38;
+}
+@media (max-width: 768px) {
+ .cal-wk-wrap {
+ -webkit-overflow-scrolling: touch;
+ overscroll-behavior-y: contain;
+ }
+ .cal-wk-wrap > .cal-wk-rail,
+ .cal-wk-wrap > .cal-wk-cols {
+ transform: translateY(var(--wk-edge-pull, 0));
+ }
+ .cal-wk-wrap.cal-wk-edge-release > .cal-wk-rail,
+ .cal-wk-wrap.cal-wk-edge-release > .cal-wk-cols {
+ transition: transform 0.14s cubic-bezier(0.2, 0.75, 0.25, 1);
+ }
+ .cal-wk-wrap.cal-wk-pull-top {
+ border-top-color: color-mix(in srgb, var(--accent, var(--red)) 65%, var(--border));
+ }
+ .cal-wk-wrap.cal-wk-pull-bottom {
+ border-bottom-color: color-mix(in srgb, var(--accent, var(--red)) 65%, var(--border));
+ }
+}
+.cal-wk-rail {
+ flex: 0 0 56px;
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ background: color-mix(in srgb, var(--fg) 2%, var(--bg));
+ position: sticky;
+ left: 0;
+ z-index: 5;
+}
+.cal-wk-rail-spacer {
+ /* Match the 32px day header. The 24px all-day row gets its own spacer
+ below, so the zoom controls sit high without shifting the hour grid. */
+ height: 32px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ position: sticky;
+ top: 0;
+ background: color-mix(in srgb, var(--fg) 2%, var(--bg));
+ z-index: 9;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ padding: 0 4px;
+}
+.cal-wk-rail-allday-spacer {
+ height: 24px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ position: sticky;
+ top: 32px;
+ z-index: 8;
+ background: color-mix(in srgb, var(--fg) 1.5%, var(--bg));
+}
+.cal-wk-zoom {
+ position: relative;
+ top: -3px;
+ width: 22px;
+ height: 22px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ border-radius: 50%;
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ opacity: 0.55;
+ transition: opacity 0.15s, background 0.15s, border-color 0.15s;
+}
+.cal-wk-zoom:hover {
+ opacity: 1;
+ background: color-mix(in srgb, var(--fg) 8%, var(--bg));
+ border-color: color-mix(in srgb, var(--accent, var(--fg)) 50%, var(--border));
+}
+.cal-wk-rail-cell {
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+ padding: 4px 8px 0;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 5%, transparent);
+ flex-shrink: 0;
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-end;
+ font-variant-numeric: tabular-nums;
+}
+.cal-wk-cols {
+ flex: 1;
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ position: relative;
+}
+.cal-wk-col {
+ border-left: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+.cal-wk-col:first-child { border-left: none; }
+.cal-wk-col-head {
+ height: 32px;
+ padding: 0 6px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ flex-shrink: 0;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 5%, transparent);
+ position: sticky;
+ top: 0;
+ z-index: 40;
+}
+/* Today on a Sunday still wins the accent treatment. */
+.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-col-head { background: color-mix(in srgb, var(--accent, var(--fg)) 12%, var(--bg)); }
+.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-dn,
+.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-dt { color: var(--accent, var(--fg)); }
+.cal-wk-dn { font-weight: 600; opacity: 0.6; }
+.cal-wk-allday {
+ height: 24px;
+ padding: 2px 4px;
+ display: flex;
+ flex-direction: row;
+ gap: 2px;
+ align-items: center;
+ background: color-mix(in srgb, var(--fg) 1.5%, var(--bg));
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ position: sticky;
+ top: 32px;
+ z-index: 39;
+}
+.cal-wk-allday-event {
+ font-size: 10.5px;
+ font-weight: 700;
+ padding: 3px 6px;
+ border-radius: 4px;
+ color: var(--cal-event-fg, var(--fg));
+ cursor: pointer;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* Hour grid body */
+.cal-wk-grid {
+ position: relative;
+ flex: 0 0 auto;
+ cursor: cell;
+ user-select: none;
+ background:
+ repeating-linear-gradient(
+ to bottom,
+ transparent 0,
+ transparent calc(var(--wk-hour-px, 52px) / 2 - 1px),
+ color-mix(in srgb, var(--fg) 4%, transparent) calc(var(--wk-hour-px, 52px) / 2),
+ transparent calc(var(--wk-hour-px, 52px) / 2 + 1px)
+ );
+}
+.cal-wk-cell {
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
+ box-sizing: border-box;
+}
+.cal-wk-cell:hover { background: color-mix(in srgb, var(--fg) 3%, transparent); }
+/* "Now" line */
+.cal-wk-now {
+ position: absolute;
+ left: 0; right: 0;
+ height: 0;
+ border-top: 2px solid color-mix(in srgb, var(--accent, var(--red, #f87171)) 90%, transparent);
+ pointer-events: none;
+ z-index: 7;
+ filter: drop-shadow(0 1px 3px color-mix(in srgb, var(--accent, var(--red)) 35%, transparent));
+}
+/* Event blocks */
+.cal-wk-block {
+ position: absolute;
+ left: calc((var(--lane, 0) * (100% / var(--lane-count, 1))) + 2px);
+ width: calc((100% / var(--lane-count, 1)) - 4px);
+ right:auto;
+ border-left: 4px solid var(--fg);
+ border-radius: 5px;
+ padding: 5px 7px 7px;
+ font-size: 12px;
+ line-height: 1.3;
+ cursor: pointer;
+ /* `clip` preserves the rounded-card boundary without creating an overflow
+ container, allowing the label inside to stick to the week scroller. */
+ overflow: clip;
+ z-index: 2;
+ box-shadow: 0 2px 6px color-mix(in srgb, #000 14%, transparent), inset 0 0 0 1px color-mix(in srgb, var(--fg) 7%, transparent);
+ animation:cal-event-enter 0.18s ease-out both;
+ transition: width 0.2s cubic-bezier(0.2, 0.75, 0.25, 1), height 0.2s cubic-bezier(0.2, 0.75, 0.25, 1), filter 0.12s, transform 0.12s, box-shadow 0.16s;
+ box-sizing: border-box;
+}
+.cal-wk-block:hover { filter: brightness(1.05); transform: translateY(-1px); box-shadow: 0 5px 14px color-mix(in srgb, #000 20%, transparent), inset 0 0 0 1px color-mix(in srgb, var(--fg) 14%, transparent); }
+.cal-wk-block-label {
+ position: sticky;
+ top: 37px;
+ z-index: 1;
+ min-width: 0;
+ pointer-events: none;
+}
+.cal-wk-has-allday .cal-wk-block-label {
+ top: 61px;
+}
+.cal-wk-block-name { font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 12px; color:color-mix(in srgb, var(--fg) 96%, transparent); }
+.cal-wk-block-time { font-size: 10.5px; opacity: 0.82; font-variant-numeric: tabular-nums; margin-top: 2px; font-weight:600; }
+@media (hover: hover) and (pointer: fine) {
+ .cal-wk-block.cal-wk-block-crowded.cal-wk-expand-left {
+ left: auto;
+ right: calc(100% - var(--lane-right) + 2px);
+ }
+ .cal-wk-block.cal-wk-block-crowded:hover {
+ width: var(--hover-width, 170px);
+ height: var(--hover-fit-height, var(--hover-height, var(--event-height))) !important;
+ overflow: clip;
+ z-index: 30;
+ filter: brightness(1.08);
+ transform: translateY(var(--hover-shift-y, -2px));
+ box-shadow: 0 8px 22px color-mix(in srgb, #000 34%, transparent), inset 0 0 0 1px color-mix(in srgb, var(--fg) 18%, transparent);
+ }
+ .cal-wk-block.cal-wk-block-crowded.cal-wk-hover-width-only:hover {
+ height: var(--event-height) !important;
+ transform: translateY(-1px);
+ }
+ .cal-wk-block.cal-wk-block-crowded:hover .cal-wk-block-label {
+ position: absolute;
+ top: 5px;
+ width: calc(var(--hover-width, 170px) - 18px);
+ animation: cal-wk-hover-label-reveal 0.2s ease-out both;
+ }
+ .cal-wk-block.cal-wk-block-crowded.cal-wk-expand-left:hover .cal-wk-block-label {
+ left: auto;
+ right: 7px;
+ }
+ .cal-wk-block.cal-wk-block-crowded.cal-wk-expand-right:hover .cal-wk-block-label {
+ left: 7px;
+ right: auto;
+ }
+ .cal-wk-block.cal-wk-block-crowded:hover .cal-wk-block-name {
+ display: block;
+ white-space: normal;
+ overflow: visible;
+ text-overflow: clip;
+ overflow-wrap: anywhere;
+ }
+}
+@media (max-width: 768px) {
+ /* Mobile week view: the hour rail already shows the time, so drop it from the
+ card and let the title wrap to fill the freed space (more readable text). */
+ .cal-wk-block-time { display: none; }
+ .cal-wk-block-name {
+ white-space: normal;
+ line-height: 1.15;
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ }
+ .cal-wk-block.cal-wk-mobile-expanded.cal-wk-expand-left {
+ left: auto;
+ right: calc(100% - var(--lane-right) + 2px);
+ }
+ .cal-wk-block.cal-wk-mobile-expanded {
+ width: var(--hover-width, 170px);
+ height: var(--hover-fit-height, var(--hover-height, var(--event-height))) !important;
+ overflow: clip;
+ z-index: 30;
+ filter: brightness(1.08);
+ transform: translateY(var(--hover-shift-y, 0));
+ box-shadow: 0 8px 22px color-mix(in srgb, #000 34%, transparent), inset 0 0 0 1px color-mix(in srgb, var(--fg) 18%, transparent);
+ }
+ .cal-wk-block.cal-wk-mobile-expanded .cal-wk-block-label {
+ position: absolute;
+ top: 5px;
+ width: calc(var(--hover-width, 170px) - 18px);
+ animation: cal-wk-hover-label-reveal 0.2s ease-out both;
+ }
+ .cal-wk-block.cal-wk-mobile-expanded.cal-wk-expand-left .cal-wk-block-label {
+ left: auto;
+ right: 7px;
+ }
+ .cal-wk-block.cal-wk-mobile-expanded.cal-wk-expand-right .cal-wk-block-label {
+ left: 7px;
+ right: auto;
+ }
+ .cal-wk-block.cal-wk-mobile-expanded .cal-wk-block-name {
+ display: block;
+ white-space: normal;
+ overflow: visible;
+ text-overflow: clip;
+ overflow-wrap: anywhere;
+ -webkit-line-clamp: unset;
+ }
+}
+/* Resize handle: a 6 px hit zone along the block's bottom edge. The
+ visible 2-px stripe only shows on hover so blocks at rest stay clean. */
+.cal-wk-block-resize {
+ position: absolute;
+ left: 4px;
+ right: 4px;
+ bottom: 0;
+ height: 6px;
+ cursor: ns-resize;
+ z-index: 3;
+}
+/* Drag-to-create ghost */
+.cal-wk-ghost {
+ position: absolute;
+ left: 2px;
+ right: 2px;
+ background: color-mix(in srgb, var(--accent, var(--fg)) 28%, transparent);
+ border: 1px dashed color-mix(in srgb, var(--accent, var(--fg)) 70%, transparent);
+ border-radius: 4px;
+ pointer-events: none;
+ z-index: 3;
+ font-size: 10px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--accent, var(--fg));
+ font-variant-numeric: tabular-nums;
+}
+.cal-wk-slot-selected {
+ background: color-mix(in srgb, var(--accent, var(--fg)) 20%, var(--bg));
+ border-style: solid;
+ border-color: var(--accent, var(--fg));
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--fg)) 20%, transparent);
+ overflow: visible;
+ pointer-events: auto;
+ cursor: pointer;
+}
+.cal-wk-slot-resize {
+ position: absolute;
+ left: 14%;
+ right: 14%;
+ bottom: -7px;
+ height: 14px;
+ z-index: 4;
+ cursor: ns-resize;
+ touch-action: none;
+ pointer-events: auto;
+}
+.cal-year { display:grid; grid-template-columns:repeat(4, 1fr); gap:10px; }
+.cal-year-month { background:color-mix(in srgb, var(--fg) 3%, var(--bg)); border:1px solid var(--border); border-radius:6px; padding:6px; cursor:pointer; transition:background 0.15s, border-color 0.15s, transform 0.15s; }
+.cal-year-month:hover { background:color-mix(in srgb, var(--fg) 8%, var(--bg)); border-color:var(--accent); transform:translateY(-1px); }
+.cal-year-month-title { font-size:11px; font-weight:600; text-align:center; margin-bottom:4px; opacity:0.7; }
+.cal-year-grid { display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; }
+.cal-year-wd { font-size:7px; text-align:center; opacity:0.3; padding:1px 0; }
+.cal-year-cell { font-size:8px; text-align:center; padding:2px 0; min-height:14px; border-radius:2px; transition:transform 0.12s, background 0.12s; }
+.cal-year-day:hover { background:var(--accent, var(--red)); color:#fff; opacity:1; transform:scale(1.15); font-weight:700; z-index:2; position:relative; }
+.cal-year-today { color:var(--accent, var(--red)); font-weight:700; opacity:1; }
+.cal-year-has { opacity:1; background:color-mix(in srgb, var(--accent) 15%, transparent); }
+.cal-year-has.cal-year-today { background:color-mix(in srgb, var(--accent, var(--red)) 25%, transparent); }
+.cal-loading { display:flex; justify-content:center; padding:40px 0; }
+.cal-badge { background:var(--accent); border-radius:50%; width:6px; height:6px; margin-left:auto; display:inline-block; flex-shrink:0; }
+/* Search input — in-panel variant lives inside the day-detail panel and
+ spans its width; no width animation since growing/shrinking on every
+ keystroke would jitter beside the events list. */
+.cal-search-input { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); border:1px solid var(--border); border-radius:5px; padding:0 8px; height:24px; color:var(--fg); font-size:11px; font-family:inherit; width:120px; box-sizing:border-box; transition:width 0.15s; }
+.cal-search-input:focus { outline:none; border-color:var(--accent); width:180px; }
+.cal-search-input.cal-day-search { width:100%; margin-bottom:0; transition:none; padding-left: 28px; }
+/* Wrap for the search input so the magnifying-glass icon can be absolute-
+ positioned inside the field. */
+.cal-search-wrap {
+ position: relative;
+ display: block;
+ margin-bottom: 6px;
+}
+.cal-search-wrap .cal-search-icon {
+ position: absolute;
+ left: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ color: var(--accent, var(--red));
+}
+.cal-day-search-meta { font-size:10px; opacity:0.5; margin:0 2px 6px; }
+.cal-search-results { display:flex; flex-direction:column; gap:4px; }
+.cal-search-count { font-size:10px; opacity:0.5; padding:4px 2px 8px; }
+/* Agenda view */
+.cal-agenda { display:flex; flex-direction:column; gap:12px; flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding-left:4px; }
+.cal-agenda-day { display:flex; flex-direction:column; gap:5px; position:relative; padding-left:14px; }
+.cal-agenda-date { font-size:12px; font-weight:800; opacity:0.72; padding:5px 2px 3px; border-bottom:1px solid var(--border); }
+.cal-agenda-today-badge {
+ display: inline-block;
+ margin-left: 6px;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ padding: 1px 6px;
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
+ color: var(--accent, var(--red));
+ vertical-align: 1px;
+}
+.cal-agenda-empty { font-size:11px; opacity:0.4; padding:8px; font-style:italic; }
+.cal-agenda-event {
+ display:flex;
+ gap:9px;
+ padding:9px 10px;
+ border-radius:6px;
+ cursor:pointer;
+ align-items:center;
+ position:relative;
+ background:color-mix(in srgb, var(--fg) 3.5%, transparent);
+ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 6%, transparent);
+ animation:cal-event-enter 0.18s ease-out both;
+ transition:background 0.15s, box-shadow 0.15s, transform 0.15s;
+}
+.cal-agenda-event:hover {
+ background:color-mix(in srgb, var(--fg) 8%, transparent);
+ box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--fg) 12%, transparent);
+ transform:translateX(2px);
+}
+.cal-agenda-event .cal-event-more,
+.cal-event-item .cal-event-more { opacity:0; transition:opacity 0.15s; }
+.cal-event-item { position:relative; }
+
+/* ── Personal Assistant ── */
+.sidebar-assistant-entry { gap: 6px; min-height: 29px; box-sizing: border-box; align-items: center; }
+.sidebar-assistant-entry .sidebar-action-icon { position: relative; left: -2px; }
+#sidebar-assistant-gear:hover { opacity: 0.8 !important; }
+.assistant-settings-form { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 2px; }
+.assistant-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: color-mix(in srgb, var(--fg) 70%, transparent); }
+.assistant-field > span { font-size: 11px; opacity: 0.6; }
+.assistant-field input[type="text"],
+.assistant-field input[type="time"],
+.assistant-field select,
+.assistant-field textarea {
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ padding: 7px 10px;
+ color: var(--fg);
+ font-size: 12px;
+ font-family: inherit;
+ box-sizing: border-box;
+}
+.assistant-field textarea { resize: vertical; min-height: 60px; }
+.assistant-field input:focus,
+.assistant-field select:focus,
+.assistant-field textarea:focus { outline: none; border-color: var(--accent, var(--red)); }
+.assistant-field-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
+.assistant-field-row > .assistant-field { flex: 1 1 180px; }
+.assistant-field-check { flex-direction: row !important; align-items: center; gap: 6px; padding-bottom: 7px; }
+.assistant-checkins { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--border); }
+.assistant-checkins h5 { margin: 0 0 2px; font-size: 12px; font-weight: 600; opacity: 0.8; }
+.assistant-checkin-row {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+}
+.assistant-checkin-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
+.assistant-checkin-head input[type="text"],
+.assistant-checkin-head input[type="time"] {
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 4px 8px;
+ color: var(--fg);
+ font-size: 12px;
+ font-family: inherit;
+}
+.assistant-checkin-head input[type="text"] { flex: 1; min-width: 120px; }
+.assistant-checkin-head input[type="time"] { width: 90px; }
+.assistant-checkin-run {
+ background: none;
+ border: 1px solid var(--border);
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.15s;
+}
+.assistant-checkin-run:hover { opacity: 1; }
+.assistant-checkin-prompt {
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 6px 8px;
+ color: var(--fg);
+ font-size: 12px;
+ font-family: inherit;
+ resize: vertical;
+ min-height: 48px;
+}
+.assistant-checkin-prompt:focus { outline: none; border-color: var(--accent, var(--red)); }
+.assistant-checkin-meta { font-size: 10px; opacity: 0.45; }
+.assistant-settings-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ margin-top: 8px;
+ padding-top: 8px;
+ border-top: 1px solid var(--border);
+}
+.assistant-tools-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ max-height: 240px;
+ overflow-y: auto;
+ padding: 6px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ margin-top: 4px;
+}
+.assistant-tool-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ align-items: center;
+}
+.assistant-tool-group-label {
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.5;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ width: 100%;
+ margin-bottom: 1px;
+}
+.assistant-tool-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-size: 11px;
+ padding: 2px 6px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.1s, border-color 0.1s;
+ user-select: none;
+}
+.assistant-tool-item:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+}
+.assistant-tool-item:has(input:checked) {
+ border-color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 10%, transparent);
+}
+.assistant-tool-item input[type="checkbox"] {
+ width: 12px;
+ height: 12px;
+ margin: 0;
+}
+
+/* Empty state */
+.cal-empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:60px 20px; text-align:center; gap:10px; }
+.cal-empty-title { font-size:14px; font-weight:600; opacity:0.8; }
+.cal-empty-msg { font-size:12px; opacity:0.5; max-width:320px; line-height:1.5; margin-bottom:8px; }
+.cal-allday-switch { margin:0; flex-shrink:0; }
+/* Stack the "All day" label above its toggle so it doesn't get squeezed /
+ truncated next to the date inputs in the row. */
+.cal-allday-ctrl { display:flex; flex-direction:column; align-items:center; gap:3px; flex-shrink:0; }
+.cal-allday-label { font-size:10px; opacity:0.5; white-space:nowrap; line-height:1; }
+.cal-form { display:flex; flex-direction:column; gap:8px; }
+.cal-form-title { font-size:13px; font-weight:600; margin-bottom:2px; }
+.cal-title-wrap { position: relative; }
+/* Calendar-picker select sits 2px higher; its border-left/background tint is
+ set in JS to the chosen calendar's colour. */
+.cal-f-cal-select { position: relative; top: -4px; }
+
+/* ── Bespoke event form ────────────────────────────────────────────
+ Big clock-face date/time hero with the title under it. Details panel
+ stays collapsed until the title gets focus or "Add details" is clicked.
+*/
+.cal-form-bespoke {
+ position: relative;
+ width: min(720px, calc(100% - 24px));
+ max-width: none;
+ margin: 12px auto;
+ padding: 18px 28px 14px;
+ box-sizing: border-box;
+ background: var(--bg);
+ /* Optional per-event tint set by JS via --ev-color. When unset, falls
+ back to the theme's neutral border so the card looks like normal. */
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ box-shadow: 0 8px 24px color-mix(in srgb, var(--fg) 6%, transparent);
+ transition: border-color 0.18s, box-shadow 0.18s;
+}
+/* When a colour is picked, lift the card with a soft tinted border + glow
+ so the choice is unmistakable. */
+.cal-form-bespoke[style*="--ev-color"] {
+ border-color: color-mix(in srgb, var(--ev-color) 55%, var(--border));
+ box-shadow: 0 8px 28px color-mix(in srgb, var(--ev-color) 22%, transparent);
+}
+.cal-form-bespoke[style*="--ev-color"] .cal-btn-primary {
+ background: var(--ev-color);
+ border-color: var(--ev-color);
+}
+.cal-form-bespoke.cal-form-bg-image .cal-btn-primary {
+ background: var(--accent, var(--red));
+ border-color: var(--accent, var(--red));
+ color: var(--bg);
+}
+.cal-form-close {
+ position: absolute; top: 10px; right: 12px;
+ background: none; border: none; color: var(--fg);
+ opacity: 0.35; cursor: pointer; font-size: 20px; line-height: 1;
+ padding: 4px 8px; border-radius: 6px;
+}
+.cal-form-close:hover { opacity: 0.9; background: color-mix(in srgb, var(--fg) 8%, transparent); }
+.cal-form-mobile-cancel { display: none; }
+
+/* "Today is …" header pinned at the top of the form. Keeps you oriented
+ even when the event date you're picking is way off in the future. */
+.cal-form-today {
+ text-align: center;
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ opacity: 0.45;
+ margin-bottom: 0;
+}
+
+/* Hero — clock face + date label. Spacing between the two lives entirely
+ on the clock's bottom margin so it's predictable. */
+.cal-hero {
+ display: flex; flex-direction: column; align-items: center;
+ gap: 0; margin-bottom: 12px;
+}
+.cal-hero-time {
+ display: inline-flex; align-items: baseline; gap: 8px;
+ font-family: ui-monospace, SFMono-Regular, "Menlo", "Consolas", monospace;
+ font-variant-numeric: tabular-nums;
+ font-size: 56px;
+ letter-spacing: 0.02em;
+ font-weight: 200;
+ color: var(--fg);
+ line-height: 1;
+ /* Behaves like a button — strip native chrome but keep it tappable. */
+ background: none;
+ border: none;
+ /* Pill that reads as a real hit target for a 56-px clock face.
+ Top padding kept tight so the clock sits close to the "Today is" line. */
+ padding: 6px 38px 18px;
+ margin: 0 0 24px;
+ border-radius: 18px;
+ cursor: pointer;
+ line-height: 1;
+ transition: background 0.15s, color 0.15s;
+}
+/* The clock is a `` and a global `button:hover` rule paints
+ every button's background on hover. Suppress it explicitly. */
+.cal-hero-time:hover { background: transparent; border-color: transparent; }
+.cal-hero-clock { font-feature-settings: "tnum"; display: inline-flex; align-items: baseline; }
+/* Per-segment hover so it's clear hh vs mm are individually clickable. */
+.cal-hero-clock-hh, .cal-hero-clock-mm {
+ display: inline-block;
+ border-radius: 6px;
+ padding: 0 2px;
+ transition: background 0.12s;
+}
+.cal-hero-clock-hh:hover, .cal-hero-clock-mm:hover {
+ background: color-mix(in srgb, var(--fg) 9%, transparent);
+}
+.cal-hero-sep { padding: 0 2px; opacity: 0.55; pointer-events: none; }
+.cal-hero-ampm {
+ font-size: 14px;
+ letter-spacing: 0.12em;
+ opacity: 0.55;
+ font-weight: 500;
+ text-transform: uppercase;
+}
+.cal-hero-date {
+ font-size: 16px;
+ opacity: 0.6;
+ letter-spacing: 0.04em;
+ background: none;
+ border: none;
+ color: inherit;
+ font-family: inherit;
+ padding: 4px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+ margin: 7px 0 0; /* extra nudge below the clock */
+ transition: background 0.15s, opacity 0.15s;
+}
+.cal-hero-date:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); opacity: 0.85; }
+
+/* Title input — flat, large, no chrome until focused */
+.cal-input.cal-hero-title {
+ font-size: 18px;
+ text-align: left;
+ padding: 10px 12px 10px 0;
+ background: transparent;
+ border: 1px solid transparent;
+ border-bottom: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
+ border-radius: 0;
+ margin-bottom: 4px;
+}
+
+/* Details panel — animated reveal via max-height + opacity. */
+.cal-form-bespoke .cal-form-details {
+ display: block;
+ max-height: 0;
+ opacity: 0;
+ overflow: hidden;
+ transition: max-height 240ms ease, opacity 180ms ease, margin 220ms ease;
+ margin-top: 0;
+}
+/* The detail children themselves keep the existing flex/gap rules from
+ the regular .cal-form, so we don't restyle inputs/rows here. */
+.cal-form-bespoke .cal-form-details > * + * { margin-top: 8px; }
+
+
+/* Location row: input + Apple Maps pin link. Pin only lights up when the
+ field has content; clicking opens maps.apple.com (native Maps app on
+ Apple devices, web fallback elsewhere). All theme vars. */
+.cal-loc-row {
+ display: flex;
+ align-items: stretch;
+ gap: 6px;
+}
+.cal-loc-input-wrap {
+ position: relative;
+ flex: 1;
+ min-width: 0;
+}
+.cal-loc-hint {
+ position: absolute;
+ inset: 0 auto 0 10px;
+ display: none;
+ align-items: center;
+ gap: 5px;
+ pointer-events: none;
+ color: color-mix(in srgb, var(--fg) 42%, transparent);
+ font-size: 12px;
+}
+.cal-loc-map {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ flex-shrink: 0;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--fg) 5%, var(--bg));
+ text-decoration: none;
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
+}
+.cal-loc-map:hover { background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg)); border-color: var(--accent, var(--red)); }
+
+@media (max-width: 520px) {
+ .cal-hero-time { font-size: 44px; }
+ .cal-form-bespoke { margin: 16px 12px; padding: 32px 16px 14px; }
+ .cal-form-mobile-cancel {
+ display: inline-flex;
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 30px;
+ height: 30px;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--bg) 82%, transparent);
+ color: var(--fg);
+ opacity: 0.72;
+ cursor: pointer;
+ z-index: 3;
+ -webkit-tap-highlight-color: transparent;
+ }
+ .cal-form-mobile-cancel:active {
+ opacity: 1;
+ background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg));
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
+ }
+}
+.cal-input { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); border:1px solid var(--border); border-radius:5px; padding:7px 10px; color:var(--fg); font-size:12px; width:100%; box-sizing:border-box; }
+.cal-input:focus { outline:none; border-color:var(--accent); }
+.cal-input-time { width:auto; flex:1; }
+.cal-form-row { display:flex; gap:8px; align-items:center; }
+.cal-form-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:4px; }
diff --git a/static/css/17-research.css b/static/css/17-research.css
new file mode 100644
index 000000000..d2fa2b1d1
--- /dev/null
+++ b/static/css/17-research.css
@@ -0,0 +1,3184 @@
+/* 17-research - 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.
+ */
+button.cal-btn { background:color-mix(in srgb, var(--fg) 8%, transparent); border:1px solid var(--border); color:var(--fg); border-radius:5px; padding:0 14px; height:28px; font-size:11px; font-weight:500; cursor:pointer; font-family:inherit; display:inline-flex; align-items:center; justify-content:center; box-sizing:border-box; transition:background 0.15s, border-color 0.15s; }
+button.cal-btn:hover { background:color-mix(in srgb, var(--fg) 14%, transparent); }
+button.cal-btn.cal-btn-primary { background:var(--accent, var(--red)); color:var(--bg); border-color:var(--accent, var(--red)); }
+button.cal-btn.cal-btn-primary:hover { opacity:0.9; background:var(--accent, var(--red)); }
+@media (max-width: 600px) {
+ .cal-modal-container { max-width:100vw; width:100vw; border-radius:0; max-height:100vh; height:100vh; }
+ .cal-day { min-height:58px; padding:3px; }
+ .cal-day-num { font-size:10px; }
+ .cal-event-preview { display:none; }
+ .cal-multiday { font-size:8.5px; padding:0 4px; }
+ .cal-event-row { min-height:16px; padding:2px 3px; gap:3px; }
+ .cal-event-row-name,
+ .cal-event-row-time { font-size:9.5px; }
+ /* Two-row toolbar on mobile:
+ Row 1 — Title (full width)
+ Row 2 — [← Today →] [view toggle] [settings/sync/filters/+New]
+ Title gets its own row so it isn't squeezed to nothing when the
+ right-side group is wide. The .cal-toolbar-nav (which now only
+ contains the title since JS moved the date-nav to the right) is
+ width:100% so it forces a wrap before .cal-toolbar-right. */
+ .cal-toolbar {
+ gap: 4px;
+ flex-wrap: wrap;
+ row-gap: 4px;
+ }
+ .cal-toolbar-nav {
+ flex-wrap: nowrap;
+ flex: 1 1 100%;
+ min-width: 0;
+ justify-content: flex-start;
+ }
+ .cal-toolbar-right {
+ margin-top: 6px;
+ margin-left: 0;
+ flex-wrap: nowrap;
+ flex-shrink: 1;
+ overflow-x: auto;
+ scrollbar-width: none;
+ width: 100%;
+ }
+ .cal-quickadd-row {
+ top: -2px;
+ }
+ .cal-toolbar-right::-webkit-scrollbar { display: none; }
+ .cal-title {
+ font-size: 13px;
+ padding: 0 4px;
+ }
+ #cal-side-prev,
+ #cal-side-next {
+ display: none !important;
+ }
+ .cal-nav { padding: 2px 5px; font-size: 10px; }
+ .cal-view-btn { padding: 2px 5px; font-size: 9px; }
+ .cal-filters {
+ gap: 3px;
+ margin: 0 0 6px;
+ padding: 0;
+ }
+ .cal-filter-toggle { top: -5px; }
+ .cal-filter-item { font-size: 9px; padding: 1px 5px; }
+ .cal-year { grid-template-columns:repeat(3, 1fr); gap:6px; }
+ .cal-year-cell { font-size:7px; }
+}
+
+/* ═══ Research Panel ═══ */
+/* .research-overlay inherits from .modal — no extra layout needed */
+.research-pane {
+ display:flex; flex-direction:column;
+ padding:10px; box-sizing:border-box;
+ font-size:12px;
+ position: relative;
+ isolation: isolate;
+ overflow: hidden;
+ /* Research hydration replaces picker options after mount. Do not inherit
+ the generic scale-in animation: scaling a changing-height frame reads as
+ a bounce, particularly next to the document split pane. */
+ animation: research-pane-enter 0.18s ease-out both;
+}
+/* Synapse signal traveling around the outer edge of the pane. The pane keeps a
+ flat background; only this thin border ring remains animated. */
+.research-pane::after {
+ content: '';
+ position: absolute; inset: 0;
+ z-index: 1;
+ pointer-events: none;
+ border-radius: inherit;
+ padding: 2px;
+ background: conic-gradient(
+ from var(--research-orbit-angle, 0deg),
+ transparent 0deg,
+ transparent 300deg,
+ color-mix(in srgb, var(--accent, var(--red)) 70%, transparent) 335deg,
+ var(--accent, var(--red)) 350deg,
+ color-mix(in srgb, var(--accent, var(--red)) 70%, transparent) 358deg,
+ transparent 360deg
+ );
+ -webkit-mask:
+ linear-gradient(#000 0 0) content-box,
+ linear-gradient(#000 0 0);
+ -webkit-mask-composite: xor;
+ mask:
+ linear-gradient(#000 0 0) content-box,
+ linear-gradient(#000 0 0);
+ mask-composite: exclude;
+}
+@media (prefers-reduced-motion: reduce) {
+ .research-pane::after { animation: none; opacity: 0.4; }
+}
+.research-pane-header {
+ display:flex; justify-content:space-between; align-items:center;
+ margin-bottom:6px; cursor:grab; user-select:none;
+}
+.research-pane-title-icon {
+ display: inline-flex;
+ align-items: center;
+}
+.research-pane-title-icon svg { display: block; }
+.research-pane-header-actions {
+ display: flex; align-items: center; gap: 2px;
+ margin-left: auto;
+}
+.research-pane-body {
+ flex: 1; min-height: 0; overflow: hidden;
+ display: flex; flex-direction: column; gap: 10px;
+ /* Flat surface — matches Library's .modal-body (no inset sub-window).
+ The outer .research-pane already provides the 10px padding + border. */
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: 0;
+ border-radius: 0;
+ font-size: 12px;
+ color: var(--fg);
+}
+/* Research uses the same flat tab navigation as Memory and Skills. Each view
+ owns its scrolling so switching tabs never leaves the form or result list
+ clipped by the modal body. */
+.research-tabs {
+ flex: 0 0 auto;
+ margin: -4px -4px 0;
+ padding: 0 4px;
+}
+.research-tabs .memory-tab {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ white-space: nowrap;
+}
+.research-tabs .memory-tab[hidden] { display: none !important; }
+.research-tabs .memory-tab svg {
+ width: 12px;
+ height: 12px;
+ flex: 0 0 12px;
+ opacity: 0.75;
+}
+.research-tab-panel {
+ flex: 1 1 auto;
+}
+.research-tab-heading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 0 0 auto;
+ min-height: 28px;
+ margin: 0 0 6px;
+}
+.research-tab-heading > .memory-count {
+ font-size: 10px;
+ opacity: 0.55;
+ font-weight: normal;
+}
+.research-tab-heading #research-history-clear { margin-left: auto; }
+/* Match the .admin-card surface used in Cookbook (Download / Serve sections):
+ var(--panel) bg with var(--border) and rounded corners. */
+.research-new-job {
+ flex-shrink: 0;
+}
+.research-query {
+ width:100%; resize:vertical; min-height:80px; max-height:240px;
+ /* Match the rest of the app's inputs (var(--bg) page tone) rather than
+ the darker panel tone — keeps the input readable on the dark sub-window. */
+ background: var(--bg); color: var(--fg);
+ border:1px solid var(--border); border-radius:6px;
+ padding:8px 10px; font-size:12px; font-family:inherit;
+ box-sizing:border-box;
+ margin-top: 6px;
+}
+.research-query:focus { outline:none; border-color:var(--accent-primary, var(--red)); }
+.research-settings-row {
+ display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+}
+.research-setting {
+ display:flex; flex-direction:column; flex:1; min-width:104px;
+}
+.research-setting:has(#research-rounds),
+.research-setting:has(#research-category) {
+ position: relative;
+ top: 0;
+}
+.research-setting:has(#research-search-provider),
+.research-setting:has(#research-endpoint) {
+ position: relative;
+ top: 5px;
+}
+.research-setting:has(#research-model) {
+ position: relative;
+ top: 1px;
+}
+.research-setting-label {
+ font-size:9px; text-transform:uppercase; letter-spacing:0.5px;
+ color:var(--accent, var(--red)); opacity:1; margin-bottom:2px;
+}
+.research-native-select {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ margin: -1px !important;
+ padding: 0 !important;
+ overflow: hidden !important;
+ clip: rect(0 0 0 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+.research-picker { position: relative; min-width: 0; }
+.research-picker-btn {
+ width: 100%; height: 28px; padding: 0 8px;
+ display: flex; align-items: center; gap: 6px;
+ background: var(--bg); color: var(--fg);
+ border: 1px solid var(--border); border-radius: 6px;
+ font: inherit; font-size: 12px; cursor: pointer; text-align: left;
+ transition: border-color 0.14s ease, background 0.14s ease;
+}
+.research-picker-btn:hover,
+.research-picker.open .research-picker-btn {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 62%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 5%, var(--bg));
+}
+.research-picker-current {
+ min-width: 0; flex: 1; display: flex; align-items: center; gap: 6px;
+}
+.research-picker-icon {
+ width: 15px; height: 15px; flex: 0 0 15px;
+ display: inline-flex; align-items: center; justify-content: center;
+ color: var(--accent, var(--red));
+}
+.research-picker-icon svg { width: 15px; height: 15px; display: block; }
+.research-picker-btn > svg {
+ width: 9px; height: 9px; flex-shrink: 0; opacity: 0.5;
+ transition: transform 0.14s ease;
+}
+.research-picker-menu {
+ position: absolute; z-index: 120;
+ top: calc(100% + 4px); left: 0;
+ width: max-content; min-width: 100%; max-width: min(280px, 82vw);
+ max-height: 230px; overflow-y: auto;
+ padding: 4px;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border); border-radius: 7px;
+ box-shadow: 0 10px 28px rgba(0,0,0,0.32);
+}
+.research-picker-option {
+ width: 100%; min-height: 31px; padding: 6px 7px;
+ display: flex; align-items: center; gap: 7px;
+ border: 0; border-radius: 5px;
+ background: transparent; color: var(--fg);
+ font: inherit; font-size: 12px; text-align: left; cursor: pointer;
+ white-space: nowrap;
+}
+.research-picker-option:hover,
+.research-picker-option:focus {
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+}
+.research-picker-option.active {
+ color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+}
+.research-picker-check { margin-left: auto; opacity: 0; color: var(--accent, var(--red)); }
+.research-controls-row {
+ display:flex; align-items:center; gap:8px; margin-top:10px;
+}
+.research-start-btn {
+ margin-left:auto;
+ display:flex; align-items:center; gap:5px;
+ height:28px; padding:0 14px; border:none; border-radius:6px;
+ background:var(--accent-primary, var(--red)); color:var(--bg);
+ font-size:12px; font-weight:600; cursor:pointer;
+ transition:opacity 0.15s;
+}
+.research-start-btn:hover { opacity:0.85; color:var(--fg); }
+.research-start-btn:hover svg { color:inherit; }
+.research-start-btn:disabled, .research-start-btn.research-start-busy {
+ opacity:0.7; cursor:wait;
+}
+.research-start-spinner {
+ display:inline-block; width:11px; height:11px; vertical-align:-1px; margin-right:4px;
+ border:2px solid currentColor; border-right-color:transparent; border-radius:50%;
+ animation: spin 0.7s linear infinite;
+}
+.research-category-row {
+ display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;
+}
+/* Mobile: keep the category chips on ONE row (scroll horizontally) instead of
+ wrapping to two rows. */
+@media (max-width: 600px) {
+ .research-category-row {
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .research-category-row::-webkit-scrollbar { display: none; }
+ .research-cat { flex-shrink: 0; white-space: nowrap; position: relative; top: -8px;
+}
+}
+/* Match .doclib-chip exactly so categories feel like document filter tags. */
+.research-cat {
+ 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, color 0.15s;
+ position: relative;
+ top: -4px;
+}
+.research-cat:hover { border-color: var(--red); }
+.research-cat.active {
+ background: color-mix(in srgb, var(--red) 15%, transparent);
+ border-color: color-mix(in srgb, var(--red) 40%, transparent);
+ color: var(--red);
+}
+/* Match the Cookbook "Trending models" toggle (a left-aligned memory-toolbar-btn
+ with an arrow + label) instead of the old tiny-uppercase borderless text. */
+.research-settings-toggle {
+ display:flex; align-items:center; gap:6px;
+ width:100%; text-align:left;
+ height:24px; padding:0 8px; margin-top:10px;
+ background:none; border:1px solid var(--border); border-radius:4px;
+ color:color-mix(in srgb, var(--fg) 60%, transparent);
+ font-size:11px; font-family:inherit; cursor:pointer;
+ transition:all 0.15s;
+}
+.research-settings-toggle:hover { color:var(--fg); border-color:var(--fg); }
+.research-settings-chevron { display:inline-flex; transition:transform 0.2s; margin-left:auto; }
+.research-settings-toggle.collapsed .research-settings-chevron { transform:rotate(-90deg); }
+/* When settings is EXPANDED, merge the toggle bar visually into the
+ settings body so it's clear the row below is the toggle's content
+ (no gap, no doubled border between them). */
+.research-settings-toggle:not(.collapsed) {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-color: transparent;
+}
+.research-settings-toggle:not(.collapsed) + .research-settings-row {
+ margin-top: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ border-top-color: color-mix(in srgb, var(--border) 50%, transparent);
+}
+.research-add-btn {
+ height:28px;
+ padding:0 10px;
+ color:var(--fg);
+ font-size:11px;
+}
+.research-add-btn:hover { background:color-mix(in srgb, var(--border) 30%, transparent); }
+
+/* Popover anchored to the Start-All button — pick parallel vs sequential
+ without the heavy "Run N jobs" modal. Drops down by default, flips to
+ drop-up when there's no room below (the .rrm-up modifier is added by
+ the JS that positions it). */
+.research-run-mode-popover {
+ position: fixed;
+ z-index: 12000;
+ min-width: 200px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.35);
+ padding: 5px;
+ font-size: 13px;
+ animation: rrm-pop-in 0.12s ease-out both;
+ transform-origin: top right;
+}
+@keyframes rrm-pop-in {
+ from { opacity: 0; transform: scale(0.96) translateY(-2px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+.research-run-mode-popover .research-run-mode-row {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ width: 100%;
+ text-align: left;
+ background: none;
+ border: none;
+ color: var(--fg);
+ font-family: inherit;
+ padding: 10px 13px;
+ border-radius: 6px;
+ cursor: pointer;
+}
+.research-run-mode-popover .research-run-mode-row svg { flex-shrink: 0; opacity: 0.8; }
+.research-run-mode-popover .rrm-title {
+ font-weight: 600;
+ font-size: 13px;
+ color: var(--fg);
+}
+.research-jobs-list {
+ flex:1; min-height:0; overflow-y:auto; padding:6px 0;
+ display: flex; flex-direction: column; gap: 6px;
+}
+.research-empty {
+ text-align:center; padding:30px 14px;
+ font-size:12px; opacity:0.4;
+}
+/* Cards match Library's .doclib-card: subtle fg 3% tint over var(--bg). */
+.research-job-card {
+ --cat-color: #db8576;
+ margin: 0;
+ padding: 9px 10px 9px 11px;
+ background: color-mix(in srgb, var(--cat-color) 4%, var(--bg));
+ border: 1px solid color-mix(in srgb, var(--cat-color) 25%, var(--border));
+ border-left: 3px solid var(--cat-color);
+ border-radius: 8px;
+ transition: background 0.15s, border-color 0.15s, transform 0.15s;
+}
+.research-job-card:hover {
+ background: color-mix(in srgb, var(--cat-color) 7%, var(--bg));
+ border-color: color-mix(in srgb, var(--cat-color) 40%, var(--border));
+}
+.research-job-card.running {
+ background: color-mix(in srgb, var(--cat-color) 8%, var(--bg));
+ box-shadow: inset 0 1px color-mix(in srgb, var(--cat-color) 14%, transparent);
+}
+.research-job-card.queued { --cat-color: var(--fg-dim, #888); }
+.background-tools-status.agent-thread { min-width: 0; width: calc(100% - 32px); max-width: 640px; }
+.chat-research-card .agent-thread-header { width: 100%; min-height: 44px; background: none; border: 0; border-radius: 0; text-align: left; font-family: inherit; }
+.chat-research-card .agent-thread-header:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
+.chat-research-card.agent-thread-node .agent-thread-dot { top: 23px; }
+.chat-research-card .agent-thread-status { min-width: 0; }
+.chat-research-background { display: inline-flex; align-items: center; gap: 5px; margin-left: 0; white-space: nowrap; flex-shrink: 0; }
+.chat-research-background .spinner-whirlpool { margin: 0 !important; }
+.chat-research-background canvas { transform: translateY(-2px); }
+.chat-research-card .research-job-query { white-space: normal; overflow-wrap: anywhere; font-size: 12px; }
+.chat-research-detail { margin-top: 5px; font-size: 12px; color: var(--fg-dim, #888); }
+.chat-research-open { display: inline-flex; align-items: center; min-height: 44px; padding: 0 8px; margin-top: 4px; color: var(--red); text-decoration: underline; }
+.chat-research-open:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; border-radius: 4px; }
+.research-job-card.error,
+.research-job-card.cancelled { --cat-color: #f44336; }
+/* Per-category theming — picks up a category accent, badge, and soft tint */
+.research-job-card[data-category] { --cat-color: var(--accent, var(--red)); }
+.research-job-card[data-category="product"] { --cat-color: #5b8abf; }
+.research-job-card[data-category="comparison"] { --cat-color: #e5a33a; }
+.research-job-card[data-category="howto"] { --cat-color: #82c882; }
+.research-job-card[data-category="landscape"] { --cat-color: #a07ae0; }
+.research-job-card[data-category="factcheck"] { --cat-color: var(--red); }
+/* Inline category label — sits beside the title in low-opacity colored
+ text instead of a separate pill. Picks up the per-card --cat-color. */
+/* Uncategorized (default-format) research = "standard", shown in the same
+ green as its left border so the green reads as a labelled category. */
+.research-cat-badge.research-cat-standard { color: var(--cat-color); }
+.research-cat-badge {
+ font-size: 9px;
+ font-weight: 600;
+ text-transform: none;
+ letter-spacing: 0;
+ padding: 2px 5px;
+ background: color-mix(in srgb, var(--cat-color) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--cat-color) 22%, transparent);
+ border-radius: 4px;
+ color: var(--cat-color, var(--accent));
+ opacity: 0.9;
+ flex-shrink: 0;
+ margin-left: 0;
+}
+
+/* Category hero banner */
+.research-hero {
+ display: flex; align-items: center; gap: 14px;
+ padding: 16px 18px; margin: 8px 0 14px;
+ border-radius: 10px;
+ background: linear-gradient(
+ 135deg,
+ color-mix(in srgb, var(--cat-color, var(--accent)) 18%, transparent) 0%,
+ color-mix(in srgb, var(--cat-color, var(--accent)) 5%, transparent) 100%
+ );
+ border-left: 4px solid var(--cat-color, var(--accent));
+ position: relative; overflow: hidden;
+}
+.research-hero-icon {
+ flex-shrink: 0; width: 32px; height: 32px;
+ color: var(--cat-color, var(--accent));
+ display: flex; align-items: center; justify-content: center;
+ filter: drop-shadow(0 2px 4px color-mix(in srgb, var(--cat-color, var(--accent)) 40%, transparent));
+}
+.research-hero-text { flex: 1; min-width: 0; }
+.research-hero-label {
+ font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px;
+ font-weight: 700; opacity: 0.7; margin-bottom: 3px;
+ color: var(--cat-color, var(--accent));
+}
+.research-hero-query {
+ font-size: 15px; font-weight: 600;
+ line-height: 1.3; color: var(--fg);
+}
+
+
+
+
+
+
+.research-job-header {
+ display:flex; align-items:center; gap:7px; min-height:24px;
+}
+.research-job-query {
+ flex:1; min-width:0; font-size:12px; font-weight:650;
+ letter-spacing: 0;
+ overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
+ color: var(--fg);
+ transform: translateY(-4px);
+}
+.research-job-time,
+.research-job-meta {
+ font-size:10px; opacity:0.5; white-space:nowrap; font-family:monospace;
+}
+.research-job-status {
+ font-size:10px; text-transform:uppercase; opacity:0.6;
+}
+.research-job-cancel,
+.research-job-remove,
+.research-job-report-link {
+ background:transparent; border:none; color:var(--fg);
+ opacity:0.4; cursor:pointer; padding:2px; display:flex;
+ transition:opacity 0.15s;
+}
+/* Minimize toggle for the per-job synapse "tree" visual. */
+.research-synapse-toggle {
+ background:transparent; border:none; color:var(--fg);
+ opacity:0.4; cursor:pointer; padding:2px; display:flex;
+ transition:opacity 0.15s;
+}
+.research-job-model {
+ font-size:9px; opacity:0.4; white-space:nowrap;
+ max-width:120px; overflow:hidden; text-overflow:ellipsis;
+}
+.research-job-actions {
+ display:flex; gap:5px; margin-top:7px;
+ align-items:center;
+}
+.research-job-thumb-frame {
+ position: relative;
+ width: 49px;
+ height: 29px;
+ border-radius: 4px;
+ flex: 0 0 49px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
+ background: var(--panel, var(--bg));
+}
+.research-job-summary {
+ align-items: flex-start;
+}
+.research-job-summary-copy {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ transform: translateY(-20px);
+}
+.research-job-summary .research-job-meta {
+ position: relative;
+ top: 12px;
+ transform: none;
+}
+.research-job-thumb {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+.research-job-thumb-empty {
+ display: inline-block;
+ background:
+ linear-gradient(135deg,
+ color-mix(in srgb, var(--fg) 7%, transparent),
+ color-mix(in srgb, var(--fg) 3%, transparent));
+}
+/* Match .doclib-toolbar-btn — same font, padding, hover */
+.research-job-action {
+ display: inline-flex; align-items: center; gap: 4px;
+ padding: 5px 10px;
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg-muted);
+ font-size: 11px;
+ font-family: inherit;
+ white-space: nowrap;
+ cursor: pointer;
+ transition: all 0.15s;
+}
+.research-job-action:hover {
+ color: var(--fg);
+ border-color: var(--fg);
+}
+.research-job-action-dim { opacity: 0.5; border-color: transparent; }
+.research-job-action-copied {
+ color: var(--color-success) !important;
+ border-color: color-mix(in srgb, var(--color-success) 45%, var(--border)) !important;
+ opacity: 1 !important;
+}
+.research-job-overflow { position: relative; margin-left: auto; }
+.research-job-more {
+ width: 30px; height: 30px; padding: 0;
+ justify-content: center; color: var(--fg-muted);
+}
+.research-job-menu {
+ position: absolute; z-index: 140;
+ right: 0; top: calc(100% + 5px);
+ min-width: 168px;
+ cursor: default;
+}
+.research-job-menu > .dropdown-item-compact svg { width: 13px; height: 13px; flex-shrink: 0; opacity: 0.72; }
+.research-job-menu > [data-menu-action="copy"] { position: relative; top: -2px; }
+.research-job-menu > [data-menu-action="dismiss"] { position: relative; top: 2px; }
+.research-job-phase {
+ font-size:10px; font-weight:600; color:var(--cat-color);
+ margin:7px 0 2px; padding:5px 7px;
+ background:color-mix(in srgb, var(--cat-color) 8%, transparent);
+ border-radius:4px;
+}
+.research-job-queued-meta {
+ font-size:10px; opacity:0.4; margin-top:2px;
+}
+.research-section-divider {
+ display:flex; align-items:center; gap:10px;
+ padding:6px 14px; font-size:10px; opacity:0.4;
+ text-transform:uppercase; letter-spacing:0.5px;
+}
+/* Foldable job sections (Active / Past research) */
+/* Collapsible section styled like the Cookbook download sub-blocks (.cookbook-card):
+ clean var(--bg) card, 8px radius, a real 13px/600 title. Chevron on the LEFT,
+ then title, count, and a status-color dot on the right (the dot stays visible
+ when folded so you get status at a glance). Keeps every modal consistent. */
+.research-section {
+ margin-top: 6px;
+ /* Same surface as the research "window" (.research-new-job -> var(--panel)). */
+ background: var(--panel);
+ border: 1px solid color-mix(in srgb, var(--fg) 12%, var(--border));
+ border-radius: 8px;
+ overflow: visible;
+}
+.research-section-body { display: flex; flex-direction: column; gap: 7px; padding: 9px; }
+.research-section-header {
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
+ padding: 9px 11px; cursor: pointer; user-select: none;
+ transition: background 0.15s;
+ border-radius: 7px 7px 0 0;
+}
+.research-section-header:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
+.research-section-title { font-size: 13px; font-weight: 650; letter-spacing: 0; }
+.research-section-chevron { flex-shrink: 0; opacity: 0.55; transition: transform 0.2s ease; }
+.research-section-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; opacity: 0.9; }
+/* The top "Research" heading is a plain card-style title — no bar/chevron/dot. */
+.research-main-header {
+ cursor: default; background: none; border: none;
+ padding: 6px 2px; font-size: 13px; font-weight: 600;
+}
+.research-main-header:hover { background: none; }
+.research-section-count {
+ /* Match the panel header's "N research" chip — small, muted, no weight. */
+ font-size: 10px; opacity: 0.6; font-weight: normal;
+ font-variant-numeric: tabular-nums;
+}
+.research-job-error {
+ font-size:11px; color:#f44336; margin-top:4px; line-height:1.4;
+ word-break:break-word;
+}
+.research-job-title-error {
+ flex: 0 1 auto;
+ min-width: 0;
+ max-width: 42%;
+ margin-top: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.research-progress-bar {
+ height:4px; background:color-mix(in srgb, var(--cat-color) 13%, var(--border)); border-radius:3px; margin-top:7px;
+ overflow:hidden;
+}
+.research-progress-fill {
+ height:100%; background:var(--cat-color, var(--accent-primary, var(--red)));
+ border-radius:2px; transition:width 0.4s ease;
+}
+/* Active research uses the same compact card language as History. The live
+ map, progress, and navigation feedback are one optional detail region so a
+ long-running job does not permanently take over the active list. */
+#research-pane #research-active-list {
+ gap: 4px;
+ padding: 2px 2px 4px 0;
+}
+#research-pane #research-active-list .research-job-card {
+ display: block;
+ box-sizing: border-box;
+ max-height: none;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ border: 1px solid var(--border);
+ border-left: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: none;
+}
+#research-pane #research-active-list .research-job-card:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 16%, transparent);
+ transform: none;
+}
+#research-pane #research-active-list .research-active-card-header {
+ min-width: 0;
+ min-height: 22px;
+ gap: 6px;
+ cursor: pointer;
+}
+#research-pane #research-active-list .research-job-query {
+ position: static;
+ transform: none;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
+}
+#research-pane #research-active-list .research-job-model {
+ max-width: 110px;
+}
+#research-pane #research-active-list .research-job-time {
+ font-family: inherit;
+ font-size: 10px;
+}
+#research-pane #research-active-list .research-active-card-chevron {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ flex: 0 0 20px;
+ color: var(--fg);
+ opacity: 0.52;
+}
+#research-pane #research-active-list .research-job-overflow {
+ align-self: flex-start;
+ margin-left: 0;
+}
+#research-pane #research-active-list .research-job-more {
+ width: 22px;
+ height: 22px;
+ min-height: 22px;
+ border: 0;
+ background: transparent;
+}
+#research-pane #research-active-list .research-active-detail {
+ width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+ padding-top: 5px;
+ overflow: hidden;
+}
+#research-pane #research-active-list .research-active-detail[hidden] {
+ display: none !important;
+}
+#research-pane #research-active-list .research-job-phase {
+ margin: 0 0 4px;
+}
+#research-pane #research-active-list .research-progress-bar {
+ margin-top: 5px;
+}
+#research-pane #research-active-list .research-navigation-trace-live {
+ margin-top: 6px;
+}
+.research-job-result {
+ margin-top:10px; border-top:1px solid var(--border); padding-top:10px;
+}
+.research-job-sources {
+ display:flex; flex-wrap:wrap; gap:4px; margin-bottom:8px;
+}
+.research-source-link {
+ font-size:10px; padding:2px 6px;
+ background:color-mix(in srgb, var(--accent-primary, var(--red)) 10%, transparent);
+ border-radius:3px; color:var(--accent-primary, var(--red));
+ text-decoration:none; white-space:nowrap;
+ max-width:200px; overflow:hidden; text-overflow:ellipsis;
+}
+.research-source-badges {
+ display:inline-flex;
+ align-items:center;
+ gap:3px;
+ margin-left:5px;
+ vertical-align:1px;
+}
+.research-source-badge {
+ font-size:8px;
+ line-height:1;
+ padding:1px 3px;
+ border-radius:3px;
+ background:color-mix(in srgb, currentColor 12%, transparent);
+ opacity:0.75;
+ text-transform:uppercase;
+}
+.research-source-state {
+ font-size:10px;
+ line-height:1.45;
+ white-space:pre-wrap;
+ color:var(--text-muted);
+ background:color-mix(in srgb, var(--accent-primary, var(--red)) 5%, transparent);
+ border:1px solid color-mix(in srgb, var(--border) 70%, transparent);
+ border-radius:5px;
+ padding:6px 7px;
+ margin:0 0 8px;
+}
+.research-navigation-trace {
+ font-size:10px;
+ line-height:1.4;
+ color:var(--text-muted);
+ background:color-mix(in srgb, var(--panel-bg, var(--bg)) 80%, transparent);
+ border:1px solid color-mix(in srgb, var(--border) 72%, transparent);
+ border-radius:5px;
+ padding:6px 7px;
+ margin:0 0 8px;
+}
+.research-nav-title {
+ font-weight:600;
+ color:var(--text-primary, var(--text));
+ margin-bottom:4px;
+}
+.research-nav-spacer {
+ height:6px;
+}
+.research-nav-row {
+ display:grid;
+ grid-template-columns:auto minmax(0, 1fr) auto;
+ gap:5px;
+ align-items:baseline;
+ min-width:0;
+}
+.research-nav-tool {
+ color:var(--accent-primary, var(--red));
+ font-weight:600;
+}
+.research-action-skipped .research-nav-tool {
+ color:var(--text-muted);
+}
+.research-source-more { font-size:10px; opacity:0.5; padding:2px 4px; }
+.research-job-report-body {
+ font-size:12px; line-height:1.55; max-height:400px; overflow-y:auto;
+}
+.research-job-loading { font-size:11px; opacity:0.5; padding:8px 0; }
+.research-library-section {
+ border-top:1px solid var(--border); flex-shrink:0;
+}
+.research-library-toggle {
+ width:100%; background:transparent; border:none; color:var(--fg);
+ padding:8px 14px; font-size:11px; text-align:left;
+ opacity:0.6; cursor:pointer; font-weight:500;
+}
+.research-library-item {
+ display:flex; align-items:center; gap:6px; flex-wrap:wrap;
+ padding:8px 14px; font-size:11px;
+ border-bottom:1px solid color-mix(in srgb, var(--border) 50%, transparent);
+}
+.research-library-item:hover { background:color-mix(in srgb, var(--border) 20%, transparent); }
+.research-lib-query {
+ flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:120px;
+}
+.research-lib-actions {
+ display:flex; gap:4px; margin-left:auto;
+}
+.research-lib-meta {
+ font-size:9px; opacity:0.4; white-space:nowrap; font-family:monospace;
+}
+.research-lib-open,
+.research-lib-delete {
+ background:transparent; border:none; color:var(--fg);
+ opacity:0.3; cursor:pointer; padding:2px; display:flex;
+ transition:opacity 0.15s;
+}
+.research-lib-delete:hover { opacity:1; color:#f44336; }
+.research-badge {
+ background:var(--accent-primary, var(--red)); border-radius:50%;
+ width:6px; height:6px; margin-left:auto; display:inline-block; flex-shrink:0;
+ position: relative; left: -4px;
+ /* Breathe when research has finished — gentle scale + glow pulse to
+ draw the eye to the unread result without being a hard blink. */
+ animation: research-badge-breathe 2.4s ease-in-out infinite;
+}
+
+/* Sidebar Deep Research running indicator — text then dot, styled the same
+ as Cookbook's running status (no glow). */
+.research-sb-running {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+.research-sb-status {
+ font-size: 8px;
+ opacity: 0.5;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: relative;
+ top: 0;
+ left: -12px;
+}
+.research-sb-dot {
+ width: 6px;
+ height: 6px;
+ flex-shrink: 0;
+ border-radius: 50%;
+ background: var(--color-success, #4caf50);
+ animation: cookbook-notif-pulse 2s ease-in-out infinite;
+ position: relative;
+ top: -1px;
+ left: -4px;
+}
+
+/* ── In-house color picker ── */
+/* A color-picker swatch input shows the chosen colour as its background; hide
+ the underlying hex text/caret so it doesn't read as "#a1b2c3" in the box.
+ (The themed/gallery variants set this too; this is the generic fallback for
+ swatches outside those scopes, e.g. the calendar-settings color dots.) */
+input.cp-swatch-input {
+ color: transparent;
+ font-size: 0;
+ text-shadow: none;
+ caret-color: transparent;
+ user-select: none;
+}
+input.cp-swatch-input::selection { background: transparent; }
+.cp-popover {
+ position: fixed; z-index: 10000;
+ display: none;
+ width: 240px;
+ background: var(--panel, #1a1a1a);
+ border: 1px solid var(--border, #333);
+ border-radius: 8px;
+ padding: 10px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.45);
+ font-family: inherit; color: var(--fg, #eee); font-size: 12px;
+ user-select: none;
+}
+@media (max-width: 768px) {
+ .cp-popover {
+ width: min(240px, calc(100vw - 24px));
+ max-height: calc(100dvh - 24px);
+ overflow-y: auto;
+ }
+ .cp-sl {
+ height: min(150px, 34dvh);
+ }
+}
+.cp-sl {
+ position: relative;
+ width: 100%; height: 160px;
+ border-radius: 6px;
+ cursor: crosshair;
+ overflow: hidden;
+ touch-action: none;
+}
+.cp-sl-white {
+ position: absolute; inset: 0;
+ background: linear-gradient(to right, #fff, rgba(255,255,255,0));
+ pointer-events: none;
+}
+.cp-sl-black {
+ position: absolute; inset: 0;
+ background: linear-gradient(to top, #000, rgba(0,0,0,0));
+ pointer-events: none;
+}
+.cp-sl-handle {
+ position: absolute;
+ width: 12px; height: 12px;
+ margin: -6px 0 0 -6px;
+ border: 2px solid #fff;
+ border-radius: 50%;
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 1px 3px rgba(0,0,0,0.5);
+ pointer-events: none;
+}
+.cp-hue {
+ position: relative;
+ margin-top: 10px;
+ height: 14px;
+ border-radius: 7px;
+ background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
+ cursor: pointer;
+ touch-action: none;
+}
+.cp-hue-handle {
+ position: absolute; top: 50%;
+ width: 10px; height: 18px;
+ margin: -9px 0 0 -5px;
+ border: 2px solid #fff;
+ border-radius: 3px;
+ box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 1px 3px rgba(0,0,0,0.5);
+ pointer-events: none;
+}
+.cp-row {
+ display: flex; align-items: center; gap: 6px; margin-top: 10px;
+}
+.cp-preview {
+ width: 24px; height: 24px;
+ border-radius: 50%;
+ border: 1px solid var(--border, #333);
+ flex-shrink: 0;
+}
+.cp-hex {
+ flex: 1; min-width: 0;
+ padding: 4px 6px;
+ background: var(--bg, #000); color: var(--fg, #eee);
+ border: 1px solid var(--border, #333);
+ border-radius: 4px;
+ font-family: ui-monospace, 'Fira Code', monospace;
+ font-size: 12px; text-transform: lowercase;
+}
+.cp-hex:focus { outline: none; border-color: var(--red, #e06c75); }
+.cp-copy {
+ width: 26px; height: 26px;
+ padding: 0;
+ background: transparent;
+ border: 1px solid var(--border, #333);
+ border-radius: 4px;
+ color: var(--fg, #eee);
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex: 0 0 26px;
+ transition: background 0.1s, color 0.1s;
+}
+.cp-copy:hover { background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent); color: var(--accent, var(--red)); }
+.cp-eyedropper {
+ width: 26px; height: 26px;
+ padding: 0;
+ background: transparent;
+ border: 1px solid var(--border, #333);
+ border-radius: 4px;
+ color: var(--fg, #eee);
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center;
+ flex-shrink: 0;
+ transition: background 0.1s;
+}
+.cp-eyedropper:hover:not(:disabled) { background: rgba(255,255,255,0.08); }
+.cp-section-label {
+ font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
+ opacity: 0.5; margin-top: 10px; margin-bottom: 4px;
+}
+.cp-swatches {
+ display: flex; flex-wrap: wrap; gap: 4px;
+}
+.cp-swatch {
+ width: 20px; height: 20px;
+ border-radius: 50%;
+ border: 1px solid var(--border, #333);
+ cursor: pointer;
+ padding: 0;
+ transition: transform 0.08s;
+}
+.cp-swatch:hover { transform: scale(1.15); }
+.cp-recent-empty {
+ font-size: 11px; opacity: 0.4; padding: 2px 0;
+}
+
+/* PDF form export modal + signature modals — match the app theme via the
+ existing modal/confirm-btn classes. Only a few atom-level styles below. */
+.pdf-export-overlay .modal-content { padding: 12px 14px; }
+.pdf-export-overlay #pdf-export-body { padding-right: 4px; }
+.pdf-export-overlay .pdf-export-section {
+ margin-bottom: 14px;
+}
+.pdf-export-overlay .pdf-export-section-title {
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--red);
+ opacity: 0.85;
+ margin-bottom: 6px;
+}
+.pdf-export-overlay .pdf-export-row {
+ display: grid;
+ grid-template-columns: 38% 62%;
+ gap: 8px;
+ align-items: center;
+ padding: 3px 0;
+}
+.pdf-export-overlay .pdf-export-row label {
+ font-size: 0.78rem;
+ color: var(--fg);
+ opacity: 0.85;
+}
+.pdf-export-input,
+.pdf-export-overlay select.pdf-export-input,
+.pdf-export-overlay input.pdf-export-input {
+ padding: 4px 6px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ border-radius: 4px;
+ font-size: 0.78rem;
+ font-family: inherit;
+}
+.pdf-export-input:focus {
+ outline: none;
+ border-color: var(--fg);
+}
+.sig-modal-overlay .modal-content { padding: 12px 14px; }
+.sig-modal-overlay .sig-canvas {
+ display: block;
+ width: 100%;
+ height: 240px;
+ background: #fff;
+ cursor: crosshair;
+ border: 1px dashed var(--border);
+ border-radius: 5px;
+ /* Drawing uses pointer events; without this, a touch-drag on the canvas pans
+ the page / sheet instead of drawing ("the whole window moves"). */
+ touch-action: none;
+}
+.sig-modal-overlay .sig-name,
+.sig-modal-overlay .sig-smoothness {
+ font-family: inherit;
+}
+.sig-modal-overlay .sig-smoothness {
+ accent-color: var(--accent, var(--red));
+ margin: 0;
+}
+.sig-modal-overlay .sig-name {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--fg);
+ border-radius: 4px;
+ font-size: 0.85rem;
+}
+.sig-modal-overlay .sig-tile {
+ position: relative;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 8px;
+ cursor: pointer;
+ background: var(--bg);
+}
+.sig-modal-overlay .sig-tile:hover { border-color: var(--fg); }
+.sig-modal-overlay .sig-tile img {
+ display: block;
+ width: 100%;
+ height: 80px;
+ object-fit: contain;
+ background: #fff;
+ border-radius: 3px;
+}
+.sig-modal-overlay .sig-tile-del {
+ position: absolute;
+ top: 1px;
+ right: 6px;
+ width: 20px;
+ height: 20px;
+ /* Center the × glyph inside the round bg — without this the button's default
+ text metrics push it past the right edge of the circle. */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0 0 1px;
+ box-sizing: border-box;
+ font-size: 0.85rem;
+ line-height: 1;
+ border: 1px solid var(--accent, var(--red));
+ background: #fff;
+ color: var(--accent, var(--red));
+ border-radius: 50%;
+ cursor: pointer;
+ opacity: 1;
+}
+.sig-modal-overlay .sig-tile-del:hover {
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, #fff);
+}
+.sig-modal-overlay .sig-new-tile {
+ border: 2px dashed var(--border);
+ border-radius: 6px;
+ padding: 8px;
+ cursor: pointer;
+ background: var(--bg);
+ color: var(--fg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 106px;
+ font-weight: 600;
+ opacity: 0.85;
+}
+.sig-modal-overlay .sig-new-tile:hover {
+ opacity: 1;
+ border-color: var(--fg);
+}
+
+/* Form checkboxes — render as a circular dot, theme-aware. Used in the
+ Export PDF modal and the inline PDF view overlay. */
+.pdf-export-overlay input[type="checkbox"],
+#doc-pdf-view input[type="checkbox"] {
+ -webkit-appearance: none;
+ appearance: none;
+ margin: 0;
+ cursor: pointer;
+ border-radius: 50%;
+ background: #fff;
+ position: relative;
+}
+.pdf-export-overlay input[type="checkbox"] {
+ width: 14px;
+ height: 14px;
+ border: 1.5px solid var(--border);
+ background: var(--bg);
+}
+.pdf-export-overlay input[type="checkbox"]:checked {
+ background: var(--fg);
+ border-color: var(--fg);
+}
+/* PDF view overlays on a white page render — keep colors theme-independent
+ so the dot is always visible against the rendered PDF. */
+#doc-pdf-view input[type="checkbox"] {
+ border: 1.5px solid #444;
+}
+#doc-pdf-view input[type="checkbox"]:checked {
+ background: #111;
+ border-color: #111;
+}
+
+/* ─────────────────────────────────────────────────────────────────────────
+ Frosted glass theme — applied when the user enables the "Glass" toggle
+ in theme settings (writes `body.theme-frosted`). Layers a semi-translucent
+ tint over every panel/modal/sidebar/dropdown and blurs whatever is
+ behind it, so the UI reads like layered glass.
+ ───────────────────────────────────────────────────────────────────────── */
+body.theme-frosted #sidebar,
+body.theme-frosted .modal-content,
+body.theme-frosted .doclib-modal-content,
+body.theme-frosted .gallery-modal-content,
+body.theme-frosted .notes-pane,
+body.theme-frosted .research-pane,
+body.theme-frosted .calendar-pane,
+body.theme-frosted .cookbook-pane,
+body.theme-frosted .doc-pane,
+body.theme-frosted .doc-pane-floating,
+body.theme-frosted .cp-popover,
+body.theme-frosted .dropdown,
+body.theme-frosted .overflow-menu,
+body.theme-frosted .doc-tab-dropdown,
+body.theme-frosted .email-reader,
+body.theme-frosted .admin-card,
+body.theme-frosted .gallery-detail-menu,
+body.theme-frosted #theme-popup .modal-content {
+ /* Tinted base color (set separately so the shorthand can't swallow it);
+ much more transparent than before so what's behind actually shows
+ through. The blur catches whatever pixels are below. */
+ background-color: color-mix(in srgb, var(--panel, var(--bg)) 32%, transparent) !important;
+ background-image: linear-gradient(180deg,
+ color-mix(in srgb, var(--fg, #fff) 14%, transparent) 0%,
+ color-mix(in srgb, var(--fg, #fff) 4%, transparent) 26%,
+ transparent 55%) !important;
+ backdrop-filter: blur(24px) saturate(170%) !important;
+ -webkit-backdrop-filter: blur(24px) saturate(170%) !important;
+ border-color: color-mix(in srgb, var(--fg) 22%, transparent) !important;
+}
+/* The sidebar header + user bar paint their own opaque --sidebar-bg/--panel.
+ Under the frosted theme the sidebar is translucent glass, so those solid
+ bands stood out as a dark (near-black) strip over the title/avatar area.
+ Make them transparent so they blend into the frosted sidebar. */
+body.theme-frosted .sidebar-header,
+body.theme-frosted .sidebar-user-bar {
+ background: transparent !important;
+}
+/* Same problem on tool modals: the sticky .modal-header paints a solid
+ var(--panel) bar (so scrolled content doesn't bleed through). Over a
+ frosted-glass modal body that solid bar reads as a dark band across the
+ title area (Calendar, Tasks, Cookbook, Memory, Email…). Give the header
+ its own matching frosted glass — translucent tint + blur — so it stays
+ readable while scrolling but blends into the panel instead of a black band. */
+body.theme-frosted .modal-header {
+ /* Transparent — NOT another panel tint. The header sits on top of the
+ already-frosted modal body, so any added tint stacks and the band
+ reads darker than its surroundings. Keep just the blur (which doesn't
+ darken) so scrolled content stays legible under the sticky header. */
+ background-color: transparent !important;
+ background-image: none !important;
+ backdrop-filter: none !important;
+ -webkit-backdrop-filter: none !important;
+}
+/* Deep Research's header isn't sticky (content doesn't scroll under it) and
+ its pane carries extra frosting — saturate + a top gradient highlight +
+ the synapse glow. The generic header blur above adds its OWN glass layer,
+ which renders a different shade than the pane behind it. Drop the header's
+ blur so the pane's frosted background shows through the header uniformly. */
+body.theme-frosted .research-pane-header {
+ background-color: transparent !important;
+ background-image: none !important;
+ backdrop-filter: none !important;
+ -webkit-backdrop-filter: none !important;
+}
+body.theme-frosted .notes-mobile-grabber {
+ background-color: transparent !important;
+ background-image: none !important;
+ backdrop-filter: none !important;
+ -webkit-backdrop-filter: none !important;
+}
+/* Inner admin-cards inside frosted modals stack their transparency on top
+ of the modal's, which kills the see-through. Make the inner card nearly
+ fully transparent so the outer modal's blur dominates. */
+body.theme-frosted .modal-content .admin-card,
+body.theme-frosted .doclib-modal-content .admin-card,
+body.theme-frosted .gallery-modal-content .admin-card,
+body.theme-frosted #theme-popup .admin-card {
+ background-color: color-mix(in srgb, var(--panel, var(--bg)) 10%, transparent) !important;
+ background-image: none !important;
+ backdrop-filter: none !important;
+ -webkit-backdrop-filter: none !important;
+}
+/* Subtle inner light highlight + softer shadow on the heavier surfaces */
+body.theme-frosted .modal-content,
+body.theme-frosted .doclib-modal-content,
+body.theme-frosted .gallery-modal-content,
+body.theme-frosted .notes-pane,
+body.theme-frosted .research-pane,
+body.theme-frosted .calendar-pane,
+body.theme-frosted .cookbook-pane,
+body.theme-frosted .doc-pane,
+body.theme-frosted .doc-pane-floating {
+ box-shadow:
+ 0 14px 36px rgba(0, 0, 0, 0.5),
+ inset 0 1px 0 color-mix(in srgb, var(--fg, #fff) 14%, transparent),
+ inset 0 -1px 0 color-mix(in srgb, #000 14%, transparent) !important;
+}
+/* Backdrops should be MUCH less opaque so the blur effect reads through to
+ the actual page content behind the modal. */
+body.theme-frosted .modal {
+ background: color-mix(in srgb, #000 12%, transparent) !important;
+}
+
+/* Mobile only: lift the Dependencies "Server" dropdown's selected text up ~2px. */
+@media (max-width: 768px) {
+ #hwfit-deps-server { padding-bottom: 4px; line-height: 1; }
+}
+
+/* Emoji rendered as monochrome line icons (OpenMoji-black via /api/emoji proxy)
+ instead of colorful system glyphs — project rule: never colorful emoji. The
+ SVG is used as a mask filled with the current text color, so it tints to the
+ theme. Sized to sit on the text baseline. */
+.emoji {
+ height: 1.15em;
+ width: 1.15em;
+ vertical-align: -0.18em;
+ margin: 0 0.05em;
+ display: inline-block;
+ background-color: currentColor;
+ -webkit-mask: var(--em) center / contain no-repeat;
+ mask: var(--em) center / contain no-repeat;
+}
+
+.assistant-message a[href^="#event-"] .emoji,
+.ai-message a[href^="#event-"] .emoji,
+.message.assistant a[href^="#event-"] .emoji {
+ position: relative;
+ top: 2px;
+}
+
+.emoji[aria-label="🔔"] {
+ position: relative;
+ top: 3px;
+}
+
+
+/* RESEARCH SCROLL FORCE — guarantee the jobs list scrolls inside the bounded
+ pane (the pane is height-capped: 85vh desktop, 100dvh mobile). Each ancestor
+ needs min-height:0 for a nested flex scroll to engage. */
+#research-pane { min-height: 0; }
+/* The BODY is the single scroller — the list flows into it and the whole body
+ scrolls. (A nested list-scroller kept clipping instead of scrolling.) */
+#research-pane .research-pane-body {
+ flex: 1 1 auto !important; min-height: 0 !important;
+ display: flex !important; flex-direction: column !important;
+ overflow-y: auto !important; -webkit-overflow-scrolling: touch;
+}
+#research-pane .research-jobs-list {
+ flex: 0 0 auto !important; min-height: 0 !important;
+ overflow: visible !important; max-height: none !important;
+}
+/* The tabbed research panel replaces the old single body scroller above:
+ each result tab now owns its list viewport. */
+#research-pane .research-tab-panel {
+ flex: 1 1 auto !important;
+ min-height: 0 !important;
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
+}
+#research-pane .research-tab-panel:not([data-research-panel="research"]) .research-jobs-list {
+ flex: 1 1 auto !important;
+ min-height: 0 !important;
+ overflow-y: auto !important;
+}
+#research-pane .research-tab-panel[data-research-panel="research"] .research-new-job {
+ margin-bottom: 0;
+}
+
+/* RESEARCH FULLSCREEN MOBILE — the pane wasn't opening to full height on mobile,
+ so its content overflowed with nowhere to scroll. Pin it to the full viewport
+ (id selector + !important beats the inherited modal-content sizing). */
+@media (max-width: 768px) {
+ /* Bottom-sheet: 90dvh tall (a touch smaller than full-screen) anchored to the
+ bottom, so there's a small gap at the top and it reads as a sheet. The body
+ still scrolls within the bounded height. */
+ #research-overlay { align-items: flex-end !important; justify-content: stretch !important; }
+ #research-pane {
+ height: 90dvh !important;
+ max-height: 90dvh !important;
+ width: 100vw !important;
+ max-width: 100vw !important;
+ border-radius: 14px 14px 0 0 !important;
+ }
+ /* Bottom breathing room so the expanded Settings + Start button clear the
+ browser's bottom toolbar / safe area and can always be scrolled into view
+ (otherwise the form fits the viewport and there's no room to reach them). */
+ #research-pane .research-pane-body {
+ padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 40px) !important;
+ touch-action: pan-y;
+ overscroll-behavior: contain;
+ }
+}
+
+/* Keep the primary action transition consistent with the shared toolbar. */
+.research-start-btn { transition: opacity 0.16s ease; }
+
+/* Right-side group of the section header: clear button (past), status dot, chevron. */
+.research-section-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
+/* "Clear all" in the Past header — styled like the cookbook-running clear btn. */
+.research-section-clear {
+ font-size: 10px; padding: 0 8px; height: 22px;
+ border-radius: 6px; border: 1px solid var(--border);
+ color: color-mix(in srgb, var(--fg) 45%, transparent);
+ background: none; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
+ font-family: inherit; white-space: nowrap; transition: all 0.15s;
+ position: relative; top: -2px;
+}
+.research-section-clear:hover { color: var(--red); border-color: var(--red); background: color-mix(in srgb, var(--red) 8%, transparent); }
+
+/* Visual Report button tinted with the research type colour (--cat-color),
+ falling back to the accent for "standard" (uncategorized) research. */
+.research-job-action.research-job-action-report {
+ color: var(--cat-color, var(--accent, var(--red)));
+ border-color: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 45%, var(--border));
+}
+
+.research-start-btn svg,
+.research-add-plus { display: inline-block; }
+
+/* Compare's selector uses the shared Start button markup, but its accent
+ background needs a readable dark foreground and a lighter hover weight. */
+#compare-model-overlay .research-start-btn {
+ color: #000;
+}
+#compare-model-overlay .research-start-btn:hover {
+ color: var(--fg);
+ font-weight: 400;
+ opacity: 1;
+}
+
+/* Footer hint under Past research linking to the Library's Research tab. */
+.research-library-hint {
+ width: 100%; flex-basis: 100%; margin: -2px 0 0; line-height: 1.2;
+ font-size: calc(1em - 2px); opacity: 0.55;
+}
+.research-library-link {
+ background: none; border: none; padding: 0; cursor: pointer;
+ font: inherit; color: var(--accent, var(--red)); text-decoration: underline;
+}
+
+
+@media (max-width: 768px) {
+ /* Mobile: the "Delete" label is too wide in Deep Research job rows; keep the
+ familiar trash action but make it icon-only. */
+ .research-job-action[data-action="delete"] {
+ width: 28px;
+ min-width: 28px;
+ padding-left: 0;
+ padding-right: 0;
+ gap: 0;
+ justify-content: center;
+ font-size: 0;
+ }
+ .research-job-action[data-action="delete"] svg {
+ width: 13px;
+ height: 13px;
+ margin: 0;
+ flex-shrink: 0;
+ }
+}
+
+/* Standard research gets a stable visual variant instead of inheriting the
+ global warm accent. Keep typography unchanged; vary only the card tint,
+ border, badge, progress bar, and report hero accent. */
+.research-job-card.research-standard {
+ --cat-color: var(--research-accent);
+ background: color-mix(in srgb, var(--research-accent) 4%, var(--bg));
+ border-color: color-mix(in srgb, var(--research-accent) 28%, var(--border));
+ border-left-color: var(--research-accent);
+}
+.research-job-card.research-standard .research-cat-badge.research-cat-standard {
+ color: var(--research-accent);
+}
+
+/* Failed research (0 sources) — red flag + actionable note. */
+.research-job-card.research-job-failed { border-color: color-mix(in srgb, #f44336 40%, var(--border)); }
+.research-cat-badge.research-cat-failed { color: #f44336; display: inline-flex; align-items: center; gap: 3px; }
+.research-cat-badge.research-cat-failed svg { width: 10px; height: 10px; }
+.research-job-failnote { position: relative; top: 2px; font-size: 10px; color: #f44336; opacity: 0.9; margin: 2px 0 6px; line-height: 1.35; }
+
+/* Shared popup-menu row feedback. Many small menus use their own item class;
+ keep the hover light and consistent instead of giving one action a special
+ glow. */
+:is(
+ .overflow-menu-item,
+ .dropdown-item,
+ .dropdown-item-compact,
+ .export-dropdown-item,
+ .sort-dropdown-item,
+ .note-reminder-menu-item,
+ .research-run-mode-popover .research-run-mode-row,
+ .ge-fx-menu-item
+) {
+ transition:
+ background-color 0.14s ease,
+ color 0.14s ease,
+ opacity 0.14s ease,
+ transform 0.14s ease;
+}
+:is(
+ .overflow-menu-item,
+ .dropdown-item,
+ .dropdown-item-compact,
+ .export-dropdown-item,
+ .sort-dropdown-item,
+ .note-reminder-menu-item,
+ .research-run-mode-popover .research-run-mode-row,
+ .ge-fx-menu-item
+):hover:not(:disabled) {
+ opacity: 1;
+ color: var(--fg);
+ background-color: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
+ transform: translateX(1px);
+}
+
+/* Slash command autocomplete popup, anchored to the message composer */
+.slash-autocomplete-popup {
+ position: fixed;
+ z-index: 9000;
+ background: var(--panel, var(--bg));
+ border: 1px solid var(--border, rgba(255,255,255,0.08));
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.35);
+ font-size: 13px;
+ color: var(--fg, #e6e6e6);
+ overflow-y: auto;
+ padding: 4px 0;
+ display: none;
+}
+.slash-ac-cat {
+ font-size: 10px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--fg-muted, #888);
+ padding: 6px 10px 2px;
+ opacity: 0.7;
+}
+.slash-ac-row {
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+ padding: 5px 10px;
+ cursor: pointer;
+ line-height: 1.3;
+ white-space: nowrap;
+ overflow: hidden;
+}
+.slash-ac-row:hover { background-color: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent); }
+.slash-ac-row-sel { background-color: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); }
+.slash-ac-token {
+ font-family: 'Fira Code', ui-monospace, monospace;
+ color: var(--accent, var(--red));
+ font-weight: 600;
+ flex-shrink: 0;
+}
+.slash-ac-help {
+ color: var(--fg);
+ opacity: 0.85;
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.slash-ac-usage {
+ color: var(--fg-muted, #888);
+ font-family: 'Fira Code', ui-monospace, monospace;
+ font-size: 11px;
+ opacity: 0.55;
+ flex-shrink: 0;
+}
+.slash-ac-empty {
+ padding: 10px;
+ color: var(--fg-muted, #888);
+ font-style: italic;
+}
+.slash-ac-empty code {
+ font-family: 'Fira Code', ui-monospace, monospace;
+ color: var(--accent, var(--red));
+}
+
+/* ══ iOS focus-zoom fix — Safari touch devices only; desktop sizes untouched ══
+ 16px is the threshold below which iOS Safari auto-zooms on focus.
+ Selects and date/time inputs are excluded on purpose — they open native
+ pickers and never zoom. */
+@supports (-webkit-touch-callout: none) {
+@media (hover: none) and (pointer: coarse) {
+
+ /* 1 ── Catch-all: every text-entry control NOT pinned with its own
+ !important. !important here beats any non-important rule regardless of
+ specificity, so this clears the long tail (settings, admin, memory,
+ notes, calendar, email, gallery, tasks, model picker, etc.). */
+ input[type="text"],
+ input[type="search"],
+ input[type="email"],
+ input[type="url"],
+ input[type="tel"],
+ input[type="password"],
+ input[type="number"],
+ input:not([type]),
+ textarea {
+ font-size: 16px !important;
+ }
+
+ /* 2 ── Fields that pin their own !important at specificity our catch-all
+ can't beat. Each is matched at equal-or-higher specificity and, being
+ later in the file, wins the tie. */
+ #message { font-size: 16px !important; } /* chat composer (was 13px !important) */
+ .cookbook-dl-repo,
+ .hwfit-search { font-size: 16px !important; } /* cookbook repo path + hardware search */
+ .ge-topbar input { font-size: 16px !important; } /* image-editor topbar input */
+ .ge-transform-field > input.ge-transform-popup-input { /* image-editor transform values */
+ font-size: 16px !important;
+ }
+}
+}
+
+@supports (-webkit-touch-callout: none) {
+@media (hover: none) and (pointer: coarse) {
+ /* Only the sub-16px tiers need bumping; large lands ABOVE 16 so it
+ stays zoom-safe AND visibly larger than medium (otherwise L collapses
+ onto M on touch). All three editor layers move together so the
+ highlight/line-number overlay stays metrically aligned with the textarea. */
+ .doc-font-m .doc-editor-textarea, .doc-font-m .doc-editor-highlight, .doc-font-m .doc-line-numbers {
+ font-size: 16px !important; /* was 13px */
+ }
+ .doc-font-l .doc-editor-textarea, .doc-font-l .doc-editor-highlight, .doc-font-l .doc-line-numbers {
+ font-size: 18px !important; /* was 15px — keep L > M */
+ }
+ /* Email compose rich-body. Medium (15px) zooms, so bump it; large (17px)
+ is already ≥16px and never zoomed — leave it so we don't shrink it. */
+ .doc-email-richbody.doc-font-m { font-size: 16px !important; }
+}
+}
+
+/* GitHub Copilot device-flow connect block (model endpoints → API) */
+.adm-copilot-connect {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+.adm-copilot-connect #adm-copilotStatus { flex-basis: 100%; margin-top: 0; }
+.adm-copilot-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 10px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+}
+.adm-copilot-wait {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+}
+.adm-copilot-coderow {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.adm-copilot-code-label {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: color-mix(in srgb, var(--fg) 45%, transparent);
+}
+.adm-copilot-code {
+ font-family: var(--mono, ui-monospace, monospace);
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ padding: 4px 10px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--fg);
+ user-select: all;
+}
+.adm-copilot-auth {
+ text-align: center;
+ text-decoration: none;
+ padding: 7px 12px;
+ font-size: 12px;
+}
+.adm-copilot-hint {
+ font-size: 11px;
+ line-height: 1.4;
+ color: color-mix(in srgb, var(--fg) 45%, transparent);
+}
+/* ChatGPT Subscription per-account usage card (Settings → Added Models) */
+.adm-chatgpt-controls {
+ display: flex;
+ justify-content: flex-end;
+ gap: 6px;
+ align-items: center;
+ margin: 4px 0 2px auto;
+}
+.adm-chatgpt-usage-chevron {
+ font-size: 10px;
+ line-height: 1;
+ display: inline-block;
+ margin-left: 2px;
+}
+.adm-chatgpt-usage {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 8px 10px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 11px;
+}
+.adm-chatgpt-usage-plan { display: flex; gap: 6px; align-items: center; }
+.adm-chatgpt-usage-limit { display: flex; flex-direction: column; gap: 6px; }
+.adm-chatgpt-usage-limit-title {
+ font-size: 10px;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: color-mix(in srgb, var(--fg) 55%, transparent);
+}
+.adm-chatgpt-usage-model {
+ text-transform: none;
+ letter-spacing: 0;
+ font-family: var(--mono, ui-monospace, monospace);
+ opacity: 0.8;
+}
+.adm-chatgpt-usage-row { display: flex; flex-direction: column; gap: 3px; }
+.adm-chatgpt-usage-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ gap: 8px;
+}
+.adm-chatgpt-usage-name {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+}
+.adm-chatgpt-usage-remaining { font-weight: 600; }
+.adm-chatgpt-usage-bar {
+ position: relative;
+ height: 6px;
+ border-radius: 3px;
+ overflow: hidden;
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+}
+.adm-chatgpt-usage-fill {
+ display: block;
+ height: 100%;
+ border-radius: 3px;
+ background: var(--accent);
+ transition: width 0.25s ease;
+}
+.adm-chatgpt-usage-warn .adm-chatgpt-usage-fill { background: #d99a2b; }
+.adm-chatgpt-usage-critical .adm-chatgpt-usage-fill { background: #d9534f; }
+.adm-chatgpt-usage-unknown .adm-chatgpt-usage-fill { background: color-mix(in srgb, var(--fg) 25%, transparent); }
+.adm-chatgpt-usage-meta {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 10px;
+ color: color-mix(in srgb, var(--fg) 50%, transparent);
+}
+.adm-chatgpt-usage-status {
+ font-size: 11px;
+ color: color-mix(in srgb, var(--fg) 60%, transparent);
+}
+.adm-chatgpt-usage-blocked { color: #d9534f; }
+.adm-chatgpt-usage-actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 2px; }
+.adm-chatgpt-reconnect-status { font-size: 11px; }
+/* Cookbook serve panel: Launch + ^ split button pair */
+.hwfit-serve-launch-group {
+ display: inline-flex;
+ align-items: stretch;
+ vertical-align: middle;
+}
+.hwfit-serve-launch-group .hwfit-serve-schedule-arrow {
+ border-top-left-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
+ border-left: 1px solid var(--border) !important;
+ padding: 0 8px !important;
+ min-width: 26px;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Mobile: drop the inline icons on Launch + Cancel in the serve panel so
+ the buttons are text-only and don't wrap on narrow screens. Icons stay
+ on desktop where horizontal space isn't tight. */
+@media (max-width: 600px) {
+ .hwfit-serve-launch > svg,
+ .hwfit-serve-cancel > svg {
+ display: none !important;
+ }
+}
+
+/* Schedule form — mounted inside the cookbook serve panel. Uses the
+ theme tokens (--bg, --panel, --border, --accent, --red) so it
+ matches the rest of the cookbook chrome instead of inline whites. */
+.hwfit-schedule-form {
+ margin: 10px 0 4px;
+ padding: 12px 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.hwfit-schedule-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 13px;
+ font-weight: 600;
+ opacity: 0.95;
+ flex-wrap: wrap;
+}
+.hwfit-schedule-title svg { opacity: 0.7; flex-shrink: 0; }
+.hwfit-schedule-title-spacer { flex: 1; min-width: 8px; }
+.hwfit-schedule-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 12px;
+}
+.hwfit-schedule-field {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ font-size: 11px;
+ opacity: 0.75;
+}
+.hwfit-schedule-label {
+ font-size: 11px;
+ opacity: 0.75;
+}
+.hwfit-sched-days {
+ display: inline-flex;
+ flex-wrap: wrap;
+ gap: 5px;
+}
+.hwfit-schedule-actions-inline {
+ display: inline-flex;
+ align-items: flex-end;
+ gap: 6px;
+ align-self: flex-end;
+ padding-bottom: 1px;
+}
+.hwfit-sched-day-chip {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ border: 1px solid var(--border);
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ font-size: 10px;
+ line-height: 1;
+ padding: 0;
+ cursor: pointer;
+ transition: background 0.12s, color 0.12s, border-color 0.12s;
+}
+.hwfit-sched-day-chip:hover { border-color: var(--accent, var(--red)); }
+.hwfit-sched-day-chip.is-on {
+ background: var(--accent, var(--red));
+ color: var(--panel, #fff);
+ border-color: var(--accent, var(--red));
+}
+.hwfit-schedule-actions-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.hwfit-schedule-actions-spacer { flex: 1; }
+
+/* Days row hosts the day-chip strip on the left and the Cancel / Save
+ action buttons on the right. The spacer between them pushes the
+ actions to the right edge. */
+.hwfit-schedule-days-row {
+ align-items: center;
+}
+/* Cancel + Save sit on the right side of the days row with matching
+ icon-plus-label chrome. */
+.hwfit-sched-cancel,
+.hwfit-sched-save {
+ display: inline-flex !important;
+ align-items: center;
+}
+.hwfit-schedule-mirror-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ opacity: 0.9;
+ cursor: pointer;
+ user-select: none;
+}
+.hwfit-sched-err {
+ font-size: 12px;
+ color: var(--red, #ff6b6b);
+ display: none;
+}
+@media (max-width: 600px) {
+ .hwfit-schedule-row { gap: 8px; }
+ .hwfit-sched-day-chip { width: 36px; height: 36px; font-size: 11px; }
+}
+
+/* ── ask_user: multiple-choice question card ─────────────────────────────
+ The agent posed a question and ended its turn. The user clicks an option
+ or types a free-text "Other" answer. */
+.ask-user-card {
+ /* Left-align like an assistant message (.msg-ai), not centered. */
+ align-self: flex-start;
+ margin: 10px auto 10px 8px;
+ width: 85%;
+ max-width: 85%;
+ box-sizing: border-box;
+ font-size: 11px;
+ line-height: 1.45;
+ padding: 12px 16px 14px;
+ border: 1px solid var(--bubble-border, var(--border));
+ border-radius: 18px 18px 18px 0;
+ background: var(--ai-bubble-bg, var(--panel));
+ overflow: hidden;
+ position: relative;
+ isolation: isolate;
+}
+.ask-user-card-attached {
+ margin-top: 0;
+ margin-left: 8px;
+ width: 85%;
+ max-width: 85%;
+}
+.ask-user-card-attached::before {
+ content: '';
+ position: absolute;
+ left: 20px;
+ top: -18px;
+ width: 2px;
+ height: 18px;
+ border-radius: 1px;
+ background: color-mix(in srgb, var(--red) 18%, transparent);
+ pointer-events: none;
+ z-index: -1;
+}
+/* Focused only programmatically (tabIndex -1) to move SR/keyboard position; no
+ visible outline on the whole card box. */
+.ask-user-card:focus { outline: none; }
+.ask-user-head {
+ display: none;
+}
+.ask-user-close {
+ display: none !important;
+}
+.ask-user-question {
+ margin: 0 0 10px;
+ font-size: 11px !important;
+ font-weight: 500;
+ line-height: 1.45;
+ color: var(--fg);
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ white-space: normal;
+}
+.ask-user-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.ask-user-option {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 8px;
+ row-gap: 2px;
+ width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+ height: auto !important;
+ /* Match the height of the free-text input below (.styled-prompt-input). */
+ min-height: 39px;
+ text-align: left;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--fg);
+ font-size: 11px !important;
+ line-height: 1.35;
+ cursor: pointer;
+ transition: background 0.12s ease, border-color 0.12s ease;
+}
+.ask-user-option:hover:not(:disabled) {
+ border-color: var(--accent, var(--red));
+ background: color-mix(in srgb, var(--accent, var(--red)) 10%, var(--bg));
+}
+.ask-user-option:disabled { cursor: default; }
+.ask-user-card.ask-user-answered {
+ opacity: 0.92;
+}
+.ask-user-card.ask-user-answered .ask-user-question {
+ margin-bottom: 8px;
+}
+.ask-user-option-label,
+.ask-user-option-desc {
+ display: block;
+ flex: 1 1 0;
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ white-space: normal;
+}
+.ask-user-option-label {
+ font-size: 11px !important;
+ font-weight: 500;
+ line-height: 1.35;
+ color: var(--accent, var(--red));
+}
+.ask-user-option-desc {
+ flex-basis: 100%;
+ width: 100%;
+ opacity: 0.65;
+ font-size: 10px !important;
+ line-height: 1.35;
+}
+/* Free-text "Other" row: input + send, on one line. */
+.ask-user-other {
+ display: flex;
+ gap: 8px;
+ margin-top: 10px;
+ min-width: 0;
+}
+/* Reuses .styled-prompt-input; override its full-width + top margin so it
+ sits inline in the flex row next to the send button. */
+.ask-user-other-input {
+ flex: 1;
+ min-width: 0;
+ width: auto;
+ margin-top: 0;
+ font-size: 11px !important;
+ line-height: 1.35;
+}
+/* Reuses .confirm-btn .confirm-btn-primary; flex-row deltas + height match to
+ the input beside it (.confirm-btn won't stretch on its own). */
+.ask-user-other-send {
+ flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ white-space: nowrap;
+ width: 32px;
+ min-width: 32px;
+ height: 32px !important;
+ min-height: 32px;
+ font-size: 11px !important;
+ line-height: 1.35;
+ padding: 0;
+ border: none;
+ border-radius: 8px;
+ background: var(--send-btn-bg, var(--red));
+ color: #fff;
+}
+.ask-user-other-send svg { display: block; flex-shrink: 0; }
+.ask-user-other-send:hover:not(:disabled) {
+ background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white));
+}
+.ask-user-other-send:disabled { opacity: 0.5; cursor: default; }
+.ask-user-answer {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 7px 9px;
+ border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 24%, var(--border));
+ border-radius: 8px;
+ background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--bg));
+ color: var(--fg);
+ font-size: 11px !important;
+ line-height: 1.35;
+}
+.ask-user-answer-label {
+ flex: 0 0 auto;
+ color: var(--accent, var(--red));
+ font-weight: 600;
+}
+.ask-user-answer-value {
+ min-width: 0;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+@media (max-width: 640px) {
+ .ask-user-card {
+ width: calc(100% - 16px);
+ max-width: calc(100% - 16px);
+ margin-left: 8px;
+ margin-right: 8px;
+ padding: 10px 11px 12px;
+ border-radius: 10px;
+ }
+ .ask-user-card-attached {
+ margin-left: 0;
+ margin-right: 0;
+ width: 100%;
+ max-width: 100%;
+ }
+ .ask-user-question {
+ font-size: 11px !important;
+ line-height: 1.45;
+ }
+ .ask-user-option {
+ padding: 7px 8px;
+ font-size: 11px !important;
+ gap: 6px;
+ }
+ .ask-user-option-label {
+ font-size: 11px !important;
+ }
+ .ask-user-option-desc {
+ font-size: 10px !important;
+ }
+ .ask-user-other {
+ flex-wrap: wrap;
+ }
+ .ask-user-other-send {
+ width: 32px;
+ min-width: 32px;
+ height: 32px !important;
+ min-height: 32px;
+ padding: 0;
+ }
+}
+
+/* ── Workspace picker ───────────────────────────────────────────── */
+/* Layout (width/flex column/max-height) inherited from base .modal-content. */
+/* Editable path/address bar: reuses .styled-prompt-input for border/bg/radius/
+ focus ring (set in the element's class list). Overrides only the deltas:
+ mono font, and full-bleed via flex stretch with no horizontal margin (the
+ modal-content's 10px padding is the gutter) instead of the base width:100%,
+ which overflowed against the overflow:auto scrollbar. */
+.workspace-cur {
+ align-self: stretch;
+ width: auto;
+ min-width: 0;
+ margin: 4px 0 8px;
+ font-family: var(--mono, monospace);
+ font-size: 12px;
+}
+/* flex/overflow inherited from base .modal-body; only the padding differs. */
+.workspace-body { padding: 6px 0; }
+.workspace-row {
+ padding: 7px 18px;
+ cursor: pointer;
+ font-size: 13px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.workspace-row > span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.workspace-row-icon { flex-shrink: 0; opacity: 0.75; }
+.workspace-row:hover {
+ background: color-mix(in srgb, var(--border) 20%, transparent);
+}
+.workspace-up { opacity: 0.7; }
+.workspace-empty { padding: 14px 18px; opacity: 0.5; font-size: 13px; }
+.workspace-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ padding: 10px 18px;
+ border-top: 1px solid var(--border);
+}
+.workspace-footer > .confirm-btn { position: relative; top: 4px; }
+.workspace-footer > .confirm-btn-primary { color: var(--bg); }
+.workspace-footer > #workspace-use { top: 3px; font-size: calc(0.85rem + 2px); font-weight: 700; }
+.workspace-footer > #workspace-use:hover { color: var(--fg); }
+.workspace-footer > .confirm-btn > svg { position: relative; left: -2px; top: 2px; }
+.workspace-footer > #workspace-use > svg { top: 3px; color: inherit; }
+.workspace-note { margin: 0 0 8px; font-size: 11px; line-height: 1.4; }
+
+.settings-system-logs-tabs {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ margin: 0 0 10px;
+ border-bottom: 1px solid var(--border);
+}
+.settings-system-logs-tab {
+ border: 0;
+ border-bottom: 2px solid transparent;
+ background: transparent;
+ color: var(--fg);
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ padding: 6px 8px 7px;
+ opacity: 0.62;
+}
+.settings-system-logs-tab.active {
+ border-bottom-color: var(--accent, var(--red));
+ color: var(--accent, var(--red));
+}
+.settings-system-logs-col {
+ gap: 10px;
+}
+.settings-system-notification-logs-col {
+ gap: 10px;
+}
+#system-log-terminal-panel[hidden],
+#system-log-notifications-panel[hidden] {
+ display: none !important;
+}
+.settings-system-logs-controls {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+.settings-system-logs-search {
+ padding: 6px 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ flex: 1;
+ min-width: 140px;
+}
+.settings-system-logs-select {
+ padding: 5px 8px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--fg);
+ font-family: inherit;
+ font-size: 11px;
+ min-width: 90px;
+}
+#log-refresh-btn {
+ height: 27px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 8px;
+}
+.settings-system-logs-autopoll-container {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11px;
+ user-select: none;
+ margin-left: auto;
+}
+#log-console-container {
+ background: #13151a;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 12px;
+ font-family: Consolas, 'Fira Code', Monaco, 'Courier New', monospace;
+ font-size: 11px;
+ height: 280px;
+ max-height: 280px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ color: #d1d4e0;
+ box-shadow: inset 0 2px 8px rgba(0,0,0,0.5);
+}
+.settings-system-logs-placeholder {
+ color: var(--color-text-dim, #7f8c8d);
+ font-style: italic;
+ font-family: inherit;
+}
+.log-line {
+ margin-bottom: 3px;
+ line-height: 1.4;
+ font-size: 11px;
+ font-family: inherit;
+}
+.log-line-info {
+ color: var(--green, #50fa7b);
+}
+.log-line-warning {
+ color: var(--warn, #f0ad4e);
+}
+.log-line-error {
+ color: var(--red, #e06c75);
+}
+.log-line-debug {
+ color: var(--color-muted, #888);
+}
+.log-line-default {
+ color: var(--fg, #9cdef2);
+}
+#notification-log-container {
+ height: 280px;
+ max-height: 280px;
+ overflow-y: auto;
+ padding: 2px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--bg) 72%, #13151a 28%);
+}
+.notification-log-entry {
+ padding: 9px 10px;
+ border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
+}
+.notification-log-entry:last-child { border-bottom: 0; }
+.notification-log-heading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+.notification-log-task {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+}
+.notification-log-status {
+ flex: 0 0 auto;
+ padding: 2px 5px;
+ border: 1px solid currentColor;
+ border-radius: 4px;
+ font-size: 9px;
+ line-height: 1;
+ text-transform: uppercase;
+ opacity: 0.82;
+}
+.notification-log-success .notification-log-status { color: var(--green, #50fa7b); }
+.notification-log-error .notification-log-status { color: var(--red, #e06c75); }
+.notification-log-meta {
+ margin-top: 3px;
+ color: var(--color-text-dim, #7f8c8d);
+ font-size: 10px;
+ overflow-wrap: anywhere;
+}
+.notification-log-body {
+ position: relative;
+ margin-top: 6px;
+ padding: 7px 8px;
+ border-left: 2px solid color-mix(in srgb, var(--fg) 28%, transparent);
+ color: color-mix(in srgb, var(--fg) 82%, transparent);
+ font-family: Consolas, 'Fira Code', Monaco, 'Courier New', monospace;
+ font-size: 11px;
+ line-height: 1.45;
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+.notification-log-body-text { display: block; padding-right: 24px; }
+.notification-log-copy {
+ position: absolute; top: 5px; right: 5px; width: 22px; height: 22px;
+ display: inline-flex; align-items: center; justify-content: center;
+ border: 0; border-radius: 4px; background: transparent; color: currentColor;
+ opacity: .45; cursor: pointer; padding: 0;
+}
+.notification-log-copy:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 8%, transparent); }
+.notification-log-copy.copied { opacity: 1; color: var(--green, #50fa7b); }
+.notification-log-error .notification-log-body {
+ border-left-color: var(--red, #e06c75);
+ color: color-mix(in srgb, var(--red, #e06c75) 88%, var(--fg));
+}
+
+/* Compare uses one full-width response card on phones. Every pane remains
+ mounted so hidden models can keep streaming while the tab strip scales to
+ larger comparisons without stacking multiple chats down the page. */
+@media (max-width: 768px) {
+ .compare-add-flap { display: none !important; }
+ .compare-mobile-tabs {
+ display: flex;
+ flex: 0 0 auto;
+ align-items: stretch;
+ gap: 5px;
+ min-width: 0;
+ padding: 0 8px 6px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ scroll-snap-type: x proximity;
+ overscroll-behavior-x: contain;
+ -webkit-overflow-scrolling: touch;
+ }
+ .compare-mobile-tabs::-webkit-scrollbar { display: none; }
+ .compare-mobile-tab {
+ position: relative;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 6px;
+ flex: 0 0 clamp(108px, 38vw, 156px);
+ min-width: 0;
+ height: 34px;
+ box-sizing: border-box;
+ padding: 0 9px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ color: color-mix(in srgb, var(--fg) 62%, transparent);
+ font-size: 10px;
+ line-height: 1;
+ font-family: inherit;
+ text-align: left;
+ scroll-snap-align: start;
+ transition: border-color .15s ease, background .15s ease, color .15s ease;
+ }
+ .compare-mobile-add {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ height: 34px;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 3%, var(--bg));
+ color: color-mix(in srgb, var(--fg) 70%, transparent);
+ font-size: 10px;
+ line-height: 1;
+ font-family: inherit;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+ .compare-mobile-add > span:first-child {
+ color: var(--accent, var(--red));
+ font-size: 15px;
+ font-weight: 500;
+ line-height: 1;
+ }
+ .compare-mobile-add:disabled { opacity: .35; cursor: default; }
+ .compare-mobile-add:not(:disabled):active {
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--bg));
+ }
+ .compare-mobile-tab.active {
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 70%, var(--border));
+ background: color-mix(in srgb, var(--accent, var(--red)) 11%, var(--bg));
+ color: var(--fg);
+ }
+ .compare-mobile-tab-slot {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 17px;
+ height: 17px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ color: color-mix(in srgb, var(--fg) 68%, transparent);
+ font-size: 9px;
+ font-weight: 700;
+ }
+ .compare-mobile-tab.active .compare-mobile-tab-slot {
+ background: var(--accent, var(--red));
+ color: var(--bg);
+ }
+ .compare-mobile-tab-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .compare-mobile-tab-state {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: color-mix(in srgb, var(--fg) 22%, transparent);
+ }
+ .compare-mobile-tab.is-streaming .compare-mobile-tab-state {
+ background: var(--red, #e06c75);
+ box-shadow: 0 0 0 0 color-mix(in srgb, var(--red, #e06c75) 65%, transparent);
+ animation: compare-mobile-tab-pulse 1s ease-in-out infinite !important;
+ will-change: opacity, transform, box-shadow;
+ }
+ .compare-mobile-tab.is-awaiting-input .compare-mobile-tab-state { background: var(--accent, var(--red)); }
+ .compare-mobile-tab.is-done .compare-mobile-tab-state {
+ width: auto;
+ height: auto;
+ border-radius: 0;
+ background: none;
+ color: var(--color-success, #4caf50);
+ font-size: 13px;
+ font-weight: 800;
+ line-height: 1;
+ }
+ .compare-mobile-tab.is-done .compare-mobile-tab-state::before { content: '✓'; }
+ .compare-mobile-tab.is-failed .compare-mobile-tab-state { background: var(--color-error, #e55); }
+ @keyframes compare-mobile-tab-pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 color-mix(in srgb, var(--red, #e06c75) 65%, transparent); }
+ 50% { opacity: .45; transform: scale(.72); box-shadow: 0 0 0 5px color-mix(in srgb, var(--red, #e06c75) 0%, transparent); }
+ }
+ .compare-grid[data-cols],
+ .compare-grid.sequential-layout {
+ display: block !important;
+ grid-template-columns: none !important;
+ flex: 1 1 0;
+ min-height: 0;
+ padding: 0 8px 6px;
+ overflow: hidden;
+ }
+ .compare-grid > .compare-pane,
+ .compare-grid.sequential-layout > .compare-pane {
+ display: none;
+ width: 100%;
+ height: 100%;
+ min-height: 0;
+ margin-left: 0 !important;
+ box-sizing: border-box;
+ }
+ .compare-grid > .compare-pane.compare-pane-mobile-active,
+ .compare-grid.sequential-layout > .compare-pane.compare-pane-mobile-active {
+ display: flex;
+ animation: compare-mobile-card-in .16s ease-out;
+ }
+ .compare-grid.sequential-layout .compare-pane .pane-header {
+ clip-path: none;
+ padding-left: 10px;
+ }
+ @keyframes compare-mobile-card-in {
+ from { opacity: .55; transform: translateY(2px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ .compare-probe-card { width: min(460px, calc(100% - 16px)); padding: 12px; }
+ .compare-probe-list { grid-template-columns: minmax(0, 1fr); }
+ .compare-probe-row:not(.fail) .compare-probe-spinner:not(.ok):not(.fail) { transform: translateX(-2px); }
+ .compare-probe-detail { flex-wrap: wrap; }
+ .compare-probe-detail-message { flex-basis: 100%; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .compare-mobile-tab.is-streaming .compare-mobile-tab-state,
+ .compare-grid > .compare-pane.compare-pane-mobile-active { animation: none; }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/* Mobile tool drawers: leave a visible strip of the workspace above tall
+ sheets so they read as draggable drawers rather than full-screen routes.
+ Content-sized windows remain shorter; only tall content is capped at 95dvh.
+ Keep the theme popup's intentionally shorter 65vh treatment unchanged. */
+@media (max-width: 768px) {
+ .modal:not(#theme-modal):not(#styled-confirm-overlay) > .modal-content,
+ .modal:not(#theme-modal):not(#styled-confirm-overlay) > .memory-modal-content,
+ .modal:not(#theme-modal):not(#styled-confirm-overlay) > .settings-modal-content {
+ height: auto !important;
+ max-height: 95dvh !important;
+ align-self: flex-end;
+ margin-bottom: calc(-1 * env(safe-area-inset-bottom, 0px)) !important;
+ }
+
+ /* Tabbed tools need a definite flex height when their active panel changes;
+ otherwise the content-sized drawer can collapse and hide the tab strip. */
+ #cookbook-modal .modal-content,
+ #tasks-modal .tasks-modal-content,
+ #memory-modal .memory-modal-content,
+ #settings-modal .settings-modal-content,
+ #email-lib-modal .modal-content,
+ .email-window-modal .modal-content,
+ #doclib-modal .doclib-modal-content,
+ #library-modal .doclib-modal-content,
+ #gallery-modal .gallery-modal-content,
+ #research-overlay .research-pane,
+ #compare-model-overlay .modal-content,
+ #custom-preset-modal .preset-modal-content {
+ height: 95dvh !important;
+ min-height: 95dvh !important;
+ max-height: 95dvh !important;
+ }
+
+ /* Explicit fullscreen states still behave as bottom drawers on phones. */
+ #email-lib-modal.email-lib-fullscreen .modal-content,
+ #doclib-modal.doclib-fullscreen:not(.modal-right-docked) .doclib-modal-content {
+ top: auto !important;
+ bottom: 0 !important;
+ height: 95dvh !important;
+ max-height: 95dvh !important;
+ border-radius: 14px 14px 0 0 !important;
+ }
+
+ .cal-modal-container {
+ height: auto !important;
+ max-height: 95dvh !important;
+ }
+
+ body.doc-view .doc-editor-pane,
+ body.notes-view .notes-pane {
+ inset: auto 0 0 !important;
+ height: 95dvh !important;
+ max-height: 95dvh !important;
+ }
+}
+
+
+
+/* Shared bulk-selection trigger. Keep every Select/Cancel control on the same
+ circle-dot and X language even when a feature rewrites the button label. */
+:is(
+ #memory-select-btn,
+ #skills-select-btn,
+ #notes-select-btn,
+ #tasks-select-btn,
+ #doclib-chats-select-btn,
+ #doclib-arc-select-btn,
+ #doclib-research-select-btn,
+ #doclib-select-btn,
+ #gallery-select-btn,
+ #gallery-albums-select-btn,
+ #gallery-editor-drafts-select,
+ #email-lib-select-btn,
+ #archive-select-btn,
+ #lib-select-btn,
+ #hwfit-cache-select
+) {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+}
+
+:is(
+ #memory-select-btn,
+ #skills-select-btn,
+ #notes-select-btn,
+ #tasks-select-btn,
+ #doclib-chats-select-btn,
+ #doclib-arc-select-btn,
+ #doclib-research-select-btn,
+ #doclib-select-btn,
+ #gallery-select-btn,
+ #gallery-albums-select-btn,
+ #gallery-editor-drafts-select,
+ #email-lib-select-btn,
+ #archive-select-btn,
+ #lib-select-btn,
+ #hwfit-cache-select
+)::before {
+ content: "";
+ width: 11px;
+ height: 11px;
+ margin-right: 3px;
+ flex: 0 0 11px;
+ background-color: currentColor;
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='12' cy='11' r='3' fill='black'/%3E%3C/svg%3E") center / contain no-repeat;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='10' fill='none' stroke='black' stroke-width='2'/%3E%3Ccircle cx='12' cy='11' r='3' fill='black'/%3E%3C/svg%3E") center / contain no-repeat;
+}
+
+:is(
+ #memory-select-btn,
+ #skills-select-btn,
+ #notes-select-btn,
+ #tasks-select-btn,
+ #doclib-chats-select-btn,
+ #doclib-arc-select-btn,
+ #doclib-research-select-btn,
+ #doclib-select-btn,
+ #gallery-select-btn,
+ #gallery-albums-select-btn,
+ #gallery-editor-drafts-select,
+ #email-lib-select-btn,
+ #archive-select-btn,
+ #lib-select-btn,
+ #hwfit-cache-select
+).active::before {
+ background-color: var(--accent, var(--red));
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 6 6 18M6 6l12 12' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18 6 6 18M6 6l12 12' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
+}
+
+:is(#memory-select-btn, #skills-select-btn, #email-lib-select-btn) > .memory-select-btn-icon {
+ display: none;
+}
+
+/* The Skills toolbar sits a pixel higher than its peers. */
+#skills-select-btn::before {
+ transform: translateY(1px);
+}
+
+
+/* Deep Research History uses the same library surface as Memory, Skills, and
+ Tasks: one framed block, compact toolbar, filter chips, bulk selection, and
+ cards that expand in place. */
+#research-pane .research-history-card {
+ flex: 1 1 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ margin: 0;
+ padding: 12px;
+}
+#research-pane .research-history-title-row {
+ flex: 0 0 auto;
+ display: flex;
+ align-items: center;
+ min-height: 24px;
+}
+#research-pane .research-history-clear-btn svg {
+ position: relative;
+ top: 2px;
+}
+#research-pane .research-history-title-row h2 {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ position: relative;
+ top: -4px;
+ margin: 0;
+ padding: 0;
+ font-size: 14px;
+ line-height: 1;
+ border: 0;
+ letter-spacing: 0;
+}
+#research-pane .research-history-title-row h2 > svg {
+ width: 14px;
+ height: 14px;
+ color: var(--accent, var(--red));
+ flex: 0 0 14px;
+}
+#research-pane .research-history-head-count {
+ position: relative;
+ top: 4px;
+ font-size: 0.6em;
+ opacity: 0.6;
+ font-weight: normal;
+}
+#research-pane .research-history-toolbar {
+ flex: 0 0 auto;
+ max-height: none;
+ overflow: visible;
+}
+#research-pane .research-history-toolbar-row {
+ align-items: center;
+ min-height: 24px;
+}
+#research-pane .research-history-toolbar-row .memory-sort-select {
+ top: -3px;
+ width: 92px;
+}
+#research-pane #research-history-select-btn {
+ position: relative;
+ top: -5px;
+}
+#research-pane #research-history-select-btn svg {
+ flex: 0 0 auto;
+ margin-right: 3px;
+ vertical-align: -2px;
+}
+#research-pane .research-history-search-wrap > svg {
+ position: absolute;
+ left: 9px;
+ z-index: 1;
+ width: 12px;
+ height: 12px;
+ color: var(--accent, var(--red));
+ pointer-events: none;
+}
+#research-pane .research-history-search-wrap .memory-search-input {
+ height: 30px;
+ margin: 0;
+ padding-left: 28px;
+}
+#research-pane #research-history-filters {
+ flex: 0 0 auto;
+ margin: -2px 0 0;
+}
+#research-pane #research-history-bulk {
+ flex: 0 0 auto;
+ position: relative;
+ top: -4px;
+ margin: 2px 0 5px;
+}
+#research-pane #research-history-bulk-delete {
+ position: relative;
+ top: -4px;
+ right: 0;
+ margin-left: auto;
+}
+#research-pane #research-history-bulk-delete svg {
+ width: 11px;
+ height: 11px;
+ margin-right: 3px;
+ vertical-align: -1px;
+}
+#research-pane .research-history-select-cb {
+ position: relative;
+ top: -4px;
+}
+#research-pane #research-history-bulk-cancel {
+ position: relative;
+ top: -2px;
+ right: 0;
+ margin-left: 4px;
+}
+#research-pane #research-past-list {
+ position: relative;
+ top: -4px;
+ flex: 1 1 0 !important;
+ min-height: 0 !important;
+ overflow-y: auto !important;
+ padding: 2px 2px 4px 0;
+ gap: 4px;
+}
+#research-pane #research-past-list .research-job-card {
+ display: block;
+ height: 67px;
+ min-height: 67px;
+ box-sizing: border-box;
+ max-height: none;
+ padding: 8px 10px;
+ background: color-mix(in srgb, var(--fg) 3%, transparent);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ box-shadow: none;
+}
+#research-pane #research-past-list .research-job-card:hover {
+ background: color-mix(in srgb, var(--fg) 5%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 16%, transparent);
+ transform: none;
+}
+#research-pane #research-past-list .research-job-card.doclib-card-expanded {
+ display: flex !important;
+ flex: 1 1 auto !important;
+ min-height: 0 !important;
+ height: 100%;
+ max-height: none;
+ overflow: hidden;
+}
+#research-pane #research-past-list .research-job-card.research-job-failed:not(.doclib-card-expanded) {
+ height: 67px;
+ min-height: 67px;
+}
+#research-pane #research-past-list .research-job-header {
+ display: flex;
+ width: 100%;
+ min-width: 0;
+ min-height: 18px;
+ gap: 6px;
+ overflow: visible;
+}
+#research-pane #research-past-list .research-job-query {
+ position: static;
+ top: auto;
+ display: block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: normal;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
+ flex: 1 1 auto;
+ min-width: 0;
+}
+#research-pane #research-past-list .research-job-header:has(.research-job-title-error) {
+ flex-wrap: wrap;
+ row-gap: 2px;
+}
+#research-pane #research-past-list .research-job-header:has(.research-job-title-error) .research-job-query {
+ order: 1;
+}
+#research-pane #research-past-list .research-job-header:has(.research-job-title-error) .research-job-retry-badge,
+#research-pane #research-past-list .research-job-header:has(.research-job-title-error) .research-job-edit-badge,
+#research-pane #research-past-list .research-job-header:has(.research-job-title-error) .research-job-overflow {
+ order: 2;
+}
+#research-pane #research-past-list .research-job-title-error {
+ order: 3;
+ flex: 0 0 100%;
+ max-width: none;
+ margin-top: 0;
+ text-align: left;
+}
+#research-pane #research-past-list .research-job-report-badge,
+#research-pane #research-past-list .research-job-discuss-badge,
+#research-pane #research-past-list .research-job-overflow {
+ flex: 0 0 auto;
+}
+ #research-pane #research-past-list .research-job-meta {
+ position: relative;
+ top: 4px;
+ display: inline-flex;
+ align-items: center;
+ margin: 0;
+ font-family: inherit;
+ font-size: 10px;
+ line-height: 1.2;
+ opacity: 0.4;
+ transform: none;
+ }
+ #research-pane #research-past-list .research-job-summary-copy {
+ transform: translateY(-22px);
+ }
+#research-pane #research-past-list .research-job-summary {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 35px;
+ margin-top: 4px;
+}
+ #research-pane #research-past-list .research-job-thumb-frame {
+ width: 48px;
+ height: 33px;
+ flex-basis: 48px;
+ border-radius: 3px;
+ transform: translateY(-14px);
+ }
+#research-pane #research-past-list .research-job-overflow {
+ margin-left: 0;
+ align-self: flex-start;
+}
+#research-pane #research-past-list .research-job-more {
+ width: 22px;
+ height: 22px;
+ min-height: 22px;
+ border: 0;
+ background: transparent;
+}
+#research-pane #research-past-list .research-job-more > svg {
+ transform: translateY(6px);
+}
+#research-pane #research-past-list .task-status-badge {
+ height: 20px;
+ min-height: 20px;
+ padding: 0 8px;
+ gap: 4px;
+}
+#research-pane #research-past-list .research-job-actions .task-status-badge {
+ display: inline-flex;
+ align-items: center;
+ flex: 0 0 auto;
+ height: 20px;
+ min-height: 20px;
+ padding: 0 8px;
+ gap: 4px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+ cursor: pointer;
+}
+#research-pane #research-past-list .research-job-actions .task-status-badge:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
+ border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border));
+}
+#research-pane #research-past-list .research-job-actions .task-status-badge > svg {
+ width: 11px;
+ height: 11px;
+ flex: 0 0 11px;
+}
+#research-pane #research-past-list .task-status-badge > svg {
+ width: 10px;
+ height: 10px;
+}
+#research-pane #research-past-list .research-job-report-badge {
+ color: var(--cat-color, var(--accent, var(--red)));
+ background: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 16%, transparent);
+ border-color: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 38%, transparent);
+}
+#research-pane #research-past-list .research-job-report-badge:hover {
+ background: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 25%, transparent);
+ border-color: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 58%, transparent);
+}
+#research-pane #research-past-list .research-job-retry-badge,
+#research-pane #research-past-list .research-job-edit-badge {
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+#research-pane #research-past-list .research-job-retry-badge:hover,
+#research-pane #research-past-list .research-job-edit-badge:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 32%, transparent);
+}
+#research-pane #research-past-list .research-job-discuss-badge {
+ color: color-mix(in srgb, var(--fg) 72%, transparent);
+ background: color-mix(in srgb, var(--fg) 7%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 18%, transparent);
+}
+#research-pane #research-past-list .research-job-discuss-badge:hover {
+ color: var(--fg);
+ background: color-mix(in srgb, var(--fg) 12%, transparent);
+ border-color: color-mix(in srgb, var(--fg) 32%, transparent);
+}
+#research-pane .research-history-card-select {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ min-width: 20px;
+ min-height: 20px;
+ flex: 0 0 auto;
+ cursor: pointer;
+ position: relative;
+ z-index: 2;
+}
+#research-pane .research-history-card-select .memory-select-cb {
+ margin: 0 1px 0 0;
+}
+#research-pane #research-past-list .research-history-selectable {
+ cursor: pointer;
+}
+#research-pane #research-past-list .research-job-result {
+ max-height: none;
+ overflow: visible;
+}
+#research-pane .research-history-card:has(.doclib-card-expanded) #research-past-list {
+ overflow: hidden !important;
+}
+#research-pane #research-past-list:has(.doclib-card-expanded) > .research-job-card:not(.doclib-card-expanded) {
+ display: none !important;
+}
+#research-pane #research-past-list .research-job-card.doclib-card-expanded .research-job-result {
+ flex: 1 1 auto !important;
+ min-height: 0 !important;
+ max-height: none !important;
+ overflow-y: auto !important;
+ overflow-x: hidden !important;
+ overscroll-behavior: contain;
+}
+
+@media (max-width: 600px) {
+ #research-pane .research-history-card {
+ padding: 10px;
+ }
+ #research-pane .research-history-toolbar-row {
+ flex-wrap: nowrap;
+ }
+ #research-pane .research-history-toolbar-row .memory-sort-select {
+ width: 84px;
+ }
+ #research-pane #research-history-clear {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+ #research-pane #research-past-list .research-job-card {
+ min-height: 77px;
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+ #research-pane #research-past-list .research-job-card.research-job-failed:not(.doclib-card-expanded) {
+ height: 77px;
+ min-height: 77px;
+ }
+ #research-pane #research-past-list .research-job-header {
+ gap: 4px;
+ }
+ #research-pane #research-past-list .research-job-report-badge,
+ #research-pane #research-past-list .research-job-discuss-badge {
+ width: auto;
+ min-width: 0;
+ height: 28px;
+ min-height: 28px;
+ padding-left: 7px;
+ padding-right: 7px;
+ justify-content: center;
+ gap: 4px;
+ }
+ #research-pane #research-past-list .research-job-report-badge > svg,
+ #research-pane #research-past-list .research-job-discuss-badge > svg {
+ width: 13px;
+ height: 13px;
+ flex: 0 0 13px;
+ }
+ #research-pane #research-past-list .research-job-report-badge .task-state-label,
+ #research-pane #research-past-list .research-job-discuss-badge .task-state-label {
+ display: inline;
+ font-size: 9px;
+ line-height: 1;
+ }
+ #research-pane #research-past-list .research-job-more {
+ width: 24px;
+ min-width: 24px;
+ }
+}
diff --git a/static/css/cookbook-research-memory-settings.css b/static/css/cookbook-research-memory-settings.css
index 67f425dae..e8310d049 100644
--- a/static/css/cookbook-research-memory-settings.css
+++ b/static/css/cookbook-research-memory-settings.css
@@ -1,22 +1,20 @@
/* Cookbook (including What Fits?), deep research, memory, settings and admin.
*
- * Split out of style.css for maintainability, not for performance: this file
- * is loaded eagerly from index.html immediately after style.css, so the rules
- * land in the cascade in exactly the order they had inside the single file.
- * Nothing here is lazy-loaded.
+ * This is an eagerly loaded tail segment of the app stylesheet cascade. The
+ * former static/style.css now lives in the numbered 00-17 fragments; these
+ * three pre-existing panel files follow those fragments without changing
+ * their relative order. Nothing here is lazy-loaded.
*
- * LOAD ORDER IS PART OF THE CONTRACT. The tags in static/index.html and
- * the PRECACHE list in static/sw.js must both list the stylesheets in this
- * order:
- * 1. static/style.css
+ * LOAD ORDER IS PART OF THE CONTRACT. static/index.html and the PRECACHE list
+ * in static/sw.js must agree:
+ * 1. static/css/00-tokens.css through static/css/17-research.css,
+ * in numeric order
* 2. static/css/documents-gallery-editor.css
* 3. static/css/email-calendar-notes-tasks.css
* 4. static/css/cookbook-research-memory-settings.css
*
- * Rules were moved verbatim. A rule stays in style.css when its selector group
- * covers more than one panel, when the class is shared design language used by
- * modules outside this panel, or when moving it would change which of two
- * equally specific rules wins.
+ * Rules in this file retain their existing cascade position. Shared rules
+ * formerly kept in static/style.css now live in the numbered fragments.
*/
/* Cookbook already shows its own running/served-status dot
diff --git a/static/css/documents-gallery-editor.css b/static/css/documents-gallery-editor.css
index 02c446729..cc37362bb 100644
--- a/static/css/documents-gallery-editor.css
+++ b/static/css/documents-gallery-editor.css
@@ -1,22 +1,20 @@
/* Document library and editor, image gallery, image editor.
*
- * Split out of style.css for maintainability, not for performance: this file
- * is loaded eagerly from index.html immediately after style.css, so the rules
- * land in the cascade in exactly the order they had inside the single file.
- * Nothing here is lazy-loaded.
+ * This is an eagerly loaded tail segment of the app stylesheet cascade. The
+ * former static/style.css now lives in the numbered 00-17 fragments; these
+ * three pre-existing panel files follow those fragments without changing
+ * their relative order. Nothing here is lazy-loaded.
*
- * LOAD ORDER IS PART OF THE CONTRACT. The tags in static/index.html and
- * the PRECACHE list in static/sw.js must both list the stylesheets in this
- * order:
- * 1. static/style.css
+ * LOAD ORDER IS PART OF THE CONTRACT. static/index.html and the PRECACHE list
+ * in static/sw.js must agree:
+ * 1. static/css/00-tokens.css through static/css/17-research.css,
+ * in numeric order
* 2. static/css/documents-gallery-editor.css
* 3. static/css/email-calendar-notes-tasks.css
* 4. static/css/cookbook-research-memory-settings.css
*
- * Rules were moved verbatim. A rule stays in style.css when its selector group
- * covers more than one panel, when the class is shared design language used by
- * modules outside this panel, or when moving it would change which of two
- * equally specific rules wins.
+ * Rules in this file retain their existing cascade position. Shared rules
+ * formerly kept in static/style.css now live in the numbered fragments.
*/
/* Document library search-term highlight — clear, high-contrast. */
diff --git a/static/css/email-calendar-notes-tasks.css b/static/css/email-calendar-notes-tasks.css
index ccc27f4f5..7a19b6021 100644
--- a/static/css/email-calendar-notes-tasks.css
+++ b/static/css/email-calendar-notes-tasks.css
@@ -1,22 +1,20 @@
/* Email, calendar, notes and tasks panels.
*
- * Split out of style.css for maintainability, not for performance: this file
- * is loaded eagerly from index.html immediately after style.css, so the rules
- * land in the cascade in exactly the order they had inside the single file.
- * Nothing here is lazy-loaded.
+ * This is an eagerly loaded tail segment of the app stylesheet cascade. The
+ * former static/style.css now lives in the numbered 00-17 fragments; these
+ * three pre-existing panel files follow those fragments without changing
+ * their relative order. Nothing here is lazy-loaded.
*
- * LOAD ORDER IS PART OF THE CONTRACT. The tags in static/index.html and
- * the PRECACHE list in static/sw.js must both list the stylesheets in this
- * order:
- * 1. static/style.css
+ * LOAD ORDER IS PART OF THE CONTRACT. static/index.html and the PRECACHE list
+ * in static/sw.js must agree:
+ * 1. static/css/00-tokens.css through static/css/17-research.css,
+ * in numeric order
* 2. static/css/documents-gallery-editor.css
* 3. static/css/email-calendar-notes-tasks.css
* 4. static/css/cookbook-research-memory-settings.css
*
- * Rules were moved verbatim. A rule stays in style.css when its selector group
- * covers more than one panel, when the class is shared design language used by
- * modules outside this panel, or when moving it would change which of two
- * equally specific rules wins.
+ * Rules in this file retain their existing cascade position. Shared rules
+ * formerly kept in static/style.css now live in the numbered fragments.
*/
@keyframes email-notif-breathe {
diff --git a/static/index.html b/static/index.html
index dcae7008b..ad04daf89 100644
--- a/static/index.html
+++ b/static/index.html
@@ -246,15 +246,32 @@
real request is discarded and the font fetched a second time. -->
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
@@ -2639,7 +2656,7 @@
-
+
diff --git a/static/js/signature.js b/static/js/signature.js
index 374bd4c94..526766808 100644
--- a/static/js/signature.js
+++ b/static/js/signature.js
@@ -316,7 +316,7 @@ class SmoothPad {
}
function _modal(innerHtml) {
- // Match the app's standard .modal pattern (defined in static/style.css).
+ // Match the app's standard .modal pattern from the main stylesheet cascade.
const overlay = document.createElement('div');
overlay.className = 'modal sig-modal-overlay';
overlay.style.cssText = 'pointer-events:auto;background:rgba(0,0,0,0.45);z-index:10100;';
diff --git a/static/login.html b/static/login.html
index 17a7d2dfa..e9dfdde82 100644
--- a/static/login.html
+++ b/static/login.html
@@ -106,7 +106,7 @@