From a4715b77a5e0ff513667950b6ac91b3d0b103773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Tue, 29 Sep 2026 17:50:04 +0200 Subject: [PATCH] refactor(css): name the fragments by area instead of by number style-part-01..08 were arbitrary 6,000-line cuts, so the names said nothing and a reader had no way to guess which file held a rule. The original stylesheet turns out to be roughly area-grouped already, so cutting where the content changes rather than every 6,000 lines produces boundaries worth naming. Classifying every top-level rule by selector prefix and smoothing over a 40-rule window gives 17 stable runs: agent-chat, compare, memory, documents, admin-settings, skills, gallery, cookbook, tasks, image-editor, email, notes, calendar, research. The numeric prefix stays because load order is load-bearing, and it also disambiguates the areas that appear twice: the original interleaves, and merging non-adjacent blocks of the same area would reorder the cascade. The names describe where a file sits, not a claim that it holds every rule for that area or only rules for it. The header in each file says so, because that is the misreading this naming invites. Reconstruction was checked against lab's style.css before rewriting: identical after whitespace normalisation, all 47,528 lines accounted for. The computed style baseline still reproduces exactly. --- static/css/{tokens.css => 00-tokens.css} | 21 +- .../{style-part-01.css => 01-agent-chat.css} | 1824 ++++- static/css/02-compare.css | 1805 +++++ static/css/03-agent-chat.css | 2846 ++++++++ static/css/04-memory.css | 1675 +++++ .../{style-part-03.css => 05-documents.css} | 2837 +------- static/css/06-admin-settings.css | 1426 ++++ static/css/07-documents.css | 1283 ++++ static/css/08-skills.css | 1088 +++ static/css/09-gallery.css | 1382 ++++ .../{style-part-05.css => 10-cookbook.css} | 4953 +++++--------- static/css/11-tasks.css | 1972 ++++++ static/css/12-gallery.css | 839 +++ ...{style-part-06.css => 13-image-editor.css} | 2278 ++----- static/css/14-email.css | 4729 +++++++++++++ .../css/{style-part-07.css => 15-notes.css} | 4042 +++-------- static/css/16-calendar.css | 1175 ++++ .../{style-part-08.css => 17-research.css} | 2213 +----- static/css/style-part-02.css | 6011 ----------------- static/css/style-part-04.css | 6011 ----------------- static/index.html | 27 +- static/sw.js | 27 +- 22 files changed, 25335 insertions(+), 25129 deletions(-) rename static/css/{tokens.css => 00-tokens.css} (89%) rename static/css/{style-part-01.css => 01-agent-chat.css} (77%) create mode 100644 static/css/02-compare.css create mode 100644 static/css/03-agent-chat.css create mode 100644 static/css/04-memory.css rename static/css/{style-part-03.css => 05-documents.css} (55%) create mode 100644 static/css/06-admin-settings.css create mode 100644 static/css/07-documents.css create mode 100644 static/css/08-skills.css create mode 100644 static/css/09-gallery.css rename static/css/{style-part-05.css => 10-cookbook.css} (55%) create mode 100644 static/css/11-tasks.css create mode 100644 static/css/12-gallery.css rename static/css/{style-part-06.css => 13-image-editor.css} (71%) create mode 100644 static/css/14-email.css rename static/css/{style-part-07.css => 15-notes.css} (55%) create mode 100644 static/css/16-calendar.css rename static/css/{style-part-08.css => 17-research.css} (57%) delete mode 100644 static/css/style-part-02.css delete mode 100644 static/css/style-part-04.css diff --git a/static/css/tokens.css b/static/css/00-tokens.css similarity index 89% rename from static/css/tokens.css rename to static/css/00-tokens.css index 2a23629ad..3c9e49d3a 100644 --- a/static/css/tokens.css +++ b/static/css/00-tokens.css @@ -1,13 +1,18 @@ -/* Fragment 1/9 of the former static/style.css. +/* 00-tokens - part of the former static/style.css. * - * This is a mechanical, order-preserving slice: rules were moved verbatim - * and this file's position in the order of static/index.html is what - * keeps the cascade identical. Reordering the links, or moving a rule between - * fragments, can change which of two competing declarations wins. + * 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. * - * static/index.html and the PRECACHE list in static/sw.js must list every - * fragment in the same order. tests/test_static_stylesheet_manifest.py and - * tests/test_css_computed_style_snapshot.py both fail if that drifts. + * 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 */ diff --git a/static/css/style-part-01.css b/static/css/01-agent-chat.css similarity index 77% rename from static/css/style-part-01.css rename to static/css/01-agent-chat.css index 7e4250bcb..9e70c4f2c 100644 --- a/static/css/style-part-01.css +++ b/static/css/01-agent-chat.css @@ -1,13 +1,18 @@ -/* Fragment 2/9 of the former static/style.css. +/* 01-agent-chat - part of the former static/style.css. * - * This is a mechanical, order-preserving slice: rules were moved verbatim - * and this file's position in the order of static/index.html is what - * keeps the cascade identical. Reordering the links, or moving a rule between - * fragments, can change which of two competing declarations wins. + * 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. * - * static/index.html and the PRECACHE list in static/sw.js must list every - * fragment in the same order. tests/test_static_stylesheet_manifest.py and - * tests/test_css_computed_style_snapshot.py both fail if that drifts. + * 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; } @@ -6010,3 +6015,1806 @@ body.bg-pattern-ascii-fireflies { 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; } + } + @keyframes loading-bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1); + } + } + /* 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; + } + + @keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.7; } + 100% { opacity: 1; } + } + + #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..df2e1e444 --- /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 fadeIn { from { opacity: 0; } to { opacity: 1; } } + @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..ed4f7e58b --- /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
 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: