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: