/* ============================================ */
/* Odysseus UI — Consolidated Stylesheet */
/* ============================================ */
/* ── Variables ──
*
* Theme-public variables (override via theme.js or custom themes):
* Core: --bg, --fg, --panel, --border, --red
* Syntax: --hl-keyword, --hl-string, --hl-comment, --hl-function,
* --hl-number, --hl-builtin, --hl-variable, --hl-params,
* --hl-bg, --hl-fg
* Accents: --accent-primary, --accent-error (set by theme.js)
* Semantic: --color-error, --color-success, --color-warning, --color-danger,
* --color-accent, --color-muted, --color-muted-alt
*/
:root {
/* Core palette */
--bg: #282c34;
--fg: #9cdef2;
--panel: #111;
--border: #355a66;
--red: #e06c75;
/* Were `var(--green)` / `var(--warn)` — self-referential, so they
resolved to invalid and every site fell back to its own literal
(or, for sites with no fallback, painted as transparent/inherit).
Anchor them to real hex so the token layer actually works. */
--green: #50fa7b;
--warn: #f0ad4e;
/* Syntax highlighting */
--hl-bg: #1e2228;
--hl-fg: #9cdef2;
--hl-keyword: #c678dd;
--hl-string: #e5c07b;
--hl-comment: #828997;
--hl-function: #61afef;
--hl-number: #d19a66;
--hl-builtin: #56b6c2;
--hl-variable: #abb2bf;
--hl-params: #a8c0d4;
/* Semantic colors */
--color-error: #ff4444;
--color-error-light: #ff6666;
--color-success: #4caf50;
--color-warning: #f0ad4e;
--color-danger: #c0392b;
--color-recording: #ff3b30;
--color-recording-hover: #d63031;
--color-muted: #888;
--color-muted-alt: #6b7280;
--color-accent: #00aaff;
--color-agent-active: #00ff00;
--color-brand-blue: #3b82f6;
--color-blind-orange: #ff9800;
--color-save-green: var(--color-success);
--color-link-hover: #66c7ff;
--color-subheader: #6b8a94;
/* Warm accent — used by the Goals/Today UI in Notes. Lives as a token so
themes can override without touching the goal CSS. */
--accent-warm: #d19a66;
}
:root.light {
--bg: #f5f5f5;
--fg: #2b2b2b;
--panel: #fff;
--border: #bbb;
--hl-bg: #f9f9f9;
--hl-fg: #2b2b2b;
--hl-keyword: #7928a1;
--hl-string: #986801;
--hl-comment: #6a737d;
--hl-function: #005cc5;
--hl-number: #986801;
--hl-builtin: #0070a0;
--hl-variable: #383a42;
--hl-params: #4a4f5c;
}
/* ── Reset & Base ── */
* { box-sizing: border-box; }
html, body { overflow-x: hidden; height: 100%; margin: 0; overscroll-behavior: none; }
body {
background-color: var(--bg);
color: var(--fg);
font-family: var(--font-family, 'Fira Code', monospace);
display: flex;
height: 100%;
height: 100dvh; /* dynamic viewport height — adapts when mobile keyboard opens */
overflow: hidden;
}
/* Self-hosted Fira Code font */
@font-face { font-family: 'Fira Code'; font-weight: 300; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Light.woff2') format('woff2'); }
@font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); }
@font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); }
/* Code block baseline */
pre, code, .hljs {
font-size: 0.95em;
line-height: 1.5;
}
/* Scrollbar styling */
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--panel);
}
::-webkit-scrollbar-thumb {
background-color: var(--red);
border-radius: 4px;
border: 2px solid var(--panel);
}
::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in srgb, var(--red) 80%, white);
}
}
html {
scrollbar-color: var(--red) var(--panel);
scrollbar-width: thin;
}
/* Utility */
.red-text { color: var(--red); }
/* ── Density Overrides ── */
:root.density-compact { font-size: 13px; }
:root.density-compact .msg { padding: 6px 10px; margin-bottom: 4px; }
:root.density-compact .list-item { padding: 4px 8px; }
:root.density-compact .sidebar .section { padding: 0; }
:root.density-spacious { font-size: 16px; }
:root.density-spacious .msg { padding: 14px 18px; margin-bottom: 12px; }
:root.density-spacious .list-item { padding: 8px 12px; }
:root.density-spacious .sidebar .section { padding: 0; }
/* ── Background Patterns ── */
:root { --bg-effect-intensity: 1; }
/* Canvas-based effects — single source of truth for intensity */
#synapse-canvas, #rain-canvas, #constellations-canvas,
#perlin-flow-canvas, #petals-canvas, #sparkles-canvas,
#embers-canvas {
opacity: var(--bg-effect-intensity, 1);
}
body.bg-pattern-dots {
background-image: radial-gradient(color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px);
background-size: 20px 20px; background-attachment: fixed;
}
body.bg-pattern-synapse {
/* CSS grid as base, canvas pulses overlay */
background-image: linear-gradient(color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(3.5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px),
linear-gradient(90deg, color-mix(in srgb, var(--bg-effect-color, var(--fg)) calc(3.5% * var(--bg-effect-intensity, 1)), transparent) 1px, transparent 1px);
background-size: 24px 24px; background-attachment: fixed;
}
body.bg-pattern-perlin-flow,
body.bg-pattern-petals,
body.bg-pattern-sparkles {
/* canvas-only backgrounds */
}
/* ── Layout ── */
/* Top bar — session meta + incognito, single row */
.chat-top-bar {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
z-index: 2;
padding: 5px 0 0;
min-height: 25px;
box-sizing: border-box;
}
.chat-top-bar::after {
content: none;
}
body.sidebar-collapsed.hamburger-left .chat-top-bar {
padding-left: 38px;
}
body.sidebar-collapsed.hamburger-right .chat-top-bar {
padding-right: 38px;
}
.incognito-indicator {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
background: color-mix(in srgb, var(--accent) 12%, transparent);
border: 1px solid var(--accent);
color: var(--accent);
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s, background 0.15s, left 0.15s;
z-index: 1;
opacity: 0.7;
}
body.sidebar-collapsed.hamburger-left .incognito-indicator {
left: 12px;
}
.incognito-indicator:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent) 20%, transparent);
}
.chat-new-btn {
position: absolute;
right: 7px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--fg);
opacity: 0.6;
cursor: pointer;
padding: 4px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.08s, background 0.08s, left 0.15s, right 0.15s;
flex-shrink: 0;
z-index: 1;
}
.chat-new-btn:hover {
opacity: 1;
color: var(--accent);
}
/* Flip new-chat and incognito when sidebar is on the right */
body:has(.sidebar.right-side) .chat-new-btn {
right: auto;
left: 12px;
}
body:has(.sidebar.right-side) .incognito-indicator {
left: auto;
right: 12px;
}
body.sidebar-collapsed.hamburger-right .incognito-indicator {
right: 42px;
}
/* Session meta — sits at top of chat area, scrolls with content */
.chat-meta-overlay {
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(calc(-50% - 2px));
font-size: 0.75em;
line-height: 1;
color: color-mix(in srgb, var(--fg) 40%, transparent);
white-space: nowrap;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
pointer-events: none;
}
.chat-meta-overlay > * {
pointer-events: auto;
}
.chat-meta-overlay #current-meta {
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
}
.chat-meta-overlay:hover {
color: color-mix(in srgb, var(--fg) 75%, transparent);
}
/* Offline model rows */
.models-row-offline {
opacity: 0.4;
pointer-events: none;
}
.models-row-offline .model-chat-btn {
pointer-events: auto;
}
/* Offline badge next to endpoint name */
.endpoint-offline-badge {
color: var(--danger, #f44);
font-size: 0.8em;
margin-left: 4px;
opacity: 0.7;
}
.chat-meta-overlay:empty,
.chat-meta-overlay:not(:has(#current-meta:not(:empty))) {
display: none;
}
.export-dropdown-wrap {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin-left: -4px;
margin-right: -20px;
}
.export-dl-btn {
background: none; border: none;
color: inherit;
cursor: pointer;
padding: 4px 5px; border-radius: 4px;
display: flex; align-items: center;
transition: color 0.08s, background 0.08s;
}
.export-dl-btn:hover {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.export-dropdown-menu {
display: none;
position: fixed;
background: var(--panel); border: 1px solid var(--border);
border-radius: 8px; padding: 4px; min-width: 120px;
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
z-index: 300;
color: var(--fg);
}
.export-dropdown-menu.open { display: block; }
.export-dropdown-item {
padding: 6px 8px; font-size: 11px; cursor: pointer;
border-radius: 6px; color: var(--fg); white-space: nowrap;
display: flex; align-items: center; gap: 10px;
transition: background 0.1s;
}
.export-dropdown-item:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.export-dropdown-item .dropdown-icon {
width: 14px; height: 14px;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; opacity: 0.5;
}
/* On mobile the chat-header export dropdown was cramped — items
were 11px font with 6px padding. Bump to readable touch targets:
larger min-width, taller rows, bigger icons + text. */
@media (max-width: 768px) {
.export-dropdown-menu {
min-width: 200px;
padding: 6px;
border-radius: 10px;
}
.export-dropdown-item {
padding: 12px 14px;
font-size: 14px;
gap: 12px;
min-height: 44px;
}
.export-dropdown-item .dropdown-icon {
width: 18px; height: 18px; opacity: 0.7;
}
.export-dropdown-item .dropdown-icon svg {
width: 18px; height: 18px;
}
}
.sidebar {
width: 240px;
background: var(--sidebar-bg, var(--panel));
border-right: 1px solid var(--border);
transition: width 0.25s ease, opacity 0.2s ease, padding 0.25s ease;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
min-height: 0;
margin: 0;
padding: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid color-mix(in srgb, var(--fg) 11%, transparent);
position: relative;
}
.sidebar-resize-handle {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 10;
background: transparent;
}
.sidebar-resize-handle:hover,
.sidebar-resize-handle.dragging {
background: var(--accent);
opacity: 0.6;
}
.sidebar.right-side .sidebar-resize-handle {
right: auto;
left: -3px;
}
.sidebar.resizing {
transition: none;
user-select: none;
}
.sidebar.right-side {
order: 2;
margin: 0;
border-right: none;
border-left: 1px solid var(--border);
}
.sidebar.hidden {
/* !important so it beats the inline width init.js restores from storage —
otherwise the width never changes and only opacity animates, making the
collapse look instant. With this, width animates from the inline value
down to 0 via the .sidebar width transition. */
width: 0 !important;
padding: 0 !important;
border: none;
overflow: hidden;
opacity: 0;
}
/* ===== Sidebar User Bar ===== */
.sidebar-user-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 12px;
flex-shrink: 0;
gap: 4px;
min-height: 48px;
}
.user-bar-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
cursor: pointer;
padding: 6px 8px;
border-radius: 8px;
transition: background 0.15s;
}
.user-bar-left:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.user-bar-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: color-mix(in srgb, var(--fg) 12%, transparent);
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
color: var(--fg);
opacity: 0.7;
flex-shrink: 0;
text-transform: uppercase;
}
.user-bar-name {
font-size: 9.75px;
font-weight: 500;
color: var(--fg);
opacity: 0.8;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-bar-actions {
display: flex;
gap: 1px;
flex-shrink: 0;
}
.user-bar-btn {
background: none;
border: none;
color: var(--fg);
opacity: 0.35;
cursor: pointer;
padding: 6px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.12s, background 0.12s;
}
.user-bar-btn:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.sidebar.hidden .sidebar-user-bar {
display: none;
}
/* Sticky sidebar header — logo, never scrolls */
.sidebar-header {
display: flex;
align-items: center;
justify-content: flex-end; /* right-align when sidebar is on left */
gap: 8px;
padding: 15px 10px 0 40px; /* top padding aligns logo with fixed hamburger */
flex-shrink: 0;
min-height: 40px;
border: none !important;
box-shadow: none !important;
position: relative;
z-index: 3;
background: var(--sidebar-bg, var(--panel));
}
.sidebar-hamburger {
display: none !important; /* external #hamburger-btn is the only toggle */
}
.sidebar-inner {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior-y: none;
scrollbar-width: none;
display: flex;
flex-direction: column;
gap: 0;
padding: 10px 8px 8px;
min-height: 0;
transition: transform 0.3s cubic-bezier(0.25, 1, 0.5, 1);
}
.sidebar-brand {
display: flex;
align-items: center;
flex-shrink: 0;
min-height: 24px;
}
.sidebar-brand-title {
font-size: 1rem;
font-weight: 600;
color: var(--brand-color, var(--red));
white-space: nowrap;
user-select: none;
position: relative;
top: 1px;
left: -10px;
}
.sidebar-sep {
display: none;
}
#sidebar-search-btn,
#sidebar-new-chat-btn {
margin: 0;
padding: 8px 8px;
}
#sessions-section {
margin-top: -1px;
}
#tools-section {
margin-top: 1px;
}
#tools-section .list-item {
padding: 8px 8px;
}
.sidebar.right-side .sidebar-header {
justify-content: flex-start;
padding-left: 10px;
padding-right: 40px;
}
.sidebar.right-side .sidebar-inner {
padding: 8px;
}
.sidebar.right-side .sidebar-brand {
justify-content: flex-start;
padding: 2px 30px 4px 4px;
}
.sidebar.right-side .sidebar-brand-title {
margin-left: 10px;
}
/* Fixed hamburger — always visible, toggles sidebar */
.hamburger-btn {
position: fixed;
top: 12px;
left: 9px;
right: auto;
z-index: 210;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--hamburger-color, var(--fg));
cursor: pointer;
opacity: 0.5;
-webkit-tap-highlight-color: transparent;
outline: none;
transition: opacity 0.15s;
padding: 0;
display: flex;
}
body.hamburger-right .hamburger-btn {
left: auto;
right: 9px;
}
.mobile-new-chat-btn {
display: none;
}
.hamburger-btn:hover {
opacity: 1;
}
/* Icon rail — mini sidebar that replaces the wide .sidebar when it's
hidden (mutually exclusive — see sidebar-layout.js:57). Fullscreen
panels reserve this strip of width via `left: var(--icon-rail-w)`
so the rail stays visible without needing a z-index hack (which
used to cover the fixed-position hamburger button). */
.icon-rail {
width: 48px;
flex-shrink: 0;
background: var(--panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 4px 8px 4px;
gap: 4px;
margin: 0;
position: relative;
box-sizing: border-box;
/* Allow hover labels (e.g. the rail-new-chat "New" tooltip) to extend
outside the 48px column. overflow:hidden was clipping them. */
overflow: visible;
}
.rail-resize-handle {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 10;
background: transparent;
}
.rail-resize-handle:hover,
.rail-resize-handle.dragging {
background: var(--accent);
opacity: 0.6;
}
.icon-rail.right-side .rail-resize-handle {
right: auto;
left: -3px;
}
.icon-rail.right-side {
order: 2;
margin: 0;
border-right: none;
border-left: 1px solid var(--border);
}
.icon-rail-divider {
width: 24px;
height: 1px;
background: var(--border);
margin: 4px 0;
}
.icon-rail-btn {
position: relative;
width: 34px;
height: 34px;
border: none;
background: transparent;
color: var(--accent, var(--red));
font-size: 16px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.08s, background 0.08s;
}
.rail-notes-badge {
position: absolute;
top: 1px;
right: 1px;
min-width: 14px;
height: 14px;
padding: 0 3px;
border-radius: 7px;
background: color-mix(in srgb, var(--accent) 85%, var(--bg));
color: var(--bg);
font-size: 9px;
font-weight: 700;
line-height: 14px;
text-align: center;
box-sizing: border-box;
pointer-events: none;
box-shadow: 0 0 0 1px var(--bg);
}
.rail-notes-badge.fired {
background: var(--red);
animation: rail-notes-pulse 1.6s ease-in-out infinite;
}
@keyframes rail-notes-pulse {
0%, 100% { box-shadow: 0 0 0 1px var(--bg), 0 0 0 0 color-mix(in srgb, var(--red) 50%, transparent); }
50% { box-shadow: 0 0 0 1px var(--bg), 0 0 0 4px color-mix(in srgb, var(--red) 15%, transparent); }
}
/* Main sidebar notes button — dot when a reminder has fired */
.tool-notes-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--red);
/* Match the Deep Research badge: right-aligned (auto) with the same
4px left nudge, so both sidebar buttons' dots line up identically. */
margin-left: auto;
position: relative;
left: -4px;
flex-shrink: 0;
align-self: center;
animation: rail-notes-pulse 1.6s ease-in-out infinite;
pointer-events: none;
}
/* Individual card — subtle accent border tint when a reminder has fired */
.note-card.note-card-reminder-due .note-card-reminder {
background: color-mix(in srgb, var(--red) 22%, transparent);
color: var(--red);
font-weight: 600;
}
.icon-rail-btn:hover { opacity: 1; background: color-mix(in srgb, var(--accent) 12%, transparent); }
.icon-rail-btn.active-section { opacity: 1; background: color-mix(in srgb, var(--color-accent) 15%, transparent); }
/* Unified "minimized" indicator for any rail/sidebar button whose modal is held open */
.rail-minimized { position: relative; }
.rail-minimized::after {
content: ''; position: absolute;
/* Default for inline spans like #email-section-title — sit just to the
right of the element so it never overlaps the icon. */
top: 50%; right: -10px; transform: translateY(-50%);
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent, var(--red));
box-shadow: 0 0 0 2px var(--bg);
pointer-events: none;
animation: rail-min-pulse 2s ease-in-out infinite;
}
/* Compact icon-rail buttons: top-right corner of the icon */
.icon-rail-btn.rail-minimized::after { top: 4px; right: 4px; transform: none; }
/* Sidebar list-items are wider — anchor right-aligned vertically centered */
.list-item.rail-minimized::after { top: 50%; right: 8px; transform: translateY(-50%); }
#tool-memory-btn.rail-minimized::after { right: 12px; }
/* Per-user nudge: the listed tools' minimized dot sits 2px further
in from the right edge so it doesn't look glued to the border. */
#tool-theme-btn.rail-minimized::after,
#tool-tasks-btn.rail-minimized::after,
#tool-notes-btn.rail-minimized::after,
#tool-library-btn.rail-minimized::after,
#tool-gallery-btn.rail-minimized::after,
#tool-compare-btn.rail-minimized::after,
#tool-calendar-btn.rail-minimized::after { right: 12px; }
/* Cookbook already shows its own running/served-status dot
(#cookbook-notif-dot, toggled with .cookbook-notif-active on the
button). Don't stack the tabbed-down pulse on top of it — the two
dots overlap. Suppress the minimized dot while the status dot is up. */
.list-item.rail-minimized.cookbook-notif-active::after { display: none; }
@media (max-width: 768px) {
/* On mobile the list-items are taller (touch-sized), so the tabbed-down
pulsing dot reads a hair low and right. Email uses its own dot and is
already aligned — only the tool list-item dots need the nudge:
4px left (right 8 → 12) and 2px up. */
.list-item.rail-minimized::after {
right: 13px;
transform: translateY(calc(-50% - 2px));
}
#tool-memory-btn.rail-minimized::after { right: 17px; }
}
@keyframes rail-min-pulse {
0%,100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Compact `_` minimize button for modal headers — matches the close
button's bordered square so the two read as a paired control. The
header's h4 carries margin-right:auto, which groups minimize + close
on the right; this button just needs a small gap before close. */
.modal-minimize-btn {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--fg);
cursor: pointer;
width: 24px;
height: 24px;
padding: 0;
margin-left: 0;
margin-right: 4px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
}
.modal-minimize-btn:hover { background: var(--fg); color: var(--bg); }
.modal.modal-minimized { display: none !important; }
/* Window tile snap ghost (desktop only) */
#tile-ghost {
position: fixed; pointer-events: none; z-index: 9000;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border: 2px solid color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
border-radius: 8px;
opacity: 0; transform: scale(0.96);
transition: left 0.12s ease, top 0.12s ease, width 0.12s ease, height 0.12s ease, opacity 0.12s, transform 0.12s;
}
#tile-ghost.visible { opacity: 1; transform: scale(1); }
/* Bottom dock — chip per minimized modal */
#minimized-dock {
position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%);
display: flex; gap: 6px; flex-wrap: wrap;
max-width: calc(100vw - 24px);
padding: 4px;
z-index: 999;
pointer-events: none;
}
.minimized-dock-chip {
pointer-events: auto;
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 8px 6px 10px;
background: var(--panel, var(--bg));
border: 1px solid var(--border);
border-radius: 999px;
color: var(--fg); font-family: inherit; font-size: 12px;
cursor: grab; touch-action: none; user-select: none;
box-shadow: 0 4px 14px rgba(0,0,0,0.35);
transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.15s, border-color 0.15s;
animation: dock-chip-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}
.minimized-dock-chip:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--panel, var(--bg)));
border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border));
}
.minimized-dock-chip:active { cursor: grabbing; }
.minimized-dock-chip.dragging {
cursor: grabbing; z-index: 1000;
box-shadow: 0 8px 24px rgba(0,0,0,0.55);
transition: none;
opacity: 0.95;
}
/* Whole-dock drag (grabbed an edge chip) */
#minimized-dock.dock-dragging { cursor: grabbing; }
#minimized-dock.dock-dragging .minimized-dock-chip {
transition: none;
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
}
/* Subtle visual cue that edge chips drag the whole dock */
#minimized-dock .minimized-dock-chip:first-child:not(:only-child),
#minimized-dock .minimized-dock-chip:last-child:not(:only-child) {
box-shadow: 0 4px 14px rgba(0,0,0,0.35),
inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
}
.minimized-dock-chip svg { opacity: 0.7; flex-shrink: 0; }
.minimized-dock-label { white-space: nowrap; }
/* Mobile: chips are icon-only round pills. Tap to restore, drag toward
the trash zone at top-center to close. touch-action:none lets the
pointermove listener claim the gesture instead of the page scroll. */
@media (max-width: 768px) {
.minimized-dock-chip {
width: 40px; height: 40px;
padding: 0 !important;
border-radius: 50% !important;
justify-content: center;
position: relative;
overflow: visible;
touch-action: none;
}
.minimized-dock-chip svg { width: 18px; height: 18px; opacity: 0.9; }
.minimized-dock-label,
.minimized-dock-x { display: none !important; }
.minimized-dock-chip.chip-free-drag {
box-shadow: 0 8px 22px rgba(0,0,0,0.55),
0 0 0 2px color-mix(in srgb, var(--accent, var(--red)) 50%, transparent) !important;
}
/* Long-press hint — chip swells and settles while the detach timer
counts down so the user feels feedback before the bubble peels out
of the chain. Returns to scale(1) at the end so there's no visual
jump when the timer fires and the chip becomes a free-drag puck. */
.minimized-dock-chip.chip-long-press {
background:
radial-gradient(circle at 30% 25%, color-mix(in srgb, #fff 28%, transparent), transparent 36%),
linear-gradient(135deg,
color-mix(in srgb, var(--accent, var(--red)) 34%, var(--panel, var(--bg))),
color-mix(in srgb, #7dd3fc 26%, var(--panel, var(--bg))) 52%,
color-mix(in srgb, #f0abfc 22%, var(--panel, var(--bg))));
border-color: color-mix(in srgb, var(--accent, var(--red)) 72%, #fff 12%) !important;
animation: chip-long-press-pulse 0.82s ease-in-out infinite;
z-index: 10;
}
.minimized-dock-chip.chip-long-press::before {
content: '';
position: absolute;
inset: -96px;
border-radius: inherit;
background:
radial-gradient(circle,
color-mix(in srgb, var(--accent, var(--red)) 42%, transparent) 0 18%,
color-mix(in srgb, #7dd3fc 34%, transparent) 34%,
color-mix(in srgb, #f0abfc 30%, transparent) 50%,
transparent 72%);
pointer-events: none;
z-index: -1;
animation: chip-long-press-ripple 0.82s ease-out infinite;
}
@keyframes chip-long-press-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.42);
box-shadow: 0 14px 42px rgba(0,0,0,0.66),
0 0 0 16px color-mix(in srgb, var(--accent, var(--red)) 42%, transparent),
0 0 72px color-mix(in srgb, #7dd3fc 44%, transparent),
0 0 118px color-mix(in srgb, #f0abfc 32%, transparent); }
}
@keyframes chip-long-press-ripple {
0% { opacity: 0.92; transform: scale(0.08); }
72% { opacity: 0.28; transform: scale(3.6); }
100% { opacity: 0; transform: scale(5.4); }
}
/* Chip whose modal is currently open — accent ring so the user can
tell at a glance which floating bubble belongs to the visible
modal. Tap it to minimize. */
.minimized-dock-chip.chip-active {
border-color: var(--accent, var(--red)) !important;
box-shadow: 0 4px 14px rgba(0,0,0,0.35),
0 0 0 2px color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
}
.minimized-dock-chip.chip-active svg { opacity: 1; }
}
/* Magnetic close zone — slides in from the side opposite the chip's
starting position so it never overlaps the dock. Always accent
color, larger than the chips, snappy spring transition. */
#dock-trash-zone {
position: fixed;
left: 50%;
width: 88px; height: 88px;
border-radius: 50%;
background: var(--accent, var(--red, #e53935));
color: #fff;
display: flex; align-items: center; justify-content: center;
pointer-events: none;
opacity: 0;
z-index: 9000;
box-shadow: 0 8px 28px color-mix(in srgb, var(--accent, var(--red, #e53935)) 55%, transparent);
transition: opacity 0.18s ease,
transform 0.26s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.18s ease;
}
/* Off-screen start position depends on which side the chip is on so
the X slides in from the opposite edge with a snappy overshoot. */
#dock-trash-zone[data-side="top"] { transform: translateX(-50%) translateY(-180%) scale(0.7); }
#dock-trash-zone[data-side="bottom"] { transform: translateX(-50%) translateY(180%) scale(0.7); }
#dock-trash-zone.visible {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
#dock-trash-zone.engaged {
transform: translateX(-50%) translateY(0) scale(1.22);
box-shadow: 0 0 0 14px color-mix(in srgb, var(--accent, var(--red, #e53935)) 22%, transparent),
0 12px 36px color-mix(in srgb, var(--accent, var(--red, #e53935)) 60%, transparent);
}
/* Whirlpool ring — fades in when chip is in capture range and spins
continuously, then bursts on drop. */
#dock-trash-zone .whirlpool {
position: absolute; inset: -8px;
border-radius: 50%;
border: 2px solid transparent;
border-top-color: rgba(255,255,255,0.9);
border-right-color: rgba(255,255,255,0.55);
border-bottom-color: rgba(255,255,255,0.25);
opacity: 0;
pointer-events: none;
animation: whirlpool-spin 0.85s linear infinite;
transition: opacity 0.15s ease;
}
#dock-trash-zone.engaged .whirlpool { opacity: 1; }
#dock-trash-zone.dropping .whirlpool {
opacity: 1;
animation: whirlpool-burst 0.36s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes whirlpool-spin { to { transform: rotate(360deg); } }
@keyframes whirlpool-burst {
0% { transform: rotate(0deg) scale(1); opacity: 1; }
60% { transform: rotate(540deg) scale(1.5); opacity: 0.6; }
100% { transform: rotate(900deg) scale(2.2); opacity: 0; }
}
/* Email chip badges:
- email-lib-modal chip: "1" badge when an email is expanded
inside (JS sets data-has-expanded).
- email-reader-* chips: auto-numbered 1, 2, 3 … via CSS counter,
so multiple opened-email windows are visually distinguishable. */
.minimized-dock-chip[data-modal-id="email-lib-modal"],
.minimized-dock-chip[data-modal-id^="email-reader-"] {
position: relative;
}
.minimized-dock-chip[data-modal-id^="email-reader-"][data-tab-num]::after {
content: attr(data-tab-num);
position: absolute;
top: -4px;
right: -4px;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: var(--accent, var(--red));
color: #fff;
font-size: 9px;
font-weight: 700;
line-height: 16px;
text-align: center;
border-radius: 8px;
box-shadow: 0 0 0 2px var(--bg);
pointer-events: none;
}
.minimized-dock-chip[data-modal-id="email-lib-modal"][data-email-unread-label]::after {
content: attr(data-email-unread-label);
position: absolute;
top: -6px;
right: 10px;
height: 16px;
padding: 0 6px;
background: var(--accent, var(--red));
color: #fff;
font-size: 9px;
font-weight: 700;
line-height: 16px;
white-space: nowrap;
border-radius: 8px;
box-shadow: 0 0 0 2px var(--bg);
pointer-events: none;
}
.minimized-dock-x {
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; border-radius: 50%;
font-size: 14px; line-height: 1; opacity: 0.4;
margin-left: 3px;
}
.minimized-dock-x:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 12%, transparent); }
@keyframes dock-chip-in {
from { opacity: 0; transform: translateY(20px) scale(0.85); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.rail-new-chat svg { transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); }
.rail-new-chat:hover svg { transform: rotate(90deg); }
/* A "New" label slides out from the right of the + as it spins, so users
discover what the icon does without needing the tooltip. */
.rail-new-chat { position: relative; }
.rail-new-chat::after {
content: 'New';
position: absolute;
left: 100%;
top: 50%;
margin-left: 6px;
transform: translateY(-50%) translateX(-6px);
opacity: 0;
pointer-events: none;
white-space: nowrap;
font-size: 11px;
font-weight: 600;
color: var(--fg);
background: var(--panel);
padding: 2px 6px;
border-radius: 4px;
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
z-index: 20;
transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.rail-new-chat:hover::after {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
#rail-admin,
#rail-settings { color: var(--fg); }
.rail-separator { width: 20px; height: 1px; background: var(--border); margin: 4px auto; }
.rail-dynamic {
position: relative;
}
.rail-dynamic::after {
content: '';
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--accent, var(--red));
opacity: 0.7;
}
/* ── Sidebar sections (flat layout) ── */
.section { padding: 0; border: none; background: none; border-radius: 0; box-shadow: none; margin: 0; }
/* Section header row — identical sizing to .list-item */
.section-header-flex {
display: flex; align-items: center; gap: 6px;
padding: 8px 8px; margin: 1px 0; border-radius: 4px;
background: transparent; cursor: pointer;
transition: background 0.08s;
height: 29px;
box-sizing: border-box;
}
.section-header-flex:hover { background: color-mix(in srgb, var(--red) 8%, transparent); }
.section-header-flex::after { content: none; }
.section-header-flex h4::before { content: none; }
/* Section title text — same font as .list-item .grow */
.section-header-flex h4,
.section-header-flex .section-title {
flex: 1; cursor: pointer; user-select: none;
display: flex; align-items: center; gap: 6px;
margin: 0; padding: 0; border: none;
font-size: 10px; font-weight: 400; font-family: inherit;
line-height: 1; letter-spacing: 0; text-transform: none;
color: var(--fg);
}
.section-icon,
.sidebar-action-icon {
flex-shrink: 0;
stroke: var(--accent, var(--red));
position: relative;
left: -1px;
color: var(--accent, var(--red));
}
/* Shared notification dot for sidebar section titles. Single source of
truth so chats / email / assistant / future-section dots all sit at
the same offset from their label. */
.sidebar-notif-dot {
display: inline-block;
width: 6px;
height: 6px;
/* Push to the right edge of the flex section-title so chats / email
/ assistant dots all line up vertically in the same column
instead of trailing right after each (differently-sized) label. */
margin-left: auto;
border-radius: 50%;
background: var(--accent, var(--red));
flex-shrink: 0;
vertical-align: middle;
}
/* The email notification gets a soft breathing glow so new-mail catches
the eye without being shouty. Vertical alignment stays with the
inherited .sidebar-notif-dot rule so it lines up with chats/assistant. */
#email-unread-dot.sidebar-notif-dot {
animation: email-notif-breathe 2.2s ease-in-out infinite;
/* Nudge in from the far-right edge so it doesn't crowd the corner. */
margin-right: 4px;
/* Tiny vertical nudge to center with the email label. */
position: relative;
top: 0.1px;
}
@keyframes email-notif-breathe {
0%, 100% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 60%, transparent);
opacity: 0.85;
}
50% {
box-shadow: 0 0 6px 2px color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
#email-unread-dot.sidebar-notif-dot { animation: none; }
}
@media (max-width: 768px) {
/* Nudge the sidebar notification dot up 1px on mobile so it lines
up with the bigger section titles. vertical-align:middle drifts
a hair low against the larger touch-friendly text. */
.sidebar-notif-dot {
position: relative;
top: -1px;
}
/* Cookbook's sidebar status dot carries an inline top:-1px, so override
with the ID + !important to nudge it 2px left / 1px up on mobile. */
#cookbook-notif-dot {
left: -1px !important;
top: -2px !important;
}
}
#sidebar-new-chat-btn svg {
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#sidebar-new-chat-btn:hover svg {
transform: rotate(90deg);
}
/* Sort/select buttons in header — compact */
.section-header-flex > div { display: flex; align-items: center; gap: 2px; }
.section-header-btn {
all: unset;
cursor: pointer; opacity: 0.4; padding: 1px 3px; border-radius: 4px;
display: inline-flex; align-items: center; justify-content: center;
transition: opacity 0.08s, background 0.08s;
}
.section-header-btn:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 7%, transparent); }
.section-header-btn.active { opacity: 0.9; color: var(--accent); }
.section-header-btn svg { width: 12px; height: 12px; }
/* Chats library — grid icon, hover-reveal so the header only toggles collapse */
#sessions-section .chats-manage-btn {
opacity: 0;
transition: opacity 0.12s, background 0.08s;
}
#sessions-section .section-header-flex:hover .chats-manage-btn,
#sessions-section .chats-manage-btn:hover,
#sessions-section .chats-manage-btn:focus-visible {
opacity: 0.45;
}
#sessions-section .chats-manage-btn:hover,
#sessions-section .chats-manage-btn:focus-visible {
opacity: 1;
}
@media (hover: none) {
#sessions-section .chats-manage-btn { opacity: 0.35; }
#sessions-section .chats-manage-btn:active { opacity: 1; }
}
/* Collapse chevron */
.section-collapse-btn {
all: unset;
cursor: pointer; display: inline-flex; align-items: center; padding: 0 2px; border-radius: 4px;
}
.section-collapse-btn:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.section-collapse-chevron { display: inline-flex; opacity: 0.3; transition: transform 0.2s, opacity 0.15s; }
.section-collapse-btn:hover .section-collapse-chevron { opacity: 0.6; }
.section.collapsed .section-collapse-chevron { transform: rotate(-90deg); opacity: 0.5; }
.section-header-flex:has(.section-header-btn) .section-collapse-btn { display: none; }
.section.collapsed .section-collapse-btn { display: inline-flex !important; }
/* Collapsed state */
.section.collapsed > *:not(h4):not(.section-title):not(.section-header-flex) { display: none !important; }
.section.collapsed .section-header-flex { margin-bottom: 0; }
.section.collapsed .section-header-btn { display: none; }
.section.collapsed { cursor: pointer; }
/* Domino expand: every time a section goes from .collapsed → open
(toggleCollapse in section-management.js adds .section-just-expanded
for ~700ms), the .list-item children cascade in one after another,
same feel as the chat input's tools menu. Each row springs in
from a tiny offset below + scaled-down, staggered by nth-child. */
.section.section-just-expanded .list-item {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.section.section-just-expanded .list-item:nth-child(1) { animation-delay: 0.04s; }
.section.section-just-expanded .list-item:nth-child(2) { animation-delay: 0.08s; }
.section.section-just-expanded .list-item:nth-child(3) { animation-delay: 0.12s; }
.section.section-just-expanded .list-item:nth-child(4) { animation-delay: 0.16s; }
.section.section-just-expanded .list-item:nth-child(5) { animation-delay: 0.20s; }
.section.section-just-expanded .list-item:nth-child(6) { animation-delay: 0.24s; }
.section.section-just-expanded .list-item:nth-child(7) { animation-delay: 0.28s; }
.section.section-just-expanded .list-item:nth-child(8) { animation-delay: 0.32s; }
.section.section-just-expanded .list-item:nth-child(9) { animation-delay: 0.36s; }
.section.section-just-expanded .list-item:nth-child(10) { animation-delay: 0.40s; }
.section.section-just-expanded .list-item:nth-child(11) { animation-delay: 0.44s; }
.section.section-just-expanded .list-item:nth-child(12) { animation-delay: 0.48s; }
@keyframes section-domino-in {
0% { opacity: 0; transform: translateY(8px) translateX(-4px) scale(0.92); }
60% { opacity: 1; }
100% { opacity: 1; transform: translateY(0) translateX(0) scale(1); }
}
/* Domino collapse: when toggleCollapse goes open → closed, JS adds
.section-just-collapsing for ~530ms before flipping in .collapsed.
During that window the items fade/slide DOWN one after another so
you see them peel off instead of vanishing as a block. Uses
nth-last-child so the BOTTOM item leaves first and the cascade
rolls upward — mirrors the "stacked deck" feeling of the open
animation reversed. */
.section.section-just-collapsing .list-item {
animation: section-domino-out 0.22s ease-in forwards;
}
.section.section-just-collapsing .list-item:nth-last-child(1) { animation-delay: 0.00s; }
.section.section-just-collapsing .list-item:nth-last-child(2) { animation-delay: 0.025s; }
.section.section-just-collapsing .list-item:nth-last-child(3) { animation-delay: 0.05s; }
.section.section-just-collapsing .list-item:nth-last-child(4) { animation-delay: 0.075s; }
.section.section-just-collapsing .list-item:nth-last-child(5) { animation-delay: 0.10s; }
.section.section-just-collapsing .list-item:nth-last-child(6) { animation-delay: 0.125s; }
.section.section-just-collapsing .list-item:nth-last-child(7) { animation-delay: 0.15s; }
.section.section-just-collapsing .list-item:nth-last-child(8) { animation-delay: 0.175s; }
.section.section-just-collapsing .list-item:nth-last-child(9) { animation-delay: 0.20s; }
.section.section-just-collapsing .list-item:nth-last-child(10) { animation-delay: 0.225s; }
.section.section-just-collapsing .list-item:nth-last-child(11) { animation-delay: 0.25s; }
.section.section-just-collapsing .list-item:nth-last-child(12) { animation-delay: 0.275s; }
@keyframes section-domino-out {
0% { opacity: 1; transform: translateY(0) translateX(0) scale(1); }
100% { opacity: 0; transform: translateY(6px) translateX(-3px) scale(0.94); }
}
@keyframes spin { to { transform: rotate(360deg); } }
.row { display:flex; gap:6px; align-items:center; }
.list-item:hover,
.models-row:hover {
background: color-mix(in srgb, var(--red) 8%, transparent);
border-color: var(--red);
}
/* Disabled tool — dimmed to signal its feature is turned off globally */
.list-item.tool-disabled {
opacity: 0.4;
}
.list-item.tool-disabled:hover {
opacity: 0.7;
}
/* Session bulk select mode */
.session-bulk-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border-bottom: 1px solid var(--border);
background: var(--sidebar-bg, var(--panel));
position: sticky;
top: 0;
z-index: 3;
font-size: 11px;
}
.session-bulk-bar.hidden { display: none; }
#session-select-all-dot {
display: inline-block;
position: relative;
top: -2px;
}
.session-bulk-btn {
background: none;
border: none;
border-radius: 4px;
color: var(--fg);
padding: 4px;
font-family: inherit;
cursor: pointer;
opacity: 0.4;
transition: opacity 0.1s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.session-bulk-btn:hover { opacity: 1; }
.session-bulk-btn-danger { color: var(--red); opacity: 0.5; }
.session-bulk-btn-danger:hover { opacity: 1; }
/* Checkbox in select mode */
.session-select-cb {
accent-color: var(--accent, var(--red));
margin: 0 4px 0 0;
flex-shrink: 0;
cursor: pointer;
}
.session-icon,
.model-icon {
flex-shrink: 0;
opacity: 0.35;
display: inline-flex;
align-items: center;
}
.session-icon.has-docs {
color: inherit;
opacity: 0.5;
}
.session-star {
width: 10px; height: 10px;
border-radius: 50%;
border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent);
flex-shrink: 0;
position: relative;
}
.session-star.processing {
animation: research-pulse 1.5s ease-in-out infinite;
}
.session-star.notify {
animation: research-done-pulse 1.2s ease-in-out 5;
opacity: 1;
/* Bigger, solid status dot so "research done" reads clearly. Use the
defined accent (bare --accent is undefined here → no colour). */
width: 14px; height: 14px;
background: var(--accent-primary, var(--red));
border-color: var(--accent-primary, var(--red));
}
/* The dot doubles as the provider-logo holder; hide that logo in the
done state so the solid notif dot doesn't collide with the SVG behind it. */
.session-star.notify svg { display: none; }
@keyframes research-done-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.7; }
}
.session-fav {
flex-shrink: 0;
cursor: pointer;
color: var(--red);
opacity: 0.6;
display: inline-flex;
align-items: center;
transition: opacity 0.1s;
}
.session-fav:hover {
opacity: 1;
}
.list-item.active-session {
background: color-mix(in srgb, var(--red) 10%, transparent);
border-color: var(--red);
border-left-color: var(--red);
}
.list-item .provider-logo { opacity: 0.4 !important; }
.list-item:has(.session-star.processing) .provider-logo { opacity: 1 !important; }
.list-item.stream-complete .provider-logo { opacity: 1 !important; }
.list-item.active {
background: color-mix(in srgb, var(--red) 10%, transparent);
border-color: var(--red);
border-left-color: var(--red);
}
/* Only show the red focus ring for keyboard navigation (Tab). Mouse
clicks shouldn't leave a sticky red outline on a sidebar item. */
.list-item:focus { outline: none; }
.list-item:focus-visible {
outline: none;
border-color: var(--red, var(--color-error));
box-shadow: 0 0 0 1px var(--red, var(--color-error));
}
/* Drag handles hidden by default, shown via body.rearrange-mode */
.drag-handle, .item-drag-handle, .folder-drag-handle { display: none; }
body.rearrange-mode .drag-handle,
body.rearrange-mode .item-drag-handle,
body.rearrange-mode .folder-drag-handle { display: inline; }
/* Drag sorting styles for list items */
.item-drag-handle {
cursor: grab;
opacity: 0.4;
font-size: 10px;
padding: 0 4px;
user-select: none;
letter-spacing: -2px;
transition: opacity 0.15s;
}
.item-drag-handle:hover {
opacity: 1;
color: var(--red);
}
.item-drag-handle:active {
cursor: grabbing;
}
.list-item.dragging,
.models-row.dragging,
.session-folder.dragging {
opacity: 0.95;
box-shadow: 0 8px 24px color-mix(in srgb, var(--red) 30%, transparent);
border-color: var(--red);
background: var(--panel);
cursor: grabbing;
}
.list-item.dragging .item-drag-handle,
.models-row.dragging .item-drag-handle,
.session-folder.dragging .folder-drag-handle {
opacity: 1;
color: var(--red);
}
/* ── Model category grouping ── */
.models-category-header {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; cursor: pointer;
font-size: 0.85em; font-weight: 600;
color: color-mix(in srgb, var(--fg) 55%, transparent);
border-radius: 4px; user-select: none;
margin-top: 4px;
transition: opacity 0.08s, background 0.08s, color 0.08s;
}
.models-category-header:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); }
.models-category-header .folder-count { font-weight: 400; opacity: 0.5; font-size: 0.9em; }
.models-endpoint-label {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; cursor: pointer;
font-size: 0.85em; font-weight: 600;
color: color-mix(in srgb, var(--fg) 55%, transparent);
border-radius: 4px; user-select: none;
transition: opacity 0.08s, background 0.08s;
}
.models-endpoint-label:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); }
.models-endpoint-label .folder-count { font-weight: 400; opacity: 0.5; }
.models-group-content.indented {
padding-left: 4px;
}
.models-group-content.indented .models-row { border-bottom: 1px solid color-mix(in srgb, var(--fg) 7%, transparent); }
.models-group-content.indented .models-row:last-child { border-bottom: none; }
/* ── Session folders ── */
.session-folder { margin: 2px 0; }
.session-folder-header {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; cursor: pointer;
font-size: 0.88em; font-weight: 600;
color: color-mix(in srgb, var(--fg) 55%, transparent);
border-radius: 4px; user-select: none;
transition: opacity 0.08s, background 0.08s;
}
.session-folder-header:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 4%, transparent); }
.session-folder-header .folder-count { font-weight: 400; opacity: 0.5; }
.session-folder-header.drag-over {
outline: 2px dashed var(--red); background: color-mix(in srgb, var(--red) 13%, transparent); opacity: 1;
}
.session-folder.dragging-folder { opacity: 0.4; }
.folder-toggle { font-size: 0.7em; width: 10px; text-align: center; }
.folder-name { font-weight: inherit; flex: 1; }
.folder-count { font-size: 0.85em; opacity: 0.5; }
.folder-drag-handle {
cursor: grab; opacity: 0.3; font-size: 0.8em; padding: 0 2px;
transition: opacity 0.08s;
}
.session-folder-header:hover .folder-drag-handle { opacity: 0.7; }
.folder-delete-btn {
background: none; border: none; color: var(--fg); opacity: 0;
cursor: pointer; font-size: 1.1em; padding: 0 2px; line-height: 1;
min-height: 0; height: auto;
transition: opacity 0.08s, color 0.08s;
}
.session-folder-header:hover .folder-delete-btn { opacity: 0.5; }
.folder-delete-btn:hover { opacity: 1 !important; color: var(--color-error); }
.session-folder-content { padding-left: 4px; }
.session-folder-content .list-item { border-bottom: 1px solid color-mix(in srgb, var(--fg) 7%, transparent); }
.session-folder-content .list-item:last-child { border-bottom: none; }
.session-show-more-btn {
display: block;
width: 100%;
background: none;
border: none;
color: var(--fg);
opacity: 0.4;
font-size: 0.8em;
padding: 6px 8px;
cursor: pointer;
text-align: center;
transition: opacity 0.15s;
}
.session-show-more-btn:hover { opacity: 0.8; }
.drag-folder-placeholder {
height: 4px; margin: 2px 0; border-radius: 2px;
background: var(--red); opacity: 0.6;
}
.unfiled-drop-zone {
min-height: 8px; border-radius: 4px; transition: all 0.15s;
}
.unfiled-drop-zone.drag-over {
min-height: 24px; outline: 2px dashed var(--red);
background: color-mix(in srgb, var(--red) 9%, transparent);
}
/* Mobile swipe-to-delete */
.swipe-delete-action {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 60px;
background: color-mix(in srgb, var(--color-error) 12%, transparent);
color: var(--color-error);
display: flex;
align-items: center;
justify-content: center;
border-radius: 0 4px 4px 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
cursor: pointer;
z-index: 1;
}
.swipe-delete-action::before {
content: '';
display: block;
width: 18px;
height: 18px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23ff4444' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 6 5 6 21 6'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
background-size: contain;
background-repeat: no-repeat;
}
.swipe-delete-action:active {
background: color-mix(in srgb, var(--color-error) 22%, transparent);
}
.drag-placeholder {
background: color-mix(in srgb, var(--red) 10%, transparent);
border: 2px dashed color-mix(in srgb, var(--color-accent) 40%, transparent);
border-radius: 6px;
margin: 4px 0;
transition: height 0.15s ease;
}
.list-item,
.models-row {
display: flex;
gap: 6px;
align-items: center;
padding: 3px 8px;
margin: 0;
border-radius: 4px;
border: none;
line-height: 1;
font-size: 13px;
background: transparent;
transition: background 0.08s;
cursor: pointer;
touch-action: pan-y;
}
.list-item button {
margin: 0;
height: 24px;
padding: 0 8px;
font-size: 9px;
}
/* Inline "+" action on a tool/section row (Library → new document,
Email → compose). Sizing forced + min-* zeroed so the mobile
touch-target rule (44px) can't inflate it. Selector intentionally
NOT scoped to `.list-item` so the same class also styles buttons
sitting in `.section-header-flex` (e.g. #email-compose-btn). */
.list-item-plus-btn {
all: unset;
box-sizing: border-box;
flex-shrink: 0;
position: relative;
left: 4px;
display: inline-flex !important;
align-items: center;
justify-content: center;
height: 14px !important;
min-height: 0 !important;
width: auto !important;
min-width: 0 !important;
padding: 0 5px !important;
border-radius: 4px;
color: var(--fg);
/* Always visible at rest. On hover (devices that have it) the + spins
90° and "new" reveals to its right — neat expand affordance. */
opacity: 1 !important;
cursor: pointer;
gap: 0;
overflow: visible;
z-index: 1;
transition: background 0.12s, color 0.12s, gap 0.18s ease;
}
.list-item-plus-btn svg.list-item-plus-icon {
width: 13px; height: 13px;
transition: transform 0.22s ease;
}
/* Legacy fallback for plus-btns without the `.list-item-plus-icon` class. */
.list-item-plus-btn svg:not(.list-item-plus-icon) { width: 13px; height: 13px; }
/* Label is absolutely positioned to the LEFT of the icon so the + stays
put — it just appears beside it on hover instead of pushing it. The
button's intrinsic width remains the icon, so neighbours don't shift. */
.list-item-plus-label {
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
padding-right: 5px;
opacity: 0;
pointer-events: none;
white-space: nowrap;
font-size: 9.5px;
line-height: 1;
letter-spacing: 0.02em;
transition: opacity 0.18s ease, transform 0.22s ease;
}
@media (hover: hover) {
.list-item-plus-btn:hover .list-item-plus-icon {
transform: rotate(90deg);
}
.list-item-plus-btn:hover .list-item-plus-label {
opacity: 1;
transform: translateY(-50%) translateX(0);
pointer-events: auto;
}
.list-item-plus-btn .list-item-plus-label {
transform: translateY(-50%) translateX(6px);
}
}
.list-item-plus-btn:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
color: var(--accent, var(--red));
}
.list-item span {
color: var(--fg);
font-size: 9.75px;
}
.grow {
flex: 1;
overflow: hidden;
text-overflow: clip;
white-space: nowrap;
}
input, textarea, button, select {
background:var(--bg);
color:var(--fg);
border:1px solid var(--border);
font-family:inherit;
padding:6.9px;
border-radius: 4px;
transition: background 0.08s, border-color 0.08s, color 0.08s, opacity 0.08s;
}
input:hover, textarea:hover, button:hover, select:hover {
border-color: var(--fg);
background-color: var(--panel);
}
:root.light input,
:root.light textarea,
:root.light button,
:root.light select {
background:#eaeaea;
color-scheme: light;
}
input[type="text"] { height:32px; width:100%; }
textarea { width:100%; min-height:32px; height:auto; max-height:30lh; overflow-y:auto; resize:none; }
button { height:32px; padding:0 10px; }
#chat-form button[type="submit"] { height:38px; }
select { height:32px; color-scheme: dark; }
.chat-container {
flex:1;
display:flex;
flex-direction:column;
padding:0 16px;
overflow:hidden;
position:relative;
min-height:0;
min-width:0;
margin-top:8px;
margin-bottom: 0;
}
.chat-meta { font-size:12px; color:color-mix(in srgb, var(--fg) 60%, transparent); margin-bottom:6px; }
.chat-history {
flex:1;
overflow-y:auto;
overflow-x:hidden;
overscroll-behavior-y: none;
margin-bottom:8px;
white-space:normal;
min-height:0;
--chat-max: 800px;
padding-left: max(0px, calc((100% - var(--chat-max)) / 2));
padding-right: max(12px, calc((100% - var(--chat-max)) / 2 + 12px));
}
/* Welcome screen — centered in available space above input bar */
#welcome-screen {
position:absolute;
top:40%;
left:50%;
transform:translate(-50%,-50%);
display:flex;
flex-direction:column;
align-items:center;
text-align:center;
pointer-events:none;
animation: welcome-enter 0.4s ease-out both;
transition: top 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, transform 0.3s ease;
}
#welcome-screen .welcome-tip,
#welcome-screen .welcome-sub,
#welcome-screen .welcome-version {
transition: opacity 0.25s ease, max-height 0.25s ease, margin 0.25s ease;
max-height: 60px;
overflow: hidden;
}
@media (max-height: 650px) {
#welcome-screen { top: 28%; }
#welcome-screen .welcome-tip { opacity: 0; max-height: 0; margin: 0; }
#welcome-screen .welcome-version { margin-top: 6px; }
.incognito-btn { margin-top: 8px; }
}
@media (max-height: 500px) {
#welcome-screen { top: 22%; }
#welcome-screen .welcome-name { font-size: 1.4rem; margin-bottom: 0; }
#welcome-screen .welcome-sub { opacity: 0; max-height: 0; margin: 0; }
.incognito-btn { margin-top: 4px !important; }
}
@media (max-height: 380px) {
#welcome-screen { opacity: 0; pointer-events: none; }
}
#welcome-screen.hidden { display:none; }
#welcome-screen.kb-hidden {
opacity: 0;
pointer-events: none;
transform: translate(-50%, -50%) scale(0.95);
}
@media (max-width: 768px) {
#welcome-screen { top: 42%; }
#welcome-screen .welcome-name { margin-bottom: 2px; }
#welcome-screen .welcome-sub { margin-bottom: 0; }
#welcome-screen .incognito-btn { margin-top: 6px; }
}
#welcome-screen .welcome-name {
font-size:2.2rem;
font-weight:700;
background: linear-gradient(135deg, var(--brand-color, var(--red)), color-mix(in srgb, var(--brand-color, var(--red)) 60%, var(--fg)));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
letter-spacing:0.03em;
margin-bottom:10px;
}
.welcome-boat {
width: 1.8rem;
height: 1.8rem;
margin-right: 0.4rem;
vertical-align: -0.15em;
color: var(--brand-color, var(--red));
}
#welcome-screen .welcome-sub {
font-size:0.85rem;
color:color-mix(in srgb, var(--fg) 60%, transparent);
opacity:0.6;
line-height:1.5;
max-width:300px;
}
#welcome-screen .welcome-version {
font-size:0.7rem;
opacity:0.25;
margin-top:12px;
pointer-events:none;
user-select:none;
}
#welcome-screen .welcome-tip {
font-size:0.75rem;
color:var(--fg);
opacity:0.2;
margin-top:24px;
max-width:320px;
line-height:1.4;
}
/* Incognito toggle — on welcome screen */
.incognito-btn {
pointer-events: auto;
background: none;
border: 1px solid var(--border);
color: var(--fg);
opacity: 0.25;
cursor: pointer;
padding: 6px 12px;
border-radius: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.15s;
margin-top: 16px;
font-family: inherit;
font-size: 11px;
}
.incognito-btn:hover {
opacity: 0.6;
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.incognito-btn.active {
opacity: 1;
color: var(--accent);
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.incognito-btn.active .eye-open { display: none; }
.incognito-btn.active .eye-blinded { display: inline !important; }
.incognito-label {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
}
/* When welcome is active, push input bar up toward center */
.chat-container .chat-input-bar {
transition: margin 0.3s ease, max-width 0.3s ease;
}
.chat-container.welcome-active .chat-input-bar {
margin-bottom:30vh;
margin-left:auto;
margin-right:auto;
max-width: 800px;
width: 100%;
}
@media (max-width:768px) {
#welcome-screen .welcome-name { font-size:1.8rem; }
#welcome-screen .welcome-sub { font-size:0.8rem; }
.chat-container.welcome-active .chat-input-bar {
margin-bottom:0;
margin-left:0;
margin-right:0;
}
}
.msg {
margin: 8px 0;
position: relative;
display: flex;
flex-direction: column;
width: fit-content;
max-width: 85%;
min-width: 80px;
border-radius: 12px;
padding: 10px 12px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
animation: msg-enter 0.3s ease-out both;
}
.msg-user {
align-items: flex-end;
margin-left: auto;
margin-right: 8px;
background: var(--user-bubble-bg, color-mix(in srgb, var(--fg) 8%, var(--bg)));
border: 1px solid var(--bubble-border, var(--border));
border-radius: 18px 18px 0 18px;
align-self: flex-end;
width: fit-content;
max-width: 85%;
min-width: 80px;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
}
.msg-ai {
align-items: flex-start;
margin-right: auto;
margin-left: 8px;
background: var(--ai-bubble-bg, var(--panel));
border: 1px solid var(--bubble-border, var(--border));
border-radius: 18px 18px 18px 0;
align-self: flex-start;
width: 85%;
max-width: 85%;
min-width: 80px;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
transition: min-height 0.25s ease-out;
}
.msg .role {
font-weight: 600;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.msg .role::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--model-dot, color-mix(in srgb, var(--fg) 30%, transparent));
flex-shrink: 0;
}
.msg .role.has-logo::before { display: none; }
.role-provider-logo { display: inline-flex; align-items: center; flex-shrink: 0; }
.role-provider-logo svg { width: 12px; height: 12px; }
.msg-user .role {
color: color-mix(in srgb, var(--fg) 60%, transparent);
}
.msg-user .role::before {
background: color-mix(in srgb, var(--fg) 40%, transparent);
}
.msg .body {
width: 100%;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
line-height: 1.5;
font-size: 0.95em;
margin: 0;
overflow: hidden;
max-width: 100%;
}
.msg .body > * {
margin-top: 8px;
margin-bottom: 8px;
}
.msg .body > *:first-child {
margin-top: 0;
}
.msg .body > *:last-child {
margin-bottom: 0;
}
.msg .body p:empty {
display: none;
}
.msg-user .body {
color: var(--fg);
}
.msg-ai .body {
color: var(--fg);
}
.rag-sources {
margin-top: 12px;
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px;
font-size: 12px;
}
.rag-sources summary {
cursor: pointer;
color: var(--red);
font-weight: bold;
font-size: 11px;
}
.rag-source-item {
margin-top: 8px;
padding: 6px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border-radius: 4px;
border-left: 2px solid var(--red);
}
.rag-similarity {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 10px;
margin-left: 6px;
}
.rag-snippet {
color: color-mix(in srgb, var(--fg) 65%, transparent);
font-size: 11px;
margin-top: 4px;
white-space: pre-wrap;
max-height: 60px;
overflow: hidden;
}
.rag-file-delete {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
cursor: pointer;
font-size: 10px;
padding: 1px 5px;
}
.rag-file-delete:hover { color: var(--fg); border-color: var(--fg); }
.rag-file-size {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 10px;
margin-left: 4px;
}
.rag-upload-zone {
border: 2px dashed var(--border);
border-radius: 6px;
padding: 12px;
text-align: center;
color: color-mix(in srgb, var(--fg) 45%, transparent);
font-size: 11px;
cursor: pointer;
transition: border-color 0.2s;
}
.rag-upload-zone.dragover {
border-color: var(--red);
color: var(--red);
}
.msg .timestamp {
font-size: 10px;
color: color-mix(in srgb, var(--fg) 45%, transparent);
margin-top: 4px;
text-align: right;
opacity: 0.7;
}
.msg-user .timestamp {
color: color-mix(in srgb, var(--fg) 72%, transparent);
}
pre {
overflow: hidden;
padding: 6px 4px 2px 4px;
border: 1px solid var(--border);
background: var(--bg);
position: relative;
line-height: 1.4;
font-size: 0.95em;
font-family: 'Fira Code', 'Courier New', monospace;
min-height: 38px;
max-width: 100%;
margin: 8px 0;
white-space: pre-wrap;
word-break: break-word;
border-radius: 4px;
min-height: 20px;
min-width: 80px;
}
/* Ensure code block headers are only slightly larger than regular text */
pre h1, pre h2, pre h3, pre h4, pre h5, pre h6 {
font-size: 1.1em;
font-weight: bold;
margin: 8px 0 4px 0;
color: var(--fg);
}
.code-lang {
font-size:0.8em;
color:color-mix(in srgb, var(--fg) 60%, transparent);
display:block;
margin-bottom:2px;
font-style:italic;
}
code { font-family:inherit; }
.loading { color:var(--red); font-style:italic; }
#chat-form { display:none; }
/* Unified chat input bar */
.chat-input-bar {
background: var(--input-bg, var(--panel));
border: 1px solid var(--input-border, var(--border));
border-radius: 16px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
width: 100%;
}
.chat-input-top {
width: 100%;
position: relative;
}
.chat-input-top > .model-picker-wrap {
position: absolute;
top: 0;
right: 0;
z-index: 2;
transform-origin: top right;
transition: opacity 0.22s ease, transform 0.22s ease;
will-change: opacity, transform;
}
.chat-input-top > .model-picker-wrap.model-picker-autohide {
opacity: 0;
pointer-events: none;
transform: translateY(-4px) scale(0.96);
}
.ghost-text-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
pointer-events: none;
white-space: pre-wrap;
overflow-wrap: break-word;
font-family: inherit;
font-size: 14px;
line-height: 1.5;
padding: 0;
border: 1px solid transparent;
color: transparent;
z-index: 1;
}
.ghost-text-overlay .ghost-suggestion {
color: color-mix(in srgb, var(--fg) 30%, transparent);
}
.chat-input-bar textarea#message {
width: 100%;
background: transparent;
border: none;
outline: none;
resize: none;
font-size: 14px;
line-height: 1.5;
color: var(--fg);
min-height: 24px;
max-height: 200px;
padding: 0;
font-family: inherit;
transition: height 0.12s ease-out;
}
.chat-input-bar textarea#message::placeholder {
color: color-mix(in srgb, var(--fg) 35%, transparent);
}
.chat-input-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 4px;
}
/* Progressive shrinkage: as the chat input bar gets narrow, sacrifice
chrome before the typing area. First the Agent/Chat toggle drops out,
then the model picker — textarea + send button always stay. Container
queries so it responds to *bar width*, not viewport width (the bar
can be in a split pane). */
.chat-input-bar { container-type: inline-size; container-name: chatbar; }
@container chatbar (max-width: 340px) {
.chat-input-right .mode-toggle { display: none !important; }
}
@container chatbar (max-width: 260px) {
#model-picker-wrap { display: none !important; }
}
.chat-input-left {
display: flex;
gap: 4px;
align-items: center;
flex-wrap: nowrap;
overflow: hidden;
min-width: 0;
flex: 1;
}
/* Collapsible buttons shrink away; collapsed ones disappear */
.chat-input-left > .input-icon-btn {
flex-shrink: 0;
}
.chat-input-left > .input-icon-btn.toolbar-collapsed {
display: none !important;
}
/* Always keep overflow wrapper visible and leftmost */
.chat-input-left > .overflow-wrapper {
flex-shrink: 0;
position: relative;
z-index: 1;
}
.input-divider {
width: 1px;
height: 16px;
background: var(--border);
opacity: 0.4;
margin: 0 2px;
flex-shrink: 0;
}
.chat-input-right {
display: flex;
gap: 8px;
align-items: center;
flex-shrink: 0;
}
.input-icon-btn {
background: none;
border: none;
color: var(--fg);
opacity: 0.5;
cursor: pointer;
padding: 6px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s, background 0.15s, color 0.15s;
position: relative;
}
.input-icon-btn:hover {
opacity: 0.8;
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.input-icon-btn.active,
.input-icon-btn.expanded {
opacity: 1;
color: var(--fg);
background: color-mix(in srgb, var(--fg) 9%, transparent);
}
/* While the menu is open the chevron stays in its highlighted state
— don't run the opacity fade transition so we never flash from
0.5 → hover-1.0 → drop-back. The state holds steady. */
.input-icon-btn.expanded { transition: none; }
/* Accent color for Research, Compare toolbar indicators */
#research-toggle-btn.active,
.tool-indicator.active {
color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
}
/* Research button glow while actively running */
#research-toggle-btn.research-running {
opacity: 1 !important;
color: var(--red);
animation: research-pulse 2s ease-in-out infinite;
}
@keyframes research-pulse {
0%, 100% { background: color-mix(in srgb, var(--red) 12%, transparent); }
50% { background: color-mix(in srgb, var(--red) 22%, transparent); }
}
.tool-indicator-x {
margin-left: 2px;
opacity: 0.4;
flex-shrink: 0;
transition: opacity 0.15s;
}
.tool-indicator:hover .tool-indicator-x {
opacity: 1;
}
/* Character indicator — hide name text below 768px, icon always visible */
@media (max-width: 768px) {
#character-indicator-name { display: none !important; }
}
/* Document indicator — hidden by default, shown when docs exist */
#doc-indicator-btn { display: none !important; }
#doc-indicator-btn.visible { display: inline-flex !important; }
/* On mobile, the minimized-dock chip replaces this indicator — keep
it hidden regardless of `.visible`. */
@media (max-width: 768px) {
#doc-indicator-btn,
#doc-indicator-btn.visible { display: none !important; }
}
.doc-indicator-active {
color: var(--accent, var(--accent-primary, var(--red))) !important;
opacity: 1 !important;
}
#doc-indicator-btn.active {
color: var(--accent, var(--accent-primary, var(--red))) !important;
opacity: 1 !important;
background: color-mix(in srgb, var(--fg) 9%, transparent) !important;
}
#overflow-doc-btn.has-docs,
#overflow-doc-btn.active {
color: var(--accent, var(--red));
opacity: 1;
}
#overflow-doc-btn.has-docs.active {
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.send-btn {
/* Prefer the theme accent (--red). A stored custom `--send-btn-bg`
override only kicks in if it's set to a non-white value — guards
against ChatGPT-style themes where the stored override leaked
to white and rendered the send button invisible. */
background: var(--send-btn-bg, var(--red));
color: #fff;
border: none;
border-radius: 8px;
min-width: 32px;
width: 32px;
height: 32px !important;
padding: 0;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.25s, border-color 0.25s, color 0.25s, width 0.3s cubic-bezier(0.34, 1, 0.64, 1), padding 0.3s, border-radius 0.3s, opacity 0.1s;
flex-shrink: 0;
overflow: hidden;
}
/* Instant feedback while the send handler is spinning up before streaming begins */
.send-btn.send-pending {
opacity: 0.55;
pointer-events: none;
animation: send-pending-pulse 0.9s ease-in-out infinite;
}
@keyframes send-pending-pulse {
0%, 100% { opacity: 0.55; }
50% { opacity: 0.85; }
}
/* Send button icon transitions — send mode forces no animation */
.send-btn[data-mode="send"] svg { animation: none !important; }
/* Spin out: + rotates away, then arrow spins in via anim-spin */
.send-btn.anim-spin-swap svg { animation: btn-spin-out 0.15s ease-in forwards; }
.send-btn.anim-spin-swap .send-btn-label { opacity: 0; transition: opacity 0.1s; }
@keyframes btn-spin-out {
0% { transform: rotate(0deg) scale(1); opacity: 1; }
100% { transform: rotate(90deg) scale(0); opacity: 0; }
}
.send-btn.anim-spin svg { animation: btn-spin-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
.send-btn.anim-launch svg { animation: btn-launch 0.3s cubic-bezier(0.6, 0, 0.4, 1) forwards; }
.send-btn.anim-land svg { animation: btn-land 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
@keyframes btn-spin-in {
0% { transform: rotate(-180deg) scale(0); opacity: 0; }
100% { transform: rotate(0deg) scale(1); opacity: 1; }
}
/* Arrow flies UP and OUT of the button before the stop icon lands in.
Stays opaque most of the way so it reads as a real launch instead of a
fade-in-place. .anim-launch temporarily lifts the button's overflow so
the icon escapes the 32px box. */
@keyframes btn-launch {
0% { transform: translateY(0) scale(1); opacity: 1; }
70% { transform: translateY(-30px) scale(0.95); opacity: 1; }
100% { transform: translateY(-58px) scale(0.55); opacity: 0; }
}
.send-btn.anim-launch { overflow: visible !important; }
.send-btn.anim-launch svg { transform-origin: 50% 50%; }
@keyframes btn-land {
0% { transform: translateY(10px) scale(0.3); opacity: 0; }
100% { transform: translateY(0) scale(1); opacity: 1; }
}
/* Stop button — Processing: Siren pulse, Receiving: Quarter turn spin */
.send-btn[data-mode="streaming"][data-phase="processing"] svg {
animation: siren-icon 1.5s ease-in-out infinite;
}
.send-btn[data-mode="streaming"][data-phase="receiving"] svg {
animation: quarter-turn 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes siren-icon {
0%, 100% { transform: scale(1); }
50% { transform: scale(0.8); }
}
@keyframes quarter-turn {
0%, 15% { transform: rotate(0deg); }
25%, 40% { transform: rotate(90deg); }
50%, 65% { transform: rotate(180deg); }
75%, 90% { transform: rotate(270deg); }
100% { transform: rotate(360deg); }
}
.send-btn:hover {
background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white));
color: #fff;
}
.send-btn.mic-mode,
.send-btn.newchat-mode {
/* Idle / new-chat / mic states blend the picked Send Btn color into
the panel so the user's color choice is visible across every state,
not only briefly while typing. */
background: color-mix(in srgb, var(--send-btn-bg, var(--red)) 30%, var(--panel, #2a2a2a));
color: var(--fg);
border: 1px solid var(--border);
transition: width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s, border-color 0.25s, color 0.25s, border-radius 0.3s;
}
.send-btn.mic-mode:hover,
.send-btn.newchat-mode:hover {
background: color-mix(in srgb, var(--send-btn-hover, var(--send-btn-bg, var(--red))) 85%, var(--panel, #2a2a2a));
color: #fff;
}
/* Hover: just spin the + 90° (no size change). The "New chat" affordance
is the tooltip (title="New chat") plus the icon rotation.
Gated on data-mode="newchat" so the arrow variant (empty-session state
which keeps the newchat-mode class but shows the send arrow) does NOT
rotate on hover. */
.send-btn[data-mode="newchat"] svg {
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.send-btn[data-mode="newchat"]:hover svg {
transform: rotate(90deg);
}
/* "New Session" expanding label */
.send-btn-label {
width: 0;
max-width: 0;
overflow: hidden;
white-space: nowrap;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
opacity: 0;
padding: 0;
margin: 0;
transition: max-width 0.35s cubic-bezier(0.34, 1.2, 0.64, 1), width 0.35s, opacity 0.25s, margin-left 0.25s;
}
.send-btn.newchat-expanded .send-btn-label {
width: auto;
max-width: 50px;
opacity: 1;
margin-left: 4px;
}
.send-btn.newchat-expanded {
width: 68px;
padding: 0 10px 0 8px;
}
.send-btn.recording {
background: var(--red) !important;
color: #fff !important;
border: none !important;
animation: pulse-recording 1.5s infinite;
}
@keyframes pulse-recording {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Mode toggle — Agent / Chat */
.mode-toggle {
display: flex;
flex-shrink: 0;
height: 28px;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
position: relative;
}
.mode-toggle::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
background: color-mix(in srgb, var(--fg) 10%, transparent);
border-radius: 9px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 0;
}
.mode-toggle.mode-chat::before {
transform: translateX(100%);
}
#mode-agent-btn {
border-radius: 10px 0 0 10px;
}
#mode-chat-btn {
border-radius: 0 10px 10px 0;
}
.mode-toggle-btn {
background: none;
border: none;
color: color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
padding: 0 10px;
font-size: 11px;
font-weight: 500;
font-family: inherit;
transition: color 0.2s;
white-space: nowrap;
height: 100%;
position: relative;
z-index: 1;
}
.mode-toggle-btn:not(.active):hover {
color: color-mix(in srgb, var(--fg) 60%, transparent);
}
.mode-toggle-btn:hover {
background: none !important;
border-color: transparent !important;
}
.mode-toggle-btn.active,
.mode-toggle-btn.active:hover,
.mode-toggle-btn.active:focus {
color: var(--fg) !important;
background: none !important;
border-color: transparent !important;
cursor: default;
}
.mode-toggle-btn + .mode-toggle-btn {
border-left: none;
}
/* Message count badge in the chat-meta header (next to the title).
Auto-hides when empty so a brand-new chat doesn't show "0 msgs". */
.chat-meta-count {
font-size: inherit;
font-weight: 400;
color: color-mix(in srgb, var(--fg) 35%, transparent);
white-space: nowrap;
user-select: none;
margin-left: 6px;
}
.chat-meta-count:empty { display: none; }
/* Session cost indicator (next to chevron in header) */
.session-cost-display {
font-size: inherit;
font-weight: 400;
color: color-mix(in srgb, var(--fg) 35%, transparent);
white-space: nowrap;
user-select: none;
margin-right: 2px;
}
/* Model picker — input bar drop-up */
.model-picker-wrap {
position: relative;
flex-shrink: 0;
}
.model-picker-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: 21px;
padding: 0 6px;
font-size: 11px;
font-weight: 500;
font-family: inherit;
background: none;
border: 1px solid transparent;
border-radius: 4px;
color: color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.model-picker-btn:hover {
border-color: var(--border);
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg);
}
.model-picker-btn #model-picker-label {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.model-picker-btn svg {
flex-shrink: 0;
opacity: 0.5;
}
.model-picker-logo {
display: inline-flex;
align-items: center;
vertical-align: -2px;
}
.model-picker-logo svg {
width: 12px;
height: 12px;
opacity: 0.7;
}
.model-picker-menu {
position: absolute;
bottom: calc(100% + 16px);
right: 0;
z-index: 300;
min-width: 260px;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 -4px 20px rgba(0,0,0,0.4);
padding: 6px;
animation: picker-roll-up 0.2s ease-out;
transform-origin: bottom right;
}
.model-picker-menu.closing {
animation: picker-roll-down 0.15s ease-in forwards;
}
.model-picker-menu.hidden { display: none; }
.model-picker-search-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 30px;
gap: 4px;
align-items: center;
margin-bottom: 4px;
transition: grid-template-columns 0.18s ease, gap 0.18s ease;
}
.model-picker-menu.no-models .model-picker-search-row {
margin-bottom: 0;
}
.model-picker-search-row.searching {
grid-template-columns: minmax(0, 1fr) 0px;
gap: 0;
}
.model-picker-search-row.searching .model-picker-action-btn {
opacity: 0;
transform: translateX(10px) scale(0.88);
pointer-events: none;
}
.model-picker-menu input[type="text"] {
width: 100%;
box-sizing: border-box;
padding: 6px 8px;
font-size: 0.82em;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font-family: inherit;
outline: none;
min-width: 0;
transition: border-color 0.15s, padding 0.18s ease, background 0.18s ease;
}
.model-picker-menu input[type="text"]:focus {
border-color: var(--red);
}
.model-picker-menu input[type="text"]::placeholder {
color: color-mix(in srgb, var(--fg) 30%, transparent);
}
.model-picker-action-btn {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: 1px solid var(--border);
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
color: color-mix(in srgb, var(--fg) 66%, transparent);
cursor: pointer;
padding: 0;
overflow: hidden;
transform: translateX(0) scale(1);
transition: opacity 0.16s ease, transform 0.18s ease, border-color 0.15s, color 0.15s, background 0.15s;
}
.model-picker-action-btn:hover,
.model-picker-action-btn:focus-visible {
border-color: var(--red);
color: var(--fg);
background: color-mix(in srgb, var(--red) 10%, var(--panel));
outline: none;
}
.model-picker-action-btn.primary {
color: var(--red);
background: color-mix(in srgb, var(--red) 8%, transparent);
}
.model-picker-action-btn svg {
width: 14px;
height: 14px;
transition: transform 0.28s ease;
}
.model-picker-action-btn:hover svg,
.model-picker-action-btn:focus-visible svg {
transform: rotate(90deg);
}
.model-picker-list {
max-height: min(280px, 50dvh);
overflow-y: auto;
}
.model-picker-list.is-empty {
max-height: 0;
overflow: hidden;
}
.model-picker-list .model-switch-item {
display: flex;
align-items: center;
padding: 5px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 0.82em;
color: var(--fg);
gap: 6px;
}
.model-picker-list .model-switch-item:hover {
background: color-mix(in srgb, var(--red) 8%, transparent);
}
.model-picker-list .model-switch-empty {
display: flex;
align-items: center;
justify-content: center;
padding: 5px 8px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 0.82em;
}
.model-picker-list .mp-section-label {
font-size: 0.72em;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.4;
padding: 6px 8px 2px;
}
/* Overflow "+" menu */
.overflow-wrapper {
position: relative;
display: flex;
align-items: center;
}
.plus-active-dot {
position: absolute;
top: 2px;
right: 2px;
width: 6px;
height: 6px;
background: var(--fg);
border-radius: 50%;
display: none;
}
.overflow-plus-btn.has-active .plus-active-dot {
display: block;
}
.overflow-menu {
position: fixed;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 4px;
min-width: 170px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
z-index: 1000;
/* Container spring-in: the rounded panel scales/grows out of the
chevron's position before the menu items domino in on top. */
transform-origin: bottom left;
animation: overflow-menu-pop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes overflow-menu-pop {
0% { transform: scale(0.6) translateY(8px); opacity: 0; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
.overflow-menu.hidden {
display: none;
}
/* Closing state: JS adds `.closing` to play the fold-in animation,
waits for it to finish, then flips to `.hidden`. Container
scales/translates back into the chevron while items peel off from
the top down so the menu collapses into its anchor. */
.overflow-menu.closing {
animation: overflow-menu-pop-out 0.22s cubic-bezier(0.5, 0, 0.75, 0) forwards;
animation-delay: 0.16s;
}
@keyframes overflow-menu-pop-out {
0% { transform: scale(1) translateY(0); opacity: 1; }
100% { transform: scale(0.6) translateY(8px); opacity: 0; }
}
.overflow-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: none;
border: none;
color: var(--fg);
opacity: 0.7;
cursor: pointer;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
transition: background 0.15s, opacity 0.15s, color 0.15s;
/* Domino-style cascade: each item slides up + fades in with a tiny
delay after the previous one (set via nth-last-child below so the
BOTTOM item appears first and the cascade rolls upward — visually
feels like the menu is "stacking up" from the chevron). The
container itself springs in via .overflow-menu's keyframe. */
animation: overflow-item-in 0.32s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(1) { animation-delay: 0.06s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(2) { animation-delay: 0.10s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(3) { animation-delay: 0.14s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(4) { animation-delay: 0.18s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(5) { animation-delay: 0.22s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(6) { animation-delay: 0.26s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(7) { animation-delay: 0.30s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(8) { animation-delay: 0.34s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(9) { animation-delay: 0.38s; }
.overflow-menu:not(.hidden):not(.closing) .overflow-menu-item:nth-last-child(10) { animation-delay: 0.42s; }
@keyframes overflow-item-in {
0% { opacity: 0; transform: translateY(10px) translateX(-6px) scale(0.9); }
60% { opacity: 1; }
100% { opacity: 1; transform: translateY(0) translateX(0) scale(1); }
}
/* Fold-in: items peel off top-down (mirror of the open's bottom-up
cascade) so the menu visibly empties before the container scales
back into the chevron. */
.overflow-menu.closing .overflow-menu-item {
animation: overflow-item-out 0.20s ease-in forwards;
}
.overflow-menu.closing .overflow-menu-item:nth-child(1) { animation-delay: 0.00s; }
.overflow-menu.closing .overflow-menu-item:nth-child(2) { animation-delay: 0.02s; }
.overflow-menu.closing .overflow-menu-item:nth-child(3) { animation-delay: 0.04s; }
.overflow-menu.closing .overflow-menu-item:nth-child(4) { animation-delay: 0.06s; }
.overflow-menu.closing .overflow-menu-item:nth-child(5) { animation-delay: 0.08s; }
.overflow-menu.closing .overflow-menu-item:nth-child(6) { animation-delay: 0.10s; }
.overflow-menu.closing .overflow-menu-item:nth-child(7) { animation-delay: 0.12s; }
.overflow-menu.closing .overflow-menu-item:nth-child(8) { animation-delay: 0.14s; }
.overflow-menu.closing .overflow-menu-item:nth-child(9) { animation-delay: 0.16s; }
.overflow-menu.closing .overflow-menu-item:nth-child(10) { animation-delay: 0.18s; }
@keyframes overflow-item-out {
0% { opacity: 1; transform: translateY(0) translateX(0) scale(1); }
100% { opacity: 0; transform: translateY(6px) translateX(-3px) scale(0.92); }
}
.overflow-menu-item:hover {
opacity: 1;
background: color-mix(in srgb, var(--red) 10%, transparent);
}
#overflow-attach-btn {
position: relative;
font-weight: 600;
}
#overflow-attach-btn svg {
transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.overflow-menu-item.active {
opacity: 1;
color: var(--fg);
}
.overflow-active-dot {
width: 6px;
height: 6px;
background: var(--fg);
border-radius: 50%;
margin-left: auto;
display: none;
flex-shrink: 0;
}
.overflow-menu-item.active .overflow-active-dot {
display: block;
}
.attach-strip {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin: 0 0 8px;
min-height: 0;
}
.attach-strip:empty {
display: none;
}
.attach-strip {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin: 6px 0 0;
min-height: 32px;
padding: 2px;
}
.attachment-placeholder {
display: flex;
align-items: center;
color: var(--fg);
opacity: 0.7;
font-style: italic;
padding: 4px 8px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.attach-btn { width:32px; display:grid; place-items:center; }
.hidden { display:none; }
.toggle { position:relative; display:inline-block; width:30px; height:16px; vertical-align:middle; }
.toggle input { opacity:0; width:0; height:0; }
.toggle .slider {
position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0;
background:color-mix(in srgb, var(--fg) 15%, transparent); transition:background .08s; border-radius:8px;
}
.toggle .slider:before {
position:absolute; content:""; height:12px; width:12px; left:2px; top:2px;
background:var(--panel); border-radius:50%; transform:translateX(0); transition:transform .08s;
box-shadow:0 1px 2px rgba(0,0,0,0.25);
}
.toggle input:checked + .slider { background:var(--red); }
.toggle input:checked + .slider:before { transform:translateX(14px); }
.copy-btn {
position:absolute; top:6px; right:6px;
background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
font-size:12px; height:24px; padding:0 8px; cursor:pointer;
opacity:0; transition:.15s;
}
.msg:hover .copy-btn { opacity:1; }
.role-timestamp {
font-size:0.7rem; color:var(--color-muted-alt); font-weight:normal; margin-left:6px;
}
.msg-footer {
display:flex; align-items:center; gap:6px;
flex-wrap: wrap;
margin-top:6px;
color:var(--color-muted-alt); font-size:0.75rem;
position: relative;
}
.msg-footer .timestamp {
font-size:0.75rem; color:var(--color-muted-alt);
margin:0; opacity:1;
}
.msg-footer .response-metrics {
font-size:0.75rem; color:var(--color-muted-alt);
transition: color .15s;
}
.msg-footer .response-metrics:hover { color:var(--fg); }
/* Context usage ring — right side of footer */
.ctx-ring {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: auto;
line-height: 0;
opacity: 0.6;
cursor: default;
transition: opacity 0.15s;
--ctx-stroke: var(--color-muted, #888);
}
.ctx-ring .ctx-ring-pct {
color: var(--color-muted, #888);
transition: color 0.1s ease;
}
.ctx-ring svg circle {
transition: stroke 0.1s ease;
}
.ctx-ring:hover {
opacity: 1;
--ctx-stroke: var(--ctx-color);
}
.ctx-ring:hover .ctx-ring-pct {
color: var(--ctx-color);
}
.ctx-ring-pct {
font-size: 0.7rem;
font-weight: 600;
line-height: 1;
}
/* Context detail popup */
.ctx-detail-popup {
position: fixed;
z-index: 200;
background: var(--panel, var(--bg));
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
min-width: 220px;
max-width: 280px;
font-size: 0.85rem;
color: var(--fg);
}
.ctx-bar-wrap {
width: 100%;
height: 6px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.ctx-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.3s;
}
.ctx-compact-btn {
display: block;
width: 100%;
margin-top: 10px;
padding: 6px 0;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 0.8rem;
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.ctx-compact-btn:hover {
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
.ctx-compact-btn:disabled {
opacity: 0.5;
cursor: default;
}
.compact-wave {
display: inline-block;
color: var(--accent, var(--red));
letter-spacing: 1px;
font-size: 0.9em;
}
/* Memory-used indicator pill */
.memory-used-pill {
display: inline-flex;
align-items: center;
background: var(--panel);
border: 1px solid var(--border);
color: var(--fg);
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 10px;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.15s, background 0.15s;
white-space: nowrap;
position: relative;
z-index: 1;
}
.memory-used-pill:hover { opacity: 1; background: var(--border); }
/* Nudge label text 1px down so it visually centers with the icon. */
.memory-used-pill-text { position: relative; top: 1px; display: inline-block; }
.memory-used-detail {
position: fixed;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 8px;
min-width: 200px;
max-width: min(360px, calc(100vw - 16px));
max-height: 50vh;
overflow-y: auto;
z-index: 300;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-size: 0.75rem;
}
.memory-used-row {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 3px 0;
line-height: 1.3;
}
.memory-used-row + .memory-used-row {
border-top: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.memory-used-badge {
flex-shrink: 0;
font-size: 0.7rem;
width: 16px;
text-align: center;
}
.memory-used-badge.pinned { color: var(--red); }
.memory-used-badge.recalled { color: var(--fg); opacity: 0.5; }
.memory-used-text {
color: var(--fg);
opacity: 0.85;
}
.msg-actions {
display:inline-flex; align-items:center; gap:4px;
}
.footer-copy-btn {
background:none; border:none;
color:var(--color-muted-alt); cursor:pointer;
padding:2px 6px; border-radius:4px;
transition: color .15s;
line-height:1;
display: inline-flex; align-items: center; justify-content: center;
}
.footer-copy-btn:hover { color:var(--accent); }
/* Delete action — same chrome as copy/download/edit but hover reveals
the destructive red tint so the user can tell it's not a benign op. */
.footer-delete-btn:hover { color: var(--red); }
.regen-btn {
background:none; border:none;
color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer;
padding:2px 6px; border-radius:4px;
transition: color .15s;
line-height:1;
}
.regen-btn:hover { color:var(--accent); }
.fork-btn {
background:none; border:none;
color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer;
padding:2px 6px; border-radius:4px;
transition: color .15s;
line-height:1;
}
.fork-btn:hover { color:var(--accent); }
.msg-action-btn {
background:none; border:none;
color:var(--muted, var(--color-muted-alt)); font-size:1.1rem; cursor:pointer;
padding:2px 6px; border-radius:4px;
transition: color .15s;
line-height:1;
}
.msg-action-btn:hover { color:var(--accent); }
.msg-action-btn[data-action="shorten"] { position:relative; top:1px; font-size:1.25rem; }
.msg-delete-btn { position:relative; top:1px; }
.msg-delete-btn:hover { color:var(--red); }
.msg-more-btn {
font-size:0.7rem; letter-spacing:0.5px;
}
.msg-overflow-menu {
position:fixed;
background:var(--panel);
border:1px solid var(--border);
border-radius:6px;
padding:4px;
box-shadow:0 4px 16px rgba(0,0,0,0.4);
z-index:100;
min-width:150px;
}
.msg-overflow-item {
display:flex;
align-items:center;
gap:6px;
width:100%;
background:none;
border:none;
border-bottom:1px solid color-mix(in srgb, var(--border) 40%, transparent);
color:var(--fg);
font-size:0.8rem;
padding:5px 8px;
border-radius:4px;
cursor:pointer;
text-align:left;
font-family:inherit;
transition:background .1s;
}
.msg-overflow-item:last-child { border-bottom:none; }
.msg-overflow-item:hover {
background:color-mix(in srgb, var(--red) 8%, transparent);
}
.overflow-icon {
width:16px;
text-align:center;
flex-shrink:0;
font-size:1rem;
}
.variant-nav {
display:inline-flex;
align-items:center;
gap:0px;
margin-left:auto;
font-size:0.85em;
font-family:inherit;
opacity:0.6;
}
.variant-divider {
opacity:0.25;
margin:0 4px 0 2px;
}
.variant-tag {
font-size:1.1em;
opacity:0.8;
margin-right:2px;
position:relative;
top:-1px;
}
.variant-tag-scissors {
top:-1px;
}
/* The ✂ "Rewrite shorter" action button glyph sits a touch low against the
other footer icons — nudge it up 2px. */
.msg-action-btn[data-action="shorten"] {
position: relative;
top: -2px;
}
/* The "?" Explain-simpler glyph also sits slightly low — nudge it up 1px. */
.msg-action-btn[data-action="explain"] {
position: relative;
top: -1px;
}
.variant-btn {
background:none;
border:none;
color:var(--fg);
cursor:pointer;
padding:4px 6px;
font-size:1em;
font-family:inherit;
opacity:0.7;
line-height:1;
}
.variant-btn:hover { opacity:1; color:var(--fg); }
.variant-btn:disabled { opacity:0.3; cursor:default; }
.variant-num {
background:none;
border:none;
color:var(--fg);
cursor:pointer;
padding:4px 4px;
font-size:1em;
font-family:inherit;
opacity:0.7;
line-height:1;
}
.variant-num:hover { opacity:1; color:var(--fg); }
.variant-num:disabled { opacity:0.5; cursor:default; }
.variant-slash {
opacity:0.4;
font-size:1em;
}
.continue-separator {
display:inline;
opacity:0.35;
font-size:0.85em;
font-style:italic;
}
.stopped-indicator {
color:var(--red);
margin-top:8px;
font-size:0.85em;
opacity:0.8;
display:flex;
align-items:center;
gap:8px;
flex-wrap:wrap;
}
/* Message edit UI */
.msg-edit-textarea {
background:var(--bg);
color:var(--fg);
border:1px solid var(--border);
border-radius:6px;
padding:10px;
font-family:inherit;
font-size:inherit;
line-height:1.6;
resize:vertical;
box-sizing:border-box;
}
.msg-edit-textarea:focus { outline:1px solid var(--hl-function, #61afef); border-color:var(--hl-function, #61afef); }
.msg-edit-bar {
display:flex;
gap:8px;
margin-top:6px;
}
.msg-edit-save, .msg-edit-cancel {
background:var(--bg);
color:var(--fg);
border:1px solid var(--border);
border-radius:6px;
padding:4px 14px;
cursor:pointer;
font-size:0.85em;
}
.msg-edit-save:hover { border-color:var(--color-save-green, #4caf50); color:var(--color-save-green, #4caf50); }
.msg-edit-cancel:hover { border-color:var(--red); color:var(--red); }
/* Edited indicator — similar to stopped-indicator */
.edited-indicator {
color:var(--fg);
margin-top:8px;
font-size:0.85em;
opacity:0.4;
font-style:italic;
}
.continue-btn {
background:none;
border:none;
color:var(--fg);
opacity:0.5;
cursor:pointer;
font-size:2.6em;
padding:2px 2px 0;
line-height:1;
}
.continue-btn:hover {
opacity:0.8;
}
.ctx-indicator {
display:inline-flex; align-items:center; gap:1px;
font-size:0.75rem;
}
.ctx-popup {
position:fixed;
z-index:250;
background:var(--panel);
border:1px solid var(--border);
border-radius:8px;
padding:10px 14px;
font-size:0.8rem;
color:var(--fg);
box-shadow:0 8px 24px rgba(0,0,0,0.4);
min-width:180px;
line-height:1.7;
}
.ctx-label {
display:inline-block;
width:60px;
color:var(--color-muted-alt);
font-size:0.75rem;
}
.edit-btn {
background:none; border:none;
color:var(--color-muted-alt); font-size:1.1rem; cursor:pointer;
padding:2px 6px; border-radius:4px;
transition: color .15s;
line-height:1;
}
.edit-btn:hover { color:var(--accent); }
.edit-textarea {
width:100%; background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
padding:8px; font-family:inherit; font-size:0.95rem;
resize:vertical; outline:none;
min-height:80px;
}
.edit-textarea:focus { border-color:var(--red); }
.edit-save-btn, .edit-cancel-btn {
background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
padding:4px 12px; cursor:pointer; font-size:0.8rem;
}
.edit-save-btn:hover { background:var(--panel); }
.edit-cancel-btn:hover { background:var(--panel); }
pre .copy-code {
position:absolute; right:6px;
background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
width:28px; height:28px; padding:0; cursor:pointer;
opacity:0; transition: opacity .15s, color .15s, border-color .15s;
display:flex; align-items:center; justify-content:center;
}
pre .copy-code { top:6px; }
pre .copy-code.bottom { top:auto; bottom:6px; }
pre:hover .copy-code { opacity:0.7; }
pre .copy-code:hover { opacity:1; }
pre .copy-code.copied {
opacity: 1;
color: var(--color-save-green, #4caf50);
border-color: var(--color-save-green, #4caf50);
background: color-mix(in srgb, var(--color-save-green, #4caf50) 18%, var(--bg));
animation: code-copy-pulse 0.36s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes code-copy-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
/* Slim text-button variant: swap "Copy" → "✓ Copied" via the
::before content while still inheriting the green flash + pulse. */
pre.pre-compact .copy-code.copied::before { content: '✓ Copied'; }
/* Edit code button — positioned left of copy button */
pre .edit-code {
position:absolute; right:42px; top:6px;
background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
width:28px; height:28px; padding:0; cursor:pointer;
opacity:0; transition: opacity .15s, color .15s, border-color .15s;
display:flex; align-items:center; justify-content:center;
}
pre .edit-code.bottom { top:auto; bottom:6px; }
pre:hover .edit-code { opacity:0.7; }
pre .edit-code:hover { opacity:1; }
/* When the edit-code button is in "save" mode (checkmark), use the
theme accent so it matches the EDITING outline + label that are
also accent-coloured — clearer that this is the confirm action. */
pre .edit-code.active {
opacity: 1;
color: var(--accent-primary, var(--red));
border-color: var(--accent-primary, var(--red));
background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, var(--bg));
}
/* Tapping the code body (not a button) toggles the overlay buttons off so
they stop covering the text on touch screens. Tap again to bring back. */
pre.buttons-hidden .copy-code,
pre.buttons-hidden .edit-code,
pre.buttons-hidden .run-code { opacity:0 !important; pointer-events:none !important; }
/* Editing state — subtle border on the code block */
/* Editing state: was a 1px subtle outline that was almost invisible on
mobile, so users couldn't tell their tap-to-edit had actually engaged.
Use the accent colour + a tinted background so it reads at a glance. */
pre.editing {
outline: 2px solid var(--accent-primary, var(--red));
outline-offset: -2px;
background: color-mix(in srgb, var(--accent-primary, var(--red)) 6%, var(--bg)) !important;
}
pre.editing code.editing { outline:none; cursor:text; }
pre.editing::before {
content: 'EDITING';
position: absolute; top: 0; left: 0;
padding: 2px 8px;
font-size: 9px; font-weight: 700; letter-spacing: 0.5px;
background: var(--accent-primary, var(--red));
color: #fff;
border-radius: 0 0 4px 0;
z-index: 2;
pointer-events: none;
}
/* Run code button — positioned left of edit button */
pre .run-code {
position:absolute; right:78px; top:6px;
background:var(--bg); color:var(--fg);
border:1px solid var(--border); border-radius:6px;
width:28px; height:28px; padding:0; cursor:pointer;
opacity:0; transition: opacity .15s, color .15s, border-color .15s;
display:flex; align-items:center; justify-content:center;
}
pre .run-code.bottom { top:auto; bottom:6px; }
pre:hover .run-code { opacity:0.7; }
pre .run-code:hover { opacity:1; color:var(--hl-function, #61afef); border-color:var(--hl-function, #61afef); }
/* Compact (single-line) code blocks: slim buttons so the row doesn't
double the height of a 1-line bash. Copy is text ("Copy" → "✓ Copied"),
Run and Edit keep their icons but at smaller sizes. Edit swaps to
a "Save" text label when its .active state is on. */
pre.pre-compact { padding-right: 200px; min-height: 0; }
pre.pre-compact .copy-code,
pre.pre-compact .edit-code,
pre.pre-compact .run-code {
height: 20px;
padding: 0;
font-size: 10px;
font-weight: 500;
line-height: 20px;
top: 3px;
}
/* Copy: text-only, hide SVG */
pre.pre-compact .copy-code {
width: auto;
padding: 0 8px;
right: 6px;
gap: 0;
}
pre.pre-compact .copy-code svg { display: none; }
pre.pre-compact .copy-code::before { content: 'Copy'; }
pre.pre-compact .copy-code.copied::before { content: '✓ Copied'; }
/* Edit: icon + "Edit" label, swap to "Save" when editing */
pre.pre-compact .edit-code {
width: auto;
padding: 0 8px 0 6px;
gap: 3px;
right: 64px;
}
pre.pre-compact .edit-code svg { width: 12px; height: 12px; }
pre.pre-compact .edit-code::after { content: 'Edit'; }
pre.pre-compact .edit-code.active::after { content: 'Save'; }
/* Run: icon + "Run" label */
pre.pre-compact .run-code {
width: auto;
padding: 0 8px 0 6px;
gap: 3px;
right: 126px;
}
pre.pre-compact .run-code svg { width: 12px; height: 12px; }
pre.pre-compact .run-code::after { content: 'Run'; }
/* Bottom-positioned slim buttons (when pre is near the top of the
viewport, the existing JS toggles .bottom to flip them down). */
pre.pre-compact .copy-code.bottom,
pre.pre-compact .edit-code.bottom,
pre.pre-compact .run-code.bottom { top: auto; bottom: 3px; }
/* Touch devices: no hover, so always show copy/run/edit buttons */
@media (hover: none) {
pre .copy-code { opacity:0.7; }
pre .edit-code { opacity:0.7; }
pre .run-code { opacity:0.7; }
}
/* Code runner output panel */
.code-runner-output {
position:relative;
border:1px solid var(--border); border-top:2px solid var(--hl-function, #61afef);
border-radius:0 0 4px 4px;
background:var(--bg);
margin:-4px 0 8px 0;
padding:8px 12px;
max-height:400px;
overflow:auto;
}
.code-runner-pre {
margin:0; padding:0;
font-family:'Fira Code', 'Courier New', monospace;
font-size:0.9em; line-height:1.5;
white-space:pre-wrap; word-break:break-word;
color:var(--fg);
background:none !important;
border:none !important;
}
.code-runner-error { color:var(--red); }
.code-runner-loading { font-style:italic; color:var(--red); padding:4px 0; }
.code-runner-close {
position:absolute; top:4px; right:4px;
background:none; border:none; color:var(--fg);
cursor:pointer; opacity:0.5; font-size:14px; padding:2px 6px;
}
.code-runner-close:hover { opacity:1; }
/* Labeled copy pill — pinned top-right INSIDE the run-output panel, not
in a separate footer. Panel is position:relative so absolute works. */
.code-runner-copy-inline {
position: absolute; top: 6px; right: 32px; /* sits LEFT of the X close (top:4 right:4 ~24px wide) */
z-index: 2;
background: var(--panel); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 3px 10px; font-size: 11px; cursor: pointer;
display: inline-flex; align-items: center;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.code-runner-copy-inline:hover {
border-color: var(--accent-primary, var(--red));
color: var(--accent-primary, var(--red));
}
/* Reserve room on the right so the output text doesn't slide under
either the Copy pill or the Close X. Applies to both the chat run
panel AND the document panel (doc-run-output reuses the same children). */
.code-runner-output,
.doc-run-output { padding-right: 110px; }
.toast {
position:fixed;
top: 16px; right: 16px;
left: auto; bottom: auto;
background:var(--panel); color:var(--fg);
border:1px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-left: 3px solid var(--accent);
padding:8px 12px; border-radius:6px; font-size:12px; opacity:0;
/* Off-screen to the right by default; .show slides to 0;
removing .show transitions to -120% (off-screen left). */
transform: translateX(120%);
transition: opacity .35s cubic-bezier(0.22, 1, 0.36, 1),
transform .45s cubic-bezier(0.22, 1, 0.36, 1);
z-index: 9999; pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
backdrop-filter: blur(12px);
max-width: min(360px, calc(100vw - 32px));
}
.toast.show { opacity:1; transform: translateX(0); }
.toast .toast-checkmark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-right: 7px;
color: var(--green, #50fa7b);
vertical-align: -3px;
transform: scale(0.65);
opacity: 0;
animation: toastCheckPop 360ms cubic-bezier(0.2, 0.9, 0.25, 1.25) forwards;
}
.toast .toast-checkmark svg polyline {
stroke-dasharray: 24;
stroke-dashoffset: 24;
animation: toastCheckDraw 420ms ease-out 120ms forwards;
}
@keyframes toastCheckPop {
0% { opacity: 0; transform: scale(0.65); }
65% { opacity: 1; transform: scale(1.16); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes toastCheckDraw {
to { stroke-dashoffset: 0; }
}
.toast.exiting {
opacity: 0;
transform: translateX(-120%);
}
.toast.error {
border-color: color-mix(in srgb, var(--color-error) 40%, transparent);
border-left-color: var(--color-error);
color: var(--color-error);
}
/* When the notes panel is docked to the right, the default top-right toast
sits directly over the Archive / View-toggle buttons in the panel header.
Flip it to the top-left so you can still reach the header after an
archive action. Mirror the slide direction too (enter from left, exit
to right) so the motion still reads naturally. */
body:has(.notes-pane.modal-right-docked) .toast {
right: auto;
left: 16px;
transform: translateX(-120%);
}
body:has(.notes-pane.modal-right-docked) .toast.show { transform: translateX(0); }
body:has(.notes-pane.modal-right-docked) .toast.exiting { transform: translateX(120%); }
@media (max-width: 768px) {
.toast {
top: 12px;
right: 12px;
max-width: calc(100vw - 24px);
/* Receive touches so the swipe-to-dismiss gesture works (desktop keeps
pointer-events:none so the toast never blocks clicks). Horizontal pan
only, so it doesn't fight page scroll. */
pointer-events: auto;
touch-action: pan-x;
}
}
.stop-btn {
position:absolute; top:2px; right:2px;
background:var(--panel);
color:var(--fg);
border:1px solid var(--fg);
font-family:inherit;
font-size:1em; line-height:1; padding:2px 5px;
cursor:pointer;
}
.small-note { font-size:12px; color:color-mix(in srgb, var(--fg) 60%, transparent); margin-top:4px; }
.row-end { justify-content:flex-end; }
.model-chat-btn {
height:32px; padding:0 10px; margin-left:auto;
}
/* Nudge the "+ Chat" label down 1px to sit centered in the button. */
.model-chat-btn-label {
position: relative;
top: 1px;
}
.openai-row {
display:flex; align-items:center; gap:6px;
}
.models-row {
display:flex; align-items:center; gap:6px; border:1px solid var(--border); padding:4px; margin:4px 0; border-radius: 4px;
}
.models-row .grow,
.models-row select {
flex:1;
display:flex;
align-items:center;
font-size: 9.75px;
}
.model-fav-btn {
width: 8px; height: 8px;
border-radius: 50%;
border: 1.5px solid color-mix(in srgb, var(--fg) 22%, transparent);
flex-shrink: 0;
cursor: pointer;
transition: all 0.15s;
position: relative;
margin-left: 4px;
}
.model-fav-btn::before {
content: '';
position: absolute;
top: -10px; left: -10px; right: -10px; bottom: -10px;
}
.model-fav-btn:hover {
border-color: var(--fg);
background: color-mix(in srgb, var(--fg) 27%, transparent);
transform: scale(1.3);
}
.model-fav-btn.active {
background: var(--fg);
border-color: var(--fg);
}
.model-fav-btn.active:hover {
opacity: 0.6;
}
.model-search-input {
width: 100%;
padding: 6px 10px;
margin-bottom: 4px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--fg);
font-family: inherit;
font-size: 0.8rem;
outline: none;
box-sizing: border-box;
transition: border-color 0.15s;
}
.model-search-input:focus {
border-color: var(--red);
}
.model-search-input::placeholder {
color: color-mix(in srgb, var(--fg) 30%, transparent);
}
.models-row button {
font-size: 9px;
height: 24px;
padding: 0 8px;
}
@media (max-width:768px){
.box { max-height:none; }
.chat-container { padding:10px; flex:1; margin-top:0; padding-top:42px; min-height:0; }
.scroll-nav-btn { width:44px; height:44px; font-size:14px; margin-bottom:0; }
.send-btn { width:48px; height:48px !important; border-radius:12px; }
.send-btn svg { width:22px; height:22px; }
#compare-toggle-btn { display:none !important; }
.section[draggable] { -webkit-user-drag:none; }
.drag-handle, .item-drag-handle, .folder-drag-handle { display:none !important; }
/* Sidebar overlays chat on mobile */
.sidebar {
position: fixed !important;
top: 0; bottom: 0; left: 0;
z-index: 200;
width: 80% !important;
max-width: 340px;
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
transition: transform 0.25s ease, opacity 0.25s ease !important;
opacity: 1 !important;
overflow: visible !important;
}
.sidebar.hidden {
width: 80% !important;
transform: translateX(-100%);
pointer-events: none;
overflow: hidden !important;
}
.sidebar.right-side.hidden {
transform: translateX(100%);
}
.sidebar.right-side {
left: auto; right: 0;
box-shadow: -4px 0 20px rgba(0,0,0,0.5);
}
/* Backdrop behind sidebar */
#sidebar-backdrop {
position: fixed;
inset: 0;
z-index: 199;
background: rgba(0,0,0,0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.35s ease;
}
#sidebar-backdrop.visible { opacity: 1; pointer-events: auto; }
/* Elastic overscroll on sidebar */
.sidebar-inner {
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: auto;
overflow-y: scroll !important;
padding-bottom: 40px;
}
.sidebar:not(.hidden) {
overscroll-behavior: auto;
}
/* Sidebar header — room for hamburger */
.sidebar-header {
padding: 18px 12px 8px 12px;
min-height: 44px;
}
.sidebar-brand-title {
font-size: 1.2rem;
left: 0 !important;
}
/* Sidebar inner — bigger spacing */
.sidebar-inner {
padding: 12px 10px 12px !important;
gap: 4px !important;
}
/* Section headers — match list item sizing */
.section-header-flex {
padding: 12px 10px !important;
height: 48px !important;
border-radius: 8px;
box-sizing: border-box;
}
.section-header-flex h4,
.section-header-flex .section-title {
font-size: 14px !important;
gap: 11px !important;
}
.section-icon,
.sidebar-action-icon {
width: 16px !important;
height: 16px !important;
left: 0 !important;
}
/* List items — bigger touch targets, bigger text */
.list-item {
padding: 12px 10px !important;
min-height: 48px;
font-size: 14px;
border-radius: 8px;
gap: 10px !important;
}
.list-item .grow {
font-size: 14px !important;
}
/* Search, New Chat & Assistant */
#sidebar-search-btn,
#sidebar-new-chat-btn,
.sidebar-assistant-entry {
padding: 12px 10px !important;
min-height: 48px !important;
}
#sidebar-search-btn .grow,
#sidebar-new-chat-btn .grow,
.sidebar-assistant-entry .grow {
font-size: 14px !important;
left: 0 !important;
}
/* Section separator — more breathing room */
.section {
margin-top: 4px;
border-radius: 8px;
}
/* Wave accent — slightly bigger on mobile */
.section-header-flex h4::before {
height: 18px;
}
/* Compact top bar on mobile — align with sidebar header */
.chat-top-bar {
padding: 1px 8px;
min-height: 14px;
margin-top: -31px;
padding-top: 31px;
}
.chat-top-bar .chat-new-btn { display: none; }
.chat-meta-overlay { font-size: 0.65em; max-width: 55%; overflow: visible; left: 50%; top: 50%; transform: translate(-50%, -50%); position: absolute; }
.chat-meta-overlay .export-dl-btn { display: inline-flex; }
/* Incognito — smaller on mobile */
.incognito-btn {
padding: 4px 10px;
font-size: 10px;
}
/* Incognito indicator — right side next to hamburger on mobile */
.incognito-indicator {
position: fixed;
top: 12px;
left: auto !important;
right: 48px !important;
transform: none;
width: 32px;
height: 32px;
z-index: 210;
opacity: 0.8;
}
/* Icon rail on mobile — hidden by default, shown in mini-sidebar state */
.icon-rail { display: none !important; }
.icon-rail.mobile-mini {
display: flex !important;
position: fixed;
top: 0; bottom: 0; left: 0;
z-index: 200;
width: 48px;
box-shadow: 2px 0 12px rgba(0,0,0,0.4);
}
.icon-rail.mobile-mini.right-side {
left: auto; right: 0;
box-shadow: -2px 0 12px rgba(0,0,0,0.4);
}
/* Chat bubbles — AI stretches full width, user stays compact */
.msg { font-size: 0.85em; }
.msg .body { font-size: 0.9em; }
.msg-user { max-width: 90% !important; margin-left: auto; margin-right: 4px; }
.msg-ai, .agent-thread { width: 100% !important; max-width: 100% !important; margin: 8px 0; }
/* Prevent inner scrollable elements from trapping vertical scroll */
.msg pre,
.agent-tool-output pre,
.agent-thread-cmd,
.msg details {
overflow-y: hidden !important;
max-height: none !important;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
touch-action: pan-y pan-x;
}
/* Models row — match list items */
.models-row {
padding: 12px 10px;
min-height: 48px;
}
/* Input icon buttons — bigger touch targets */
.input-icon-btn {
padding: 10px;
min-width: 44px;
min-height: 44px;
}
/* Tool indicators — icon only on mobile (hide text, keep x) */
.tool-indicator > span {
display: none !important;
}
.tool-indicator .tool-indicator-x {
display: inline-block !important;
opacity: 0.6;
}
/* Hamburger — always right on mobile */
.hamburger-btn {
width: 44px;
height: 44px;
top: 6px;
left: auto !important;
right: 4px !important;
-webkit-tap-highlight-color: transparent;
}
.hamburger-btn:hover,
.hamburger-btn:active {
background: none !important;
border: none !important;
box-shadow: none !important;
}
/* New chat — always left on mobile */
.mobile-new-chat-btn {
display: flex;
position: fixed;
top: 12px;
left: 8px;
z-index: 210;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--fg);
cursor: pointer;
opacity: 0.5;
padding: 0;
}
/* Modal close button — bigger on mobile */
.close-btn,
.modal-close {
min-width: 44px;
min-height: 44px;
width: 44px;
height: 44px;
font-size: 14px;
}
/* Code block buttons — slightly bigger touch targets */
pre .copy-code,
pre .edit-code,
pre .run-code {
width: 44px;
height: 44px;
}
/* Touch-friendly targets for small buttons */
.export-dl-btn {
min-width: 44px;
min-height: 44px;
}
.section-header-btn {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* 44×44 touch buttons starting from right:6 ends at 50. ~8px gap is
enough to be distinguishable without spreading them too far apart. */
pre .edit-code {
right: 58px;
}
pre .run-code {
right: 110px;
}
/* Dropdowns — bigger touch targets on mobile */
.dropdown-item-compact {
padding: 12px 12px !important;
font-size: 14px !important;
min-height: 44px;
gap: 10px !important;
}
.dropdown-item-compact .dropdown-icon {
width: 18px !important;
height: 18px !important;
}
.dropdown-item-compact .dropdown-icon svg {
width: 16px !important;
height: 16px !important;
}
.dropdown,
.session-dropdown {
padding: 6px !important;
border-radius: 12px !important;
}
/* Safe area padding for notched devices */
.chat-input-bar {
padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}
/* Mode toggle — larger touch targets */
.mode-toggle {
height: 34px;
border-radius: 12px;
}
.mode-toggle::before {
border-radius: 11px;
}
.mode-toggle-btn {
padding: 0 14px;
font-size: 12px;
min-height: 34px;
}
#mode-agent-btn { border-radius: 12px 0 0 12px; }
#mode-chat-btn { border-radius: 0 12px 12px 0; }
/* Diff mode — stack toolbar, bigger buttons */
.diff-toolbar {
padding: 8px 12px;
gap: 6px;
flex-wrap: wrap;
}
.diff-toolbar-btn {
padding: 6px 12px;
font-size: 12px;
min-height: 36px;
}
.diff-chunk-btn {
width: 28px;
height: 28px;
font-size: 14px;
}
/* Document/email/gallery/research library — full-height bottom-sheet
on mobile. Keep the rounded top corners + border-top so they match
the cookbook/calendar/compare look instead of looking like raw
full-bleed panels. */
.doclib-modal-content,
.gallery-modal-content {
width: 100vw !important;
max-width: 100vw !important;
/* vh fallback, dvh override so the modal adapts to mobile
URL-bar show/hide. Order matters: later same-specificity rule
wins, so dvh must come after vh. */
max-height: 100vh !important;
max-height: 100dvh !important;
height: 100vh;
height: 100dvh;
border-radius: 14px 14px 0 0 !important;
border: none !important;
border-top: 1px solid var(--border) !important;
box-shadow: none !important;
padding: 6px !important;
padding-bottom: env(safe-area-inset-bottom, 6px) !important;
margin: 0 !important;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
#email-lib-modal .doclib-grid {
max-height: none;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* Library modal layout: pin the header + tab strip, give the active
panel (admin-card) the remaining height, and let the grid scroll
internally. The load-more button sits below the grid as a sibling
inside admin-card so it stays visible at the bottom of the panel
without competing with the grid for scroll. */
#doclib-modal .doclib-modal-content {
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
}
#doclib-modal .modal-header,
#doclib-modal .lib-tabs {
flex: 0 0 auto !important;
}
#doclib-modal .modal-body {
flex: 1 1 0 !important;
min-height: 0 !important;
overflow: hidden !important;
}
#doclib-modal .admin-card {
flex: 1 1 0 !important;
min-height: 0 !important;
}
/* :not(:has(.doclib-card-expanded)) scopes these to the normal list
state — when a document card is expanded, the existing expand-state
rules take over (grid claims flex:1 with overflow:hidden so the
expanded card can scroll itself). */
#doclib-modal .doclib-grid:not(:has(.doclib-card-expanded)) {
flex: 1 1 0 !important;
min-height: 0 !important;
max-height: none !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
#doclib-modal .doclib-load-more {
margin: 8px auto 12px !important;
}
/* Squeeze a little more height for the preview content by tightening
the spacing inside an expanded doc card on mobile. */
#doclib-modal .doclib-card.doclib-card-expanded,
#email-lib-modal .doclib-card.doclib-card-expanded,
#memory-modal .doclib-card.doclib-card-expanded {
gap: 2px !important;
padding: 4px 6px !important;
height: 100% !important;
}
/* Skills preview kept stopping at partial height because the flex
chain (modal → tabs → panel → admin-card → grid → card) wouldn't
reliably hand the card a full-height parent. But the skills LIST
already scrolls correctly inside #skills-list, so that grid box
already has the right bounded height (the area below the tabs).
Anchor the expanded card to THAT box with position:absolute so it
fills the list area exactly — header + tabs stay visible.
NOTE: position:relative here is UNCONDITIONAL (not gated behind
:has(.doclib-card-expanded)). Firefox mobile builds without :has()
support never applied the gated rule, so the absolute card lost its
anchor and only filled ~50% — while Chromium/desktop-narrow worked.
Relative-when-collapsed is harmless (no abs children then). */
#memory-modal #skills-list.doclib-grid {
position: relative !important;
}
#memory-modal .doclib-card.skill-card.doclib-card-expanded {
position: absolute !important;
inset: 0 !important;
/* Height is set in JS (skills.js _fillSkillCardHeight) as an explicit
px value — Firefox does NOT treat inset:0 stretch OR height:100%
(against the flex-sized grid) as a definite height, so grid/flex
children never filled. An explicit px height is unambiguous. */
margin: 0 !important;
padding: 8px 10px calc(8px + env(safe-area-inset-bottom, 0px)) !important;
background: var(--bg) !important;
/* Flex column. JS (_fillSkillCardHeight) sets EXPLICIT px heights on
the preview +
; in a flex column an explicit-height item
(flex:0 0 auto + height) is honoured. (Grid was worse here — the
collapsed 1fr track overrode the preview's explicit height.) */
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
border: none !important;
border-radius: 0 !important;
box-sizing: border-box !important;
}
/* Preview is the 1fr grid track — give it a definite-height flex column
so the (flex:1) fills and the footer pins at the bottom. */
#memory-modal .doclib-card.skill-card.doclib-card-expanded > .doclib-card-preview {
display: flex !important;
flex-direction: column !important;
min-height: 0 !important;
overflow: hidden !important;
}
/* The card now fills the screen, but a base rule (.skill-card...
.doclib-card-preview { flex: 0 1 auto }) sizes the preview to its
CONTENT — so a medium SKILL.md left the preview at ~half the card
(the "only 50%" the debug confirmed: card was full, content wasn't).
Force the preview AND the to FILL the card. Extra .skill-card
in the selector out-specifies both the base and the generic mobile
rule so this wins without ambiguity. */
#memory-modal .doclib-card.skill-card.doclib-card-expanded > .doclib-card-preview {
flex: 1 1 auto !important;
min-height: 0 !important;
}
#memory-modal .doclib-card.skill-card.doclib-card-expanded .skill-md-pre {
flex: 1 1 auto !important;
min-height: 0 !important;
}
/* Flatten the expanded doc/email card on mobile — drop the inner
border + background so it doesn't read as a "subwindow" inside the
modal (the chat preview doesn't have that nested-card look). The
body prefix beats the desktop email rule later in the file that
paints a 2px accent border + box-shadow with !important. */
body #doclib-modal .doclib-card.doclib-card-expanded,
body #email-lib-modal .doclib-card.doclib-card-expanded,
body #memory-modal .doclib-card.doclib-card-expanded {
background: transparent !important;
border: none !important;
border-radius: 0 !important;
box-shadow: none !important;
}
/* Same layout pattern the chat preview uses: preview itself clips,
the (or PDF iframe) inside owns the scroll, and the action
bar pins to the bottom with extra bottom padding so it floats
above the iOS safe-area / home-indicator strip. */
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview,
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview {
padding: 4px 6px 20px !important;
flex: 1 1 auto !important;
min-height: 82dvh !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
border-top: none !important;
}
/* The "Load more" button is flex-shrink:0 and sits in the panel AFTER the
grid, so even when a card is expanded it reserves ~40px at the panel
bottom — shrinking the visible grid and clipping the action bar by that
much (the "black bar"). The global collapse rule's max-height:0 can't
remove a flex-shrink:0 item, so hide it outright on expand. */
#doclib-modal [data-doclib-panel="documents"]:has(.doclib-card-expanded) .doclib-load-more,
#doclib-modal [data-doclib-panel="documents"]:has(.doclib-card-expanded) .doclib-inline-load-more {
display: none !important;
}
/* Small bottom padding now that the load-more no longer steals space —
the action bar sits low, just clearing the home-indicator safe area. */
#doclib-modal [data-doclib-panel="documents"] .doclib-card.doclib-card-expanded .doclib-card-preview {
padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px)) !important;
}
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .doclib-card-pdf-frame,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-body,
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre,
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview .skill-md-editor {
flex: 1 1 auto !important;
min-height: 0 !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
/* Strip the code-box visual treatment so the / reader body
doesn't read as a "sub-window" inside the modal. */
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
}
/* The skill editor textarea keeps a light frame on mobile (it's an
input, not read-only text) — override the strip-down above. */
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview .skill-md-editor {
background: var(--bg) !important;
border: 1px solid var(--border) !important;
padding: 8px !important;
}
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code,
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-preview code.hljs,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview code.hljs,
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-preview pre code {
background: transparent !important;
border: none !important;
padding: 0 !important;
box-shadow: none !important;
}
#doclib-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions,
#memory-modal .doclib-card.doclib-card-expanded .doclib-card-expanded-actions {
padding: 6px 4px 0 !important;
margin-top: 4px !important;
flex-shrink: 0 !important;
}
/* Email reader on mobile inherits the library modal's horizontal
padding (the .doclib-modal-content default — 6px). No extra
overrides for the modal-content / modal-body / admin-card / grid
chain; we just clean up the inner email reader header/body
padding so the From / To lines align with the email subject. */
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-header,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-atts,
#email-lib-modal .doclib-card.doclib-card-expanded .doclib-card-preview .email-reader-body {
padding-left: 6px !important;
padding-right: 6px !important;
}
/* Mobile email-reader header: meta on the left, actions on the right
(two stacked rows). The two .email-reader-actions-row siblings
render as their own flex-row strips so primary (reply/reply-all/
forward) sits above secondary (AI/summary/more), instead of
flattening into one line that collided with the recipient chips. */
#email-lib-modal .email-reader-header,
.email-reader-tab-modal .email-reader-header,
.email-window-modal .email-reader-header {
padding: 8px 8px !important;
gap: 6px !important;
flex-direction: row !important;
align-items: flex-start !important;
}
#email-lib-modal .email-reader-actions,
.email-reader-tab-modal .email-reader-actions,
.email-window-modal .email-reader-actions {
display: flex !important;
flex-direction: column !important;
align-items: flex-end !important;
gap: 4px !important;
margin-left: auto !important;
flex-shrink: 0 !important;
position: relative !important;
top: -3px !important; /* lift the reply/forward/etc. action buttons up on mobile */
}
#email-lib-modal .email-reader-actions-row,
.email-reader-tab-modal .email-reader-actions-row,
.email-window-modal .email-reader-actions-row {
display: flex !important;
flex-direction: row !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: flex-end !important;
gap: 4px !important;
}
#email-lib-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn,
.email-reader-tab-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn,
.email-window-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn {
width: 44px !important;
height: 44px !important;
flex: 0 0 auto !important;
display: inline-flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
gap: 3px !important;
padding: 4px 2px !important;
}
#email-lib-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg,
.email-reader-tab-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg,
.email-window-modal .email-reader-actions .memory-toolbar-btn.reader-icon-btn svg {
width: 16px !important;
height: 16px !important;
}
/* Gallery-style label under each button — only on mobile. The
global rule below the @media block hides them on desktop. */
#email-lib-modal .reader-btn-label,
.email-reader-tab-modal .reader-btn-label,
.email-window-modal .reader-btn-label {
display: inline-block !important;
font-size: 8.5px !important;
font-weight: 500 !important;
line-height: 1 !important;
letter-spacing: 0.02em !important;
opacity: 0.75 !important;
white-space: nowrap !important;
}
/* List-view cards keep the framed look from their own .doclib-card /
.memory-item base styles — no extra grid padding here, so the
spacing matches the documents/library tab exactly. */
/* Tighten the top of the email sheet — modal header + description +
account row + toolbar were stacking with full desktop spacing and
pushed the email subjects way down. */
#email-lib-modal .modal-header {
padding: 4px 8px !important;
min-height: 0 !important;
}
#email-lib-modal .modal-header h4 {
/* Match the other tool headers (base .modal-header h4 = 1rem); was
13px, which read noticeably smaller than Calendar/Tasks/etc. */
font-size: 1rem !important;
line-height: 1.2 !important;
}
#email-lib-modal .modal-body {
gap: 4px !important;
}
#email-lib-modal .admin-card {
gap: 4px !important;
}
#email-lib-modal .admin-card > .doclib-desc {
display: none !important;
}
/* When an email is expanded the list-mode toolbar siblings are
already hidden by the existing :has rule. Hide the modal-header
entirely AND zero out the modal-content top padding so the email
reader claims the full sheet height. The swipe-down gesture (and
the dock chip) still dismiss the modal. */
#email-lib-modal:has(.doclib-card-expanded) .modal-header,
#email-lib-modal.email-reading .modal-header {
display: none !important;
}
#email-lib-modal:has(.doclib-card-expanded) .doclib-modal-content,
#email-lib-modal.email-reading .doclib-modal-content {
padding-top: 0 !important;
}
#email-lib-modal:has(.doclib-card-expanded) .modal-body,
#email-lib-modal.email-reading .modal-body {
gap: 0 !important;
}
/* Flatten the From / To bar so it doesn't read as a cut-off framed
strip with a hard background change. Remove the background, drop
the border-bottom to a faint divider, and let it blend with the
sheet. */
#email-lib-modal .email-reader-header {
background: transparent !important;
border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent) !important;
}
#email-lib-modal .email-card-nav-btn {
padding: 6px 10px !important;
min-width: 40px !important;
height: 38px !important;
}
#email-lib-modal .email-card-nav-btn svg {
width: 18px !important;
height: 18px !important;
}
/* Nudge the prev/next arrow cluster ~4px to the right so it sits
comfortably out at the edge instead of crowding the subject text. */
#email-lib-modal .email-card-nav-arrows {
transform: translateX(4px);
}
/* Done check — extend the actual TAP area via padding + negative
margin (a transparent ::before doesn't change the parent's hit
box). Layout stays the same height as library cards because the
margin cancels the padding visually, but the clickable region is
~37px square. */
#email-lib-modal .email-card-done {
position: relative !important;
z-index: 5 !important;
pointer-events: auto !important;
touch-action: manipulation !important;
padding: 12px !important;
margin: -12px !important;
}
#email-lib-modal .email-card-done svg {
width: 13px !important;
height: 13px !important;
}
/* Make sure no overlay or pseudo intercepts the tap. */
#email-lib-modal .email-card-done * { pointer-events: none; }
/* Tighten the From / To meta bar and the email body — they had
desktop padding (10–14px) that wasted real estate on phones. */
#email-lib-modal .email-reader-header {
padding: 6px 4px !important;
gap: 4px !important;
}
#email-lib-modal .email-reader-meta {
font-size: 11px !important;
}
#email-lib-modal .email-reader-meta-row strong {
min-width: 28px !important;
}
#email-lib-modal .email-reader-atts {
padding: 6px 4px !important;
}
#email-lib-modal .email-reader-body {
padding: 8px 4px !important;
}
/* Make sure the grid claims the full admin-card height when a doc
is expanded — the existing rule sets flex:1, but on mobile we also
need a hard height fallback because the parent uses dvh which
desktop-tuned selectors don't always trickle through. */
#doclib-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid,
#memory-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid {
height: 100% !important;
}
/* Skills modal: keep the header + tab strip visible; the expanded
card fills the skills-list area below them via position:absolute
(see the #skills-list rule above). Just make sure the tab-panel +
its card give the list its full height. */
#memory-modal .memory-tab-panel[data-memory-panel="skills"] > .admin-card:has(.doclib-card-expanded) {
flex: 1 1 auto !important;
min-height: 0 !important;
}
.doclib-card-header {
padding: 10px 8px;
gap: 4px;
}
.doclib-card-session,
.doclib-card-time {
display: none;
}
.doclib-card-expanded-actions {
flex-wrap: wrap;
}
/* Keep the footer buttons identical to the chat/research footers on
mobile too — those have no mobile enlargement, so neither should
these (otherwise the doc footer reads in a larger/different font). */
.doclib-card-action-btn {
font-size: 10px;
padding: 3px 8px;
}
/* Chat top bar — adjusted for reduced height */
/* Suggestion nav — bigger touch targets */
.doc-suggestion-nav-btn {
padding: 6px 8px;
font-size: 18px;
}
.doc-suggestion-close {
padding: 10px 12px;
margin: -10px -12px;
}
}
#mobile-backdrop, #mobile-menu-btn { display:none !important; }
#sidebar-backdrop { display:none !important; }
/* ----- Loading spinner ----- */
@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;
}
.search-status {
font-size: 0.85em;
color: var(--red);
margin-top: 4px;
padding: 4px;
border-left: 2px solid var(--red);
background: color-mix(in srgb, var(--red) 5%, transparent);
}
/* Loading indicator for messages */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
}
.loading-dots {
display: flex;
gap: 4px;
}
.loading-dot {
width: 6px;
height: 6px;
background-color: var(--red);
border-radius: 50%;
}
.loading-dot:nth-child(1) {
animation: loading-bounce 1.4s infinite ease-in-out both;
}
.loading-dot:nth-child(2) {
animation: loading-bounce 1.4s infinite ease-in-out both;
animation-delay: -0.32s;
}
.loading-dot:nth-child(3) {
animation: loading-bounce 1.4s infinite ease-in-out both;
animation-delay: -0.64s;
}
@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;
}
.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; }
/* Cookbook's modal-content is var(--bg) (inline) instead of the default
var(--panel), so its sticky header — which defaults to var(--panel) —
read as a different-coloured band. Match the header to the cookbook
body so the panel is one uniform colour. */
#cookbook-modal .modal-header {
background: var(--bg);
/* Cookbook opts out of the sticky header on mobile; the
title bar scrolls away with the content instead of following. */
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);
}
.close-btn,
.modal-close {
background:var(--bg);
color:var(--fg);
border:1px solid var(--fg);
font-size:12px;
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:4px;
line-height:1;
flex-shrink:0;
}
.close-btn:hover,
.modal-close:hover {
background:var(--fg);
color:var(--bg);
}
/* Minimize button — sits beside the close button on every modal */
.minimize-btn {
background:var(--bg);
color:var(--fg);
border:1px solid var(--fg);
font-size:14px;
font-weight:700;
width:24px;
height:24px;
padding:0 0 6px 0; /* nudge the underscore visually toward the middle */
display:inline-flex;
align-items:center;
justify-content:center;
line-height:1;
cursor:pointer;
border-radius:4px;
flex-shrink:0;
margin-left:auto; /* push to the right so it docks next to .close-btn */
margin-right:4px;
}
.minimize-btn:hover {
background:var(--fg);
color:var(--bg);
}
@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:0;
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;
}
.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); }
.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); }
/* 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-bottom-btn.show { opacity:1; pointer-events:auto; }
@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);
}
/* Compare model selector — match the calendar modal's clean header
(no border underline). */
#compare-model-overlay .modal-header h4 {
pointer-events: none;
}
/* Compare modal sizes to content — the global .modal-content max-height
+ .modal-body overflow combo makes BOTH the outer card and the inner
body scrollable, so even when the content fits the viewport you get
a stray vertical scrollbar. Drop the cap and disable inner scroll
here; if the viewport is genuinely tiny the modal still won't exceed
it because it's centered and the parent .modal flex layout shrinks. */
#compare-model-overlay .modal-content {
max-height: none;
overflow: visible;
}
#compare-model-overlay .modal-body {
overflow: visible;
flex: 0 0 auto;
}
.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: 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;
}
/* Document library search-term highlight — clear, high-contrast. */
mark.doclib-search-hl {
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; }
.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%; }
.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;
}
.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); }
.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); }
.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%;
}
.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; }
/* 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;
}
/* Tool modals fill the full mobile viewport — top edge to bottom —
instead of stopping at the 85dvh sheet height and leaving a gap.
Email's content carries both `modal-content` + `doclib-modal-content`,
so the generic 85dvh rule above (later in source, equal specificity)
was capping it; the ID-scoped selectors here win on specificity and
bring every tool (cookbook / tasks / memory / settings / email /
library) to the same top edge. */
#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: 100vh !important;
max-height: 100dvh !important;
height: 100vh !important;
height: 100dvh !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 — go full-bleed on mobile so content extends to the
very bottom (no wasted vh strip below). The parent modal centers
them; padding-top reset is in the per-modal rules below. */
#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: 100vh !important;
max-height: 100dvh !important;
height: 100vh !important;
height: 100dvh !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;
transition: margin-left 0.25s cubic-bezier(0.34, 1.56, 0.64, 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-rm-btn {
background: none; border: none; color: var(--fg); 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: -1px;
}
.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: absolute;
top: 0; 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-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;
}
.compare-probe-card {
background: var(--panel); border-radius: 12px; padding: 20px 24px;
display: flex; flex-direction: column; align-items: center; gap: 12px;
min-width: 280px; max-width: 90vw; box-shadow: 0 8px 32px rgba(0,0,0,0.3);
overflow: hidden;
}
.compare-probe-title {
font-size: 13px; font-weight: 600; opacity: 0.7;
}
.compare-probe-list {
display: grid; grid-template-columns: 1fr 1fr; gap: 6px; min-width: 320px; width: 100%;
}
.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 {
padding: 2px 8px; background: transparent; color: var(--fg); border: 1px solid var(--border);
border-radius: 4px; cursor: pointer; font-size: 10px; font-family: inherit;
opacity: 0.7; transition: opacity 0.15s, border-color 0.15s; white-space: nowrap; flex-shrink: 0;
}
.compare-probe-action-btn:hover { opacity: 1; border-color: var(--accent); }
@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;
}
.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; }
/* 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;
}
.compare-pane .pane-header {
display: flex;
align-items: center;
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: hidden;
min-width: 0;
flex-wrap: wrap;
}
.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; }
.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%;
padding: 6px 10px; font-size: 0.7em;
text-align: left; background: none; border: none; border-radius: 4px;
color: var(--fg); cursor: pointer; transition: background 0.1s;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pane-model-item:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
.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;
}
/* When 4+ panes, timer wraps to its own row */
.compare-grid[data-cols="4"] .pane-timer,
.compare-grid[data-cols="5"] .pane-timer,
.compare-grid[data-cols="6"] .pane-timer {
width: 100%; order: 99; margin-top: -4px; padding-bottom: 2px; padding-left: 2px;
}
.pane-finish-badge {
font-weight: 600; color: var(--red);
}
.compare-pane.winner .pane-header {
background: color-mix(in srgb, var(--red) 12%, transparent);
border-bottom-color: color-mix(in srgb, var(--red) 30%, var(--border));
}
.compare-pane.winner .pane-title {
color: var(--red);
}
.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-mode-tabs {
display: flex;
gap: 4px;
}
/* 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: 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;
font-family: inherit;
}
.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-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(--fg) 20%, transparent); opacity: 0.6; }
.compare-rematch-btn:hover { opacity: 1; }
/* 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: 6px;
max-height: 300px;
overflow-y: auto;
min-width: 220px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
padding: 4px;
}
.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(--color-blind-orange); border-color: var(--color-blind-orange);
background: rgba(255, 152, 0, 0.1);
}
.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: #5b8def; border-color: #5b8def;
background: rgba(91, 141, 239, 0.1);
}
@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;
}
/* 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: 0.95em;
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;
}
/* 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: color-mix(in srgb, var(--fg) 3%, transparent);
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);
}
/* Tables */
.msg table {
border-collapse: collapse;
margin: 0.5em 0;
font-size: 0.9em;
width: 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;
}
.msg td {
padding: 5px 12px;
border: 1px solid var(--border);
}
/* 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;
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(--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);
}
/* Preset buttons */
.preset-btn {
height: 27.2px;
padding: 0 8.5px;
margin-left: 4px;
border: 1px solid var(--red);
border-radius: 4px;
background: var(--bg);
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: 2px;
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;
}
.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);
}
@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 7px;
font-size: 13px;
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; }
/* 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: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 {
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;
}
/* ── 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;
}
/* ── Processing pulse animation (reused by session-star) ── */
@keyframes research-pulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
.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: stream-complete-pulse 2s ease-in-out infinite;
}
.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: 4px;
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; }
.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;
}
/* 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;
/* -17px (was -15) nudges the terminating dot 2px further left so it
sits flush with the thread's left rail when the search node is
expanded. This is the "big glow dot at the bottom" the user sees
after a web_search step. */
left: -17px;
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;
}
.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-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-status {
font-size: 0.85em;
opacity: 0.5;
}
.agent-thread-chevron {
font-size: 0.7em;
transition: transform 0.2s ease;
opacity: 0.4;
}
.agent-thread-node.open .agent-thread-chevron {
transform: rotate(90deg);
}
.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;
}
/* 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;
}
}
/* ===== 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; }
/* 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);
}
/* ============================================ */
/* 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.75em;
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: 13px !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;
}
.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; }
@keyframes memory-synapse-pulse {
0%, 100% { opacity: 0.35; transform: scale(1); }
50% { opacity: 0.65; transform: scale(1.02); }
}
@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-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; }
/* 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;
}
.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;
color: color-mix(in srgb, var(--fg) 50%, transparent);
}
.memory-add-row {
display: flex;
gap: 6px;
align-items: center;
height: 32px;
}
.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::placeholder {
color: color-mix(in srgb, var(--fg) 40%, transparent);
}
.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);
}
.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: 1;
cursor: default;
}
.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;
}
.memory-toolbar-toggle:has(input:not(:checked)) > span {
text-decoration: line-through;
text-decoration-color: color-mix(in srgb, var(--fg) 30%, transparent);
}
/* Bulk action bar */
.memory-bulk-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 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;
}
/* Nudge the bulk-bar action buttons up 2px (and Memory's -2px left) to
align with the row baseline. Covers both the Memory bulk bar
(Cancel/Delete) and the Skills bulk bar (Cancel/Approve/Delete) — both
live inside #memory-modal. */
#memory-modal .memory-bulk-bar #memory-bulk-cancel,
#memory-modal .memory-bulk-bar #memory-bulk-delete {
position: relative;
top: -2px;
left: -2px;
}
#memory-modal .memory-bulk-bar #skills-bulk-cancel,
#memory-modal .memory-bulk-bar #skills-bulk-publish,
#memory-modal .memory-bulk-bar #skills-bulk-audit,
#memory-modal .memory-bulk-bar #skills-bulk-delete-nonpassing,
#memory-modal .memory-bulk-bar #skills-bulk-delete {
position: relative;
top: -2px;
}
/* Research bulk bar — right-align the action buttons (keep All + count on
the left). Cancel now lives on the Select toggle, so Archive anchors. */
#doclib-research-bulk #doclib-research-bulk-archive {
margin-left: auto;
}
#doclib-research-bulk .memory-toolbar-btn {
position: relative;
top: 3px;
right: 16px;
}
/* Archive bulk buttons — nudge down 1px to match research. */
#doclib-arc-bulk .memory-toolbar-btn {
position: relative;
top: 1px;
}
/* Same right-aligned layout for the other bulk bars — Chats, Documents,
Archive, Skills, Memories. Cancel's auto-margin pushes the action group
to the right; 8px of extra right padding seats it off the edge (matching
the research bar's 8px nudge). */
#doclib-chats-bulk #doclib-chats-bulk-archive,
#doclib-bulk-bar #doclib-bulk-archive,
#doclib-arc-bulk #doclib-arc-bulk-restore,
#email-lib-bulk #email-lib-bulk-actions,
#tasks-bulk-bar #tasks-bulk-delete,
#serve-bulk-bar #serve-bulk-delete,
#gallery-bulk-bar #gallery-bulk-actions,
#gallery-editor-drafts-bulk #gallery-editor-drafts-bulk-delete,
#memory-modal .memory-bulk-bar #memory-bulk-delete,
#memory-modal .memory-bulk-bar #skills-bulk-publish {
margin-left: auto;
position: relative;
top: -1px;
}
/* X-icon Cancel button used in every bulk-select bar (Esc target). The bare SVG
sits slightly too high vs. the adjacent text buttons — nudge it down 2px. */
[id$="-bulk-cancel"] svg {
position: relative;
top: 2px;
}
#doclib-chats-bulk,
#doclib-bulk-bar,
#doclib-arc-bulk,
#email-lib-bulk,
#tasks-bulk-bar,
#serve-bulk-bar,
#gallery-bulk-bar,
#gallery-editor-drafts-bulk,
#memory-modal .memory-bulk-bar {
padding-right: 18px;
}
/* Drafts bulk bar defaults to justify-content:flex-end (whole row hugs the
right). Reset it so All + count sit on the left and only the action button
is pushed right — matching every other bulk bar. */
#gallery-editor-drafts-bulk {
justify-content: flex-start;
}
/* Nudge the whole memory + skills bulk buttons (icon + label together) up. */
#memory-modal #memory-bulk-bar .memory-toolbar-btn,
#memory-modal #skills-bulk-bar .memory-toolbar-btn {
position: relative;
top: -2px;
}
.memory-bulk-check-all {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: color-mix(in srgb, var(--fg) 60%, transparent);
font-size: 10px;
padding: 4px 8px;
border-radius: 4px;
user-select: none;
position: relative;
top: 0;
}
.memory-bulk-check-all:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
#memory-selected-count {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 10px;
flex: 1;
}
/* Custom checkbox — toggle dot (shared by select-all and per-item) */
.memory-select-cb,
.memory-bulk-check-all input {
-webkit-appearance: none;
appearance: none;
width: 6px !important;
height: 6px !important;
min-width: 6px;
min-height: 6px;
max-width: 6px;
max-height: 6px;
padding: 0;
border: 1px solid var(--border);
border-radius: 50%;
background: transparent;
cursor: pointer;
flex-shrink: 0;
margin: 0;
align-self: center;
position: relative;
box-sizing: content-box;
transition: all 0.15s;
}
.memory-select-cb:hover,
.memory-bulk-check-all input:hover {
border-color: var(--red);
}
.memory-select-cb:checked,
.memory-bulk-check-all input:checked {
background: var(--red);
border-color: var(--red);
}
.memory-count {
font-size: 11px;
color: var(--color-muted);
}
.memory-search-input {
height: 24px;
margin-top: 6px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
font-family: inherit;
font-size: 11px;
width: 100%;
box-sizing: border-box;
}
.memory-search-input:focus {
outline: none;
border-color: var(--red);
}
.memory-list {
flex: 1;
min-height: 0; /* Required so flex:1 inside a flex parent can shrink rather than push its parent past 85vh */
overflow-y: auto;
overflow-x: hidden; /* Stop the synapse sweep from triggering a sideways scrollbar */
overscroll-behavior: contain;
display: flex;
flex-direction: column;
gap: 4px;
}
.memory-item.task-paused {
opacity: 0.45 !important;
filter: saturate(0.55);
background: repeating-linear-gradient(
45deg,
color-mix(in srgb, var(--fg) 2%, transparent),
color-mix(in srgb, var(--fg) 2%, transparent) 8px,
color-mix(in srgb, var(--fg) 5%, transparent) 8px,
color-mix(in srgb, var(--fg) 5%, transparent) 16px
) !important;
}
.memory-item.task-paused:hover {
opacity: 0.85 !important;
filter: saturate(0.9);
}
.task-status-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
padding: 1px 6px;
border-radius: 3px;
flex-shrink: 0;
cursor: pointer;
border: 1px solid transparent;
line-height: 16px;
font-family: 'Fira Code', monospace;
transition: transform 0.12s ease, border-color 0.12s ease, background 0.12s ease, filter 0.12s ease;
user-select: none;
}
.task-paused-badge {
color: var(--orange, #ffb86c);
background: color-mix(in srgb, var(--orange, #ffb86c) 22%, transparent);
border-color: color-mix(in srgb, var(--orange, #ffb86c) 35%, transparent);
}
.task-active-badge {
color: var(--green, #50fa7b);
background: color-mix(in srgb, var(--green, #50fa7b) 20%, transparent);
border-color: color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
}
.task-status-badge:hover {
filter: brightness(1.08) saturate(1.15);
}
.task-paused-badge:hover {
background: color-mix(in srgb, var(--orange, #ffb86c) 30%, transparent);
border-color: color-mix(in srgb, var(--orange, #ffb86c) 55%, transparent);
}
.task-active-badge:hover {
background: color-mix(in srgb, var(--green, #50fa7b) 28%, transparent);
border-color: color-mix(in srgb, var(--green, #50fa7b) 55%, transparent);
}
.task-builtin-badge {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
padding: 1px 6px;
border-radius: 8px;
flex-shrink: 0;
white-space: nowrap;
}
.task-builtin-badge.modified {
color: var(--orange, #ff9800);
border-color: color-mix(in srgb, var(--orange, #ff9800) 40%, transparent);
background: color-mix(in srgb, var(--orange, #ff9800) 12%, transparent);
}
.memory-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
max-height: 200px;
flex-shrink: 0; /* memory-list is a flex column; without this, items get squeezed to fit */
transition: all 0.15s;
}
.memory-item-title {
font-size: 12px;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.memory-item:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
border-color: color-mix(in srgb, var(--fg) 16%, transparent);
}
/* Synapse pulse — a brief horizontal light sweep runs left → right across
each memory like a signal traversing a neural pathway. Per-item stagger
via nth-child + varied durations keeps the list shimmering rather than
pulsing in sync. */
#memory-list .memory-item {
position: relative;
overflow: hidden;
}
#memory-list .memory-item::after {
/* Sweep highlight rides the border ring only — gradient-fill + mask cutout
keeps the bright pulse on the 1px stroke instead of washing the body. */
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
pointer-events: none;
background: linear-gradient(
to right,
transparent 0%,
transparent calc(var(--sweep, -20%) - 8%),
color-mix(in srgb, var(--red) 85%, transparent) var(--sweep, -20%),
transparent calc(var(--sweep, -20%) + 8%),
transparent 100%
);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: memory-synapse-sweep 6.2s linear infinite;
animation-delay: 0.4s;
}
#memory-list .memory-item:nth-child(2n)::after { animation-duration: 7.4s; animation-delay: 1.6s; }
#memory-list .memory-item:nth-child(3n)::after { animation-duration: 8.8s; animation-delay: 3.2s; }
#memory-list .memory-item:nth-child(5n)::after { animation-duration: 9.3s; animation-delay: 4.7s; }
#memory-list .memory-item:nth-child(7n)::after { animation-duration: 5.5s; animation-delay: 2.3s; }
#memory-list .memory-item:hover::after { animation: none; opacity: 0; }
@property --sweep {
syntax: '';
inherits: false;
initial-value: -20%;
}
@keyframes memory-synapse-sweep {
/* Sweep traverses left → right in the first ~12% of the cycle (≈0.7s of
a 6.2s loop), then waits offscreen. */
0% { --sweep: -20%; }
12% { --sweep: 120%; }
13%, 100% { --sweep: 120%; }
}
@media (prefers-reduced-motion: reduce) {
#memory-list .memory-item::after { animation: none; opacity: 0; }
}
.memory-pinned:hover {
background: color-mix(in srgb, var(--red) 4%, transparent);
border-color: var(--border);
border-left-color: var(--red);
}
.memory-item-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.memory-item-text {
font-size: 11px;
line-height: 1.5;
word-break: break-word;
color: var(--fg);
}
.memory-item-edit-input {
flex: 1;
padding: 3px 5px;
border-radius: 4px;
border: 1px solid var(--red);
background: var(--bg);
color: var(--fg);
font-family: inherit;
font-size: 11px;
min-width: 0;
}
.memory-item-edit-input:focus {
outline: none;
}
/* Edit row: text input + category select side by side */
.memory-edit-row {
display: flex;
gap: 4px;
flex: 1;
min-width: 0;
}
.memory-edit-cat-select {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--red);
border-radius: 4px;
font-family: inherit;
font-size: 9px;
padding: 2px 3px;
cursor: pointer;
flex-shrink: 0;
}
.memory-edit-cat-select:focus {
outline: none;
}
.memory-item-editing {
border-color: color-mix(in srgb, var(--red) 40%, transparent);
background: color-mix(in srgb, var(--red) 3%, transparent);
}
.memory-menu-btn {
background: none;
border: 1px solid transparent;
color: var(--color-muted);
font-size: 18px;
width: 24px;
height: 24px;
line-height: 24px;
padding: 0;
border-radius: 6px;
cursor: pointer;
flex-shrink: 0;
opacity: 0;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.memory-item:hover .memory-menu-btn {
opacity: 1;
}
.memory-menu-btn:hover {
background: color-mix(in srgb, var(--fg) 7%, transparent);
border-color: var(--border);
color: var(--fg);
}
.memory-item-dropdown {
display: none;
position: fixed;
z-index: 1000;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
min-width: auto;
width: max-content;
}
.memory-item-dropdown .dropdown-item-compact {
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 6px;
white-space: nowrap;
}
.memory-item-dropdown .dropdown-item-compact:hover {
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.memory-dropdown-delete:hover {
color: var(--red) !important;
}
.memory-item-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: auto;
opacity: 0;
transition: opacity 0.15s;
}
.memory-item:hover .memory-item-actions {
opacity: 1;
}
/* Skill rows show actions at a dim opacity by default so view/run/delete are
always discoverable, then brighten on hover. */
.memory-item.skill-row .memory-item-actions {
opacity: 0.35;
position: relative;
top: -1px;
}
.memory-item.skill-row:hover .memory-item-actions {
opacity: 1;
}
.memory-item-btn {
background: none;
border: 1px solid transparent;
color: var(--color-muted);
font-size: 11px;
height: 22px;
padding: 0 6px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
}
@media (max-width: 768px) {
/* Nudge the ••• menu button up on mobile so it visually aligns with the
title row rather than sitting a hair below it. */
.memory-item-actions .memory-item-btn { transform: translateY(-3px); }
/* Email rows sit on a slightly different baseline (extra meta row /
nav-arrows cluster), so pull the menu button back down 1px. */
#email-lib-modal .memory-item-actions .memory-item-btn { transform: translateY(-2px); }
/* Base rule above hides .memory-item-actions until hover. Mobile has no
hover → the ⋮ button in cookbook serve / library cards was invisible
and effectively unclickable. Force-show on mobile. */
.memory-item-actions { opacity: 0.7 !important; }
.memory-item-actions .memory-item-btn {
width: 32px;
height: 32px;
min-width: 32px;
}
}
/* Research-preview sub-sections — used by the research-tab expand pattern. */
.doclib-research-section-label {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.55;
margin: 8px 0 4px;
}
.doclib-research-sources ol {
margin: 0;
padding-left: 18px;
font-size: 11px;
line-height: 1.5;
}
.doclib-research-sources a {
color: var(--accent, var(--red));
text-decoration: none;
}
.doclib-research-sources a:hover { text-decoration: underline; }
.doclib-research-summary {
font-size: 11px;
line-height: 1.5;
}
.doclib-research-summary p { margin: 4px 0; }
.memory-item-btn:hover {
color: var(--fg);
border-color: var(--border);
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.memory-item-btn.delete:hover {
color: var(--color-error);
border-color: var(--color-error);
}
.memory-item-btn.save {
color: var(--red);
}
.memory-item-btn.save:hover {
border-color: var(--red);
}
.memory-item-btn.pin {
padding: 1px 4px;
opacity: 0.4;
display: flex;
align-items: center;
justify-content: center;
}
.memory-item-btn.pin.active {
opacity: 1;
}
.memory-pin-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fg);
opacity: 0.5;
transition: all 0.15s;
}
.memory-item-btn.pin.active .memory-pin-dot {
background: var(--red);
opacity: 1;
}
.memory-pinned {
border-left: 3px solid var(--red);
border-radius: 4px;
background: color-mix(in srgb, var(--red) 4%, transparent);
}
.memory-pinned .memory-item-actions {
opacity: 1;
}
.memory-pinned .memory-item-actions .memory-item-btn:not(.pin) {
opacity: 0;
}
.memory-pinned:hover .memory-item-actions .memory-item-btn:not(.pin) {
opacity: 1;
}
/* Category filter chips */
.memory-category-filters {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.memory-cat-chip {
background: none;
border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 60%, transparent);
font-size: 10px;
height: 22px;
padding: 0 8px;
display: inline-flex;
align-items: center;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
transition: all 0.15s;
text-transform: lowercase;
}
.memory-cat-chip:hover {
border-color: var(--red);
color: var(--red);
}
.memory-cat-chip.active {
background: color-mix(in srgb, var(--red) 15%, transparent);
border-color: color-mix(in srgb, var(--red) 40%, transparent);
color: var(--red);
}
/* Sort select */
.memory-sort-select {
position: relative;
top: 3px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font-family: inherit;
font-size: 11px;
height: 24px;
padding: 0 6px;
cursor: pointer;
}
.memory-sort-select:focus {
outline: none;
border-color: var(--red);
}
/* Item metadata row */
.memory-item-meta {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
/* Category badge on each item */
.memory-cat-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: color-mix(in srgb, var(--fg) 55%, transparent);
text-transform: lowercase;
}
.memory-cat-identity {
background: color-mix(in srgb, var(--hl-function) 15%, transparent);
color: var(--hl-function);
}
.memory-cat-preference {
background: color-mix(in srgb, var(--hl-keyword) 15%, transparent);
color: var(--hl-keyword);
}
.memory-cat-contact {
background: color-mix(in srgb, #98c379 15%, transparent);
color: #98c379;
}
.memory-cat-project {
background: color-mix(in srgb, var(--hl-string) 15%, transparent);
color: var(--hl-string);
}
.memory-cat-goal {
background: color-mix(in srgb, var(--red) 15%, transparent);
color: var(--red);
}
.memory-cat-task {
background: color-mix(in srgb, #d19a66 15%, transparent);
color: #d19a66;
}
.memory-cat-pinned {
background: color-mix(in srgb, var(--red) 15%, transparent);
color: var(--red);
}
/* Source and time metadata */
.memory-item-source,
.memory-item-time,
.memory-item-uses {
font-size: 9px;
color: color-mix(in srgb, var(--fg) 35%, transparent);
}
.memory-item-uses {
font-family: monospace;
color: color-mix(in srgb, var(--fg) 55%, transparent);
}
.memory-item-source::before,
.memory-item-time::before {
content: '\00b7 ';
}
/* Empty state */
.memory-empty {
padding: 24px 16px;
color: color-mix(in srgb, var(--fg) 35%, transparent);
text-align: center;
font-size: 11px;
font-style: italic;
}
/* Suggestions area */
.memory-suggestions {
display: flex;
flex-direction: column;
gap: 6px;
}
.memory-suggestions.hidden {
display: none;
}
.memory-suggestions-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: color-mix(in srgb, var(--fg) 70%, transparent);
padding-bottom: 4px;
border-bottom: 1px solid var(--border);
}
.memory-suggestions-actions,
.memory-suggestion-actions {
display: flex;
align-items: center;
gap: 5px;
flex-shrink: 0;
}
.memory-suggestions-actions {
position: relative;
left: -4px;
}
.memory-suggestions-actions .memory-item-btn,
.memory-suggestion-actions .memory-item-btn {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--border) 85%, transparent);
}
.memory-suggestions-actions .memory-item-btn.save,
.memory-suggestion-actions .memory-item-btn.save {
background: color-mix(in srgb, var(--red) 9%, transparent);
border-color: color-mix(in srgb, var(--red) 28%, transparent);
}
.memory-suggestion-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
padding: 5px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--red) 3%, transparent);
}
/* Tidy animations */
.memory-tidy-editing {
border-color: color-mix(in srgb, var(--hl-function) 40%, transparent);
background: color-mix(in srgb, var(--hl-function) 6%, transparent);
transition: all 0.3s;
}
.memory-tidy-text-old {
opacity: 0.4;
text-decoration: line-through;
transition: opacity 0.25s;
}
.memory-tidy-text-new {
color: var(--hl-function);
transition: color 0.4s;
}
.memory-tidy-removing {
text-decoration: line-through;
opacity: 0;
max-height: 0;
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
border-color: transparent;
overflow: hidden;
transition: opacity 0.3s, max-height 0.4s 0.1s, padding 0.4s 0.1s, border-color 0.3s;
}
/* ===== DOCUMENT EDITOR (ARTIFACTS) PANEL ===== */
/* Doc editor is a body-level sibling of chat-container */
/* Divider between chat and doc editor */
.doc-divider {
width: 1px;
flex-shrink: 0;
background: color-mix(in srgb, var(--fg) 11%, transparent);
cursor: col-resize;
transition: background 0.15s;
position: relative;
z-index: 10;
}
.doc-divider::before {
content: '';
position: absolute;
top: 0; bottom: 0;
left: -10px;
width: 21px;
cursor: col-resize;
}
.doc-divider:hover {
background: color-mix(in srgb, var(--fg) 30%, transparent);
}
/* Always-visible "›" handle on the drag divider — clickable to collapse the
panel, and signals the divider is interactive. */
.doc-divider-collapse {
position: absolute;
top: 50%;
left: 2px;
transform: translateY(-50%);
width: 20px;
height: 34px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 7px;
background: var(--panel);
color: var(--fg);
font-size: 16px;
font-weight: 700;
line-height: 1;
opacity: 0.6;
cursor: pointer;
transition: opacity 0.15s, border-color 0.15s, left 0.22s ease;
z-index: 12;
}
/* When in fullscreen mode (cursor outside the doc), the flap itself slides
to the OUTSIDE edge of the divider — visually belonging to the chat side
the cursor is on, not the doc. The left/right shift pairs with the glyph
rotation for a single coordinated transition. */
.doc-divider-collapse[data-mode="fullscreen"] {
left: -22px;
}
.doc-divider:hover .doc-divider-collapse { opacity: 0.92; }
.doc-divider-collapse:hover { opacity: 1 !important; border-color: var(--accent, var(--red)); }
/* The same `›` glyph is in the markup; CSS rotates 180° for the left-pointing
(fullscreen) state. Smooth transition pairs with the chevron's slide. */
.doc-divider-collapse > span {
display: inline-block;
transition: transform 0.22s ease, opacity 0.18s ease;
}
.doc-divider-collapse[data-mode="fullscreen"] > span {
transform: rotate(180deg);
}
/* Secondary "hide panel" X button in the divider — invisible until the pane
is fullscreen, then floats just below the unfullscreen chevron so the user
has a one-tap escape that minimises the pane instead of just exiting
fullscreen. */
.doc-divider-hide {
position: absolute;
top: 50%;
left: 2px;
width: 20px;
height: 20px;
margin-top: 22px;
display: none;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--panel);
color: var(--accent, var(--red));
cursor: pointer;
padding: 0;
z-index: 12;
opacity: 0.85;
transition: opacity 0.15s, border-color 0.15s;
}
.doc-divider-hide:hover {
opacity: 1;
border-color: var(--accent, var(--red));
}
/* Smooth entrance — slide in from the left + fade up so it doesn't snap into
place when fullscreen activates. The chevron's vertical centering uses
translateY(-50%), so the animation has to keep that part. */
@keyframes doc-fs-chevron-in {
from { opacity: 0; transform: translateY(-50%) translateX(-18px); }
to { opacity: 0.85; transform: translateY(-50%) translateX(0); }
}
/* Copy / Export split button — main click copies, the caret opens the export menu. */
.doc-split-btn {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--border);
border-radius: 7px;
overflow: hidden;
height: 22px;
flex-shrink: 0;
}
.doc-split-btn .doc-split-main,
.doc-split-btn .doc-split-caret {
border: none !important;
border-radius: 0 !important;
height: 100% !important;
min-height: 0 !important;
/* Keep the element fully opaque so the divider line stays crisp; dim the
glyph via colour instead (the base .doc-action-icon-btn fades the whole
element to 0.3, which also hides the divider). */
opacity: 1 !important;
color: color-mix(in srgb, var(--fg) 55%, transparent) !important;
background: none !important;
transition: color 0.12s, background 0.12s;
}
.doc-split-btn:hover .doc-split-main,
.doc-split-btn:hover .doc-split-caret { color: var(--fg) !important; }
.doc-split-btn .doc-split-caret {
border-left: 1px solid var(--border) !important;
padding: 0 5px 0 7px !important;
}
.doc-split-btn .doc-split-main:hover,
.doc-split-btn .doc-split-caret:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent) !important;
color: var(--fg) !important;
}
/* Editor pane — body-level flex sibling */
.doc-editor-pane {
flex: 1;
min-width: 0;
max-width: 70vw;
container-type: inline-size;
container-name: docpane;
display: flex;
flex-direction: column;
background: var(--bg);
border-left: 1px solid var(--border);
box-shadow: -10px 0 22px rgba(0, 0, 0, 0.16);
overflow: hidden;
height: 100%;
position: relative;
z-index: 1;
color-scheme: dark;
/* Smooth open: slide in from the right + fade. Same easing/duration as
the notes pane so both drawers feel like one mechanism. */
animation: doc-pane-enter 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
transform-origin: right center;
will-change: transform, opacity;
}
@keyframes doc-pane-enter {
from { transform: translateX(24px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.doc-editor-pane.doc-pane-leaving {
animation: doc-pane-leave 160ms cubic-bezier(0.4, 0, 1, 1) both;
pointer-events: none;
}
@keyframes doc-pane-leave {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(24px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.doc-editor-pane,
.doc-editor-pane.doc-pane-leaving { animation: none; }
}
.doc-loading-overlay {
position: absolute;
inset: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg);
}
/* ---- Tab bar ---- */
.doc-tab-bar {
display: flex;
align-items: stretch;
background: var(--bg);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
height: 36px;
}
.doc-tab-scroll {
display: flex;
align-items: stretch;
flex: 1;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
justify-content: flex-start;
/* Fade tabs into the bar's background at the edges (next to the scroll
arrows) so an overflowing tab dissolves instead of being hard-cut. The
fade is conditional — when we're at an edge there's nothing to fade to,
so the mask gradient becomes flat on that side (no shadow). */
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
}
.doc-tab-scroll.is-at-left {
-webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 18px), transparent 100%);
mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 18px), transparent 100%);
}
.doc-tab-scroll.is-at-right {
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 100%);
mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 100%);
}
.doc-tab-scroll.is-at-left.is-at-right {
-webkit-mask-image: none;
mask-image: none;
}
.doc-tab-scroll::-webkit-scrollbar { display: none; }
.doc-tab-arrow {
background: none;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
font-size: 18px;
padding: 0 6px;
flex-shrink: 0;
transition: opacity 0.15s;
line-height: 1;
display: flex;
align-items: center;
position: relative;
top: -2px;
}
.doc-tab-arrow:hover {
opacity: 1;
}
#doc-tab-right,
#doc-tab-left {
position: relative;
top: 3px;
}
/* Mobile swipe-down grab handle at the top of the doc sheet. */
.doc-mobile-grabber { display: none; }
@media (max-width: 768px) {
body.doc-view .doc-mobile-grabber {
display: block;
flex-shrink: 0;
height: 18px;
position: relative;
background: transparent;
background-color: transparent;
background-image: none;
touch-action: none;
cursor: grab;
}
body.doc-view .doc-mobile-grabber::before {
content: '';
position: absolute;
top: 7px;
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 4px;
background: var(--fg);
opacity: 0.25;
border-radius: 2px;
}
}
/* Ghost tab shown in the empty state (new, not-yet-saved document). Muted +
italic so it reads as a placeholder, and non-interactive so clicking it
can't hit the tab handlers (it has no data-doc-id). */
.doc-tab.doc-tab-ghost {
/* New, not-yet-saved doc tab. It already carries .active, so it shows the
accent underline like any active tab — that's all we want. The old dashed
border + italic/dim "pending" styling looked weird, so they're gone. */
pointer-events: none;
}
.doc-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 0 10px;
font-family: inherit;
font-size: 11px;
color: var(--fg);
opacity: 0.4;
cursor: pointer;
white-space: nowrap;
transition: opacity 0.1s, background 0.1s;
flex-shrink: 0;
border-right: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
position: relative;
}
.doc-tab:hover {
opacity: 0.7;
background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.doc-tab.active {
opacity: 1;
background: color-mix(in srgb, var(--fg) 5%, transparent);
border-radius: 7px 7px 0 0;
}
.doc-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--red);
}
.doc-tab-title {
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
}
.doc-tab-title-input {
background: transparent;
border: 1px solid var(--fg);
border-radius: 2px;
color: var(--fg);
font-family: inherit;
font-size: 11px;
padding: 0 4px;
height: 18px;
width: 120px;
max-width: 180px;
outline: none;
}
.doc-tab-lang {
font-size: 9px;
opacity: 0.5;
}
.doc-tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
color: var(--fg);
opacity: 0.4;
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 0 5px;
margin-left: 3px;
flex-shrink: 0;
align-self: center;
transition: opacity 0.12s;
}
.doc-tab-close:hover { opacity: 1; }
/* Mobile-only footer (Close + Copy); hidden on desktop and in email mode. */
.doc-mobile-footer { display: none; }
.doc-tab-new {
background: none;
border: none;
color: var(--fg);
opacity: 0.25;
cursor: pointer;
font-size: 11px;
font-weight: 600;
padding: 0 10px;
transition: opacity 0.1s;
flex-shrink: 0;
display: flex;
align-items: center;
height: 100%;
}
.doc-tab-new:hover {
opacity: 0.7;
}
.doc-tab-play {
background: none;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
padding: 0 2px;
font-size: 10px;
line-height: 1;
transition: opacity 0.1s, color 0.1s;
flex-shrink: 0;
}
.doc-tab-play:hover {
opacity: 1;
color: var(--green, #4ec970);
}
.doc-tab-play.active {
opacity: 1;
color: var(--green, #4ec970);
}
.doc-tab.dragging {
opacity: 0.3;
}
.doc-tab.drag-over {
border-left: 2px solid var(--fg);
}
/* ---- HTML preview iframe ---- */
.doc-html-preview {
flex: 1;
width: 100%;
border: none;
background: #fff;
}
/* ---- Editor header ---- */
.doc-editor-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
/* Moved to the bottom as a footer: flex order pushes it last in the pane
column, and the divider flips to the top edge. */
order: 99;
border-top: 1px solid var(--border);
flex-shrink: 0;
flex-wrap: nowrap;
min-height: 36px;
background: var(--bg);
}
/* Version now lives in the document tab, not the footer. */
#doc-version-badge { display: none !important; }
/* Language icon chip on doc tabs — sits between the version pill and title.
Hidden when empty so docs without a language don't get awkward spacing. */
.doc-tab-lang {
display: inline-flex; align-items: center;
flex-shrink: 0;
align-self: center;
}
.doc-tab-lang:empty { display: none; }
.doc-tab-lang svg { display: block; }
.doc-tab-version {
font-size: 9px;
font-weight: 600;
padding: 1px 6px;
cursor: pointer;
flex-shrink: 0;
align-self: center;
line-height: 1.4;
/* Sits to the LEFT of the title now — space it off the title text. */
margin-right: 6px;
/* Accent pill so it's obvious the version is a clickable control. */
color: var(--accent-primary, var(--red));
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 45%, transparent);
background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
border-radius: 9px;
}
.doc-tab-version:hover {
border-color: var(--accent-primary, var(--red));
background: color-mix(in srgb, var(--accent-primary, var(--red)) 22%, transparent);
}
.doc-close-btn {
order: -1;
opacity: 0.5;
flex-shrink: 0;
}
.doc-close-btn:hover {
opacity: 1;
}
.doc-editor-actions {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
justify-content: flex-end;
}
.doc-left .doc-editor-actions {
margin-left: 0;
}
.doc-version-badge {
background: color-mix(in srgb, var(--red) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--red) 55%, transparent);
color: var(--red);
padding: 1px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 600;
line-height: 1.4;
cursor: pointer;
user-select: none;
transition: background 0.1s, border-color 0.1s;
opacity: 0.9;
}
.doc-version-badge:hover {
opacity: 1;
background: color-mix(in srgb, var(--red) 20%, transparent);
border-color: var(--red);
}
.doc-stream-indicator {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--accent);
opacity: 0.9;
white-space: nowrap;
animation: doc-stream-pulse 1.5s ease-in-out infinite;
}
.doc-stream-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
@keyframes doc-stream-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.doc-updated-flash {
animation: doc-update-flash 0.6s ease-out;
}
@keyframes doc-update-flash {
0% { box-shadow: inset 0 0 0 2px var(--accent); }
100% { box-shadow: inset 0 0 0 2px transparent; }
}
/* In the doc footer the type picker sits next to the accent Copy/Export split —
match its 28px height and 6px radius so the right-hand controls line up. */
.doc-actions-footer #doc-language-select {
height: 28px;
border-radius: 6px;
font-size: 11px;
top: 0;
}
/* Lang-type icon shown to the LEFT of the language select. Browsers won't
render SVG inside , so this surface the current selection's icon
externally. */
#doc-language-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px; height: 18px;
color: var(--accent-primary, var(--red));
flex-shrink: 0;
}
#doc-language-icon:empty { display: none; }
#doc-language-icon svg { display: block; }
/* ── Custom language type picker (replaces visible chrome of native
— s can't render SVG). Hidden select stays as the source of truth. */
.doc-langpicker-native-hidden {
position: absolute !important;
width: 1px !important; height: 1px !important;
padding: 0 !important; margin: -1px !important;
overflow: hidden !important; clip: rect(0,0,0,0) !important;
border: 0 !important;
}
.doc-langpicker-trigger {
display: inline-flex; align-items: center; gap: 6px;
height: 28px; padding: 0 8px 0 10px;
background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
font-size: 11px; cursor: pointer;
transition: border-color 0.12s, background 0.12s;
}
.doc-langpicker-trigger:hover {
border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 60%, var(--border));
}
.doc-langpicker-trigger svg { display: block; flex-shrink: 0; }
.doc-langpicker-label {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
max-width: 90px;
}
.doc-langpicker-ico-blank {
display: inline-block; width: 14px; height: 14px; flex-shrink: 0;
}
.doc-langpicker-menu {
background: var(--bg);
border: 1px solid var(--border); border-radius: 8px;
padding: 4px;
box-shadow: 0 10px 28px rgba(0,0,0,0.32);
max-height: 60vh; overflow-y: auto;
z-index: 10000;
min-width: 160px;
}
.doc-langpicker-item {
display: flex; align-items: center; gap: 8px;
width: 100%;
padding: 6px 10px;
background: none; color: var(--fg);
border: none; border-radius: 5px;
font-size: 12px; text-align: left;
cursor: pointer;
}
.doc-langpicker-item:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.doc-langpicker-item.is-selected {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 14%, transparent);
color: var(--accent-primary, var(--red));
}
.doc-langpicker-item svg { display: block; flex-shrink: 0; }
.doc-langpicker-item .doc-langpicker-label { max-width: 220px; }
.doc-language-select {
background-color: var(--bg);
background-image: url("data:image/svg+xml;utf8, ");
background-repeat: no-repeat;
background-position: right 4px center;
color: var(--fg);
color-scheme: dark;
border: 1px solid var(--border);
border-radius: 5px;
position: relative;
top: 0;
font-family: inherit;
font-size: 10px;
padding: 2px 20px 2px 8px;
height: 22px;
/* Fixed width so the right-anchored chevron doesn't shift when the selected
option's text width changes. */
width: 96px;
text-overflow: ellipsis;
opacity: 0.8;
cursor: pointer;
-moz-appearance: none;
appearance: none;
}
/* Light theme: tint the chevron with the light foreground instead of cyan. */
:root.light .doc-language-select {
background-image: url("data:image/svg+xml;utf8, ");
}
/* New-tab "+" spins on hover, like the library sidebar "+". */
.doc-tab-new svg { transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1); }
.doc-tab-new:hover svg { transform: rotate(180deg) scale(1.15); }
.doc-action-btn {
background: none;
color: var(--fg);
border: none;
font-family: inherit;
font-size: 10px;
padding: 2px 8px;
height: 22px;
cursor: pointer;
opacity: 0.45;
transition: opacity 0.1s;
}
.doc-action-btn:hover {
opacity: 1;
}
.doc-action-icon-btn {
background: none;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
padding: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.1s;
}
.doc-action-icon-btn:hover {
opacity: 1;
}
/* The "T" icon stays centered in the fixed-size button so it grows
symmetrically (from the center) instead of jumping around as the size
changes. The S/M/L letter is pinned to the bottom-right corner, absolutely
positioned so it never shifts the icon either. */
#doc-fontsize-btn { transform: translateY(-1px); }
#doc-fontsize-btn svg { flex: 0 0 auto; }
.doc-fontsize-levels {
position: absolute;
right: 6px;
bottom: 3px;
display: inline-flex;
line-height: 1;
pointer-events: none;
}
.doc-fontsize-levels i {
font-style: normal;
font-weight: 700;
font-size: 7px;
/* Bare --accent is undefined in this codebase, was falling back to the
hardcoded blue #4a9eff — use the real theme accent. */
color: var(--accent-primary, var(--red));
opacity: 1;
}
.doc-fontsize-levels i.active { opacity: 1; }
/* Collapsed buttons hidden in header, shown in overflow menu */
.doc-collapsible-btn.doc-collapsed { display: none !important; }
.doc-overflow-wrapper { order: -1; }
.doc-overflow-toggle { opacity: 0.5; }
.doc-overflow-toggle:hover { opacity: 1; }
.doc-overflow-menu {
display: none;
position: fixed;
z-index: 9999;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
padding: 4px 0;
min-width: 140px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.doc-overflow-menu.open { display: block; }
.doc-overflow-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 5px 12px;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 11px;
cursor: pointer;
white-space: nowrap;
transition: background 0.1s;
}
.doc-overflow-item:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.doc-overflow-item svg { flex-shrink: 0; opacity: 0.5; }
/* Markdown Edit/Preview two-icon switch — segmented, styled to match the Copy
split button. The active half gets a static highlight (no sliding). */
.md-view-toggle {
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 7px;
overflow: hidden;
flex-shrink: 0;
height: 22px;
}
.md-view-toggle .md-view-opt {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 100%;
padding: 0 !important;
border: none !important;
border-radius: 0 !important;
background: none !important;
color: var(--fg);
opacity: 0.45;
cursor: pointer;
transition: opacity 0.12s, color 0.12s, background 0.12s;
}
.md-view-toggle .md-view-opt:hover { opacity: 0.8; }
.md-view-toggle .md-view-opt.active {
opacity: 1;
color: var(--fg);
background: color-mix(in srgb, var(--fg) 8%, transparent) !important;
/* "Punch" pop when a side becomes active (only fires on an actual switch —
re-applying .active without a change doesn't restart the animation). */
animation: md-view-punch 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes md-view-punch {
0% { transform: scale(0.8); }
55% { transform: scale(1.18); }
100% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.md-view-toggle .md-view-opt.active { animation: none; }
}
/* Mobile: the doc/email footer controls (format toggle, export button, export
menu) were slimmer than the Send button, so the row looked ragged. Bump them
to match the Send button height (and give the export/overflow menu bigger,
touch-friendly rows). */
@media (max-width: 768px) {
.md-view-toggle { height: 28px; }
.md-view-toggle .md-view-opt { width: 38px; }
.doc-action-icon-btn { padding: 6px; }
.email-send-btn { height: 28px; }
/* The type/language picker was the slim one stuck at 22px — match it to the
rest of the row. */
.doc-language-select { height: 28px; font-size: 13px; padding: 2px 22px 2px 10px; }
.doc-overflow-item { font-size: 13px; padding: 9px 14px; }
.doc-overflow-item .overflow-icon svg,
.doc-overflow-item svg { width: 16px; height: 16px; }
.email-more-menu .dropdown-item-compact { padding: 9px 12px; font-size: 13px; }
/* The doc footer is tight once the run/preview toggle joins it, so on mobile
go icon-only for Close, Undo & Copy (their icons are clear).
font-size:0 drops the text node next to the SVG (the SVG has fixed w/h, so
it's unaffected). Slightly narrower type picker buys a little more room. */
#doc-actions-footer #doc-undo-btn span,
#doc-actions-footer #doc-footer-close-btn span,
#doc-email-actions #doc-email-discard-btn span { display: none; }
#doc-actions-footer #doc-undo-btn,
#doc-actions-footer #doc-footer-close-btn,
#doc-email-actions #doc-email-discard-btn { gap: 0; padding: 0 9px; }
#doc-actions-footer #doc-footer-copy-btn { gap: 0; padding: 0 13px; font-size: 0; }
/* In reply mode the button carries a "Reply" label that should stay visible
(the icon-only treatment above is only for the plain Copy action). */
#doc-actions-footer #doc-footer-copy-btn[data-mode="reply"] { gap: 5px; padding: 0 13px; font-size: 12px; }
#doc-actions-footer #doc-language-select { width: 80px; }
}
/* Markdown formatting toolbar */
/* In code-mode the toolbar only hosts the view toggles + utility buttons
(font-size, diff). Hide markdown-only formatting controls — bold,
italic, headings, list, link, attach, code-dropdown, emoji, hr, and
the PDF-only buttons. The view toggles + fontsize + diff stay. */
.doc-md-toolbar[data-mode="code"] [data-md],
.doc-md-toolbar[data-mode="code"] .md-dd-toggle,
.doc-md-toolbar[data-mode="code"] #md-toolbar-attach-btn,
.doc-md-toolbar[data-mode="code"] #md-toolbar-emoji-slot,
.doc-md-toolbar[data-mode="code"] .md-toolbar-pdf-only,
.doc-md-toolbar[data-mode="code"] .md-toolbar-sep {
display: none !important;
}
.doc-md-toolbar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1px;
padding: 2px 8px;
border-bottom: 1px solid var(--border);
background: var(--bg);
flex-shrink: 0;
overflow: hidden;
height: 36px;
position: relative;
/* Same edge fade as the tab strip — toolbar buttons dissolve into the bar's
background at the edges instead of being hard-cut when they overflow. */
-webkit-mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
mask-image: linear-gradient(to right, transparent 0, #000 18px, #000 calc(100% - 18px), transparent 100%);
}
.doc-md-toolbar button {
background: none;
border: none;
color: var(--fg);
opacity: 0.35;
padding: 4px 7px;
font-family: inherit;
font-size: 11px;
cursor: pointer;
white-space: nowrap;
line-height: 1.3;
transition: opacity 0.1s;
min-height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.doc-md-toolbar button:hover {
opacity: 1;
}
.doc-md-toolbar button:active {
opacity: 0.6;
}
/* Active formatting state — set by the email WYSIWYG sync. Mirrors the
selection's current marks (B/I/S, heading level, list) so the toolbar
acts as an indicator, not just a launcher. */
.doc-md-toolbar button.is-active {
opacity: 1;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
}
.doc-md-toolbar button.is-active:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
}
/* No black tap-flash / focus background on the doc toolbar + tab controls —
they're icon buttons, the opacity change is the only feedback we want. */
.doc-md-toolbar button,
.doc-tab-new,
.doc-tab-arrow,
.doc-tab-play {
-webkit-tap-highlight-color: transparent;
}
.doc-md-toolbar button:focus,
.doc-md-toolbar button:focus-visible,
.doc-tab-new:focus,
.doc-tab-new:focus-visible,
.doc-tab-arrow:focus,
.doc-tab-arrow:focus-visible {
outline: none;
background: none;
}
/* Grouped formatting dropdown toggles (heading / code / list) */
.md-dd-toggle { gap: 1px !important; }
/* Drop the dropdown chevron lower and tint it accent so it reads as a menu cue. */
.md-dd-toggle svg { opacity: 1; margin-left: 1px; flex-shrink: 0; transform: translateY(4px); color: var(--accent-primary, var(--red, #4a9eff)); }
.md-dd-ico {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
flex-shrink: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
opacity: 0.7;
}
.md-toolbar-sep {
width: 1px;
height: 12px;
background: color-mix(in srgb, var(--border) 60%, transparent);
margin: 0 4px;
flex-shrink: 0;
}
.md-toolbar-items {
display: flex;
align-items: center;
gap: 1px;
flex: 1 1 auto;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
scroll-behavior: smooth;
}
.md-toolbar-items::-webkit-scrollbar { display: none; }
/* On a PDF, the annotation tools (text / checkmark / signature) are the primary
actions — pull them to the far left of the toolbar. `order` is visual only
(they're display:none on non-PDF docs), so it has no effect elsewhere. */
.md-toolbar-items #doc-pdf-add-text-btn { order: -3; }
.md-toolbar-items #doc-pdf-add-check-btn { order: -2; }
.md-toolbar-items #doc-pdf-add-sign-btn { order: -1; }
@media (min-width: 769px) {
.md-toolbar-items #doc-pdf-add-text-btn {
margin-left: 14px;
}
}
/* Stack the signature icon over a tiny "sign" caption so the tool reads
clearly (the icon alone is ambiguous). The JS toggles inline display
none/'' for PDF mode, so this flex display only applies when shown. */
#doc-pdf-add-sign-btn {
flex-direction: column;
align-items: center;
gap: 0;
line-height: 1;
}
/* Pull the icon down toward the "sign" label so they read as one unit. */
#doc-pdf-add-sign-btn svg { transform: translateY(2px); }
.doc-pdf-sign-label {
font-size: 6.5px;
letter-spacing: 0.3px;
margin-top: 0;
opacity: 1;
color: var(--accent, var(--red));
}
/* Edge scroll arrows — appear when the toolbar has more icons off-screen. */
.md-scroll-arrow {
position: absolute;
top: 1px;
bottom: 1px;
width: 28px;
display: flex;
align-items: center;
border: none;
cursor: pointer;
color: var(--fg);
padding: 0 !important;
min-height: 0 !important;
opacity: 1 !important;
z-index: 6;
}
.md-scroll-arrow svg { position: relative; top: 2px; opacity: 0.75; transition: opacity 0.1s; }
.md-scroll-arrow:hover svg { opacity: 1; }
@media (min-width: 769px) {
/* On desktop the arrow rides 2px lower than the toolbar baseline. */
.md-scroll-arrow svg { top: 0; }
}
.md-scroll-left {
left: 0;
justify-content: flex-start;
padding-left: 3px;
/* Mostly-solid toolbar bg so the icons behind the arrow are hidden, fading
to transparent only at the inner edge. */
background: linear-gradient(to right, var(--bg) 0%, var(--bg) 80%, transparent 100%);
}
.md-scroll-right {
right: 0;
justify-content: flex-end;
padding-right: 3px;
background: linear-gradient(to left, var(--bg) 0%, var(--bg) 80%, transparent 100%);
}
.md-toolbar-overflow-wrapper {
position: relative;
flex-shrink: 0;
margin-left: auto;
}
.md-toolbar-overflow-toggle {
background: none;
border: 1px solid transparent;
color: var(--fg);
opacity: 0.4;
padding: 2px 4px;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.1s;
}
.md-toolbar-overflow-toggle:hover {
opacity: 1;
}
.md-toolbar-overflow-menu {
display: none;
position: fixed;
z-index: 1000;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px;
min-width: 0;
width: max-content;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
backdrop-filter: blur(12px);
}
.md-toolbar-overflow-menu.open {
display: flex;
flex-wrap: wrap;
gap: 2px;
max-width: 200px;
}
.md-toolbar-overflow-item {
background: none;
border: 1px solid transparent;
color: var(--fg);
opacity: 0.5;
padding: 4px 8px;
border-radius: 4px;
font-family: inherit;
font-size: 11px;
cursor: pointer;
white-space: nowrap;
line-height: 1.3;
transition: opacity 0.1s, background 0.1s;
}
.md-toolbar-overflow-item:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border-color: var(--border);
}
/* Find bar */
.doc-find-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: var(--panel, var(--bg));
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.doc-find-input {
flex: 1;
min-width: 0;
padding: 3px 6px;
font-size: 12px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
outline: none;
}
.doc-find-input:focus { border-color: var(--accent); }
.doc-find-count {
font-size: 11px;
opacity: 0.6;
white-space: nowrap;
min-width: 50px;
text-align: center;
}
.doc-find-nav, .doc-find-close {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
padding: 2px 5px;
font-size: 14px;
border-radius: 3px;
opacity: 0.7;
}
.doc-find-nav:hover, .doc-find-close:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 10%, transparent); }
/* Editor — highlighted overlay + transparent textarea.
Lock the editor wrap to a fixed text-column width, centered horizontally
in the doc pane. This eliminates wrap-related drift bugs as a class:
the textarea + overlay always have the same available width regardless
of viewport, sidebar state, dock state, or window resize. Whitespace on
either side reads like a writing-app column (Bear / iA Writer pattern).
container-type: inline-size lets us hide the line-number gutter via a
container query when the editor's own width is narrow (rather than
viewport-width, which would miss the case of a narrow side-docked
editor inside a wide window). */
.doc-editor-wrap {
position: relative;
flex: 1;
min-height: 0;
/* Cap the column at ~820px outer (~760px content area after the 60px
of horizontal padding the textarea+overlay use for the gutter). */
max-width: 820px;
width: 100%;
margin-left: auto;
margin-right: auto;
overflow: hidden;
background: var(--bg);
container-type: inline-size;
container-name: doceditor;
}
/* When the editor itself is narrow, soft-wrap makes one logical line
span multiple visual rows but the gutter still shows ONE number per
logical line — so the gutter and the visible rows fall out of sync.
Hide the gutter (and reclaim the 48px of left padding it reserved)
below a threshold where the mismatch reads as a glitch. */
@container doceditor (max-width: 360px) {
.doc-line-numbers { display: none !important; }
.doc-editor-textarea,
.doc-editor-highlight {
padding-left: 12px !important;
}
}
.doc-line-numbers {
position: absolute;
top: 0; left: 0; bottom: 0;
width: 36px;
padding: 10px 8px 10px 0;
margin: 0;
font-family: inherit;
font-size: 11px;
line-height: 1.45;
text-align: right;
color: var(--fg);
opacity: 0.18;
background: var(--bg);
overflow: hidden;
white-space: pre;
z-index: 2;
pointer-events: none;
user-select: none;
}
/* Find marks live in the syntax-highlight overlay, which sits at
z-index:0 under a transparent textarea — so they're always visible
through the text layer. The previous color-mix variant could
compute to near-invisible on themes with dark/desaturated accents;
forced solid colors + !important so it ALWAYS pops. */
mark.doc-find-mark {
background: var(--accent) !important;
color: var(--bg) !important;
border-radius: 2px;
padding: 0 1px;
box-shadow: 0 0 0 1px var(--accent);
}
mark.doc-find-mark.current {
background: var(--accent) !important;
color: var(--bg) !important;
box-shadow: 0 0 0 2px var(--fg);
outline: 1px solid var(--fg);
}
/* Find-match overlay rects — drawn on top of the textarea, work in
every doc mode (markdown, email, plain) regardless of whether the
syntax-highlight overlay is shown. Translucent band so the underlying
text stays readable; current match gets a brighter solid band. */
.doc-find-rect {
background: color-mix(in srgb, var(--accent) 25%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 65%, transparent);
}
.doc-find-rect.current {
background: color-mix(in srgb, var(--accent) 55%, transparent);
box-shadow: inset 0 0 0 2px var(--accent);
}
.doc-editor-highlight {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
margin: 0;
padding: 10px 12px 10px 48px;
font-family: inherit;
font-size: 11px !important;
line-height: 1.45 !important;
tab-size: 4;
white-space: pre-wrap;
word-wrap: break-word;
overflow: hidden;
/* `scrollbar-gutter: stable` reserves scrollbar-track width in the
layout even when no scrollbar is shown. The textarea below has the
same setting — without this, the textarea would consume scrollbar
space the moment its content overflows vertically, shrinking its
content width and wrapping lines earlier than the overlay. The
visible-text-drift bug user reports as "after ~16 rows it wraps
even though there's space" was caused by exactly that. */
scrollbar-gutter: stable;
scrollbar-width: none;
pointer-events: none;
z-index: 0;
background: var(--hl-bg, var(--bg)) !important;
color: var(--hl-fg, var(--fg));
border: none;
/* Disable ligatures + kerning everywhere in the editor. Monospace fonts
like Fira Code form ligatures for `=>`, `!=`, `->`, `==` etc., but
hljs splits those pairs into separate s in the overlay, which
breaks the ligature on this side while the textarea still forms it.
Result: visible row drift whenever code contains those pairs. Pin
ligatures off in both layers (textarea below) so widths stay equal. */
font-variant-ligatures: none !important;
font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
font-kerning: none !important;
text-rendering: geometricPrecision !important;
box-sizing: border-box;
}
.doc-editor-highlight::-webkit-scrollbar { display: none; }
/* Document font size options */
.doc-font-m .doc-editor-textarea,
.doc-font-m .doc-editor-highlight,
.doc-font-m .doc-line-numbers { font-size: 13px !important; }
.doc-font-l .doc-editor-textarea,
.doc-font-l .doc-editor-highlight,
.doc-font-l .doc-line-numbers { font-size: 15px !important; }
.doc-email-richbody.doc-font-m { font-size: 15px !important; }
.doc-email-richbody.doc-font-l { font-size: 17px !important; }
/* Markdown base text should match chat text color, not code color */
.doc-editor-highlight .language-markdown {
color: var(--fg) !important;
}
.doc-editor-highlight code,
.doc-editor-highlight code.hljs,
.doc-editor-highlight .hljs {
font-family: inherit;
font-size: inherit !important;
line-height: inherit !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
overflow: hidden !important;
display: block;
pointer-events: none;
}
.doc-editor-textarea {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: 100%;
height: 100% !important;
max-height: none !important;
min-height: 0 !important;
z-index: 1;
background: transparent !important;
color: transparent !important;
color-scheme: dark;
caret-color: var(--fg);
border: none;
outline: none;
resize: none;
font-family: inherit;
/* Caret position only matches the underlying highlight if BOTH layers use
identical metrics — !important on font-size + line-height defends against
anything else in the cascade nudging the textarea but not the overlay. */
font-size: 11px !important;
line-height: 1.45 !important;
padding: 10px 12px 10px 48px;
overflow-y: scroll;
/* Pair with .doc-editor-highlight's scrollbar-gutter: stable so the
textarea's content width DOESN'T shrink the moment its scrollbar
appears (overflow-y: scroll keeps scrollbar permanent, gutter
reserves the space layout-wise). Without this, line wrap diverges
between textarea and overlay whenever content exceeds the visible
area — caret stays right, but typed text appears on a different row
than the caret. */
scrollbar-gutter: stable;
/* The highlight overlay hides its scrollbar, so the textarea must too —
otherwise the scrollbar shrinks the textarea's text-area width and
wraps lines earlier than the overlay, putting the caret on the wrong
line entirely. */
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
tab-size: 4;
white-space: pre-wrap;
word-wrap: break-word;
-moz-appearance: none;
appearance: none;
box-sizing: border-box;
/* Mirror the ligature/kerning lockdown from .doc-editor-highlight above.
Without this the textarea forms `=>`/`!=`/`->`/`==` as single-glyph
ligatures while the overlay can't (hljs splits the pair into
separate spans), so glyph widths diverge and the visible text drifts
down relative to the caret as code accumulates. */
font-variant-ligatures: none !important;
font-feature-settings: "kern" 0, "liga" 0, "calt" 0, "dlig" 0 !important;
font-kerning: none !important;
text-rendering: geometricPrecision !important;
}
.doc-editor-textarea::-webkit-scrollbar { display: none; }
.doc-editor-textarea:hover,
.doc-editor-textarea:focus,
.doc-editor-textarea:active {
background: transparent !important;
/* Used to force `color: transparent` here so the hidden two-layer
textarea wouldn't bleed through on hover/focus. Now that the
textarea renders its OWN visible text (overlay is hidden), forcing
transparent here makes typed text disappear the moment the cursor
enters the page. Keep the visible fg color instead. */
color: var(--fg) !important;
outline: none !important;
}
.doc-editor-textarea::placeholder {
color: var(--fg);
opacity: 0.25;
}
/* Show real text when selecting so copy/paste is visible */
.doc-editor-textarea::selection {
color: var(--fg);
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
}
/* ---- Selection indicator badge ---- */
.doc-selection-badge {
font-size: 10px;
color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
border-radius: 4px;
padding: 1px 4px 1px 6px;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.doc-selection-clear {
background: none;
border: none;
color: var(--fg);
opacity: 0.5;
cursor: pointer;
font-size: 13px;
line-height: 1;
padding: 0 2px;
}
.doc-selection-clear:hover {
opacity: 1;
color: var(--red, var(--color-error));
}
.doc-edit-tag {
font-size: 0.75em;
opacity: 0.5;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border-radius: 4px;
padding: 1px 5px;
margin-right: 2px;
white-space: nowrap;
}
/* Attachment cards in user messages */
.attach-cards {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.attach-card {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
/* Same border as the chat bubbles. */
border: 1px solid var(--bubble-border, var(--border));
font-size: 12px;
transition: background 0.15s;
}
.attach-card[style*="cursor: pointer"]:hover,
.attach-card[data-file-id]:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
}
.attach-card-icon {
flex-shrink: 0;
opacity: 0.6;
}
.attach-card-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 200px;
}
.attach-card-size {
opacity: 0.45;
font-size: 11px;
white-space: nowrap;
}
/* Import prompt banner (above chatbar) */
.import-prompt-banner {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
margin: 0 auto 6px;
max-width: 800px;
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
border-left: 3px solid var(--accent);
border-radius: 6px;
font-size: 12px;
color: var(--fg);
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
backdrop-filter: blur(12px);
animation: modal-enter 0.15s ease-out;
}
.import-prompt-banner span { flex: 1; }
.import-prompt-banner button {
padding: 3px 12px;
border: 1px solid var(--border);
border-radius: 5px;
background: none;
color: var(--fg);
cursor: pointer;
font-size: 12px;
white-space: nowrap;
}
.import-prompt-banner button:hover {
border-color: var(--fg);
}
.import-prompt-dismiss {
border: none !important;
background: none !important;
opacity: 0.5;
font-size: 16px !important;
padding: 0 4px !important;
}
.import-prompt-dismiss:hover { opacity: 1; background: none !important; }
.doc-selection-overlay {
position: absolute;
background: color-mix(in srgb, var(--red) 10%, transparent);
border-left: 2px solid color-mix(in srgb, var(--red) 50%, transparent);
pointer-events: none;
z-index: 0;
transition: top 0.05s;
}
/* ── Suggestion comments (Google Docs style) ── */
.doc-suggestion-highlight {
position: absolute;
background: color-mix(in srgb, var(--accent) 12%, transparent);
border-left: 3px solid var(--accent);
pointer-events: none;
z-index: 1;
transition: top 0.1s, opacity 0.15s;
}
/* Suggestion card — fixed next to editor, anchored to the change */
.doc-suggestion-card {
position: fixed;
width: 250px;
background: var(--panel);
border: 1px solid var(--accent);
border-radius: 10px;
padding: 12px 12px 10px;
font-size: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
animation: suggestion-enter 0.25s ease-out;
z-index: 250;
overflow: visible;
}
/* Arrow pointing toward the editor */
.doc-suggestion-card::before {
content: '';
position: absolute;
top: 16px;
left: -10px;
width: 18px;
height: 18px;
background: var(--panel);
border-left: 2px solid var(--accent);
border-bottom: 2px solid var(--accent);
transform: rotate(45deg);
z-index: 1;
}
.doc-suggestion-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.doc-suggestion-nav {
display: flex;
align-items: center;
gap: 4px;
}
.doc-suggestion-nav-btn {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 2px 4px;
opacity: 0.35;
transition: opacity 0.1s;
}
.doc-suggestion-nav-btn:hover { opacity: 1; }
.doc-suggestion-close {
background: none;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 6px 8px;
margin: -6px -8px;
border-radius: 6px;
transition: opacity 0.1s, background 0.1s;
}
.doc-suggestion-close:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 8%, transparent); }
.doc-suggestion-counter {
font-size: 10px;
opacity: 0.4;
font-weight: 600;
letter-spacing: 0.5px;
}
/* Inline diff markers — injected into the code highlight element */
.sugg-inline-del {
background: color-mix(in srgb, var(--red) 20%, transparent);
color: color-mix(in srgb, var(--red) 70%, var(--fg));
text-decoration: line-through;
text-decoration-thickness: 1px;
text-decoration-color: color-mix(in srgb, var(--red) 40%, transparent);
border-radius: 2px;
padding: 0 2px;
}
.sugg-inline-add {
background: color-mix(in srgb, var(--green) 25%, transparent);
color: var(--green);
border-radius: 2px;
padding: 0 2px;
}
/* ---- Diff mode ---- */
.diff-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 12px;
background: color-mix(in srgb, var(--fg) 4%, var(--bg));
border-bottom: 1px solid var(--border);
font-size: 11px;
flex-shrink: 0;
}
.diff-toolbar-status {
opacity: 0.5;
font-size: 10px;
margin-right: auto;
}
.diff-toolbar-btn {
background: none;
border: 1px solid var(--border);
color: var(--fg);
font-size: 10px;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, background 0.15s;
}
.diff-toolbar-btn:hover {
border-color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
}
.diff-toolbar-btn-accept { color: var(--green); border-color: color-mix(in srgb, var(--green) 30%, transparent); }
.diff-toolbar-btn-accept:hover { border-color: var(--green); background: color-mix(in srgb, var(--green) 10%, transparent); }
.diff-toolbar-btn-reject { color: var(--red); border-color: color-mix(in srgb, var(--red) 30%, transparent); }
.diff-toolbar-btn-reject:hover { border-color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
.diff-line-del {
display: block;
background: color-mix(in srgb, var(--accent) 18%, transparent);
border-left: 3px solid var(--accent);
padding-left: 4px;
margin-left: -7px;
text-decoration: line-through;
opacity: 0.7;
}
.diff-line-add {
display: block;
background: color-mix(in srgb, var(--accent) 28%, transparent);
border-left: 3px solid var(--accent);
padding-left: 4px;
margin-left: -7px;
}
/* Inline diff summary (version history cards) */
.diff-del {
color: var(--accent);
text-decoration: line-through;
opacity: 0.7;
}
.diff-add {
color: var(--accent);
font-weight: 600;
}
.diff-line-equal {
display: block;
}
.diff-chunk-resolved {
opacity: 0.3;
transition: opacity 0.3s;
}
.diff-chunk-actions {
position: absolute;
right: 8px;
top: 0;
display: flex;
gap: 4px;
z-index: 5;
pointer-events: auto;
}
/* Diff mode: textarea sits on top of the highlight where chunk buttons live.
Disable its pointer events so clicks reach the buttons in the layer below. */
.doc-editor-wrap.diff-mode .doc-editor-textarea {
pointer-events: none;
}
.doc-editor-wrap.diff-mode .doc-editor-highlight {
pointer-events: auto;
z-index: 2;
}
.diff-chunk-btn {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--bg);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
transition: border-color 0.15s, background 0.15s;
opacity: 0.6;
}
.diff-chunk-btn:hover { opacity: 1; }
.diff-chunk-btn-accept { color: var(--green); }
.diff-chunk-btn-accept:hover { border-color: var(--green); background: color-mix(in srgb, var(--green) 15%, transparent); }
.diff-chunk-btn-reject { color: var(--red); }
.diff-chunk-btn-reject:hover { border-color: var(--red); background: color-mix(in srgb, var(--red) 15%, transparent); }
.doc-suggestion-accept-all {
flex: 1;
padding: 5px 8px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 10px;
font-family: inherit;
background: transparent;
border: 1px solid var(--accent);
color: var(--accent);
transition: background 0.1s;
}
.doc-suggestion-accept-all:hover {
background: color-mix(in srgb, var(--accent) 15%, transparent);
}
.doc-suggestion-reason {
opacity: 0.6;
margin-bottom: 6px;
font-size: 11px;
line-height: 1.4;
}
.doc-suggestion-diff {
background: var(--bg);
border-radius: 4px;
padding: 6px 8px;
font-family: var(--code-font, monospace);
font-size: 11px;
margin-bottom: 8px;
max-height: 100px;
overflow-y: auto;
word-break: break-word;
}
.doc-suggestion-del {
color: var(--red);
text-decoration: line-through;
opacity: 0.7;
}
.doc-suggestion-add {
color: var(--green);
}
.doc-suggestion-actions {
display: flex;
gap: 6px;
}
.doc-suggestion-accept,
.doc-suggestion-dismiss {
flex: 1;
padding: 5px 8px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
font-weight: 500;
transition: background 0.1s;
}
.doc-suggestion-accept {
background: var(--accent, var(--red));
color: #fff;
}
.doc-suggestion-accept:hover {
filter: brightness(1.15);
}
.doc-suggestion-dismiss {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
}
.doc-suggestion-dismiss:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
@keyframes suggestion-enter {
from { opacity: 0; transform: translateX(10px); }
to { opacity: 1; transform: translateX(0); }
}
/* Mobile: suggestion card overlays top of editor (no room on side) */
@media (max-width: 768px) {
.doc-suggestion-card {
right: 8px;
right: 8px;
width: auto;
top: 8px !important;
}
.doc-suggestion-card::before { display: none; }
}
/* ---- Streaming animation ---- */
.doc-editor-textarea[readonly] {
caret-color: var(--red);
}
.doc-editor-wrap.animating::after {
content: '';
position: absolute;
top: 6px; right: 8px;
width: 6px; height: 6px;
border-radius: 50%;
background: var(--red);
animation: doc-pulse 0.8s ease-in-out infinite;
z-index: 3;
pointer-events: none;
}
@keyframes doc-pulse {
0%, 100% { opacity: 0.3; transform: scale(0.8); }
50% { opacity: 1; transform: scale(1.2); }
}
/* Diff overlay */
.doc-diff-overlay {
position: absolute;
inset: 0;
background: var(--bg);
z-index: 5;
overflow-y: auto;
padding: 8px 12px;
font-family: 'Fira Code', monospace;
font-size: 0.85rem;
line-height: 1.5;
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 6px;
}
.doc-diff-overlay.visible { opacity: 1; }
.doc-diff-overlay.fading { opacity: 0; transition: opacity 0.4s ease; }
.doc-diff-stats {
display: flex;
gap: 10px;
padding: 4px 0 8px;
font-size: 0.8rem;
font-weight: 600;
}
.diff-stat-del { color: var(--warn); }
.diff-stat-add { color: var(--green); }
.doc-diff-content { }
.doc-diff-line {
padding: 1px 8px;
border-radius: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.doc-diff-line.same {
opacity: 0.4;
}
.doc-diff-line.del {
background: color-mix(in srgb, var(--warn) 15%, transparent);
color: var(--warn);
text-decoration: line-through;
text-decoration-color: color-mix(in srgb, var(--warn) 40%, transparent);
}
.doc-diff-line.add {
background: color-mix(in srgb, var(--green) 12%, transparent);
color: var(--green);
}
.doc-diff-sep {
text-align: center;
padding: 2px 0;
font-size: 0.7rem;
opacity: 0.3;
color: var(--fg);
}
/* Version history panel (slide-out) */
.doc-version-panel {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 300px;
background: var(--bg);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
z-index: 200;
box-shadow: 4px 0 16px rgba(0,0,0,0.35);
animation: version-slide-in 0.2s ease-out;
/* Match the app modal aesthetic so nothing inherits the large body font. */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px;
letter-spacing: -0.01em;
}
@keyframes version-slide-in {
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
.doc-version-panel.hidden {
display: none;
}
@media (max-width: 768px) {
.doc-version-panel {
left: 0;
right: 0;
top: auto;
bottom: 0;
width: 100%;
height: 50vh;
border-right: none;
border-left: none;
border-top: 1px solid var(--border);
border-radius: 14px 14px 0 0;
box-shadow: 0 -4px 16px rgba(0,0,0,0.3);
/* It's a bottom sheet on mobile — slide UP from the bottom, not in from
the left (the desktop animation looked like a black box janking in). */
animation: version-slide-up 0.2s ease-out;
}
}
@keyframes version-slide-up {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.doc-version-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
font-size: 12px;
font-weight: 600;
color: var(--fg);
}
.doc-version-list {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.doc-version-item {
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 6px;
cursor: pointer;
transition: all 0.15s;
background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.doc-version-item:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: var(--fg);
}
.doc-version-info {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.doc-version-num {
font-weight: 600;
font-size: 12px;
color: var(--fg);
}
.doc-version-source {
font-size: 10px;
color: var(--fg);
opacity: 0.5;
background: color-mix(in srgb, var(--fg) 6%, transparent);
padding: 1px 6px;
border-radius: 4px;
}
.doc-version-time {
font-size: 10px;
color: var(--fg);
opacity: 0.4;
margin-left: auto;
}
.doc-version-summary {
font-size: 11px;
color: var(--fg);
opacity: 0.5;
margin-bottom: 4px;
}
.doc-version-restore {
background: none;
border: 1px solid var(--border);
color: var(--fg);
font-size: 10px;
padding: 2px 8px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.doc-version-restore:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: var(--fg);
}
/* "latest" badge */
.doc-version-latest {
font-size: 10px;
font-weight: 600;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
padding: 1px 6px;
border-radius: 4px;
}
/* Diff preview lines — small and muted so they don't dominate the item. */
.doc-version-diff {
font-size: 10px;
line-height: 1.5;
opacity: 0.8;
margin-top: 2px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
word-break: break-word;
}
.doc-version-diff .diff-del { color: var(--red); opacity: 0.75; }
.doc-version-diff .diff-add { color: #3fb950; }
/* Mobile: doc editor takes over full screen as toggle */
@media (max-width: 768px) {
body.doc-view .doc-editor-pane {
/* Force its own full-screen window on mobile. !important on these so an
inline width/position left over from desktop drag-resize, or the base
desktop split layout (flex: 1; max-width: 70vw; border-left), can never
render it as a narrow side pane ("sidebar") on a phone. */
position: fixed !important;
inset: 0 !important;
top: 0 !important; right: 0 !important; bottom: 0 !important; left: 0 !important;
max-width: 100% !important;
width: 100% !important;
flex: none !important;
z-index: 170;
/* Stroke the top edge so the rounded corners read as a curved sheet edge. */
border: 1px solid var(--border);
border-bottom: none;
/* Rounded top corners like the other mobile sheet windows. */
border-radius: 14px 14px 0 0;
/* Slide up from the bottom (sheet), not in from the side, on mobile. */
animation: sheet-enter 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) both;
transform-origin: bottom center;
}
body.doc-view .doc-divider {
display: none;
}
/* Hide chat behind doc panel on mobile */
body.doc-view .chat-container {
display: none;
}
/* Doc + email windows alternate: whichever was opened last is in front.
Default (a doc was opened last) → email windows sit BELOW the full-screen
doc pane (170) so the draft is on top. When the user re-opens an email
window (body.email-front, set by openEmailLibrary / reader open+restore) →
the email windows jump ABOVE the doc. Covers the email library AND any open
reader window (email-reader-); both are .modal at z-250 by default. */
body.doc-view #email-lib-modal,
body.doc-view .modal[id^="email-reader-"] { z-index: 150 !important; }
body.doc-view.email-front #email-lib-modal,
body.doc-view.email-front .modal[id^="email-reader-"] { z-index: 300 !important; }
/* Hide new-session button and hamburger when doc editor is open on mobile */
body.doc-view .mobile-new-chat-btn,
/* Hide the global hamburger while Compare Mode is active — the compare
header has its own close button in the top-right, and overlapping the
two looks like a bug. The .compare-active class lives on the chat
container, not body, so use :has(). */
body:has(.compare-active) .hamburger-btn { display: none !important; }
/* Mobile: hide the sidebar hamburger when a document panel (or notes pane)
is open — those sheets cover the whole screen on mobile, so a floating
hamburger sticking out over them is just clutter / mis-tap bait. */
@media (max-width: 768px) {
body.doc-view .hamburger-btn,
body:has(#notes-pane) .hamburger-btn { display: none !important; }
}
/* Make room for hamburger button alongside the tab/header bars (fallback if shown) */
body.doc-view.sidebar-collapsed.hamburger-left .doc-tab-bar,
body.doc-view.sidebar-collapsed.hamburger-left .doc-editor-header,
body.doc-view.sidebar-collapsed.hamburger-left .doc-md-toolbar {
padding-left: 44px;
}
body.doc-view.sidebar-collapsed.hamburger-right .doc-tab-bar,
body.doc-view.sidebar-collapsed.hamburger-right .doc-editor-header,
body.doc-view.sidebar-collapsed.hamburger-right .doc-md-toolbar {
padding-right: 44px;
}
/* ── Tab bar — match header height, bigger touch targets ── */
.doc-tab-bar {
padding: 0;
height: 40px;
}
.doc-tab {
padding: 0 12px;
font-size: 13px;
}
/* Bigger × touch target on mobile. */
.doc-tab-close {
font-size: 22px;
padding: 0 8px;
opacity: 0.5;
}
.doc-tab .doc-tab-menu-btn {
opacity: 0.4 !important;
padding: 4px 6px !important;
}
.doc-tab .doc-tab-menu-btn svg {
width: 14px !important;
height: 14px !important;
}
/* Footer is identical to desktop now, so the separate mobile Close/Copy
footer is dropped and the per-tab × stays (matching desktop). */
.doc-mobile-footer { display: none !important; }
.doc-tab-new {
font-size: 13px !important;
padding: 0 14px !important;
gap: 4px;
}
/* No hamburger padding — it's hidden in doc-view */
body.doc-view.sidebar-collapsed.hamburger-left .doc-tab-bar,
body.doc-view.sidebar-collapsed.hamburger-left .doc-editor-header,
body.doc-view.sidebar-collapsed.hamburger-left .doc-md-toolbar {
padding-left: 0 !important;
}
body.doc-view.sidebar-collapsed.hamburger-right .doc-tab-bar,
body.doc-view.sidebar-collapsed.hamburger-right .doc-editor-header,
body.doc-view.sidebar-collapsed.hamburger-right .doc-md-toolbar {
padding-right: 0 !important;
}
/* ── Header — identical layout to desktop (left-aligned), match tab height ── */
.doc-editor-header {
padding: 6px 12px 6px 8px;
gap: 6px;
min-height: 40px;
}
.doc-import-label,
#doc-version-badge {
display: none !important;
}
/* ── Markdown toolbar — same height as tab bar ── */
.doc-md-toolbar {
height: 40px !important;
padding: 4px 8px;
gap: 2px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* The toolbar's padding-left is forced to 0 by the hamburger rules, so give
the Edit/Preview toggle its own left margin to clear the screen edge +
the 18px mask fade. Margin isn't overridden by those !important paddings. */
.doc-md-toolbar .md-view-toggle { margin-left: 8px; }
.doc-md-toolbar button {
font-size: 13px !important;
padding: 6px 10px !important;
min-width: 34px;
min-height: 34px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.md-toolbar-sep {
height: 18px;
margin: 0 4px;
}
}
/* Opacity slider in the theme tabs row — lets the user see the page behind
the modal while tweaking colors. JS toggles .hidden based on active tab. */
.theme-opacity-wrap {
display: inline-flex;
align-items: center;
gap: 5px;
margin-left: auto;
margin-right: 6px;
padding: 2px 9px;
border: 1px solid var(--border);
border-radius: 999px;
opacity: 0.65;
transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
height: 22px;
align-self: center;
/* Now a toggle, not a slider wrapper. */
cursor: pointer;
color: var(--fg);
font: inherit;
background: transparent;
}
.theme-opacity-wrap.hidden { display: none; }
.theme-opacity-wrap:hover { opacity: 1; }
/* The title h4 already carries margin-right:auto to group header controls
on the right. BOTH the Peek button AND the injected minimize button also
have margin-left:auto — and multiple competing auto-margins split the
free space, stranding Peek in the middle. Zero their left-autos here so
the h4 alone pushes Peek + minimize + close together, flush right. */
.modal-header .theme-opacity-wrap,
.modal-header .modal-minimize-btn { margin-left: 0 !important; }
/* On = peeking through; highlight with the accent so the state is obvious. */
.theme-opacity-wrap.active {
opacity: 1;
border-color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
color: var(--accent, var(--red));
}
.theme-opacity-label { font-size: 10px; font-weight: 600; letter-spacing: 0.02em; }
.theme-opacity-wrap > svg { flex-shrink: 0; opacity: 0.7; }
.theme-opacity-wrap input[type="range"] {
width: 92px;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: color-mix(in srgb, var(--fg) 18%, transparent);
border-radius: 2px;
outline: none;
margin: 0;
}
.theme-opacity-wrap input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent, var(--red));
border: none;
cursor: pointer;
}
.theme-opacity-wrap input[type="range"]::-moz-range-thumb {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent, var(--red));
border: none;
cursor: pointer;
}
/* ── Theme editor: hover-to-highlight zone ── */
.theme-zone-highlight {
position: fixed;
pointer-events: none;
z-index: 9998;
border: 2px dashed var(--accent, var(--red));
border-radius: 6px;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 40%, transparent),
0 0 18px color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
animation: theme-zone-pulse 1.4s ease-in-out infinite;
}
@keyframes theme-zone-pulse {
0%, 100% { opacity: 0.95; }
50% { opacity: 0.55; }
}
/* Highlight the color row itself too so the user has a strong visual link
between the input they're hovering and the zone overlay on the page. */
#theme-tab-customize .color-row {
transition: background 0.15s, border-color 0.15s;
border-radius: 6px;
}
#theme-tab-customize .color-row:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent);
}
/* "Auto-saved" pill that flashes after each color/font change. */
.theme-autosaved-pill {
position: sticky;
bottom: 8px;
margin: 8px auto 0;
width: fit-content;
display: flex;
align-items: center;
gap: 5px;
background: color-mix(in srgb, var(--color-success, #4caf50) 18%, var(--bg));
color: var(--color-success, #4caf50);
border: 1px solid color-mix(in srgb, var(--color-success, #4caf50) 40%, transparent);
border-radius: 12px;
padding: 4px 10px;
font-size: 11px;
font-weight: 500;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s ease-out, transform 0.2s ease-out;
pointer-events: none;
z-index: 5;
}
.theme-autosaved-pill.visible {
opacity: 1;
transform: translateY(0);
}
/* ---- Doc fullscreen ---- */
.doc-editor-pane.doc-fullscreen {
flex: 1;
max-width: 100%;
width: 100% !important;
border-left: none;
}
/* Keep the hamburger reachable in fullscreen — version history can still
hide it (the panel covers that area). */
body:has(.doc-version-panel:not(.hidden)) .hamburger-btn {
display: none !important;
}
/* ---- Document run output ---- */
.doc-run-output {
border-top: 2px solid var(--hl-function, #61afef);
background: var(--bg);
padding: 6px 12px 8px;
max-height: 200px;
overflow: auto;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.85em;
line-height: 1.5;
position: relative;
}
.doc-run-output .code-runner-pre { background: none !important; border: none !important; margin: 0; padding: 0; }
.doc-run-output .code-runner-error { color: var(--red); }
.doc-run-output .code-runner-loading { font-style: italic; color: var(--red); }
.doc-run-output .code-runner-close { position: absolute; top: 4px; right: 4px; background: none; border: none; color: var(--fg); cursor: pointer; opacity: 0.5; font-size: 14px; padding: 2px 6px; }
.doc-run-output .code-runner-close:hover { opacity: 1; }
.doc-run-pre { color: var(--fg); white-space: pre-wrap; word-break: break-word; margin: 0; }
.doc-run-error { color: var(--red); white-space: pre-wrap; word-break: break-word; margin: 0; }
/* ---- Markdown preview ---- */
.doc-md-preview {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
font-family: inherit;
font-size: 12px;
line-height: 1.6;
color: var(--fg);
background: var(--bg);
}
.doc-md-preview pre {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
padding: 10px 12px;
overflow-x: auto;
}
.doc-md-preview code {
font-size: 0.95em;
}
.doc-md-preview p { margin: 0.6em 0; }
.doc-md-preview h1, .doc-md-preview h2, .doc-md-preview h3 {
margin: 0.8em 0 0.4em;
color: var(--hl-string);
}
.doc-md-preview ul, .doc-md-preview ol {
margin-left: 20px;
margin-bottom: 0.6em;
}
.doc-md-preview blockquote {
border-left: 3px solid var(--border);
padding-left: 12px;
margin: 0.6em 0;
opacity: 0.8;
}
/* ---- Active state for doc action icon buttons ---- */
.doc-action-icon-btn.active {
opacity: 1;
color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
}
#doc-run-btn:hover {
color: var(--hl-function, #61afef);
}
/* ===== ADMIN PANEL (inside settings modal) ===== */
/* Cards */
.admin-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
}
.admin-card h2 {
font-size: 14px;
font-weight: 600;
letter-spacing: -0.03em;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
}
.admin-danger-card {
border-color: color-mix(in srgb, var(--color-error) 27%, transparent);
}
/* Toggle switch */
.admin-toggle-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-toggle-label {
font-size: 13px;
font-weight: 500;
}
.admin-toggle-sub {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 11px;
margin-top: 2px;
}
/* Hide the native up/down spinners on the Max auto-skills number input. */
#skill-max-input {
-moz-appearance: textfield;
appearance: textfield;
}
#skill-max-input::-webkit-inner-spin-button,
#skill-max-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.admin-switch {
position: relative;
width: 30px;
height: 16px;
flex-shrink: 0;
display: inline-block;
}
.admin-switch input {
opacity: 0;
width: 0;
height: 0;
}
.admin-slider {
position: absolute;
inset: 0;
background: color-mix(in srgb, var(--fg) 50%, transparent);
border-radius: 8px;
cursor: pointer;
transition: background 0.08s;
}
.admin-slider::before {
content: '';
position: absolute;
left: 2px;
top: 2px;
width: 12px;
height: 12px;
background: var(--panel);
border-radius: 50%;
transition: transform 0.08s;
box-shadow: 0 1px 2px rgba(0,0,0,0.25);
}
.admin-switch input:checked + .admin-slider {
background: var(--red);
}
.admin-switch input:checked + .admin-slider::before {
transform: translateX(14px);
}
/* User rows */
.admin-user-row {
display: flex;
flex-direction: column;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 6px;
transition: border-color 0.15s;
}
.admin-user-row:hover {
border-color: color-mix(in srgb, var(--fg) 20%, var(--border));
}
.admin-user-info {
display: flex;
align-items: center;
gap: 8px;
}
.admin-user-name { font-size: 13px; font-weight: 500; }
/* Privilege panel */
.admin-priv-panel {
max-height: 600px;
transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.2s ease;
overflow: hidden;
}
.admin-priv-panel.hidden {
max-height: 0 !important;
opacity: 0;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
border-top: none !important;
}
/* Privilege toggle rows */
.admin-priv-panel [data-priv] {
accent-color: var(--accent, var(--red));
}
/* Section headers */
.admin-priv-section {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.35;
font-weight: 600;
margin: 10px 0 4px;
}
.admin-priv-section:first-child {
margin-top: 0;
}
/* Model checkbox list */
.priv-models-list {
max-height: 150px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
background: var(--bg);
scrollbar-width: thin;
}
.priv-models-list::-webkit-scrollbar {
width: 4px;
}
.priv-models-list::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--fg) 15%, transparent);
border-radius: 2px;
}
.priv-models-list label {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 2px;
font-size: 11px;
cursor: pointer;
border-radius: 3px;
transition: background 0.1s;
}
.priv-models-list label:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.priv-models-list input[type="checkbox"] {
accent-color: var(--accent, var(--red));
margin: 0;
flex-shrink: 0;
}
/* All/None links */
.priv-models-all,
.priv-models-none {
font-size: 10px;
opacity: 0.5;
color: var(--fg);
text-decoration: none;
cursor: pointer;
transition: opacity 0.1s;
}
.priv-models-all:hover,
.priv-models-none:hover {
opacity: 1;
}
/* MCP tool toggles panel */
.mcp-tools-panel {
width: 100%;
padding: 8px 0 4px;
border-top: 1px solid var(--border);
margin-top: 8px;
max-height: 600px;
transition: max-height 0.3s ease, opacity 0.2s ease, padding 0.2s ease;
overflow: hidden;
}
.mcp-tools-panel.hidden {
max-height: 0 !important;
opacity: 0;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
}
.mcp-tools-panel .mcp-tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.mcp-tools-panel .mcp-tools-header span:first-child {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
opacity: 0.35;
font-weight: 600;
}
.mcp-tools-panel .mcp-tools-header a {
font-size: 10px;
opacity: 0.5;
color: var(--fg);
text-decoration: none;
cursor: pointer;
transition: opacity 0.1s;
}
.mcp-tools-panel .mcp-tools-header a:hover {
opacity: 1;
}
.mcp-tools-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.mcp-tools-list::-webkit-scrollbar {
width: 4px;
}
.mcp-tools-list::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--fg) 15%, transparent);
border-radius: 2px;
}
.mcp-tools-list label {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px;
font-size: 11px;
cursor: pointer;
border-radius: 3px;
min-width: 0;
}
.mcp-tools-list label > span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.mcp-tools-list label:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.mcp-tools-list input[type="checkbox"] {
accent-color: var(--accent, var(--red));
margin: 0;
flex-shrink: 0;
}
/* Dot-style toggle (mirrors .note-check-dot) for the Add Models row,
replacing the native checkbox while keeping it for click handling. */
.adm-cb-hidden {
position: absolute;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
}
.adm-check-dot {
/* Lock the dot to a fixed 12×12 box. The parent rule
`.mcp-tools-list label > span { flex: 1 }` was stretching this span
to fill the row when it didn't have its own flex override. */
flex: 0 0 12px !important;
width: 12px !important;
height: 12px;
min-width: 12px;
max-width: 12px;
box-sizing: border-box;
border-radius: 50%;
border: 1.5px solid color-mix(in srgb, var(--fg) 35%, transparent);
position: relative;
transition: background 0.2s, border-color 0.2s, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.adm-check-dot::after {
content: '';
position: absolute;
left: 50%;
top: 45%;
width: 5px;
height: 2.5px;
border-left: 1.5px solid #fff;
border-bottom: 1.5px solid #fff;
transform: translate(-50%, -50%) rotate(-45deg) scale(0);
transform-origin: center;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.adm-model-row:hover .adm-check-dot {
border-color: var(--accent, var(--red));
transform: scale(1.15);
}
.adm-model-row:active .adm-check-dot {
transform: scale(0.9);
}
.adm-cb-hidden:checked + .adm-check-dot {
background: var(--accent, var(--red));
border-color: var(--accent, var(--red));
}
.adm-cb-hidden:checked + .adm-check-dot::after {
transform: translate(-50%, -50%) rotate(-45deg) scale(1);
}
/* Disabled endpoints — dim the row but keep the toggle/delete buttons
at full opacity so the user can re-enable or remove without squinting. */
.admin-user-row.admin-ep-disabled { opacity: 0.55; }
/* Most recently added endpoint — brief accent glow so the user can
spot the new row immediately after Adding / Find. Fades out cleanly. */
@keyframes adm-ep-just-added-glow {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
60% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 0%, transparent); background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent); }
100% { box-shadow: 0 0 0 0 transparent; background: transparent; }
}
.admin-user-row.adm-ep-just-added {
border-radius: 8px;
animation: adm-ep-just-added-glow 2.2s ease-out;
}
.admin-user-row.admin-ep-disabled .admin-btn-sm,
.admin-user-row.admin-ep-disabled .admin-btn-delete,
.admin-user-row.admin-ep-disabled .admin-badge-off { opacity: 1; }
/* Local / API subsection labels inside the Endpoints card */
.adm-ep-section-head {
display: flex;
align-items: center;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--fg);
opacity: 0.5;
margin-bottom: 4px;
}
/* Collapsible Add-Models subsections (API / Local) — the whole header row
acts as a toggle so a long cloud-API form can be tucked away when you
only want to paste a local URL. */
.adm-section-toggle {
cursor: pointer;
user-select: none;
opacity: 0.8;
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 9px;
margin-bottom: 6px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
transition: border-color 0.12s, background 0.12s, opacity 0.12s;
}
.adm-section-toggle:hover {
opacity: 1;
border-color: var(--red);
background: color-mix(in srgb, var(--red) 8%, transparent);
}
.adm-section-toggle .adm-section-caret { opacity: 0.6; }
.adm-section-toggle:focus-visible { outline: 1px solid var(--red); outline-offset: 1px; }
/* When expanded, square off the bottom so the header reads as attached to
the form it controls. */
.adm-add-section:not(.collapsed) .adm-section-toggle {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
margin-bottom: 0;
}
.adm-section-caret {
margin-left: auto;
flex-shrink: 0;
transition: transform 0.18s ease;
}
/* Collapsed: hide the form body and point the caret right. */
.adm-add-section.collapsed .admin-model-form { display: none; }
.adm-add-section.collapsed .adm-section-caret { transform: rotate(-90deg); }
.adm-quickstart-section {
margin-top: 7px;
}
.adm-quickstart-toggle {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
opacity: 0.72;
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 8px;
font-size: 11px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.adm-quickstart-toggle:hover {
opacity: 1;
border-color: var(--red);
}
.adm-quickstart-section:not(.collapsed) .adm-quickstart-toggle {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.adm-quickstart-body {
display: flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border);
border-top: 0;
border-radius: 0 0 6px 6px;
padding: 6px 8px;
}
.adm-quickstart-section.collapsed .adm-quickstart-body { display: none; }
.adm-quickstart-section.collapsed .adm-section-caret { transform: rotate(-90deg); }
/* Custom provider picker (logo + name) replacing the native */
.adm-provider-picker { position: relative; margin-bottom: 6px; }
.adm-provider-combo {
display: flex;
align-items: stretch;
}
.adm-provider-combo input {
flex: 1;
min-width: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.adm-provider-btn {
width: 100%;
display: flex; align-items: center; gap: 8px;
background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 5px 8px; font-family: inherit; font-size: 12px;
cursor: pointer; text-align: left;
}
.adm-provider-combo .adm-provider-btn {
width: 128px;
flex-shrink: 0;
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
justify-content: space-between;
}
.adm-provider-btn:hover { border-color: color-mix(in srgb, var(--fg) 30%, var(--border)); }
.adm-provider-current { flex: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.adm-provider-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.adm-provider-logo {
width: 14px; height: 14px;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0; color: var(--fg); opacity: 0.85;
}
.adm-provider-logo svg { width: 14px; height: 14px; }
.adm-provider-logo:empty {
background: color-mix(in srgb, var(--fg) 12%, transparent);
border-radius: 50%;
}
.adm-provider-caret { flex-shrink: 0; opacity: 0.5; transition: transform 0.15s; }
.adm-provider-picker:has(.adm-provider-menu:not(.hidden)) .adm-provider-caret {
transform: rotate(180deg);
}
.adm-provider-menu {
position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 100;
max-height: 280px; overflow-y: auto;
background: var(--panel);
border: 1px solid var(--border); border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
padding: 4px;
}
.adm-provider-menu.hidden { display: none; }
.adm-provider-item {
display: flex; align-items: center; gap: 8px;
padding: 5px 8px; border-radius: 4px;
font-size: 12px; cursor: pointer;
}
.adm-provider-item:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.adm-provider-item.active { background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); }
/* When the Appearance tab is open, the #settings-modal-opacity slider (in
the header) fades the modal so the user can see the rest of the UI react
as toggles flip. The fade is applied as a background color-mix in JS
(settings.js) rather than element opacity, so the controls/text stay
crisp — mirroring the Theme customizer's opacity slider. */
/* Search provider fallback chain — chips + drag-reorder */
.search-fallback-chain {
flex: 1; display: flex; align-items: center;
flex-wrap: wrap; gap: 6px; min-height: 28px;
}
.search-fb-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 4px 3px 6px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid var(--border); border-radius: 10px;
font-size: 11px; cursor: grab; user-select: none;
}
.search-fb-chip.dragging { opacity: 0.4; }
.search-fb-grip { opacity: 0.35; font-size: 10px; line-height: 1; cursor: grab; }
.search-fb-logo {
width: 12px; height: 12px;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0; opacity: 0.85;
}
.search-fb-logo svg { width: 12px; height: 12px; }
.search-fb-remove {
background: none; border: none; color: var(--fg);
opacity: 0.45; cursor: pointer;
font-size: 14px; line-height: 1; padding: 0 2px;
transition: opacity 0.15s, color 0.15s;
}
.search-fb-remove:hover { opacity: 1; color: var(--red); }
.search-fb-add {
background: var(--bg); color: var(--fg);
border: 1px dashed var(--border); border-radius: 10px;
padding: 2px 6px; font-family: inherit; font-size: 11px;
outline: none; cursor: pointer;
}
.search-fb-add:focus,
.search-fb-add:hover {
border-color: var(--accent, var(--red));
border-style: solid;
}
.mcp-tools-search {
width: 100%;
box-sizing: border-box;
margin-bottom: 6px;
padding: 5px 8px;
font-size: 11px;
font-family: inherit;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
outline: none;
}
.mcp-tools-search:focus {
border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
}
.mcp-tools-count {
font-size: 11px;
font-weight: 600;
opacity: 0.7;
}
/* Badges */
.admin-badge {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--red) 20%, transparent);
color: var(--red);
font-weight: 600;
}
.admin-badge-off {
background: color-mix(in srgb, var(--color-error) 20%, transparent);
color: var(--color-error);
}
/* Buttons */
.admin-btn-delete {
background: none;
border: 1px solid color-mix(in srgb, var(--color-error) 27%, transparent);
color: var(--color-error);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 11px;
font-family: inherit;
transition: all 0.15s;
}
.admin-btn-delete:hover {
background: var(--color-error);
border-color: var(--color-error);
color: #fff;
}
.admin-btn-add {
padding: 4px 10px;
border: none;
border-radius: 6px;
background: var(--red);
color: #fff;
cursor: pointer;
font-weight: 600;
font-size: 11px;
font-family: inherit;
transition: all 0.15s;
}
.admin-btn-add:hover {
background: color-mix(in srgb, var(--red) 80%, white);
}
.admin-btn-add:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.admin-btn-sm {
padding: 3px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
color: var(--fg);
cursor: pointer;
font-size: 11px;
}
.admin-btn-sm:hover {
background: var(--border);
border-color: var(--red);
}
.admin-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid var(--border);
border-top-color: var(--red);
border-radius: 50%;
animation: admin-spin 0.6s linear infinite;
vertical-align: -2px;
margin-right: 2px;
}
@keyframes admin-spin {
to { transform: rotate(360deg); }
}
/* Forms */
.admin-add-form {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.admin-add-form input {
flex: 1;
min-width: 120px;
padding: 5px 8px;
height: 32px;
box-sizing: border-box;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-family: inherit;
font-size: 12px;
}
.admin-add-form input:focus { outline: none; border-color: var(--red); }
.admin-switch-inline {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: color-mix(in srgb, var(--fg) 60%, transparent);
cursor: default;
}
.admin-model-form {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
}
.admin-model-form input,
.admin-model-form select {
padding: 5px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 12px;
}
.admin-model-form input:focus { outline: none; border-color: var(--red); }
.admin-model-form-row {
display: flex;
gap: 6px;
}
.admin-model-form-row input { flex: 1; }
.adm-ep-inline-msg {
min-height: 16px;
margin-top: 5px;
font-size: 11px;
}
/* Endpoint items */
.admin-ep-item {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: var(--bg);
border-radius: 6px;
margin-bottom: 4px;
font-size: 12px;
}
.admin-ep-info { flex: 1; overflow: hidden; }
.admin-ep-name { color: var(--fg); font-weight: 500; }
.admin-ep-detail {
color: color-mix(in srgb, var(--fg) 40%, transparent);
font-size: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-ep-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
/* Status messages */
.admin-error { color: var(--color-error); font-size: 11px; margin-top: 4px; }
.admin-success { color: var(--color-success); font-size: 11px; margin-top: 4px; }
.admin-empty {
color: color-mix(in srgb, var(--fg) 40%, transparent);
font-size: 12px;
padding: 10px 0;
text-align: center;
}
/* Endpoint-list empty states ("No endpoints configured" / "None") get a
left-aligned, accent-colored treatment so they read as "this row is your
placeholder, click Add" rather than a centered "nothing here" grey blob. */
#adm-epList-local .admin-empty,
#adm-epList-api .admin-empty {
text-align: left;
padding: 8px 4px;
color: var(--accent-primary, var(--accent, var(--red)));
}
/* RAG upload zone */
.admin-rag-upload-zone {
border: 2px dashed var(--border);
border-radius: 8px;
padding: 14px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
margin-bottom: 8px;
}
.admin-rag-upload-zone:hover,
.admin-rag-upload-zone.dragover {
border-color: var(--red);
background: color-mix(in srgb, var(--red) 7%, transparent);
}
.admin-rag-upload-zone p {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 11px;
margin-top: 4px;
}
.admin-rag-icon { font-size: 18px; }
.admin-rag-dir-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
}
.admin-rag-dir-row input {
flex: 1;
padding: 5px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 12px;
}
.admin-rag-dir-row input:focus { outline: none; border-color: var(--red); }
.admin-rag-dir-row button {
padding: 5px 8px;
border: none;
border-radius: 6px;
background: var(--red);
color: #fff;
cursor: pointer;
font-size: 12px;
white-space: nowrap;
}
.admin-rag-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 6px;
}
.admin-rag-section-label {
font-size: 10px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 8px 0 4px;
}
.admin-rag-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background: var(--bg);
border-radius: 6px;
margin-bottom: 4px;
font-size: 12px;
}
.admin-rag-item-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fg);
}
.admin-rag-item-meta {
color: color-mix(in srgb, var(--fg) 40%, transparent);
font-size: 10px;
margin: 0 6px;
flex-shrink: 0;
}
.admin-rag-item .admin-btn-delete { font-size: 10px; padding: 2px 6px; }
.admin-rag-status {
font-size: 11px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
margin-top: 6px;
}
/* Token reveal */
.admin-token-reveal {
margin-top: 8px;
padding: 8px;
background: color-mix(in srgb, var(--red) 8%, var(--bg));
border: 1px solid color-mix(in srgb, var(--red) 25%, var(--border));
border-radius: 8px;
}
/* Selects in admin cards */
.admin-card select {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 12px;
}
.admin-card input {
font-family: inherit;
font-size: 12px;
}
.admin-card label {
font-size: 12px;
}
/* ---- Document Library ---- */
.doclib-modal-content {
width: min(600px, 92vw);
max-height: 85vh;
font-size: 12px;
background: var(--bg);
}
/* Anchor doclib modal to top so only the bottom edge moves on resize */
#doclib-modal {
align-items: flex-start;
padding-top: 8vh;
}
.doclib-modal-content .modal-header h4 {
font-size: 1rem;
}
/* The "open in new tab" email modal inherited the 1rem doclib header, which
read way bigger than the email subject in the regular (inline) reader. Pin
it smaller so the two views are consistent. */
.email-reader-tab-modal .modal-header h4 {
font-size: 13px;
font-weight: 600;
}
/* Match the sticky modal-header to the doclib modal-content body color
(var(--bg)) instead of the global var(--panel) default — otherwise the
header reads as a darker stripe above the email list. */
.doclib-modal-content > .modal-header {
background: var(--bg);
}
.doclib-stats {
font-size: 11px;
color: var(--fg-muted);
margin-bottom: 8px;
}
.doclib-toolbar {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.doclib-search {
flex: 1;
padding: 6px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 12px;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
#archive-select-btn { height: auto; padding: 5px 8px; }
.doclib-search:focus {
border-color: var(--red);
}
.doclib-sort {
padding: 6px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-size: 12px;
font-family: inherit;
outline: none;
cursor: pointer;
}
.doclib-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.doclib-chip {
padding: 2px 10px;
border-radius: 12px;
font-size: 10px;
border: 1px solid var(--border);
background: transparent;
color: var(--fg-muted);
cursor: pointer;
user-select: none;
transition: background 0.15s, border-color 0.15s;
}
.doclib-chip:hover {
border-color: var(--red);
}
.doclib-chip.active {
background: color-mix(in srgb, var(--red) 15%, transparent);
border-color: color-mix(in srgb, var(--red) 40%, transparent);
color: var(--red);
}
/* Document library — language chip row */
.doclib-lang-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 2px 0;
max-height: 52px;
overflow-y: auto;
}
.doclib-lang-chips:empty { display: none; }
/* Mobile: keep tag chips on ONE row and scroll horizontally instead of wrapping
into a tall multi-line block (Serve, library — anywhere .doclib-lang-chips is used). */
@media (max-width: 768px) {
.doclib-lang-chips {
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
max-height: none;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.doclib-lang-chips::-webkit-scrollbar { display: none; }
.doclib-lang-chips > * { flex-shrink: 0; }
}
#doclib-tidy-btn, #doclib-select-btn, #doclib-chats-tidy-btn {
position: relative;
top: -3px;
}
/* Document library — list layout */
.doclib-grid {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 400px;
overflow-y: auto;
padding: 0;
position: relative;
}
/* Gallery grid domino-in cascade on open — same recipe as the document/email
libraries. Applied to #gallery-grid in gallery.js on the first render after
each open, removed after the longest delay so re-renders feel instant. */
.gallery-just-opened > .gallery-card {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.gallery-just-opened > :nth-child(1) { animation-delay: 0.02s; }
.gallery-just-opened > :nth-child(2) { animation-delay: 0.04s; }
.gallery-just-opened > :nth-child(3) { animation-delay: 0.06s; }
.gallery-just-opened > :nth-child(4) { animation-delay: 0.08s; }
.gallery-just-opened > :nth-child(5) { animation-delay: 0.10s; }
.gallery-just-opened > :nth-child(6) { animation-delay: 0.12s; }
.gallery-just-opened > :nth-child(7) { animation-delay: 0.14s; }
.gallery-just-opened > :nth-child(8) { animation-delay: 0.16s; }
.gallery-just-opened > :nth-child(9) { animation-delay: 0.18s; }
.gallery-just-opened > :nth-child(10) { animation-delay: 0.20s; }
.gallery-just-opened > :nth-child(11) { animation-delay: 0.22s; }
.gallery-just-opened > :nth-child(12) { animation-delay: 0.24s; }
.gallery-just-opened > :nth-child(13) { animation-delay: 0.26s; }
.gallery-just-opened > :nth-child(14) { animation-delay: 0.28s; }
.gallery-just-opened > :nth-child(15) { animation-delay: 0.30s; }
.gallery-just-opened > :nth-child(16) { animation-delay: 0.32s; }
.gallery-just-opened > :nth-child(17) { animation-delay: 0.34s; }
.gallery-just-opened > :nth-child(18) { animation-delay: 0.36s; }
.gallery-just-opened > :nth-child(19) { animation-delay: 0.38s; }
.gallery-just-opened > :nth-child(20) { animation-delay: 0.40s; }
.gallery-just-opened > :nth-child(n+21) { animation-delay: 0.42s; }
/* Tasks list domino-in cascade on open — mirrors gallery / doclib. Applied to
#tasks-list in tasks.js on the first render after each open, removed after
the longest delay so re-renders feel instant. */
.tasks-just-opened > .task-card {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.tasks-just-opened > :nth-child(1) { animation-delay: 0.02s; }
.tasks-just-opened > :nth-child(2) { animation-delay: 0.04s; }
.tasks-just-opened > :nth-child(3) { animation-delay: 0.06s; }
.tasks-just-opened > :nth-child(4) { animation-delay: 0.08s; }
.tasks-just-opened > :nth-child(5) { animation-delay: 0.10s; }
.tasks-just-opened > :nth-child(6) { animation-delay: 0.12s; }
.tasks-just-opened > :nth-child(7) { animation-delay: 0.14s; }
.tasks-just-opened > :nth-child(8) { animation-delay: 0.16s; }
.tasks-just-opened > :nth-child(9) { animation-delay: 0.18s; }
.tasks-just-opened > :nth-child(10) { animation-delay: 0.20s; }
.tasks-just-opened > :nth-child(11) { animation-delay: 0.22s; }
.tasks-just-opened > :nth-child(12) { animation-delay: 0.24s; }
.tasks-just-opened > :nth-child(13) { animation-delay: 0.26s; }
.tasks-just-opened > :nth-child(14) { animation-delay: 0.28s; }
.tasks-just-opened > :nth-child(15) { animation-delay: 0.30s; }
.tasks-just-opened > :nth-child(16) { animation-delay: 0.32s; }
.tasks-just-opened > :nth-child(17) { animation-delay: 0.34s; }
.tasks-just-opened > :nth-child(18) { animation-delay: 0.36s; }
.tasks-just-opened > :nth-child(19) { animation-delay: 0.38s; }
.tasks-just-opened > :nth-child(20) { animation-delay: 0.40s; }
.tasks-just-opened > :nth-child(n+21) { animation-delay: 0.42s; }
/* Document library cascade — same recipe as email library, applied per-tab
the first time content loads (chats / archive / research / documents).
Module-level Set in documentLibrary.js prevents re-firing on tab swaps
or filter/sort re-renders within the same page session. */
.doclib-just-opened > .memory-item,
.doclib-just-opened > .doclib-card {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.doclib-just-opened > :nth-child(1) { animation-delay: 0.02s; }
.doclib-just-opened > :nth-child(2) { animation-delay: 0.04s; }
.doclib-just-opened > :nth-child(3) { animation-delay: 0.06s; }
.doclib-just-opened > :nth-child(4) { animation-delay: 0.08s; }
.doclib-just-opened > :nth-child(5) { animation-delay: 0.10s; }
.doclib-just-opened > :nth-child(6) { animation-delay: 0.12s; }
.doclib-just-opened > :nth-child(7) { animation-delay: 0.14s; }
.doclib-just-opened > :nth-child(8) { animation-delay: 0.16s; }
.doclib-just-opened > :nth-child(9) { animation-delay: 0.18s; }
.doclib-just-opened > :nth-child(10) { animation-delay: 0.20s; }
.doclib-just-opened > :nth-child(11) { animation-delay: 0.22s; }
.doclib-just-opened > :nth-child(12) { animation-delay: 0.24s; }
.doclib-just-opened > :nth-child(13) { animation-delay: 0.26s; }
.doclib-just-opened > :nth-child(14) { animation-delay: 0.28s; }
.doclib-just-opened > :nth-child(15) { animation-delay: 0.30s; }
.doclib-just-opened > :nth-child(16) { animation-delay: 0.32s; }
.doclib-just-opened > :nth-child(17) { animation-delay: 0.34s; }
.doclib-just-opened > :nth-child(18) { animation-delay: 0.36s; }
.doclib-just-opened > :nth-child(19) { animation-delay: 0.38s; }
.doclib-just-opened > :nth-child(20) { animation-delay: 0.40s; }
.doclib-just-opened > :nth-child(n+21) { animation-delay: 0.42s; }
/* Domino cascade on first open of the email library — mirrors the sidebar
.section-just-expanded recipe so list items spring in staggered. Class
is applied to #email-lib-grid in emailLibrary.js on the first render
only, then removed after the longest delay so re-renders feel instant. */
.email-lib-just-opened .doclib-card {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.email-lib-just-opened .doclib-card:nth-child(1) { animation-delay: 0.02s; }
.email-lib-just-opened .doclib-card:nth-child(2) { animation-delay: 0.04s; }
.email-lib-just-opened .doclib-card:nth-child(3) { animation-delay: 0.06s; }
.email-lib-just-opened .doclib-card:nth-child(4) { animation-delay: 0.08s; }
.email-lib-just-opened .doclib-card:nth-child(5) { animation-delay: 0.10s; }
.email-lib-just-opened .doclib-card:nth-child(6) { animation-delay: 0.12s; }
.email-lib-just-opened .doclib-card:nth-child(7) { animation-delay: 0.14s; }
.email-lib-just-opened .doclib-card:nth-child(8) { animation-delay: 0.16s; }
.email-lib-just-opened .doclib-card:nth-child(9) { animation-delay: 0.18s; }
.email-lib-just-opened .doclib-card:nth-child(10) { animation-delay: 0.20s; }
.email-lib-just-opened .doclib-card:nth-child(11) { animation-delay: 0.22s; }
.email-lib-just-opened .doclib-card:nth-child(12) { animation-delay: 0.24s; }
.email-lib-just-opened .doclib-card:nth-child(13) { animation-delay: 0.26s; }
.email-lib-just-opened .doclib-card:nth-child(14) { animation-delay: 0.28s; }
.email-lib-just-opened .doclib-card:nth-child(15) { animation-delay: 0.30s; }
.email-lib-just-opened .doclib-card:nth-child(16) { animation-delay: 0.32s; }
.email-lib-just-opened .doclib-card:nth-child(17) { animation-delay: 0.34s; }
.email-lib-just-opened .doclib-card:nth-child(18) { animation-delay: 0.36s; }
.email-lib-just-opened .doclib-card:nth-child(19) { animation-delay: 0.38s; }
.email-lib-just-opened .doclib-card:nth-child(20) { animation-delay: 0.40s; }
/* Cap the cascade at 20 — beyond that they animate together so opening a
long inbox doesn't take forever to settle. */
.email-lib-just-opened .doclib-card:nth-child(n+21) { animation-delay: 0.42s; }
/* Inside the email library modal, the grid needs to grow with the modal —
but `flex: 1 1 0` collapses to 0 when the parent isn't height-constrained
(which is the non-fullscreen default). Use `auto` basis + a sensible
`min-height` floor so it shows ~400px naturally and absorbs more when
the modal is resized / fullscreened. */
#email-lib-modal .doclib-grid {
max-height: none;
height: auto;
flex: 1 1 auto;
min-height: 400px;
}
/* ── Mobile compose FAB (email) ──
Replaces the top-right "New" button on phones with a round mail-icon button
bottom-right that collapses to a circle while the list scrolls and springs
back out to "New" when scrolling stops. Desktop is unchanged. */
.email-lib-fab { display: none; }
@media (max-width: 768px) {
/* The absolute FAB anchors to the email panel card. */
#email-lib-modal .admin-card { position: relative; }
/* Hide the toolbar "New" button — the FAB is the mobile entry point. */
#email-lib-compose-btn { display: none; }
#email-lib-modal .email-lib-fab {
--fab-size: 56px;
display: flex;
align-items: center;
gap: 9px;
/* Hidden until the email list has rendered; JS adds .fab-revealed to pop
it in (scale-from-center). Avoids the button flashing at the top and
sliding down before _positionFab() places it. */
transform: scale(0);
opacity: 0;
transform-origin: center center;
position: absolute;
right: calc(16px + env(safe-area-inset-right, 0px));
bottom: calc(18px + env(safe-area-inset-bottom, 0px));
height: var(--fab-size);
min-width: var(--fab-size);
padding: 0 20px 0 18px;
border: none;
border-radius: 999px;
background: var(--accent-primary, var(--red));
color: #fff;
font-family: inherit;
font-size: 15px;
font-weight: 600;
line-height: 1;
cursor: pointer;
box-shadow: 0 6px 18px rgba(0,0,0,.38), 0 2px 6px rgba(0,0,0,.28);
z-index: 30;
pointer-events: auto;
/* This (base) transition governs the EXPAND — slower + graceful. */
transition:
padding 420ms cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 240ms ease,
transform 140ms ease,
background 140ms ease;
will-change: padding, transform;
}
#email-lib-modal .email-lib-fab svg { flex: 0 0 auto; display: block; }
#email-lib-modal .email-lib-fab .email-lib-fab-label {
overflow: hidden;
white-space: nowrap;
max-width: 120px;
opacity: 1;
/* EXPAND timing — label eases out a touch after the width opens up. */
transition:
max-width 420ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 300ms 60ms cubic-bezier(0.22, 1, 0.36, 1),
margin 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Collapsed (while scrolling) → pure circle, icon only. We DON'T set an
explicit width: the container is auto-width and shrinks smoothly as the
padding + label max-width animate (min-width keeps it circular). Setting a
fixed width here made the collapse snap, since auto↔px width can't tween.
This (.collapsed) transition governs the COLLAPSE — kept snappy. */
#email-lib-modal .email-lib-fab.collapsed {
padding: 0;
justify-content: center;
transition:
padding 230ms cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 200ms ease,
transform 140ms ease;
}
#email-lib-modal .email-lib-fab.collapsed .email-lib-fab-label {
max-width: 0;
opacity: 0;
margin-left: -9px;
transition:
max-width 230ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 150ms cubic-bezier(0.4, 0, 0.2, 1),
margin 230ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Entrance: circle-expand-from-middle pop, played once when revealed. No
`forwards` fill so the resting transform (scale 1) comes from this rule —
that keeps the :active press transform working afterwards. */
#email-lib-modal .email-lib-fab.fab-revealed {
transform: scale(1);
opacity: 1;
animation: emailFabPop 380ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
#email-lib-modal .email-lib-fab:active {
transform: scale(0.93);
filter: brightness(0.92);
box-shadow: 0 3px 10px rgba(0,0,0,.4);
}
}
@keyframes emailFabPop {
from { transform: scale(0); opacity: 0; }
60% { transform: scale(1.06); opacity: 1; }
to { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
#email-lib-modal .email-lib-fab,
#email-lib-modal .email-lib-fab .email-lib-fab-label { transition-duration: 1ms; }
#email-lib-modal .email-lib-fab.fab-revealed { animation: none; }
}
/* When the modal-content has explicit height (fullscreen or user-resized),
drop the floor so the grid sizes purely from flex. */
#email-lib-modal.email-lib-fullscreen .doclib-grid,
#email-lib-modal .modal-content[style*="height"] .doclib-grid {
min-height: 0;
}
/* Document library: same fix as the email library (#74). When fullscreened
the modal-content gets an inline `height: 100vh`, but the inner
.doclib-grid stays capped at the 400px default so the list looks like a
tiny strip floating in a giant empty modal. Mirror the email recipe:
make the modal a flex column, give the body/admin-card claim of the
remaining height, and let the grid take the rest. Scoped to the
fullscreen class so windowed-mode layout is unchanged. */
/* Inner flex chain that lets the chats/documents grid claim the full
remaining height of the modal. Applies in BOTH fullscreen AND
right-docked states — without the docked state included, dragging a
fullscreen doclib to the right snap zone breaks the flex layout
because exitFullscreen removes .doclib-fullscreen, and the grid
falls back to its base max-height: 400px showing only ~8 items.
The CSS variable :is() selector lets both states share one rule. */
#doclib-modal.doclib-fullscreen .doclib-modal-content,
#doclib-modal.modal-right-docked .doclib-modal-content,
#doclib-modal.modal-left-docked .doclib-modal-content {
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Fullscreen positioning — scoped to NOT apply when also right-docked.
Without this exclusion, dragging a fullscreen doclib to the right snap
zone keeps .doclib-fullscreen on the modal and these !important rules
override the dock's inline styles, leaving the modal stuck fullscreen
instead of becoming a side panel. */
#doclib-modal.doclib-fullscreen:not(.modal-right-docked) .doclib-modal-content {
position: fixed !important;
top: 0 !important;
left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
right: 0 !important;
bottom: 0 !important;
width: auto !important;
max-width: none !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
transform: none !important;
}
#doclib-modal.doclib-fullscreen .modal-header,
#doclib-modal.doclib-fullscreen .lib-tabs,
#doclib-modal.modal-right-docked .modal-header,
#doclib-modal.modal-right-docked .lib-tabs,
#doclib-modal.modal-left-docked .modal-header,
#doclib-modal.modal-left-docked .lib-tabs {
flex: 0 0 auto;
}
#doclib-modal.doclib-fullscreen .modal-body,
#doclib-modal.modal-right-docked .modal-body,
#doclib-modal.modal-left-docked .modal-body {
flex: 1 1 0;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
#doclib-modal.doclib-fullscreen .admin-card,
#doclib-modal.modal-right-docked .admin-card,
#doclib-modal.modal-left-docked .admin-card {
flex: 1 1 0;
min-height: 0;
display: flex;
flex-direction: column;
}
#doclib-modal.doclib-fullscreen .doclib-grid:not(:has(.doclib-card-expanded)),
#doclib-modal.modal-right-docked .doclib-grid:not(:has(.doclib-card-expanded)),
#doclib-modal.modal-left-docked .doclib-grid:not(:has(.doclib-card-expanded)) {
max-height: none;
height: auto;
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
}
/* "Reply" docks the open email modal to the left as a sidebar so the doc
panel (which slides in from the right of the chat area) is visible
side-by-side. The .modal backdrop is already pointer-events:none for
email modals — only the content takes pointer events — so the chat /
doc panel underneath stays interactive. */
.modal.email-snap-left {
align-items: stretch;
justify-content: flex-start;
left: 0 !important;
width: 100% !important;
}
.modal.email-snap-left .modal-content {
left: var(--email-doc-split-left-x, 0px) !important;
width: var(--email-doc-split-email-w, 420px) !important;
max-width: var(--email-doc-split-email-w, 420px) !important;
border-right: 1px solid var(--border);
box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18);
border-radius: 0;
}
@media (min-width: 769px) {
body.email-doc-split-active.doc-view #email-lib-modal,
body.email-doc-split-active.doc-view.email-front #email-lib-modal,
body.email-doc-split-active.doc-view .modal[id^="email-reader-"],
body.email-doc-split-active.doc-view.email-front .modal[id^="email-reader-"] {
z-index: 150 !important;
}
body.email-doc-split-active #email-lib-modal.email-snap-left,
body.email-doc-split-active #email-lib-modal.modal-left-docked {
left: var(--email-doc-split-left-x, 0px) !important;
width: var(--email-doc-split-email-w, 420px) !important;
overflow: hidden !important;
z-index: 155 !important;
}
body.email-doc-split-active #email-lib-modal.email-snap-left .modal-content,
body.email-doc-split-active #email-lib-modal.modal-left-docked .modal-content,
body.email-doc-split-active .modal[id^="email-reader-"].email-snap-left .modal-content,
body.email-doc-split-active .modal[id^="email-reader-"].modal-left-docked .modal-content {
position: absolute !important;
left: 0 !important;
top: 0 !important;
bottom: 0 !important;
right: auto !important;
width: var(--email-doc-split-email-w, 420px) !important;
max-width: var(--email-doc-split-email-w, 420px) !important;
height: 100vh !important;
max-height: 100vh !important;
transform: none !important;
margin: 0 !important;
}
body.email-doc-split-active.doc-view .doc-divider {
display: none !important;
}
body.email-doc-split-active.doc-view .doc-editor-pane {
position: fixed !important;
left: var(--email-doc-split-right-x, 420px) !important;
right: 0 !important;
top: 0 !important;
bottom: 0 !important;
width: auto !important;
max-width: none !important;
height: 100vh !important;
z-index: 260 !important;
margin-top: 0 !important;
transform: none !important;
}
}
/* Email reader "Search text in this thread" / from-sender toggle —
DISABLED on all viewports while the search/threaded-sidebar UX is too
buggy to ship. Re-enable by removing this rule + the JS .remove()
guards in emailLibrary.js. */
body [data-act="from-sender"] {
display: none !important;
}
/* Snap-to-right docking. A modal dragged to the right edge becomes a
docked side panel (mirrors Notes/Doc panels). Body reserves space via
padding-right so the chat / notes / doc panel underneath shrinks to
fit instead of being hidden behind the panel. */
body.right-dock-active {
padding-right: var(--right-dock-w, 0px);
transition: padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
body.left-dock-active {
padding-left: var(--left-dock-w, 0px);
transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.modal.modal-right-docked {
align-items: stretch;
justify-content: flex-end;
}
.modal.modal-right-docked .modal-content {
border-left: 1px solid var(--border);
box-shadow: -4px 0 14px rgba(0, 0, 0, 0.18);
border-radius: 0;
}
.modal.modal-left-docked {
align-items: stretch;
justify-content: flex-start;
}
.modal.modal-left-docked .modal-content {
border-right: 1px solid var(--border);
box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18);
border-radius: 0;
/* Pin transitions OFF on the dock's positioning properties. The wide
sidebar collapse/expand changes --sidebar-w instantly, which means
the docked modal's `left: calc(...)` jumps by ~240px. Any CSS
transition on `left` (inherited or otherwise) would animate that
jump as a fly-across. Same defense for right-docked / fullscreen
panels in case future themes add a generic transition. */
transition: none !important;
}
.modal.modal-right-docked .modal-content,
#email-lib-modal.email-lib-fullscreen .modal-content,
#doclib-modal.doclib-fullscreen .doclib-modal-content {
transition: none !important;
}
.modal.modal-right-docked .email-reader-header,
.modal.modal-left-docked .email-reader-header {
flex-direction: column;
gap: 6px;
}
.modal.modal-right-docked .email-reader-actions,
.modal.modal-left-docked .email-reader-actions {
align-self: flex-end;
}
.modal.modal-right-docked .email-reader-meta-row,
.modal.modal-left-docked .email-reader-meta-row {
display: grid;
grid-template-columns: 1fr;
gap: 2px;
align-items: start;
}
.modal.modal-right-docked .email-reader-meta-row strong,
.modal.modal-left-docked .email-reader-meta-row strong {
min-width: 0;
}
.modal.modal-right-docked .recipient-chip,
.modal.modal-left-docked .recipient-chip {
max-width: 100%;
}
.archive-list {
margin-top: 8px;
border-top: 1px solid var(--border);
padding-top: 8px;
}
#archive-modal .memory-sort-select,
#archive-modal .memory-toolbar-btn {
height: 32px;
}
.archive-menu-btn {
position: relative;
top: 2px;
}
#archive-chips .doclib-chip {
height: 22px;
display: inline-flex;
align-items: center;
font-size: 10px;
padding: 0 8px;
}
/* Library tab panels — Chats / Archive / Research / Documents share the
same toolbar dimensions so the sort dropdown + Select button line up
identically across tabs. */
#doclib-panel-chats .memory-sort-select,
#doclib-panel-archive .memory-sort-select,
#doclib-panel-research .memory-sort-select,
[data-doclib-panel="documents"] .memory-sort-select {
height: 24px;
font-size: 11px;
width: 86px;
}
#doclib-panel-chats .memory-toolbar-btn,
#doclib-panel-archive .memory-toolbar-btn,
#doclib-panel-research .memory-toolbar-btn,
[data-doclib-panel="documents"] .memory-toolbar-btn {
height: 24px;
font-size: 11px;
position: relative;
top: -3px;
}
/* Research's Recent (sort) + Select sat 1px lower than the other tabs — nudge up. */
#doclib-panel-research .memory-sort-select { position: relative; top: 1.5px; }
#doclib-panel-research .memory-toolbar-btn { top: -4.5px; }
#doclib-research-search { position: relative; top: -1.5px; }
[data-doclib-panel] { padding-top: 6px !important; }
#doclib-panel-chats, #doclib-panel-archive { padding-top: 14px !important; }
#doclib-panel-chats .memory-desc, #doclib-panel-archive .memory-desc, #doclib-panel-research .memory-desc { margin-top: 7px; }
#doclib-panel-chats .memory-search-input,
#doclib-panel-archive .memory-search-input {
height: 30px !important;
min-height: 30px !important;
flex-shrink: 0;
font-size: 11px;
}
/* Unified search bar across Library tabs + Memory modal — same height,
same magnifying-glass icon at the start, consistent padding. */
#doclib-panel-chats .memory-search-input,
#doclib-panel-archive .memory-search-input,
#doclib-panel-research .memory-search-input,
[data-doclib-panel="documents"] .memory-search-input,
#memory-modal .memory-search-input,
#tasks-modal .memory-search-input {
height: 30px;
min-height: 30px;
font-size: 11px;
background-image: url("data:image/svg+xml;utf8, ");
background-repeat: no-repeat;
background-position: 9px center;
padding-left: 28px;
}
#doclib-panel-chats .doclib-chip {
height: 28px;
}
#doclib-panel-archive .doclib-chip {
height: 28px;
}
#doclib-panel-chats .doclib-chip,
#doclib-panel-archive .doclib-chip {
display: inline-flex;
align-items: center;
font-size: 9px;
padding: 0 8px;
border-radius: 10px;
}
.doclib-grid:has(.doclib-card-expanded) > .doclib-card:not(.doclib-card-expanded) {
display: none;
}
/* Hide everything except the grid when a card is expanded */
.admin-card:has(.doclib-card-expanded) > *:not(.doclib-grid):not(.hwfit-cached-list) {
opacity: 0;
max-height: 0;
overflow: hidden;
pointer-events: none;
margin: 0 !important;
padding: 0 !important;
transition: opacity 0.12s ease, max-height 0.2s ease;
}
.admin-card:has(.doclib-card-expanded) > .memory-bulk-bar {
display: none;
}
.admin-card:has(.doclib-card-expanded) > .doclib-grid,
.admin-card:has(.doclib-card-expanded) > .hwfit-cached-list {
max-height: none !important;
min-height: 0 !important;
overflow: visible !important;
}
/* Firefox-mobile (no :has()) fallback: when reading an email, fully remove the
list-mode chrome (accounts row, toolbar, bulk bar, desc) — display:none so no
residual flex-gap leaves an empty strip above the reader — and zero the
admin-card gap so the grid/reader starts flush at the top. */
#email-lib-modal.email-reading .admin-card > *:not(.doclib-grid):not(.hwfit-cached-list) {
display: none !important;
}
#email-lib-modal.email-reading .admin-card { gap: 0 !important; }
/* Override for chat-rows + research-rows: those expand inline with a
bounded preview (max-height: 60vh), so the grid must stay clipped
and scrollable. Without this scoping the overflow:visible above lets
the expanded chat preview escape the grid and overlap whatever's
underneath/beside the modal (e.g., a docked sibling panel) — what
the user calls "merging with other windows". */
.admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid {
overflow: hidden auto !important;
}
/* Email modal needs the grid to STAY a constrained flex container when
expanded — overflow:visible (set above for cookbook) lets content escape
instead of letting the expanded reader fill via flex:1. We keep the same
max-height:none + min-height:0 but flip overflow back so the reader
actually fills the modal. */
#email-lib-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid,
#email-lib-modal.email-reading .admin-card > .doclib-grid {
overflow: hidden !important;
display: flex !important;
flex: 1 1 auto !important;
}
#email-lib-modal.email-reading .doclib-modal-content {
min-height: var(--email-reading-modal-min-h, auto);
}
#email-lib-modal .doclib-card.doclib-card-expanded {
flex: 1 1 auto !important;
height: 100% !important;
/* Neutral frame on the active email — the accent border + glow felt
overbearing on desktop; the size jump alone is enough signal. */
border: 1px solid var(--border) !important;
box-shadow: 0 6px 18px rgba(0,0,0,0.12) !important;
animation: none !important;
}
/* Desktop-only, ONLY on the currently-expanded email card. Nudges the title
row down 6px / right 2px, bolds the subject, hides the timestamp from the
meta line (the reader header carries its own date), and pulls the prev/next
nav arrows in 4px. */
@media (min-width: 769px) {
#email-lib-modal .doclib-card-expanded .email-card-titlerow,
#email-lib-modal .doclib-card-expanded .email-card-titlerow > * {
position: relative;
top: 6px;
left: 2px;
}
/* Smaller subject — the reader header below already carries weight. */
#email-lib-modal .doclib-card-expanded .email-card-titlerow .memory-item-title {
font-weight: 700;
font-size: 12px;
}
/* A bit more breathing room around the title row so the smaller bold text
doesn't crowd the meta line below. (Timestamp kept — user changed mind.) */
#email-lib-modal .doclib-card-expanded .email-card-titlerow {
padding: 4px 0 6px;
line-height: 1.35;
}
/* Nudge the date down/left in the meta row, and give the meta line +4px of
extra bottom space so the shifted date isn't clipped. */
#email-lib-modal .doclib-card-expanded .email-meta-date {
position: relative;
top: 4px;
left: 0;
}
#email-lib-modal .doclib-card-expanded .memory-item-meta {
padding-bottom: 4px;
}
#email-lib-modal .doclib-card-expanded .email-card-nav-arrows {
position: relative;
left: -4px;
}
}
/* Skills modal: same mechanism as email above. The default expanded-grid
rule (overflow:visible, no flex) lets the card expand inline to ~content
height instead of filling — which is why the skill preview stopped at
partial height. Flip the grid back to a constrained flex container and
let the expanded card fill it, exactly like the email reader. */
#memory-modal .admin-card:has(.doclib-card-expanded) > .doclib-grid {
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
flex: 1 1 auto !important;
}
#memory-modal .doclib-card.doclib-card-expanded {
flex: 1 1 auto !important;
height: 100% !important;
}
/* When the from-sender sidebar is open inside the email card, the email
body would otherwise lose 280px to padding-right and read as a narrow
strip. Widen the whole modal so the body keeps its normal text width
and the sidebar appears alongside, not on top of it. Also force the
modal to its full max-height — short emails would otherwise leave the
sidebar squeezed into a tiny vertical strip. */
#email-lib-modal .modal-content:has(.from-sender-panel) {
width: min(1020px, 95vw) !important;
height: 85vh !important;
transition: width 0.22s ease-out, height 0.22s ease-out;
}
#email-lib-modal .modal-content {
transition: width 0.22s ease-out, height 0.22s ease-out;
}
/* Cookbook's cached-model list should scale with viewport height, not be capped at 400px */
.hwfit-cached-list {
max-height: min(75vh, 900px) !important;
}
/* Drag-and-drop visual hint for the email compose pane. Subtle accent
outline + tinted overlay so it's obvious files will attach if dropped. */
.doc-editor-pane.email-dragover {
outline: 2px dashed var(--accent, #2563eb);
outline-offset: -8px;
background: color-mix(in srgb, var(--accent, #2563eb) 6%, transparent);
}
.doc-editor-pane.email-dragover::after {
content: 'Drop to attach';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--accent, #2563eb);
color: #fff;
padding: 8px 16px;
border-radius: 6px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
}
/* Email reader window — make the body always fill the available vertical
space regardless of how the window is positioned/resized. The user can
drag the bottom-right corner to resize; the body re-flexes automatically. */
.email-window-modal .modal-content {
display: flex !important;
flex-direction: column !important;
resize: both;
overflow: hidden;
min-width: 320px;
min-height: 240px;
/* When user resizes/drags so the window covers a side, force the inner
body to fill all the way to the bottom. The default max-height: 85vh
(set inline by _makeDraggable's exitFullscreen) and 80vh from the
opener both cap at the *viewport* — if the browser window then changes
size (e.g. OS aero-snap), the modal stays at the old height and
truncates rows. Drop the cap whenever an explicit width OR height has
been written inline by the resize handle / drag handler. */
}
.email-window-modal .modal-content[style*="width"],
.email-window-modal .modal-content[style*="height"] {
max-height: calc(100vh - 16px) !important;
}
.email-window-modal .email-window-body {
flex: 1 1 auto !important;
min-height: 0 !important;
/* Explicit basis of 0 makes the body grow to fill remaining space rather
than its content's intrinsic height (which was capping the thread). */
flex-basis: 0 !important;
max-height: none !important;
overflow: auto !important;
overscroll-behavior: contain;
}
/* Individual email reader window — fullscreen mode (drag-to-top snap or
double-click header). Same recipe as the library modal: pin to viewport
edges and let the inner body absorb the remaining height. */
body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .modal-content {
position: fixed !important;
inset: 0 !important;
left: 0 !important;
top: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
transform: none !important;
display: flex !important;
flex-direction: column !important;
}
body:not(.email-doc-split-active) .email-window-modal.email-window-fullscreen .email-window-body {
flex: 1 1 auto !important;
min-height: 0 !important;
max-height: none !important;
}
/* Email library modal: fullscreen mode + larger mobile sheet (mirrors the
cookbook treatment so the toolbar and bulk actions stay reachable).
Leaves whichever navigation strip is currently visible on the left
(the wide sidebar OR the icon rail — they're mutually exclusive, so
one of the two CSS vars is 0 at any given time, set by init.js). */
body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content {
position: fixed !important;
top: 0 !important;
left: calc(var(--icon-rail-w, 48px) + var(--sidebar-w, 0px)) !important;
right: 0 !important;
bottom: 0 !important;
width: auto !important;
max-width: none !important;
height: 100vh !important;
max-height: 100vh !important;
border-radius: 0 !important;
transform: none !important;
}
/* Mobile: the /email route adds .email-lib-fullscreen, but the desktop full-
bleed rule above squares off the corners and offsets the email by the icon
rail. On phones the email should be the normal bottom-sheet — full width,
rounded top corners. Same specificity as the rule above + later in source,
so it wins on mobile. */
@media (max-width: 768px) {
body:not(.email-doc-split-active) #email-lib-modal.email-lib-fullscreen:not(.modal-right-docked) .modal-content {
left: 0 !important;
right: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
height: 100dvh !important;
max-height: 100dvh !important;
border-radius: 14px 14px 0 0 !important;
border-top: 1px solid var(--border) !important;
}
}
/* Make the inner panes actually grow to fill the fullscreen container
instead of staying at their natural size. The body owns the remaining
height below the header; the admin-card + grid then expand into it. */
#email-lib-modal.email-lib-fullscreen .modal-body {
flex: 1 1 auto !important;
min-height: 0 !important;
overflow: hidden !important;
}
#email-lib-modal.email-lib-fullscreen .modal-body > .admin-card {
flex: 1 1 auto !important;
min-height: 0 !important;
}
#email-lib-modal.email-lib-fullscreen .doclib-grid {
flex: 1 1 auto !important;
min-height: 0 !important;
max-height: none !important;
overflow: auto !important;
}
@media (max-width: 768px) {
/* Mobile email modal sizing — keep in sync with the rule earlier in the
file. 75dvh tall always so flex children (the expanded email reader)
have a real height to grow into. */
#email-lib-modal .modal-content {
max-height: 90dvh !important;
max-height: 90vh !important;
height: 90dvh !important;
height: 90vh !important;
}
/* Inner panes grow to fill the modal-content — without flex:1 on the
body, the expanded email reader sits in a tiny box because there's
nothing pushing it to take the remaining height. overflow:hidden +
min-height:0 lets each layer pass its constraints down. */
#email-lib-modal .modal-body,
#email-lib-modal .admin-card {
flex: 1 1 auto !important;
min-height: 0 !important;
overflow: hidden !important;
max-height: none !important;
}
#email-lib-modal .doclib-grid {
flex: 1 1 auto !important;
min-height: 0 !important;
max-height: none !important;
overflow-y: auto !important;
}
/* modal-content keeps its scroll OFF here — the inner flex children
(doclib-grid for the list view, email-reader-body for the expanded
reader) own the scroll surfaces. Without this, double-scroll layouts
trap touches and the reader can't claim full height. */
#email-lib-modal .modal-content {
overflow: hidden !important;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
touch-action: pan-y;
}
/* Attachment row on phones: cap to ~2 rows so it never dominates the
reader. If more chips exist, the row scrolls vertically. Smaller chip
padding + max-width keeps each chip compact so two rows usually fit
everything anyway. */
#email-lib-modal .email-reader-atts {
padding: 4px 10px !important;
gap: 3px !important;
max-height: calc(2 * 22px + 12px); /* 2 rows × chip height + padding */
overflow-y: auto;
align-content: flex-start;
}
#email-lib-modal .email-attachment-chip {
padding: 1px 6px !important;
font-size: 10px !important;
max-width: 130px !important;
line-height: 18px !important;
}
}
/* Mobile: only ONE scroll surface inside the cookbook modal. The
modal-content is the scroller. Everything inside (cookbook-body,
cookbook-group, all the inner lists/panels) gets overflow: visible
so touch-pan never gets trapped in a nested scroller.
Without this, three levels of overflow:auto + max-height combinations
produce dead-zone areas where swipes do nothing. */
@media (max-width: 768px) {
#cookbook-modal .modal-content {
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
touch-action: pan-y;
}
#cookbook-modal .cookbook-body,
#cookbook-modal .cookbook-group,
#cookbook-modal .cookbook-section-body,
#cookbook-modal .hwfit-cached-list,
#cookbook-modal .doclib-grid,
#cookbook-modal .hwfit-list,
#cookbook-modal .hwfit-panel-cmd {
overflow: visible !important;
max-height: none !important;
min-height: 0 !important;
}
/* Running tmux output: cap how tall an expanded card gets (a long-lived job
can leave thousands of lines) so it doesn't balloon, but keep its own
scroll so you can read it. Outputs default collapsed on mobile now, so
you're usually looking at one expanded card at a time — no wall of nested
scrollers, and you collapse it to move past. */
#cookbook-modal .cookbook-output-pre,
#cookbook-modal .cookbook-task .cookbook-output-pre {
max-height: 45vh !important;
min-height: 0 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
overscroll-behavior: auto; /* chain to the modal at the scroll boundary */
}
/* cookbook-body's flex:1 was needed when it owned scrolling — drop it
so the inner content drives modal-content's scroll height. */
#cookbook-modal .cookbook-body {
flex: 0 0 auto !important;
height: auto !important;
}
}
.memory-toolbar {
transition: opacity 0.12s ease, max-height 0.2s ease;
max-height: 120px;
}
/* The Servers list reuses .memory-toolbar for layout but must grow with every
added server — the 120px cap above was clipping manually-added servers. */
.memory-toolbar.cookbook-servers-toolbar {
max-height: none;
overflow: visible;
}
.doclib-card {
background: color-mix(in srgb, var(--fg) 3%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s, opacity 0.3s, max-height 0.3s ease;
position: relative;
}
.doclib-card.doclib-card-deleting {
opacity: 0;
max-height: 0 !important;
overflow: hidden;
margin: 0;
padding: 0;
border-color: transparent;
}
.doclib-card:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
border-color: color-mix(in srgb, var(--fg) 16%, transparent);
}
.doclib-card.selected,
/* Universal selection highlight: any card-shaped row (documents, chats,
archive, research, skills, memories, tasks) gets the same accent outline
when its checkbox is checked. Done via :has() so the highlight follows
selection without per-renderer JS changes. The canonical checkbox class
is .memory-select-cb across every list. */
.memory-item:has(.memory-select-cb:checked),
.doclib-card:has(.memory-select-cb:checked),
.doclib-chat-row:has(.memory-select-cb:checked) {
border-color: color-mix(in srgb, var(--red) 40%, transparent);
background: color-mix(in srgb, var(--red) 4%, transparent);
/* 2px accent outline overlaid on the 1px border — reads as a thicker
selected border without shifting layout (every card would otherwise
need a 2px transparent border to keep the same width). */
outline: 2px solid var(--accent-primary, var(--red));
outline-offset: -1px;
}
.doclib-card-header {
display: flex;
align-items: center;
padding: 7px 8px;
gap: 6px;
min-width: 0;
}
/* Mobile only: push the unexpanded email card's title-row text down 4px so
it sits visually centered with the surrounding card padding. Targets the
actual title-row class (.email-card-titlerow) — the earlier attempt used
.doclib-card-header which the email card builds differently. */
@media (max-width: 768px) {
#email-lib-modal .doclib-card:not(.doclib-card-expanded) .email-card-titlerow {
margin-top: 4px;
}
}
.doclib-card-title {
font-size: 11px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex: 1;
}
.doclib-card-ver {
padding: 1px 6px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
color: var(--accent, var(--red));
font-size: 9px;
flex-shrink: 0;
font-weight: 600;
letter-spacing: 0.3px;
min-width: 24px;
text-align: left;
box-sizing: border-box;
}
.doclib-card-ver-muted {
background: color-mix(in srgb, var(--fg) 6%, transparent);
color: color-mix(in srgb, var(--fg) 35%, transparent);
}
.doclib-card-lang {
padding: 1px 6px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--accent);
font-size: 9px;
text-align: left;
flex-shrink: 0;
min-width: 52px;
box-sizing: border-box;
}
.doclib-card-session {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 60px;
width: 100px;
max-width: 100px;
font-size: 9px;
color: var(--fg-muted);
flex-shrink: 0;
text-align: left;
}
.doclib-card-time {
flex-shrink: 0;
opacity: 0.5;
font-size: 9px;
color: var(--fg-muted);
min-width: 50px;
text-align: left;
}
/* Footer — used by archive cards */
.doclib-card-footer {
display: contents;
}
/* Preview — hidden by default, shown on expand */
.doclib-card-preview {
display: none;
position: relative;
padding: 8px 12px;
font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
font-size: 9.5px;
line-height: 1.5;
color: var(--code-fg, var(--hl-fg, var(--fg)));
border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
margin: 0;
}
.doclib-card-preview pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
}
.doclib-card-expanded .doclib-card-preview pre {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.doclib-card-preview code.hljs {
background: none;
padding: 0;
}
/* Expanded-only action bar — inside preview. Buttons shifted up 4px by
trimming the row's top margin so they sit closer to the preview text. */
.doclib-card-expanded-actions {
display: none;
align-items: flex-start;
gap: 6px;
padding: 8px 0 2px;
border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
margin-top: 4px;
}
.doclib-card-expanded-actions > .doclib-card-action-btn,
.doclib-card-expanded-actions .doclib-action-btn-row > .doclib-card-action-btn {
position: relative;
top: -4px;
}
.doclib-action-group {
display: flex;
flex-direction: column;
gap: 3px;
}
.doclib-action-btn-row {
display: flex;
gap: 6px;
}
.doclib-action-hint-row {
display: flex;
gap: 6px;
}
/* Match the chat/research footer buttons exactly: flat, bordered,
app font, accent on hover. */
.doclib-card-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
box-sizing: border-box;
font-size: 10px;
padding: 3px 8px;
border-radius: 4px;
background: none;
border: 1px solid var(--border);
color: var(--fg-muted);
cursor: pointer;
/* These buttons live inside .doclib-card-preview, which forces a
monospace font. The chat/research footer buttons instead inherit the
modal font, which is the app font (Fira Code) on mobile and Inter on
desktop (.modal-content's Inter rule is inside @media min-width:769px).
Mirror that here so all three footers match in both contexts. */
font-family: var(--font-family, 'Fira Code', monospace);
transition: border-color 0.15s, color 0.15s;
}
@media (min-width: 769px) {
.doclib-card-action-btn {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.015em;
}
}
.doclib-card-action-btn:hover {
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
.doclib-card-text-btn-danger {
color: var(--color-danger, #e06c75);
border-color: var(--color-danger, #e06c75);
}
.doclib-card-text-btn-danger:hover {
color: #ff4d4d;
border-color: #ff4d4d;
}
.doclib-card-expanded-actions > .doclib-card-action-btn {
font-size: 10px;
padding: 3px 8px;
}
/* Push the Open/Clone group to the right edge of the action bar — Delete
anchors the left, primary actions sit opposite. */
.doclib-card-expanded-actions > .doclib-action-group {
margin-left: auto;
}
.doclib-btn-hint {
font-weight: normal;
opacity: 0.35;
font-size: 8px;
min-width: 58px;
padding: 0 8px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.doclib-btn-hint { display: none !important; }
}
/* Chat row expand preview — matches the documents-tab expand style. */
.doclib-chat-row { flex-direction: column; align-items: stretch; }
.doclib-chat-row .doclib-card-chevron {
opacity: 0.3;
transition: transform 0.2s ease, opacity 0.15s;
flex-shrink: 0;
}
.doclib-chat-row.doclib-card-expanded .doclib-card-chevron {
opacity: 0.6;
}
/* Release .memory-item's base max-height: 200px when a chat row is
expanded. Without this, the row clips at 200px on desktop and the
preview's messages/actions visually spill past the row's box —
"colliding" with the next chat in the column. The mobile takeover
below already handles this via the same override; this rule is the
desktop-or-any-viewport equivalent. */
.doclib-chat-row.doclib-card-expanded {
max-height: none;
overflow: visible;
}
/* When a chat row is expanded, take over the grid the same way documents
do: hide siblings, hide the admin-card toolbar, and let this row plus
its preview claim the full available height. .memory-item's default
max-height: 200px must be overridden or the preview will be tiny. */
/* Mobile-only "card takeover" for an expanded chat — hides siblings,
hides the admin-card toolbar, and lets this row + its preview claim
the full available height. On desktop the expanded preview just
renders inline below the row with its messages constrained by
max-height + overflow:auto so the user gets a normal scroll. */
@media (max-width: 820px) {
.doclib-grid:has(.doclib-chat-row.doclib-card-expanded) > .doclib-chat-row:not(.doclib-card-expanded) {
display: none;
}
.admin-card:has(.doclib-chat-row.doclib-card-expanded) > *:not(.doclib-grid) {
display: none;
}
.admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid {
flex: 1 1 auto !important;
max-height: none !important;
overflow-y: auto !important;
}
.doclib-chat-row.doclib-card-expanded {
flex: 1 1 auto !important;
max-height: none !important;
min-height: 0 !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
}
.doclib-chat-row.doclib-card-expanded .doclib-chat-header {
flex: 0 0 auto !important;
}
.doclib-chat-row.doclib-card-expanded .doclib-chat-preview {
flex: 1 1 auto !important;
min-height: 0 !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
padding-bottom: 4px !important;
}
.doclib-chat-row.doclib-card-expanded .doclib-chat-preview-messages {
-webkit-overflow-scrolling: touch;
max-height: none !important;
flex: 1 1 auto !important;
min-height: 0 !important;
}
}
.doclib-chat-preview {
font-size: 11px;
padding: 0 4px 6px;
}
.doclib-chat-preview .doclib-chat-open-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 3px 8px;
border-radius: 4px;
color: var(--fg-muted);
border: 1px solid var(--border);
background: none;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.doclib-chat-preview .doclib-chat-open-btn:hover {
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
.doclib-chat-preview-messages {
margin-top: 6px;
max-height: 320px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-right: 4px;
}
/* Desktop expanded state — match the documents-tab behavior: hide
sibling chats, hide the admin-card toolbar/header, and let the
expanded row + its messages preview claim the full modal height.
Chat rows use `memory-item doclib-chat-row` (no `doclib-card` class)
so the document-tab's global rules at line 12284-12304 skip them —
these mirror those rules scoped to chat rows. The mobile media query
above (820px) keeps the same takeover behavior for phones. */
@media (min-width: 821px) {
.doclib-grid:has(.doclib-chat-row.doclib-card-expanded) > .doclib-chat-row:not(.doclib-card-expanded) {
display: none;
}
.admin-card:has(.doclib-chat-row.doclib-card-expanded) > *:not(.doclib-grid) {
display: none;
}
.admin-card:has(.doclib-chat-row.doclib-card-expanded) > .doclib-grid {
flex: 1 1 auto !important;
max-height: none !important;
overflow: hidden auto !important;
}
.doclib-chat-row.doclib-card-expanded {
flex: 1 1 auto !important;
max-height: none !important;
min-height: 0 !important;
display: flex !important;
flex-direction: column !important;
}
.doclib-chat-row.doclib-card-expanded .doclib-chat-preview {
flex: 1 1 auto !important;
min-height: 0 !important;
display: flex !important;
flex-direction: column !important;
}
.doclib-chat-row.doclib-card-expanded .doclib-chat-preview-messages {
flex: 1 1 auto !important;
min-height: 0 !important;
max-height: none !important;
overflow-y: auto !important;
}
}
.doclib-chat-preview-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 2px 0 2px;
border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
margin-top: 2px;
flex-shrink: 0;
/* Whole action footer nudged down 5px. */
position: relative;
top: 5px;
}
.doclib-chat-delete-btn,
.doclib-chat-archive-btn,
.doclib-chat-restore-btn,
.doclib-chat-discuss-btn,
.doclib-chat-copy-btn {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
padding: 3px 8px;
border-radius: 4px;
background: none;
cursor: pointer;
font-family: inherit;
transition: border-color 0.15s, color 0.15s;
}
.doclib-chat-archive-btn,
.doclib-chat-restore-btn,
.doclib-chat-discuss-btn,
.doclib-chat-copy-btn {
color: var(--fg-muted);
border: 1px solid var(--border);
}
/* Restore sits on the right of the Delete in archive previews. */
.doclib-chat-restore-btn { margin-left: auto; }
.doclib-chat-restore-btn:hover {
color: var(--accent, var(--red));
border-color: var(--accent, var(--red));
}
/* Delete + Archive pin to the left of the actions row; the
`margin-right: auto` on the last left-side button (Archive) pushes
Copy + Open to the right. Delete sits furthest left. */
.doclib-chat-archive-btn { margin-right: auto; }
.doclib-chat-archive-btn:hover,
.doclib-chat-discuss-btn:hover,
.doclib-chat-copy-btn:hover {
color: var(--accent, var(--red));
border-color: var(--accent, var(--red));
}
.doclib-chat-delete-btn {
color: var(--color-danger, #e06c75);
border: 1px solid var(--color-danger, #e06c75);
}
.doclib-chat-delete-btn:hover {
color: #ff4d4d;
border-color: #ff4d4d;
}
/* When a chat is archived there's no Archive button, so Delete becomes
the only left-side action and needs the auto margin to push Open right. */
.doclib-chat-preview-actions:not(:has(.doclib-chat-archive-btn)) > .doclib-chat-delete-btn {
margin-right: auto;
}
/* Hide the "..." menu while the chat card is expanded — archive +
delete live in the preview footer instead. */
.doclib-chat-row.doclib-card-expanded ._chat-menu { display: none; }
.doclib-chat-preview-actions.doclib-chat-preview-actions-top {
border-top: none;
border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
margin-top: 4px;
margin-bottom: 6px;
padding: 0 0 6px;
justify-content: flex-start;
}
.doclib-chat-preview-messages .doclib-chat-msg {
margin: 4px 0 10px;
padding-left: 8px;
border-left: 2px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.doclib-chat-preview-messages .doclib-chat-msg.user {
border-left-color: color-mix(in srgb, var(--accent, var(--red)) 60%, transparent);
}
.doclib-chat-preview-messages .doclib-chat-msg-role {
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.55;
margin-bottom: 2px;
}
.doclib-chat-preview-messages .doclib-chat-msg.user .doclib-chat-msg-role {
color: var(--accent, var(--red));
opacity: 0.85;
}
.doclib-chat-preview-messages .doclib-chat-msg-body {
white-space: pre-wrap;
word-break: break-word;
opacity: 0.85;
line-height: 1.45;
}
/* Chat-bubble preview — mirrors the real chat layout. User bubbles hug
right with an accent tint; assistant bubbles hug left with a neutral
panel tint and a small model tag at the top. */
.doclib-chat-bubble-row {
display: flex;
margin: 6px 0;
}
.doclib-chat-bubble-row.user { justify-content: flex-end; }
.doclib-chat-bubble-row.assistant { justify-content: flex-start; }
.doclib-chat-bubble {
max-width: 85%;
padding: 6px 10px 8px;
border-radius: 14px;
font-size: 11px;
line-height: 1.45;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.doclib-chat-bubble-row.user .doclib-chat-bubble {
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
border-bottom-right-radius: 4px;
}
.doclib-chat-bubble-row.assistant .doclib-chat-bubble {
border-bottom-left-radius: 4px;
}
.doclib-chat-msg-model {
display: inline-block;
font-size: 8px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.55;
margin-bottom: 3px;
padding: 1px 6px;
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg-muted, var(--fg));
}
.doclib-chat-bubble-body { word-break: break-word; }
.doclib-chat-bubble-body p { margin: 4px 0; }
.doclib-chat-bubble-body p:first-child { margin-top: 0; }
.doclib-chat-bubble-body p:last-child { margin-bottom: 0; }
.doclib-chat-bubble-body pre {
font-size: 10px;
margin: 4px 0;
padding: 6px 8px;
background: color-mix(in srgb, var(--bg) 60%, transparent);
border-radius: 6px;
overflow-x: auto;
}
.doclib-chat-bubble-body code {
font-size: 10px;
padding: 1px 4px;
border-radius: 3px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.doclib-chat-bubble-body pre code { background: none; padding: 0; }
.doclib-chat-bubble-body ul,
.doclib-chat-bubble-body ol {
margin: 4px 0;
padding-left: 18px;
}
.doclib-card-collapse-chevron {
display: none;
align-items: center;
opacity: 0.3;
cursor: pointer;
flex-shrink: 0;
margin-left: 4px;
transition: opacity 0.15s;
}
.doclib-card-collapse-chevron:hover {
opacity: 0.7;
}
.doclib-card-expanded .doclib-card-collapse-chevron {
display: inline-flex;
}
/* Expanded card — fills the whole grid */
.doclib-card.doclib-card-expanded {
flex: 1;
min-height: 0;
background: color-mix(in srgb, var(--fg) 3%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: doclib-expand 0.2s ease-out;
}
@keyframes doclib-expand {
from { opacity: 0.5; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.doclib-card.doclib-card-expanded {
flex-direction: column !important;
align-items: stretch !important;
max-height: none !important;
gap: 4px !important;
}
.doclib-card.doclib-card-expanded > .memory-item-actions { display: none; }
/* Non-preview children of an expanded doc card carry an inline flex:1 from
the row layout. With the card now flex-column those children would steal
half the height and shrink the preview — pin them at auto height. */
.doclib-card.doclib-card-expanded > div:not(.doclib-card-preview):not(.doclib-card-header):not(.memory-item-actions):not(.email-card-reader) {
flex: 0 0 auto !important;
}
/* The email reader IS the scroll container — keep its flex:1 so its
internal body can claim the rest of the card's height and scroll. */
.doclib-card.doclib-card-expanded > .email-card-reader {
flex: 1 1 auto !important;
min-height: 0 !important;
}
.doclib-card.doclib-card-expanded .doclib-card-preview {
display: flex;
flex-direction: column;
flex: 1 1 auto !important;
overflow-y: auto;
min-height: 0;
}
.doclib-card.doclib-card-expanded .doclib-card-expanded-actions {
display: flex;
flex-shrink: 0;
}
/* 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: 10.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;
}
/* Hide the section headers while a card is expanded (full-screen reader). */
.doclib-grid:has(.doclib-card-expanded) > .skills-section-header { display: none; }
/* #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: 46px;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 6px;
}
.skill-conf-dot {
display: inline-block;
width: 7px; height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
/* 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-name {
font-weight: 600;
font-size: 0.9em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-word;
line-height: 1.3;
}
.skill-card-desc {
font-size: 10px;
opacity: 0.55;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 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-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-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(--color-danger, #e06c75) 26%, transparent);
color: var(--color-danger, #e06c75);
border-color: color-mix(in srgb, var(--color-danger, #e06c75) 38%, transparent);
}
.skill-necessity-irrelevant {
background: color-mix(in srgb, var(--color-danger, #e06c75) 18%, transparent);
color: var(--color-danger, #e06c75);
}
.skill-duplicate-keep {
background: color-mix(in srgb, var(--color-success, #4ade80) 22%, transparent);
color: var(--color-success, #4ade80);
}
.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-conf { font-weight: 700; }
.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; }
.skill-card.doclib-card-expanded .skill-kebab-btn { display: none; }
.skill-card.doclib-card-expanded .skill-chevron-up { display: inline-flex; }
/* Kebab dropdown */
.skill-kebab-menu {
position: fixed;
z-index: 100002;
min-width: 150px;
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: 8px;
width: 100%;
padding: 7px 9px;
background: none;
border: none;
border-radius: 5px;
cursor: pointer;
color: var(--fg);
font-size: 12px;
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;
}
/* Size the expanded skill card to its content (footer sits right under the
preview) but cap at the modal height so a long SKILL.md scrolls instead
of overflowing. The inherited `.doclib-card-expanded { flex: 1 }` forced
the card to fill the whole modal, which left a big empty void between a
short skill's text and the footer. */
.skill-card.doclib-card-expanded {
flex: 0 1 auto !important;
max-height: 100% !important;
}
/* :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: color-mix(in srgb, var(--fg) 3%, transparent);
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)); }
/* 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);
}
.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: 8px;
}
/* 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);
}
/* 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-selected-count {
color: color-mix(in srgb, var(--fg) 50%, 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;
}
}
/* ── 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;
}
/* 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;
}
/* 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; }
/* Search input + its "↵ enter to tag" hint live in a relative wrapper that
carries the 2px down-shift, so input and hint move together. */
.gallery-search-wrap { position: relative; flex: 1; min-width: 0; display: flex; top: 2px; }
.gallery-search {
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-select-btn { position: relative; top: 2px; font-size: 12px; padding: 10px 11px 12px; }
.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;
}
.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 {
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-chip-clear {
background: none;
border: none;
color: inherit;
opacity: 0.6;
cursor: pointer;
padding: 0;
margin-left: 1px;
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
position: relative;
top: -4px;
}
.gallery-chip-clear:hover { opacity: 1; }
/* Favorite button on card */
/* Video cards: fills the same slot as , plus a small play badge
so it's clear the thumbnail represents a video. */
.gallery-card video {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
background: #000;
display: block;
}
.gallery-card-play {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
.gallery-card-play svg { margin-left: 2px; }
.gallery-detail-image video {
max-width: 100%;
max-height: 70vh;
display: block;
}
.gallery-fav-btn {
position: absolute; top: 0px; right: 4px; z-index: 2;
background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
width: 26px; height: 26px; font-size: 14px;
color: rgba(255,255,255,0.6); cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s;
padding: 0;
}
.gallery-card:hover .gallery-fav-btn { opacity: 1; }
.gallery-fav-btn.gallery-fav-active { opacity: 1; color: var(--red); }
.gallery-dl-btn {
position: absolute; top: 0px; left: 4px; z-index: 2;
background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
width: 26px; height: 26px;
color: rgba(255,255,255,0.75); cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s, background 0.15s;
padding: 0;
}
.gallery-card:hover .gallery-dl-btn { opacity: 1; }
.gallery-dl-btn:hover { background: rgba(0,0,0,0.7); color: #fff; }
@media (hover: none) {
.gallery-dl-btn { opacity: 0.55; }
}
/* In select mode the per-thumbnail hover buttons (favorite + download) just
get in the way of picking — hide them so the card is a clean select target.
`body.gallery-selecting` is the authoritative signal (one class for the
whole grid) — the per-card .gallery-card-selectable is kept as a backup
in case the body class is missed. */
body.gallery-selecting .gallery-fav-btn,
body.gallery-selecting .gallery-dl-btn,
.gallery-card-selectable .gallery-fav-btn,
.gallery-card-selectable .gallery-dl-btn {
display: none !important;
}
/* AI tag chips in detail view */
.gallery-ai-tags { display: flex; gap: 4px; flex-wrap: wrap; }
/* Space the user's tag chips off the "add tag" input below them. */
#gallery-user-tag-chips { margin-bottom: 4px; }
.gallery-ai-chip {
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
border-radius: 10px; padding: 3px 8px; font-size: 10px;
color: var(--fg); opacity: 0.8;
font: inherit; font-size: 10px;
cursor: pointer;
transition: background 0.12s, opacity 0.12s, border-color 0.12s;
}
.gallery-ai-chip:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
}
/* AI-generated tags: muted neutral with a tiny sparkle marker so the
user can tell at a glance these came from the model, not them. */
.gallery-aitag-chip {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
display: inline-flex;
align-items: center;
gap: 4px;
}
.gallery-aitag-chip:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
border-color: color-mix(in srgb, var(--fg) 35%, transparent);
}
.gallery-aitag-mark {
font-size: 8px;
opacity: 0.55;
color: var(--accent, var(--red));
}
/* User-applied tags keep the accent-colored chip — they're the personal/
curated tags, so they get the strongest visual weight. */
.gallery-user-chip {
font-weight: 600;
}
/* × to remove a user tag (appears inside the chip). */
.gallery-tag-x {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 5px;
width: 13px;
height: 13px;
border-radius: 50%;
font-size: 12px;
line-height: 1;
opacity: 0.55;
transition: opacity 0.12s, background 0.12s, color 0.12s;
}
.gallery-user-chip:hover .gallery-tag-x { opacity: 0.85; }
.gallery-tag-x:hover {
opacity: 1;
background: color-mix(in srgb, var(--red, #ff5555) 22%, transparent);
color: var(--red, #ff5555);
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 8px;
max-height: 60vh;
overflow-y: auto;
padding: 2px;
transition: border-color 0.15s, background 0.15s;
}
.gallery-grid.gallery-dragover {
border: 2px dashed var(--red);
border-radius: 8px;
background: color-mix(in srgb, var(--red) 5%, transparent);
}
.gallery-card {
position: relative;
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
}
/* Upload-affordance tile pinned to the top-left of the Photos grid. Matches
the Upload album tile in the Albums tab — dashed border, centered icon. */
.gallery-card-upload {
border-style: dashed;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
opacity: 0.75;
transition: opacity 0.12s, border-color 0.15s, transform 0.15s;
}
/* The "Start AI tag" button turns into a Cancel control during a tag run. */
.gallery-tag-cancelling {
color: var(--red) !important;
border-color: color-mix(in srgb, var(--red) 45%, var(--border)) !important;
}
/* Skeleton/shimmer placeholder tiles shown while the first page of photos loads. */
.gallery-card-skeleton {
cursor: default;
background: linear-gradient(100deg,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
background-size: 200% 100%;
animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
}
.gallery-card-skeleton:hover { transform: none; box-shadow: none; border-color: var(--border); }
@keyframes gallery-skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Chat attachment image: shimmer skeleton + centered whirlpool shown while a
sent photo uploads / its thumbnail loads (see chatRenderer buildAttachCards). */
.attach-image-skeleton {
position: relative;
width: 160px; height: 120px;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(100deg,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
background-size: 200% 100%;
animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
}
.attach-image-skeleton > .spinner-whirlpool { margin: 0 !important; }
/* Corner "Aa" button on a chat photo thumbnail — opens the vision/OCR editor
so the user can correct what the vision model fed to the LLM. */
.attach-image-preview { position: relative; }
.attach-vision-model,
.attach-image-name {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attach-vision-model {
font-size: 10px;
color: var(--accent, var(--red));
opacity: 0.85;
margin-top: 3px;
}
.attach-image-name {
font-size: 10px;
opacity: 0.5;
margin-top: 1px;
}
.attach-ocr-btn {
position: absolute; top: 4px; right: 4px;
z-index: 5; /* sit above msg-action overlays so the click lands here */
height: 22px;
background: rgba(0,0,0,0.55); color: #fff;
border: 1px solid rgba(255,255,255,0.2); border-radius: 4px;
padding: 0 8px 0 6px; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
font-size: 11px; font-weight: 500; line-height: 1;
opacity: 0.75; transition: opacity 0.15s;
}
.attach-ocr-btn:hover { opacity: 1; }
.attach-ocr-btn svg { display: block; }
/* On mobile keep just the (larger) edit icon — the "Caption" label crowds
the corner of the small chat photo. */
@media (max-width: 768px) {
.attach-ocr-btn .attach-ocr-label { display: none; }
.attach-ocr-btn {
width: 28px; height: 28px;
padding: 0;
}
.attach-ocr-btn svg { width: 16px; height: 16px; }
}
/* Vision/OCR text editor modal — opened from the "Aa" button above. */
.vision-editor-overlay {
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,0.55);
display: flex; align-items: center; justify-content: center;
padding: 16px;
}
.vision-editor-panel {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
width: min(560px, 92vw);
max-height: 88vh;
display: flex; flex-direction: column; gap: 8px;
box-shadow: 0 14px 40px rgba(0,0,0,0.4);
}
.vision-editor-title {
font-size: 13px; font-weight: 600; color: var(--fg);
display: flex; align-items: center; gap: 6px;
}
.vision-editor-desc { font-size: 11px; opacity: 0.6; line-height: 1.4; }
.vision-editor-text {
width: 100%; min-height: 180px;
background: var(--panel); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 8px 10px;
font-family: inherit; font-size: 12px; line-height: 1.5;
resize: vertical; outline: none;
}
.vision-editor-text:focus { border-color: var(--accent-primary, var(--red)); }
.vision-editor-hint { font-size: 10.5px; opacity: 0.55; line-height: 1.4; }
.vision-editor-actions {
display: flex; gap: 8px; justify-content: flex-end;
margin-top: 4px;
}
.vision-editor-btn {
background: var(--panel); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 14px; font-size: 12px; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
display: inline-flex; align-items: center; gap: 5px;
}
.vision-editor-btn svg { display: block; }
/* Optical nudge: the button text sits 1px higher than the SVG glyphs
alongside it. Drop the text down 1px so they line up. */
.vision-editor-btn .vision-btn-label { position: relative; top: 1px; }
.vision-editor-btn:hover { border-color: color-mix(in srgb, var(--fg) 40%, var(--border)); }
.vision-editor-btn-primary {
background: var(--accent-primary, var(--red));
color: #fff; border-color: transparent;
}
.vision-editor-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
/* Quick full-size lightbox shown when the user taps a chat photo thumbnail. */
.attach-lightbox {
position: fixed; inset: 0; z-index: 9998;
background: rgba(0, 0, 0, 0.85);
display: flex; align-items: center; justify-content: center;
padding: 16px; cursor: zoom-out;
animation: attach-lightbox-fade 0.12s ease-out;
}
.attach-lightbox img {
max-width: 100%; max-height: 100%;
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
object-fit: contain;
}
.attach-lightbox-err {
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 10px; font-size: 12px;
}
@keyframes attach-lightbox-fade {
from { opacity: 0; }
to { opacity: 1; }
}
.gallery-card-upload:hover {
opacity: 1;
border-color: var(--red);
transform: translateY(-1px);
}
.gallery-card-upload-inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--fg);
}
.gallery-card-upload-label {
font-size: 12px;
font-weight: 500;
opacity: 0.8;
}
.gallery-card:hover {
border-color: var(--red);
box-shadow: 0 2px 8px color-mix(in srgb, var(--red) 15%, transparent);
transform: translateY(-1px);
}
.gallery-select-dot {
position: absolute; top: 6px; left: 6px; z-index: 2;
width: 10px; height: 10px; border-radius: 50%; cursor: pointer;
background: color-mix(in srgb, var(--fg) 30%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 50%, transparent);
transition: background 0.15s;
}
.gallery-select-dot.selected { background: var(--red); border-color: var(--red); }
.gallery-select-dot:hover { background: color-mix(in srgb, var(--red) 50%, transparent); }
/* Strong accent ring on the whole photo when it's selected — inset
box-shadow (not outline) because outline-offset:-4px inside an
overflow:hidden rounded card renders sharp-cornered on Firefox.
box-shadow inset always respects the card's border-radius. */
.gallery-card:has(.gallery-select-dot.selected) {
box-shadow: inset 0 0 0 7px var(--red);
}
.gallery-select-btn {
padding: 9px 10px 11px; background: transparent; color: var(--fg);
border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
font-size: 11px; font-family: inherit; opacity: 0.6; transition: all 0.15s;
margin-top: -4px !important;
}
.gallery-select-btn:hover { opacity: 1; }
/* Match the library Select toggle's red tint (.memory-toolbar-btn.active).
Bumped contrast so the toggled state actually reads on mobile — the prior
15% red on transparent was nearly invisible against the page background. */
.gallery-select-btn.active {
background: color-mix(in srgb, var(--red) 28%, transparent);
color: var(--red);
border-color: var(--red);
font-weight: 600;
opacity: 1;
}
/* Toolbar action buttons sit 2px lower than the generic select-btn so
they baseline-align with the toolbar's selects/inputs instead of
floating above them. */
.gallery-toolbar-action {
margin-top: 0 !important;
display: inline-flex;
align-items: center;
gap: 2px;
}
.gallery-bulk-bar {
display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 12px;
justify-content: flex-end;
}
.gallery-bulk-bar.hidden { display: none; }
.gallery-bulk-all {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; opacity: 0.75; cursor: pointer; margin-right: auto;
}
.gallery-bulk-all input { accent-color: var(--accent, var(--red)); cursor: pointer; }
.gallery-bulk-delete {
padding: 3px 10px; background: transparent; color: var(--red);
border: 1px solid var(--red); border-radius: 4px; cursor: pointer;
font-size: 11px; font-family: inherit;
}
.gallery-bulk-delete:hover { background: var(--red); color: #fff; }
.gallery-bulk-cancel {
padding: 3px 10px; background: transparent; color: var(--fg);
border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
font-size: 11px; font-family: inherit; opacity: 0.6;
}
.gallery-bulk-cancel:hover { opacity: 1; }
.gallery-card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-card-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px 8px;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
color: #fff;
}
.gallery-card-prompt {
font-size: 10px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gallery-card-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
font-size: 9px;
opacity: 0.8;
}
.gallery-card-model {
padding: 1px 5px;
border-radius: 6px;
background: rgba(255,255,255,0.15);
}
.gallery-empty {
text-align: center;
color: color-mix(in srgb, var(--fg) 35%, transparent);
padding: 32px 16px;
font-size: 12px;
font-style: italic;
}
.gallery-load-more {
display: block;
margin: 10px auto 0;
padding: 6px 16px;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 11px;
transition: border-color 0.15s, color 0.15s;
}
.gallery-load-more:hover {
border-color: var(--red);
color: var(--red);
}
/* Gallery detail overlay */
.gallery-detail {
position: absolute;
inset: 0;
background: var(--panel);
z-index: 10;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.gallery-detail-header {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px 4px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
/* Pin to top so a tall (portrait) photo can't cover the Back / ⋮
menu when the detail body grows past the visible area. */
position: sticky;
top: 0;
background: var(--panel);
/* Above the image's rotate/nav buttons (z-index:3) so they scroll cleanly
behind the header instead of colliding with it. */
z-index: 5;
min-height: 0;
}
.gallery-detail-back {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
position: relative;
top: -2px;
}
/* Slightly larger header icons (back/edit/heart/⋮) — override the inline 14px. */
.gallery-detail-header svg { width: 16px; height: 16px; }
.gallery-detail-back:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.gallery-detail-action {
background: none;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
transition: border-color 0.15s;
}
.gallery-detail-action:hover {
border-color: var(--fg);
}
.gallery-detail-action.danger {
color: var(--color-error, #e55);
}
.gallery-detail-action.danger:hover {
border-color: var(--color-error, #e55);
}
/* Overflow menu — replaces the seven individual action buttons that used
to crowd the right side of the detail header. */
.gallery-detail-menu-wrap { position: relative; }
.gallery-detail-menu-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 26px;
padding: 0;
position: relative;
top: -2px;
}
.gallery-detail-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 150px;
padding: 3px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
z-index: 12;
display: flex;
flex-direction: column;
gap: 0;
}
.gallery-detail-menu[hidden] { display: none; }
.gallery-detail-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
/* Tighter than the global default — the photo-detail menu has 8
items so every pixel of vertical padding adds up. */
padding: 4px 8px;
font-size: 11px;
gap: 8px;
line-height: 1.2;
}
.gallery-detail-menu .dropdown-item-compact .dropdown-icon,
.gallery-detail-menu .dropdown-item-compact .dropdown-icon svg {
width: 12px;
height: 12px;
}
.gallery-detail-body {
display: flex;
gap: 16px;
padding: 12px;
flex: 1;
min-height: 0;
}
.gallery-detail-image {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
}
/* Shrink-wraps the actual image so overlay children (heart, face boxes)
anchor to the IMAGE bounds, not the wider letterboxed wrapper. */
.gallery-detail-img-frame {
position: relative;
display: inline-flex;
max-width: 100%;
max-height: 80vh;
}
.gallery-detail-image img,
.gallery-detail-image video,
.gallery-detail-img-frame img,
.gallery-detail-img-frame video {
max-width: 100%;
max-height: 80vh;
border-radius: 6px;
object-fit: contain;
display: block;
}
.gallery-detail-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.85);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
z-index: 3;
padding: 0;
}
.gallery-detail-image:hover .gallery-detail-nav { opacity: 0.85; }
.gallery-detail-nav:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
.gallery-detail-nav-prev { left: 8px; }
.gallery-detail-nav-next { right: 8px; }
/* Rotate buttons — same pattern as the prev/next arrows but pinned to the
top corners. Hover-revealed so they don't clutter the image. */
.gallery-detail-rotate {
position: absolute;
top: 8px;
background: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.85);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
z-index: 3;
padding: 0;
}
.gallery-detail-image:hover .gallery-detail-rotate { opacity: 0.85; }
.gallery-detail-rotate:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
.gallery-detail-rotate-ccw { left: 8px; }
.gallery-detail-rotate-cw { right: 8px; }
/* Inline heart that lives next to the "Date" label in the sidebar.
Tiny outline icon by default; fills red when favorited. */
.gallery-detail-date-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.gallery-detail-fav-inline {
background: none;
border: none;
padding: 0;
/* Nudge up 8px so the heart sits visually above the Date label
baseline rather than centred on it. */
margin-top: -8px;
border-radius: 4px;
color: var(--fg-muted);
opacity: 0.6;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: opacity 0.15s, color 0.15s;
}
/* Shrink the SVG so the heart matches the height of the small uppercase
Date label sitting next to it. */
.gallery-detail-fav-inline svg {
width: 12px;
height: 12px;
display: block;
}
.gallery-detail-fav-inline:hover {
opacity: 1;
}
.gallery-detail-fav-inline.active {
opacity: 1;
color: var(--red);
}
.gallery-detail-nav-disabled {
opacity: 0 !important;
pointer-events: none;
}
@media (hover: none) {
.gallery-detail-nav { opacity: 0.7; }
}
.gallery-detail-sidebar {
width: 240px;
flex-shrink: 0;
/* Vertical scroll for long metadata, but never horizontal — overflow:auto
alone would let a wide child (e.g. long album name in the select) push
the sidebar into horizontal-scroll mode. */
overflow-x: hidden;
overflow-y: auto;
min-width: 0;
}
/* Constrain any child that could otherwise stretch the sidebar wider than
its declared width (selects with long option text, long URLs, etc.). */
.gallery-detail-sidebar select,
.gallery-detail-sidebar input,
.gallery-detail-sidebar .gallery-detail-prompt {
max-width: 100%;
box-sizing: border-box;
}
.gallery-detail-section {
margin-bottom: 12px;
}
.gallery-date-rel {
opacity: 0.55;
margin-left: 4px;
font-size: 11px;
font-style: italic;
}
.gallery-detail-section label {
display: block;
font-size: 10px;
font-weight: 600;
opacity: 0.6;
margin-bottom: 3px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.gallery-detail-section div {
font-size: 12px;
word-break: break-word;
}
.gallery-detail-prompt {
white-space: pre-wrap;
line-height: 1.4;
}
.gallery-tag-input,
.gallery-detail-name-input {
width: 100%;
padding: 5px 8px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 11px;
margin-bottom: 4px;
}
.gallery-tag-input:focus,
.gallery-detail-name-input:focus {
border-color: var(--red);
outline: none;
}
/* ↵ enter hint inside the Add-a-tag field (accent), replacing the "press Enter"
placeholder text. */
.gallery-tag-input-wrap { position: relative; }
.gallery-tag-input-wrap .gallery-tag-input { padding-right: 24px; }
.gallery-tag-enter-hint {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
margin-top: -2px; /* the input has 4px bottom margin; center on the box */
color: var(--accent, var(--red));
font-size: 13px;
line-height: 1;
pointer-events: none;
opacity: 0.8;
}
.gallery-detail-name-input { font-size: 13px; font-weight: 500; }
/* ↵ enter hint on the image name field — accent, shown while editing to signal
that Enter saves. */
.gallery-name-wrap { position: relative; }
.gallery-name-enter {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
margin-top: -2px;
color: var(--accent, var(--red));
opacity: 0.85;
pointer-events: none;
display: none;
}
.gallery-name-wrap:focus-within .gallery-name-enter { display: inline-flex; }
.gallery-name-wrap:focus-within .gallery-detail-name-input { padding-right: 28px; }
.gallery-tag-save {
background: none;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 10px;
padding: 3px 10px;
border-radius: 4px;
transition: border-color 0.15s, color 0.15s;
}
.gallery-tag-save:hover {
border-color: var(--red);
color: var(--red);
}
@media (max-width: 600px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px;
}
/* Single-row toolbar on mobile: search + Sources + Recent (sort) all share
one row. The search bar shrinks so the dropdowns fit beside it. */
.gallery-toolbar {
flex-wrap: nowrap;
gap: 4px;
}
.gallery-search-wrap {
flex: 1 1 0;
min-width: 0;
}
.gallery-search {
/* Drop the desktop right-padding reserved for the "enter to tag" hint —
hide the hint on mobile (below) so the input can use its full width. */
padding-right: 8px;
}
.gallery-search-enter-hint { display: none; }
/* Disable the row break so the filters stay inline with search. */
.gallery-toolbar-break { display: none; }
.gallery-model-filter,
.gallery-sort {
flex: 0 0 auto;
/* Cap each dropdown so search keeps a usable amount of width. */
max-width: 100px;
font-size: 11px;
padding: 4px 6px;
/* Nudge down 2px to baseline with the search input (which carries a
2px down-shift via its wrapper). */
position: relative;
top: 2px;
}
.gallery-select-btn {
padding: 6px 8px 8px;
font-size: 10px;
flex-shrink: 0;
/* Push Select to the far right of the single-row toolbar, regardless
of its DOM position (sits between search and the dropdowns). */
order: 99;
margin-left: auto;
}
/* Tabs scroll horizontally rather than wrapping when narrow. */
.gallery-tabs {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.gallery-tabs::-webkit-scrollbar { display: none; }
.gallery-tab { flex-shrink: 0; }
/* Album chips also scroll horizontally so they don't stack. */
.gallery-album-chips {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.gallery-album-chips::-webkit-scrollbar { display: none; }
.gallery-chip { flex-shrink: 0; }
/* Tasks filter chips — same horizontal-swipe behaviour as the gallery
albums / library chips on mobile: one row, slide-to-see-more. */
.tasks-activity-filters {
overflow-x: auto;
flex-wrap: nowrap !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.tasks-activity-filters::-webkit-scrollbar { display: none; }
.tasks-activity-filters > * { flex-shrink: 0; }
/* Detail view */
.gallery-detail-body {
flex-direction: column;
}
.gallery-detail-sidebar {
width: 100%;
}
/* Album grid: smaller tile minimum on mobile. */
.gallery-albums-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
}
/* ── Scoreboard table ── */
.scoreboard-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88em;
}
.scoreboard-table th {
text-align: left;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 0.85em;
color: color-mix(in srgb, var(--fg) 55%, transparent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.scoreboard-table th:not(:first-child) {
text-align: center;
}
.scoreboard-table td {
padding: 5px 10px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
text-align: center;
}
.scoreboard-table td.scoreboard-model {
text-align: left;
font-weight: 500;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.scoreboard-table td.scoreboard-pct {
font-weight: 600;
color: var(--red);
}
.scoreboard-table tbody tr:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
/* ── Compare search results ── */
.compare-search-results {
display: flex;
flex-direction: column;
gap: 2px;
}
.compare-search-result {
padding: 6px 0;
border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
}
.compare-search-result:last-child {
border-bottom: none;
}
.search-result-title {
color: var(--color-accent);
text-decoration: none;
font-weight: 500;
font-size: 0.9em;
display: block;
line-height: 1.3;
}
.search-result-title:hover {
color: var(--color-link-hover);
text-decoration: underline;
}
.search-result-snippet {
color: color-mix(in srgb, var(--fg) 70%, transparent);
font-size: 0.82em;
line-height: 1.4;
margin-top: 2px;
}
.search-result-url {
color: color-mix(in srgb, var(--fg) 35%, transparent);
font-size: 0.75em;
margin-top: 3px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Cookbook Tool ── */
/* ── Cookbook ── */
.cookbook-serve-preset {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
margin-bottom: 3px;
cursor: pointer;
transition: background 0.15s;
}
.cookbook-serve-preset:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.cookbook-serve-preset-name {
font-size: 11px;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cookbook-serve-preset-meta {
font-size: 9px;
font-family: 'Fira Code', monospace;
color: var(--fg-muted);
opacity: 0.5;
}
.cookbook-serve-preset-launch {
background: none;
border: none;
color: #50fa7b;
cursor: pointer;
font-size: 10px;
opacity: 0.6;
padding: 0 2px;
position: relative;
top: -4px;
}
.cookbook-serve-preset-launch:hover { opacity: 1; }
.cookbook-serve-preset-rm {
background: none;
border: none;
color: var(--fg-muted);
cursor: pointer;
font-size: 10px;
opacity: 0.3;
padding: 0 2px;
position: relative;
top: -4px;
}
.cookbook-serve-preset-rm:hover { opacity: 0.8; color: var(--red); }
.cookbook-body {
display: flex;
flex-direction: column;
gap: 10px;
overflow-y: auto;
flex: 1;
min-height: 0;
scrollbar-width: thin;
}
.cookbook-body::-webkit-scrollbar {
width: 4px;
}
.cookbook-body::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--fg) 15%, transparent);
border-radius: 4px;
}
.cookbook-body::-webkit-scrollbar-track {
background: transparent;
padding: 4px 0;
}
.cookbook-group {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-height: 0;
}
/* Cards — match admin-card */
.cookbook-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.cookbook-card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.cookbook-card-header-actions { display: flex; gap: 4px; }
.cookbook-card-title { font-size: 13px; font-weight: 600; }
.cookbook-card-desc {
font-size: 11px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
}
/* Fields grid */
.cookbook-fields {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 6px;
margin-top: 4px;
}
.cookbook-field-label {
display: flex;
flex-direction: column;
font-size: 11px;
color: color-mix(in srgb, var(--fg) 60%, transparent);
gap: 3px;
}
/* Inputs — match admin-add-form input */
.cookbook-field-input {
padding: 5px 8px;
font-size: 12px;
font-family: inherit;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
outline: none;
}
.cookbook-field-input:focus { border-color: var(--red); }
#hwfit-dl-server, #hwfit-usecase, #hwfit-server-select, #hwfit-cache-server, #serve-sort { height: 28px; width: 88px; }
/* Serve tab — match Documents sizing, but leave room for the active "Cancel"
label and center it vertically so the descenders don't clip. */
#hwfit-cache-select {
min-width: 58px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
}
/* Command preview */
.cookbook-cmd-preview {
margin: 4px 0 0;
padding: 8px 10px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
font-family: 'Fira Code', monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
color: var(--fg);
}
/* Buttons — match admin-btn-sm */
.cookbook-actions { display: flex; gap: 6px; margin-top: 2px; }
.cookbook-btn {
padding: 4px 10px;
min-width: 54px;
text-align: center;
font-size: 11px;
font-family: inherit;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--fg);
cursor: pointer;
transition: all 0.15s;
}
.cookbook-btn:hover {
background: var(--border);
border-color: var(--accent);
}
.cookbook-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.cookbook-run-btn {
background: var(--accent);
color: var(--panel);
border-color: var(--accent);
font-weight: 600;
}
.cookbook-run-btn:hover { opacity: 0.85; }
.cookbook-stop-btn {
background: var(--color-error);
border-color: var(--color-error);
}
/* Output */
.cookbook-output-wrap {
position: relative;
margin: 0;
}
.cookbook-output-kill {
position: absolute;
top: 4px;
right: 34px;
width: 22px;
height: 22px;
padding: 0;
background: none;
border: 1px solid transparent;
border-radius: 4px;
color: var(--fg-muted);
font-size: 16px;
line-height: 1;
cursor: pointer;
opacity: 0;
transition: opacity .15s, color .15s;
display: flex;
align-items: center;
justify-content: center;
}
.cookbook-output-kill:hover { color: var(--color-error, var(--warn)); border-color: var(--border); }
.cookbook-output-wrap:hover .cookbook-output-kill { opacity: 0.7; }
.cookbook-output-wrap .copy-code {
position: absolute;
top: 6px;
right: 6px;
}
.cookbook-output-wrap:hover .copy-code { opacity: 0.7; }
.cookbook-output-pre {
margin: 0;
padding: 8px 10px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
font-family: 'Fira Code', monospace;
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
max-height: 180px;
overflow-y: auto;
}
.cookbook-output-error { color: var(--color-error); }
/* Downloaded/cached models */
.hwfit-cached-item {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 6px 8px;
margin: 2px 0;
border-radius: 6px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--fg) 3%, transparent);
cursor: default;
font-size: 12px;
}
.hwfit-cached-item .hwfit-serve-panel {
flex-basis: 100%;
margin-top: 4px;
}
.hwfit-cached-item:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.hwfit-cached-header {
font-size: 10px;
opacity: 0.4;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border: none !important;
background: none !important;
padding: 2px 8px;
cursor: default !important;
}
.hwfit-cached-header:hover { background: none !important; }
.hwfit-cached-name {
font-weight: 600;
flex-shrink: 0;
}
.hwfit-cached-repo {
flex: 1;
color: var(--fg-muted);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hwfit-cached-size,
.hwfit-cached-files {
font-size: 10px;
color: var(--fg-muted);
white-space: nowrap;
}
.hwfit-cached-serve,
.hwfit-cached-delete {
flex-shrink: 0;
}
.hwfit-cached-delete {
opacity: 0.4;
font-size: 10px;
}
.hwfit-cached-delete:hover {
opacity: 1;
color: var(--color-error, var(--warn));
}
.hwfit-cached-badge {
font-size: 9px;
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
white-space: nowrap;
}
.hwfit-cached-ready {
background: color-mix(in srgb, var(--color-success, #4caf50) 20%, transparent);
color: var(--color-success, #4caf50);
}
.hwfit-cached-dl {
background: color-mix(in srgb, var(--color-warning, #f0ad4e) 20%, transparent);
color: var(--color-warning, #f0ad4e);
}
/* Cookbook notification dot */
#tool-cookbook-btn {
position: relative;
}
#rail-cookbook {
position: relative;
}
.cookbook-open-loading {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
flex-shrink: 0;
margin-left: 2px;
color: var(--accent, var(--red));
opacity: 0.9;
}
.cookbook-open-loading .ai-spinner,
.cookbook-open-loading .ai-spinner-whirlpool {
display: inline-flex !important;
width: 14px;
height: 14px;
margin: 0 !important;
gap: 0 !important;
}
.cookbook-open-loading canvas {
display: block;
}
#rail-cookbook .cookbook-open-loading {
position: absolute;
right: 2px;
top: -2px;
width: 12px;
height: 12px;
margin-left: 0;
}
/* The serve-list 'downloading' / 'download stalled' status text sits a hair
low against the rest of the meta line on mobile — nudge it up 2px. */
@media (max-width: 768px) {
.cookbook-dl-status {
position: relative;
top: -2px;
}
}
.cookbook-notif-dot {
width: 6px;
height: 6px;
flex-shrink: 0;
border-radius: 50%;
background: var(--color-success, #4caf50);
/* Drives the breathing glow in the keyframes (matches email's dot). */
--notif-glow: var(--color-success, #4caf50);
animation: cookbook-notif-pulse 2s ease-in-out infinite;
}
.cookbook-notif-dot.cookbook-notif-error {
background: var(--color-error, #f44);
--notif-glow: var(--color-error, #f44);
position: relative;
left: -2px;
top: -1px;
}
.cookbook-tab-error-dot { --notif-glow: var(--color-error, #f44); }
.rail-notify-error { color: var(--color-error, #f44) !important; }
.cookbook-tab-error-dot {
display: inline-block; width: 5px; height: 5px; border-radius: 50%;
background: var(--color-error, #f44); margin-left: 4px; vertical-align: middle;
animation: cookbook-notif-pulse 2s ease-in-out infinite;
}
@keyframes cookbook-notif-pulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 0 0 color-mix(in srgb, var(--notif-glow, var(--accent, var(--red))) 60%, transparent);
}
50% {
opacity: 0.85;
box-shadow: 0 0 6px 2px color-mix(in srgb, var(--notif-glow, var(--accent, var(--red))) 55%, transparent);
}
}
.cookbook-clear-btn {
font-size: 10px !important;
padding: 0 8px !important;
height: 22px !important;
border-radius: 6px !important;
border: 1px solid var(--border) !important;
color: color-mix(in srgb, var(--fg) 40%, transparent) !important;
opacity: 1 !important;
background: none !important;
}
.cookbook-clear-btn:hover {
color: var(--red) !important;
border-color: var(--red) !important;
background: color-mix(in srgb, var(--red) 8%, transparent) !important;
}
.cookbook-bulk-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
margin-bottom: 4px;
background: color-mix(in srgb, var(--red) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--red) 20%, transparent);
border-radius: 6px;
font-size: 11px;
}
.cookbook-bulk-bar.hidden { display: none; }
.serve-select-cb {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; cursor: pointer;
background: var(--border); transition: background 0.15s;
align-self: center;
position: relative;
top: -2px;
}
#serve-bulk-bar {
position: relative;
top: -6px;
}
#serve-bulk-bar #serve-bulk-cancel {
top: 0 !important;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 24px;
line-height: 1;
}
#serve-bulk-bar #serve-bulk-cancel svg {
top: 0;
}
.serve-select-cb.selected { background: var(--red); }
.serve-select-cb:hover { background: color-mix(in srgb, var(--red) 50%, transparent); }
#hwfit-cache-select.active {
background: var(--red);
color: #fff;
border-color: var(--red);
}
.cookbook-serve-dirs {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.cookbook-serve-dir-pill {
font-size: 10px;
font-family: 'Fira Code', monospace;
padding: 2px 8px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
color: var(--fg-muted);
letter-spacing: 0.2px;
}
/* "running" pill on a Serve-tab card when the model has a live serve task. */
.cookbook-serve-running-pill {
display: inline-block;
margin-left: 6px;
padding: 1px 7px;
border-radius: 10px;
font-size: 9px;
font-weight: 500;
text-transform: lowercase;
letter-spacing: 0.3px;
vertical-align: 2px;
position: relative;
top: -1px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
}
.cookbook-serve-dir-edit {
font-size: 9px;
color: var(--fg-muted);
opacity: 0.4;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.cookbook-serve-dir-edit:hover {
opacity: 0.7;
color: var(--accent, var(--red));
}
.cookbook-gpu-group {
display: flex;
gap: 2px;
margin-top: -4px;
}
.cookbook-gpu-btn {
width: 22px; height: 22px;
font-size: 10px;
font-family: 'Fira Code', monospace;
border: 1px solid var(--border);
border-radius: 4px;
background: none;
color: var(--fg-muted);
cursor: pointer;
opacity: 0.4;
transition: all 0.15s;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.cookbook-gpu-btn:hover {
opacity: 0.7;
border-color: var(--fg-muted);
}
.cookbook-gpu-btn.active {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent);
border-color: color-mix(in srgb, var(--fg) 55%, transparent);
color: var(--fg);
font-weight: 600;
}
/* Probe annotation states (set by "Free?" button) */
.cookbook-gpu-btn.gpu-free {
opacity: 1;
border-color: #4ade80;
color: #4ade80;
}
.cookbook-gpu-btn.gpu-busy {
opacity: 0.85;
border-color: color-mix(in srgb, var(--red) 70%, transparent);
color: var(--red);
background: color-mix(in srgb, var(--red) 8%, transparent);
}
.cookbook-gpu-btn.gpu-missing {
opacity: 0.2;
text-decoration: line-through;
cursor: not-allowed;
}
/* Keep both GPU action labels on one line — they wrapped to two rows on
mobile, which looked broken. */
.cookbook-gpu-probe, .cookbook-gpu-clear { white-space: nowrap; }
/* "Clear Server" lives in the actions row next to "Probe GPUs".
Inherits .cookbook-btn sizing; this rule just adds the destructive
red tint on hover so the "this kills processes" cue stays. */
.cookbook-gpu-clear:hover:not(:disabled) {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 60%, var(--border));
background: color-mix(in srgb, var(--red) 8%, transparent);
}
.cookbook-gpu-clear:disabled { opacity: 0.4; cursor: wait; }
.cookbook-gpu-clear:disabled { opacity: 0.4; cursor: wait; }
/* GPU probe popup — per-GPU process list with kill buttons */
.cookbook-gpu-popup {
position: absolute;
z-index: 240;
min-width: 280px;
max-width: 420px;
background: var(--panel, #1a1a1a);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
font-family: 'Fira Code', monospace;
font-size: 11px;
color: var(--fg);
}
.cookbook-gpu-popup-head {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
font-weight: 600;
}
.cookbook-gpu-popup-stats {
flex: 1;
font-weight: 400;
color: var(--fg-muted);
font-size: 10px;
}
.cookbook-gpu-popup-close {
background: none;
border: none;
color: var(--fg-muted);
cursor: pointer;
font-size: 14px;
padding: 0 4px;
}
.cookbook-gpu-popup-close:hover { color: var(--fg); }
.cookbook-gpu-popup-body {
padding: 4px 0;
max-height: 320px;
overflow-y: auto;
}
.cookbook-gpu-popup-empty {
padding: 10px;
color: var(--fg-muted);
font-style: italic;
}
.cookbook-gpu-proc {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
}
.cookbook-gpu-proc:hover { background: color-mix(in srgb, var(--fg) 5%, transparent); }
.cookbook-gpu-proc-info {
flex: 1;
display: flex;
gap: 8px;
align-items: center;
min-width: 0;
}
.cookbook-gpu-proc-pid {
color: var(--fg-muted);
width: 56px;
flex-shrink: 0;
}
.cookbook-gpu-proc-name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.cookbook-gpu-proc-mem {
color: var(--fg-muted);
flex-shrink: 0;
}
.cookbook-gpu-proc-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.cookbook-gpu-kill {
border: 1px solid color-mix(in srgb, var(--red) 60%, transparent);
background: none;
color: var(--red);
border-radius: 3px;
padding: 2px 8px;
cursor: pointer;
font-family: inherit;
font-size: 10px;
}
.cookbook-gpu-kill[data-sig="KILL"] {
background: color-mix(in srgb, var(--red) 12%, transparent);
}
.cookbook-gpu-kill:hover:not(:disabled) {
background: color-mix(in srgb, var(--red) 20%, transparent);
}
.cookbook-gpu-kill:disabled { opacity: 0.4; cursor: wait; }
.cookbook-hf-link {
font-size: 9px;
text-decoration: none;
color: var(--fg-muted);
opacity: 0.5;
padding: 1px 5px;
border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
border-radius: 3px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
vertical-align: 1px;
letter-spacing: 0.3px;
font-weight: 600;
}
.cookbook-hf-link:hover {
opacity: 0.8;
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
/* Running tab sections */
.cookbook-saved-section,
.cookbook-serve-section,
.cookbook-dl-section {
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 6px;
overflow: hidden;
}
.cookbook-section-header {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
cursor: pointer;
user-select: none;
/* Persistent surface + border so it reads as a clickable bar instead of
blending into the panel background. */
background: color-mix(in srgb, var(--fg) 5%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
transition: background 0.15s, border-color 0.15s;
}
.cookbook-section-header:hover {
background: color-mix(in srgb, var(--fg) 9%, transparent);
border-color: color-mix(in srgb, var(--fg) 22%, transparent);
}
.cookbook-section-header .cookbook-section-title {
flex: 1;
}
.cookbook-section-header .cookbook-clear-btn {
margin-left: 0;
position: relative;
top: -3px;
}
/* "Stop all" sits just left of "Clear finished"; it carries the auto margin so
the pair is pushed together to the right of the section title. */
.cookbook-section-header .cookbook-stop-all-btn {
margin-left: auto;
margin-right: 6px;
position: relative;
top: -3px;
}
.cookbook-stop-all-btn {
font-size: 10px !important;
padding: 0 8px !important;
height: 22px !important;
border-radius: 6px !important;
border: 1px solid var(--border) !important;
color: color-mix(in srgb, var(--fg) 40%, transparent) !important;
opacity: 1 !important;
background: none !important;
}
.cookbook-stop-all-btn:hover {
color: var(--warn, #f0ad4e) !important;
border-color: var(--warn, #f0ad4e) !important;
background: color-mix(in srgb, var(--warn, #f0ad4e) 8%, transparent) !important;
}
.cookbook-section-chevron {
flex-shrink: 0;
opacity: 0.6;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.cookbook-section-header:hover .cookbook-section-chevron { opacity: 1; }
.cookbook-section-body {
padding: 0;
}
.cookbook-saved-toggle-header {
font-size: 10px;
font-weight: 600;
color: var(--fg-muted);
text-transform: uppercase;
letter-spacing: 0.3px;
margin-bottom: 4px;
}
.cookbook-saved-toggle:hover { color: var(--fg); }
.cookbook-saved-item {
margin: 2px 0;
border-radius: 4px;
font-size: 11px;
}
.cookbook-saved-header {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 4px;
transition: background 0.08s;
}
.cookbook-saved-header:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.cookbook-saved-item .hwfit-serve-panel {
margin: 4px 0 4px 6px;
}
.cookbook-saved-host {
font-size: 10px;
color: var(--fg-muted);
font-family: 'Fira Code', monospace;
}
.cookbook-saved-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cookbook-saved-launch {
font-size: 10px;
padding: 2px 8px;
}
.cookbook-saved-del {
background: none;
border: none;
color: var(--fg-muted);
opacity: 0.3;
cursor: pointer;
font-size: 11px;
padding: 0 2px;
}
.cookbook-saved-del:hover {
opacity: 1;
color: var(--color-error, var(--warn));
}
/* Serve config panel */
.hwfit-serve-panel {
margin-top: 6px;
padding: 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.cookbook-serve-slots {
display: flex;
gap: 3px;
justify-content: flex-end;
margin-bottom: 4px;
}
.cookbook-slot-btn {
min-width: 22px; height: 22px;
padding: 0 6px;
font-size: 10px; font-weight: 600;
border: 1px solid var(--border);
border-radius: 4px;
background: none;
white-space: nowrap;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
color: var(--fg-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: all 0.15s;
}
.cookbook-slot-btn:hover { opacity: 0.9; border-color: var(--fg-muted); }
/* Saved-configs split button: "Save" + dropdown-arrow joined into one control,
but each segment keeps its own style — Save uses the filled accent
"Done"-button look; the arrow stays a subtle outlined menu trigger. */
.cookbook-saved-split { gap: 0; }
.cookbook-saved-save,
.cookbook-saved-arrow { max-width: none; }
.cookbook-saved-save {
padding: 0 10px;
gap: 4px;
background: var(--red);
color: #fff;
border-color: var(--red);
font-weight: 600;
opacity: 1;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.cookbook-saved-save:hover {
background: color-mix(in srgb, var(--red) 80%, white);
border-color: color-mix(in srgb, var(--red) 80%, white);
opacity: 1;
}
.cookbook-saved-arrow {
padding: 0 6px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.cookbook-slot-saved { background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--accent); }
.cookbook-slot-saved:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.cookbook-slot-btn.active { opacity: 1; background: var(--accent); color: #fff; border-color: var(--accent);
}
.cookbook-slot-wrap {
position: relative;
display: inline-flex;
}
.cookbook-slot-wrap:hover .cookbook-slot-del { opacity: 1; }
.cookbook-slot-del {
position: absolute;
top: -5px;
right: -5px;
width: 14px;
height: 14px;
padding: 0;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--bg, #1a1a1a);
color: var(--fg-muted);
font-size: 12px;
line-height: 1;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, color 0.15s, border-color 0.15s;
display: flex;
align-items: center;
justify-content: center;
}
.cookbook-slot-del:hover {
color: #f44;
border-color: #f44;
}
.cookbook-card-backend { display: none; }
/* Dependencies row — aligned tags */
.cookbook-dep-row {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
}
/* Let the deps list fill the available window height instead of being
capped at the default .doclib-grid 400-px box. */
[data-backend-group="Dependencies"] #cookbook-deps-list {
flex: 1;
max-height: none;
}
/* Settings tab: stack the HF token + Servers cards as distinct blocks
(matches the Download tab's block-per-section layout). */
.cookbook-settings-stack {
display: flex;
flex-direction: column;
gap: 10px;
flex: 1;
/* Scroll the whole settings panel so the Servers card can grow to hold every
server (it used to be a cramped internal scroll box that clipped them). */
overflow-y: auto;
min-height: 0;
}
.cookbook-settings-stack.hidden { display: none; }
.cookbook-dep-row.cookbook-dep-blocked { opacity: 0.4; }
.cookbook-dep-info { flex: 1; min-width: 0; }
.cookbook-dep-section {
display: flex;
align-items: baseline;
gap: 8px;
margin: 12px 2px 4px;
}
.cookbook-dep-section-title {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
}
.cookbook-dep-section-note {
font-size: 10px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
}
.cookbook-dep-tag {
font-size: 9px;
padding: 0 8px;
border-radius: 4px;
white-space: nowrap;
text-align: center;
min-width: 62px;
box-sizing: border-box;
line-height: 1;
/* Explicit height so the tags (Install / Installed) are the EXACT
same height as the sibling tags — Firefox gives buttons a taller
native box otherwise. Mirrors the server-row tag rule (height:24px). */
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cookbook-dep-target {
border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 50%, transparent);
}
.cookbook-dep-cat {
background: color-mix(in srgb, var(--fg) 10%, transparent);
color: color-mix(in srgb, var(--fg) 60%, transparent);
}
.cookbook-dep-installed {
background: color-mix(in srgb, var(--green, #50fa7b) 18%, transparent);
color: var(--green, #50fa7b);
border: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
}
.cookbook-dep-na {
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: color-mix(in srgb, var(--fg) 60%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
cursor: help;
}
.cookbook-dep-install {
background: var(--accent, var(--red));
color: #fff;
border: none;
cursor: pointer;
font-family: inherit;
font-weight: 500;
position: relative;
top: -3px;
/* Strip the native button box so it's the same height as the sibling tags
(Firefox renders taller otherwise); height comes from .cookbook-dep-tag. */
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.cookbook-dep-install:hover { opacity: 0.85; }
/* Installed split button: "Installed" label + separator + ▾ caret; clicking it
opens the actions menu (Update). Replaces the old ⋮ button. */
.cookbook-dep-installed-btn {
padding: 0;
cursor: pointer;
font-family: inherit;
overflow: hidden;
position: relative;
top: -3px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.cookbook-dep-installed-btn .cookbook-dep-installed-label { padding: 0 8px; }
.cookbook-dep-installed-btn .cookbook-dep-caret {
display: inline-flex;
align-items: center;
align-self: stretch;
padding: 0 7px;
font-size: 12px;
opacity: 0.85;
border-left: 1px solid color-mix(in srgb, var(--green, #50fa7b) 35%, transparent);
}
.cookbook-dep-installed-btn:hover { filter: brightness(1.15); }
.hwfit-serve-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px;
margin-bottom: 6px;
}
.hwfit-serve-row label {
font-size: 10px;
color: var(--fg-muted);
white-space: nowrap;
letter-spacing: 0.3px;
}
.hwfit-serve-row label select,
.hwfit-serve-row label input {
display: block;
margin-top: 2px;
}
.hwfit-sf {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-family: inherit;
font-size: 12px;
padding: 0 6px;
height: 28px;
}
.hwfit-sf[data-field="backend"],
.hwfit-sf[data-field="dtype"],
.hwfit-sf[data-field="tp"] {
height: 32px;
box-sizing: border-box;
width: 100%;
}
.hwfit-sf:focus {
border-color: var(--accent, var(--red));
outline: none;
}
.hwfit-sf[type="text"] { width: 100%; }
.hwfit-sf.hwfit-sf-wide { width: 100%; }
.hwfit-sf.hwfit-sf-full { grid-column: 1 / -1; }
.hwfit-serve-checks {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 6px;
}
.hwfit-sf-cb {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--fg-muted);
cursor: pointer;
padding: 3px 0;
}
.hwfit-sf-cb:hover { color: var(--fg); }
/* Speculative method + tokens controls — render inline beside the checkbox */
.hwfit-spec-group .hwfit-spec-method,
.hwfit-spec-group .hwfit-spec-tokens {
height: 20px;
padding: 0 4px;
font-size: 11px;
/* Match the generic .hwfit-sf serve controls: inherit font + 4px radius
so the Speculative method/token widgets read as the same control
family as the rest of the panel (they were 'Fira Code'/3px before). */
font-family: inherit;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
}
.hwfit-spec-group .hwfit-spec-method { min-width: 110px; }
.hwfit-spec-group .hwfit-spec-tokens { width: 44px; text-align: center; }
/* Themed step buttons replacing the native number-input spinner. */
.hwfit-numstep {
display: inline-flex;
align-items: stretch;
height: 20px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
overflow: hidden;
}
.hwfit-numstep:focus-within { border-color: var(--accent, var(--red)); }
.hwfit-numstep .hwfit-spec-tokens {
border: none !important;
border-radius: 0 !important;
width: 38px;
height: 100%;
background: transparent;
-moz-appearance: textfield;
appearance: textfield;
margin: 0;
}
.hwfit-numstep .hwfit-spec-tokens::-webkit-outer-spin-button,
.hwfit-numstep .hwfit-spec-tokens::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.hwfit-numstep-btn {
width: 16px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border: none;
color: var(--accent, var(--red));
font-family: inherit;
font-size: 15px;
font-weight: 700;
line-height: 1;
cursor: pointer;
padding: 0;
opacity: 1;
transition: background 0.12s, opacity 0.12s, color 0.12s;
}
.hwfit-numstep-btn:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
color: var(--accent, var(--red));
}
.hwfit-numstep-btn:active {
background: color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
}
.hwfit-numstep-btn:focus { outline: none; }
.hwfit-numstep-btn[data-step="-1"] { border-right: 1px solid var(--border); }
.hwfit-numstep-btn[data-step="1"] { border-left: 1px solid var(--border); }
.hwfit-spec-group:has(input[type="checkbox"]:not(:checked)) .hwfit-spec-method,
.hwfit-spec-group:has(input[type="checkbox"]:not(:checked)) .hwfit-spec-tokens,
.hwfit-spec-group:has(input[type="checkbox"]:not(:checked)) .hwfit-numstep {
opacity: 0.45;
pointer-events: none;
}
/* Custom toggle switch */
.hwfit-sf-cb input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
width: 28px;
height: 14px;
border-radius: 7px;
background: color-mix(in srgb, var(--fg) 15%, transparent);
position: relative;
cursor: pointer;
margin: 0;
flex-shrink: 0;
transition: background 0.2s;
}
.hwfit-sf-cb input[type="checkbox"]::after {
content: '';
position: absolute;
top: 1.5px;
left: 2px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #000;
transition: transform 0.2s, background 0.2s;
}
.hwfit-sf-cb input[type="checkbox"]:checked {
background: var(--accent, var(--red));
}
.hwfit-sf-cb input[type="checkbox"]:checked::after {
transform: translateX(14px);
background: #fff;
}
.hwfit-serve-extra {
margin-bottom: 6px;
}
.hwfit-serve-extra label {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 10px;
color: var(--fg-muted);
letter-spacing: 0.3px;
}
.hwfit-serve-extra .hwfit-sf {
width: 100%;
}
.hwfit-serve-cmd {
margin: 6px 0;
padding: 8px 10px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 4px;
font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
font-size: 10px;
white-space: pre-wrap;
word-break: break-all;
width: 100%;
box-sizing: border-box;
resize: none;
color: var(--fg);
line-height: 1.5;
min-height: 36px;
overflow: hidden;
}
.hwfit-serve-actions {
display: flex;
gap: 6px;
margin-top: 4px;
align-items: center;
}
.hwfit-serve-actions .cookbook-btn {
padding: 5px 14px;
font-size: 11px;
}
.hwfit-serve-actions-spacer { flex: 1 1 auto; }
.hwfit-serve-launch {
background: var(--accent-primary, var(--red));
color: #fff;
border: 1px solid var(--accent-primary, var(--red));
border-radius: 4px;
font-weight: 700;
}
.hwfit-serve-launch:hover {
opacity: 0.9;
}
/* Task header ⋮ menu button */
.cookbook-task-save-btn {
background: none;
border: none;
color: var(--fg-muted);
cursor: pointer;
opacity: 0.3;
padding: 2px;
flex-shrink: 0;
transition: opacity 0.15s;
position: relative;
top: -4px;
transform: scale(1.15);
}
.cookbook-task-save-btn:hover { opacity: 0.8; }
.cookbook-task-edit-btn {
background: none;
border: none;
color: var(--fg-muted);
cursor: pointer;
opacity: 0.3;
padding: 2px;
flex-shrink: 0;
transition: opacity 0.15s;
position: relative;
top: -4px;
transform: scale(1.15);
}
.cookbook-task-edit-btn:hover { opacity: 0.8; }
.cookbook-task-menu-btn {
background: none;
border: 1px solid transparent;
color: var(--fg-muted);
font-size: 16px;
width: 22px;
height: 22px;
cursor: pointer;
opacity: 0;
border-radius: 4px;
position: relative;
top: -3px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s;
flex-shrink: 0;
}
.cookbook-task:hover .cookbook-task-menu-btn { opacity: 0.6; }
.cookbook-task-menu-btn:hover {
opacity: 1 !important;
background: color-mix(in srgb, var(--fg) 7%, transparent);
border-color: var(--border);
color: var(--fg);
}
@media (max-width: 768px) {
.cookbook-task .cookbook-task-menu-btn {
opacity: 0.72;
width: 32px;
height: 32px;
min-width: 32px;
top: -5px;
}
.cookbook-task .cookbook-task-menu-btn:active {
opacity: 1;
background: color-mix(in srgb, var(--fg) 9%, transparent);
border-color: var(--border);
}
}
/* Same z-index treatment as .cookbook-task-dropdown — cookbook modal's
auto-stack climbs past low values; popups append to body and need to
sit above the modal regardless. */
.hwfit-cached-dropdown,
.cookbook-gpu-split-menu,
.cookbook-saved-menu,
.cookbook-dep-menu {
z-index: 10000;
}
/* Launch-command textarea wrapper — Copy pill floats at the top-right
corner of the field (chat run-output pattern). */
.hwfit-serve-cmd-wrap {
position: relative;
}
.hwfit-serve-cmd-wrap .hwfit-serve-cmd {
/* Just enough breathing room so a cursor at line-end doesn't actually
touch the Copy icon — text otherwise uses the full width of the box. */
padding-right: 32px;
}
.hwfit-serve-copy-inline {
position: absolute;
top: 4px; right: 4px;
z-index: 2;
width: 26px !important;
height: 26px !important;
min-width: 26px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.12s, color 0.12s;
}
.hwfit-serve-copy-inline:hover { opacity: 1; color: var(--accent-primary, var(--red)); }
.hwfit-serve-copy-inline.copied { opacity: 1; color: var(--color-save-green, #4caf50); }
.hwfit-serve-copy-inline svg { display: block; }
/* Split button: Clear Server (main) + ^ arrow (more actions). The two halves
are visually joined — left button square on its right edge, arrow square
on its left edge — so they read as one widget. */
.cookbook-gpu-split { display: inline-flex; flex-shrink: 0; }
.cookbook-gpu-split .cookbook-gpu-split-main {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.cookbook-gpu-split .cookbook-gpu-split-arrow {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-left-width: 0 !important;
padding: 0 6px !important;
min-width: 24px;
}
.cookbook-gpu-split .cookbook-gpu-split-arrow svg { display: block; }
.cookbook-task-dropdown {
/* Must sit above the cookbook modal (whose auto-stack z-index starts at
300 and climbs whenever the modal is brought to the front). 1000 was
not enough — the dropdown rendered behind the modal on mobile. */
z-index: 10000;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
min-width: auto;
width: max-content;
}
.cookbook-task-dropdown .dropdown-item-compact {
padding: 6px 10px;
font-size: 12px;
cursor: pointer;
border-radius: 4px;
white-space: nowrap;
}
.cookbook-task-dropdown .dropdown-item-compact:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
}
.cookbook-dropdown-danger:hover {
color: var(--red) !important;
}
.cookbook-task-retry:hover { color: var(--color-warning, #f0ad4e); }
.cookbook-task-kill:hover { color: var(--color-error, var(--warn)); }
/* Serve "Edit command" modal */
.cookbook-edit-overlay {
position: fixed; inset: 0; z-index: 10000;
background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center;
}
.cookbook-edit-modal {
width: min(720px, 92vw);
background: var(--panel, var(--bg));
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.35);
display: flex; flex-direction: column; gap: 10px;
}
.cookbook-edit-title {
font-size: 13px; font-weight: 600; color: var(--fg);
}
.cookbook-edit-textarea {
width: 100%;
min-height: 140px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-family: ui-monospace, Menlo, Consolas, monospace;
font-size: 12px;
padding: 8px;
outline: none;
resize: vertical;
white-space: pre;
overflow: auto;
}
.cookbook-edit-textarea:focus { border-color: var(--accent, var(--fg)); }
.cookbook-edit-actions {
display: flex; gap: 8px; justify-content: flex-end;
}
.cookbook-edit-save {
background: color-mix(in srgb, var(--accent) 20%, transparent) !important;
border-color: var(--accent) !important;
}
/* Running tasks */
/* Each running task is a real card with a left accent stripe coloured by
status, instead of an invisible divider against bg. */
.cookbook-task {
margin: 0 0 8px;
border: 1px solid var(--border);
border-left: 3px solid color-mix(in srgb, var(--fg) 30%, transparent);
border-radius: 8px;
overflow: hidden;
max-width: 100%;
min-width: 0;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
transition: border-color 0.15s, background 0.15s;
}
.cookbook-task:hover {
border-color: color-mix(in srgb, var(--fg) 25%, var(--border));
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
}
/* Status-driven left stripe via :has() — graceful fallback to neutral. */
.cookbook-task:has(.cookbook-task-running) { border-left-color: var(--green, #50fa7b); }
.cookbook-task:has(.cookbook-task-done) { border-left-color: var(--green, #50fa7b); }
.cookbook-task:has(.cookbook-task-error) { border-left-color: var(--color-error, var(--warn, #f87171)); }
.cookbook-task:has(.cookbook-task-queued) { border-left-color: var(--color-warning, #f0ad4e); }
/* Serve crashed / unreachable — full red frame so a dead server in the
Running tab is obvious at a glance (mirrors the endpoint health dot). */
.cookbook-task.cookbook-task-unreachable,
.cookbook-task[data-type="serve"][data-status="error"],
.cookbook-task[data-type="serve"][data-status="crashed"] {
border-color: var(--color-error, #f44);
border-left-color: var(--color-error, #f44);
background: color-mix(in srgb, var(--color-error, #f44) 8%, var(--bg));
}
.cookbook-task-header {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
background: color-mix(in srgb, var(--fg) 7%, transparent);
font-size: 12px;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
}
.cookbook-task-type {
text-transform: uppercase;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.5px;
padding: 1px 5px;
border-radius: 3px;
flex-shrink: 0;
width: 67.3px;
text-align: center;
}
.cookbook-task-type[data-type="serve"] {
background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
color: var(--green, #50fa7b);
}
.cookbook-dl-add-server {
position: relative;
top: -3px;
}
.cookbook-task-type[data-type="download"] {
background: color-mix(in srgb, var(--fg) 10%, transparent);
color: var(--fg-muted);
}
/* Finished state — overrides the per-type colors so a completed download or
serve task shows the same green FINISHED chip. */
.cookbook-task-type.cookbook-task-type-done {
background: color-mix(in srgb, var(--green, #50fa7b) 16%, transparent);
color: var(--green, #50fa7b);
}
.cookbook-task-backend {
width: 67.3px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
opacity: 0.5;
}
.cookbook-task-name {
flex: 1;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cookbook-task-wave {
font-size: 11px;
letter-spacing: -1px;
color: #fff;
/* Gentle pulse on top of the cycling bars so a running task reads as clearly
alive — especially when the card is collapsed and the log isn't visible. */
animation: cookbook-wave-pulse 1.3s ease-in-out infinite;
}
@keyframes cookbook-wave-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
.cookbook-task-indicator {
min-width: 24px;
text-align: center;
flex-shrink: 0;
display: inline-flex;
justify-content: center;
align-items: center;
/* The done/clear pill inside is wider than 24px and would otherwise
overflow into the edit/save buttons that sit immediately before it on
a serve task. Push it right so it stops crashing into them on desktop. */
margin-left: 8px;
}
.cookbook-task-check {
display: inline-flex;
align-items: center;
gap: 3px;
position: relative;
top: 2px;
cursor: pointer;
padding: 1px 6px 1px 4px;
border-radius: 9px;
color: var(--red, #ff5555);
transition: background 0.15s;
}
.cookbook-task-check svg { flex-shrink: 0; }
.cookbook-task-check:hover { background: color-mix(in srgb, var(--red, #ff5555) 18%, transparent); }
/* Shows "done" (green) normally; on hover the icon + label swap to a red ✕ /
"clear" to reveal it's a dismiss action. */
.cookbook-task-done-label,
.cookbook-task-clear-label {
font-size: 9px;
line-height: 1;
text-transform: lowercase;
}
.cookbook-task-done-label { color: var(--green, #50fa7b); }
.cookbook-task-clear-label { display: none; color: var(--red, #ff5555); }
.cookbook-task-check:hover .cookbook-task-done-label { display: none; }
.cookbook-task-check:hover .cookbook-task-clear-label { display: inline; }
/* Default: show the green check. On hover: swap to a red ✕ to signal "clear". */
.cookbook-task-clear-ico { display: none; }
.cookbook-task-check:hover .cookbook-task-check-ico { display: none; }
.cookbook-task-check:hover .cookbook-task-clear-ico { display: inline; }
/* "Serve" button on a finished download — green pill matching the "running" /
finished badge (it sits next to the green FINISHED chip + check). */
.cookbook-task-serve-btn {
font-size: 9px;
font-weight: 600;
padding: 1px 6px;
border: none;
border-radius: 3px;
line-height: 16px;
flex-shrink: 0;
cursor: pointer;
font-family: inherit;
background: color-mix(in srgb, var(--green, #50fa7b) 20%, transparent);
color: var(--green, #50fa7b);
position: relative;
top: -2px;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.cookbook-task-serve-btn:hover { background: color-mix(in srgb, var(--green, #50fa7b) 32%, transparent); }
.cookbook-task-sub {
padding: 1px 10px 4px;
line-height: 1;
display: flex;
align-items: center;
gap: 10px;
}
.cookbook-task-session {
font-size: 9px;
color: var(--fg-muted);
font-family: 'Fira Code', monospace;
opacity: 0.35;
letter-spacing: 0.3px;
}
.cookbook-task-server {
font-size: 9px;
color: var(--fg-muted);
font-family: 'Fira Code', monospace;
opacity: 0.4;
}
.cookbook-task-uptime {
font-size: 9px;
color: var(--fg-muted);
font-family: 'Fira Code', monospace;
opacity: 0.4;
}
.cookbook-task-status {
font-size: 9px;
padding: 1px 6px;
border-radius: 3px;
font-weight: 600;
text-align: left;
flex-shrink: 0;
line-height: 16px;
}
.cookbook-task-running { background: color-mix(in srgb, var(--green, #50fa7b) 20%, transparent); color: var(--green, #50fa7b); }
/* Stopping: same pill treatment as "running" but orange. */
.cookbook-task-stopping { background: color-mix(in srgb, var(--orange, #ffb86c) 22%, transparent); color: var(--orange, #ffb86c); }
.cookbook-task-done { background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
.cookbook-task-error { background: color-mix(in srgb, var(--color-error, var(--warn)) 20%, transparent); color: var(--color-error, var(--warn)); }
/* Crashed: same filled-pill treatment as "running" but red, with a red border. */
.cookbook-task-crashed { background: color-mix(in srgb, var(--red, #ff5555) 16%, transparent); color: var(--red, #ff5555); border: 1px solid var(--red, #ff5555); padding: 0 5px; }
.cookbook-task-stopped { background: color-mix(in srgb, var(--color-warning, #f0ad4e) 22%, transparent); color: var(--color-warning, #f0ad4e); }
.cookbook-task-queued { background: color-mix(in srgb, var(--color-warning, #f0ad4e) 15%, transparent); color: var(--color-warning, #f0ad4e); }
/* Stopped / crashed servers get an orange surround instead of the heavier
red — communicates "this isn't running, here's the last command you used,
relaunch when ready" without screaming "error". Color stays normal so the
command text remains easy to copy. */
.cookbook-task[data-status="stopped"],
.cookbook-task[data-status="error"],
.cookbook-task[data-status="crashed"] {
border-color: color-mix(in srgb, var(--color-warning, #f0ad4e) 55%, var(--border));
background: color-mix(in srgb, var(--color-warning, #f0ad4e) 6%, var(--bg));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-warning, #f0ad4e) 18%, transparent);
}
.cookbook-task[data-status="stopped"] .cookbook-output-pre,
.cookbook-task[data-status="error"] .cookbook-output-pre,
.cookbook-task[data-status="crashed"] .cookbook-output-pre {
border-top-color: color-mix(in srgb, var(--color-warning, #f0ad4e) 35%, var(--border));
}
.cookbook-task-retry,
.cookbook-task .cookbook-output-wrap { margin: 0; }
.cookbook-task .cookbook-output-pre {
border-radius: 0;
border-top: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
max-height: 150px;
max-width: 100%;
overflow-x: hidden;
box-sizing: border-box;
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
}
.cookbook-task .cookbook-output-pre:empty {
display: none;
}
.cookbook-task-collapsed {
display: none !important;
}
.cookbook-task-header {
cursor: pointer;
}
/* Env bar — match admin-card */
.cookbook-env-bar {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
}
.cookbook-env-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cookbook-env-row > .cookbook-field-label {
flex: 1 1 140px;
min-width: 120px;
}
.cookbook-env-row .cookbook-field-input {
width: 100%;
box-sizing: border-box;
}
.cookbook-extra-label { grid-column: 1 / -1; }
/* Tabs — match library tabs */
.cookbook-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
margin-bottom: 8px;
}
/* Mobile: every modal tab strip should swipe horizontally instead of wrapping
or getting cut off. Hide the scrollbar (still pannable via touch / drag). */
@media (max-width: 768px) {
.cookbook-tabs,
.memory-tabs,
.admin-tabs,
.lib-tabs,
.gallery-tabs,
.preset-tabs {
flex-wrap: nowrap !important;
overflow-x: auto !important;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.cookbook-tabs::-webkit-scrollbar,
.memory-tabs::-webkit-scrollbar,
.admin-tabs::-webkit-scrollbar,
.lib-tabs::-webkit-scrollbar,
.gallery-tabs::-webkit-scrollbar,
.preset-tabs::-webkit-scrollbar {
display: none;
}
.cookbook-tabs > *,
.memory-tabs > *,
.admin-tabs > *,
.lib-tabs > *,
.gallery-tabs > *,
.preset-tabs > * {
flex-shrink: 0;
}
}
.cookbook-tab {
/* `background: none` isn't enough on Windows — Chrome/Edge fall back to the
OS native button background (dark gray / black under dark mode) when
no explicit color + appearance reset is applied. Force transparent +
appearance:none so the tab inherits the modal's panel color. */
appearance: none;
-webkit-appearance: none;
background: transparent;
background-color: transparent;
padding: 6px 14px;
font-size: 12px;
font-family: inherit;
color: var(--fg-muted);
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: color 0.1s, border-color 0.1s;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 4px;
}
.cookbook-tab:hover {
color: var(--fg);
}
.cookbook-tab.active {
color: var(--accent, var(--red));
border-bottom-color: var(--accent, var(--red));
}
/* Mobile: hide tab icons so labels don't wrap to a new row */
@media (max-width: 640px) {
.cookbook-tab svg {
display: none;
}
.cookbook-tab {
padding: 6px 10px;
font-size: 13px;
}
/* Mobile cookbook text: matched to the calendar/library modals so
cookbook doesn't read noticeably larger than the rest of the app.
Inputs stay at 16px specifically — anything smaller triggers iOS
Safari's auto-zoom on focus, which yanks the layout. */
.cookbook-body {
font-size: 13px;
}
.cookbook-body h2 { font-size: 14px; }
.cookbook-body .memory-desc,
.cookbook-body .doclib-desc { font-size: 12px; }
.cookbook-body .memory-item-title { font-size: 13px; }
.cookbook-body .memory-item-meta { font-size: 11px !important; }
.cookbook-body .hwfit-sf,
.cookbook-body .cookbook-field-input,
.cookbook-body .cookbook-dl-repo { font-size: 16px; }
.cookbook-body .memory-toolbar-btn { font-size: 12px; }
}
/* Mobile cookbook sizing — kept in line with calendar/library modals. */
@media (max-width: 768px) {
/* The Speculative control (checkbox + method dropdown + token stepper)
is too wide for a phone — the stepper ran off the right edge of the
modal. Let the group wrap onto its own line, take full width, and
shrink the method dropdown so the +/− stepper stays on-screen. */
.hwfit-spec-group {
flex-wrap: wrap;
flex-basis: 100%;
row-gap: 4px;
}
.hwfit-spec-group .hwfit-spec-method { min-width: 0; flex: 1 1 auto; }
.hwfit-numstep { flex: 0 0 auto; }
.cookbook-card-title { font-size: 13px; }
.cookbook-card-desc { font-size: 12px; }
.cookbook-field-label { font-size: 12px; }
.cookbook-field-input { font-size: 16px; padding: 7px 10px; }
/* Dropdown pickers (Dependencies / Download tabs) don't need the 16px
no-zoom-on-focus trick that text inputs do, and 16px reads oversized next
to the Serve tab's 11px selects. Shrink just the selects to match. */
select.cookbook-field-input { font-size: 13px !important; padding: 5px 8px; }
/* Repo input + model search read oversized at 16px; bring their text and
placeholder hints down to match the rest of the cookbook. */
.cookbook-dl-repo, .hwfit-search { font-size: 13px !important; }
.cookbook-cmd-preview { font-size: 12px; padding: 8px 10px; }
.cookbook-btn { font-size: 12px; padding: 6px 12px; }
.cookbook-tab { font-size: 13px; }
.cookbook-task-header { font-size: 13px; padding: 6px 10px; }
.cookbook-task-name { font-size: 13px; }
.cookbook-task-sub { font-size: 11px; }
.cookbook-task-status {
font-size: 10px;
/* Vertical alignment of the phase/loading badge against the task title. */
position: relative;
top: 2px;
}
/* Keep the ascii wave aligned with the badge (both 2px down). */
.cookbook-task-wave {
position: relative;
top: 2px;
}
/* Session id (serve-…) + uptime sub-line down 1px. */
.cookbook-task-session,
.cookbook-task-uptime {
position: relative;
top: 1px;
}
/* Rest of the header row down 2px to match the status badge + wave: the
model title, the serve/download type tag, and the edit/save/menu icons. */
.cookbook-task-type,
.cookbook-task-name,
.cookbook-task-edit-btn,
.cookbook-task-save-btn {
position: relative;
top: 2px;
}
/* The ⋮ menu sits higher than the rest of the row. */
.cookbook-task-menu-btn {
position: relative;
top: -2px;
/* Mobile has no hover — the base rule's opacity:0 left the ⋮ button
invisible AND effectively unclickable. Always show + give a proper
touch target. */
opacity: 0.7 !important;
width: 32px !important;
height: 32px !important;
font-size: 18px !important;
}
/* The copied-confirmation checkmark sits 2px higher than the copy icon. */
.cookbook-output-copy.copied svg {
position: relative;
top: 2px;
}
.cookbook-section-title { font-size: 12px; }
.cookbook-settings-label { font-size: 12px; }
.cookbook-settings-hint { font-size: 11px; }
.cookbook-settings-input { font-size: 16px; }
/* Settings stack is flex:1 + overflow:hidden with no inner scroll, so on a
short mobile viewport its lower half gets clipped. Let it scroll. */
.cookbook-settings-stack { overflow-y: auto !important; -webkit-overflow-scrolling: touch; }
/* The Servers card is flex:1 + its own overflow-y:auto — a nested scroll that
collapses and crops its list inside the now-scrolling stack. Let both cards
take natural height and scroll the whole stack as one instead. */
.cookbook-settings-stack > .admin-card {
flex: 0 0 auto !important;
overflow: visible !important;
}
.cookbook-dl-repo { font-size: 16px; }
.cookbook-dl-btn { font-size: 14px; }
.cookbook-serve-preset-name { font-size: 14px; }
.cookbook-serve-preset-meta { font-size: 12px; }
.cookbook-saved-name { font-size: 14px; }
.cookbook-saved-host { font-size: 12px; }
.cookbook-slot-btn { font-size: 13px; }
/* The serve-panel "Save" split button reads larger than the surrounding
controls (it's also bold) — knock it down so it matches the row. */
.cookbook-saved-save,
.cookbook-saved-arrow { font-size: 11px; }
.cookbook-output-pre { font-size: 12px; }
.cookbook-dep-tag { font-size: 12px; }
.cookbook-checkbox-label { font-size: 13px; }
.cookbook-gpu-btn { font-size: 13px; }
}
/* Slider — range + text value */
.cookbook-slider-wrap {
display: flex;
align-items: center;
gap: 8px;
}
.cookbook-slider {
flex: 1;
min-width: 0;
height: 6px;
margin: 8px 0;
padding: 0;
-webkit-appearance: none;
appearance: none;
background: linear-gradient(to right, var(--accent, var(--red)) 0%, var(--accent, var(--red)) 50%, color-mix(in srgb, var(--fg) 12%, transparent) 50%);
border-radius: 4px;
outline: none;
cursor: pointer;
transition: opacity 0.1s;
}
.cookbook-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--fg);
border: 2px solid var(--panel);
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
}
.cookbook-slider:hover::-webkit-slider-thumb {
transform: scale(1.15);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent, var(--red)) 20%, transparent);
}
.cookbook-slider:active::-webkit-slider-thumb {
transform: scale(1.25);
box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
}
.cookbook-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--fg);
border: 2px solid var(--panel);
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
cursor: pointer;
}
.cookbook-slider::-moz-range-progress {
background: var(--accent, var(--red));
height: 6px;
border-radius: 4px;
}
.cookbook-slider::-webkit-slider-runnable-track {
height: 6px;
border-radius: 4px;
}
.cookbook-slider::-moz-range-track {
height: 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 12%, transparent);
}
.cookbook-slider-value {
width: 64px !important;
flex-shrink: 0;
text-align: center;
font-size: 11px;
}
/* Toggle switches — match admin-switch */
.cookbook-checkbox-row {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 6px 14px;
padding: 4px 0;
}
.cookbook-checkbox-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: color-mix(in srgb, var(--fg) 60%, transparent);
cursor: pointer;
user-select: none;
}
.cookbook-checkbox-label:hover { color: var(--fg); }
.cookbook-checkbox {
appearance: none;
-webkit-appearance: none;
width: 30px;
height: 16px;
background: color-mix(in srgb, var(--fg) 15%, transparent);
border-radius: 8px;
position: relative;
cursor: pointer;
transition: background 0.08s;
flex-shrink: 0;
}
.cookbook-checkbox::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--panel);
box-shadow: 0 1px 2px rgba(0,0,0,0.25);
transition: transform 0.08s;
}
.cookbook-checkbox:checked {
background: var(--red);
}
.cookbook-checkbox:checked::after {
transform: translateX(14px);
}
/* GPU toggle grid */
.cookbook-gpu-row {
display: flex;
align-items: center;
gap: 4px;
margin-top: 8px;
}
.cookbook-gpu-label {
font-size: 11px;
color: color-mix(in srgb, var(--fg) 60%, transparent);
margin-right: 4px;
white-space: nowrap;
}
.cookbook-gpu-btn {
width: 26px;
height: 26px;
padding: 0;
font-size: 11px;
font-family: inherit;
font-weight: 600;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
transition: all 0.15s;
}
.cookbook-gpu-btn:hover {
color: var(--fg);
border-color: var(--red);
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.cookbook-gpu-btn.active {
background: var(--red);
color: var(--panel);
border-color: var(--red);
}
/* Instance / Save buttons */
.cookbook-add-instance-btn,
.cookbook-save-btn {
background: transparent;
border: 1px dashed color-mix(in srgb, var(--accent) 50%, transparent);
color: color-mix(in srgb, var(--fg) 70%, transparent);
font-size: 11px;
}
.cookbook-add-instance-btn:hover,
.cookbook-save-btn:hover {
border-color: var(--accent);
color: var(--fg);
}
.cookbook-remove-instance-btn {
background: transparent;
border: none;
color: color-mix(in srgb, var(--fg) 40%, transparent);
font-size: 12px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.cookbook-remove-instance-btn:hover { color: var(--red); }
/* Preset chips */
/* Saved card styling */
.cookbook-saved-card .cookbook-card-desc {
font-size: 11px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
}
.cookbook-delete-preset-btn {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 12px;
}
.cookbook-delete-preset-btn:hover {
color: var(--red);
}
/* Sliders span full row for alignment */
.cookbook-field-slider {
grid-column: 1 / -1;
}
/* Kill button */
.cookbook-kill-btn {
border-color: var(--color-error);
color: var(--color-error);
background: transparent;
}
.cookbook-kill-btn:hover {
background: var(--color-error);
color: var(--panel);
border-color: var(--color-error);
}
/* Error diagnosis banner */
.cookbook-diagnosis {
margin-top: 6px;
padding: 10px 12px;
background: color-mix(in srgb, var(--color-error) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent);
border-radius: 6px;
}
.cookbook-diag-message {
font-size: 12px;
font-weight: 600;
color: var(--color-error);
margin-bottom: 8px;
}
.cookbook-diag-fixes {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.cookbook-diag-btn {
font-size: 11px;
padding: 4px 10px;
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--color-error) 40%, transparent);
color: var(--fg);
}
.cookbook-diag-btn:hover {
border-color: var(--color-error);
background: color-mix(in srgb, var(--color-error) 12%, transparent);
}
/* ── What Fits? (hardware model fitting tab in cookbook) ── */
.cookbook-group.hidden { display: none !important; }
/* Section titles */
.cookbook-section-title {
font-size: 12px;
font-weight: 600;
color: var(--fg);
opacity: 0.7;
margin: 10px 0 4px;
}
.cookbook-section-title:first-child { margin-top: 0; }
/* Download input */
.cookbook-dl-input {
display: flex;
gap: 6px;
margin-bottom: 4px;
align-items: stretch;
}
.cookbook-dl-repo {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-family: 'Fira Code', monospace;
font-size: 12px;
padding: 5px 8px;
}
.cookbook-dl-repo:focus {
border-color: var(--accent, var(--red));
outline: none;
}
.cookbook-dl-repo::placeholder {
color: var(--fg-muted);
opacity: 0.5;
font-family: inherit;
}
.cookbook-dl-btn {
background: var(--accent, var(--red));
color: #fff;
border: none;
border-radius: 4px;
padding: 0 14px;
height: 28px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
margin-top: -4px;
}
.cookbook-dl-btn:hover {
opacity: 0.9;
}
/* HF link in search panel */
.hwfit-panel-hf-link {
font-size: 10px;
color: var(--red);
text-decoration: none;
margin-left: auto;
padding: 2px 6px;
border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
border-radius: 3px;
transition: all 0.15s;
}
.hwfit-panel-hf-link:hover {
color: #fff;
background: var(--red);
border-color: var(--red);
}
/* Add Server collapsible */
.cookbook-server-details {
margin: 4px 0 6px;
}
.cookbook-server-toggle {
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
user-select: none;
list-style: none;
padding: 4px 6px;
border-radius: 4px;
font-size: 11px;
color: var(--fg-muted);
transition: background 0.1s, color 0.1s;
}
.cookbook-server-toggle::-webkit-details-marker { display: none; }
.cookbook-server-toggle:hover { color: var(--fg); background: color-mix(in srgb, var(--fg) 5%, transparent); }
.cookbook-server-toggle svg {
opacity: 0.5;
transition: transform 0.2s;
}
.cookbook-server-details[open] .cookbook-server-toggle svg {
transform: rotate(45deg);
}
.cookbook-settings-content {
padding: 6px 0;
}
.cookbook-settings-row {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 8px;
}
.cookbook-settings-label {
font-size: 10px;
color: var(--fg-muted);
}
.cookbook-settings-hint {
opacity: 0.5;
font-weight: 400;
}
.cookbook-settings-input {
width: 100%;
font-size: 11px;
padding: 4px 6px;
}
.cookbook-settings-subtitle {
font-size: 10px;
font-weight: 600;
color: var(--fg-muted);
text-transform: uppercase;
letter-spacing: 0.3px;
margin: 8px 0 4px;
display: flex;
align-items: center;
gap: 6px;
}
.cookbook-server-add {
background: none;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--fg-muted);
font-size: 13px;
width: 16px;
height: 16px;
position: relative;
top: -3px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 0;
padding-bottom: 3px;
line-height: 16px;
text-align: center;
transition: color 0.1s, border-color 0.1s;
}
.cookbook-server-add:hover {
color: var(--fg);
border-color: var(--fg);
}
.cookbook-server-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.cookbook-srv-status {
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 50%;
background: #777;
transition: background 0.2s, box-shadow 0.2s, transform 0.15s;
cursor: pointer;
}
.cookbook-srv-status:hover { transform: scale(1.3); }
.cookbook-srv-status.testing {
background: var(--color-warning, #f0ad4e);
animation: cookbook-srv-pulse 0.9s ease-in-out infinite;
}
.cookbook-srv-status.ok {
background: var(--color-success, #50fa7b);
/* Soft outer ring + breathing halo so active servers glow like LEDs. */
box-shadow:
0 0 0 2px color-mix(in srgb, var(--color-success, #50fa7b) 25%, transparent),
0 0 10px 2px color-mix(in srgb, var(--color-success, #50fa7b) 55%, transparent);
animation: cookbook-srv-glow-ok 2.4s ease-in-out infinite;
}
.cookbook-srv-status.fail {
background: var(--red, #e06c75);
box-shadow:
0 0 0 2px color-mix(in srgb, var(--red, #e06c75) 25%, transparent),
0 0 10px 2px color-mix(in srgb, var(--red, #e06c75) 55%, transparent);
}
@keyframes cookbook-srv-pulse {
0%, 100% { opacity: 0.45; }
50% { opacity: 1; }
}
@keyframes cookbook-srv-glow-ok {
0%, 100% {
box-shadow:
0 0 0 2px color-mix(in srgb, var(--color-success, #50fa7b) 22%, transparent),
0 0 8px 1px color-mix(in srgb, var(--color-success, #50fa7b) 45%, transparent);
}
50% {
box-shadow:
0 0 0 3px color-mix(in srgb, var(--color-success, #50fa7b) 38%, transparent),
0 0 14px 3px color-mix(in srgb, var(--color-success, #50fa7b) 75%, transparent);
}
}
.cookbook-server-row input.hwfit-sf,
.cookbook-server-row select.hwfit-sf {
font-size: 11px;
padding: 0 6px;
margin: 0;
height: 24px;
min-height: 24px;
max-height: 24px;
line-height: 24px;
box-sizing: border-box;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font-family: inherit;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.cookbook-server-row select.hwfit-sf {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23999'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 6px center;
padding-right: 18px;
}
/* Each server is its own card block (mirrors the Running tab's task cards):
full border + accent left-rail + rounded corners + subtle fill, spaced. */
.cookbook-server-entry {
margin-bottom: 8px;
padding: 8px 10px;
border: 1px solid var(--border);
border-left: 3px solid color-mix(in srgb, var(--fg) 30%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
box-sizing: border-box;
}
.cookbook-server-entry:last-child { margin-bottom: 0; }
/* Keep the row inside the card on narrow screens — the fixed-width host/path
inputs would otherwise overflow the card's background to the right. */
@media (max-width: 768px) {
.cookbook-server-row .cookbook-srv-host,
.cookbook-server-row .cookbook-srv-path { flex: 1 1 100% !important; width: auto !important; min-width: 0 !important; }
}
.cookbook-server-row .cookbook-srv-name { width: 60px; flex-shrink: 0; flex-grow: 0; }
.cookbook-server-row .cookbook-srv-host { flex: 1; min-width: 100px; }
.cookbook-server-row .cookbook-srv-host[readonly] { opacity: 0.4; cursor: default; }
.cookbook-server-row .cookbook-srv-port { width: 40px; flex-shrink: 0; flex-grow: 0; }
.cookbook-server-row .cookbook-srv-env { width: 65px; flex-shrink: 0; flex-grow: 0; }
.cookbook-server-row .cookbook-srv-path { flex: 1; min-width: 80px; }
/* Normalize every control on a server row to the same 24-px height — with
many servers the small per-control height drift was very visible. */
.cookbook-server-row > * {
height: 24px;
box-sizing: border-box;
flex-shrink: 0;
}
.cookbook-server-row > input.hwfit-sf,
.cookbook-server-row > select.hwfit-sf,
.cookbook-server-row > button,
.cookbook-server-row .cookbook-srv-actions > button {
height: 24px;
line-height: 22px;
padding: 0 8px;
font-size: 11px;
box-sizing: border-box;
}
.cookbook-server-row .cookbook-srv-actions > .close-btn { width: 24px; padding: 0; line-height: 22px; }
.cookbook-server-row .cookbook-dep-tag { display: inline-flex; align-items: center; height: 24px; padding: 0 6px; line-height: 1; }
.cookbook-server-row .cookbook-srv-status { width: 8px; height: 8px; align-self: center; }
.cookbook-server-rm {
width: 22px;
height: 22px;
font-size: 10px;
opacity: 0.55;
border: 1px solid var(--border);
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: opacity 0.12s, background 0.12s, color 0.12s, border-color 0.12s;
}
.cookbook-server-rm:hover {
opacity: 1;
background: var(--red);
color: #fff;
border-color: var(--red);
}
/* Labelled "Delete server" variant (lives in the Model Directory row) — override
the 22x22 icon box with a normal text button. */
.cookbook-server-rm-btn {
width: auto;
height: auto;
padding: 2px 8px;
font-size: 10px;
font-family: inherit;
background: none;
color: var(--red);
border-color: color-mix(in srgb, var(--red) 40%, var(--border));
white-space: nowrap;
position: relative;
top: -3px;
}
/* The "+" glyph in the Servers-header "+ Add" button, nudged up 1px (scoped so
the shared calendar +New pill is unaffected). */
#cookbook-server-add .cal-add-plus { position: relative; top: -1px; }
/* Save button on a new server entry — same shape as Delete, accent-colored;
turns green once saved. */
.cookbook-server-save-btn {
width: auto;
padding: 2px 8px;
font-size: 10px;
font-family: inherit;
cursor: pointer;
background: none;
color: var(--accent, var(--red));
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
border-radius: 4px;
white-space: nowrap;
}
.cookbook-server-save-btn:hover { background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent); }
/* Cancel (discard a new server) — same shape/size as Save, muted. */
.cookbook-server-cancel-btn {
width: auto;
padding: 2px 8px;
font-size: 10px;
font-family: inherit;
cursor: pointer;
background: none;
color: var(--fg-muted, var(--fg));
border: 1px solid var(--border);
border-radius: 4px;
white-space: nowrap;
}
.cookbook-server-cancel-btn:hover { color: var(--fg); border-color: var(--fg); }
.cookbook-server-save-btn.saved {
color: var(--green, #50fa7b);
border-color: color-mix(in srgb, var(--green, #50fa7b) 45%, var(--border));
cursor: default;
}
.cookbook-path-row {
display: flex;
gap: 4px;
margin-bottom: 3px;
align-items: center;
}
.cookbook-path-input {
flex: 1;
}
.cookbook-path-add {
background: none;
border: 1px solid var(--border);
border-radius: 3px;
color: var(--fg-muted);
font-size: 12px;
width: 18px;
height: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
transition: color 0.1s, border-color 0.1s;
}
.cookbook-path-add:hover {
color: var(--fg);
border-color: var(--fg);
}
/* Server selector in search toolbar */
.hwfit-server-select {
min-width: 70px;
}
/* GPU toggle buttons */
.hwfit-gpu-toggles {
display: flex;
gap: 2px;
align-items: center;
position: relative;
top: -3px;
}
.hwfit-gpu-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg-muted);
font-size: 10px;
padding: 0 5px;
cursor: pointer;
min-width: 20px;
height: 28px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.1s, color 0.1s, border-color 0.1s;
font-family: inherit;
}
.hwfit-gpu-btn:hover {
color: var(--fg);
border-color: var(--fg);
}
.hwfit-gpu-btn.active {
background: var(--accent, var(--red));
color: #fff;
border-color: var(--accent, var(--red));
}
/* Pool selector for heterogeneous GPU boxes — sits left of the RAM/GPU buttons */
.hwfit-gpu-group {
background: var(--bg-elev, var(--bg));
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-size: 10px;
height: 28px;
padding: 0 4px;
cursor: pointer;
box-sizing: border-box;
font-family: inherit;
max-width: 190px;
}
.hwfit-gpu-group:hover { border-color: var(--fg); }
/* Brief highlight on the serve command box when a saved config is loaded, so
the click clearly registers (loading is otherwise silent). */
.cookbook-cmd-flash {
animation: cookbookCmdFlash 0.6s ease;
}
@keyframes cookbookCmdFlash {
0% { box-shadow: 0 0 0 0 var(--accent, var(--red)); border-color: var(--accent, var(--red)); }
30% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 40%, transparent); border-color: var(--accent, var(--red)); }
100% { box-shadow: 0 0 0 0 transparent; }
}
/* "Confirmed working" tick on a saved serve config (auto-saved once its endpoint registered) */
.cookbook-saved-confirmed {
flex-shrink: 0;
display: inline-flex;
align-items: center;
line-height: 0;
}
.hwfit-container { display: flex; flex-direction: column; gap: 8px; }
.hwfit-toolbar {
display: flex; gap: 4px; align-items: center; flex-wrap: wrap;
}
.hwfit-toolbar select,
.hwfit-toolbar input {
height: 28px;
padding: 0 6px;
font-size: 11px;
font-family: inherit;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
box-sizing: border-box;
}
.hwfit-toolbar select:focus,
.hwfit-toolbar input:focus { outline: none; border-color: var(--red); }
.hwfit-toolbar .hwfit-server-select { min-width: 70px; flex-shrink: 0; }
.hwfit-toolbar .hwfit-usecase { min-width: 70px; flex-shrink: 0; }
.hwfit-toolbar .hwfit-quant { min-width: 50px; flex-shrink: 0; }
.hwfit-toolbar .hwfit-search { flex: 1; min-width: 80px; }
.hwfit-server-toggle { flex-shrink: 0; font-size: 10px !important; padding: 3px 8px !important; white-space: nowrap; }
.hwfit-toolbar .hwfit-host { width: 110px; flex-shrink: 0; }
.hwfit-env-row { gap: 6px; flex-wrap: wrap; }
.hwfit-env-row .hwfit-envtype { width: auto; min-width: 70px; flex-shrink: 0; }
.hwfit-env-row .hwfit-envpath { flex: 1; min-width: 100px; }
.hwfit-env-row .hwfit-gpus { width: 90px; flex-shrink: 0; }
.hwfit-hw {
display: flex; flex-wrap: wrap; column-gap: 4px; row-gap: 7px; padding: 4px 0;
}
.hwfit-hw-chip {
font-size: 10px; padding: 0 8px; border-radius: 6px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg); opacity: 0.7; white-space: nowrap;
border: 0;
font-family: inherit;
line-height: 1;
height: 17px;
box-sizing: border-box;
display: inline-flex;
align-items: center;
gap: 3px;
}
.hwfit-hw-chip button,
.hwfit-hw-chip-dismiss,
.hwfit-hw-chip-manual,
.hwfit-hw-chip-toggle,
.hwfit-hw-chip-x {
appearance: none;
-webkit-appearance: none;
background: none;
border: 0;
color: inherit;
font: inherit;
padding: 0;
/* Inherit the chip's line-height so a slightly larger × glyph
doesn't push past the 17px chip height (which was clipping the
text vertically in the multi-button layout). */
line-height: inherit;
}
.hwfit-chip-x {
display: inline-block;
transform: translateY(-1px);
}
.hwfit-hw-chip-dismiss { cursor: pointer; }
.hwfit-hw-chip-dismiss:hover { opacity: 1; }
/* Two-part chip: text body toggles dim on click, × removes the chip.
Visual minimal — just a slightly bigger × so it's easier to hit. */
.hwfit-hw-chip-toggle {
cursor: pointer;
/* `!important` because the earlier `.hwfit-hw-chip button { font:
inherit }` reset (same specificity, defined later in source)
was winning over the plain rule and leaving the manual chip's
button at the browser-default ~13px. Forcing it locally so the
manual chip's text matches every other chip's text. */
font-size: 10px !important;
line-height: 1 !important;
transform: translateY(-3px);
}
.hwfit-hw-chip-x {
cursor: pointer;
/* `!important` for the same cascade reason as the toggle button —
the `.hwfit-hw-chip button { font: inherit }` reset was leaving
the regular chips' × inheriting the chip's 10px while the
manual chip's × landed on the browser default (~13px), making
manual look bigger. Lock every × at 13px. */
font-size: 13px !important;
line-height: 1 !important;
transform: translateY(-5px);
}
.hwfit-hw-chip-x:hover { opacity: 1; }
.hwfit-hw-chip-off {
/* Ghosted: drop the pill background so it reads as "off" rather than
"selected but faded". Lower text opacity matches the muted feel. */
background: transparent !important;
opacity: 0.4;
}
.hwfit-hw-chip-manual {
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
color: var(--accent, var(--red));
opacity: 1;
cursor: pointer;
font-family: inherit;
margin-inline: 3px;
padding-left: 7px;
padding-right: 7px;
}
.hwfit-hw-chip-manual:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
}
.hwfit-hw-manual-btn {
min-width: 42px;
}
.hwfit-manual-panel {
display: flex;
gap: 4px;
align-items: center;
flex-wrap: wrap;
width: 100%;
margin-top: 2px;
}
.hwfit-manual-panel.hidden { display: none; }
.hwfit-manual-panel label {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 9px;
color: var(--fg-muted);
}
.hwfit-manual-panel select,
.hwfit-manual-panel input,
.hwfit-manual-panel button {
height: 24px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 4px;
font: inherit;
font-size: 10px;
padding: 0 6px;
box-sizing: border-box;
}
.hwfit-manual-panel input { width: 72px; }
.hwfit-manual-panel button { cursor: pointer; }
.hwfit-manual-panel .hwfit-hw-manual-save,
.hwfit-manual-panel .hwfit-hw-manual-clear {
/* -3 (was -2) — 1px more up to optically align with the labeled
inputs to their left. */
transform: translateY(-3px);
}
.hwfit-manual-panel button:hover { border-color: var(--fg); }
/* GPU driver error (e.g. NVML version mismatch) — stands out from the muted
chips and hints there's a real problem, with the full message on hover. */
.hwfit-hw-chip-error {
background: color-mix(in srgb, var(--red) 16%, transparent);
color: var(--red); opacity: 1; cursor: help;
border: 1px solid color-mix(in srgb, var(--red) 40%, transparent);
}
.hwfit-list {
max-height: 52vh; overflow-y: auto; display: flex; flex-direction: column; gap: 2px;
}
.hwfit-loading {
display: flex; align-items: center; justify-content: center;
color: var(--fg-muted); padding: 16px 0; font-size: 12px;
}
.hwfit-row {
display: flex; align-items: center; gap: 6px; padding: 5px 8px;
border-radius: 6px; cursor: pointer; font-size: 11px;
transition: background 0.1s;
}
.hwfit-row:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }
/* Already-downloaded rows: dim slightly so attention falls on undownloaded
options. The green dot stays bright as the "this is local" cue. */
.hwfit-row:has(.hwfit-dl-dot) { opacity: 0.55; }
.hwfit-row:has(.hwfit-dl-dot):hover { opacity: 0.9; }
.hwfit-row:has(.hwfit-dl-dot) .hwfit-dl-dot { opacity: 1; }
.hwfit-header {
cursor: default; position: sticky; top: 0; z-index: 1;
background: var(--panel); border-bottom: 1px solid var(--border);
padding: 4px 8px; font-weight: 600;
}
.hwfit-header:hover { background: var(--panel); }
.hwfit-header .hwfit-col { font-size: 9px; color: var(--fg-muted); text-transform: uppercase; letter-spacing: 0.3px; }
.hwfit-header .hwfit-name { font-size: 9px; }
.hwfit-sortable { cursor: pointer; user-select: none; }
.hwfit-sortable:hover { color: var(--fg) !important; }
.hwfit-sort-active { color: var(--red) !important; }
.hwfit-col {
flex-shrink: 0; white-space: nowrap; text-align: left;
font-size: 9px; color: var(--fg-muted);
}
.hwfit-fit { width: 52px; font-weight: 700; text-transform: uppercase; }
.hwfit-name {
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
font-weight: 500; font-size: 11px; color: var(--fg);
}
.hwfit-c-params { width: 42px; }
.hwfit-c-quant { width: 52px; }
.hwfit-c-vram { width: 42px; }
.hwfit-c-ctx { width: 32px; }
.hwfit-c-speed { width: 44px; }
.hwfit-c-score { width: 40px; font-weight: 700; font-size: 11px; color: var(--fg); }
.hwfit-c-mode { width: 48px; }
.hwfit-moe {
display: inline-block; padding: 0 4px; border-radius: 4px; margin-left: 4px;
background: color-mix(in srgb, var(--red) 15%, transparent);
color: var(--red); font-weight: 600; font-size: 8px;
}
.hwfit-sort, .hwfit-quant { width: auto; min-width: 70px; flex-shrink: 0; }
.hwfit-row-active { background: color-mix(in srgb, var(--red) 8%, transparent); }
/* ── Inline action panel (expands below a model row) ── */
.hwfit-action-panel {
border: 1px solid var(--border); border-left: 3px solid var(--red);
border-radius: 0 6px 6px 6px; background: var(--panel);
padding: 10px 12px; margin: 2px 0 6px; font-size: 11px;
display: flex; flex-direction: column; gap: 8px;
animation: hwfit-panel-in 0.15s ease-out;
}
@keyframes hwfit-panel-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.hwfit-panel-header {
display: flex; align-items: center; gap: 8px; min-width: 0;
}
.hwfit-panel-model {
font-size: 11px; font-weight: 600; color: var(--fg);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
}
.hwfit-panel-badge {
font-size: 9px; padding: 2px 8px; border-radius: 10px;
background: color-mix(in srgb, var(--red) 12%, transparent);
color: var(--red); font-weight: 600; flex-shrink: 0;
}
.hwfit-panel-fields {
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
}
.hwfit-panel-fields label, .hwfit-adv-grid label {
display: flex; align-items: center; gap: 4px; font-size: 10px; color: var(--fg-muted); white-space: nowrap;
}
.hwfit-panel-fields input, .hwfit-panel-fields select,
.hwfit-adv-grid input, .hwfit-adv-grid select {
background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
color: var(--fg); font-size: 11px; padding: 3px 6px; font-family: inherit;
outline: none; transition: border-color 0.15s;
}
.hwfit-panel-fields input:focus, .hwfit-adv-grid input:focus,
.hwfit-panel-fields select:focus, .hwfit-adv-grid select:focus {
border-color: var(--red);
}
.hwfit-panel-fields input[type="text"] { width: 60px; }
.hwfit-adv-grid input[type="text"] { width: 80px; }
.hwfit-adv-grid {
display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 6px 0;
}
.hwfit-cb { cursor: pointer; }
.hwfit-cb input[type="checkbox"] { margin: 0 2px 0 0; }
.hwfit-panel-advanced, .hwfit-panel-settings {
font-size: 10px;
}
.hwfit-panel-advanced summary, .hwfit-panel-settings summary {
cursor: pointer; color: var(--fg-muted); font-size: 10px;
user-select: none; padding: 2px 0;
}
.hwfit-panel-advanced summary:hover, .hwfit-panel-settings summary:hover { color: var(--fg); }
.hwfit-panel-cmd {
font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
font-size: 10px; padding: 6px 8px; border-radius: 4px;
background: var(--bg); border: 1px solid var(--border);
white-space: pre-wrap; word-break: break-all; max-height: 60px; overflow-y: auto;
color: var(--fg-muted);
}
.hwfit-panel-actions {
display: flex; gap: 4px; flex-wrap: wrap;
}
/* ── Saved presets ── */
.hwfit-preset {
display: flex; align-items: center; gap: 8px; padding: 6px 10px;
border: 1px solid var(--border); border-radius: 6px; font-size: 11px;
transition: border-color 0.15s;
}
.hwfit-preset:hover { border-color: var(--red); }
.hwfit-preset-name { font-weight: 600; color: var(--fg); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hwfit-preset-model { font-size: 9px; color: var(--fg-muted); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hwfit-preset-backend { font-size: 9px; padding: 1px 6px; border-radius: 8px; background: color-mix(in srgb, var(--fg) 8%, transparent); color: var(--fg-muted); }
@media (max-width: 600px) {
.hwfit-c-ctx, .hwfit-c-speed, .hwfit-c-mode { display: none; }
.hwfit-panel-fields { flex-direction: column; align-items: stretch; }
.hwfit-panel-fields input[type="text"] { width: 100%; }
.hwfit-preset-model { display: none; }
}
/* ===== Settings Modal Layout ===== */
.settings-modal-content {
width: min(720px, 92vw);
max-height: 85vh;
padding: 0;
container-type: inline-size;
container-name: settings-modal;
}
.settings-modal-content .modal-header {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.settings-layout {
display: flex;
min-height: 400px;
max-height: calc(85vh - 60px);
}
.settings-modal-content[style*="height"],
#settings-modal.modal-right-docked .settings-modal-content {
overflow: hidden;
}
.settings-modal-content[style*="height"] .settings-layout,
#settings-modal.modal-right-docked .settings-layout {
flex: 1 1 0;
min-height: 0;
max-height: none;
}
.settings-modal-content[style*="height"] .settings-panels,
#settings-modal.modal-right-docked .settings-panels,
.settings-modal-content[style*="height"] .settings-sidebar,
#settings-modal.modal-right-docked .settings-sidebar {
min-height: 0;
}
.settings-sidebar {
width: 160px;
flex-shrink: 0;
border-right: 1px solid var(--border);
padding: 8px;
display: flex;
flex-direction: column;
gap: 2px;
background: color-mix(in srgb, var(--fg) 2%, transparent);
}
.settings-sidebar-divider { height: 1px; background: var(--border); margin: 8px 12px; }
.settings-sidebar-label {
font-size: 9px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.35;
padding: 4px 12px 2px;
}
.settings-nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: none;
background: none;
color: var(--color-muted);
font-family: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
border-radius: 6px;
transition: all 0.1s;
text-align: left;
white-space: nowrap;
}
.settings-nav-item:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg);
}
.settings-nav-item.active {
background: color-mix(in srgb, var(--red) 12%, transparent);
color: var(--red);
}
.settings-nav-item svg {
flex-shrink: 0;
opacity: 0.7;
}
.settings-nav-item.active svg {
opacity: 1;
}
.settings-sidebar-divider { height: 1px; background: var(--border); margin: 8px 12px; }
/* Keyboard shortcuts */
.shortcut-category {
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
opacity: 0.4; padding: 8px 0 4px; margin-top: 4px;
}
.shortcut-category:first-child { margin-top: 0; padding-top: 0; }
.shortcut-row {
display: flex; align-items: center; justify-content: space-between;
padding: 5px 0; border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
}
.shortcut-row:last-child { border-bottom: none; }
.shortcut-row.shortcut-conflict { background: color-mix(in srgb, var(--warn) 6%, transparent); border-radius: 4px; padding: 5px 6px; }
.shortcut-label { font-size: 12px; display: flex; align-items: center; gap: 6px; }
.shortcut-icon { display: inline-flex; opacity: 0.5; flex-shrink: 0; }
.shortcut-warn {
display: inline-flex; align-items: center; justify-content: center;
width: 14px; height: 14px; border-radius: 50%; background: var(--warn); color: #fff;
font-size: 9px; font-weight: 700; margin-left: 4px;
}
.shortcut-controls { display: flex; align-items: center; gap: 4px; }
/* Inline hint shown while rebinding a shortcut ("press a key" → "↵ Enter
to save"). Subtle so it doesn't compete with the key caps. */
.shortcut-hint {
font-size: 10px;
opacity: 0.6;
color: var(--accent, var(--red));
white-space: nowrap;
margin-right: 2px;
}
.shortcut-hint[hidden] { display: none; }
.shortcut-key {
font-family: inherit; font-size: 0; padding: 2px 4px;
background: transparent; border: none; border-radius: 4px;
color: var(--fg); cursor: pointer; display: flex; align-items: center; gap: 2px;
transition: all 0.15s;
}
.shortcut-key:hover { background: color-mix(in srgb, var(--accent, #cc6a3a) 8%, transparent); }
/* Unbound shortcut — show a dashed "Set" placeholder instead of keycaps. */
.shortcut-key-unset { font-size: 10px; }
.shortcut-unset {
font-size: 10px;
padding: 2px 8px;
border: 1px dashed var(--border);
border-radius: 4px;
color: color-mix(in srgb, var(--fg) 45%, transparent);
}
.shortcut-key-unset:hover .shortcut-unset { border-color: var(--accent, var(--red)); color: var(--fg); }
.shortcut-key kbd {
display: inline-block; font-family: inherit; font-size: 10px;
padding: 2px 6px; min-width: 20px; text-align: center;
/* Highlight kbd chips in the theme accent so they stand out from
normal text and clearly mark "this is a key you press". */
background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg));
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, var(--border));
color: var(--accent, var(--red));
border-radius: 3px;
box-shadow: 0 1px 0 color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
line-height: 1.4;
font-weight: 600;
}
.shortcut-key.listening {
background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, transparent);
animation: shortcut-pulse 1s infinite;
}
.shortcut-key.listening kbd { border-color: var(--accent, #cc6a3a); }
.shortcut-action-btn {
width: 24px; height: 24px; border-radius: 4px; border: 1px solid var(--border);
background: transparent; color: var(--fg); cursor: pointer; font-size: 13px;
display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
.shortcut-action-btn:hover { border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 10%, var(--bg)); }
.shortcut-action-btn.is-reset { opacity: 0.5; }
.shortcut-action-btn.is-reset:hover { opacity: 1; }
@keyframes shortcut-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.settings-panels {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
min-width: 0;
}
.settings-appearance-panel {
flex-direction: column;
}
.settings-appearance-panel:not(.hidden) {
display: flex;
}
.settings-appearance-panel > .admin-card:nth-of-type(1) { order: 3; }
.settings-appearance-panel > .admin-card:nth-of-type(2) { order: 1; }
.settings-appearance-panel > .admin-card:nth-of-type(3) { order: 2; }
.settings-appearance-panel > .admin-card:nth-of-type(n+4) { order: 4; }
/* Mobile: stack tabs on top */
@media (max-width: 600px) {
.settings-layout {
flex-direction: column;
}
.settings-sidebar {
width: auto;
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
padding: 6px;
}
.settings-nav-item {
padding: 6px 10px;
font-size: 11px;
}
}
/* Snapped/narrow Settings window: move the tab rail to the top. Viewport media
alone misses desktop right-half snapping, where the modal is narrow but the
browser window is not. */
@container settings-modal (max-width: 620px) {
.settings-layout {
flex-direction: column;
}
.settings-sidebar {
width: auto;
max-height: none;
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
overflow-y: hidden;
padding: 6px;
scrollbar-width: none;
}
.settings-sidebar::-webkit-scrollbar {
display: none;
}
.settings-sidebar-divider,
.settings-sidebar-label {
display: none;
}
.settings-nav-item {
flex: 0 0 auto;
padding: 6px 10px;
font-size: 11px;
}
.settings-panels {
padding: 12px 14px;
}
}
/* ── Entrance Animations ── */
/* Welcome name — left-to-right wipe with a touch of horizontal stretch.
Fires on initial render via the .welcome-name rule; restarts on Nobody⇄
Odysseus toggle via JS reflow trick. */
.welcome-name {
transform-origin: left center;
animation: welcome-name-reveal 0.55s cubic-bezier(0.34, 1.32, 0.55, 1) both;
}
/* Hold the welcome-screen entrance animations until the app has finished its
initial load (fonts loaded + layout settled). Running them mid-load made the
splash jump/flicker as the page reflowed ("haywire"). Until body.welcome-ready
is set by JS, the splash sits in its final, static state (no flash, since the
non-animated state IS the resting state); adding the class then plays the
entrance once, cleanly. */
body:not(.welcome-ready) #welcome-screen,
body:not(.welcome-ready) .welcome-name {
animation: none !important;
}
/* Hold the splash invisible (the entrance's start state) until ready, so when
the animation is released it fades/wipes in smoothly instead of flashing
from fully-visible to the animation's hidden first frame. */
body:not(.welcome-ready) #welcome-screen {
opacity: 0;
}
@keyframes welcome-name-reveal {
from {
clip-path: inset(0 100% 0 0);
transform: scaleX(0.96);
opacity: 0.5;
}
to {
clip-path: inset(0 0 0 0);
transform: scaleX(1);
opacity: 1;
}
}
@keyframes welcome-enter {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
@keyframes msg-enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes picker-roll-up {
from {
opacity: 0;
transform: scaleY(0.4) translateY(8px);
}
to {
opacity: 1;
transform: scaleY(1) translateY(0);
}
}
@keyframes picker-roll-down {
from {
opacity: 1;
transform: scaleY(1) translateY(0);
}
to {
opacity: 0;
transform: scaleY(0.4) translateY(8px);
}
}
@keyframes modal-enter {
from {
opacity: 0;
transform: scale(0.95) translateY(8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes modal-exit {
from {
opacity: 1;
transform: scale(1) translateY(0);
}
to {
opacity: 0;
transform: scale(0.97) translateY(6px);
}
}
.modal-content.modal-closing {
animation: modal-exit 0.18s ease-in both;
}
@keyframes cookbook-modal-enter {
0% {
opacity: 0;
transform: translateY(12px) scale(0.94);
filter: saturate(0.85);
}
65% {
opacity: 1;
transform: translateY(-2px) scale(1.012);
filter: saturate(1.05);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
filter: none;
}
}
#cookbook-modal .modal-content.cookbook-modal-entering {
animation: cookbook-modal-enter 0.28s cubic-bezier(0.22, 1.35, 0.36, 1) both;
}
/* Mobile: real bottom-sheet slide-up — the existing 12px translate is
too subtle on a phone, the modal effectively just appeared. */
@media (max-width: 768px) {
#cookbook-modal .modal-content.cookbook-modal-entering {
animation: cookbook-modal-enter-mobile 0.32s cubic-bezier(0.22, 1, 0.36, 1) both;
}
}
@keyframes cookbook-modal-enter-mobile {
0% { opacity: 0; transform: translateY(100%); }
100% { opacity: 1; transform: translateY(0); }
}
#cookbook-modal .modal-content:not(.cookbook-modal-entering):not(.modal-closing) {
animation: none;
}
#cookbook-modal .modal-content.modal-closing {
animation: modal-exit 0.18s cubic-bezier(0.4, 0, 1, 1) both;
}
/* Per-token streaming fade — new words materialize */
.token-new {
animation: token-fade 0.4s ease-out both;
}
@keyframes token-fade {
from { opacity: 0; }
to { opacity: 1; }
}
/* Disable entrance animations when loading chat history (bulk render) */
.chat-history.no-animate .msg {
animation: none;
}
/* Prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.msg,
.modal-content,
#welcome-screen,
.toast,
.token-new {
animation: none !important;
transition: none !important;
}
}
/* ── Tasks ── */
.tasks-modal-content { max-width: 600px; width: min(600px, 92vw); background: var(--bg); font-size: 12px; }
/* Tasks tabs reuse the .memory-tab look. The Brain window's tab bar is
full-bleed (its underline spans the whole modal width). The Tasks bar is a
direct child of .modal-content (padding:10px), so cancel that padding with
negative side margins to span edge-to-edge, then re-inset the tabs by 10px
so they line up with the rest of the modal content — matching the Brain bar. */
.tasks-modal-content .tasks-tabs {
margin: -2px -10px 8px;
padding: 0 10px;
}
/* Activity log — compact by default. Click the row to expand body+actions. */
.task-log-row {
--cat-hue: 220;
border: 1px solid var(--border);
border-radius: 8px;
padding: 4px 10px;
margin-bottom: 3px;
background: color-mix(in srgb, var(--fg) 2%, transparent);
position: relative;
cursor: pointer;
transition: padding .12s ease;
}
.task-log-row:hover {
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.task-log-row.expanded { padding: 8px 10px 6px; }
.task-log-row-head {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
margin-bottom: 0;
}
.task-log-row.expanded .task-log-row-head { margin-bottom: 4px; }
/* Collapsed: body + footer hidden. Expanded: visible. Running/skipped rows
don't expand at all (no body to show). */
.task-log-row:not(.expanded):not(.is-running):not(.is-skipped) .task-log-row-body,
.task-log-row:not(.expanded):not(.is-running):not(.is-skipped) .task-log-row-actions,
.task-log-row:not(.expanded):not(.is-running):not(.is-skipped) .task-log-prompt {
display: none;
}
.task-log-name {
font-weight: 600;
/* Pull saturation from the per-row hue but mix with the foreground so the
title still reads in dark mode. Lightness stays adaptive. */
color: hsl(var(--cat-hue) 60% 60%);
}
.task-log-repeat {
font-size: 10px;
font-weight: 500;
line-height: 1;
color: color-mix(in srgb, var(--fg) 62%, transparent);
border-radius: 999px;
padding: 1px 0;
white-space: nowrap;
}
@media (prefers-color-scheme: light) {
.task-log-name { color: hsl(var(--cat-hue) 65% 38%); }
}
/* Per-account prefix in fan-out results — e.g. "[Default] No recent emails"
becomes a compact accent chip + plain message. Makes multi-account activity
rows readable instead of a bracket soup. */
/* Running / queued status line in the Activity tab — whirlpool + label. */
.task-log-running {
display: inline-flex;
align-items: center;
gap: 0;
font-size: 11px;
opacity: 0.75;
}
.task-log-running-label { font-style: normal; }
/* New right-side placement: "Running " sits where the timestamp
normally would, on the head row's right edge. */
.task-log-running-inline {
display: inline-flex;
align-items: center;
gap: 0;
font-size: 11px;
opacity: 0.75;
}
.task-log-running-inline .task-log-running-label { font-weight: 500; }
.task-log-running-elapsed {
margin-left: 6px;
opacity: 0.6;
font-variant-numeric: tabular-nums;
}
/* Slim single-line row for skipped (noop) runs — body/actions stripped, font
shrunk, opacity dropped. Distinguishes "task ran but had nothing to do"
from a "real" entry without flooding the feed visually. */
.task-log-row.is-skipped {
padding: 4px 8px;
opacity: 0.45;
font-size: 11px;
background: transparent;
}
.task-log-row.is-skipped .task-log-row-head { padding: 0; }
.task-log-row.is-skipped .task-log-name { font-weight: 500; }
.task-log-row.is-skipped .task-log-skipped-reason {
margin-left: 6px;
font-style: italic;
opacity: 0.85;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.task-log-row.is-skipped:hover { opacity: 0.7; }
.task-log-account-tag {
display: inline-block;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 1px 7px;
margin-right: 4px;
border-radius: 10px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
vertical-align: 1px;
}
.task-log-time {
opacity: 0.5;
font-variant-numeric: tabular-nums;
}
.task-log-status {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
background: color-mix(in srgb, var(--fg) 30%, transparent);
}
.task-log-status-ok { background: #4ade80; box-shadow: 0 0 6px #4ade80, 0 0 3px #4ade80; }
.task-log-status-error { background: var(--red, #f87171); box-shadow: 0 0 6px var(--red, #f87171), 0 0 3px var(--red, #f87171); }
.task-log-status-info { background: color-mix(in srgb, var(--fg) 25%, transparent); }
.task-log-status-queued { background: #fbbf24; box-shadow: 0 0 0 2px color-mix(in srgb, #fbbf24 30%, transparent); }
.task-log-status-running { background: #60a5fa; animation: task-log-pulse 1.4s ease-in-out infinite; }
.task-log-status-skipped { background: color-mix(in srgb, var(--fg) 20%, transparent); }
.task-log-status-aborted { background: color-mix(in srgb, var(--fg) 30%, transparent); border: 1px dashed color-mix(in srgb, var(--fg) 50%, transparent); }
@keyframes task-log-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, #60a5fa 60%, transparent); }
50% { box-shadow: 0 0 0 4px color-mix(in srgb, #60a5fa 0%, transparent); }
}
.task-log-row-body {
font-size: 12px;
line-height: 1.5;
color: color-mix(in srgb, var(--fg) 85%, transparent);
overflow-wrap: anywhere;
word-break: break-word;
}
.task-log-row-body p { margin: 0 0 6px 0; }
.task-log-row-body p:last-child { margin-bottom: 0; }
.task-log-row-body pre {
margin: 4px 0;
padding: 6px 8px;
background: color-mix(in srgb, var(--fg) 5%, transparent);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
font-size: 11px;
max-height: 180px;
overflow: auto;
}
.task-log-row-body code {
font-size: 11px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
padding: 1px 4px;
border-radius: 3px;
}
.task-log-row-body ul, .task-log-row-body ol {
margin: 4px 0;
padding-left: 20px;
}
.task-log-row-body li { margin: 2px 0; }
/* Markdown headings inside a task result — without these, ## headings
render at browser-default huge sizes and blow out the cramped row. */
.task-log-row-body h1,
.task-log-row-body h2,
.task-log-row-body h3,
.task-log-row-body h4 {
margin: 10px 0 4px;
line-height: 1.3;
font-weight: 650;
color: var(--fg);
}
.task-log-row-body h1 { font-size: 14px; }
.task-log-row-body h2 { font-size: 13px; }
.task-log-row-body h3,
.task-log-row-body h4 { font-size: 12px; opacity: 0.9; }
.task-log-row-body h1:first-child,
.task-log-row-body h2:first-child,
.task-log-row-body h3:first-child { margin-top: 0; }
.task-log-row-body hr {
border: none;
border-top: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
margin: 8px 0;
}
.task-log-row-body blockquote {
margin: 4px 0;
padding: 2px 0 2px 10px;
border-left: 2px solid color-mix(in srgb, var(--fg) 20%, transparent);
opacity: 0.85;
}
.task-log-row-body a { color: var(--accent, #60a5fa); text-decoration: none; }
.task-log-row-body a:hover { text-decoration: underline; }
.task-log-row-body strong { font-weight: 650; color: var(--fg); }
/* Email-summary tables — render compactly instead of default browser ugliness. */
.task-log-row-body table {
border-collapse: collapse;
width: 100%;
margin: 6px 0;
font-size: 11px;
}
.task-log-row-body th,
.task-log-row-body td {
border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
padding: 3px 6px;
text-align: left;
vertical-align: top;
}
.task-log-row-body th {
background: color-mix(in srgb, var(--fg) 5%, transparent);
font-weight: 600;
}
.task-log-row-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 4px;
}
.task-log-open-chat,
.task-log-copy {
display: inline-flex;
align-items: center;
gap: 3px;
background: none;
border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
border-radius: 5px;
color: color-mix(in srgb, var(--fg) 60%, transparent);
font-family: inherit;
font-size: 10px;
padding: 1px 6px;
cursor: pointer;
transition: background .15s, color .15s, border-color .15s;
line-height: 1.4;
}
.task-log-open-chat:hover,
.task-log-copy:hover {
color: var(--fg);
border-color: color-mix(in srgb, var(--fg) 30%, transparent);
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
/* Activity filter chips — toggle-out model: ON by default (solid),
click to toggle OFF (dimmed + strikethrough) to hide that group. */
.tasks-af-chip {
font-size: 11px;
padding: 3px 10px;
border: 1px solid color-mix(in srgb, var(--fg) 16%, transparent);
border-radius: 12px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
color: var(--fg);
cursor: pointer;
font-family: inherit;
transition: opacity .12s, background .12s, border-color .12s;
}
.tasks-af-chip:hover { border-color: color-mix(in srgb, var(--fg) 32%, transparent); }
.tasks-af-chip.off {
opacity: 0.4;
text-decoration: line-through;
background: transparent;
}
.tasks-af-chip.active {
border-color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
opacity: 1;
}
.tasks-af-chip-error { border-color: color-mix(in srgb, var(--red, #f87171) 45%, transparent); }
.tasks-af-chip-error:not(.off) { color: var(--red, #f87171); }
/* "Open in Deep Research" is now a regular clickable chat link (a
`#research-` markdown anchor in the assistant's message), rendered as
an a.chat-link. Nudge it 4px right so it sits slightly inset. */
a.chat-link[href^="#research-"] {
margin-left: 4px;
}
.task-log-row.is-long .task-log-row-body {
max-height: 8.5em;
overflow: hidden;
position: relative;
mask-image: linear-gradient(to bottom, #000 70%, transparent);
-webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
}
.task-log-row.is-long.expanded .task-log-row-body {
max-height: none;
overflow: visible;
mask-image: none;
-webkit-mask-image: none;
}
.task-log-row-toggle {
margin-top: 6px;
background: none;
border: none;
font-family: inherit;
font-size: 11px;
cursor: pointer;
padding: 2px 0;
color: transparent; /* hide raw "Show more" text */
}
.task-log-row-toggle::before {
color: color-mix(in srgb, var(--accent, var(--fg)) 80%, transparent);
opacity: 0.8;
}
.task-log-row:not(.expanded) .task-log-row-toggle::before { content: 'Show more'; }
.task-log-row.expanded .task-log-row-toggle::before { content: 'Show less'; }
.task-log-row-toggle:hover::before { opacity: 1; }
.task-log-prompt {
margin-top: 6px;
font-size: 11px;
}
.task-log-prompt summary {
cursor: pointer;
opacity: 0.5;
user-select: none;
}
.task-log-prompt summary:hover { opacity: 0.8; }
.task-log-prompt pre {
margin: 4px 0 0;
padding: 6px 8px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
font-size: 11px;
opacity: 0.75;
max-height: 200px;
overflow: auto;
}
.task-card {
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 0;
background: color-mix(in srgb, var(--fg) 2%, transparent);
min-height: 47px;
box-sizing: border-box;
align-items: center;
}
/* When expanded, height grows to fit the detail panel. */
.task-card.expanded { align-items: flex-start; }
/* Nudge the card text up (dot/menu keep their own offsets); the leading icon
and built-in tag ride a bit higher to sit on the title's cap line. */
.task-card .memory-item-title,
.task-card .memory-item-meta { position: relative; top: -4px; }
/* Always show the per-card ⋮ kebab on task cards (the default opacity:0 +
hover-reveal is unreliable on touch and easy to miss on desktop).
pointer-events / z-index belt-and-braces against any sibling overlay,
margin reset undoes the global `.modal-body button { margin-top:6px }`
that was punting the hit-target down. */
.task-card .memory-item-actions {
opacity: 0.55;
pointer-events: auto;
position: relative;
z-index: 5;
}
.task-card:hover .memory-item-actions,
.task-card .memory-item-actions:hover { opacity: 1; }
.task-card .memory-item-actions .memory-item-btn {
margin: 0 !important;
pointer-events: auto;
position: relative;
z-index: 5;
width: 28px;
min-width: 28px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Make the SVG inside the kebab transparent to pointer events so the click
always lands on the BUTTON itself (some browsers / nested SVG events lose
the click when hitting the inner glyph). */
.task-card .memory-item-actions .memory-item-btn svg { pointer-events: none; }
.task-card .task-builtin-badge { position: relative; top: -4px; }
/* Per-card select checkbox rides up to the title line. The "All" checkbox is
#tasks-select-all (not .memory-select-cb), so it stays put. */
.task-card .memory-select-cb { position: relative; top: -4px; }
/* Bigger ⋮ dropdown on mobile (its buttons carry inline styles → !important). */
@media (max-width: 768px) {
.task-dropdown { min-width: 160px !important; padding: 6px !important; }
.task-dropdown button { font-size: 13px !important; padding: 10px 12px !important; gap: 10px !important; }
.task-dropdown button svg { width: 15px !important; height: 15px !important; }
}
.task-card-header {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.task-card-name {
font-size: 12px;
font-weight: 600;
}
.task-card-schedule {
font-size: 10.5px;
opacity: 0.5;
margin-left: auto;
}
.task-card-output {
font-size: 10px;
opacity: 0.45;
}
.task-card-prompt {
font-size: 11px;
opacity: 0.55;
margin: 4px 0;
line-height: 1.4;
}
.task-card-meta {
font-size: 10.5px;
opacity: 0.45;
margin-bottom: 6px;
}
.task-card-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.task-btn {
font-family: inherit;
font-size: 11px;
padding: 4px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: none;
color: var(--fg);
cursor: pointer;
opacity: 0.7;
transition: opacity 0.15s, background 0.15s;
}
.task-btn:hover { opacity: 1; }
.task-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.task-btn-primary {
background: var(--red);
color: #fff;
border-color: transparent;
opacity: 1;
}
.task-btn-primary:hover { opacity: 0.85; }
.task-btn-danger { color: var(--red); border-color: color-mix(in srgb, var(--red) 30%, transparent); }
.task-btn-danger:hover { opacity: 1; border-color: var(--red); }
.tasks-clock {
font-size: 10px;
opacity: 0.35;
text-align: center;
padding: 6px 0 8px;
}
.task-preset-grid {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;
}
.task-preset-card {
display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
padding: 12px; border: 1px solid var(--border); border-radius: 8px;
background: var(--bg); cursor: pointer; transition: all 0.15s; text-align: left;
color: var(--fg);
}
.task-preset-card:hover {
border-color: var(--accent, #cc6a3a); background: color-mix(in srgb, var(--accent, #cc6a3a) 8%, var(--bg));
}
.task-preset-icon { font-size: 20px; margin-bottom: 2px; }
.task-preset-label { font-size: 12px; font-weight: 600; }
.task-preset-desc { font-size: 10px; opacity: 0.5; line-height: 1.3; }
.task-form { display: flex; flex-direction: column; gap: 4px; }
.task-form-label {
font-size: 11px;
opacity: 0.55;
font-weight: 500;
display: block;
/* Consistent rhythm: 8px above each label (section gap), 4px below to the
field — same spacing for Name/Prompt/Trigger/Output/Model/Chain. */
margin: 8px 0 4px;
}
.task-form-input {
font-family: inherit;
font-size: 11px;
padding: 6px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
transition: border-color 0.15s;
box-sizing: border-box;
width: 100%;
max-width: 100%;
min-width: 0;
}
/* A 's closed width otherwise grows to its widest option (the long
action descriptions), overflowing the modal — clip it to the field width. */
select.task-form-input { text-overflow: ellipsis; }
.task-form-input:focus {
outline: none;
border-color: var(--red);
}
.task-form-textarea { resize: vertical; min-height: 60px; }
.task-form-actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 8px; }
.task-form-toggle {
display: flex; gap: 4px;
}
.task-toggle-btn {
flex: 1; padding: 4px 8px; font-size: 10px; font-family: inherit;
border: 1px solid var(--border); border-radius: 6px; background: transparent;
color: var(--fg); opacity: 0.4; cursor: pointer; transition: all 0.15s;
}
.task-toggle-btn:hover { opacity: 0.7; border-color: var(--fg); }
.task-toggle-btn.active {
opacity: 1; border-color: var(--red); color: var(--red);
background: color-mix(in srgb, var(--red) 10%, transparent);
}
.task-time-picker, .task-date-picker {
display: flex;
align-items: center;
gap: 4px;
}
.task-time-select, .task-date-select {
width: auto;
min-width: 48px;
font-size: 11px;
}
.task-time-sep { opacity: 0.4; font-size: 11px; }
.task-history-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.task-runs-list { display: flex; flex-direction: column; gap: 6px; overflow-y: auto; }
.task-run-item {
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 10px;
}
.task-run-item-header {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.task-run-time { margin-left: auto; font-size: 10px; opacity: 0.45; }
.task-run-result {
font-size: 11px;
opacity: 0.6;
margin-top: 4px;
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
}
/* ── Settings Form Utilities ── */
.settings-row {
display: flex;
align-items: center;
gap: 8px;
}
.settings-col {
display: flex;
flex-direction: column;
gap: 6px;
}
.settings-label {
font-size: 12px;
min-width: 70px;
}
.settings-select {
flex: 1;
/* Allow the select to shrink below its longest option's width inside a flex
row — without this a long model name (e.g. the Vision picker) overflowed
the card off-screen on mobile. */
min-width: 0;
max-width: 100%;
padding: 5px 8px;
font-family: inherit;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
transition: border-color 0.15s;
appearance: none;
-webkit-appearance: none;
-moz-appearance: textfield;
outline: none;
}
input.settings-select::-webkit-outer-spin-button,
input.settings-select::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.settings-select::placeholder { color: color-mix(in srgb, var(--fg) 35%, transparent); }
.settings-select:focus {
outline: none;
border-color: var(--red);
}
/* Default-chat fallback chain editor. Each row mirrors the primary
endpoint/model selectors, indented under them to read as a chain. */
.settings-fallbacks {
display: flex;
flex-direction: column;
gap: 6px;
}
.settings-fallbacks:not(:empty) { margin-top: 2px; }
.settings-fallback-row {
display: flex;
align-items: center;
gap: 6px;
padding-left: 12px;
border-left: 2px solid color-mix(in srgb, var(--fg) 12%, transparent);
}
.settings-fallback-num {
font-size: 11px;
opacity: 0.4;
min-width: 14px;
text-align: right;
}
.settings-fallback-row .settings-select { flex: 1; min-width: 0; }
.settings-fallback-remove {
flex-shrink: 0;
margin-right: 4px;
width: 22px;
height: 22px;
line-height: 1;
font-size: 15px;
/* Nudge the × glyph 5px left within the button (button size unchanged). */
text-indent: -5px;
border: 1px solid var(--border);
border-radius: 6px;
background: transparent;
color: color-mix(in srgb, var(--fg) 55%, transparent);
cursor: pointer;
transition: border-color 0.12s, color 0.12s, background 0.12s;
position: relative;
top: -6px;
}
.settings-fallback-remove:hover {
border-color: var(--red);
color: var(--red);
background: color-mix(in srgb, var(--red) 10%, transparent);
}
.settings-fallback-add {
align-self: flex-start;
margin-top: 2px;
font-size: 11px;
padding: 3px 9px;
border: 1px dashed var(--border);
border-radius: 6px;
background: transparent;
color: color-mix(in srgb, var(--fg) 65%, transparent);
cursor: pointer;
transition: border-color 0.12s, color 0.12s;
}
.settings-fallback-add:hover {
border-color: var(--red);
color: var(--red);
}
.settings-input {
flex: 1;
padding: 5px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-family: inherit;
font-size: 12px;
transition: border-color 0.15s;
}
/* Hide the native number-input spinner arrows (e.g. SMTP/IMAP Port) — they
render unstyled and ugly. Field still accepts only numbers. */
.settings-input[type="number"]::-webkit-outer-spin-button,
.settings-input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.settings-input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.settings-input:focus {
outline: none;
border-color: var(--red);
}
/* ── Contacts manager (Settings → Integrations → CardDAV) ── */
.contacts-add-row {
display: flex;
gap: 6px;
margin-bottom: 8px;
align-items: center;
}
.contacts-list {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 360px;
overflow-y: auto;
}
.contact-row {
padding: 6px 4px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
}
.contact-row:last-child { border-bottom: none; }
.contact-row-edit { margin-top: 4px; }
/* ── Sort Dropdown ── */
.sort-dropdown {
position: absolute;
right: 0;
top: 100%;
z-index: 1000;
min-width: 120px !important;
width: max-content;
padding: 4px !important;
margin-top: 4px;
background: var(--panel) !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0,0,0,0.4) !important;
backdrop-filter: none !important;
}
.sort-dropdown-item {
cursor: pointer;
padding: 6px 8px !important;
font-size: 11px !important;
border-radius: 6px !important;
border-bottom: none !important;
white-space: nowrap;
transition: background 0.1s;
}
.sort-dropdown-item:hover {
background: color-mix(in srgb, var(--accent) 10%, transparent) !important;
}
.sort-dropdown-sep {
border-top: 1px solid var(--border);
margin-top: 2px;
padding-top: 6px;
}
/* Mobile: bigger taps for the chats sort/funnel menu and the select-mode
bulk-action bar. Also drop the "Rearrange" item — it's a touch-finicky
feature better left to desktop. */
@media (max-width: 768px) {
/* Nudge the funnel/sort button right on mobile so it doesn't hug the
chevron next to it. */
#session-sort-btn { transform: translateX(11px); }
#session-sort-dropdown.sort-dropdown {
min-width: 200px !important;
padding: 6px !important;
}
#session-sort-dropdown .sort-dropdown-item {
padding: 12px 14px !important;
font-size: 14px !important;
}
#session-rearrange-toggle { display: none !important; }
/* Select-mode bar — make Archive / Delete / Cancel buttons + the
Select-all toggle finger-sized. */
.session-bulk-bar {
padding: 8px 10px;
font-size: 14px;
gap: 10px;
}
.session-bulk-btn {
padding: 10px;
min-width: 44px;
min-height: 44px;
}
.session-bulk-btn svg { width: 20px; height: 20px; }
#session-select-all-dot { font-size: 22px !important; padding: 6px; }
#session-select-all-label { font-size: 14px !important; padding: 4px; }
}
/* ── Admin: Built-in tools ── */
.admin-tool-category {
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 6px;
overflow: hidden;
transition: border-color 0.15s;
}
.admin-tool-category:hover {
border-color: color-mix(in srgb, var(--fg) 20%, var(--border));
}
.admin-tool-cat-header {
font-size: 12px;
font-weight: 500;
padding: 8px 10px;
transition: background 0.15s;
}
.admin-tool-cat-header:hover {
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.admin-tool-cat-body {
border-top: 1px solid var(--border);
padding: 4px 0;
}
.admin-tool-cat-body.hidden {
display: none;
}
.admin-tool-row {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
padding-right: 28px; /* align toggles with header toggle (chevron 12px + gap 6px + padding) */
transition: background 0.08s;
}
.admin-tool-row:hover {
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.admin-tool-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 1px;
}
.admin-tool-name {
font-size: 11px;
font-weight: 500;
}
.admin-tool-desc {
font-size: 10px;
opacity: 0.4;
}
.admin-tool-ctx {
font-size: 9px;
opacity: 0.3;
flex-shrink: 0;
min-width: 35px;
text-align: right;
font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
}
/* Cookbook serve param hints */
.hwfit-hint {
display: inline-flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
border-radius: 50%;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg);
font-size: 8px;
font-weight: 600;
opacity: 0.35;
cursor: help;
margin-left: 2px;
vertical-align: middle;
transition: opacity 0.1s;
}
.hwfit-hint:hover {
opacity: 0.8;
background: color-mix(in srgb, var(--accent) 15%, transparent);
}
.hwfit-dl-dot {
color: var(--green);
font-size: 8px;
margin-left: 4px;
opacity: 0.7;
}
.hwfit-parser-tag {
font-size: 9px;
opacity: 0.4;
background: color-mix(in srgb, var(--fg) 8%, transparent);
padding: 1px 5px;
border-radius: 3px;
margin-left: 4px;
font-family: 'Berkeley Mono', 'SF Mono', 'Fira Code', monospace;
}
/* Cookbook output copy — show only on hover */
.cookbook-output-wrap .cookbook-output-copy {
opacity: 0;
transition: opacity 0.1s;
}
.cookbook-output-wrap:hover .cookbook-output-copy {
opacity: 0.5;
}
.cookbook-output-wrap .cookbook-output-copy:hover {
opacity: 1;
}
#cookbook-dl-btn-search {
position: relative;
top: -4px;
padding-top: 0;
padding-bottom: 0;
height: 28px;
}
/* Cached model menu button */
.hwfit-cached-menu-btn {
background: none;
border: none;
color: var(--fg);
font-size: 16px;
cursor: pointer;
opacity: 0;
padding: 2px 4px;
border-radius: 4px;
transition: opacity 0.1s;
flex-shrink: 0;
position: relative;
top: -2px;
}
.memory-item:hover .hwfit-cached-menu-btn,
.hwfit-cached-item:hover .hwfit-cached-menu-btn { opacity: 0.4; }
.hwfit-cached-menu-btn:hover { opacity: 1 !important; }
.hwfit-cached-menu-btn svg { pointer-events: none; }
@media (max-width: 768px) {
#cookbook-modal .hwfit-cached-menu-btn {
opacity: 0.72 !important;
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
justify-content: center;
top: -4px;
}
#cookbook-modal .hwfit-cached-menu-btn:active {
opacity: 1 !important;
background: color-mix(in srgb, var(--fg) 9%, transparent);
}
}
/* Quick run button */
.cookbook-run-btn {
background: var(--accent, var(--red)) !important;
color: var(--panel) !important;
border-color: var(--accent, var(--red)) !important;
font-weight: 600;
}
.cookbook-run-btn:hover {
opacity: 0.9;
}
#hwfit-cache-scan {
position: relative;
top: 1px;
width: 51px;
}
#serve-search {
height: 32px;
}
#cookbook-dl-btn {
position: relative;
top: -4px;
}
/* Cookbook model directory tags */
.cookbook-modeldir-tag {
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg);
display: inline-flex;
align-items: center;
gap: 4px;
font-family: 'Berkeley Mono', 'SF Mono', monospace;
}
.cookbook-modeldir-default {
opacity: 0.5;
}
/* Default-server radio-check in a Settings server title (same look as the
model-dir download target). Active = accent-tinted. */
.cookbook-srv-default {
cursor: pointer;
opacity: 0.35;
display: inline-flex;
align-items: center;
gap: 3px;
line-height: 1;
transition: opacity 0.12s, color 0.12s;
}
.cookbook-srv-default:hover { opacity: 0.8; }
.cookbook-srv-default.active { opacity: 1; color: var(--accent, var(--red)); }
.cookbook-srv-default-label { font-size: 10px; font-weight: 600; letter-spacing: 0.02em; }
/* Download-target toggle inside a model-dir tag */
.cookbook-modeldir-dl {
cursor: pointer;
opacity: 0.35;
display: inline-flex;
align-items: center;
line-height: 0;
transition: opacity 0.12s, color 0.12s;
}
.cookbook-modeldir-dl:hover { opacity: 0.8; }
.cookbook-modeldir-dl.active { opacity: 1; color: var(--accent, var(--red)); }
/* The tag currently flagged as the download target — clearly highlighted so
it's obvious where downloads land. */
.cookbook-modeldir-target {
opacity: 1;
color: var(--accent, var(--red));
font-weight: 600;
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 60%, transparent);
}
.cookbook-modeldir-rm {
cursor: pointer;
opacity: 0.4;
font-size: 10px;
}
.cookbook-modeldir-rm:hover {
opacity: 1;
}
.cookbook-modeldir-add {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-size: 10px;
font-weight: 500;
height: 19.5px;
box-sizing: border-box;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
position: relative;
top: -3px;
padding: 0 9px;
transition: border-color 0.12s, background 0.12s;
}
.cookbook-modeldir-add:hover { border-color: var(--accent, var(--red)); background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent); }
/* Cookbook serve optimizations */
.hwfit-serve-opts {
display: flex;
align-items: center;
gap: 8px;
margin: 4px 0;
}
.hwfit-apply-opts {
font-size: 11px !important;
min-width: auto !important;
white-space: nowrap;
}
.hwfit-opts-desc {
font-size: 9px;
opacity: 0.4;
font-style: italic;
}
/* Library modal tabs */
.lib-tabs,
.admin-tabs {
display: flex;
gap: 0;
margin-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.lib-tab,
.admin-tab {
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--fg-muted);
font-size: 12px;
font-family: inherit;
padding: 6px 14px;
cursor: pointer;
transition: color 0.1s, border-color 0.1s;
}
.lib-tab:hover,
.admin-tab:hover { color: var(--fg); }
.lib-tab.active,
.admin-tab.active {
color: var(--accent, var(--red));
border-bottom-color: var(--accent, var(--red));
}
/* Cookbook tab count badge */
.cookbook-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent, var(--red)) 20%, transparent);
color: var(--accent, var(--red));
font-size: 9px;
font-weight: 700;
margin-left: 4px;
line-height: 1;
}
/* ── Generate Visual Report button (deep research) ── */
.view-report-btn {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 12px;
padding: 8px 16px;
border: 1px solid color-mix(in srgb, var(--red) 30%, transparent);
border-radius: 8px;
background: color-mix(in srgb, var(--red) 10%, transparent);
color: var(--fg);
font-family: inherit;
font-size: 0.88em;
font-weight: 500;
transition: background 0.15s ease;
cursor: pointer;
}
.view-report-btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--red) 18%, transparent);
}
.view-report-btn svg {
flex-shrink: 0;
}
.report-btn-wrap {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.view-report-btn.chat-about-btn {
border-color: color-mix(in srgb, var(--fg) 22%, transparent);
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.view-report-btn.chat-about-btn:hover:not(:disabled) {
background: color-mix(in srgb, var(--fg) 12%, transparent);
}
.view-report-btn:disabled {
opacity: 0.6;
cursor: progress;
}
.report-spinner {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.88em;
color: var(--fg);
opacity: 0.7;
}
.report-spinner-text {
font-weight: 500;
}
.report-spinner-dots::after {
content: '';
animation: report-dots 1.4s steps(4, end) infinite;
}
@keyframes report-dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
/* ── Continue research hint ────────────────────────── */
.continue-research-wrap {
margin-top: 8px;
}
.continue-research-hint {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.78em;
color: var(--fg-dim, var(--fg));
opacity: 0.5;
padding: 4px 0;
}
.continue-research-hint svg {
flex-shrink: 0;
opacity: 0.6;
}
/* ── Research reconnect & timer ────────────────────── */
.msg.research-reconnect {
border-left: 3px solid var(--red);
background: color-mix(in srgb, var(--red) 5%, transparent);
}
.research-timer {
font-size: 0.8em;
opacity: 0.6;
margin-top: 4px;
font-family: monospace;
color: var(--fg-dim, var(--fg));
}
/* ── Research synapse visualization ───────────────────────────────
Live SVG graph of an in-flight deep-research run. The query is the
central node; sub-questions branch off per round; sources are leaf
nodes that pop in as they're captured. Pulses indicate activity. */
.research-synapse {
margin: 6px 0 4px;
border: 1px solid var(--border);
border-radius: 10px;
background:
radial-gradient(ellipse at center, color-mix(in srgb, var(--accent, var(--red)) 10%, transparent) 0%, transparent 70%),
color-mix(in srgb, var(--panel) 50%, var(--bg));
overflow: hidden;
}
.research-synapse .rs-stage {
height: 200px;
position: relative;
}
.research-synapse-compact .rs-stage { height: 130px; }
.research-synapse-compact .rs-meta { padding: 4px 8px 5px; font-size: 10px; }
.research-synapse-compact .rs-label-sub { font-size: 8px; }
.research-synapse svg {
display: block;
width: 100%;
height: 100%;
}
.research-synapse .rs-edge {
stroke: var(--border);
stroke-width: 1.2;
fill: none;
opacity: 0.55;
}
.research-synapse .rs-edge.rs-edge-firing {
stroke: var(--accent, var(--red));
stroke-width: 2;
opacity: 1;
filter: drop-shadow(0 0 4px var(--accent, var(--red)));
animation: rs-fire 1.1s ease-out;
}
@keyframes rs-fire {
0% { stroke-dasharray: 4 200; stroke-dashoffset: 200; opacity: 0; }
20% { opacity: 1; }
100% { stroke-dasharray: 200 4; stroke-dashoffset: -200; opacity: 0.55; }
}
.research-synapse .rs-node {
fill: var(--bg);
stroke: var(--accent, var(--red));
stroke-width: 1.5;
transition: all 0.3s ease;
transform-box: fill-box;
transform-origin: center;
}
.research-synapse .rs-node-root {
fill: var(--accent, var(--red));
stroke: var(--accent, var(--red));
}
/* Sub & leaf nodes stay inside the accent palette so the whole graph
reads as one organism. Leaves are softer/lower-contrast dots that
float around their sub — not green-on-stem (which read as palm fronds). */
.research-synapse .rs-node-sub {
stroke: color-mix(in srgb, var(--accent, var(--red)) 70%, var(--fg));
}
.research-synapse .rs-node-leaf {
stroke: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
fill: color-mix(in srgb, var(--accent, var(--red)) 22%, var(--bg));
}
.research-synapse .rs-node-new {
animation: rs-pop 0.6s ease-out;
}
@keyframes rs-pop {
0% { transform: scale(0); opacity: 0; }
60% { transform: scale(1.25); opacity: 1; }
100% { opacity: 1; }
}
.research-synapse .rs-pulse {
fill: var(--accent, var(--red));
opacity: 0;
animation: rs-pulse 2.6s ease-out infinite;
transform-box: fill-box;
transform-origin: center;
}
@keyframes rs-pulse {
0% { transform: scale(1); opacity: 0.65; }
100% { transform: scale(5); opacity: 0; }
}
.research-synapse .rs-label {
fill: var(--fg);
font-size: 10px;
font-family: ui-monospace, "JetBrains Mono", monospace;
pointer-events: none;
opacity: 0.85;
}
.research-synapse .rs-label-sub {
font-size: 9px;
opacity: 0.7;
}
.research-synapse .rs-meta {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
padding: 6px 10px 8px;
font-family: ui-monospace, "JetBrains Mono", monospace;
font-size: 11px;
color: var(--fg-dim, var(--fg));
opacity: 0.85;
border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
}
.research-synapse .rs-meta .rs-status {
color: var(--accent, var(--red));
font-weight: 600;
}
.research-synapse .rs-meta b {
color: var(--fg);
font-weight: 600;
}
.research-synapse .rs-meta .rs-sep { opacity: 0.4; }
.research-synapse.rs-complete .rs-pulse { animation: none; opacity: 0; }
.research-synapse.rs-complete .rs-node-root {
fill: var(--color-success, #4caf50);
stroke: var(--color-success, #4caf50);
}
.research-synapse.rs-complete .rs-meta .rs-status { color: var(--color-success, #4caf50); }
.research-synapse.rs-error .rs-meta .rs-status { color: var(--red); }
/* ── Raw findings items (inside sources-style box) ── */
.finding-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.finding-item .source-link {
border-radius: 6px 6px 0 0;
}
.finding-summary {
padding: 6px 8px 8px;
font-size: 0.82em;
line-height: 1.5;
color: var(--fg-dim, var(--fg));
opacity: 0.85;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.finding-item:last-child .finding-summary {
border-bottom: none;
margin-bottom: 0;
}
/* ═══════════════════════════════════════════════════════════
Gallery Editor
═══════════════════════════════════════════════════════════ */
/* Tab bar */
.gallery-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
padding: 0 16px;
background: var(--panel);
}
.gallery-tab {
padding: 8px 18px;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--fg);
opacity: 0.6;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: opacity 0.15s, border-color 0.15s;
}
.gallery-tab:hover { opacity: 0.85; }
.gallery-tab.active {
opacity: 1;
border-bottom-color: var(--red);
}
/* Icon + label layout inside each tab. */
.gallery-tab {
display: inline-flex;
align-items: center;
gap: 6px;
}
.gallery-tab-icon {
display: inline-flex;
align-items: center;
opacity: 0.85;
}
.gallery-tab.active .gallery-tab-icon { opacity: 1; }
/* Close × on the Edit tab — appears on hover. */
.gallery-tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 2px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
/* Use a slightly larger glyph at a larger line-height so the × sits
visually centered — the U+00D7 character has uneven em-box metrics. */
font-size: 16px;
line-height: 16px;
font-weight: 500;
cursor: pointer;
padding-bottom: 2px;
box-sizing: border-box;
}
.gallery-tab:hover .gallery-tab-close,
.gallery-tab.active .gallery-tab-close { opacity: 0.65; }
/* Hide the close × entirely on the Edit tab when there's no edit open. */
.gallery-tab[data-tab="editor"]:not(.has-edit) .gallery-tab-close { display: none !important; }
.gallery-tab-close:hover { opacity: 1 !important; background: color-mix(in srgb, var(--red) 25%, transparent); color: var(--red); }
/* Inline rename input shown when the Edit tab is double-clicked. */
.gallery-tab-rename-input {
background: var(--bg);
border: 1px solid var(--red);
border-radius: 3px;
color: var(--fg);
font: inherit;
font-size: 13px;
padding: 1px 4px;
width: 140px;
outline: none;
}
/* Albums tab — grid of album cards with cover thumbnails. */
.gallery-albums-container {
padding: 12px 4px;
max-height: 70vh;
overflow-y: auto;
border: 2px dashed transparent;
border-radius: 8px;
transition: border-color 0.15s, background 0.15s;
}
.gallery-settings-container {
padding: 8px 4px;
max-height: 72vh;
overflow-y: auto;
}
.gallery-albums-container.gallery-dragover {
border-color: var(--red);
background: color-mix(in srgb, var(--red) 5%, transparent);
}
.gallery-albums-empty {
display: flex; flex-direction: column; align-items: center; gap: 12px;
padding: 48px 16px;
opacity: 0.7;
font-size: 13px;
}
.gallery-albums-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.gallery-album-card {
position: relative;
display: flex;
flex-direction: column;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.gallery-album-card:hover {
transform: translateY(-2px);
border-color: var(--red);
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 12%, transparent);
}
.gallery-album-menu-btn {
position: absolute;
top: 6px; right: 6px;
width: 24px; height: 24px;
display: flex; align-items: center; justify-content: center;
background: color-mix(in srgb, var(--bg) 70%, transparent);
color: var(--fg);
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
border-radius: 6px;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
z-index: 2;
backdrop-filter: blur(4px);
/* Plain "…" text — the previous SVG was rendering as a flat black
block on some themes when its currentColor didn't contrast. */
font-size: 16px;
line-height: 1;
font-weight: 600;
padding: 0 0 4px;
}
.gallery-album-card:hover .gallery-album-menu-btn,
.gallery-album-menu-btn:focus-visible { opacity: 1; }
.gallery-album-menu-btn:hover { background: var(--bg); }
.gallery-album-menu-pop {
position: absolute;
top: 34px; right: 6px;
min-width: 140px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
display: flex; flex-direction: column;
padding: 4px;
z-index: 3;
}
.gallery-album-menu-pop[hidden] { display: none; }
.gallery-album-cover {
aspect-ratio: 4 / 3;
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.gallery-album-cover img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
}
.gallery-album-placeholder {
opacity: 0.4;
font-size: 40px;
display: flex; align-items: center; justify-content: center;
width: 100%; height: 100%;
}
.gallery-album-info {
padding: 8px 10px;
display: flex; flex-direction: column; gap: 2px;
}
.gallery-album-name {
font-size: 13px;
font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gallery-album-count {
font-size: 11px;
opacity: 0.55;
}
.gallery-album-card-add {
border-style: dashed;
opacity: 0.7;
}
.gallery-album-card-add:hover { opacity: 1; }
/* Edit-tab empty state — shown before any image is loaded. */
.gallery-editor-landing {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px;
padding: 64px 16px;
flex: 1;
text-align: center;
color: var(--fg);
}
.gallery-editor-landing h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
/* "vision model" link in the AI-tagging description → opens Settings. */
.ge-vision-link {
color: var(--accent, var(--red));
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.ge-vision-link:hover { opacity: 0.8; }
.ge-alpha-tag {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
vertical-align: middle;
padding: 1px 5px;
border-radius: 4px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
position: relative;
top: -1px;
}
.gallery-editor-landing p {
margin: 0;
opacity: 0.6;
font-size: 13px;
max-width: 320px;
}
.gallery-editor-landing-actions {
display: flex; gap: 10px;
margin-top: 8px;
}
/* Bigger primary action buttons specifically inside the editor landing —
the gallery-wide .gallery-select-btn is a compact toolbar style which is
too small for the empty-state hero. */
.gallery-editor-landing-actions .gallery-select-btn {
padding: 7px 22px 17px;
font-size: 13px;
opacity: 0.85;
}
.gallery-editor-landing-actions .gallery-select-btn:hover { opacity: 1; }
/* Template picker — native so it renders reliably across browsers
without fighting our custom flex layouts. */
.gallery-editor-template-label {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
margin-top: 18px;
font-size: 11px;
opacity: 0.55;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.gallery-editor-template-select {
min-width: 240px;
padding: 8px 12px;
background: var(--panel);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font: inherit;
font-size: 13px;
text-transform: none;
letter-spacing: normal;
cursor: pointer;
opacity: 1;
}
.gallery-editor-template-select:hover { border-color: var(--red); }
.gallery-editor-template-select:focus { outline: none; border-color: var(--red); }
/* Saved-drafts grid on the editor landing. Surfaces every persisted
in-progress project so the user can resume without re-opening the
original photo. Each card: thumbnail + name + last-saved hint + ×. */
.gallery-editor-drafts {
width: min(720px, 92%);
margin: 28px auto 0; /* centered */
padding-top: 18px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 10px;
}
.gallery-editor-drafts {
position: relative;
}
.gallery-editor-drafts-loading {
position: absolute;
inset: 30px 0 0 0;
z-index: 4;
display: flex;
align-items: center;
justify-content: center;
background: var(--panel);
border-radius: 6px;
min-height: 80px;
}
.gallery-editor-drafts-header {
display: flex;
align-items: center;
justify-content: center; /* center the title / search / select */
gap: 8px;
margin-bottom: 8px;
}
/* In select mode the bulk bar drops in below the header — pull it up. */
#gallery-editor-drafts-bulk { margin-top: -12px; }
.gallery-editor-drafts-title {
margin: 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.55;
font-weight: 600;
text-align: left;
flex-shrink: 0;
}
.gallery-editor-drafts-search {
flex: 1 1 auto;
min-width: 0;
max-width: 280px;
height: 26px; /* same thickness as the Select button */
box-sizing: border-box;
padding: 0 8px;
font-size: 12px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
}
.gallery-editor-drafts-search:focus { outline: none; border-color: var(--red); }
/* The shared .gallery-select-btn has a -4px top margin + tall asymmetric padding
meant for the main gallery toolbar; in this header it makes the button a
different height / offset from the search box. Normalize so it lines up. */
.gallery-editor-drafts-header .gallery-select-btn {
margin-top: 0 !important;
height: 26px; /* match the search bar */
box-sizing: border-box;
padding: 0 10px;
font-size: 12px;
line-height: 1;
flex-shrink: 0;
}
#gallery-editor-drafts-select { margin-left: auto; flex-shrink: 0; }
#gallery-editor-drafts-select.active {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
.gallery-editor-draft-card.selected {
outline: 2px solid var(--accent, var(--red));
outline-offset: -2px;
}
.gallery-editor-draft-card.select-mode { cursor: pointer; }
/* Graceful exit when a project card is deleted — fade + shrink before re-render. */
.gallery-editor-draft-card.gallery-draft-removing {
opacity: 0;
transform: scale(0.92);
transition: opacity 0.24s ease, transform 0.24s ease;
pointer-events: none;
}
/* Draft select dot uses the standard small .gallery-select-dot look (same as the
photo grid) — no oversized white-ring checkbox. */
.gallery-editor-drafts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.gallery-editor-draft-card {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--panel) 70%, transparent);
cursor: pointer;
transition: border-color 0.15s, background 0.15s, transform 0.15s;
text-align: left;
}
.gallery-editor-draft-card:hover {
border-color: var(--accent, var(--red));
background: color-mix(in srgb, var(--panel) 90%, transparent);
}
.gallery-editor-draft-card:focus-visible {
outline: 2px solid var(--accent, var(--red));
outline-offset: 2px;
}
.gallery-editor-draft-thumb {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: 4px;
background: var(--bg);
display: block;
}
.gallery-editor-draft-thumb-empty {
background: repeating-linear-gradient(
45deg,
color-mix(in srgb, var(--fg) 4%, transparent),
color-mix(in srgb, var(--fg) 4%, transparent) 6px,
transparent 6px,
transparent 12px
);
}
.gallery-editor-draft-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.gallery-editor-draft-name {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-editor-draft-meta {
font-size: 10px;
opacity: 0.55;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-editor-draft-delete {
position: absolute;
top: -2px;
right: 8px;
width: 22px;
height: 22px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 14px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
}
.gallery-editor-draft-card:hover .gallery-editor-draft-delete { opacity: 1; }
.gallery-editor-draft-delete:hover { background: var(--red); }
/* Editor layout */
.gallery-editor {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
/* Prevent the editor's own scroll/touch from chaining out to the
gallery modal or the page body — critical on mobile so touching
the canvas doesn't slide the whole page. */
overscroll-behavior: contain;
touch-action: pan-y;
}
/* Pin the topbar to the top of the editor so its action buttons remain
visible if the gallery modal body itself ends up scrolling. */
.ge-topbar { position: sticky; top: 0; z-index: 5; }
.ge-topbar.ge-topbar-menu-open { z-index: 10006; }
/* Top bar */
.ge-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
gap: 8px;
}
.ge-topbar-left, .ge-topbar-right {
display: flex;
align-items: center;
gap: 4px;
}
/* "ALPHA" badge — flags the editor as in-development. */
.ge-alpha-badge {
flex-shrink: 0;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
line-height: 1;
padding: 3px 6px;
margin-right: 4px;
border-radius: 4px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
text-transform: uppercase;
user-select: none;
cursor: default;
}
@media (max-width: 768px) {
/* Keep the toolbar tight on mobile — badge stays but shrinks. */
.ge-alpha-badge { font-size: 8px; padding: 2px 5px; margin-right: 2px; }
}
.ge-diffusion-status {
font-size: 11px;
cursor: pointer;
padding: 2px 8px;
border-radius: 4px;
transition: all 0.15s;
white-space: nowrap;
}
.ge-diffusion-status.online {
color: var(--color-success, #4ade80);
}
.ge-diffusion-status.offline {
color: var(--color-error, #ef4444);
opacity: 0.7;
}
.ge-diffusion-status.offline:hover {
opacity: 1;
background: color-mix(in srgb, var(--color-error) 10%, transparent);
}
.ge-topbar-fill {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--accent, var(--red));
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.ge-topbar-fill:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
border-color: var(--accent, var(--red));
}
/* Mask-color swatch in the topbar — small label + circular swatch so
the user can pick a contrasting overlay colour from anywhere. */
.ge-topbar-mask-color-wrap {
display: inline-flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.ge-topbar-mask-color-label {
font-size: 10px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.ge-topbar-mask-color {
width: 18px;
height: 18px;
flex: 0 0 18px;
}
.ge-topbar-mask-color.cp-swatch-input {
width: 18px;
height: 18px;
flex: 0 0 18px;
border-radius: 50%;
padding: 0;
}
.ge-topbar-sep {
width: 1px;
height: 16px;
background: var(--border);
margin: 0 4px;
}
/* Editor body (toolbar + canvas + panel) */
.ge-editor-body {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
position: relative;
}
.gallery-editor-container {
flex: 1;
min-height: 0;
display: none;
}
/* Toolbar (left) */
.ge-toolbar {
display: flex;
flex-direction: column;
gap: 2px;
/* Asymmetric horizontal padding shifts the whole column 4 px left —
buttons, hover backgrounds, active highlight pills, and section
separators all move together. */
padding: 8px 8px 8px 0;
background: var(--panel);
border-right: 1px solid var(--border);
width: 56px;
flex-shrink: 0;
}
.ge-tool-sep {
font-size: 8px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-muted);
text-align: center;
padding: 8px 0 4px;
border-top: 1px solid var(--border);
margin-top: 4px;
opacity: 0.6;
flex-shrink: 0;
}
/* All tool buttons share a fixed height so the column reads as a clean
grid. Long labels (e.g. "Remove BG") truncate with ellipsis instead of
wrapping to two lines and making one button taller than the others. */
.ge-tool-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 0 2px;
height: 42px;
flex-shrink: 0;
border: none;
background: none;
color: var(--fg);
opacity: 0.6;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s, opacity 0.15s;
}
.ge-tool-btn .ge-tool-label {
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ge-tool-btn { position: relative; }
/* Hover background is now a pseudo so we can shift it 2 px left
independently from the button's own bounds (which contain the icon
and label at their already-shifted positions). */
.ge-tool-btn:hover { opacity: 0.85; }
.ge-tool-btn:hover::after {
content: '';
position: absolute;
inset: 0 2px 0 -2px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border-radius: 6px;
z-index: -1;
pointer-events: none;
}
.ge-tool-btn.active {
opacity: 1;
color: var(--red);
background: none;
/* Padding stays the same as inactive buttons so the icon stays
horizontally aligned with its neighbors. The taller highlight is
drawn by ::before so it can extend up/down without pushing the
button or shifting the icon. */
}
.ge-tool-btn.active::before {
content: '';
position: absolute;
/* Highlight pill shifted 2 px left of the button bounds so it sits
under the icon + label (which are also nudged left). Same total
width as before — just slid. */
inset: 0 2px 0 -2px;
background: color-mix(in srgb, var(--red) 18%, transparent);
border-radius: 6px;
z-index: 0;
pointer-events: none;
}
/* Tool button contents stay above the active highlight pseudo. Also
shifted an additional 2 px left so the icon + label sit further inside
the highlight pill (which still anchors to the button bounds). */
.ge-tool-btn > * { position: relative; z-index: 1; left: -2px; }
.ge-tool-icon { font-size: 18px; line-height: 1; }
.ge-tool-label { font-size: 9px; line-height: 1.25; }
/* AI badge — same ✦ glyph the Enhance tool uses, pinned to the top-
left of any tool button marked `ai: true`. Same color as the icon
itself (inherits foreground) so it reads as part of the tool, not
a flag. */
.ge-tool-ai {
position: absolute !important;
top: 1px;
z-index: 2;
color: inherit;
opacity: 0.7;
pointer-events: none;
font-size: 11px;
line-height: 1;
font-weight: 700;
left: 3px !important;
}
.ge-tool-btn.is-ai:hover .ge-tool-ai { opacity: 0.95; }
.ge-tool-btn.is-ai.active .ge-tool-ai { opacity: 1; }
/* Inline ✦ marker used on AI action buttons (Generate, Remove, etc.)
so they read as AI-backed at a glance — same glyph as .ge-tool-ai
in the toolbar, just sitting inline next to the label. */
.ge-btn-ai-mark {
display: inline-block;
font-size: 11px;
line-height: 1;
font-weight: 700;
opacity: 0.75;
margin-right: 1px;
}
.ge-btn-ai:hover .ge-btn-ai-mark { opacity: 1; }
/* Per-tool clear-selection badge — shows a tiny X in the top-right of
the Lasso / Wand button when each holds a selection. Click clears
the selection without switching tools (handler stops propagation). */
.ge-tool-clear {
position: absolute !important;
top: 3px;
right: 3px;
width: 14px;
height: 14px;
display: none;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
background: var(--red);
cursor: pointer;
opacity: 0.95;
z-index: 2;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--red) 70%, transparent),
0 1px 4px color-mix(in srgb, var(--red) 50%, transparent);
transition: opacity 0.12s, transform 0.12s, box-shadow 0.12s;
left: auto !important;
}
.ge-tool-btn.has-selection .ge-tool-clear { display: inline-flex; }
.ge-tool-clear:hover {
opacity: 1;
transform: scale(1.15);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--red) 80%, transparent),
0 2px 6px color-mix(in srgb, var(--red) 60%, transparent);
}
/* Aspect-ratio placeholder shown while a draft is loading. */
.ge-canvas-placeholder {
background: color-mix(in srgb, var(--fg) 8%, transparent);
border: 1px dashed color-mix(in srgb, var(--fg) 20%, transparent);
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
}
/* Canvas area (center) */
.ge-canvas-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: auto;
background: var(--bg);
position: relative;
min-width: 0;
/* Stop touch interactions inside the editor from dragging the whole
page (or the gallery modal). Touches on the canvas are owned by the
editor's drawing logic — no native pan/zoom. */
overscroll-behavior: contain;
}
.ge-main-canvas {
touch-action: none;
}
.ge-wand-loading {
position: absolute;
inset: 0;
z-index: 25;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--bg) 30%, transparent);
pointer-events: none;
}
/* Loading overlay shown while a large image is decoding — centered
whirlpool + "Loading" caption over a translucent dim. */
.ge-loading-overlay {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--bg) 70%, transparent);
pointer-events: none;
}
/* Full-editor cover (project load): above the toolbar/panel and near-opaque so
the top toolbar/old content doesn't peek through while loading. */
.ge-loading-overlay-full {
z-index: 200;
background: color-mix(in srgb, var(--bg) 92%, transparent);
pointer-events: auto;
}
.ge-loading-overlay .ge-loading-inner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
border-radius: 10px;
background: var(--panel);
border: 1px solid var(--border);
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
}
.ge-loading-overlay .ge-loading-text {
font-size: 12px;
opacity: 0.8;
}
/* Shortcuts cheatsheet (toggled by `?` or the keyboard icon in the top bar). */
#ge-shortcuts-overlay {
position: absolute;
inset: 0;
z-index: 50;
background: color-mix(in srgb, var(--bg) 70%, transparent);
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
#ge-shortcuts-overlay[hidden] { display: none; }
.ge-shortcuts-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 18px 12px;
box-shadow: 0 8px 30px color-mix(in srgb, var(--fg) 25%, transparent);
width: min(720px, 92vw);
max-height: 86vh;
overflow-y: auto;
color: var(--fg);
}
.ge-shortcuts-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
font-weight: 600;
font-size: 13px;
}
.ge-shortcuts-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px 24px;
}
@media (min-width: 720px) {
.ge-shortcuts-grid { grid-template-columns: repeat(4, 1fr); }
}
.ge-shortcuts-col h5 {
margin: 0 0 6px;
font-size: 11px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 600;
}
/* Each shortcut row: key chips + description. The key chips are usually
chained with "+" so we use a small horizontal gap between siblings,
plus a larger left margin on the trailing text via the kbd:last-of-type
sibling rule — keeps "Ctrl+Shift+D Deselect" readable instead of glued. */
.ge-shortcuts-col > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px;
padding: 4px 0;
opacity: 0.85;
line-height: 1.5;
}
/* Push the description text away from the last kbd by adding margin
to any text node that follows a kbd. Flex+wrap handles wrap on narrow. */
.ge-shortcuts-col > div kbd + kbd { margin-left: 0; }
.ge-shortcuts-col > div kbd:last-of-type { margin-right: 6px; }
.ge-shortcuts-card kbd,
#ge-shortcuts-popover kbd {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
font-weight: 600;
padding: 2px 6px;
border: 1px solid color-mix(in srgb, var(--accent, #cc6a3a) 55%, transparent);
border-bottom-width: 2px;
border-radius: 4px;
background: color-mix(in srgb, var(--accent, #cc6a3a) 14%, transparent);
color: var(--accent, #cc6a3a);
min-width: 18px;
line-height: 1.4;
}
.ge-shortcuts-foot {
margin-top: 12px;
font-size: 11px;
opacity: 0.55;
text-align: center;
}
.ge-main-canvas {
/* Cursor is managed entirely by JS so each tool can show its own
icon (move arrow, circle overlay, etc.) without CSS fighting it. */
image-rendering: pixelated;
box-shadow: 0 2px 12px rgba(0,0,0,0.3);
background: repeating-conic-gradient(#808080 0% 25%, #a0a0a0 0% 50%) 50% / 16px 16px;
}
/* Transform-overlay canvas — sits over the main canvas with extra
margin so resize / rotation handles can render OUTSIDE the image
bounds. Pointer events disabled so it doesn't intercept clicks. */
.ge-transform-overlay {
position: absolute;
image-rendering: pixelated;
pointer-events: none;
z-index: 5;
}
/* Right panel */
.ge-right-panel {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-left: 1px solid var(--border);
background: var(--panel);
/* The whole panel scrolls so every tool control is reachable; the
layers-actions row uses position:sticky to stay pinned at the
bottom of the viewport while scrolling. */
overflow-y: auto;
overflow-x: hidden;
}
/* Controls */
.ge-controls {
flex: 0 0 auto;
padding: 10px 10px 6px;
border-bottom: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 8px;
}
/* Brush controls (Color + Size) — give the two rows breathing room
between them, since neither sits inside a separator-bordered section. */
#ge-brush-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Floating Transform popup — horizontal layout, draggable from
anywhere on the popup (matches the FX / adjust popups). Defaults
to the right side of the editor (over the layers panel). */
/* Match the .ge-adj-popup layout convention: icon + title + [_][×]
header bar, then the body. Drag from the header (same as FX popups). */
.ge-transform-popup {
position: absolute;
z-index: 11;
display: flex;
flex-direction: column;
gap: 4px;
padding: 0 0 8px;
background: color-mix(in srgb, var(--panel) 96%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
backdrop-filter: blur(6px);
user-select: none;
}
.ge-transform-popup.ge-transform-popup-dragging .ge-transform-popup-head { cursor: grabbing; }
.ge-transform-popup-head {
/* Inherits .ge-adj-head styling — flex row, icon + title + buttons,
bottom border, grab cursor — with the title nudged down 2px for
visual balance and explicit horizontal padding so the icon doesn't
hug the left edge. */
display: flex !important;
align-items: center;
gap: 6px;
padding: 8px 12px;
padding-top: 10px;
touch-action: none;
cursor: grab;
}
.ge-transform-popup-head:active { cursor: grabbing; }
/* Force the title to take all middle space so the [_][×] cluster
stays pinned to the right edge. */
.ge-transform-popup-head .ge-adj-title { flex: 1 1 auto !important; }
.ge-transform-popup-head .ge-head-btns { margin-left: auto !important; }
.ge-transform-popup-head .ge-adj-title { position: relative; top: 2px; }
/* Icon nudged 4px lower than the title baseline so it reads as the
row's anchor rather than floating above it. */
.ge-transform-popup-head .ge-adj-icon { position: relative; top: 4px; }
/* Mobile-only: shift the head content UP to match the rest of the
editor's mobile popup styling. */
@media (max-width: 820px) {
.ge-transform-popup-head .ge-adj-title { top: -2px; }
.ge-transform-popup-head .ge-adj-icon { top: 0; }
}
/* Make sure the head-buttons cluster sits hard-right with a small
gap, matching the FX-popup convention. */
.ge-transform-popup-head .ge-head-btns { margin-left: auto; }
.ge-transform-min-hint {
display: inline-flex;
align-items: center;
opacity: 0.45;
margin-right: -2px;
line-height: 1;
color: var(--fg-muted);
}
/* Small "Merge" text label next to each merge / flatten button so the
icons read at a glance instead of needing the title-tooltip. */
.ge-merge-label {
font-size: 10px;
margin-left: 4px;
opacity: 0.75;
}
.ge-transform-popup-body {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
}
.ge-transform-popup-minimised .ge-transform-popup-body,
.ge-transform-popup-minimised .ge-transform-popup-hint {
display: none;
}
.ge-transform-popup-hint { padding: 0 12px; }
.ge-transform-field {
display: inline-flex;
align-items: center;
gap: 4px;
position: relative;
}
.ge-transform-field label {
font-size: 11px;
font-weight: 600;
opacity: 0.65;
min-width: 12px;
text-align: right;
}
/* Hide the native browser spin-buttons entirely — we render our own
themed ▲/▼ via .ge-transform-spin (see below). */
.ge-transform-popup-input {
width: 76px;
height: 24px;
padding: 0 18px 0 6px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 11px;
font-variant-numeric: tabular-nums;
text-align: right;
cursor: text;
-moz-appearance: textfield;
box-sizing: border-box;
}
.ge-transform-popup-input::-webkit-outer-spin-button,
.ge-transform-popup-input::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
display: none;
}
/* Rotation field reserves extra room on the right for both the °
suffix AND the custom spinner so they don't sit on top of each other.
The suffix renders inside that reserved zone. */
.ge-transform-popup-input-rot { width: 76px; padding-right: 30px; }
.ge-transform-popup-input:focus { outline: none; border-color: var(--red); }
.ge-transform-input-locked {
opacity: 0.4;
cursor: not-allowed;
}
/* Custom themed spinner — two slim chevron buttons stacked to the right
of the input. Tight stack so the pair reads as one control instead of
two floating arrows. */
.ge-transform-spin {
position: absolute;
right: 3px;
top: 50%;
/* Nudged up 4px relative to the input's vertical center. */
transform: translateY(calc(-50% - 4px));
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
height: 18px;
}
.ge-transform-spin button {
flex: 1 1 0;
width: 12px;
min-height: 0;
padding: 0;
background: transparent;
color: var(--fg-muted);
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
font-size: 7px;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.ge-transform-spin button:first-child { border-radius: 3px 3px 0 0; border-bottom: 0; }
.ge-transform-spin button:last-child { border-radius: 0 0 3px 3px; }
.ge-transform-spin button:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
color: var(--fg);
border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
}
.ge-transform-spin button:active {
background: color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
}
.ge-transform-input-locked + .ge-transform-spin { display: none; }
/* Position the ° suffix to the LEFT of the spinner so they don't
overlap. The rotation input's larger padding leaves room. */
.ge-transform-popup-suffix {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
opacity: 0.55;
pointer-events: none;
}
.ge-transform-aspect-btn {
background: none;
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg-muted);
cursor: pointer;
/* 2px smaller than the W/H input height so it reads as a chip-style
toggle rather than another form field. Pulled up 2px so it tucks
between W and H rather than sitting on their baseline. */
height: 22px;
width: 22px;
padding: 0;
position: relative;
top: -3px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.12s, color 0.12s, border-color 0.12s;
}
.ge-transform-aspect-btn:hover { opacity: 1; }
.ge-transform-aspect-btn.active {
opacity: 1;
color: var(--accent, var(--red));
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
}
/* Quick-action cluster (flip H, flip V, rotate 90°) — sits between the
rotation input and the Apply button. Each button matches the input
height and uses the editor's accent palette on hover. */
.ge-transform-quick {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0 4px;
/* Pulled up 2px so the flip/rotate icons sit slightly higher than the
W/H/rotation inputs, matching the visual weight of the head row. */
position: relative;
top: -2px;
border-left: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.ge-transform-quick-btn {
width: 24px;
height: 24px;
padding: 0;
background: none;
color: var(--fg-muted);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
transition: opacity 0.12s, color 0.12s, border-color 0.12s, background 0.12s;
}
.ge-transform-quick-btn:hover {
opacity: 1;
color: var(--fg);
border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.ge-transform-quick-btn:active {
background: color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
}
.ge-transform-popup-hint {
font-size: 10px;
opacity: 0.5;
margin: 0;
line-height: 1.3;
}
/* Floating Inpaint prompt — pops next to the user's last brush stroke
so they can type a description and Generate without diverting to
the side panel. */
.ge-inpaint-popup {
position: fixed;
z-index: 280;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
}
.ge-inpaint-popup-input {
width: 240px;
padding: 6px 8px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 12px;
position: relative;
top: 0;
}
.ge-inpaint-popup-input:focus { outline: none; border-color: var(--red); }
.ge-inpaint-popup-run {
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
position: relative;
top: 1px;
}
.ge-inpaint-popup-close {
background: none;
border: none;
color: var(--fg-muted);
font-size: 16px;
line-height: 1;
padding: 2px 4px;
margin-left: 2px;
cursor: pointer;
position: relative;
top: 0;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s;
}
.ge-inpaint-popup-close:hover { opacity: 1; color: var(--fg); }
/* Loading state: the popup's panel chrome (bg/border/shadow) gets out
of the way so only the whirlpool floats over the canvas. */
.ge-inpaint-popup.ge-inpaint-popup-loading {
background: transparent;
border-color: transparent;
box-shadow: none;
padding: 0;
min-width: 0;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Hide the controls wrapper entirely when every tool section inside is
hidden — otherwise the padding + border draws an empty band above the
layers panel even when no tool needs sliders/buttons. */
.ge-controls:not(:has(> *:not([style*="display: none"]):not([style*="display:none"]))) {
display: none;
}
.ge-controls input[type="range"] {
width: 100%;
box-sizing: border-box;
margin: 0;
display: block;
}
.ge-control-row {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--fg);
}
.ge-control-row label {
flex-shrink: 0;
opacity: 0.7;
min-width: 36px;
}
.ge-color-picker {
width: 24px;
height: 24px;
border: 1px solid var(--border);
border-radius: 50%;
padding: 0;
cursor: pointer;
background: none;
overflow: hidden;
}
/* Strip the native chrome (border + padding around
the swatch in each engine) so the visible color fills the circle. */
.ge-color-picker::-webkit-color-swatch-wrapper { padding: 0; }
.ge-color-picker::-webkit-color-swatch {
border: none;
border-radius: 50%;
}
.ge-color-picker::-moz-color-swatch {
border: none;
border-radius: 50%;
}
/* attachColorPicker swaps the native for a styled
text input with the same `.ge-color-picker` class plus
`.cp-swatch-input`. Force the 24×24 circular swatch back on it so
it doesn't render as a wide text box stretched across the row. */
.ge-color-picker.cp-swatch-input {
width: 24px;
height: 24px;
flex: 0 0 24px;
border: 1px solid var(--border);
border-radius: 50%;
padding: 0;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
/* Hide the (read-only) text caret + selection so the element reads
as a swatch, not a text field. */
color: transparent;
text-shadow: none;
caret-color: transparent;
font-size: 0;
user-select: none;
}
.ge-color-picker.cp-swatch-input::selection { background: transparent; }
.ge-color-picker.cp-swatch-input:focus {
outline: 1px solid var(--red);
outline-offset: 1px;
}
.ge-size-slider {
flex: 1 1 auto;
min-width: 0;
height: 8px;
accent-color: var(--red);
-webkit-appearance: none;
appearance: none;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
margin: 0;
}
.ge-size-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 9px; height: 9px;
border-radius: 50%;
background: var(--red);
border: none; cursor: pointer;
}
.ge-size-slider:hover::-webkit-slider-thumb,
.ge-size-slider.is-using::-webkit-slider-thumb,
.ge-size-slider:active::-webkit-slider-thumb {
width: 16px; height: 16px;
}
.ge-size-slider::-moz-range-thumb {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--red);
border: none; cursor: pointer;
}
.ge-size-slider:hover::-moz-range-thumb,
.ge-size-slider.is-using::-moz-range-thumb,
.ge-size-slider:active::-moz-range-thumb {
width: 16px; height: 16px;
}
.ge-size-label { font-size: 10px; opacity: 0.5; min-width: 28px; text-align: right; }
/* Eraser controls: tighter rows + a small brush preview on the left of
each label so the user can see what each slider affects. The three
previews share the same dot but each leans on a different rendering
trick (alpha / scatter / blur) to mirror its slider's effect. */
.ge-eraser-row {
display: flex !important;
align-items: center !important;
gap: 8px;
padding: 2px 0;
/* Lets the trailing value span pin to the row's right edge so it
stays visible when the slider expands left over the label. */
position: relative;
}
/* Inline value chip next to the label text. (Previously absolutely-
positioned over the slider track to ride the expand animation, which
caused overlap now that the dynamic-slider behavior is off for these.) */
.ge-eraser-row label > span[id$="-label"] {
margin-left: 4px;
padding: 0 4px;
border-radius: 3px;
cursor: text;
font-size: 10px;
opacity: 0.7;
font-variant-numeric: tabular-nums;
}
.ge-eraser-row label > span[id$="-label"]:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
/* When the value chip is pulled out of its label and placed AFTER the
slider (see DOM transform in galleryEditor.js init), it sits on the
row's right edge and naturally shortens the slider's flex space. */
.ge-eraser-row > span[id$="-label"].ge-slider-value {
flex: 0 0 auto;
margin: 0 0 0 6px;
padding: 0 4px;
font-size: 10px;
opacity: 0.7;
font-variant-numeric: tabular-nums;
min-width: 34px;
text-align: right;
cursor: text;
border-radius: 3px;
white-space: nowrap;
}
.ge-eraser-row > span[id$="-label"].ge-slider-value:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
opacity: 1;
}
/* Per-tool model selector row (lives at the top of each AI tool's
section in the side panel). Mirrors the inpaint Model row layout. */
.ge-tool-model-row {
display: flex !important;
align-items: center;
gap: 6px;
margin-bottom: 6px;
min-width: 0;
}
.ge-tool-model-row > label {
font-size: 11px;
opacity: 0.6;
flex: 0 0 auto;
}
.ge-tool-model {
flex: 1 1 0;
min-width: 0;
font-size: 10px;
padding: 2px 4px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
}
/* Section label inside a dropdown — small uppercase header. */
.dropdown-section-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.5;
font-weight: 600;
padding: 6px 8px 2px;
}
.dropdown-section-divider {
height: 1px;
background: var(--border);
margin: 4px 0;
opacity: 0.6;
}
/* Inline numeric editor that replaces the value chip on click. */
.ge-slider-edit {
position: absolute;
z-index: 11;
font: inherit;
font-size: 10px;
font-variant-numeric: tabular-nums;
padding: 1px 4px;
border: 1px solid var(--red);
border-radius: 3px;
background: var(--bg);
color: var(--fg);
outline: none;
text-align: center;
}
/* Merge dropdown — position is set by JS each time it opens (relative to
the button's bounding rect) because the parent .ge-layers has
overflow:hidden which would otherwise clip the popup. */
.ge-merge-menu {
position: fixed;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
padding: 3px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
z-index: 50;
display: flex;
flex-direction: column;
min-width: 140px;
}
.ge-merge-menu[hidden] { display: none; }
.ge-merge-menu .dropdown-item-compact {
background: none;
border: none;
width: 100%;
text-align: left;
padding: 5px 8px;
font-size: 11px;
gap: 8px;
font: inherit;
font-size: 11px;
}
/* Thin separator inside a tool section. */
.ge-section-divider {
border: 0;
border-top: 1px solid var(--border);
margin: 10px -2px;
opacity: 0.6;
}
/* Small explanatory paragraph under AI tool section labels. */
.ge-section-hint {
font-size: 10.5px;
line-height: 1.45;
opacity: 0.55;
margin: 0 0 8px;
}
/* Clone-tool hint swaps between "Alt-click" (desktop) and "Double-tap"
(mobile) so the on-screen instruction matches the actual gesture. */
.ge-clone-hint-mobile { display: none; }
@media (max-width: 820px) {
.ge-clone-hint-desktop { display: none; }
.ge-clone-hint-mobile { display: inline; }
}
/* Section title + adjacent "?" affordance — collapses the long
explanatory paragraph into a hoverable tooltip so the side panel
stays compact. */
.ge-section-title-with-help {
display: inline-flex;
align-items: center;
gap: 2px;
}
/* Drop the source-whitespace text node between the title text and the
? span so the chip hugs the title with no extra gap. */
.ge-section-title-with-help .ge-section-help { margin-left: 0; }
.ge-section-help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
background: color-mix(in srgb, var(--fg) 12%, transparent);
color: var(--fg-muted);
font-size: 9px;
font-weight: 700;
cursor: help;
user-select: none;
transition: background 0.12s, color 0.12s;
}
.ge-section-help:hover,
.ge-section-help:focus-visible {
background: color-mix(in srgb, var(--fg) 24%, transparent);
color: var(--fg);
outline: none;
}
/* Lightweight section title — used inside the inpaint panel to group
the mask actions (Eye / Invert / Clear) under a "Selection" header. */
.ge-section-title {
margin: 10px 0 4px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.5;
font-weight: 600;
}
/* Selection-row buttons stay compact so eye + invert + clear fit on
one line beside each other. */
.ge-inpaint-mask-row {
display: flex !important;
gap: 4px;
align-items: center;
}
.ge-inpaint-mask-row .ge-btn {
flex: 0 0 auto;
}
.ge-inpaint-mask-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.5;
font-weight: 600;
margin-right: 4px;
}
/* Fixed label column so the three sliders start at the same x and have
the same length when unused. Without this, "Flow" (4 chars) leaves
more room than "Opacity" (7 chars). */
.ge-eraser-row label {
font-size: 10px;
opacity: 0.55;
flex: 0 0 78px;
white-space: nowrap;
display: inline-flex;
justify-content: space-between;
gap: 4px;
}
/* Fixed-size slider track for all side-panel sliders — no dynamic
width/height expansion on drag. Only the thumb grows on interaction
(see ::-webkit-slider-thumb / ::-moz-range-thumb below). */
.ge-eraser-row input[type="range"] {
flex: 1 1 auto;
min-width: 0;
height: 8px;
accent-color: var(--red);
-webkit-appearance: none;
appearance: none;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
margin: 0;
position: relative;
z-index: 2;
}
.ge-eraser-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
}
.ge-eraser-row input[type="range"]:hover::-webkit-slider-thumb,
.ge-eraser-row input[type="range"].is-using::-webkit-slider-thumb,
.ge-eraser-row input[type="range"]:active::-webkit-slider-thumb {
width: 16px; height: 16px;
transition: width 0.12s ease 0s, height 0.12s ease 0s;
}
.ge-eraser-row input[type="range"]::-moz-range-thumb {
width: 9px; height: 9px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
}
.ge-eraser-row input[type="range"]:hover::-moz-range-thumb,
.ge-eraser-row input[type="range"].is-using::-moz-range-thumb,
.ge-eraser-row input[type="range"]:active::-moz-range-thumb {
width: 16px; height: 16px;
transition: width 0.12s ease 0s, height 0.12s ease 0s;
}
/* Inpaint Brush size slider — slightly larger thumb than the rest of
the panel since it's the primary interactive control. */
#ge-inpaint-brush-slider::-webkit-slider-thumb { width: 12px; height: 12px; }
#ge-inpaint-brush-slider:hover::-webkit-slider-thumb,
#ge-inpaint-brush-slider.is-using::-webkit-slider-thumb,
#ge-inpaint-brush-slider:active::-webkit-slider-thumb { width: 18px; height: 18px; }
#ge-inpaint-brush-slider::-moz-range-thumb { width: 12px; height: 12px; }
#ge-inpaint-brush-slider:hover::-moz-range-thumb,
#ge-inpaint-brush-slider.is-using::-moz-range-thumb,
#ge-inpaint-brush-slider:active::-moz-range-thumb { width: 18px; height: 18px; }
/* Help "?" icon next to a section title. Subtle by default, hover
brightens. Tooltip via native `title` attribute. */
.ge-section-help {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
margin-left: 4px;
font-size: 9px;
font-weight: 600;
border: 1px solid var(--border);
border-radius: 50%;
color: var(--fg-muted);
background: none;
cursor: help;
vertical-align: middle;
position: relative;
top: -1px;
opacity: 0.7;
transition: opacity 0.15s, color 0.15s, border-color 0.15s;
}
.ge-section-help:hover { opacity: 1; color: var(--fg); border-color: var(--fg-muted); }
/* Layer-FX popup — floating window bound to one layer. */
.ge-fx-popup {
position: fixed;
z-index: 300;
width: 320px;
max-height: 80vh;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 12px 32px rgba(0,0,0,0.45);
display: flex;
flex-direction: column;
font-size: 11px;
color: var(--fg);
}
.ge-fx-popup-head {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
font-weight: 600;
}
.ge-fx-popup-title { font-size: 12px; }
.ge-fx-popup-close {
background: none; border: none; color: var(--fg-muted);
font-size: 18px; line-height: 1; cursor: pointer; padding: 0 4px;
}
.ge-fx-popup-close:hover { color: var(--fg); }
.ge-fx-popup-body { padding: 8px 10px; overflow-y: auto; }
.ge-fx-popup-foot {
display: flex; gap: 6px; padding: 6px 10px;
border-top: 1px solid var(--border);
}
.ge-fx-group-title {
font-size: 10px; font-weight: 600; opacity: 0.6;
text-transform: uppercase; letter-spacing: 0.5px;
margin: 8px 0 4px;
}
.ge-fx-group-title:first-child { margin-top: 0; }
.ge-fx-cb-tone {
font-size: 10px; opacity: 0.55; font-style: italic;
margin: 4px 0 2px;
}
.ge-fx-row {
display: flex; align-items: center; gap: 6px;
padding: 2px 0;
}
.ge-fx-row-label {
font-size: 10px; opacity: 0.7;
flex: 0 0 110px; white-space: nowrap;
}
.ge-fx-row input[type="range"] {
flex: 1 1 auto; min-width: 0;
height: 6px; accent-color: var(--red);
-webkit-appearance: none; appearance: none;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
}
.ge-fx-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 11px; height: 11px; border-radius: 50%;
background: var(--red); border: none; cursor: pointer;
}
.ge-fx-row input[type="range"]::-moz-range-thumb {
width: 11px; height: 11px; border-radius: 50%;
background: var(--red); border: none; cursor: pointer;
}
.ge-fx-row-value {
font-size: 10px; opacity: 0.7; font-variant-numeric: tabular-nums;
flex: 0 0 40px; text-align: right;
}
/* Layer-row FX button — tinted when the layer has any non-identity FX. */
.ge-layer-fx-btn {
opacity: 0.55;
}
.ge-layer-fx-btn:hover { opacity: 0.9; }
.ge-layer-fx-btn.active {
opacity: 1;
color: var(--accent, var(--red));
}
/* Add-mask button on each layer row. Subtle by default; lights up red
when a lasso/wand selection is live so the user instantly sees that
clicking it will bake that selection into a mask on this layer. */
.ge-layer-mask-btn {
opacity: 0.45;
}
.ge-layer-mask-btn:hover { opacity: 0.9; }
.ge-layer-mask-btn.from-selection {
opacity: 1;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
}
/* Shared frosted-glass style for FX menu + adjustment popups. */
.ge-frosted {
background: color-mix(in srgb, var(--panel) 70%, transparent);
backdrop-filter: blur(14px) saturate(140%);
-webkit-backdrop-filter: blur(14px) saturate(140%);
border: 1px solid color-mix(in srgb, var(--fg) 12%, transparent);
box-shadow: 0 12px 40px rgba(0,0,0,0.55);
}
/* FX dropdown menu from the layer-row fx icon. */
.ge-fx-menu {
position: fixed;
z-index: 305;
min-width: 200px;
padding: 4px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 2px;
}
.ge-fx-menu-item {
background: none;
border: none;
text-align: left;
padding: 7px 10px;
font-size: 12px;
color: var(--fg);
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.ge-fx-menu-icon {
display: inline-flex;
align-items: center;
color: var(--fg-muted);
flex-shrink: 0;
}
.ge-fx-menu-item:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
}
.ge-fx-menu-item:hover .ge-fx-menu-icon { color: var(--fg); }
/* Per-type adjustment popup. */
.ge-adj-popup {
position: fixed;
z-index: 305;
width: 320px;
border-radius: 10px;
display: flex;
flex-direction: column;
font-size: 11px;
color: var(--fg);
}
.ge-adj-popup .ge-adj-head {
display: flex; align-items: center; gap: 6px;
padding: 8px 12px;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
cursor: grab;
user-select: none;
}
.ge-adj-popup .ge-adj-title { flex: 1 1 auto; }
.ge-adj-popup .ge-head-btns { margin-left: auto; }
.ge-adj-popup .ge-adj-head:active { cursor: grabbing; }
.ge-adj-icon {
display: inline-flex;
align-items: center;
color: var(--fg-muted);
flex-shrink: 0;
}
.ge-adj-title { font-weight: 600; font-size: 12px; flex: 1 1 auto; }
.ge-adj-min, .ge-adj-close, .ge-history-close {
background: none; border: none; color: var(--fg-muted);
font-size: 16px; line-height: 1; cursor: pointer; padding: 0 4px;
flex-shrink: 0;
}
.ge-history-close:hover { color: var(--fg); }
/* Mobile-only: shift head icons/buttons up and enlarge the minimise
glyph so it reads as a window-minimise affordance, not a centered
minus. Desktop keeps the original tight head styling. */
@media (max-width: 820px) {
.ge-adj-icon {
position: relative;
top: -4px;
}
.ge-adj-title {
position: relative;
top: -2px;
}
.ge-adj-min, .ge-adj-close, .ge-history-close {
position: relative;
top: -4px;
}
.ge-adj-min,
.ge-adj-popup .ge-adj-min,
.ge-history-head .ge-adj-min,
.ge-transform-popup-head .ge-adj-min {
font-size: 22px !important;
font-weight: 600 !important;
padding: 0 8px !important;
line-height: 0.6 !important;
position: relative;
top: 4px;
}
.ge-adj-head .ge-transform-aspect-btn {
position: relative;
top: -4px;
}
}
.ge-adj-min:hover, .ge-adj-close:hover { color: var(--fg); }
/* Floating dock at bottom-right for minimised FX popups. */
#ge-fx-dock {
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 12px;
z-index: 304;
display: flex;
flex-direction: row;
gap: 6px;
align-items: center;
pointer-events: none;
max-width: calc(100vw - 24px);
flex-wrap: wrap;
justify-content: center;
}
.ge-fx-dock-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 8px;
border-radius: 6px;
font-size: 11px;
color: var(--fg);
cursor: pointer;
pointer-events: auto;
max-width: 220px;
white-space: nowrap;
overflow: hidden;
}
.ge-fx-dock-chip:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.ge-fx-dock-icon { display: inline-flex; align-items: center; }
.ge-fx-dock-label { overflow: hidden; text-overflow: ellipsis; }
.ge-fx-dock-close {
margin-left: 2px; opacity: 0.55; padding: 0 2px;
font-size: 14px; line-height: 1;
}
.ge-fx-dock-close:hover { opacity: 1; color: var(--red); }
/* Sub-layer row icon in the layer panel. */
.ge-adj-sub-name {
display: inline-flex !important;
align-items: center;
gap: 4px;
}
.ge-adj-sub-icon {
display: inline-flex; align-items: center;
color: var(--accent, var(--red));
flex-shrink: 0;
}
.ge-adj-sub-icon svg { width: 11px; height: 11px; }
/* History panel — labeled timeline of edits. */
#ge-history-panel {
position: fixed;
z-index: 10004;
width: 240px;
max-height: 60vh;
border-radius: 8px;
display: flex;
flex-direction: column;
color: var(--fg);
font-size: 11px;
}
.ge-history-head {
display: flex; align-items: center; gap: 6px;
padding: 8px 10px;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
cursor: grab;
user-select: none;
}
.ge-history-head:active { cursor: grabbing; }
.ge-history-title { font-weight: 600; font-size: 12px; flex: 1 1 auto; }
.ge-history-close {
background: none; border: none; color: var(--fg-muted);
font-size: 16px; line-height: 1; cursor: pointer; padding: 0 4px;
flex-shrink: 0;
}
.ge-history-close:hover { color: var(--fg); }
.ge-history-list { overflow-y: auto; padding: 4px 0; }
.ge-history-row {
display: flex; align-items: center; gap: 8px;
width: 100%;
padding: 5px 10px;
background: none; border: none;
text-align: left; cursor: pointer;
color: var(--fg); font-size: 11px;
}
.ge-history-row:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.ge-history-row.current { background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
.ge-history-row.current .ge-history-row-dot { background: var(--accent, var(--red)); }
.ge-history-row.future { opacity: 0.45; }
.ge-history-row-dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--fg-muted);
flex-shrink: 0;
}
.ge-history-row-label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ge-history-row-time { font-size: 9px; opacity: 0.55; flex-shrink: 0; }
/* Tight button group in popup heads — min + close sit immediately next
to each other with no inherited flex gap, pushed to the right edge. */
.ge-head-btns {
display: inline-flex;
align-items: center;
gap: 0;
margin-left: auto;
flex-shrink: 0;
}
.ge-head-btns > button {
width: 26px;
height: 26px;
padding: 0 !important;
margin: 0;
font-size: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
}
.ge-head-btns > button:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
}
/* Detail-header heart toggle — sits next to the icon Edit button. */
.gallery-detail-fav-header { opacity: 0.7; transition: opacity 0.15s, color 0.15s; }
.gallery-detail-fav-header:hover { opacity: 1; }
.gallery-detail-fav-header.active { color: var(--red); opacity: 1; }
/* Canvas loading overlay — covers the canvas area while a blocking op
(rotation, big resize, etc.) runs. */
.ge-canvas-loading {
position: absolute;
inset: 0;
z-index: 50;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 8px;
pointer-events: auto;
}
.ge-canvas-loading-spinner {
width: 32px; height: 32px;
border: 3px solid color-mix(in srgb, var(--fg) 25%, transparent);
border-top-color: var(--accent, var(--red));
border-radius: 50%;
animation: ge-canvas-spin 0.8s linear infinite;
}
.ge-canvas-loading-msg { font-size: 12px; opacity: 0.85; }
@keyframes ge-canvas-spin { to { transform: rotate(360deg); } }
/* Missing-dependency notice in tool sections (Bg Remove etc.). */
.ge-dep-notice {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
border-radius: 6px;
margin: 4px 0 8px;
font-size: 11px;
}
.ge-dep-notice-text { flex: 1 1 auto; line-height: 1.4; }
.ge-dep-notice-text strong { display: block; margin-bottom: 2px; }
.ge-dep-notice-text code {
font-family: 'Fira Code', monospace;
font-size: 10px;
padding: 1px 4px;
border-radius: 3px;
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.ge-dep-notice .ge-btn { flex-shrink: 0; }
/* Brief highlight on a Cookbook deps row arrived-at via deep-link. */
.cookbook-pkg-flash { animation: cookbook-pkg-flash 1.4s ease-out; }
@keyframes cookbook-pkg-flash {
0% { background: color-mix(in srgb, var(--accent, var(--red)) 30%, transparent); }
100% { background: transparent; }
}
/* Flash highlight when an anchor-link ([Name](#task-/#skill-/#research-))
opens a panel and focuses a specific item. */
@keyframes anchor-item-flash {
0% { background: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent); }
60% { background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent); }
100% { background: transparent; }
}
.task-card-flash,
.skill-row-flash,
.research-card-flash {
animation: anchor-item-flash 2s ease-out;
border-radius: 6px;
}
/* Preview dot icons for the Blur sub-menu — visual cue for each blur
type (Gaussian = soft round, Zoom = radial streaks). */
.ge-blur-icon {
width: 14px;
height: 14px;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
background: var(--fg);
}
.ge-blur-icon.ge-blur-gaussian {
background: radial-gradient(circle, var(--fg) 0%, var(--fg) 25%, transparent 75%);
filter: blur(1.5px);
}
.ge-blur-icon.ge-blur-zoom {
background: radial-gradient(circle, var(--fg) 0%, var(--fg) 20%, transparent 60%);
box-shadow:
0 -5px 0 -3px var(--fg),
0 5px 0 -3px var(--fg),
-5px 0 0 -3px var(--fg),
5px 0 0 -3px var(--fg),
-4px -4px 0 -3px var(--fg),
4px -4px 0 -3px var(--fg),
-4px 4px 0 -3px var(--fg),
4px 4px 0 -3px var(--fg);
}
/* Inline-swatch color picker — small square that fits next to a title. */
.ge-inline-swatch {
width: 16px !important;
height: 16px !important;
padding: 0 !important;
border: 1px solid var(--border) !important;
border-radius: 3px !important;
cursor: pointer;
flex-shrink: 0;
-webkit-appearance: none;
appearance: none;
background: none;
}
.ge-inline-swatch::-webkit-color-swatch-wrapper { padding: 0; }
.ge-inline-swatch::-webkit-color-swatch { border: none; border-radius: 2px; }
.ge-inline-swatch::-moz-color-swatch { border: none; border-radius: 2px; }
/* Drop overlay over the gallery editor while a file drag is in flight. */
.ge-drop-overlay {
position: absolute;
inset: 0;
z-index: 200;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border: 3px dashed color-mix(in srgb, var(--accent, var(--red)) 80%, transparent);
border-radius: 8px;
}
.ge-drop-overlay-msg {
padding: 12px 20px;
font-size: 14px;
font-weight: 600;
color: var(--fg);
background: color-mix(in srgb, var(--panel) 70%, transparent);
backdrop-filter: blur(10px);
border-radius: 8px;
}
/* Small buttons that show an SVG icon + text label side-by-side. */
.ge-btn-iconlabel {
display: inline-flex !important;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.ge-btn-iconlabel svg {
flex-shrink: 0;
opacity: 0.85;
}
.ge-btn-iconlabel:hover svg { opacity: 1; }
.ge-adj-body {
padding: 8px 12px;
max-height: 60vh;
overflow-y: auto;
overflow-x: hidden;
}
.ge-adj-popup { box-sizing: border-box; overflow: hidden; }
.ge-adj-foot {
display: flex; gap: 6px; justify-content: flex-end;
padding: 8px 12px;
border-top: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
}
/* Mobile-only: oversize the foot Cancel/Apply buttons to match the
Transform popup. Desktop keeps the original ge-btn-sm sizing. */
@media (max-width: 820px) {
.ge-adj-foot .ge-adj-cancel-btn,
.ge-adj-foot .ge-adj-apply-btn {
padding: 8px 16px !important;
font-size: 13px !important;
min-width: 80px;
text-align: center;
}
}
.ge-adj-row {
display: flex; align-items: center; gap: 3px;
padding: 0;
margin: 0;
min-width: 0;
line-height: 1;
}
.ge-adj-row > label {
flex: 0 0 96px;
font-size: 10px;
opacity: 0.7;
}
.ge-adj-row input[type="range"] {
flex: 1 1 auto;
min-width: 0;
height: 6px;
accent-color: var(--red);
-webkit-appearance: none; appearance: none;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
}
/* Mobile-only: fixed-size sliders + taller rows so the thumb growing
on active doesn't push the row height around. */
@media (max-width: 820px) {
.ge-adj-row {
min-height: 28px;
}
.ge-adj-row input[type="range"] {
flex: 0 0 160px;
width: 160px;
min-width: 160px;
max-width: 160px;
}
}
.ge-adj-row input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--red);
cursor: pointer; border: none;
transition: width 0.12s ease, height 0.12s ease;
}
.ge-adj-row input[type="range"]:hover::-webkit-slider-thumb,
.ge-adj-row input[type="range"]:active::-webkit-slider-thumb {
width: 18px; height: 18px;
}
.ge-adj-row input[type="range"]::-moz-range-thumb {
width: 12px; height: 12px;
border-radius: 50%; background: var(--red); border: none; cursor: pointer;
transition: width 0.12s ease, height 0.12s ease;
}
.ge-adj-row input[type="range"]:hover::-moz-range-thumb,
.ge-adj-row input[type="range"]:active::-moz-range-thumb {
width: 18px; height: 18px;
}
/* Value chip — fixed-width column, text aligned LEFT so the chip's
left edge always sits immediately after the slider regardless of
value length. Without a fixed width Hue's "180 °" made its slider
shorter than Saturation's "100" since slider was flex-1. */
.ge-adj-value {
flex: 0 0 34px;
margin-left: 0;
padding: 0 2px;
font-size: 10px; opacity: 0.7; text-align: left;
font-variant-numeric: tabular-nums;
white-space: nowrap;
cursor: text;
}
.ge-adj-value:hover { opacity: 1; }
.ge-adj-cb-tone {
font-size: 10px; opacity: 0.55; font-style: italic;
margin: 2px 0 0;
line-height: 1;
}
.ge-adj-cb-row {
padding: 0;
gap: 3px;
}
.ge-adj-cb-row .ge-adj-cb-dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block; flex-shrink: 0;
box-shadow: 0 0 0 1px color-mix(in srgb, var(--fg) 20%, transparent);
}
.ge-adj-cb-tone-picker {
margin: 0 0 8px;
}
.ge-adj-cb-tone-select {
width: 100%;
padding: 6px 8px;
font-size: 12px;
border-radius: 6px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 90%, transparent);
color: var(--fg);
}
.ge-adj-cb-tone-select:focus { outline: none; border-color: var(--red); }
.ge-adj-hist-details {
margin-bottom: 8px;
}
.ge-adj-hist-details > summary {
font-size: 11px;
color: var(--fg-muted);
cursor: pointer;
padding: 4px 0;
list-style: none;
user-select: none;
}
.ge-adj-hist-details > summary::-webkit-details-marker { display: none; }
.ge-adj-hist-details > summary::before {
content: '▸ ';
display: inline-block;
transition: transform 0.12s;
}
.ge-adj-hist-details[open] > summary::before {
content: '▾ ';
}
.ge-adj-hist-details .ge-adj-hist-wrap {
margin-top: 4px;
}
.ge-adj-histogram {
display: block;
width: 100%;
height: 80px;
border-radius: 4px;
background: rgba(0,0,0,0.25);
}
.ge-adj-hist-wrap {
position: relative;
margin-bottom: 14px;
}
.ge-adj-hist-handles {
position: absolute;
left: 0; right: 0;
bottom: -10px;
height: 12px;
pointer-events: none;
}
.ge-adj-hist-handle {
position: absolute;
width: 12px;
height: 12px;
cursor: ew-resize;
pointer-events: auto;
border-style: solid;
border-width: 0 6px 10px 6px;
border-color: transparent transparent var(--fg-muted) transparent;
filter: drop-shadow(0 1px 1px rgba(0,0,0,0.4));
}
.ge-adj-hist-handle:hover { border-bottom-color: var(--fg); }
.ge-adj-hist-handle.hist-h-black { border-bottom-color: #111; }
.ge-adj-hist-handle.hist-h-white { border-bottom-color: #fff; }
.ge-adj-hist-handle.hist-h-gamma { border-bottom-color: var(--accent, var(--red)); }
/* Per-slider revert button. */
.ge-adj-revert {
background: none; border: none;
color: var(--fg-muted); cursor: pointer;
padding: 2px 4px; margin-left: 2px;
opacity: 0.55;
display: inline-flex; align-items: center; justify-content: center;
transition: opacity 0.15s, color 0.15s;
}
.ge-adj-revert:hover { opacity: 1; color: var(--fg); }
/* Adjustment sub-layer row in the layer panel — indented under parent. */
.ge-adj-sub-item {
padding-left: 18px;
opacity: 0.85;
border-left: 2px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
margin-left: 6px;
font-size: 10.5px;
}
.ge-adj-sub-item .ge-layer-name {
opacity: 0.85;
font-style: italic;
}
/* Mask sub-layer rows. Same indent as adj sub-layers but the
highlight bar uses fg-muted instead of accent — these aren't FX
effects, just selection-state. When activated (the click target
for paint / inpaint / generate), border + name go full strength. */
.ge-mask-sub-item {
border-left-color: color-mix(in srgb, var(--fg) 30%, transparent);
}
.ge-mask-sub-item.active {
border-left-color: var(--red);
opacity: 1;
}
.ge-mask-sub-item.active .ge-layer-name { opacity: 1; font-style: normal; font-weight: 600; }
.ge-eraser-preview {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--fg);
flex-shrink: 0;
display: inline-block;
position: relative;
}
/* Opacity preview — translucent disk */
#ge-eraser-preview-opacity { opacity: 0.5; }
/* Flow preview — dotted ring to suggest discrete stamping */
#ge-eraser-preview-flow {
background: none;
border: 2px dotted var(--fg);
opacity: 0.7;
width: 12px;
height: 12px;
}
/* Softness preview — blurred edge */
#ge-eraser-preview-softness {
background: radial-gradient(circle, var(--fg) 0%, var(--fg) 30%, transparent 75%);
opacity: 0.7;
}
/* Wand feather preview — solid disk that visibly softens at its edge
as the user drags the Feather slider. JS sets --feather-blur. */
#ge-wand-feather-preview {
filter: blur(var(--feather-blur, 0px));
opacity: 0.8;
}
.ge-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.ge-btn {
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font-size: 11px;
cursor: pointer;
transition: background 0.15s;
}
.ge-btn:hover { background: color-mix(in srgb, var(--fg) 10%, var(--bg)); }
.ge-btn.active {
background: color-mix(in srgb, var(--accent, var(--red)) 16%, var(--bg));
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
.ge-btn-sm { padding: 3px 6px; font-size: 10px; }
/* Busy state on AI tool buttons — whirlpool + verb text side by side. */
.ge-btn-processing {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.ge-btn-busy-label { font-size: inherit; line-height: 1; }
/* Mask-vis eye button is icon-only — strip the inherited .ge-btn
background / border so it reads as a plain affordance, matching
the layer-row eye toggles. */
.ge-mask-vis-btn {
background: none !important;
border: none !important;
padding: 2px 4px !important;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.5;
transition: opacity 0.12s;
}
.ge-mask-vis-btn:hover { opacity: 0.85; background: none !important; }
.ge-mask-vis-btn.visible { opacity: 0.9; }
.ge-mask-vis-btn.visible:hover { opacity: 1; }
/* Paint/Erase segmented toggle in the Inpaint Brush section. */
.ge-inpaint-mode-btn {
font-size: 11px;
padding: 4px 8px;
opacity: 0.6;
border: 1px solid var(--border);
}
.ge-inpaint-mode-btn:hover { opacity: 0.85; }
.ge-inpaint-mode-btn.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
font-weight: 600;
}
/* Wand New / + Add / − Subtract segmented toggle. */
.ge-wand-mode-btn {
flex: 1 1 0;
font-size: 11px;
padding: 4px 6px;
opacity: 0.6;
border: 1px solid var(--border);
}
.ge-wand-mode-btn:hover { opacity: 0.85; }
.ge-wand-mode-btn.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
font-weight: 600;
}
.ge-wand-live-btn {
flex: 0 0 auto;
opacity: 0.6;
min-width: 46px;
height: 22px;
padding: 0 10px !important;
border-radius: 999px !important;
font-size: 10px;
line-height: 1;
background: color-mix(in srgb, var(--fg) 5%, transparent);
transition: opacity 0.15s, color 0.15s, border-color 0.15s, background 0.15s;
}
.ge-wand-live-btn:hover { opacity: 0.85; }
.ge-wand-live-btn.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
font-weight: 600;
}
/* Top-bar buttons (undo/redo, zoom +/-, Fit, 1:1, Resize, Edge, Import,
Download, Save) — visually their glyphs sit too low compared to the
surrounding labels, so shift contents up 2 px via asymmetric padding. */
/* Visually the buttons sit lower than the surrounding text labels on the
topbar. Keep the box padding symmetric but lift the whole box 2 px so it
centers against the adjacent label baselines. */
.ge-topbar .ge-btn,
.ge-topbar .ge-btn-sm,
.ge-topbar select,
.ge-topbar input { position: relative; top: -2px; }
/* The tiny "Gen" / "Inpaint" inline labels now look too high relative to
the shifted-up controls — nudge them down 2 px to re-align. */
.ge-topbar span[style*="font-size:9px"] { position: relative; top: 2px; }
/* The Gen and Inpaint model selects — must visually sit on the same line as
the small "Gen" / "Inpaint" labels (which are at top:2px). Use !important
so this beats the generic `.ge-topbar select` rule above no matter what
element-vs-class specificity quirks. */
.ge-topbar .ge-ai-model { position: relative !important; top: 1px !important; }
/* The ◢ glyph baseline reads lower than the surrounding text — shift it
up 1 px so it visually centers with the "Edge" label. */
.ge-edge-glyph { position: relative; top: -1px; }
/* Flex-line-break helper used in the mobile transform popup. Hidden by
default so desktop keeps its single-row layout. */
.ge-row-break { display: none; }
/* Stacked button — glyph on top, small uppercase label below. Currently
used for Undo/Redo so users get an obvious "UNDO" / "REDO" affordance
label without losing the compact icon. */
.ge-stacked-btn {
display: inline-flex !important;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
padding: 2px 8px !important;
line-height: 1;
}
.ge-stacked-btn .ge-stacked-glyph {
font-size: 14px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
height: 14px;
}
.ge-stacked-btn .ge-stacked-glyph svg { display: block; }
.ge-stacked-btn .ge-stacked-label {
font-size: 8px;
letter-spacing: 0.06em;
opacity: 0.65;
font-weight: 600;
position: relative;
top: 2px;
}
/* Zoom % indicator — magnifier glyph on top, percentage label below.
Matches the Undo/Redo stacked-button layout so the topbar reads as
one consistent row of stacked icons. */
.ge-zoom-stack {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
line-height: 1;
padding: 0 4px;
}
.ge-zoom-stack .ge-zoom-glyph {
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
/* Glyph nudged 1 px right (was 2 px left). */
position: relative;
left: -1px;
}
.ge-zoom-stack .ge-zoom-glyph svg {
width: 16px;
height: 16px;
}
.ge-zoom-stack .ge-zoom-label {
font-size: 8px;
letter-spacing: 0.06em;
opacity: 0.65;
font-weight: 600;
position: relative;
top: 4px;
left: 1px;
}
/* + Add sits 1 px below the title baseline to match the surrounding
header buttons after the recent header restructure. */
.ge-layers-header #ge-add-layer { position: relative; top: 1px; }
/* Layer-row buttons in the right panel — the unicode glyphs (◉, ⤢) have
irregular baseline metrics so the box reads as too low; shift up 2 px. */
.ge-layer-vis,
.ge-layer-btn { position: relative; top: -2px; }
/* Delete (×) button glyph baseline reads even lower than the visibility
icon — push it up an extra 2 px so it visually centers. */
.ge-layer-btn.danger { top: -4px; }
/* Save / Save as new / Download dropdown anchored to the primary Save
button in the topbar. */
.ge-save-wrap { position: relative; display: inline-block; }
.ge-save-menu {
position: fixed;
min-width: 160px;
padding: 4px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
z-index: 10005;
display: flex;
flex-direction: column;
gap: 2px;
}
.ge-save-menu[hidden] { display: none; }
.ge-save-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
}
/* Resize and Edge popups in the topbar — same anchored-dropdown pattern as
the Save menu. Resize lists preset canvas sizes; Edge is a tiny form.
Image + Filter menus reuse the same chrome. */
.ge-resize-wrap,
.ge-edge-wrap,
.ge-image-wrap,
.ge-filter-wrap { position: relative; display: inline-block; }
.ge-resize-menu,
.ge-edge-menu,
.ge-image-menu,
.ge-filter-menu {
position: absolute;
top: calc(100% + 2px);
right: 0;
padding: 4px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
z-index: 12;
display: flex;
flex-direction: column;
gap: 2px;
}
/* Mobile: escape the topbar's stacking context (z 5) so the dropdown
lands above the bottom-sheet panels (z 50 / 60). Switch to fixed and
pin under the topbar. */
@media (max-width: 820px) {
.ge-resize-menu,
.ge-edge-menu,
.ge-image-menu,
.ge-filter-menu {
position: fixed;
top: 44px;
right: 8px;
z-index: 500;
max-width: calc(100vw - 16px);
}
}
.ge-resize-menu { min-width: 200px; }
.ge-edge-menu { min-width: 220px; padding: 10px; gap: 6px; }
.ge-image-menu { min-width: 180px; }
.ge-filter-menu { min-width: 180px; }
.ge-resize-menu[hidden],
.ge-edge-menu[hidden],
.ge-image-menu[hidden],
.ge-filter-menu[hidden] { display: none; }
.ge-resize-menu .dropdown-item-compact,
.ge-image-menu .dropdown-item-compact,
.ge-filter-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
}
/* Sub-menu label inside the Filter / Image dropdown — small uppercase
header so "Blur" / "Selection" reads as a category, not an action. */
.ge-filter-submenu-label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.08em;
opacity: 0.45;
font-weight: 600;
padding: 4px 8px 2px;
pointer-events: none;
}
/* Disabled dropdown items (e.g. Fill when there's no selection) read
as muted + non-interactive. */
.ge-image-menu .dropdown-item-compact:disabled,
.ge-filter-menu .dropdown-item-compact:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Filter live-preview modal — translucent backdrop over the editor
with a frosted parameter panel anchored top-right. The active layer
re-renders live as the user drags the sliders. */
.ge-filter-overlay {
position: absolute;
inset: 0;
z-index: 14;
display: flex;
align-items: flex-start;
justify-content: flex-end;
padding: 60px 20px 20px;
background: rgba(0, 0, 0, 0.18);
pointer-events: auto;
}
.ge-filter-modal {
width: 240px;
background: color-mix(in srgb, var(--panel) 95%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
backdrop-filter: blur(10px);
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.ge-filter-modal-head {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.7;
}
.ge-filter-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.ge-filter-row label {
font-size: 10px;
opacity: 0.7;
display: flex;
justify-content: space-between;
align-items: center;
}
.ge-filter-row-value {
font-variant-numeric: tabular-nums;
font-weight: 600;
opacity: 0.85;
}
.ge-filter-row input[type="range"] {
width: 100%;
}
.ge-filter-modal-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 4px;
}
.ge-menu-divider {
height: 1px;
background: var(--border);
margin: 4px 0;
}
.ge-edge-form {
display: flex;
flex-direction: column;
gap: 8px;
}
.ge-edge-label {
font-size: 11px;
opacity: 0.65;
}
.ge-edge-input {
padding: 6px 8px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 12px;
}
.ge-edge-input:focus { outline: none; border-color: var(--red); }
.ge-edge-actions {
display: flex;
gap: 6px;
}
.ge-edge-actions .ge-btn { flex: 1; }
.ge-edge-hint {
margin: 0;
font-size: 10px;
opacity: 0.5;
line-height: 1.4;
}
/* Styled "New canvas" prompt — replaces the native browser prompt(). */
.ge-canvas-prompt {
max-width: 360px;
width: 90vw;
}
.ge-canvas-prompt .modal-header h4 { margin: 0; font-size: 14px; }
.ge-canvas-prompt-row {
display: flex;
align-items: flex-end;
gap: 10px;
justify-content: center;
margin: 6px 0 2px;
}
.ge-canvas-prompt-field {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
min-width: 0;
}
.ge-canvas-prompt-field > span {
font-size: 11px;
opacity: 0.65;
}
.ge-canvas-prompt-field input {
width: 100%;
padding: 8px 10px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font: inherit;
font-size: 14px;
}
.ge-canvas-prompt-field input:focus { outline: none; border-color: var(--red); }
.ge-canvas-prompt-x {
font-size: 18px;
opacity: 0.4;
align-self: center;
padding-bottom: 8px;
}
.ge-canvas-prompt-hint {
margin: 12px 0 0;
font-size: 11px;
opacity: 0.5;
text-align: center;
}
.ge-btn-primary {
background: var(--red);
color: #fff;
border-color: var(--red);
font-weight: 600;
}
.ge-btn-primary:hover { background: color-mix(in srgb, var(--red) 85%, #000); }
.ge-zoom-label { font-size: 10px; opacity: 0.5; }
/* Nudge whirlpool spinners 1px down so they sit on the text baseline
when placed inside action buttons (Generate, Harmonize, etc). */
.ge-btn .ai-spinner-whirlpool,
.ge-btn .spinner-whirlpool,
button .ai-spinner-whirlpool,
button .spinner-whirlpool {
position: relative;
top: 1px;
}
/* Layers panel */
.ge-layers {
/* Natural height now that the whole right panel scrolls — flex:1
would collapse the list inside a scrolling parent. */
flex: 0 0 auto;
display: flex;
flex-direction: column;
}
/* Vertical drag handle that sits on the LEFT edge of the right panel.
Drag horizontally to widen / narrow the entire side panel. Slim
by default; brighter on hover so the user notices it's interactive. */
.ge-panel-resize {
position: absolute;
left: -3px;
top: 0;
bottom: 0;
width: 6px;
cursor: ew-resize;
background: transparent;
z-index: 5;
transition: background 0.12s;
}
.ge-panel-resize:hover,
.ge-panel-resize:active {
background: color-mix(in srgb, var(--red) 30%, transparent);
}
/* Center grip dots so the handle reads as draggable. */
.ge-panel-resize::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 2px;
height: 24px;
background: color-mix(in srgb, var(--fg) 35%, transparent);
border-radius: 2px;
}
/* The right panel needs `position: relative` so the handle (absolute)
anchors to it instead of the viewport. */
.ge-right-panel { position: relative; }
/* Floating value bubble that pops above the slider thumb while
dragging. Solves the "I can't see the number" problem when the
slider's opaque plate covers the row's right-edge value chip. */
.ge-slider-bubble {
/* Fixed positioning so the bubble escapes any overflow:hidden/auto
on the slider's ancestor chain (the layers list clips, and used
to swallow the bubble entirely). JS sets viewport-relative
left/top; the translate centers and lifts the bubble above the
slider thumb. */
position: fixed;
transform: translate(-50%, -100%);
z-index: 10000;
padding: 5px 10px;
background: var(--red);
color: #fff;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
border-radius: 6px;
box-shadow: 0 3px 10px color-mix(in srgb, var(--red) 40%, transparent);
pointer-events: none;
white-space: nowrap;
opacity: 0;
transition: opacity 0.1s;
}
.ge-slider-bubble[hidden] { display: none; }
.ge-slider-bubble.visible { opacity: 1; }
/* Tail pointing DOWN at the slider (bubble sits above). */
.ge-slider-bubble::after {
content: '';
position: absolute;
left: 50%;
bottom: -4px;
transform: translateX(-50%);
width: 8px;
height: 8px;
background: var(--red);
clip-path: polygon(0 0, 100% 0, 50% 100%);
}
/* Floating thumbnail shown when hovering a layer row. */
.ge-layer-thumb {
position: fixed;
z-index: 60;
padding: 4px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 6px 18px color-mix(in srgb, var(--fg) 30%, transparent);
pointer-events: none;
display: none;
}
.ge-layer-thumb canvas {
display: block;
border-radius: 4px;
}
.ge-layers-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
color: var(--fg);
opacity: 0.8;
border-bottom: 1px solid var(--border);
position: relative;
}
.ge-layers-title { flex: 1; }
.ge-layers-grab { display: none; } /* mobile only */
.ge-layers-merge-wrap { position: relative; display: inline-flex; }
.ge-layers-merge-wrap #ge-layers-merge-btn { position: relative; top: 0; }
.ge-layers-merge-menu {
/* Fixed positioning so the menu escapes the layers panel's overflow
clipping (which made it look transparent). JS sets exact coords
via getBoundingClientRect on the trigger button at open time. */
position: fixed;
z-index: 200;
min-width: 160px;
padding: 4px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
display: flex;
flex-direction: column;
gap: 2px;
pointer-events: auto;
}
.ge-layers-merge-menu[hidden] { display: none; }
.ge-layers-merge-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
}
.ge-layers-actions {
display: flex;
gap: 4px;
padding: 6px 8px;
border-top: 1px solid var(--border);
flex-wrap: wrap;
/* Pin the action row to the bottom of the right panel's scroll
viewport. Without this, when the tool-controls section is tall
enough to overflow the panel, the merge buttons get pushed below
the fold and require scrolling to reach. */
position: sticky;
bottom: 0;
background: var(--panel);
z-index: 4;
/* Centered cluster instead of stretching the buttons to fill the
panel width. */
justify-content: center;
}
/* Merge / flatten buttons sit content-sized so the cluster reads as a
centered group of three, not three stretched bars across the panel. */
.ge-layers-actions .ge-btn {
flex: 0 0 auto;
min-width: 0;
padding: 4px 8px;
font-size: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
}
.ge-layers-actions .ge-icon-btn {
/* Icon-only buttons stay narrow — no risk of label overflow. */
flex: 0 0 auto;
padding: 4px 8px;
}
.ge-layers-actions .ge-btn svg {
flex-shrink: 0;
margin-right: 0 !important;
}
.ge-layers-list {
/* Cap the list so a huge layer stack scrolls within its own box
rather than pushing the whole panel arbitrarily tall. */
max-height: 320px;
overflow-y: auto;
padding: 0;
}
.ge-layer-item {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 8px;
cursor: pointer;
border-left: 2px solid transparent;
transition: background 0.1s;
font-size: 11px;
color: var(--fg);
}
/* Row-hover tint removed — kept the layer row's background stable so
it doesn't shift visually as the user reaches for the opacity slider. */
.ge-layer-item.active {
background: color-mix(in srgb, var(--red) 8%, transparent);
border-left-color: var(--red);
}
/* "Active parent" — the layer is selected, but a mask sub-layer below
it is the actual paint target. Soft hint so the user can see which
parent owns the active mask without competing with the mask's
strong red highlight. */
.ge-layer-item.active-parent {
background: color-mix(in srgb, var(--fg) 4%, transparent);
border-left-color: color-mix(in srgb, var(--fg) 30%, transparent);
}
.ge-layer-vis {
border: none;
background: none;
color: var(--fg);
cursor: pointer;
padding: 2px 4px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.4;
transition: opacity 0.12s;
}
.ge-layer-vis:hover { opacity: 0.8; }
.ge-layer-vis.visible { opacity: 0.9; }
.ge-layer-vis.visible:hover { opacity: 1; }
.ge-layer-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.ge-layer-name-input {
flex: 1;
background: var(--bg);
border: 1px solid var(--border);
color: var(--fg);
font-size: 11px;
padding: 1px 4px;
border-radius: 3px;
min-width: 0;
}
/* All sliders inside the image editor share the same visual language as
the layer opacity slider — rounded pill, dim track, red thumb. Track
AND thumb grow when interacting for finer tweaking. */
.gallery-editor-container input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 4px;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
accent-color: var(--red);
cursor: pointer;
transition: height 0.15s ease;
}
.gallery-editor-container input[type="range"]:hover,
.gallery-editor-container input[type="range"]:focus,
.gallery-editor-container input[type="range"]:active {
height: 10px;
}
.gallery-editor-container input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease, height 0.12s ease;
}
.gallery-editor-container input[type="range"]::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease, height 0.12s ease;
}
.gallery-editor-container input[type="range"]:hover::-webkit-slider-thumb,
.gallery-editor-container input[type="range"]:focus::-webkit-slider-thumb,
.gallery-editor-container input[type="range"]:active::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
.gallery-editor-container input[type="range"]:hover::-moz-range-thumb,
.gallery-editor-container input[type="range"]:focus::-moz-range-thumb,
.gallery-editor-container input[type="range"]:active::-moz-range-thumb {
width: 18px;
height: 18px;
}
/* Layer-row opacity slider — fixed track, only thumb grows on
interaction. Takes a bit of row space at rest, but no jumpy expand
animation. */
.ge-layer-opacity {
width: 70px;
height: 6px;
accent-color: var(--red);
flex-shrink: 0;
-webkit-appearance: none;
appearance: none;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
position: relative;
top: 0;
}
.ge-layer-opacity:hover::-webkit-slider-thumb,
.ge-layer-opacity:active::-webkit-slider-thumb,
.ge-layer-opacity.dragging::-webkit-slider-thumb {
width: 18px;
height: 18px;
}
.ge-layer-opacity:hover::-moz-range-thumb,
.ge-layer-opacity:active::-moz-range-thumb,
.ge-layer-opacity.dragging::-moz-range-thumb {
width: 18px;
height: 18px;
}
/* Shrink the slider thumb — the native default is ~16-20 px which looks
massive next to a 3 px track in the compact layer row. */
.ge-layer-opacity::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
/* Match the track: shrink only after a 0.5s grace period. */
transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
}
.ge-layer-opacity::-moz-range-thumb {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease 0.5s, height 0.12s ease 0.5s;
}
.ge-layer-opacity:hover::-webkit-slider-thumb,
.ge-layer-opacity:active::-webkit-slider-thumb,
.ge-layer-opacity.dragging::-webkit-slider-thumb,
.ge-layer-opacity:hover::-moz-range-thumb,
.ge-layer-opacity:active::-moz-range-thumb,
.ge-layer-opacity.dragging::-moz-range-thumb {
transition: width 0.12s ease 0s, height 0.12s ease 0s;
}
.ge-layer-controls {
display: flex;
gap: 2px;
flex-shrink: 0;
}
/* Left-edge drag handle on each layer row. */
.ge-layer-drag {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
margin-right: 2px;
opacity: 0.4;
cursor: grab;
flex-shrink: 0;
}
.ge-layer-drag:hover { opacity: 0.85; }
/* While dragSort lifts the item via absolute positioning, the placeholder
slot shows where it'll land — give it a subtle red outline so users see
the target clearly. */
.ge-layers-list .drag-placeholder {
background: color-mix(in srgb, var(--red) 12%, transparent);
border: 1px dashed color-mix(in srgb, var(--red) 50%, transparent);
border-radius: 4px;
box-sizing: border-box;
}
.ge-layer-item.dragging {
background: var(--panel);
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 4px;
}
.ge-layer-btn {
border: none;
background: none;
color: var(--fg);
cursor: pointer;
font-size: 12px;
padding: 0 2px;
opacity: 0.4;
transition: opacity 0.1s;
}
.ge-layer-btn:hover { opacity: 0.8; }
.ge-layer-btn.danger:hover { color: var(--red); opacity: 1; }
/* Crop apply button */
.ge-crop-apply {
position: absolute;
z-index: 10;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 6px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
.ge-crop-apply input[type="number"] {
width: 56px;
padding: 3px 4px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 11px;
-moz-appearance: textfield;
}
.ge-crop-apply input[type="number"]::-webkit-outer-spin-button,
.ge-crop-apply input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.ge-crop-x { font-size: 11px; opacity: 0.5; }
.ge-crop-apply-btn {
padding: 4px 10px;
background: var(--red);
color: #fff;
border: none;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
position: relative;
top: -2px;
}
.ge-crop-apply-btn:hover { background: color-mix(in srgb, var(--red) 85%, #000); }
/* Text input floating */
.ge-text-input {
background: var(--bg);
border: 1px solid var(--red);
color: var(--fg);
padding: 4px 8px;
font-size: 14px;
border-radius: 4px;
min-width: 120px;
}
.ge-brush-cursor {
position: fixed; pointer-events: none; z-index: 10000;
border: 1.5px solid #fff; border-radius: 50%;
display: none;
box-sizing: border-box;
}
.ge-brush-cursor::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 3px; height: 3px;
margin: -1.5px 0 0 -1.5px;
background: rgba(255,255,255,0.8);
border-radius: 50%;
}
.ge-inpaint-section {
padding: 8px 0; border-top: 1px solid var(--border); margin-top: 4px;
}
.ge-inpaint-popover-head {
display: none;
}
@media (min-width: 821px) {
.ge-controls.ge-inpaint-popover-host {
padding: 0;
border-bottom: 0;
gap: 0;
}
.ge-inpaint-section.ge-inpaint-popover {
position: fixed;
z-index: 10004;
width: 330px;
max-height: calc(100vh - 24px);
overflow-y: auto;
padding: 10px;
margin: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--panel);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.42);
}
.ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: -10px -10px 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border);
cursor: grab;
user-select: none;
}
.ge-inpaint-section.ge-inpaint-popover > .ge-section-title-with-help:not(.ge-inpaint-popover-title) {
display: none;
}
.ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-title {
margin: 0;
flex: 1 1 auto;
}
.ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-close {
flex: 0 0 auto;
position: relative;
top: -8px;
border: none;
background: none;
color: var(--fg-muted);
font-size: 18px;
line-height: 1;
cursor: pointer;
padding: 0 4px;
border-radius: 4px;
transition: color 0.14s ease, background-color 0.14s ease;
}
.ge-inpaint-section.ge-inpaint-popover .ge-inpaint-popover-close:hover {
color: var(--fg);
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
}
.ge-inpaint-model-row {
display: flex;
align-items: center;
gap: 8px;
}
.ge-inpaint-model-row label {
flex: 0 0 auto;
font-size: 10px;
opacity: 0.65;
}
.ge-inpaint-model-row #ge-ai-inpaint {
flex: 1 1 auto;
min-width: 0;
font-size: 10px;
padding: 4px 6px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
}
.ge-inpaint-prompt {
width: 100%; padding: 6px 8px; background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 4px; font-size: 12px;
font-family: inherit; margin-top: 4px; box-sizing: border-box;
}
.ge-inpaint-prompt:focus { border-color: var(--red); outline: none; }
/* Responsive: stack on narrow screens */
@media (max-width: 700px) {
/* The editor body holds the sidebar | canvas | layers panel; on
mobile we stack them as toolbar (top) → canvas (middle) → layers
(bottom). Without this the body stays a flex row and the canvas
ends up zero-width because the sidebar takes the full width. */
.ge-editor-body {
flex-direction: column;
}
/* Brush/Size-style rows: stack the label above its slider so the
slider gets a full row instead of getting smashed against the
panel's right edge. Doesn't touch .ge-eraser-row which has its own
value-chip-on-the-right layout. */
.ge-control-row:not(.ge-eraser-row):has(input[type="range"]) {
flex-direction: column;
align-items: stretch;
gap: 2px;
}
.ge-control-row:not(.ge-eraser-row):has(input[type="range"]) > input[type="range"] {
width: 100%;
}
/* More breathing room around lasso/wand sliders on mobile so they
don't hug the right edge of the controls panel. */
#ge-lasso-section .ge-eraser-row,
#ge-wand-section .ge-eraser-row {
padding-right: 12px;
margin-top: 6px;
}
/* Lasso action buttons (Clear / Copy / To Mask / Bg Remove): pin to
the right edge of the row instead of left-aligned. */
#ge-lasso-section .ge-actions,
#ge-wand-section .ge-actions {
justify-content: flex-end;
}
/* Universal thumb-grow on active for every slider on mobile. */
.ge-controls input[type="range"]:active::-webkit-slider-thumb,
.ge-controls input[type="range"].is-using::-webkit-slider-thumb {
width: 22px;
height: 22px;
}
.ge-controls input[type="range"]:active::-moz-range-thumb,
.ge-controls input[type="range"].is-using::-moz-range-thumb {
width: 22px;
height: 22px;
}
.ge-controls input[type="range"]::-webkit-slider-thumb,
.ge-controls input[type="range"]::-moz-range-thumb {
transition: width 0.12s ease, height 0.12s ease;
}
.ge-toolbar {
flex-direction: row;
width: 100%;
height: auto;
padding: 4px 0;
border-right: none;
border-bottom: 1px solid var(--border);
overflow-x: auto;
overflow-y: hidden;
flex-shrink: 0;
gap: 0;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.ge-toolbar::-webkit-scrollbar { display: none; }
/* Section labels in the toolbar were vertical breaks — turn them
into thin vertical dividers when the column flows horizontally. */
.ge-tool-sep {
border-top: none;
border-left: 1px solid var(--border);
margin: 0 4px;
padding: 0;
width: 1px;
height: 24px;
text-indent: -9999px;
overflow: hidden;
flex-shrink: 0;
}
.ge-tool-btn {
flex-shrink: 0;
width: 48px;
}
/* Canvas is the main thing — give it as much vertical space as
the editor body can spare. min-height keeps it visible even when
the right panel is at its max. */
.ge-canvas-area {
flex: 1 1 auto;
min-height: 40vh;
}
/* Right panel = layers list. Docked as a bottom sheet with a peek
state: collapsed shows the header strip (and the active layer
row peeks through), expanded reveals all layers + actions. The
user swipes up to expand, down to collapse — the active layer is
never fully hidden. */
.ge-right-panel {
position: fixed;
left: 0;
right: 0;
bottom: 0;
width: 100%;
max-height: 70vh;
height: 70vh;
background: var(--panel);
border-left: none;
border-top: 1px solid var(--border);
border-radius: 12px 12px 0 0;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
z-index: 50;
flex-shrink: 0;
/* Peek height grows with the layer count up to a cap. JS sets the
`--peek-height` custom property from _renderLayerPanel(); default
fallback is enough for the header + one row. */
transform: translateY(calc(100% - var(--peek-height, 110px)));
transition: transform 0.22s ease-out;
}
.ge-right-panel.expanded {
transform: translateY(0);
}
/* Minimized: panel slides almost fully off-screen — only a thin
handle strip remains visible so the user can swipe back up. */
.ge-right-panel.minimized {
transform: translateY(calc(100% - 24px));
}
.ge-layers-grab {
display: block !important;
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: color-mix(in srgb, var(--fg) 30%, transparent);
border-radius: 2px;
}
.ge-layers-header {
padding-top: 16px;
position: relative;
}
/* In peek state, the layers list takes the full panel height (~70vh)
and never overflows so its own scrollbar doesn't activate. Constrain
the list to the visible peek area so it scrolls properly when there
are more layers than the peek can show. */
.ge-right-panel:not(.expanded) .ge-layers-list {
max-height: calc(var(--peek-height, 110px) - 52px);
-webkit-overflow-scrolling: touch;
}
/* Mobile layer-row buttons — bump hit targets up so fingers can
reliably tap visibility / fx / delete. */
.ge-layer-vis,
.ge-layer-btn,
.ge-layer-fx-btn {
width: 40px !important;
height: 40px !important;
font-size: 18px !important;
display: inline-flex;
align-items: center;
justify-content: center;
}
.ge-layer-btn svg,
.ge-layer-fx-btn svg {
width: 18px;
height: 18px;
}
/* Layer opacity slider stays at its expanded size at all times on
mobile — there's room for it and the resize-on-hover felt jittery
when scrolling. */
.ge-layer-opacity,
.ge-layer-opacity:hover,
.ge-layer-opacity:active,
.ge-layer-opacity.dragging {
width: 160px !important;
height: 10px !important;
}
/* Transform / adjust popups dock at the TOP of the viewport on
mobile so they overlay the topbar instead of eating canvas space.
Fixed positioning escapes any transformed ancestor stacking. */
.ge-transform-popup,
.ge-adj-popup,
.ge-fx-popup {
position: fixed !important;
left: 8px !important;
right: 8px !important;
top: 32px !important;
bottom: auto !important;
max-width: calc(100vw - 16px) !important;
width: auto !important;
min-width: 0 !important;
min-height: 240px !important;
max-height: calc(100vh - 40px);
overflow-y: auto;
z-index: 10003 !important;
}
.ge-fx-menu {
position: fixed !important;
left: 8px !important;
right: 8px !important;
top: auto !important;
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px) !important;
width: auto !important;
min-width: 0 !important;
max-width: calc(100vw - 16px) !important;
z-index: 10002 !important;
}
/* Hide both the minimise and × close buttons in the header on
mobile — a clearly labelled Cancel button next to Apply in the
body is more discoverable than tiny header icons. */
.ge-transform-popup #ge-transform-min,
.ge-transform-popup #ge-transform-cancel {
display: none !important;
}
/* Wrap the transform body across three rows on mobile:
Row 1: W + H
Row 2: ↻ rotate input + rotate-90 quick
Row 3: flip-h flip-v ............ Apply */
.ge-transform-popup-body {
flex-wrap: wrap !important;
align-items: center;
row-gap: 8px;
column-gap: 6px;
}
.ge-transform-popup-body #ge-transform-rot {
width: 3.5ch !important;
min-width: 0 !important;
}
/* 3-row layout, enforced by explicit `.ge-row-break` elements in the
DOM (the only reliable way to force a flex-wrap line break).
Row 1: W input + H input
Row 2: aspect lock + ↻ rotate input
Row 3: flip-h, flip-v, rot-90 + Cancel + Apply */
.ge-transform-popup-body .ge-row-break {
display: block;
flex-basis: 100%;
height: 0;
width: 100%;
}
/* Flip buttons slightly bigger so they're easier to tap. */
.ge-transform-quick-btn {
padding: 8px 10px !important;
min-width: 40px;
min-height: 40px;
}
.ge-transform-quick-btn svg {
width: 18px;
height: 18px;
}
/* Cancel + Apply share the right side of row 2, same size, paired. */
.ge-transform-popup-body #ge-transform-cancel-btn {
margin-left: auto;
}
.ge-transform-popup-body #ge-transform-cancel-btn,
.ge-transform-popup-body #ge-transform-apply {
padding: 8px 16px !important;
font-size: 13px !important;
min-width: 80px;
text-align: center;
}
/* Stepper-style number control on mobile:
[ − ] [ input ] [ + ]
Stack reverses so the − sits visually on the LEFT of the input, +
on the RIGHT. Big finger-sized squares, big +/- glyphs, both
buttons support tap-and-hold auto-repeat (JS) for fast scrubbing. */
.ge-transform-field {
display: inline-flex !important;
align-items: stretch;
gap: 0;
}
.ge-transform-field > label {
display: inline-flex;
align-items: center;
padding-right: 6px;
font-weight: 600;
}
.ge-transform-field:not(:has(#ge-transform-rot)) > label {
position: relative;
top: -1px;
}
.ge-transform-spin {
display: inline-flex !important;
flex-direction: row !important;
/* Use grid-order to swap positions of [−][+]: the − should sit
BEFORE the input, the + AFTER it. Achieve this by absolute
order — first child styled differently, second styled
differently, then we move the whole .ge-transform-spin's first
button visually before the input via reverse-flex. */
order: 0;
width: auto !important;
}
.ge-transform-field {
flex-direction: row;
}
/* Re-flow: label + [-] + input + [+]
We split the .ge-transform-spin's two buttons across the field's
flex line using order: order:0 for −, order:2 for input, order:3
for +. The spin itself gets `display: contents` so its
children can be reordered independently against the input. */
.ge-transform-spin {
display: contents !important;
}
.ge-transform-field > input.ge-transform-popup-input {
order: 2;
text-align: center;
}
.ge-transform-spin button[data-spin="down"] {
order: 1;
}
.ge-transform-spin button[data-spin="up"] {
order: 3;
}
/* The buttons themselves — chunky, square, identical in size. Reset
the base `:first-child` rule (which strips the bottom border for
the old stacked layout) so both buttons render with the same
visual box in the new row layout. */
.ge-transform-spin button {
box-sizing: border-box !important;
width: 36px !important;
height: 36px !important;
min-width: 36px;
min-height: 36px;
padding: 0 0 0 0 !important;
font-size: 18px !important;
font-weight: 700;
line-height: 1 !important;
border: 1px solid var(--border) !important;
border-radius: 6px !important;
flex-shrink: 0;
display: inline-flex !important;
align-items: center;
justify-content: center;
position: relative;
top: -4px;
}
.ge-transform-spin button:first-child,
.ge-transform-spin button:last-child {
border-radius: 6px !important;
border-bottom: 1px solid var(--border) !important;
}
/* The × glyph in close buttons also sits visually low — lift it 4 px
to match the +/- button alignment. Same asymmetric-padding trick. */
.modal-close,
.close-btn,
.ge-adj-close,
.ge-adj-min {
padding-top: 0 !important;
padding-bottom: 8px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
}
.ge-transform-spin button[data-spin="down"] { margin-right: 4px; }
.ge-transform-spin button[data-spin="up"] { margin-left: 4px; }
/* The input between them — generous tap area and clear typography. */
.ge-transform-field > input.ge-transform-popup-input {
height: 36px !important;
padding: 0 6px !important;
font-size: 14px !important;
min-width: 56px;
position: relative;
top: 2px;
}
/* Rotate input — compact so it fits on row 2 alongside the flip
quick buttons and the Cancel + Apply pair. */
.ge-transform-field > #ge-transform-rot.ge-transform-popup-input {
min-width: 0;
width: 64px !important;
}
/* Suffix (°) sits right after the rotate input but before the + button. */
.ge-transform-popup-suffix { order: 3; align-self: center; padding: 0 2px; }
/* Slider thumb grows only while actively dragged — keeps the resting
row compact, thumb pops larger when the user grabs it. */
.gallery-editor-container input[type="range"]:active::-webkit-slider-thumb,
.ge-layer-opacity:active::-webkit-slider-thumb,
.ge-layer-opacity.dragging::-webkit-slider-thumb {
width: 22px !important;
height: 22px !important;
}
.gallery-editor-container input[type="range"]:active::-moz-range-thumb,
.ge-layer-opacity:active::-moz-range-thumb,
.ge-layer-opacity.dragging::-moz-range-thumb {
width: 22px !important;
height: 22px !important;
}
/* Slider value bubble on mobile — bigger text/padding for
readability. Position is JS-driven via `position: fixed` from the
base rule, so don't override top/bottom here — `bottom: 100%`
with fixed positioning was pushing the bubble off-screen. */
.ge-slider-bubble {
font-size: 14px !important;
padding: 5px 10px !important;
}
/* Editor topbar — scrolls horizontally on narrow viewports. Buttons
fill more vertical+horizontal space so each is a comfortable
finger-tap target instead of feeling cramped. */
.ge-topbar {
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 6px;
padding: 6px 8px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.ge-topbar::-webkit-scrollbar { display: none; }
.ge-topbar-left,
.ge-topbar-right {
flex-shrink: 0;
gap: 6px;
}
.ge-alpha-badge {
display: none;
}
#ge-undo {
position: sticky !important;
left: 8px;
background: var(--panel);
z-index: 8;
box-shadow: 8px 0 10px -10px color-mix(in srgb, var(--fg) 55%, transparent);
}
.ge-topbar .ge-btn,
.ge-topbar .ge-btn-sm {
padding: 7px 11px !important;
font-size: 13px !important;
min-height: 34px;
line-height: 1;
top: 0 !important;
}
.ge-topbar .ge-btn svg,
.ge-topbar .ge-btn-sm svg {
width: 16px;
height: 16px;
}
.ge-topbar select,
.ge-topbar input,
.ge-topbar .ge-ai-model {
padding: 6px 8px !important;
font-size: 12px !important;
min-height: 32px;
top: 0 !important;
}
.ge-topbar .ge-canvas-size {
font-size: 12px !important;
padding: 0 6px;
line-height: 34px;
}
#ge-history-panel {
left: 0 !important;
right: 0 !important;
top: auto !important;
bottom: 0 !important;
width: auto !important;
max-height: min(60dvh, 420px);
border-radius: 12px 12px 0 0 !important;
z-index: 10001 !important;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
}
#ge-history-panel .ge-history-head { cursor: default; }
#ge-shortcuts-btn {
display: none !important;
}
/* Topbar popups that need real interaction (Resize sliders, Edge
form, Save with sub-items) become bottom-sheet slide-ups on
mobile so there's room to interact. Image and Filter are tight
single-column dropdowns — they stay anchored to their button. */
.ge-resize-menu,
.ge-edge-menu,
.ge-save-menu {
position: fixed !important;
left: 0 !important;
right: 0 !important;
top: auto !important;
bottom: 0 !important;
min-width: 0 !important;
max-width: 100vw !important;
width: auto;
padding: 18px 16px 24px !important;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px 12px 0 0 !important;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
z-index: 10005;
animation: ge-controls-slide-up 0.2s ease-out;
}
/* Image / Filter stay as anchored dropdowns; just cap width
so they don't run off-screen on narrow viewports. */
.ge-image-menu,
.ge-filter-menu {
max-width: calc(100vw - 16px);
}
.ge-resize-menu::before,
.ge-edge-menu::before,
.ge-save-menu::before {
content: '';
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: color-mix(in srgb, var(--fg) 30%, transparent);
border-radius: 2px;
}
/* The Edge form needs a bit of breathing room when it spans the
viewport; the W/H inputs in the crop apply pop should stay compact. */
.ge-edge-menu { padding: 12px; }
.ge-canvas-prompt {
width: calc(100vw - 16px) !important;
max-width: none !important;
}
/* Inpaint floating prompt — keep it inside the canvas area on
mobile so it doesn't run off the right edge. */
.ge-inpaint-popup {
max-width: calc(100vw - 16px);
}
.ge-inpaint-popup-input { width: 160px; }
/* Tool-specific controls (brush size/color, eraser opacity/flow,
lasso feather, inpaint prompt panel, etc.) lift out of the right
panel and dock as a slide-up bottom sheet so they have actual room
to breathe on a phone. A short grab-handle visually separates it
from the canvas. */
.ge-controls {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
z-index: 60;
border-bottom: none;
border-top: 1px solid var(--border);
background: var(--panel);
border-radius: 12px 12px 0 0;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
max-height: 60vh;
overflow-y: auto;
padding: 18px 16px 24px;
gap: 12px;
/* Slide-up entrance animation when the controls become visible. */
animation: ge-controls-slide-up 0.18s ease-out;
}
.ge-controls::before {
content: '';
position: absolute;
top: 6px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 4px;
background: color-mix(in srgb, var(--fg) 30%, transparent);
border-radius: 2px;
}
@keyframes ge-controls-slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
/* User swiped the sheet down — keep tool active but hide its
controls. Re-tapping the same tool button toggles back. */
.ge-controls.dismissed {
transform: translateY(100%);
pointer-events: none;
/* Smooth slide-out matching the slide-in duration. */
animation: none;
transition: transform 0.18s ease-in;
}
}
/* ── Group Chat ────────────────────────────────────── */
.msg-group .role {
font-weight: 600;
}
#group-toggle-btn.active,
.overflow-menu-item#overflow-group-btn.active {
color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
}
/* Group model picker — match app theme */
#group-model-picker .modal-content {
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
}
#group-model-picker .modal-header {
border-bottom: 1px solid var(--border);
}
#group-model-picker .memory-item {
border-radius: 6px;
transition: background 0.1s;
}
#group-model-picker .memory-item:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
#group-model-picker input[type="checkbox"] {
accent-color: var(--accent, var(--red));
}
#group-model-picker input[type="radio"] {
accent-color: var(--accent, var(--red));
}
#group-model-picker .btn-primary {
background: var(--accent, var(--red));
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
#group-model-picker .btn-primary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
#group-model-picker .btn-primary:hover:not(:disabled) {
filter: brightness(1.1);
}
/* ── Email document type ── */
.doc-email-header {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px 12px;
border-bottom: 1px solid var(--border);
background: var(--bg);
flex-shrink: 0;
}
.email-field {
display: flex;
align-items: center;
gap: 8px;
}
.email-field label {
font-size: 11px;
font-weight: 600;
color: var(--fg);
opacity: 0.5;
min-width: 50px;
text-align: right;
}
.email-field input {
flex: 1;
background: transparent;
border: 1px solid var(--border);
border-radius: 4px;
padding: 5px 8px;
font-size: 13px;
font-family: inherit;
color: var(--fg);
outline: none;
}
.email-field input:focus {
border-color: var(--accent, #4a9eff);
}
.email-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 2px;
}
.email-draft-btn {
background: transparent;
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 14px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
}
.email-draft-btn:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
.email-draft-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* ── Email inbox list ── */
.email-body { border-top: 1px solid var(--border); }
.email-folder-bar { padding: 4px 8px; border-bottom: 1px solid var(--border); }
.email-folder-select {
background: transparent; border: 1px solid var(--border); border-radius: 4px;
color: var(--fg); font-size: 11px; padding: 2px 6px; cursor: pointer; width: 100%; height: 26px;
}
.email-list { overflow-y: auto; max-height: 400px; }
.email-item {
display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px;
cursor: pointer; border-bottom: 1px solid var(--border); position: relative; transition: background 0.1s;
}
.email-item:hover { background: var(--hover-bg, rgba(255,255,255,0.03)); }
.email-item:hover .email-menu-btn { opacity: 0.5; }
.email-item-spam { opacity: 0.4; }
.email-item-spam:hover { opacity: 0.75; }
.email-tag-spam {
display: inline-flex; align-items: center; gap: 4px;
background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
color: var(--accent, var(--red));
border-radius: 10px; padding: 1px 6px; font-size: 9px; font-weight: 600;
text-transform: lowercase; letter-spacing: 0.3px;
}
.email-spam-unflag {
background: none; border: none; color: inherit; cursor: pointer;
padding: 0 2px; font-size: 10px; line-height: 1; opacity: 0.6;
}
.email-spam-unflag:hover { opacity: 1; color: var(--fg); }
/* Unread state is now a subtle icon, not full styling */
.email-done-check {
display: inline-flex; align-items: center; justify-content: center;
width: 16px; height: 16px; border-radius: 50%; background: transparent;
border: 1.5px solid var(--border); color: transparent;
flex-shrink: 0; cursor: pointer; transition: all 0.15s;
margin-right: 4px;
}
.email-done-check:hover { border-color: var(--accent, #4a9eff); }
.email-done-check.active { background: var(--accent, #4a9eff); border-color: var(--accent, #4a9eff); color: #fff; }
.email-done-check.active svg { display: block; }
.email-done-check svg { display: none; }
/* Email titles that carry a cached AI summary get a thin underline-dot hint
so the user knows a hover-preview is available. */
.memory-item-title.email-card-has-summary {
cursor: help;
text-decoration: underline dotted color-mix(in srgb, var(--accent) 60%, transparent) 1px;
text-underline-offset: 3px;
}
.doclib-card.email-card-removing {
pointer-events: none !important;
max-height: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
border-color: transparent !important;
opacity: 0;
transform: translateX(-10px) scale(0.985);
transition:
opacity 0.18s ease,
transform 0.22s ease,
max-height 0.23s ease,
padding 0.23s ease,
margin 0.23s ease,
border-color 0.18s ease;
}
@media (prefers-reduced-motion: reduce) {
.doclib-card.email-card-removing {
transition: opacity 0.08s ease;
transform: none;
}
}
/* Library card done check — always visible, subtle icon next to title */
.email-card-done {
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0; cursor: pointer;
transition: opacity 0.15s, color 0.15s;
opacity: 0.15; color: var(--fg);
}
/* Hover preview: bright accent when un-checked so the user sees a check
coming; dim+grey when already active so they can distinguish the
"click to UN-check" target from the active state itself. */
.email-card-done:not(.active):hover {
opacity: 0.75 !important;
color: var(--accent-primary, var(--red));
}
.email-card-done.active { opacity: 0.95; color: var(--accent-primary, var(--red)); }
.email-card-done.active:hover {
opacity: 0.35 !important;
color: var(--fg) !important;
}
.email-card-done.just-checked {
animation: check-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Reverse animation when un-checking — shrink-and-fade so the click is
obviously felt even when hover styling otherwise keeps the icon visible. */
.email-card-done.just-unchecked {
animation: check-unpop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes check-pop {
0% { transform: scale(1); }
40% { transform: scale(1.7) rotate(-10deg); }
70% { transform: scale(1.1) rotate(4deg); }
100% { transform: scale(1) rotate(0); }
}
@keyframes check-unpop {
0% { transform: scale(1); opacity: 0.95; }
35% { transform: scale(0.45) rotate(8deg); opacity: 0.05; }
65% { transform: scale(0.9); opacity: 0.2; }
100% { transform: scale(1); }
}
/* Expanded email preview in library — override .memory-item row layout */
.doclib-card.email-card-expanded.memory-item {
cursor: default !important;
background: var(--bg) !important;
border: 1px solid var(--border) !important;
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
/* Fill the available grid height instead of the old hardcoded 70vh —
the modal already caps height (90dvh on mobile, auto on desktop), so
the card just needs to flex into whatever remains. */
flex: 1 1 auto !important;
height: 100% !important;
min-height: 0 !important;
max-height: none !important;
padding: 0 !important;
overflow: hidden !important;
gap: 0 !important;
}
.doclib-card.email-card-expanded.memory-item > div:first-child {
/* The summary content (subject, date) - acts as the title bar. Left padding
trimmed 14→6px to shift the title 8px left total. */
padding: 2px 4px 10px !important; /* top trimmed to lift the header text up */
margin-top: -14px !important; /* pull the whole bar up into the freed space */
border-bottom: 1px solid var(--border);
flex: 0 0 auto !important;
width: 100%;
}
.doclib-card.email-card-expanded .memory-item-actions {
display: none !important;
}
/* When expanded inline, show the subject a touch larger than the 12px card
title (close to the new-tab window header) without being oversized. */
.doclib-card.email-card-expanded .memory-item-title {
font-size: 14px;
font-weight: 600;
}
/* The sender name is redundant once expanded (the From: row shows it just
below), so drop it and keep only the date — which then sits left-aligned
directly under the subject. Nudge the date 1px right to line up exactly with
the subject text, and up closer to it. */
.doclib-card.email-card-expanded .email-meta-sender,
.doclib-card.email-card-expanded .email-meta-sep {
display: none;
}
.doclib-card.email-card-expanded .memory-item-meta {
margin-top: -9px !important;
}
.doclib-card.email-card-expanded .email-meta-date {
margin-left: 3px;
}
.email-card-reader {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
font-size: 12px;
}
.email-card-reader-loading {
background: color-mix(in srgb, var(--panel) 82%, transparent);
}
.email-card-reader-loading .spinner-whirlpool,
.email-card-reader-loading .ai-spinner-whirlpool {
opacity: 0.85;
}
/* Per-email unread dot in the expanded reader's title row. Background color
stays inline (per-sender hue from _senderColor), but the dot gets a soft
breathing glow + a 4px vertical nudge so it reads as centered against
the row instead of riding the baseline. */
.email-card-unread-dot {
position: relative;
top: 0;
animation: email-card-unread-breathe 2.2s ease-in-out infinite;
}
@keyframes email-card-unread-breathe {
0%, 100% {
box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 0%, transparent);
opacity: 0.85;
transform: scale(1);
}
50% {
/* currentColor here is unreliable (background, not color) — use a
subtle white-tinted halo so the glow shows on any sender hue. */
box-shadow: 0 0 6px 2px rgba(255, 255, 255, 0.18);
opacity: 1;
transform: scale(1.15);
}
}
@media (prefers-reduced-motion: reduce) {
.email-card-unread-dot { animation: none; }
}
.email-reader-header {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 12px;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
background: var(--bg);
flex-shrink: 0;
}
.email-reader-header > .email-reader-meta {
flex: 1; min-width: 0;
}
.email-reader-meta {
min-width: 0; opacity: 0.85; line-height: 1.7; font-size: 11px;
display: flex; flex-direction: column; gap: 4px;
}
.email-reader-meta-row {
display: flex; align-items: center; gap: 6px;
min-width: 0;
}
.email-reader-meta-row strong { opacity: 0.5; font-weight: 600; flex-shrink: 0; min-width: 36px; }
.email-reader-meta-row > span {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
min-width: 0; flex: 1;
}
/* Recipient chips */
.recipient-chips {
display: inline-flex !important; flex-wrap: wrap; gap: 4px;
white-space: normal !important; overflow: visible !important;
text-overflow: clip !important;
}
.recipient-chip {
display: inline-flex; align-items: center;
padding: 1px 8px; font-size: 10px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--fg);
white-space: nowrap;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, max-width 0.2s;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
.recipient-chip:hover {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 12%, transparent);
border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 40%, transparent);
}
.recipient-chip.expanded {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 15%, transparent);
border-color: var(--accent-primary, var(--red));
max-width: 500px;
}
@container docpane (max-width: 460px) {
.email-reader-header {
flex-direction: column;
gap: 6px;
}
.email-reader-actions {
align-self: flex-end;
}
.email-reader-meta-row {
display: grid;
grid-template-columns: 1fr;
gap: 2px;
align-items: start;
}
.email-reader-meta-row strong {
min-width: 0;
}
.recipient-chip {
max-width: 100%;
}
}
.email-reader-actions {
display: flex; gap: 4px; flex-wrap: nowrap; align-items: center;
flex-shrink: 0;
justify-content: flex-end;
margin-top: -4px;
}
/* The HTML wraps the buttons in two .email-reader-actions-row divs (primary
+ secondary). On mobile those flatten via `display: contents` inside the
max-width:768px block; apply the same here so the whole row stays on one
line on desktop too. */
.email-reader-actions-row {
display: contents;
}
.email-reader-atts {
display: flex; flex-wrap: wrap; gap: 6px;
padding: 8px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.email-reader-body {
font-size: 12px;
line-height: 1.55;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: anywhere;
flex: 1;
overflow-y: auto;
padding: 14px;
min-height: 0;
/* Use the OS colored emoji font for incoming mail content */
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla";
font-variant-emoji: emoji;
}
/* When rendering HTML emails, override pre-wrap so layout works */
.email-reader-body.html-body {
white-space: normal;
}
.email-reader-body.html-body img { max-width: 100%; height: auto; }
.email-reader-body.html-body table { max-width: 100%; border-collapse: collapse; }
/* Preserve highlights — sender's intent (snark, callouts) gets through, but
rendered with a theme-aware accent so it stays legible in any theme. The
sanitizer rewrites `… ` to . */
.email-reader-body mark {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
color: inherit;
padding: 0 2px;
border-radius: 2px;
}
/* "Other from this sender" — slide-out panel inside the email reader. */
.email-card-reader.from-sender-open { position: relative; }
.from-sender-panel {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 280px;
background: var(--bg);
border-left: 1px solid var(--border);
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.18);
z-index: 5;
display: flex;
flex-direction: column;
animation: from-sender-slide 0.22s ease-out;
}
@keyframes from-sender-slide {
from { transform: translateX(8px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.from-sender-head {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px 12px 10px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.from-sender-title-wrap { flex: 1; min-width: 0; }
.from-sender-title {
margin: 0;
font-size: 14px;
font-weight: 600;
line-height: 1.2;
color: var(--fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.from-sender-addr {
font-size: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
opacity: 0.55;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.from-sender-count {
font-size: 10px;
opacity: 0.6;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.from-sender-list {
flex: 1;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.from-sender-loading { display: flex; justify-content: center; padding: 24px 0; }
.from-sender-empty { padding: 16px 12px; font-size: 11px; opacity: 0.6; }
/* Chat-bubble feel — each email row is a self-contained darker bubble. */
.from-sender-row {
position: relative;
display: flex;
flex-direction: row;
align-items: stretch;
width: 100%;
min-height: 44px;
text-align: left;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
border-radius: 12px;
color: var(--fg);
font-family: inherit;
transition: background 0.12s, border-color 0.12s, transform 0.08s;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
overflow: hidden;
}
.from-sender-row-main {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
align-items: stretch;
text-align: left;
background: none;
border: none;
cursor: pointer;
color: inherit;
font-family: inherit;
/* Shifted text up 2px / left 2px relative to the bubble (padding 8/20 → 6/18). */
padding: 6px 4px 10px 18px;
transition: background 0.12s;
}
.from-sender-row-main:hover {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.from-sender-row-main:active { transform: translateY(1px); }
.from-sender-row-more {
flex: 0 0 auto;
background: none;
border: none;
color: var(--fg);
cursor: pointer;
opacity: 0.45;
width: 28px;
padding: 0 4px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity 0.12s, background 0.12s;
}
.from-sender-row-more:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.from-sender-row:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
border-color: color-mix(in srgb, var(--fg) 20%, transparent);
}
.from-sender-row.from-sender-unread { font-weight: 600; }
.from-sender-row.from-sender-unread::before {
content: '';
position: absolute;
left: 7px;
top: 50%;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent, var(--red));
transform: translateY(-50%);
box-shadow: 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
}
.from-sender-row-top {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
min-width: 0;
}
.from-sender-row-bottom {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
margin-top: 3px;
}
.from-sender-subj {
font-size: 12px;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.from-sender-att {
flex-shrink: 0;
opacity: 0.55;
color: var(--fg);
}
.from-sender-row:hover .from-sender-att { opacity: 0.85; }
.from-sender-date {
font-size: 10px;
opacity: 0.55;
flex-shrink: 0;
}
.from-sender-folder {
font-size: 9px;
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.4px;
background: color-mix(in srgb, var(--fg) 10%, transparent);
padding: 1px 5px;
border-radius: 3px;
margin-left: auto;
}
/* Header row above search — holds the sender chip, attachment toggle,
and a small close X for exiting the panel. */
.from-sender-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 8px 8px 12px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.from-sender-header-empty {
font-size: 11px;
opacity: 0.6;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.from-sender-close {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
width: 22px;
height: 22px;
padding: 0 0 4px 0;
font-size: 16px;
line-height: 1;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.55;
flex-shrink: 0;
position: relative;
top: -4px;
transition: opacity 0.12s, background 0.12s;
}
.from-sender-close:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.from-sender-toggle {
/* Default: compact 22x22 circle. When .is-active (filter ON) it stretches
vertically to match the header's content height — that's the visual cue
that the filter is engaged. */
display: inline-flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid var(--border);
color: var(--fg);
border-radius: 50%;
width: 22px;
height: 22px;
padding: 0;
font-family: inherit;
cursor: pointer;
opacity: 0.75;
flex-shrink: 0;
margin-left: auto;
position: relative;
top: -2px;
transition: opacity 0.12s, background 0.12s, border-color 0.12s, color 0.12s,
height 0.18s ease, border-radius 0.18s ease;
}
.from-sender-toggle:hover { opacity: 1; }
.from-sender-toggle.is-active {
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
color: var(--accent, var(--red));
opacity: 1;
/* Grow vertically to match the chip's height when the filter is engaged. */
align-self: stretch;
height: auto;
border-radius: 999px;
top: 0;
}
.from-sender-search-wrap {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
/* The search input now sits alone in its row (chip + toggles moved to the
header), so render it as a normal field instead of a chip-input child. */
.from-sender-search-wrap > .from-sender-search {
width: 100%;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 10px;
font-size: 12px;
color: var(--fg);
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.from-sender-search-wrap > .from-sender-search:focus { border-color: var(--accent, var(--red)); }
.from-sender-search-wrap { position: relative; }
/* Multi-chip container in the header — wraps when several chips accumulate. */
.from-sender-chips {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
flex: 1 1 auto;
min-width: 0;
}
.from-sender-chips:empty { flex: 0 0 0; }
/* Suggestion dropdown rendered just below the search input. */
.from-sender-suggest {
position: absolute;
left: 12px;
right: 12px;
top: calc(100% - 4px);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25);
z-index: 20;
max-height: 240px;
overflow-y: auto;
}
.from-sender-suggest-item {
display: flex;
align-items: baseline;
gap: 8px;
padding: 6px 10px;
font-size: 12px;
color: var(--fg);
cursor: pointer;
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.from-sender-suggest-item:last-child { border-bottom: none; }
.from-sender-suggest-item.active {
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.from-sender-suggest-item .suggest-name { font-weight: 500; }
.from-sender-suggest-item .suggest-addr {
font-size: 11px;
opacity: 0.55;
margin-left: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 60%;
}
/* Chip-input search bar — sender chip lives inline with the input;
X-ing the chip turns it into a global search. */
.from-sender-chip-input {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
background: color-mix(in srgb, var(--fg) 4%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 6px;
transition: border-color 0.15s;
}
.from-sender-chip-input:focus-within { border-color: var(--accent, var(--red)); }
.from-sender-chip {
display: inline-flex;
align-items: center;
gap: 4px;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
color: var(--accent, var(--red));
border-radius: 999px;
padding: 2px 4px 2px 8px;
font-size: 11px;
font-weight: 500;
max-width: 60%;
flex-shrink: 0;
}
.from-sender-chip-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
.from-sender-chip-x {
background: none;
border: none;
color: inherit;
cursor: pointer;
width: 16px;
height: 16px;
border-radius: 50%;
padding: 0;
font-size: 18px;
line-height: 0.55;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
position: relative;
top: -4px;
transition: opacity 0.12s, background 0.12s;
}
.from-sender-chip-x:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
}
.from-sender-search {
flex: 1;
min-width: 80px;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 12px;
padding: 4px 4px;
outline: none;
}
.from-sender-mode-note {
font-size: 9px;
opacity: 0.5;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.4px;
}
.email-card-reader.from-sender-open .email-reader-body { padding-right: 292px; }
@media (max-width: 768px) {
.from-sender-panel { width: 100%; }
.email-card-reader.from-sender-open .email-reader-body { padding-right: 0; visibility: hidden; }
}
/* Force any phone/sms/date/address auto-link to inherit the body color so
numbers don't render as bright-blue browser-default text inside email
content. format-detection in turns off detection on iOS, but some
mail clients ship pre-wrapped tel: links — those still need taming. */
.email-reader-body a[href^="tel:"],
.email-reader-body a[href^="sms:"],
.email-reader-body a[x-apple-data-detectors],
.email-bubble-body a[href^="tel:"],
.email-bubble-body a[href^="sms:"],
.email-bubble-body a[x-apple-data-detectors],
.email-thread-turn-body a[href^="tel:"],
.email-thread-turn-body a[href^="sms:"],
.email-thread-turn-body a[x-apple-data-detectors] {
color: inherit !important;
text-decoration: none !important;
pointer-events: auto;
}
.email-reader-body a,
.email-bubble-body a,
.email-thread-turn-body a {
color: var(--accent-primary, var(--red));
text-decoration: underline;
overflow-wrap: anywhere;
word-break: normal;
cursor: pointer;
}
.email-reader-body a:hover,
.email-bubble-body a:hover,
.email-thread-turn-body a:hover { opacity: 0.8; }
/* Summary block — same band chrome as Attachments / Signature / Earlier-
thread (no accent tint, no rounded corners, no leading star icon). The
only difference is the label, so all collapsible sections in the email
reader look uniform. */
.email-summary-panel {
margin: 0 0 12px 0;
padding: 10px 12px;
background: color-mix(in srgb, var(--accent-primary, var(--red)) 8%, transparent);
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 30%, transparent);
border-radius: 6px;
}
.email-summary-header {
display: flex; align-items: center; gap: 5px;
font-size: 10px; font-weight: 600; text-transform: uppercase;
color: var(--accent-primary, var(--red)); opacity: 0.85;
margin-bottom: 6px; letter-spacing: 0.4px;
}
.email-summary-content {
font-size: 12px; line-height: 1.5; color: var(--fg); white-space: pre-wrap;
}
/* Click-to-fold: tap the summary header to collapse/expand. The chevron
flips when collapsed, the content hides + the panel's bottom margin
tightens so a folded summary doesn't take up vertical space. */
.email-summary-toggle { cursor: pointer; user-select: none; }
.email-summary-toggle:hover { opacity: 1; }
.email-summary-panel.collapsed { padding-bottom: 6px; margin-bottom: 6px; }
.email-summary-panel.collapsed .email-summary-header { margin-bottom: 0; }
.email-summary-panel.collapsed .email-summary-content { display: none; }
.email-summary-panel.collapsed .email-summary-chevron { transform: rotate(-90deg); }
/* Foldable attachments — same fold-on-click UX as the summary panel. */
.email-reader-atts-wrap {
display: flex; flex-direction: column;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.email-reader-atts-header {
display: flex; align-items: center; gap: 5px;
padding: 6px 14px;
font-size: 10px; font-weight: 600; text-transform: uppercase;
color: var(--fg); opacity: 0.7;
cursor: pointer; user-select: none;
letter-spacing: 0.4px;
}
.email-reader-atts-wrap > .email-reader-atts {
border-bottom: none !important;
}
.email-reader-atts-wrap.collapsed > .email-reader-atts { display: none; }
.email-reader-atts-wrap.collapsed .email-summary-chevron { transform: rotate(-90deg); }
/* Quote fold = neutral full-width band (matches attachments header). */
.email-quote-fold {
margin: 0 -14px;
padding: 0;
border: 0;
border-top: 1px solid var(--border) !important;
border-radius: 0;
outline: 0 !important;
box-shadow: none !important;
/* Neutralise any rich-mail HTML that injects a yellow / colored bg
or border onto blockquote ancestors. */
background: transparent !important;
}
/* Same neutralisation for any element nested inside the fold (the inner
blockquote, paragraphs, divs from the original message). Yellow outlines
in Outlook-style mails almost always come from inline border-color or
box-shadow on these. */
.email-quote-fold *,
.email-quote-fold *::before,
.email-quote-fold *::after {
outline-color: transparent !important;
box-shadow: none !important;
}
.email-quote-fold blockquote,
.email-quote-fold blockquote[type="cite"],
.email-quote-fold table,
.email-quote-fold div {
border-color: var(--border) !important;
/* Override the .msg blockquote rule that sets a colored left border via
var(--hl-function) — that's where the "yellow" outline was coming from. */
border-left-color: var(--border) !important;
border-radius: 0 !important;
background: transparent !important;
}
/* JS marks the last .email-quote-fold in each email body with this class,
so it's the only one that gets rounded bottom corners. Avoids :has()
browser-support quirks. */
.email-quote-fold.last-fold {
border-radius: 0 0 8px 8px;
overflow: hidden;
}
/* ── Threaded reply turns (recursive parser) ── */
.email-thread-turn-body {
padding: 8px 14px 4px;
font-size: 12px;
line-height: 1.45;
}
/* Nested turns indent slightly + get their own card outline so the
reply hierarchy is obvious at a glance. */
.email-thread-turn-body .email-thread-turn {
margin: 8px 0 4px;
margin-left: 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 2%, transparent);
}
.email-thread-turn-body .email-thread-turn .email-fold-summary {
padding: 5px 10px;
}
.email-thread-turn-body .email-thread-turn .email-thread-turn-body {
padding: 6px 10px 2px;
font-size: 11px;
}
/* The original blockquote's left stripe is redundant when each turn is
already a card — drop it inside thread bodies. */
.email-thread-turn-body blockquote {
border-left: none !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
}
/* Flatten the inner blockquote: no rounded corners, no yellow tint
inherited from rich-mail HTML. Subtle left border + neutral bg. */
.email-quote-fold blockquote {
margin: 0;
padding: 8px 12px;
border: 0;
border-left: 2px solid var(--border);
border-radius: 0 !important;
background: transparent !important;
color: inherit;
}
/* "From … · Date …" chip appended to the summary line */
.email-fold-summary-meta {
margin-left: 6px;
font-size: 10px;
font-weight: 500;
text-transform: none;
letter-spacing: 0;
opacity: 0.55;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
/* Sender name is the primary affordance — slightly larger and not all-caps so
"From: Sam" feels like a click target on the person, not a section label. */
.email-fold-summary-name {
font-size: 11px;
font-weight: 600;
text-transform: none;
letter-spacing: 0.1px;
color: var(--fg);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.email-quote-fold .email-fold-summary {
display: flex; align-items: center; gap: 5px;
padding: 6px 14px;
cursor: pointer; list-style: none;
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.4px;
/* Force neutral page colors regardless of any rich-mail HTML wrapping us */
color: var(--fg) !important;
background: transparent !important;
background-color: transparent !important;
text-shadow: none !important;
font-style: normal !important;
opacity: 0.7;
user-select: none;
border-radius: 0;
}
.email-quote-fold .email-fold-summary > * {
/* Prevent inline-color attrs on summary children (icon SVGs, meta span)
from picking up a forced color from ancestor email HTML. */
color: inherit !important;
background: transparent !important;
}
.email-quote-fold .email-fold-summary::-webkit-details-marker,
.email-quote-fold .email-fold-summary::marker { display: none; content: none; }
.email-quote-fold .email-fold-summary { list-style: none; list-style-type: none; }
.email-quote-fold .email-fold-summary:hover { opacity: 1; }
.email-quote-fold[open] .email-summary-chevron { transform: rotate(180deg); }
.email-quote-fold[open] > *:not(summary) {
padding: 8px 14px 12px 14px;
border-top: 1px solid var(--border);
}
/* Signature fold = clean full-width band like the attachments header. No
accent overlay, no rounded corners — neutral chrome so it doesn't look
like the AI Summary panel. */
.email-sig-fold {
margin: 8px 0 0;
padding: 0;
border: 0;
border-top: 1px dashed color-mix(in srgb, var(--fg) 18%, transparent);
background: transparent;
border-radius: 0;
}
.email-sig-fold .email-fold-summary {
display: flex; align-items: center; gap: 5px;
padding: 4px 0;
cursor: pointer;
list-style: none; list-style-type: none;
font-size: 9.5px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.4px;
color: var(--fg); opacity: 0.55;
user-select: none;
}
/* Suppress the global `summary::before { content: '▶' }` rule from
leaking the chevron-emoji onto our header — we use the trailing SVG
chevron only. */
.email-sig-fold .email-fold-summary::before,
.email-quote-fold .email-fold-summary::before { content: none !important; }
.email-sig-fold .email-fold-summary::-webkit-details-marker,
.email-sig-fold .email-fold-summary::marker { display: none; content: none; }
.email-sig-fold .email-fold-summary:hover { opacity: 1; }
.email-sig-fold[open] .email-summary-chevron { transform: rotate(180deg); }
.email-sig-fold[open] > *:not(summary) {
padding: 6px 0 0;
border-top: 0;
font-size: 11px; line-height: 1.45;
color: color-mix(in srgb, var(--fg) 75%, transparent);
white-space: pre-wrap;
}
/* ── Chat-bubble layout for email threads ──
Each parsed turn becomes a bubble. The active account's outgoing
replies align right (mine); everyone else aligns left (theirs).
Bubbles read top→bottom oldest→newest, like a chat. */
.email-bubbles {
display: flex;
flex-direction: column;
gap: 10px;
padding: 4px 0 8px;
margin: 0 -2px;
}
.email-bubble-row {
display: flex;
align-items: flex-end;
gap: 8px;
width: 100%;
min-width: 0;
}
.email-bubble-row.email-bubble-mine {
flex-direction: row-reverse;
justify-content: flex-start;
}
.email-bubble-row.email-bubble-theirs {
/* Avatar lives at the END of the row (right side) so the bubble has
the full left edge to start from — gives bigger reading width and
a cleaner left margin. */
flex-direction: row-reverse;
justify-content: flex-end;
}
.email-bubble {
flex: 0 1 auto;
max-width: 90%;
min-width: 0;
padding: 8px 12px;
border: 1px solid var(--border);
background: var(--panel);
font-size: 12.5px;
line-height: 1.5;
word-wrap: break-word;
overflow-wrap: anywhere;
overflow: hidden;
}
.email-bubble-mine .email-bubble {
background: color-mix(in srgb, var(--bubble-accent, var(--fg)) 10%, var(--bg));
border-color: color-mix(in srgb, var(--bubble-accent, var(--border)) 35%, var(--border));
border-radius: 14px 14px 4px 14px;
}
.email-bubble-theirs .email-bubble {
background: color-mix(in srgb, var(--bubble-accent, var(--fg)) 5%, var(--panel));
border-color: color-mix(in srgb, var(--bubble-accent, var(--border)) 25%, var(--border));
border-radius: 14px 14px 14px 4px;
}
.email-bubble-head {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 10.5px;
margin-bottom: 4px;
opacity: 0.7;
min-width: 0;
}
.email-bubble-mine .email-bubble-head { justify-content: flex-end; }
.email-bubble-author {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50ch;
}
.email-bubble-date {
opacity: 0.7;
font-weight: 400;
white-space: nowrap;
}
.email-bubble-body {
white-space: normal;
/* Reader-controlled typography — override sender's inline styles AND
attribute-based sizing (, , , h1..h6). Weight
and italics still pass through for emphasis. */
font-family: inherit;
font-size: var(--email-body-size, 13.5px);
line-height: 1.5;
}
.email-bubble-body *:not(code):not(pre):not(kbd):not(samp) {
font-family: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
}
/* Heading tags carry browser-default font-size multipliers that bypass
`font-size: inherit` when an inline style was set; pin them too. */
.email-bubble-body h1,
.email-bubble-body h2,
.email-bubble-body h3,
.email-bubble-body h4,
.email-bubble-body h5,
.email-bubble-body h6,
.email-bubble-body big,
.email-bubble-body small,
.email-bubble-body font {
font-size: inherit !important;
line-height: inherit !important;
}
.email-bubble-body img {
max-width: 100%;
height: auto;
}
/* Sender HTML often wraps content in a fixed-width
or
. Override so content fills the reader's available
width — otherwise resizing the email window doesn't reflow anything. */
.email-bubble-body,
.email-bubble-body * {
max-width: 100% !important;
}
.email-bubble-body div[style*="width"],
.email-bubble-body table[width],
.email-bubble-body table {
width: 100% !important;
box-sizing: border-box !important;
}
.email-bubble-body blockquote {
border-left: 2px solid var(--border) !important;
margin: 4px 0 !important;
padding: 4px 10px !important;
background: transparent !important;
border-radius: 0 !important;
color: inherit;
}
.email-bubble-body table {
max-width: 100%;
}
.email-bubble-avatar {
flex-shrink: 0;
width: 26px;
height: 26px;
border-radius: 50%;
/* Per-sender color set inline (`style="background:hsl(...)"`); this
fallback only kicks in when JS hasn't assigned one. */
background: color-mix(in srgb, var(--fg) 14%, transparent);
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.22);
display: flex;
align-items: center;
justify-content: center;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.3px;
user-select: none;
}
/* Sig folds inside a bubble: tone down — no full-bleed band, since the
bubble already provides the chrome. */
.email-bubble-body .email-sig-fold {
margin: 8px 0 0;
border-top: 1px dashed var(--border);
}
.email-bubble-body .email-sig-fold .email-fold-summary,
.email-bubble-body .email-quote-fold .email-fold-summary {
padding: 4px 0;
}
.email-bubble-body .email-sig-fold[open] > *:not(summary),
.email-bubble-body .email-quote-fold[open] > *:not(summary) {
padding: 6px 0 0;
border-top: 0;
}
@media (max-width: 600px) {
.email-bubble { max-width: 96%; }
.email-bubble-avatar { display: none; }
}
/* ── Schedule Send modal ── */
.schedule-send-body {
display: flex;
flex-direction: column;
gap: 6px;
padding: 16px 18px 12px;
}
.schedule-send-label {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
text-transform: uppercase;
color: var(--fg);
opacity: 0.55;
margin-top: 6px;
}
.schedule-send-label:first-child { margin-top: 0; }
.schedule-send-presets {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 4px;
}
.schedule-send-presets .memory-toolbar-btn {
padding: 5px 10px 11px;
font-size: 12px;
justify-content: center;
line-height: 1;
}
.schedule-send-confirm svg {
width: 12px;
height: 12px;
vertical-align: -1px;
margin-right: 6px;
}
.schedule-send-datetime {
font-size: 13px;
padding: 8px 10px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
outline: none;
font-family: inherit;
width: 100%;
box-sizing: border-box;
}
.schedule-send-datetime:focus {
border-color: var(--hl-function);
}
.schedule-send-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 10px 18px 14px;
border-top: 1px solid var(--border);
margin-top: 4px;
}
.schedule-send-confirm {
background: var(--accent-primary, var(--red)) !important;
color: #fff !important;
border-color: var(--accent-primary, var(--red)) !important;
}
.schedule-send-confirm:hover {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 85%, white) !important;
}
/* Senders embed inline colors in their HTML that clash with the app's
theme (black text in dark mode, white backgrounds, yellow highlights,
etc.). Force theme colors on every descendant of the rendered body so
emails inherit the user's chosen palette regardless of what the
sender's mail client emits. */
.email-reader-body *,
.email-bubble-body * {
color: var(--fg) !important;
-webkit-text-fill-color: currentColor !important;
background-color: transparent !important;
background-image: none !important;
}
.email-reader-body a,
.email-bubble-body a,
.email-thread-turn-body a {
color: var(--hl-function) !important;
}
.email-reader-body a[href^="tel:"],
.email-reader-body a[href^="sms:"],
.email-reader-body a[href^="x-apple-data-detectors:"],
.email-reader-body a[x-apple-data-detectors],
.email-bubble-body a[href^="tel:"],
.email-bubble-body a[href^="sms:"],
.email-bubble-body a[href^="x-apple-data-detectors:"],
.email-bubble-body a[x-apple-data-detectors],
.email-thread-turn-body a[href^="tel:"],
.email-thread-turn-body a[href^="sms:"],
.email-thread-turn-body a[href^="x-apple-data-detectors:"],
.email-thread-turn-body a[x-apple-data-detectors] {
color: inherit !important;
-webkit-text-fill-color: currentColor !important;
text-decoration: none !important;
background-color: transparent !important;
background-image: none !important;
-webkit-background-clip: border-box !important;
background-clip: border-box !important;
}
/* Prefer local/system emoji fonts; avoid remote font providers. */
.doc-editor-textarea, .doc-editor-highlight, #doc-editor-code,
.doc-md-preview, .doc-csv-preview {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', monospace;
font-variant-emoji: text;
}
/* Preview panes claim the editor's space so the action footer stays pinned
at the bottom of the pane even when the rendered content is short
(shorter table, single-paragraph markdown, etc). */
.doc-csv-preview,
.doc-md-preview {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
/* Email documents override: use colored system emoji in compose. */
.doc-editor-textarea.email-mode,
#doc-editor-code.email-mode,
#doc-editor-code.email-mode .doc-editor-highlight,
#doc-editor-code.email-mode .doc-editor-textarea {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", "Twemoji Mozilla" !important;
font-variant-emoji: emoji;
}
/* Single-layer rendering for ALL documents (not just email): the highlight
overlay ( with hljs spans) and the transparent textarea use two
different rendering paths internally — the textarea uses the browser's
native form line-breaker, the overlay uses CSS line-breaking — and they
can never be guaranteed to wrap byte-identical no matter how many CSS
properties are pinned. Pick one source of truth: make the textarea its
own visible text, hide the overlay. Trade-off: no syntax highlighting in
the live editor (rendered markdown preview / chat output still has it).
The caret is now glued to the typed text by definition since both are
rendered by the same element. */
.doc-editor-highlight,
#doc-editor-code.email-mode .doc-editor-highlight {
display: none !important;
}
/* Find bar is open: float the highlight overlay ON TOP of the
textarea with transparent background + transparent text, so only
the solid spans are visible. pointer-events
off so clicks still go through to the textarea below. */
body.doc-find-active .doc-editor-highlight,
body.doc-find-active #doc-editor-code.email-mode .doc-editor-highlight {
display: block !important;
z-index: 5 !important;
background: transparent !important;
pointer-events: none;
}
body.doc-find-active .doc-editor-highlight,
body.doc-find-active .doc-editor-highlight * {
color: transparent !important;
background: transparent !important;
}
/* Marks remain solid so they pop through the otherwise-invisible
overlay; outlined extra-bright for the currently-focused one. */
body.doc-find-active mark.doc-find-mark {
background: var(--accent) !important;
color: var(--bg) !important;
}
body.doc-find-active mark.doc-find-mark.current {
background: var(--accent) !important;
box-shadow: 0 0 0 2px var(--fg) !important;
outline: 1px solid var(--fg) !important;
}
.doc-editor-textarea,
#doc-editor-code.email-mode .doc-editor-textarea {
color: var(--fg) !important;
caret-color: var(--fg) !important;
z-index: 1;
}
/* Emoji picker (monochrome icon picker) */
.emoji-picker-btn {
background: none; border: none; color: var(--fg); opacity: 0.35;
padding: 4px 7px; cursor: pointer; min-height: 28px;
display: inline-flex; align-items: center; justify-content: center;
transition: opacity 0.1s;
}
.emoji-picker-btn:hover { opacity: 1; }
.emoji-picker {
width: min(300px, 92vw); max-height: min(300px, 55vh);
background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
display: flex; flex-direction: column; overflow: hidden;
}
.emoji-picker-search {
padding: 8px 12px; background: var(--panel); border: none;
border-bottom: 1px solid var(--border); color: var(--fg);
font-size: 12px; font-family: inherit; outline: none;
}
.emoji-picker-groups { overflow-y: auto; padding: 4px 0; flex: 1; }
.emoji-picker-group { padding: 0 6px 4px 6px; }
.emoji-picker-group-name {
font-size: 10px; font-weight: 600; opacity: 0.5;
text-transform: uppercase; letter-spacing: 0.5px;
padding: 4px 4px 2px 4px;
}
.emoji-picker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 1px; }
.emoji-picker-item {
background: none; border: none; cursor: pointer;
padding: 4px; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
color: var(--fg); transition: background 0.1s;
}
.emoji-picker-item:hover { background: color-mix(in srgb, var(--accent) 15%, transparent); }
.emoji-picker-item svg { width: 16px; height: 16px; }
.email-avatar {
width: 28px; height: 28px; border-radius: 50%; background: var(--accent, #4a9eff);
color: #fff; display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 600; flex-shrink: 0; margin-top: 1px;
}
.email-item-content { flex: 1; min-width: 0; overflow: hidden; }
.email-item-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.email-sender { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.email-sender-clickable { cursor: pointer; }
.email-sender-clickable:hover { text-decoration: underline; }
.email-filter-chip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
margin: 4px 8px 6px;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
color: var(--fg);
}
.email-filter-chip-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.email-filter-chip-clear {
background: none;
border: none;
color: inherit;
cursor: pointer;
font-size: 14px;
line-height: 1;
padding: 0 2px;
opacity: 0.6;
}
.email-filter-chip-clear:hover { opacity: 1; }
.email-date { font-size: 10px; opacity: 0.5; white-space: nowrap; flex-shrink: 0; }
.email-subject { font-size: 11px; opacity: 0.6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }
.email-tags { display: inline-flex; gap: 3px; margin-left: 6px; vertical-align: middle; }
.email-tag {
display: inline-block;
font-size: 9px;
line-height: 1;
padding: 2px 5px;
border-radius: 8px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.3px;
background: rgba(127, 127, 127, 0.18);
color: var(--fg);
}
.email-tag-work { background: rgba(96, 165, 250, 0.22); color: #60a5fa; }
.email-tag-personal { background: rgba(74, 222, 128, 0.22); color: #4ade80; }
.email-tag-finance { background: rgba(250, 204, 21, 0.22); color: #facc15; }
.email-tag-bills { background: rgba(251, 146, 60, 0.22); color: #fb923c; }
.email-tag-receipt { background: rgba(251, 146, 60, 0.22); color: #fb923c; }
.email-tag-travel { background: rgba(167, 139, 250, 0.22); color: #a78bfa; }
.email-tag-newsletter { background: rgba(148, 163, 184, 0.22); color: #94a3b8; }
.email-tag-promo,
.email-tag-marketing { background: rgba(244, 114, 182, 0.22); color: #f472b6; }
.email-tag-notification { background: rgba(148, 163, 184, 0.22); color: #94a3b8; }
.email-tag-security { background: rgba(248, 113, 113, 0.22); color: #f87171; }
.email-tag-urgent { background: color-mix(in srgb, var(--color-error, #e06c75) 25%, transparent); color: var(--color-error, #e06c75); font-weight: 600; }
.email-tag-reply-soon { background: rgba(240, 173, 78, 0.22); color: #f0ad4e; }
.email-tag-social { background: rgba(56, 189, 248, 0.22); color: #38bdf8; }
.email-tag-shopping { background: rgba(236, 72, 153, 0.22); color: #ec4899; }
.email-tag-calendar { background: rgba(167, 139, 250, 0.22); color: #a78bfa; }
.email-menu-wrap { position: relative; flex-shrink: 0; }
.email-menu-btn {
background: none; border: none; color: var(--fg); opacity: 0; cursor: pointer;
padding: 4px; transition: opacity 0.15s;
position: relative; top: 0px;
}
.email-dropdown {
position: absolute; right: 0; top: 100%; z-index: 9999;
min-width: 120px; background: var(--bg); border: 1px solid var(--border);
border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); padding: 4px 0;
}
.email-loading { padding: 16px; text-align: center; font-size: 12px; opacity: 0.5; }
.email-load-more { padding: 8px; text-align: center; }
.email-load-more-btn {
background: transparent; border: 1px solid var(--border); border-radius: 4px;
color: var(--fg); padding: 4px 12px; font-size: 11px; cursor: pointer;
}
.email-load-more-btn:hover { background: var(--hover-bg, rgba(255,255,255,0.05)); }
.email-spinner {
display: inline-block; width: 12px; height: 12px; border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff; border-radius: 50%; animation: email-spin 0.6s linear infinite;
vertical-align: middle; margin-right: 4px;
}
@keyframes email-spin { to { transform: rotate(360deg); } }
/* Email section doesn't collapse — hide the auto-injected chevron */
#email-section .section-collapse-btn { display: none !important; }
/* Compose + button: show on section hover, spin on button hover */
#email-compose-btn {
opacity: 0;
transition: opacity 0.15s ease;
}
#email-section:hover #email-compose-btn {
opacity: 0.7;
}
#email-compose-btn:hover {
opacity: 1 !important;
background: none !important;
}
#email-compose-btn svg {
width: 11px;
height: 11px;
transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#email-compose-btn .list-item-plus-label {
top: calc(50% - 0.5px);
color: var(--fg);
font-size: 10px;
}
#email-compose-btn:hover svg {
transform: rotate(180deg) scale(1.15);
}
/* Library row "+" — identical to the email compose "+": hidden until the row
is hovered, no accent box, and the same rotate-in animation. */
#library-new-doc-btn {
opacity: 0;
transition: opacity 0.15s ease;
}
#tool-library-btn:hover #library-new-doc-btn {
opacity: 0.7;
}
#library-new-doc-btn:hover {
opacity: 1 !important;
background: none !important;
}
#library-new-doc-btn svg {
transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
}
#library-new-doc-btn .list-item-plus-label {
top: calc(50% + 1px);
}
#library-new-doc-btn:hover svg {
transform: rotate(180deg) scale(1.15);
}
/* Satisfying send effect: email slides out to the right and fades */
.email-send-fx {
animation: email-send-slide 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
pointer-events: none;
}
@keyframes email-send-slide {
0% {
transform: translateX(0);
opacity: 1;
filter: blur(0);
}
60% {
filter: blur(1px);
}
100% {
transform: translateX(110%);
opacity: 0;
filter: blur(3px);
}
}
/* MD toolbar attach button — matches other toolbar buttons */
.md-toolbar-attach-btn {
background: none; border: none; color: var(--fg); opacity: 0.35;
padding: 4px 7px; cursor: pointer; min-height: 28px;
display: inline-flex; align-items: center; justify-content: center;
transition: opacity 0.1s;
}
.md-toolbar-attach-btn:hover { opacity: 1; }
/* Email reader icon buttons — vertical icon + label stack. */
.memory-toolbar-btn.reader-icon-btn {
width: 48px;
height: 44px;
padding: 4px 2px;
position: relative;
top: 1px;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
flex: 0 0 auto;
}
.memory-toolbar-btn.reader-icon-btn svg { width: 18px; height: 18px; }
.memory-toolbar-btn.reader-icon-btn.active {
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 40%, transparent);
color: var(--accent, var(--red));
}
/* The "More" (kebab) reader button — accent-colored so it stands out as the
actions-menu trigger. When the dropdown is open we add .reader-more-active
for a filled tint, matching the toggle states elsewhere. */
.memory-toolbar-btn.reader-icon-btn[data-act="more"] {
color: var(--accent, var(--red));
}
.memory-toolbar-btn.reader-icon-btn[data-act="more"] svg { fill: currentColor; }
.memory-toolbar-btn.reader-icon-btn.reader-more-active {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
color: var(--accent, var(--red));
}
/* Cookbook Serve dropdown triggers (the cached-model kebab + the saved-config
arrow) — accent-colored to signal they open a menu, with a filled tint when
their dropdown is open. Matches the email reader More button pattern. */
.hwfit-cached-menu-btn,
.cookbook-saved-arrow {
color: var(--accent, var(--red));
/* Base .memory-item-btn is flex with align-items but no justify-content,
so the kebab SVG sat left-aligned inside the button instead of being
horizontally centered. Pin it center. */
justify-content: center;
}
.hwfit-cached-menu-btn svg { fill: currentColor; }
.hwfit-cached-menu-btn.cookbook-menu-active,
.cookbook-saved-arrow.cookbook-menu-active {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
color: var(--accent, var(--red));
}
/* Mobile: center the kebab/menu dropdowns in the viewport so they don't
slide left/right as the window width changes (they were positioned via
inline `right: ...px` relative to the kebab button). Stable position is
easier to land on by touch. The inline `top:` set by the menu code is
preserved; we only override the horizontal axis. */
@media (max-width: 768px) {
.email-card-dropdown,
.cookbook-saved-menu,
.cookbook-dep-menu {
left: 50% !important;
right: auto !important;
transform: translateX(-50%);
}
}
/* Mobile-only Cancel item appended to dropdowns (kebab/More menus). On
desktop outside-click closes them cleanly; on touch that's fiddly, so
give an explicit Cancel row. Visually separated from the action items
above by a top divider + slight muted color. */
.dropdown-cancel-mobile { display: none; }
@media (max-width: 768px) {
.dropdown-cancel-mobile {
display: flex;
border-top: 1px solid var(--border);
margin-top: 4px;
padding-top: 8px;
opacity: 0.85;
}
}
/* Launch-command Copy button morphs into Cancel once Launch has been clicked. */
.hwfit-serve-copy.hwfit-serve-cancel {
border-color: var(--color-danger, #e06c75) !important;
color: var(--color-danger, #e06c75) !important;
}
.hwfit-serve-copy.hwfit-serve-cancel:hover {
background: color-mix(in srgb, var(--color-danger, #e06c75) 14%, transparent);
}
/* Email modal title unread badge — small accent pill next to "Email". */
.email-lib-unread-badge {
display: inline-block;
font-size: 0.55em;
font-weight: 700;
background: var(--accent-primary, var(--red));
color: var(--bg, #1a1a1a);
border-radius: 9px;
padding: 1px 7px;
margin-left: 8px;
vertical-align: 2px;
line-height: 1.3;
letter-spacing: 0.02em;
cursor: pointer;
}
.email-lib-unread-badge:hover { filter: brightness(1.08); }
#email-lib-modal.email-lib-unread-active .doclib-modal-content {
box-shadow:
0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 22%, transparent),
0 0 18px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent),
var(--shadow-lg, 0 18px 50px rgba(0, 0, 0, 0.35));
}
#email-lib-modal .email-lib-header-actions .minimize-btn {
position: relative;
left: 6px;
}
/* Per-card prev/next nav — only visible on the currently expanded card,
sits at the right of the title row next to the done check. */
.email-card-nav-arrows {
display: none;
margin-left: auto;
gap: 2px;
flex-shrink: 0;
align-items: center;
position: relative;
top: -3px;
left: 19px;
}
.doclib-card.email-card-expanded .email-card-nav-arrows {
display: inline-flex;
}
.email-card-header-menu {
display: none;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 28px;
height: 28px;
padding: 0;
border: none;
border-radius: 6px;
background: none;
color: var(--fg);
opacity: 0.65;
cursor: pointer;
position: relative;
top: -3px;
transition: opacity 0.12s, background 0.12s;
}
.doclib-card.email-card-expanded .email-card-header-menu {
display: inline-flex;
}
.email-card-header-menu:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.email-card-nav-btn {
background: none;
border: none;
color: var(--fg);
opacity: 0.65;
cursor: pointer;
padding: 6px 8px;
border-radius: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity 0.12s, background 0.12s;
}
.email-card-nav-btn:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.email-card-nav-btn:disabled {
opacity: 0.25;
cursor: default;
pointer-events: none;
}
/* ── Email document editor ── */
.doc-email-header {
display: flex; flex-direction: column; gap: 6px; padding: 10px 12px;
border-bottom: 1px solid var(--border); background: var(--bg); flex-shrink: 0;
}
.email-field { display: flex; align-items: center; gap: 8px; position: relative; }
.email-field label { font-size: 11px; font-weight: 600; color: var(--fg); opacity: 0.5; min-width: 50px; text-align: right; flex-shrink: 0; }
.email-field input {
flex: 1; width: 100%; background: transparent; border: 1px solid var(--border); border-radius: 4px;
padding: 5px 8px; font-size: 13px; font-family: inherit; color: var(--fg); outline: none;
min-width: 0;
}
.email-field input:focus { border-color: var(--accent, #4a9eff); }
@container docpane (max-width: 460px) {
.doc-email-header .email-field {
display: grid;
grid-template-columns: 1fr;
gap: 3px;
align-items: stretch;
}
.doc-email-header .email-field label {
min-width: 0;
text-align: left;
}
.doc-email-header .email-field input {
width: 100%;
}
}
/* Cc toggle and attach button are absolute so they don't steal width from the To input */
.email-field .email-cc-toggle {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
z-index: 2;
}
.email-field input { padding-right: 60px; }
.email-field #doc-email-cc, .email-field #doc-email-bcc, .email-field #doc-email-subject { padding-right: 8px; }
.doc-email-actions {
display: flex; gap: 8px; justify-content: flex-end; padding: 10px 14px;
border-top: 1px solid var(--border); background: var(--bg); flex-shrink: 0;
align-items: center;
}
/* Documents footer — X + Undo stay on the LEFT, Copy/Export group pushed
to the RIGHT. Email composer keeps its original flex-end layout. */
#doc-actions-footer {
justify-content: flex-start;
}
#doc-actions-footer .email-send-split {
margin-left: auto;
}
/* WYSIWYG email body — what the recipient sees, edited in place. */
.doc-email-richbody {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 12px 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
color: var(--fg);
background: var(--bg);
outline: none;
white-space: pre-wrap;
word-wrap: break-word;
}
.doc-email-richbody:empty::before {
content: "Write your email\2026";
opacity: 0.4;
pointer-events: none;
}
.doc-email-richbody a { color: var(--accent, #4af); }
.doc-email-richbody blockquote {
margin: 6px 0; padding-left: 10px;
border-left: 3px solid var(--border); opacity: 0.8;
}
.doc-email-richbody h1, .doc-email-richbody h2, .doc-email-richbody h3 { margin: 0.4em 0; }
.doc-email-richbody p { margin: 0.5em 0; }
.email-more-menu {
position: absolute; bottom: 100%; right: 0; margin-bottom: 6px;
min-width: 160px; background: var(--bg); border: 1px solid var(--border);
border-radius: 6px; box-shadow: 0 -4px 12px rgba(0,0,0,0.2); padding: 4px;
z-index: 100;
}
.email-send-btn {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 20%, transparent);
color: var(--accent-primary, var(--red));
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 50%, transparent);
border-radius: 6px;
padding: 0 14px; height: 28px; font-size: 11px; font-weight: 600; cursor: pointer;
font-family: inherit; transition: all 0.15s; white-space: nowrap;
display: inline-flex; align-items: center; gap: 6px;
}
/* Split send button: [ Send │ ▾ ] — the caret drops the send-options menu UP. */
.email-send-split { position: relative; display: inline-flex; }
.email-send-split .email-send-main {
border-top-right-radius: 0; border-bottom-right-radius: 0; border-right: none;
}
.email-send-split .email-send-caret {
border-top-left-radius: 0; border-bottom-left-radius: 0;
padding: 0 8px; gap: 0;
/* its left border is the single divider between the two halves */
border-left: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 50%, transparent);
}
.email-send-split .email-send-caret svg { transition: transform 0.15s ease; }
.email-send-split .email-send-caret[aria-expanded="true"] svg { transform: rotate(180deg); }
.email-send-btn svg { flex-shrink: 0; }
.email-send-btn:hover {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 30%, transparent);
border-color: var(--accent-primary, var(--red));
}
.email-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.email-draft-btn {
background: none; border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 60%, transparent);
border-radius: 6px;
padding: 0 12px; height: 28px; font-size: 11px; cursor: pointer;
font-family: inherit; transition: all 0.15s; white-space: nowrap;
}
.email-draft-btn:hover { border-color: var(--fg); color: var(--fg); }
.email-draft-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.email-discard-btn {
background: transparent;
/* Dim the label text via color-mix instead of `opacity:0.6` on the whole
button — opacity multiplies through to the SVG and washes out the accent
X glyph. Per-element color dimming keeps the X at full accent strength. */
color: color-mix(in srgb, var(--fg) 60%, transparent);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 14px; font-size: 12px; cursor: pointer; font-family: inherit;
}
/* The ✕ glyph itself reads in accent. */
.email-discard-btn svg { color: var(--accent-primary, var(--red)); opacity: 1; }
.email-discard-btn:hover { opacity: 1; color: var(--red, #e55); border-color: var(--red, #e55); }
/* Compose email "more" menu button — icon only, matches draft btn height */
#doc-email-more-btn {
background: none; border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 60%, transparent);
border-radius: 6px;
width: 28px; height: 28px; padding: 0;
cursor: pointer; font-family: inherit; transition: all 0.15s;
display: inline-flex; align-items: center; justify-content: center;
}
#doc-email-more-btn:hover {
border-color: var(--fg); color: var(--fg);
}
#doc-email-more-btn svg { opacity: 0.8; }
.email-more-wrap { display: inline-flex; }
.email-attachments {
display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 0 0 58px;
}
.email-attachment-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 8px; font-size: 11px; background: var(--hover-bg, rgba(255,255,255,0.05));
border: 1px solid var(--border); border-radius: 12px; color: var(--fg);
text-decoration: none; cursor: pointer; transition: background 0.15s, border-color 0.15s;
max-width: 200px; min-width: 0;
}
.email-attachment-chip:hover {
background: color-mix(in srgb, var(--accent) 15%, transparent);
border-color: var(--accent);
/* Expand chip on hover so the full filename is revealed. Native title-
attribute tooltips were slow / unreliable, so we just grow the chip. */
max-width: 90vw;
}
.email-attachment-chip > span:not(.att-size) {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
flex: 1 1 auto; min-width: 0;
}
.email-attachment-chip:hover > span:not(.att-size) {
overflow: visible;
text-overflow: clip;
}
.email-attachment-chip .att-size { opacity: 0.5; font-size: 10px; flex-shrink: 0; }
/* "Open in editor" launch icon — same prominent style on desktop AND mobile
(was 24px / dim / no border on desktop, easy to miss). Accent-tinted
background + border makes it read as a real action. */
.email-attachment-open {
display: inline-flex; align-items: center; gap: 4px;
height: 22px; padding: 0 9px; border-radius: 11px;
margin-left: 6px; flex-shrink: 0;
font-size: 10px; font-weight: 500; letter-spacing: 0.02em;
color: var(--accent-primary, var(--red));
background: color-mix(in srgb, var(--accent-primary, var(--red)) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--accent-primary, var(--red)) 40%, transparent);
cursor: pointer; transition: background 0.1s, border-color 0.1s, color 0.1s;
}
.email-attachment-open svg,
.email-attachment-open > svg {
width: 12px; height: 12px;
opacity: 0.9;
}
.email-attachment-open:hover {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 22%, transparent);
border-color: color-mix(in srgb, var(--accent-primary, var(--red)) 70%, transparent);
}
.email-attachment-open-label { line-height: 1; }
/* Collapsed chip: open button is icon-only (the labeled pill crowds the
small chip). Hover expands the chip and reveals the "Open" label too. */
.email-attachment-chip:not(:hover) .email-attachment-open-label {
display: none;
}
.email-attachment-chip:not(:hover) .email-attachment-open {
width: 22px;
padding: 0;
justify-content: center;
gap: 0;
}
@media (max-width: 768px) {
/* Mobile: keep the pill but ensure a comfortable touch target. */
.email-attachment-open {
height: 26px; padding: 0 10px;
min-height: 26px !important;
}
/* Attachment chip body — modest minimum height so the open icon sits
neatly without dominating. */
.email-attachment-chip {
padding: 6px 8px !important;
min-height: 36px !important;
}
}
/* Compose attachment chips (when sending new email) */
.email-compose-atts {
display: flex; flex-wrap: wrap; gap: 6px;
padding: 6px 0 0 58px;
}
.email-compose-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 4px 4px 8px; font-size: 11px;
background: var(--hover-bg, rgba(255,255,255,0.05));
border: 1px solid var(--border); border-radius: 12px; color: var(--fg);
}
.email-compose-chip .compose-chip-name { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.email-compose-chip .att-size { opacity: 0.5; font-size: 10px; }
.email-compose-chip .compose-chip-remove {
background: none; border: none; color: var(--fg); opacity: 0.5;
font-size: 16px; line-height: 1; padding: 0 4px; cursor: pointer;
}
.email-compose-chip .compose-chip-remove:hover { opacity: 1; color: var(--red, #e55); }
.email-cc-toggle {
background: none; border: none; color: var(--fg);
opacity: 0.4; font-size: 11px; cursor: pointer;
padding: 4px 8px; font-family: inherit;
}
.email-cc-toggle:hover { opacity: 1; color: var(--accent, #4a9eff); }
.email-autocomplete {
position: absolute; top: 100%; left: 58px; right: 0; z-index: 1000;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2); max-height: 240px; overflow-y: auto;
margin-top: 2px;
}
.contact-suggestion {
display: flex; justify-content: space-between; gap: 8px;
padding: 6px 10px; font-size: 12px; cursor: pointer;
border-bottom: 1px solid var(--border);
}
.contact-suggestion:last-child { border-bottom: none; }
.contact-suggestion:hover, .contact-suggestion.active {
background: color-mix(in srgb, var(--accent) 15%, transparent);
}
.contact-suggestion .contact-name { font-weight: 600; color: var(--fg); }
.contact-suggestion .contact-email { opacity: 0.6; font-size: 11px; }
.email-ai-reply-btn {
background: none;
color: color-mix(in srgb, var(--fg) 60%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0 12px; height: 28px; font-size: 11px; cursor: pointer;
font-family: inherit; transition: all 0.15s;
display: inline-flex; align-items: center; white-space: nowrap;
}
.email-ai-reply-btn:hover {
border-color: var(--accent, #4a9eff);
color: var(--accent, #4a9eff);
}
.email-ai-reply-btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Align select/new buttons in popup library toolbar */
#email-lib-select-btn, #email-lib-refresh-btn, #email-lib-compose-btn { position: relative; top: -4px; }
/* Select + Refresh sit slightly lower than Compose on desktop. */
#email-lib-select-btn, #email-lib-refresh-btn { top: -2px; }
@media (max-width: 768px) {
/* On mobile they're 1px higher than desktop. */
#email-lib-select-btn, #email-lib-refresh-btn { top: -3px; }
}
/* On mobile, the "New" (compose) button deserves a touch-friendly size
so it stands out from the smaller toolbar buttons next to it. Lift
it slightly visually with a bigger SVG, padding, and font. */
@media (max-width: 768px) {
#email-lib-compose-btn {
padding: 8px 14px !important;
font-size: 13px !important;
min-height: 36px;
border-radius: 8px;
font-weight: 600;
top: -2px;
}
#email-lib-compose-btn svg {
width: 14px !important;
height: 14px !important;
margin-right: 5px !important;
}
}
/* ── Notes ── */
/* Notes panel — body-level flex sibling of chat-container */
body.notes-view .chat-container { flex: 1; min-width: 0; }
/* Mobile: notes panel takes over full screen */
@media (max-width: 768px) {
body.notes-view .notes-pane {
position: fixed;
inset: 0;
max-width: 100%;
width: 100% !important;
/* Desktop sets height: min(80vh, 820px) which wins over inset:0's bottom:0
and leaves a gap at the bottom of the viewport. Force full height here. */
height: 100dvh !important;
max-height: 100dvh !important;
z-index: 170;
/* Bottom-sheet treatment like the document editor: stroked, rounded top,
slides up from the bottom. */
border: 1px solid var(--border);
border-bottom: none;
border-radius: 14px 14px 0 0;
/* Pane itself never scrolls — its inner .notes-pane-body is the scroller.
Two nested scrollers caused the body's flex:1 to lose its height bound
on Firefox mobile and the last row got clipped off-screen. */
overflow: hidden;
animation: sheet-enter 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) both;
transform-origin: bottom center;
}
/* Required for the flex:1 body to actually constrain to remaining pane
height instead of expanding to its content (default min-height:auto on
flex items). Without this both grid and list views overflow the viewport
bottom on phones. */
body.notes-view .notes-pane-body {
min-height: 0;
-webkit-overflow-scrolling: touch;
}
body.notes-view #notes-divider {
display: none;
}
body.notes-view .notes-mobile-grabber {
display: block;
flex-shrink: 0;
height: 18px;
position: relative;
background: var(--bg);
touch-action: none;
cursor: grab;
}
body.notes-view .notes-mobile-grabber::before {
content: '';
position: absolute;
top: 7px;
left: 50%;
transform: translateX(-50%);
width: 36px;
height: 4px;
background: var(--fg);
opacity: 0.25;
border-radius: 2px;
}
body.notes-view .chat-container {
display: none;
}
body.notes-view .mobile-new-chat-btn,
body.notes-view .hamburger-btn {
display: none !important;
}
body.notes-view #notes-close-btn {
display: inline-flex !important;
}
}
/* ── Mobile notes UX ────────────────────────────────────
Tiles become read-only previews on touch ≤768px wide.
Tap opens a fullscreen overlay where editing happens; long-press
toggles drag-to-reorder. Wired up in static/js/notes.js. */
body.notes-mobile-mode .note-card-corner,
body.notes-mobile-mode .note-card-corner-edit,
body.notes-mobile-mode .note-card-corner-archive,
body.notes-mobile-mode .note-card-corner-copy,
body.notes-mobile-mode .note-card-corner-trash,
body.notes-mobile-mode .note-card-corner-unarchive,
body.notes-mobile-mode .note-card-corner-done,
body.notes-mobile-mode .note-card-edit-corner,
body.notes-mobile-mode .note-card-done,
body.notes-mobile-mode .note-card-actions,
body.notes-mobile-mode .note-cl-quickadd,
body.notes-mobile-mode .note-card .note-checkbox-rm,
/* Panel-fullscreen toggle is redundant on mobile — the panel always
fills the viewport already. */
body.notes-mobile-mode #notes-fullscreen-toggle {
display: none !important;
}
/* Header toggle icons (archive view, list/grid switch) — defaults are
tuned for the dense desktop header; on mobile bump them slightly so
they read as tap targets without dominating the header. */
body.notes-mobile-mode #notes-archive-toggle,
body.notes-mobile-mode #notes-view-toggle {
width: 30px;
height: 30px;
padding: 6px;
}
body.notes-mobile-mode #notes-archive-toggle svg,
body.notes-mobile-mode #notes-view-toggle svg {
width: 17px;
height: 17px;
}
/* Disable inline checkbox toggling on tiles — checking off a todo
requires opening the note. The visual dot stays but it's styled
as a passive status indicator, not a tappable button. */
body.notes-mobile-mode .note-card .note-checkbox,
body.notes-mobile-mode .note-card .note-checkbox-rm {
pointer-events: none;
}
body.notes-mobile-mode .note-card .note-check-dot,
body.notes-mobile-mode .note-card .note-checkbox-dot,
body.notes-mobile-mode .note-card .note-cl-dot {
opacity: 0.55;
transform: none !important;
transition: none !important;
cursor: default !important;
}
/* The whole card is the tap target on mobile — bigger feedback */
body.notes-mobile-mode .note-card {
cursor: pointer;
-webkit-tap-highlight-color: transparent;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
body.notes-mobile-mode .note-card:active {
transform: scale(0.985);
}
/* Drag-to-reorder mode — desaturate the grid and sweep a subtle
shimmer across every tile so it's obvious you're in rearrange mode
(the Google Keep "active edit" cue). Also disable scroll inside the
panel so the drag works smoothly. */
body.notes-drag-mode .notes-pane-body {
overflow: hidden;
touch-action: none;
}
body.notes-drag-mode .note-card {
cursor: grab;
transform: scale(0.985);
filter: saturate(0.7);
opacity: 0.92;
position: relative;
overflow: hidden;
transition: transform 0.22s cubic-bezier(0.2, 0.7, 0.3, 1),
filter 0.18s ease,
opacity 0.18s ease;
}
body.notes-drag-mode .note-card::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
115deg,
transparent 30%,
color-mix(in srgb, var(--fg) 14%, transparent) 50%,
transparent 70%
);
background-size: 250% 100%;
animation: notes-drag-shimmer 2.1s linear infinite;
border-radius: inherit;
z-index: 1;
mix-blend-mode: overlay;
}
@media (max-width: 768px) {
body.notes-drag-mode .note-card {
filter: none;
opacity: 0.96;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
}
body.notes-drag-mode .note-card::after {
display: none;
}
}
@keyframes notes-drag-shimmer {
0% { background-position: 250% 0; }
100% { background-position: -150% 0; }
}
/* The grabbed card lifts above its siblings; suppress the shimmer on
it so it reads as the "live" element being moved. */
body.notes-drag-mode .note-card.note-card-dragging {
transform: scale(1.06);
box-shadow: 0 16px 40px rgba(0,0,0,0.45);
opacity: 1;
filter: none;
z-index: 10001;
cursor: grabbing;
transition: box-shadow 0.18s ease;
}
body.notes-drag-mode .note-card.note-card-dragging::after { display: none; }
body.notes-drag-mode .note-card .note-card-pin {
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
body.notes-drag-mode .note-card-pinned .note-card-pin,
body.notes-drag-mode .note-card-pin.active,
body.notes-drag-mode .note-card-pin svg {
visibility: hidden !important;
opacity: 0 !important;
}
/* Placeholder — same footprint as the lifted card, dotted outline so
the drop target is visible while the card follows the finger. */
.note-card-placeholder {
background: color-mix(in srgb, var(--fg) 5%, transparent);
border: 2px dashed color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 8px;
box-sizing: border-box;
flex-shrink: 0;
transition: width 0.18s ease, height 0.18s ease;
}
@media (prefers-reduced-motion: reduce) {
body.notes-drag-mode .note-card::after { animation: none; }
}
/* ── Fullscreen single-note edit overlay ──────────────── */
.note-fullscreen-overlay {
position: fixed;
inset: 0;
z-index: 10500;
background: var(--bg);
display: flex;
flex-direction: column;
opacity: 0;
/* Bigger zoom range so the transition reads as a real "tile expands
into a page" motion. transform-origin is set per-tile by JS. */
transform: scale(0.45);
transform-origin: center center;
transition: opacity 0.28s ease, transform 0.32s cubic-bezier(0.18, 0.74, 0.24, 1.06);
will-change: transform, opacity;
}
.note-fullscreen-overlay.open {
opacity: 1;
transform: scale(1);
}
.note-fullscreen-overlay.closing {
opacity: 0;
transform: scale(0.7);
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.4, 0, 0.6, 1);
}
.note-fullscreen-header {
flex-shrink: 0;
padding: 8px 12px;
display: flex;
align-items: center;
gap: 6px;
border-bottom: 1px solid var(--border);
}
.note-fullscreen-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
}
/* Compact the relocated Archive/Delete buttons so two of them fit
alongside the back chevron without crowding. */
.note-fullscreen-actions .note-form-text-btn {
padding: 8px 12px;
font-size: 12px;
border-radius: 8px;
}
.note-fullscreen-actions .note-form-text-btn svg {
width: 14px;
height: 14px;
margin-right: 4px;
}
.note-fullscreen-back {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px; height: 40px;
background: none;
border: none;
color: var(--fg);
border-radius: 8px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.note-fullscreen-back:active {
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.note-fullscreen-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px 14px 24px;
}
/* The reused .note-form inside the fullscreen overlay should breathe —
it was originally sized to slot into a grid cell. */
.note-fullscreen-body .note-form {
max-width: 720px;
margin: 0 auto;
border: none;
box-shadow: none;
background: transparent;
}
/* Bigger touch targets inside the fullscreen overlay. The default form
is tuned for the grid-card context (tiny icons fit a 200px wide
tile); on a phone-sized full-bleed page we want chunky thumb-sized
controls. */
.note-fullscreen-back svg {
width: 28px;
height: 28px;
}
.note-fullscreen-overlay .note-form-type-pill {
padding: 10px 16px;
font-size: 14px;
}
.note-fullscreen-overlay .note-form-type-pill svg {
width: 16px;
height: 16px;
}
.note-fullscreen-overlay .note-cl-row {
padding: 8px 0;
gap: 8px;
align-items: center;
}
.note-fullscreen-overlay .note-cl-grip {
font-size: 22px;
line-height: 1;
padding: 6px 8px;
opacity: 0.5;
cursor: grab;
touch-action: none;
user-select: none;
/* Pull 4px toward the left edge of the row. */
margin-left: -4px;
}
.note-fullscreen-overlay .note-cl-grip:active { cursor: grabbing; opacity: 0.9; }
.note-fullscreen-overlay .note-cl-dot {
width: 16px;
height: 16px;
border-width: 2px;
border-radius: 50%;
flex-shrink: 0;
position: relative;
/* Generous invisible tap target around the visible dot */
}
.note-fullscreen-overlay .note-cl-dot::before {
content: '';
position: absolute;
inset: -12px;
}
/* Filled checkmark glyph when the row is marked done — gives a clear
visual cue beyond the colored fill. Nudged up 2px so it sits
centered in the smaller dot. */
.note-fullscreen-overlay .note-cl-row.done .note-cl-dot::after {
content: '';
position: absolute;
left: 2px; top: 1px;
width: 8px; height: 4px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(-45deg);
}
.note-fullscreen-overlay .note-cl-text {
font-size: 14px;
padding: 7px 6px;
min-height: 32px;
}
.note-fullscreen-overlay .note-cl-rm {
width: 40px;
height: 40px;
font-size: 28px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
/* Nudge 4px further toward the right edge of the row. */
margin-right: -4px;
}
.note-fullscreen-overlay .note-cl-rm:active { opacity: 1; }
/* Lifted row + placeholder while dragging a checklist item. Mirrors
the card-drag look so the two interactions feel consistent. */
.note-cl-row-dragging {
box-shadow: 0 12px 28px rgba(0,0,0,0.4);
background: var(--bg);
border-radius: 8px;
opacity: 0.98;
}
.note-cl-row-placeholder {
background: color-mix(in srgb, var(--fg) 5%, transparent);
border: 2px dashed color-mix(in srgb, var(--fg) 22%, transparent);
border-radius: 6px;
margin: 4px 0;
box-sizing: border-box;
transition: height 0.16s ease;
}
.note-fullscreen-overlay .note-form-text-btn {
padding: 10px 16px;
font-size: 14px;
}
.note-fullscreen-overlay .note-form-text-btn svg {
width: 16px;
height: 16px;
}
/* + Add (new checklist item) was tiny — bump for thumbs */
.note-fullscreen-overlay .note-cl-add {
display: inline-flex;
align-items: center;
padding: 10px 18px;
font-size: 15px;
font-weight: 600;
border-radius: 8px;
letter-spacing: -0.01em;
opacity: 0.85;
}
.note-fullscreen-overlay .note-cl-add:active { opacity: 1; }
/* + Add and the camera/photo button share a row inside checklists, so
they're both within thumb reach when editing items. The whole row
reads as a single dashed "tap to add" surface — tapping ANYWHERE
on it triggers + Add (the photo button stays its own target). */
.note-fullscreen-overlay .note-cl-add-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
padding: 4px 8px 4px 4px;
border: 1px solid color-mix(in srgb, var(--fg) 22%, transparent);
border-radius: 10px;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease;
}
.note-fullscreen-overlay .note-cl-add-row:active {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--fg) 36%, transparent);
}
.note-fullscreen-overlay .note-cl-add-row .note-cl-add {
flex: 1;
justify-content: flex-start;
background: transparent;
border: none;
}
.note-fullscreen-overlay .note-cl-add-row .note-cl-add { margin: 0; }
.note-fullscreen-overlay .note-cl-add-row .note-form-photo-btn {
display: inline-flex;
align-items: center;
gap: 4px;
width: auto;
height: 36px;
padding: 0 12px 0 10px;
border-radius: 8px;
/* No border / no background — the surrounding +Add row provides the
bordered container, the camera is just an icon inside it. */
border: none;
background: transparent;
}
.note-fullscreen-overlay .note-cl-add-row .note-form-photo-btn::before {
content: '+';
font-size: 17px;
font-weight: 600;
line-height: 1;
opacity: 0.85;
}
.note-fullscreen-overlay .note-cl-add-row .note-form-photo-btn svg {
width: 16px;
height: 16px;
}
/* Read-mode overlay shown over the textarea for plain notes. Renders
linkified content so URLs are tappable. Click anywhere off a link
to flip to edit mode (focuses the underlying textarea). */
.note-form-content-reader {
font-size: 15px;
line-height: 1.55;
padding: 10px 6px;
white-space: pre-wrap;
word-wrap: break-word;
color: var(--fg);
cursor: text;
min-height: 40px;
}
.note-form-content-reader a {
color: var(--accent, var(--red));
text-decoration: underline;
text-underline-offset: 2px;
}
.note-form-content-reader a:active { opacity: 0.7; }
/* Per-row read mode for todo items — replaces the bare with a
clickable linkified span when the value contains a URL. */
.note-fullscreen-overlay .note-cl-text-reader {
flex: 1;
padding: 7px 6px;
font-size: 14px;
line-height: 1.4;
min-height: 32px;
display: inline-flex;
align-items: center;
word-break: break-word;
color: var(--fg);
cursor: text;
}
.note-fullscreen-overlay .note-cl-text-reader a {
color: var(--accent, var(--red));
text-decoration: underline;
text-underline-offset: 2px;
}
.note-fullscreen-overlay .note-cl-text-reader a:active { opacity: 0.7; }
.note-fullscreen-overlay .note-cl-row.done .note-cl-text-reader {
opacity: 0.5;
text-decoration: line-through;
}
/* Reminder bell + other header icon-buttons. Tightened down 2px so
the bell sits more proportionally next to the title field. */
.note-fullscreen-overlay .note-form-icon-btn {
width: 36px;
height: 36px;
}
.note-fullscreen-overlay .note-form-icon-btn svg {
width: 16px;
height: 16px;
}
/* The plain-note textarea was rows="4" — fine for an in-grid card but
cramped in fullscreen. Let it grow to fill the available height so
writing feels like a real notepad, not a tweet box. The read-mode
reader inherits the same minimum so the layout doesn't jump when
the user taps to edit. */
.note-fullscreen-overlay .note-form-content,
.note-fullscreen-overlay .note-form-content-reader {
min-height: 55vh;
font-size: 15px;
line-height: 1.55;
}
/* Tags input is JS-relocated into the bottom actions row, pinned
left of Cancel/Update. Style it as a dashed pill so it reads as
metadata rather than another control. */
.note-fullscreen-overlay .note-form-actions-group .note-form-label {
flex: 1 1 auto;
min-width: 0;
font-size: 13px;
padding: 8px 10px;
margin: 0 auto 0 0;
background: transparent;
border: 1px dashed color-mix(in srgb, var(--fg) 16%, transparent);
border-radius: 6px;
text-align: left;
}
/* Reorganized meta-row layout for fullscreen: color picker pinned to
the LEFT, type-toggle (Note/Todo/Draw) shoved to the RIGHT, with
the photo button next to the color picker. Tags are moved up into
the form header (next to the reminder bell) via JS. */
.note-fullscreen-overlay .note-form-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding-top: 8px;
}
.note-fullscreen-overlay .note-color-picker { order: 1; }
.note-fullscreen-overlay .note-form-photo-btn { order: 2; }
.note-fullscreen-overlay .note-form-type-seg {
order: 3;
margin-left: auto; /* shove right */
/* The overlay enlarges the pills (padding 10px), but the base seg is only
28px tall with overflow:hidden — which clipped them. Grow the track so
the bigger touch targets fit. */
height: 40px;
border-radius: 12px;
}
.note-fullscreen-overlay .note-form-actions-group {
order: 4;
width: 100%;
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
}
.note-fullscreen-overlay .note-form-actions-group .note-form-cancel,
.note-fullscreen-overlay .note-form-actions-group .note-form-save {
flex: 0 0 auto;
}
.notes-mobile-grabber { display: none; }
/* Notes panel: mounted like a window, docked to the right on desktop. */
.notes-pane-backdrop {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
pointer-events: none;
animation: notes-backdrop-fade-in 180ms ease both;
}
.notes-pane-backdrop .notes-pane {
pointer-events: auto;
}
.notes-pane-backdrop:has(.notes-pane.modal-right-docked),
.notes-pane-backdrop:has(.notes-pane.modal-left-docked) {
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
pointer-events: none;
}
.notes-pane-backdrop:has(.notes-pane.modal-right-docked) .notes-pane,
.notes-pane-backdrop:has(.notes-pane.modal-left-docked) .notes-pane {
pointer-events: auto;
}
.notes-pane-backdrop:has(.notes-pane.notes-window-fullscreen) {
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
@keyframes notes-backdrop-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.notes-pane {
display: flex;
flex-direction: column;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
width: min(880px, 92vw);
height: min(80vh, 820px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
letter-spacing: -0.015em;
/* Smooth open: scale up + fade in from the centre. */
animation: notes-pane-enter 200ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
transform-origin: center center;
will-change: transform, opacity;
}
.notes-pane.modal-right-docked {
border-left: 1px solid var(--border);
border-radius: 0;
box-shadow: -4px 0 14px rgba(0, 0, 0, 0.18);
transition: none !important;
}
.notes-pane.modal-left-docked {
border-right: 1px solid var(--border);
border-radius: 0;
box-shadow: 4px 0 14px rgba(0, 0, 0, 0.18);
transition: none !important;
}
@keyframes notes-pane-enter {
from { transform: scale(0.96); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.notes-pane.notes-pane-leaving {
animation: notes-pane-leave 160ms cubic-bezier(0.4, 0, 1, 1) both;
pointer-events: none;
}
@keyframes notes-pane-leave {
from { transform: scale(1); opacity: 1; }
to { transform: scale(0.96); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.notes-pane,
.notes-pane.notes-pane-leaving { animation: none; }
}
.notes-pane-header {
display: flex;
align-items: center;
gap: 4px;
justify-content: space-between;
padding: 0 0 6px;
margin: 10px 10px 0;
border-bottom: 1px solid var(--border);
background: inherit;
flex-shrink: 0;
flex-wrap: nowrap;
min-height: 30px;
overflow: hidden;
}
/* Archive mode — sepia tint over the panel + a header strip so it's clearly
a different view. Active reminders sort + glow rules don't apply here. */
.notes-pane-archive {
background: color-mix(in srgb, #b48a4a 6%, var(--bg));
}
.notes-pane-archive .notes-pane-header {
background: color-mix(in srgb, #b48a4a 12%, var(--bg));
border-bottom-color: color-mix(in srgb, #b48a4a 35%, var(--border));
}
.notes-pane-archive .notes-pane-body {
background: color-mix(in srgb, #b48a4a 4%, var(--bg));
}
/* Smooth the archive↔active swap so the colour tint eases in/out instead
of snapping. Covers the pane, its header, and its body. */
.notes-pane,
.notes-pane .notes-pane-header,
.notes-pane .notes-pane-body {
transition: background 0.28s ease, border-color 0.28s ease;
}
.notes-pane-archive .notes-pane-title::after {
content: 'Archive';
margin-left: 8px;
padding: 2px 8px 2px 22px;
font-size: 9px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: #b48a4a;
background-color: color-mix(in srgb, #b48a4a 18%, transparent);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b48a4a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='5' rx='1'/%3E%3Cpath d='M4 8v11a2 2 0 002 2h12a2 2 0 002-2V8'/%3E%3Cpath d='M10 12h4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 6px center;
background-size: 11px 11px;
border: 1px solid color-mix(in srgb, #b48a4a 45%, transparent);
border-radius: 10px;
vertical-align: middle;
}
/* Hide the "Add a to-do…" quick-add bar in archive view — you can't add
new items from the archive. */
.notes-pane-archive .notes-quick-add { display: none !important; }
.notes-pane-archive .note-card {
background: color-mix(in srgb, #b48a4a 8%, var(--panel));
border-color: color-mix(in srgb, #b48a4a 25%, var(--border));
}
.notes-pane-header .doc-action-icon-btn {
flex-shrink: 0;
}
.notes-pane-header .notes-pane-title {
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
flex-shrink: 1;
}
.notes-pane-footer {
padding: 6px 8px;
border-top: 1px solid var(--border);
background: var(--panel);
flex-shrink: 0;
display: flex;
gap: 6px;
}
.notes-new-btn {
background: none;
border: 1px dashed color-mix(in srgb, var(--accent) 40%, transparent);
color: color-mix(in srgb, var(--fg) 50%, transparent);
border-radius: 6px;
padding: 6px 0;
flex: 1;
cursor: pointer;
font-size: 11px;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: border-color 0.15s, color 0.15s;
}
.notes-new-btn:hover {
border-color: var(--accent);
color: var(--fg);
}
.notes-pane-title {
/* Match the other tool headers (.modal-header h4). */
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.03em;
color: var(--red);
white-space: nowrap;
height: 24px;
line-height: 22px;
padding: 0 6px;
margin: 6px 0 0 0;
display: inline-block;
vertical-align: middle;
/* Optical nudge — sits 1px above its row baseline on desktop, more on mobile
where the surrounding controls are taller. Overridden in the mobile
media block below. */
position: relative;
top: -1px;
box-sizing: border-box;
}
@media (max-width: 768px) {
.notes-pane-title { top: -4px; }
.notes-pane-title svg {
position: relative;
top: -2px;
}
/* Mobile header is tight — keep Archive / Toggle View icon-only. */
.notes-header-btn-label { display: none; }
.notes-header-text-btn { width: 32px !important; padding: 0 !important; }
}
.notes-header-btn-label {
font-size: 11px;
font-weight: 500;
line-height: 1;
letter-spacing: 0.2px;
white-space: nowrap;
}
.notes-header-text-btn {
width: auto !important;
padding: 0 8px !important;
}
.notes-pane-body {
flex: 1;
overflow-y: auto;
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
scrollbar-width: thin;
}
.note-card.note-card-sliding-out {
transform: translateX(120%);
opacity: 0;
transition: transform 0.32s ease-in, opacity 0.32s ease-in;
pointer-events: none;
}
/* ⋯ corner menu button (replaced the copy corner button) */
.note-card-corner-menu {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
padding: 2px;
opacity: 0.5;
transition: opacity 0.15s;
display: inline-flex;
}
.note-card-corner-menu:hover { opacity: 0.9; }
/* Dropdown spawned by the ⋯ menu */
.note-corner-menu-dropdown {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
padding: 4px;
min-width: 168px;
font-size: 12px;
animation: ncm-pop 0.12s ease-out both;
}
@keyframes ncm-pop { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
.note-corner-menu-dropdown .ncm-item {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 12px;
text-align: left;
padding: 8px 10px;
border-radius: 6px;
cursor: pointer;
}
.note-corner-menu-dropdown .ncm-item:hover {
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
/* "Agent" tag on a note that has a linked agent chat session */
.note-agent-tag {
align-self: flex-start;
display: inline-flex;
align-items: center;
gap: 5px;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
color: var(--accent, var(--red));
border-radius: 999px;
padding: 3px 10px 3px 8px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
margin-top: 2px;
transition: background 0.12s;
}
.note-agent-tag:hover { background: color-mix(in srgb, var(--accent, var(--red)) 24%, transparent); }
.note-card {
/* Same tint that .doclib-card uses so a default (uncolored) note
visually separates from the panel background it sits on. */
background: color-mix(in srgb, var(--fg) 3%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
/* Extra right padding so the corner buttons (pin / edit / copy / done)
never collide with the title, and a min-height so an empty/short
note is still tall enough to fit that row of corner controls. */
padding: 10px 14px;
padding-right: 30px;
min-height: 64px;
cursor: default;
transition: background 0.15s;
display: flex;
flex-direction: column;
gap: 4px;
/* Keep drawings/images clipped to the card so they can't spill outside it —
but allow a small overhang (the pin bubble sits at right:-8px and would
otherwise be cropped). overflow-clip-margin extends the clip-box without
letting full images bleed past. */
overflow: clip;
overflow-clip-margin: 14px;
/* Don't shrink inside the flex-column list pane — otherwise cards squish to
fit (clipped/too short) and the container never overflows, so it won't
scroll. Keeping natural height makes the list overflow → scrollable. */
flex-shrink: 0;
}
.note-card:hover:not([class*="note-color-"]) {
background: color-mix(in srgb, var(--fg) 6%, transparent);
}
.note-card { position: relative; }
.note-card-image { max-width: 100%; }
/* Mobile: bigger, more legible note cards + a sticky quick-add bar so you can
add a to-do without scrolling back up; and keep media inside the card. */
@media (max-width: 768px) {
.note-card { padding: 14px 16px; padding-right: 34px; min-height: 84px; font-size: 15px; }
.note-card-image { max-height: 260px; max-width: 100%; object-fit: cover; }
/* Sticky create-todo bar — pin it to the top of the scrolling list pane.
Safe now that the cards no longer shrink (the list actually overflows). */
.notes-quick-add {
position: sticky;
top: -8px;
z-index: 12;
flex-shrink: 0;
margin-top: 0;
box-shadow: 0 -8px 0 8px var(--panel), 0 8px 0 0 var(--panel);
}
}
.note-card-pinned {
border-top: 2px solid color-mix(in srgb, var(--fg) 30%, transparent);
}
.note-card.note-card-reminder-fired {
animation: note-reminder-pulse 1.2s ease-in-out 2;
}
@keyframes note-reminder-pulse {
0%, 100% {
box-shadow:
inset 0 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent),
0 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 0%, transparent);
}
50% {
box-shadow:
inset 0 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent),
0 0 0 5px color-mix(in srgb, var(--accent, var(--red)) 26%, transparent),
0 0 18px color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
}
}
/* Sticky outside glow — applied to the exact note whose reminder fired.
It stays until the user interacts with that card. */
.note-card.note-card-reminder-fired-sticky {
position: relative;
outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 36%, transparent);
outline-offset: 2px;
animation: note-reminder-glow 1.6s ease-in-out infinite;
}
@keyframes note-reminder-glow {
0%, 100% {
box-shadow:
inset 0 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent),
0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 14%, transparent),
0 0 14px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
}
50% {
box-shadow:
inset 0 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent),
0 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 22%, transparent),
0 0 26px color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
}
}
.note-card-selected {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* Hover-only buttons (Google Keep style) */
.note-card-select,
.note-card-pin {
position: absolute;
top: -8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 50%;
width: 18px;
height: 18px;
padding: 0;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
color: var(--fg);
opacity: 0.7;
z-index: 2;
transition: opacity 0.15s, transform 0.15s, background 0.15s;
}
.note-card-select { left: -8px; }
.note-card-pin { right: -8px; }
/* Pencil + checkmark glyphs both have empty space along the top of their
24×24 viewBox, so flex-centering leaves them sitting visually high.
Nudge the inner SVG down a touch to compensate. */
.note-card-edit-corner svg,
.note-card-done svg {
position: relative;
top: 0.5px;
}
.note-card-edit-corner {
position: absolute;
/* Both Edit + Done at top-right: Edit on the left (right:40px),
Done on the right (right:6px). */
top: 6px; right: 40px;
width: 28px; height: 28px;
/* Fully invisible at rest. Materializes (with a solid panel fill blocking
any text underneath) only when the card is hovered or the button is
focused. Same rule on touch — opacity stays 0 until the card is tapped
to expose actions. */
background: transparent;
border: 1px solid transparent;
border-radius: 50%;
cursor: pointer; padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg); opacity: 0;
touch-action: manipulation;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
z-index: 2;
}
.note-card:hover .note-card-edit-corner {
opacity: 0.85;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
.note-card-edit-corner:hover,
.note-card-edit-corner:focus-visible {
opacity: 1 !important;
background: color-mix(in srgb, var(--accent) 18%, var(--panel)) !important;
border-color: color-mix(in srgb, var(--accent) 50%, var(--border)) !important;
}
@media (hover: none) {
.note-card-edit-corner {
opacity: 0.7;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
}
/* Copy corner — bottom-right counterpart to the edit/done corner buttons.
Round, subtle, hover-reveal on desktop and always faintly visible on
touch so mobile users can tap it without needing hover. */
.note-card-copy-corner {
position: absolute;
bottom: 6px;
right: 6px;
top: auto;
width: 26px;
height: 26px;
background: transparent;
border: 1px solid transparent;
border-radius: 50%;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg);
opacity: 0;
touch-action: manipulation;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
z-index: 2;
}
.note-card:hover .note-card-copy-corner {
opacity: 0.7;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
.note-card-copy-corner:hover,
.note-card-copy-corner:focus-visible {
opacity: 1 !important;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, var(--panel)) !important;
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border)) !important;
}
@media (hover: none) {
.note-card-copy-corner {
opacity: 0.6;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
}
/* Pin button on mobile — only reveal once the user has long-pressed to
enter rearrange (drag) mode. Outside drag mode the card stays clean. */
body.notes-mobile-mode.notes-drag-mode .note-card-pin {
display: flex !important;
opacity: 0.7;
}
body.notes-mobile-mode.notes-drag-mode .note-card-pin.active {
opacity: 1;
}
/* Unarchive corner reuses the pencil's spot in archive view. */
.note-card-unarchive-corner:hover,
.note-card-unarchive-corner:focus-visible {
background: color-mix(in srgb, var(--accent) 22%, transparent) !important;
border-color: color-mix(in srgb, var(--accent) 50%, var(--border)) !important;
color: var(--accent);
}
/* "Done" pill in the bottom-right of every active note card. Visible-only on
hover (and always on touch) so it's clearly readable but doesn't clutter
the grid at rest. Distinct from the edit pencil so it's obvious what it
does — green checkmark + label, not just an icon. */
/* Matches .note-card-edit-corner footprint (22×22 circle), just anchored
to the bottom-right corner instead of top-right. */
.note-card-done {
position: absolute;
top: 6px;
right: 6px;
width: 28px;
height: 28px;
background: transparent;
border: 1px solid transparent;
border-radius: 50%;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--green, #98c379);
opacity: 0;
touch-action: manipulation;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
z-index: 2;
}
.note-card:hover .note-card-done {
opacity: 0.85;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
.note-card-done:hover,
.note-card-done:focus-visible {
opacity: 1 !important;
background: color-mix(in srgb, var(--green, #98c379) 22%, var(--panel)) !important;
border-color: color-mix(in srgb, var(--green, #98c379) 50%, var(--border)) !important;
}
@media (hover: none) {
.note-card-done {
opacity: 0.7;
background: var(--panel);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
}
}
/* Edit + Done are now side-by-side at top-right by default — title-only
fallback no longer needed. */
.note-card-selectmode .note-card-done { display: none !important; }
/* Copy corner — bottom-right of the card. Same opaque-fill pattern as
trash/unarchive so the underlying text/image never bleeds through. */
.note-card-corner-copy {
position: absolute;
bottom: 6px;
right: 6px;
width: 28px;
height: 28px;
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
border-radius: 50%;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg);
visibility: hidden;
touch-action: manipulation;
transition: background 0.15s, border-color 0.15s, color 0.15s;
z-index: 2;
}
.note-card-corner-copy svg { position: relative; top: 1px; opacity: 0.85; transition: opacity 0.15s; }
.note-card:hover .note-card-corner-copy { visibility: visible; }
.note-card-corner-copy:hover,
.note-card-corner-copy:focus-visible {
background: color-mix(in srgb, var(--accent) 18%, var(--panel));
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
color: var(--accent);
}
.note-card-corner-copy:hover svg { opacity: 1; }
@media (hover: none) {
.note-card-corner-copy { visibility: visible; }
.note-card-corner-copy svg { opacity: 0.7; }
}
/* Copy stays on every active card regardless of body shape — Edit + Done
live at the top-right pair, so the bottom-right copy slot is always
free. Hidden only in select mode. */
.note-card-selectmode .note-card-corner-copy { display: none !important; }
/* ── Archive view corners — trash (top-left) + unarchive (top-right) ── */
/* Background fill stays fully opaque (var(--panel)) so the note text or
image behind doesn't bleed through. Visibility is gated by `visibility`
+ a faded ICON instead of `opacity` on the whole button. */
.note-card-corner-trash,
.note-card-corner-unarchive {
position: absolute;
top: 6px;
width: 28px;
height: 28px;
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
border-radius: 50%;
cursor: pointer;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--fg);
visibility: hidden;
touch-action: manipulation;
transition: background 0.15s, border-color 0.15s, color 0.15s;
z-index: 2;
}
.note-card-corner-trash svg,
.note-card-corner-unarchive svg { opacity: 0.85; transition: opacity 0.15s; }
.note-card-corner-trash { left: 6px; }
.note-card-corner-unarchive { right: 6px; }
.note-card:hover .note-card-corner-trash,
.note-card:hover .note-card-corner-unarchive { visibility: visible; }
.note-card-corner-trash:hover,
.note-card-corner-trash:focus-visible {
background: color-mix(in srgb, var(--red) 18%, var(--panel));
border-color: color-mix(in srgb, var(--red) 50%, var(--border));
color: var(--red);
}
.note-card-corner-trash:hover svg,
.note-card-corner-unarchive:hover svg { opacity: 1; }
.note-card-corner-unarchive:hover,
.note-card-corner-unarchive:focus-visible {
background: color-mix(in srgb, var(--accent) 18%, var(--panel));
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
color: var(--accent);
}
@media (hover: none) {
.note-card-corner-trash,
.note-card-corner-unarchive { visibility: visible; }
.note-card-corner-trash svg,
.note-card-corner-unarchive svg { opacity: 0.7; }
}
/* ─── Auto-AI ✨ chip on note cards ─────────────────────────────────── */
.note-card-ai-chip {
display: inline-flex;
align-items: center;
gap: 4px;
margin-left: 6px;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 500;
font-family: inherit;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, transparent);
color: var(--accent, var(--red));
cursor: pointer;
transition: background 0.12s, transform 0.12s, box-shadow 0.12s;
vertical-align: middle;
}
.note-card-ai-chip:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
transform: translateY(-1px);
box-shadow: 0 2px 6px color-mix(in srgb, var(--accent, var(--red)) 30%, transparent);
}
.note-card-ai-chip svg {
flex-shrink: 0;
animation: note-ai-shine 2.4s ease-in-out infinite;
}
@keyframes note-ai-shine {
0%, 100% { opacity: 0.85; filter: drop-shadow(0 0 0 transparent); }
50% { opacity: 1; filter: drop-shadow(0 0 4px color-mix(in srgb, var(--accent, var(--red)) 60%, transparent)); }
}
.note-card-ai-solving {
cursor: default;
background: color-mix(in srgb, var(--fg) 8%, transparent);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
color: color-mix(in srgb, var(--fg) 70%, transparent);
}
.note-card-ai-solving svg { animation: spin 0.9s linear infinite; filter: none; }
.note-card-ai-done { background: color-mix(in srgb, var(--accent, var(--red)) 8%, transparent); }
.note-card-ai-done svg { animation: none; }
#notes-auto-ai-toggle.active { color: var(--accent, var(--red)); }
/* ─── AI improve button (note form header) ───────────────────────────── */
.note-form-improve-btn {
width: 38px !important;
height: 38px !important;
color: var(--accent, var(--red));
}
.note-form-improve-btn svg {
width: 22px !important;
height: 22px !important;
animation: note-ai-shine 2.4s ease-in-out infinite;
}
.note-form-improve-btn:disabled,
.note-form-improve-btn.busy {
opacity: 0.6;
cursor: wait;
}
.note-form-improve-btn.busy svg {
animation: spin 0.9s linear infinite;
filter: none;
}
.note-card:hover .note-card-select,
.note-card:hover .note-card-pin,
.note-card-selected .note-card-select,
.note-card-pinned .note-card-pin {
display: flex;
}
.note-card-select:hover, .note-card-pin:hover {
opacity: 1;
transform: scale(1.15);
background: color-mix(in srgb, var(--accent) 15%, var(--bg));
}
.note-card-pin.active {
display: flex;
background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--bg));
border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
color: var(--accent, var(--red));
opacity: 1;
}
.note-card-selected .note-card-select {
background: var(--accent);
border-color: var(--accent);
color: #fff;
opacity: 1;
}
/* Select-mode checkbox on cards (mirrors docs library) */
.note-card-cb {
position: absolute;
top: 6px;
left: 6px;
z-index: 3;
margin: 0;
}
.note-card-selectmode {
padding-left: 28px;
cursor: pointer;
}
/* In select mode the whole card is a checkbox — kill every hover affordance
that would otherwise suggest "preview" or "open" so users know exactly
what their tap will do (toggle selection). */
.note-card-selectmode .note-card-pin,
.note-card-selectmode .note-card-actions { pointer-events: none; opacity: 0.4; }
.note-card-selectmode .note-card-edit-corner,
.note-card-selectmode .note-card-pin {
display: none !important;
}
.note-card-selectmode:hover { background: color-mix(in srgb, var(--fg) 3%, transparent) !important; }
.note-card-selectmode .note-card-title:hover { opacity: 1 !important; }
.note-card-selectmode .note-card-edit-corner,
.note-card-selectmode:hover .note-card-edit-corner { opacity: 0 !important; }
/* Bottom action row — hidden everywhere. The archive/delete actions and the
color picker now live inside the edit form (click the pencil corner). */
.note-card-actions { display: none; }
.note-agent-menu {
background: var(--panel, var(--bg));
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
padding: 4px 0;
min-width: 140px;
font-size: 12px;
}
.note-card-colors {
display: flex;
gap: 3px;
}
/* Re-asserted AFTER the base rule above so the mobile-hide actually
wins the cascade — earlier @media (hover:none) block was being
overridden because the unconditional display:flex rule came later. */
@media (hover: none) {
.note-card-colors { display: none; }
}
.note-card-color-dot {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: pointer;
border: 1.5px solid color-mix(in srgb, var(--fg) 20%, transparent);
transition: transform 0.15s, border-color 0.15s;
}
.note-card-color-dot:hover {
transform: scale(1.25);
border-color: var(--fg);
}
.note-card-color-dot.active {
border-color: var(--fg);
border-width: 2px;
}
.note-card-action {
background: none;
border: none;
cursor: pointer;
padding: 3px;
color: var(--fg);
opacity: 0.4;
display: flex;
align-items: center;
border-radius: 3px;
transition: opacity 0.15s, background 0.15s;
}
.note-card-action svg { transform: translateY(2px); }
/* Note image */
.note-card-image {
width: 100%;
max-height: 180px;
object-fit: cover;
border-radius: 4px;
margin: 4px 0;
display: block;
}
.note-form-image-wrap {
position: relative;
margin: 4px 0;
}
.note-form-image {
width: 100%;
max-height: 200px;
object-fit: cover;
border-radius: 4px;
display: block;
/* Images are draggable=true by default; that catches the mousedown
and beats the X button to the click. Disabling drag at the CSS
level (the -webkit-user-drag prop) keeps the X button reliably
clickable even if the JS forgets to set draggable="false" on the
element. Unprefixed user-drag was never standardized — Firefox
drops it with a warning. */
-webkit-user-drag: none;
user-select: none;
}
.note-form-image-rm {
position: absolute;
top: 6px;
right: 6px;
z-index: 5;
background: rgba(0,0,0,0.65);
border: none;
color: #fff;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.note-form-photo-btn {
flex: 0 0 auto;
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
border-radius: 4px;
width: auto;
min-width: 32px;
height: 32px;
padding: 0 6px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
opacity: 1;
transition: opacity 0.15s, border-color 0.15s, background 0.15s;
}
.note-form-photo-btn:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* Quick-add bar (always at top) */
.notes-quick-add {
display: flex;
align-items: center;
gap: 4px;
background: var(--panel);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 35%, var(--border));
border-radius: 6px;
/* Toggle pill sits at the very left of the bar — no inset padding. The
pill's own border provides visual breathing room from the wrapper edge. */
padding: 4px 6px 4px 4px;
margin-top: 2px;
margin-bottom: 8px;
transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
/* Subtle idle glow so the field reads as "you can type here" without
the user having to click it first. Fades out the moment the user
hovers or focuses it. */
animation: notes-quick-pulse 2.8s ease-in-out infinite;
}
@keyframes notes-quick-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 0%, transparent); }
50% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 14%, transparent); }
}
.notes-quick-add:hover {
border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
animation: none;
}
.notes-quick-add:focus-within {
border-color: var(--accent, var(--red));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
background: color-mix(in srgb, var(--fg) 3%, var(--panel));
animation: none;
}
.notes-quick-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--fg);
caret-color: var(--accent, var(--red));
font-family: inherit;
font-size: 12px;
padding: 6px 0;
}
.notes-quick-input::placeholder {
color: color-mix(in srgb, var(--fg) 45%, transparent);
}
/* Blinking text caret hint before the placeholder so empty + unfocused
reads as a live input. Sits on the wrapper because elements
can't carry ::before. The pseudo-element is ALWAYS rendered (so its
width is reserved and the placeholder doesn't jump left when the
hint hides); only the colour/animation toggle, not the layout. */
.notes-quick-add::before {
content: '|';
color: var(--accent, var(--red));
margin-right: 6px;
font-weight: 500;
font-size: 13px;
animation: notes-quick-caret 1s steps(2, jump-none) infinite;
pointer-events: none;
}
/* Hide visually (keep space) on hover, focus, or once the input has any
text — prevents the layout shift the user reported when the hint
collapsed and the placeholder slid left. */
.notes-quick-add:hover::before,
.notes-quick-add:focus-within::before,
.notes-quick-add:not(:has(.notes-quick-input:placeholder-shown))::before {
visibility: hidden;
animation: none;
}
@keyframes notes-quick-caret {
0%, 49% { opacity: 1; }
50%, 100%{ opacity: 0; }
}
.notes-quick-icon {
background: none;
border: none;
color: var(--fg);
opacity: 0.45;
cursor: pointer;
padding: 5px;
border-radius: 4px;
display: flex;
align-items: center;
transition: opacity 0.15s, background 0.15s;
}
.notes-quick-icon:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
/* 2-pill Note/Todo toggle in the quick-add bar — same look as the form's
type-seg but slimmer to fit the compact row. */
.notes-quick-type-seg {
display: inline-flex;
height: 28px;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
position: relative;
flex-shrink: 0;
/* Negative order pushes the toggle BEFORE the wrapper's ::before caret
hint, so the blinking | sits between the toggle and the input rather
than at the far-left edge. */
order: -1;
}
.notes-quick-type-seg::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
background: color-mix(in srgb, var(--fg) 10%, transparent);
border-radius: 6px;
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 0;
}
.notes-quick-type-seg.is-todo::before { transform: translateX(100%); }
.notes-quick-type-pill {
background: none;
border: none;
color: color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
padding: 0 11px;
font-family: inherit;
transition: color 0.2s;
white-space: nowrap;
height: 100%;
display: inline-flex;
align-items: center;
position: relative;
z-index: 1;
}
.notes-quick-type-pill svg {
width: 16px;
height: 16px;
}
.notes-quick-type-pill:not(.active):hover {
color: color-mix(in srgb, var(--fg) 65%, transparent);
}
.notes-quick-type-pill.active,
.notes-quick-type-pill.active:hover,
.notes-quick-type-pill.active:focus {
color: var(--fg);
cursor: default;
}
/* Mobile: thumb-sized Note/Todo toggle. The 22px desktop pill is too
small to hit reliably on a phone. */
@media (max-width: 768px) {
.notes-quick-type-seg {
height: 36px;
border-radius: 10px;
}
.notes-quick-type-seg::before {
border-radius: 9px;
}
.notes-quick-type-pill {
padding: 0 14px;
}
.notes-quick-type-pill svg {
width: 17px;
height: 17px;
}
/* Match the input + photo button heights to the bigger toggle so the
row reads as one consistent bar. */
.notes-quick-input {
height: 36px;
font-size: 15px;
}
.notes-quick-icon {
padding: 9px;
}
.notes-quick-icon svg {
width: 18px;
height: 18px;
}
}
.notes-empty-msg {
text-align: center;
opacity: 0.4;
padding: 30px 20px;
font-size: 11px;
}
/* Loading skeleton */
.notes-skeleton {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 4px;
}
.notes-skeleton-card {
height: 72px;
border-radius: 6px;
border: 1px solid var(--border);
background: linear-gradient(
90deg,
color-mix(in srgb, var(--fg) 4%, transparent) 0%,
color-mix(in srgb, var(--fg) 10%, transparent) 50%,
color-mix(in srgb, var(--fg) 4%, transparent) 100%
);
background-size: 200% 100%;
animation: notes-skeleton-shimmer 1.4s ease-in-out infinite;
}
.notes-skeleton-card.short { height: 46px; }
@keyframes notes-skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Type toggle (note vs todo) */
.note-form-type-row {
display: flex;
gap: 4px;
margin-top: -2px;
}
/* Reminder bell button */
.note-form-remind-btn {
flex: 0 0 auto;
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
border-radius: 4px;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 1;
position: relative;
top: -2px;
transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
}
.note-form-remind-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.note-form-remind-btn.has-date { color: var(--accent); border-color: var(--accent); }
/* Calendar event form + notes form: jingle the little bell when the user
picks a reminder. Anchored near the top of the bell so the swing reads
as the bell clanging side-to-side. */
.cal-remind-bell,
.note-form-remind-btn > svg {
transform-origin: 50% 4px;
transition: transform 0.1s ease;
}
.cal-remind-bell.jingling,
.note-form-remind-btn > svg.jingling {
animation: cal-bell-jingle 0.65s cubic-bezier(0.36, 0, 0.66, -0.56) both;
}
@keyframes cal-bell-jingle {
0% { transform: rotate(0deg); }
15% { transform: rotate(-22deg); }
30% { transform: rotate(18deg); }
45% { transform: rotate(-14deg); }
60% { transform: rotate(10deg); }
75% { transform: rotate(-6deg); }
88% { transform: rotate(3deg); }
100% { transform: rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
.cal-remind-bell.jingling,
.note-form-remind-btn > svg.jingling { animation: none; }
}
/* Reminder tag inside form */
.note-form-reminder-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.note-form-reminder-tags:empty { display: none; }
.note-reminder-tag {
display: inline-flex;
align-items: center;
gap: 6px;
background: color-mix(in srgb, var(--accent) 14%, transparent);
color: var(--accent);
border: none;
border-radius: 12px;
padding: 4px 4px 4px 9px;
font-size: 10px;
font-family: inherit;
cursor: pointer;
font-weight: 500;
}
.note-reminder-tag:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.note-reminder-tag-x {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
background: color-mix(in srgb, var(--accent) 25%, transparent);
font-size: 12px;
line-height: 1;
position: relative;
top: -2px;
}
.note-reminder-tag-x:hover { background: var(--red); color: #fff; }
/* Reminder tag on card */
.note-card-reminder {
display: inline-flex;
align-items: center;
gap: 4px;
background: color-mix(in srgb, var(--accent) 12%, transparent);
color: var(--accent);
font-size: 9px;
font-weight: 500;
padding: 2px 7px;
border-radius: 10px;
align-self: flex-start;
margin-top: 2px;
}
.note-card-reminder.overdue {
background: color-mix(in srgb, var(--red) 18%, transparent);
color: var(--red);
}
/* Reminder dropdown menu */
.note-reminder-menu {
position: fixed;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 6px 24px rgba(0,0,0,0.25);
/* Above the fullscreen note overlay (z-index: 10500) — otherwise the
reminder picker opens hidden behind it on mobile and reads as
"broken". */
z-index: 12000;
min-width: 220px;
padding: 6px 0;
font-family: inherit;
font-size: 12px;
}
.note-reminder-menu-title {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
opacity: 0.5;
padding: 6px 14px 4px;
letter-spacing: 0.5px;
}
.note-reminder-menu-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 12px;
padding: 7px 14px;
cursor: pointer;
text-align: left;
transition: background 0.1s;
}
.note-reminder-menu-item:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); }
.note-reminder-menu-item.active { color: var(--accent); }
.note-reminder-menu-sub {
font-size: 10px;
opacity: 0.5;
margin-left: 12px;
}
.note-reminder-menu-check { color: var(--accent); font-size: 12px; }
.note-reminder-menu-divider {
height: 1px;
background: var(--border);
margin: 4px 0;
}
.note-reminder-menu-picker {
padding: 6px 14px 8px;
}
.note-reminder-date-input {
width: 100%;
background: color-mix(in srgb, var(--fg) 5%, transparent);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-family: inherit;
font-size: 12px;
padding: 6px 8px;
outline: none;
color-scheme: dark light;
box-sizing: border-box;
}
.note-reminder-date-input:focus { border-color: var(--accent); }
.note-reminder-menu-confirm {
font-weight: 600;
color: var(--accent);
justify-content: center;
}
.note-reminder-menu-confirm.disabled,
.note-reminder-menu-confirm[disabled] {
opacity: 0.4;
cursor: not-allowed;
color: var(--fg);
}
.note-reminder-menu-arrow {
opacity: 0.5;
margin-left: 8px;
font-size: 14px;
line-height: 1;
}
.note-reminder-menu-back {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
font-size: 11px;
opacity: 0.7;
padding: 6px 12px;
cursor: pointer;
text-align: left;
transition: opacity 0.1s, background 0.1s;
}
.note-reminder-menu-back:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 6%, transparent); }
.note-reminder-menu-arrow-back {
font-size: 14px;
line-height: 1;
}
.note-reminder-menu-sublabel {
font-size: 10px;
opacity: 0.5;
padding: 6px 14px 2px;
letter-spacing: 0.3px;
}
.note-reminder-weekday-row {
display: flex;
gap: 4px;
padding: 4px 12px 8px;
flex-wrap: wrap;
}
.note-reminder-day-chip {
flex: 1 1 0;
min-width: 24px;
height: 28px;
background: color-mix(in srgb, var(--fg) 5%, transparent);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--fg);
font-family: inherit;
font-size: 11px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.1s, border-color 0.1s, color 0.1s;
padding: 0;
}
.note-reminder-day-chip.wide { padding: 0 6px; }
.note-reminder-day-chip:hover {
background: color-mix(in srgb, var(--accent) 10%, transparent);
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}
.note-reminder-day-chip.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.note-form-type-btn,
.note-form-type-toggle {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
border-radius: 4px;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
}
.note-form-type-toggle:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.note-form-type-toggle[data-type="todo"] { color: var(--accent); }
/* Note | Todo | Draw segmented toggle — mirrors .mode-toggle (agent/chat) */
.note-form-type-seg {
display: inline-flex;
height: 32px;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
.note-form-type-seg::before {
content: '';
position: absolute;
top: 0;
left: 0;
/* 3 pills now (note / todo / draw) — Goal was removed. Slider takes
1/3 of the track and translates by full multiples of itself. */
width: 33.3333%;
height: 100%;
background: color-mix(in srgb, var(--fg) 10%, transparent);
border-radius: 9px;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 0;
}
.note-form-type-seg.is-todo::before { transform: translateX(100%); }
.note-form-type-seg.is-draw::before { transform: translateX(200%); }
.note-form-type-pill {
background: none;
border: none;
color: color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
padding: 0 12px;
font-size: 13px;
font-weight: 500;
font-family: inherit;
transition: color 0.2s;
white-space: nowrap;
height: 100%;
display: inline-flex;
align-items: center;
gap: 4px;
position: relative;
z-index: 1;
}
.note-form-type-pill:not(.active):hover { color: color-mix(in srgb, var(--fg) 60%, transparent); }
.note-form-type-pill.active,
.note-form-type-pill.active:hover,
.note-form-type-pill.active:focus {
color: var(--fg);
cursor: default;
}
@media (max-width: 480px) {
.note-form-type-pill span { display: none; }
.note-form-type-pill { padding: 0 8px; }
}
.note-card-action:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.note-card-delete:hover { color: var(--red); }
/* Tag/label */
.note-card-label {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 2px;
}
.note-card-label-chip {
border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
background: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--accent);
border-radius: 999px;
padding: 2px 6px;
font: inherit;
font-size: 9px;
font-weight: 600;
line-height: 1.2;
cursor: pointer;
}
.note-card-label-chip:hover {
background: color-mix(in srgb, var(--accent) 18%, transparent);
border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
}
/* Search bar */
.notes-search-bar {
padding: 6px 8px;
display: flex;
align-items: center;
gap: 6px;
}
.notes-select-trigger {
position: relative;
top: 3px;
flex-shrink: 0;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font-family: inherit;
font-size: 11px;
height: 30px;
padding: 0 12px;
cursor: pointer;
opacity: 0.75;
transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
}
.notes-select-trigger:hover { opacity: 1; border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
.notes-select-trigger.active {
opacity: 1;
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
.notes-search-bar .memory-search-input {
flex: 1;
min-width: 0;
height: 30px;
min-height: 30px;
font-size: 11px;
padding: 0 10px 0 28px;
background-color: var(--bg);
background-image: url("data:image/svg+xml;utf8, ");
background-repeat: no-repeat;
background-position: 9px center;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-family: inherit;
outline: none;
transition: border-color 0.15s, background-color 0.15s;
}
.notes-search-bar .memory-search-input:focus {
border-color: var(--red);
}
/* Bulk-bar appears under the search bar — inset its sides to match the
search bar's own 8px horizontal padding so the two visually align. */
#notes-bulk-bar.memory-bulk-bar {
margin: 0 8px 4px;
}
/* Grid view: true CSS Grid plus JS-computed row spans, so cards masonry-pack
vertically without the jumpy rebalancing of CSS columns. */
.notes-pane.notes-view-grid .notes-pane-body {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-auto-rows: 4px;
grid-auto-flow: dense;
gap: 0 8px;
padding: 8px;
overflow-x: hidden;
align-items: start;
align-content: start;
}
.notes-pane.notes-view-grid .note-card {
margin: 0 0 8px;
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
word-break: break-word;
display: flex;
flex-direction: column;
gap: 4px;
}
.notes-pane.notes-view-grid .note-form,
.notes-pane.notes-view-grid .notes-quick-add {
grid-column: 1 / -1;
margin-bottom: 0;
grid-row-end: span 16;
}
/* Edit form in grid view: span the full row and size naturally — same shape
as the new-note form. The earlier max-width: 380px + max-height: 220px
inner-scroll combo read as a tiny popup floating in a wider panel and
didn't actually fit the form's content. */
.notes-pane.notes-view-grid .note-form {
width: 100%;
max-width: none;
margin-left: 0;
margin-right: 0;
box-sizing: border-box;
}
/* Pinned section break: the first unpinned card jumps to column 1, leaving
any leftover cell in the pinned row empty. Reads as a visual divider
between pinned and unpinned without needing a separator element. */
.notes-pane.notes-view-grid .note-card-pinned + .note-card:not(.note-card-pinned) {
grid-column-start: 1;
}
/* Mobile: 2-lane masonry via the CSS-Grid row-span trick. Each card spans
N tiny rows (4px each) based on its measured height; the grid auto-places
cards into the two columns so left/right lanes flow independently — no
row alignment between them. JS in notes.js sets `grid-row-end: span N`
on every card after render. Pure CSS multi-column was tried but its
`column-span: all` interaction with sticky positioning broke clicks on
the quick-add bar (Firefox mobile). */
@media (max-width: 768px) {
.notes-pane.notes-view-grid .notes-pane-body {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 4px;
grid-auto-flow: dense;
gap: 0 8px;
align-content: start;
}
.notes-pane.notes-view-grid .note-card {
margin: -8px 0 6px;
transform: translateY(-12px);
/* grid-row-end set inline by JS (_applyMasonry) */
}
/* Full-width quick-add + edit form. Quick-add stays sticky at top so
the user can always reach it; it's NOT inside the masonry pack. */
.notes-pane.notes-view-grid .notes-labels-bar,
.notes-pane.notes-view-grid .notes-quick-add,
.notes-pane.notes-view-grid .note-form {
grid-column: 1 / -1;
}
.notes-pane.notes-view-grid .notes-labels-bar { grid-row: span 16; }
.notes-pane.notes-view-grid .notes-quick-add { grid-row: span 12; }
.notes-pane.notes-view-grid .note-form {
width: 100%;
max-width: none;
margin-left: 0;
margin-right: 0;
box-sizing: border-box;
grid-row: auto / span 64;
}
.notes-pane.notes-view-grid .note-form:has(.note-form-type-seg.is-draw) {
grid-row: auto / span 152;
}
.notes-pane.notes-view-grid .note-form.note-form-new {
transform: translateY(-28px);
}
.notes-pane.notes-view-grid .notes-labels-bar {
padding-top: 0;
margin-top: -6px;
margin-bottom: 0;
}
.notes-pane.notes-view-grid .notes-quick-add {
margin-top: -32px;
margin-bottom: 4px;
}
/* Pinned-section break: first unpinned card jumps to column 1. */
.notes-pane.notes-view-grid .note-card-pinned + .note-card:not(.note-card-pinned) {
grid-column-start: 1;
}
}
/* Label filter bar */
.notes-labels-bar {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 0 8px 0;
margin-top: -6px;
margin-bottom: -2px;
}
.notes-label-chip {
background: transparent;
border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 60%, transparent);
font-size: 10px;
padding: 3px 10px;
border-radius: 10px;
cursor: pointer;
font-family: inherit;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.notes-label-chip:hover {
color: var(--fg);
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}
.notes-label-chip.active {
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 60%, transparent);
font-weight: 600;
}
.notes-label-chip-reminders {
display: inline-flex;
align-items: center;
gap: 2px;
}
.notes-label-chip-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 14px;
height: 14px;
padding: 0 4px;
border-radius: 7px;
background: color-mix(in srgb, var(--fg) 12%, transparent);
color: inherit;
font-size: 9px;
font-weight: 600;
margin-left: 3px;
}
.notes-label-chip-reminders.active .notes-label-chip-count {
background: color-mix(in srgb, var(--accent) 30%, transparent);
color: var(--accent);
}
.notes-label-chip-reminders.active.negated {
background: color-mix(in srgb, var(--red, #e55) 14%, transparent);
color: var(--red, #e55);
border-color: color-mix(in srgb, var(--red, #e55) 50%, transparent);
}
.notes-label-chip-reminders.active.negated .notes-label-chip-count {
background: color-mix(in srgb, var(--red, #e55) 25%, transparent);
color: var(--red, #e55);
}
.notes-label-clear-past {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--red, #e55);
border-color: color-mix(in srgb, var(--red, #e55) 45%, var(--border));
}
.notes-label-clear-past:hover {
color: var(--red, #e55);
background: color-mix(in srgb, var(--red, #e55) 12%, transparent);
border-color: color-mix(in srgb, var(--red, #e55) 65%, var(--border));
}
/* Today + Goals filter chips — share styling, tint with a warm accent so
they read as "long-term work" alongside the cool reminder chip. */
.notes-label-chip-today,
.notes-label-chip-goals {
display: inline-flex;
align-items: center;
gap: 2px;
}
.notes-label-chip-today.active,
.notes-label-chip-goals.active {
background: color-mix(in srgb, var(--accent-warm) 22%, transparent);
color: var(--accent-warm);
border-color: color-mix(in srgb, var(--accent-warm) 60%, transparent);
}
.notes-label-chip-today.active .notes-label-chip-count,
.notes-label-chip-goals.active .notes-label-chip-count {
background: color-mix(in srgb, var(--accent-warm) 30%, transparent);
color: var(--accent-warm);
}
/* ── Goal cards ──────────────────────────────────────────────────────── */
/* A goal note is structurally a checklist (note_type='goal') but visually
marked with a small Goal pill in the corner + a slightly warmer accent
so users can spot it at a glance among regular todos. */
.note-card-goal {
border-left: 3px solid color-mix(in srgb, var(--accent-warm) 80%, transparent);
}
.note-goal-pill {
position: absolute;
top: 6px;
left: 8px;
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px 2px 5px;
background: color-mix(in srgb, var(--accent-warm) 18%, transparent);
color: var(--accent-warm);
border-radius: 999px;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
line-height: 1;
z-index: 2;
}
.note-card-goal .note-card-header {
/* Goal cards reserve space for the pill so the title doesn't collide. */
padding-left: 56px;
}
/* In select mode the left-side checkbox takes the pill's spot — drop it
so the row stays legible. */
.note-card-selectmode.note-card-goal .note-goal-pill { display: none; }
.note-card-selectmode.note-card-goal .note-card-header { padding-left: 0; }
/* Description blurb above the checklist on goal cards */
.note-goal-desc {
font-size: 12px;
opacity: 0.7;
margin: 4px 0 6px;
white-space: pre-wrap;
line-height: 1.4;
}
/* ── Goal form (Break-down editor) ───────────────────────────────────── */
.note-form-goal {
display: flex;
flex-direction: column;
gap: 8px;
}
.note-form-goal-desc {
width: 100%;
box-sizing: border-box;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font-family: inherit;
font-size: 13px;
padding: 8px 10px;
resize: vertical;
min-height: 60px;
}
.note-form-goal-desc:focus {
outline: none;
border-color: color-mix(in srgb, var(--accent-warm) 60%, var(--border));
}
.note-form-goal-actions {
display: flex;
align-items: center;
gap: 8px;
}
.note-form-goal-ai {
display: inline-flex;
align-items: center;
background: color-mix(in srgb, var(--accent-warm) 16%, transparent);
color: var(--accent-warm);
border: 1px solid color-mix(in srgb, var(--accent-warm) 45%, transparent);
border-radius: 6px;
padding: 5px 10px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.note-form-goal-ai:hover:not(:disabled) {
background: color-mix(in srgb, var(--accent-warm) 26%, transparent);
border-color: color-mix(in srgb, var(--accent-warm) 65%, transparent);
}
.note-form-goal-ai:disabled,
.note-form-goal-ai.busy {
opacity: 0.6;
cursor: wait;
}
.note-form-goal-hint {
font-size: 11px;
opacity: 0.55;
}
/* Fresh goal form: hide the title input + tag input + reminder button so the
user only sees the single "what do you want to achieve?" textarea. */
.note-form-goal-fresh ~ .note-form-actions-group { display: none; }
.note-form:has(.note-form-goal-fresh) .note-form-title,
.note-form:has(.note-form-goal-fresh) .note-form-label,
.note-form:has(.note-form-goal-fresh) .note-form-remind-btn,
.note-form-bespoke:has(.note-form-goal-fresh) .note-form-title,
.note-form-bespoke:has(.note-form-goal-fresh) .note-form-label,
.note-form-bespoke:has(.note-form-goal-fresh) .note-form-remind-btn {
display: none !important;
}
.note-form-goal-fresh .note-form-goal-desc {
font-size: 15px;
min-height: 90px;
padding: 12px 14px;
}
.note-form-goal-fresh.building .note-form-goal-desc {
opacity: 0.55;
pointer-events: none;
}
.note-form-goal-fresh.building::after {
content: 'AI is planning your goal…';
display: block;
text-align: center;
font-size: 12px;
opacity: 0.7;
margin-top: 6px;
animation: note-goal-pulse 1.4s ease-in-out infinite;
}
@keyframes note-goal-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* ── Today view ──────────────────────────────────────────────────────── */
.notes-today-wrap {
display: flex;
flex-direction: column;
gap: 4px;
margin: 4px 0 12px;
}
.notes-today-header {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--accent-warm);
padding: 6px 10px 4px;
}
.notes-today-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.notes-today-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--panel, var(--bg));
border: 1px solid var(--border);
border-left: 3px solid color-mix(in srgb, var(--accent-warm) 70%, transparent);
border-radius: 8px;
transition: opacity 0.2s, transform 0.2s;
}
.notes-today-row.done {
opacity: 0.35;
transform: scale(0.98);
}
.notes-today-row .note-check-dot {
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid color-mix(in srgb, var(--fg) 40%, transparent);
cursor: pointer;
transition: background 0.15s, border-color 0.15s;
}
.notes-today-row .note-check-dot:hover {
border-color: var(--accent-warm);
background: color-mix(in srgb, var(--accent-warm) 20%, transparent);
}
.notes-today-row.done .note-check-dot {
background: var(--accent-warm);
border-color: var(--accent-warm);
}
.notes-today-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.notes-today-title {
font-size: 11px;
font-weight: 600;
color: var(--accent-warm);
letter-spacing: 0.03em;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notes-today-title:hover { text-decoration: underline; }
.notes-today-step {
font-size: 14px;
color: var(--fg);
line-height: 1.35;
}
.notes-today-progress {
flex-shrink: 0;
font-size: 10px;
font-weight: 600;
opacity: 0.6;
font-variant-numeric: tabular-nums;
}
.notes-empty {
text-align: center;
padding: 32px 16px;
font-size: 13px;
opacity: 0.55;
font-style: italic;
}
/* Bulk action bar */
.notes-bulk-info {
font-size: 10px;
opacity: 0.6;
margin-left: 4px;
}
.notes-bulk-btn {
opacity: 0.6;
}
.notes-bulk-btn:hover { opacity: 1; }
/* Form label input */
.note-form-label {
background: transparent;
border: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
color: var(--fg);
font-size: 11px;
padding: 3px 6px;
border-radius: 4px;
outline: none;
width: 80px;
font-family: inherit;
opacity: 0.6;
transition: opacity 0.15s, border-color 0.15s;
}
.note-form-label:hover, .note-form-label:focus { opacity: 1; border-color: var(--border); }
.note-form-label.flash-once {
animation: noteLabelFlash 0.6s ease-out;
}
@keyframes noteLabelFlash {
0% { background: color-mix(in srgb, var(--accent) 35%, transparent); }
100% { background: transparent; }
}
.note-form-label:not([value=""]) { opacity: 0.9; border-style: solid; }
.note-card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 6px;
}
.note-card-title {
font-size: 13px;
font-weight: 600;
cursor: pointer;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.note-card-title:hover { opacity: 0.7; }
.note-card-title.empty::before {
content: 'No title';
font-weight: 500;
opacity: 0.3;
font-style: italic;
}
.note-content-preview {
font-size: 12px;
opacity: 0.6;
line-height: 1.4;
/* Roomy preview: up to ~14 lines. Content is also sliced to 600 chars
client-side, so cards still stay reasonable in the list. */
max-height: 240px;
overflow: hidden;
cursor: pointer;
white-space: pre-wrap;
word-break: break-word;
}
.note-pin-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px;
flex-shrink: 0;
display: flex;
align-items: center;
}
.note-pin-dot {
width: 7px;
height: 7px;
border-radius: 50%;
border: 1.5px solid color-mix(in srgb, var(--fg) 25%, transparent);
transition: all 0.15s;
}
.note-pin-btn:hover .note-pin-dot { border-color: var(--fg); }
.note-pin-btn.active .note-pin-dot {
background: var(--accent);
border-color: var(--accent);
}
.note-x-btn {
background: none;
border: none;
cursor: pointer;
padding: 2px;
flex-shrink: 0;
color: var(--fg);
opacity: 0.25;
display: flex;
align-items: center;
transition: opacity 0.15s, color 0.15s;
}
.note-x-btn:hover {
opacity: 0.9;
color: var(--red);
}
/* Inline due date next to title */
.note-due-inline {
font-size: 9px;
padding: 1px 6px;
border-radius: 3px;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: color-mix(in srgb, var(--fg) 60%, transparent);
opacity: 0.7;
white-space: nowrap;
flex-shrink: 0;
align-self: center;
}
.note-due-inline.note-due-overdue {
background: color-mix(in srgb, var(--red) 18%, transparent);
color: var(--red);
opacity: 1;
font-weight: 600;
}
/* Note card drag */
.note-card { cursor: grab; }
.note-card:active { cursor: grabbing; }
/* Dragged card = "drop preview" — it already sits at the swap-target slot,
so making it the most visible card on screen tells the user exactly where
the note will land on release. */
.note-card.dragging {
opacity: 0.92;
cursor: grabbing;
transform: scale(1.03) rotate(-0.6deg);
box-shadow: 0 14px 32px rgba(0,0,0,0.4), 0 0 0 2px var(--accent, var(--red));
border-color: var(--accent, var(--red)) !important;
background: color-mix(in srgb, var(--accent, var(--red)) 6%, var(--panel)) !important;
z-index: 10;
position: relative;
/* Let elementFromPoint see THROUGH the dragged card so the swap detector
can find the sibling underneath the finger. Without this, in single-row
(list view) the dragged card follows the finger and forever occludes
anything else, so no swap ever fires. The body-level touchmove listener
still receives events regardless. */
pointer-events: none;
}
.notes-pane-body.drag-active .note-card:not(.dragging) {
transition: transform 0.22s cubic-bezier(0.34, 1.2, 0.64, 1), border-color 0.15s, opacity 0.15s;
opacity: 0.78;
}
.notes-pane-body.drag-active .note-card:not(.dragging):hover {
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, var(--border));
opacity: 1;
}
/* Checklist drag grip */
.note-cl-grip {
cursor: grab;
opacity: 0.2;
font-size: 9px;
letter-spacing: -2px;
user-select: none;
flex-shrink: 0;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.15s;
}
.note-cl-row:hover .note-cl-grip { opacity: 0.6; }
.note-cl-row.dragging {
opacity: 0.4;
background: color-mix(in srgb, var(--accent) 8%, transparent);
border-radius: 4px;
}
.note-cl-row.drop-before {
box-shadow: 0 -2px 0 0 var(--accent);
}
.note-cl-row.drop-after {
box-shadow: 0 2px 0 0 var(--accent);
}
.note-card-footer {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
}
.note-delete-btn {
background: none;
border: none;
color: var(--fg);
opacity: 0.2;
cursor: pointer;
padding: 2px;
transition: opacity 0.15s;
}
.note-delete-btn:hover { opacity: 0.8; color: var(--red); }
.note-archive-btn {
background: none; border: none; color: var(--fg); opacity: 0.2; cursor: pointer; padding: 2px; transition: opacity 0.15s;
}
.note-archive-btn:hover { opacity: 0.7; }
/* Checklist preview — cap height so cards with 30 items don't push the
grid layout into oblivion; the inner list scrolls inside the card. */
.note-checklist-preview {
display: flex;
flex-direction: column;
gap: 0;
min-height: 18px;
max-height: 240px;
overflow-y: auto;
overscroll-behavior: contain;
/* Card has padding-right:30px reserved for the top-corner edit/done/pin
buttons — but those sit above the checklist, not next to it. Pull the
checklist back into that reserved gutter so todo text can extend
further right. The copy button (bottom-right, 28×28) only appears on
hover; leave just enough room (last row) so it doesn't hide the final
item's tail. */
margin-right: -22px;
padding-right: 4px;
}
.note-checklist-preview > .note-checkbox:last-child .note-check-text {
/* Reserve room on the LAST row only for the bottom-right copy button so it
doesn't cover the final item's tail. Applied to the text — not to the
row — so the X delete button still reaches the same edge it does on
every other row. */
padding-right: 30px;
}
.note-card.doclib-card-expanded .note-checklist-preview {
/* When the card is the expanded one in the grid, let the list use the
whole available height instead of the compact cap. */
max-height: none;
}
.note-checklist-preview:empty::before {
content: 'No todos';
font-size: 10px;
opacity: 0.3;
font-style: italic;
}
.note-cl-quickadd {
/* In flow now (was position:absolute), rendered AFTER the reminder badge
so the "+ Add item" input sits underneath the reminder instead of
overlapping it. Slight right inset keeps clear of the bottom-right
copy button on hover. */
display: block;
padding-top: 2px;
padding-right: 32px;
margin-top: 2px;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
}
.note-card:hover .note-cl-quickadd,
.note-cl-quickadd:focus-within {
opacity: 1;
pointer-events: auto;
}
.note-cl-quickadd-input {
width: 100%;
background: transparent;
border: none;
border-top: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
color: var(--fg);
font-size: 11px;
font-family: inherit;
padding: 4px 2px 2px;
outline: none;
opacity: 0.6;
transition: opacity 0.15s, border-color 0.15s;
}
.note-cl-quickadd-input::placeholder { color: var(--fg); opacity: 0.5; }
.note-cl-quickadd-input:focus { opacity: 1; border-top-color: var(--border); }
.note-card-selectmode .note-cl-quickadd { display: none !important; }
.note-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
cursor: pointer;
/* Vertical padding stays 0 so short single-line rows pack tightly.
Left padding bumped so the dot's hover-scale (1.15x) doesn't clip
against the card edge. */
padding: 0 4px 0 8px;
line-height: 1.25;
border-radius: 4px;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.note-checkbox:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.note-check-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.note-link {
color: var(--accent-primary, var(--red));
text-decoration: underline;
word-break: break-all;
}
.note-link:hover { opacity: 0.8; }
.note-checkbox-rm {
flex: 0 0 auto;
background: transparent;
border: none;
color: var(--fg);
opacity: 0;
cursor: pointer;
padding: 2px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
margin-right: 0;
transition: opacity 0.12s, background 0.12s, color 0.12s;
}
.note-checkbox:hover .note-checkbox-rm { opacity: 0.55; }
.note-checkbox-rm:hover { opacity: 1 !important; color: var(--red); background: color-mix(in srgb, var(--red) 12%, transparent); }
.note-card-selectmode .note-checkbox-rm { display: none; }
.note-check-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1.75px solid color-mix(in srgb, var(--fg) 35%, transparent);
/* Solid panel fill so the dot reads as a tappable target even when an
image or colored card background sits behind it. */
background: var(--panel);
flex-shrink: 0;
position: relative;
/* Anchor the hover/active scale to the LEFT edge so the dot grows
rightward only. Default center origin pushed the scaled dot off the
left of the card (the parent .note-checklist-preview has overflow:auto
and clips horizontally as a side-effect of overflow-y:auto). */
transform-origin: left center;
transition: background 0.2s, border-color 0.2s, transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.note-check-dot::after {
content: '';
position: absolute;
left: 50%;
top: 45%;
width: 7px;
height: 3.5px;
border-left: 1.75px solid #fff;
border-bottom: 1.75px solid #fff;
transform: translate(-50%, -50%) rotate(-45deg) scale(0);
transform-origin: center;
transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.notes-select-mode .note-checkbox { cursor: default; pointer-events: none; }
.notes-select-mode .note-checkbox:hover { background: transparent; }
.notes-select-mode .note-checkbox:hover .note-check-dot { transform: none; border-color: color-mix(in srgb, var(--fg) 35%, transparent); }
.note-checkbox:hover .note-check-dot {
border-color: var(--accent);
transform: scale(1.15);
}
.note-checkbox:active .note-check-dot {
transform: scale(0.9);
}
.note-checkbox.done .note-check-dot {
background: var(--accent);
border-color: var(--accent);
animation: note-check-pop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.note-checkbox.done .note-check-dot::after {
transform: translate(-50%, -50%) rotate(-45deg) scale(1);
}
@keyframes note-check-pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
100% { transform: scale(1); }
}
.note-check-text {
transition: opacity 0.25s;
line-height: 1.3;
padding-right: 4px;
word-break: break-word;
}
.note-checkbox.done .note-check-text {
position: relative;
opacity: 0.4;
}
.note-checkbox.done .note-check-text::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: calc(50% - 1px);
height: 1px;
background: currentColor;
animation: note-strike 0.3s ease-out forwards;
transform-origin: left center;
}
@keyframes note-strike {
0% { transform: scaleX(0); }
100% { transform: scaleX(1); }
}
/* Due date badge */
.note-due-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 10%, transparent);
color: var(--fg);
opacity: 0.6;
}
.note-due-overdue {
background: color-mix(in srgb, var(--red) 20%, transparent);
color: var(--red);
opacity: 1;
font-weight: 600;
}
/* Color classes — theme-aware via color-mix with --panel */
.note-color-red { background: color-mix(in srgb, var(--red) 18%, var(--panel)); border-color: color-mix(in srgb, var(--red) 30%, var(--border)); }
.note-color-orange { background: color-mix(in srgb, #d19a66 18%, var(--panel)); border-color: color-mix(in srgb, #d19a66 30%, var(--border)); }
.note-color-yellow { background: color-mix(in srgb, var(--hl-string) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-string) 30%, var(--border)); }
.note-color-green { background: color-mix(in srgb, #98c379 18%, var(--panel)); border-color: color-mix(in srgb, #98c379 30%, var(--border)); }
.note-color-blue { background: color-mix(in srgb, var(--hl-function) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-function) 30%, var(--border)); }
.note-color-purple { background: color-mix(in srgb, var(--hl-keyword) 18%, var(--panel)); border-color: color-mix(in srgb, var(--hl-keyword) 30%, var(--border)); }
/* 3D top-edge highlight — an inset 2px stripe inside every note card that
reads as a lifted "glassy" lip. Each colored note gets a stronger stripe
in its own hue so the colour reads even before you focus on the card. */
.note-card { box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent); }
.note-color-red { box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--red) 55%, transparent); }
.note-color-orange { box-shadow: inset 0 2px 0 0 color-mix(in srgb, #d19a66 60%, transparent); }
.note-color-yellow { box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--hl-string) 60%, transparent); }
.note-color-green { box-shadow: inset 0 2px 0 0 color-mix(in srgb, #98c379 60%, transparent); }
.note-color-blue { box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--hl-function) 60%, transparent); }
.note-color-purple { box-shadow: inset 0 2px 0 0 color-mix(in srgb, var(--hl-keyword) 60%, transparent); }
/* Color picker dots */
.note-color-picker {
display: flex;
gap: 5px;
align-items: center;
}
.note-color-dot {
width: 16px;
height: 16px;
border-radius: 50%;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.15s, transform 0.15s;
flex-shrink: 0;
}
.note-color-dot:hover { transform: scale(1.15); }
.note-color-dot.active {
border-color: var(--fg);
}
/* Note form */
.note-form {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
transition: background 0.2s, border-color 0.2s;
/* Container so the action buttons can collapse to icons when the note
card is narrow (see .note-form-collapsible). */
container-type: inline-size;
container-name: noteform;
}
@media (max-width: 768px) {
.notes-pane:not(.notes-view-grid) .note-form.note-form-new {
transform: translateY(6px);
}
}
/* Label sits after the icon in each action button */
.note-form-text-btn .nft-label { margin-left: 5px; }
/* Never let the action buttons spill outside the card */
.note-form-actions-group { flex-wrap: wrap; row-gap: 6px; }
/* When the note card is narrow, collapse Archive / Delete / Cancel to
icon-only (their text label hides; tooltip via title= remains). The
Save/Update button is NOT collapsible — it always keeps its label and
stays last. */
@container noteform (max-width: 360px) {
.note-form-collapsible .nft-label { display: none; }
.note-form-collapsible { padding-left: 9px; padding-right: 9px; }
}
.note-form.note-color-red { background: color-mix(in srgb, var(--red) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--red) 30%, var(--border)) !important; }
.note-form.note-color-orange { background: color-mix(in srgb, #d19a66 18%, var(--panel)) !important; border-color: color-mix(in srgb, #d19a66 30%, var(--border)) !important; }
.note-form.note-color-yellow { background: color-mix(in srgb, var(--hl-string) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-string) 30%, var(--border)) !important; }
.note-form.note-color-green { background: color-mix(in srgb, #98c379 18%, var(--panel)) !important; border-color: color-mix(in srgb, #98c379 30%, var(--border)) !important; }
.note-form.note-color-blue { background: color-mix(in srgb, var(--hl-function) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-function) 30%, var(--border)) !important; }
.note-form.note-color-purple { background: color-mix(in srgb, var(--hl-keyword) 18%, var(--panel)) !important; border-color: color-mix(in srgb, var(--hl-keyword) 30%, var(--border)) !important; }
.note-form-header {
display: flex;
align-items: center;
gap: 8px;
padding-right: 0;
}
.note-form-header .note-form-remind-btn {
margin-right: -7px; /* align bell center with X-button column on todo rows */
position: relative;
top: -2px;
}
.note-form-title {
flex: 1;
min-width: 0;
background: transparent;
border: none;
border-bottom: 1px solid var(--border);
color: var(--fg);
font-size: 14px;
font-weight: 600;
padding: 4px 0;
outline: none;
}
.note-form-title:focus { border-color: var(--fg); }
.note-form-header { position: relative; }
.note-form-header .note-form-due {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
border: 0;
padding: 0;
margin: 0;
right: 0;
bottom: 0;
}
.note-form-due-btn {
flex: 0 0 auto;
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
border-radius: 4px;
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0.85;
transition: opacity 0.15s, border-color 0.15s, background 0.15s;
}
.note-form-due-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.note-form-due-btn.has-date {
opacity: 1;
border-style: solid;
background: color-mix(in srgb, var(--accent) 18%, transparent);
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
.note-type-toggle {
display: flex;
gap: 4px;
}
.note-type-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
font-size: 11px;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s, background 0.15s;
}
.note-type-btn.active {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.note-type-btn:hover { opacity: 0.8; }
.note-form-content {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
font-size: 12px;
padding: 8px;
border-radius: 4px;
resize: vertical;
/* Roomier default so editing a note isn't cramped; JS auto-grow takes
it further as you type. */
min-height: 120px;
line-height: 1.5;
font-family: inherit;
outline: none;
}
.note-form-content:focus { border-color: var(--fg); }
.note-form-meta {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.note-form-meta .note-form-label {
width: 112px;
flex: 0 1 140px;
}
.note-form-meta .note-color-picker { gap: 3px; }
.note-form-actions-group {
display: flex;
gap: 6px;
align-items: center;
/* Take the full width of the meta row so Archive/Delete can sit on the
LEFT side and the spacer pushes Cancel + Update to the right edge. */
flex: 1 1 auto;
flex-shrink: 0;
flex-wrap: nowrap;
}
.note-form-actions-spacer { flex: 1 1 auto; }
/* Unified text+icon action button — used for Archive, Delete, Cancel, Update
so all 4 read as a matched set. Matches the existing .note-form-save
sizing/padding via the rules at ~25023. */
.note-form-text-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid var(--border);
color: var(--fg);
border-radius: 6px;
padding: 5px 12px;
font-size: 12px;
font-family: inherit;
font-weight: 500;
cursor: pointer;
transition: background 0.12s, border-color 0.12s, color 0.12s;
white-space: nowrap;
}
.note-form-text-btn:hover {
background: color-mix(in srgb, var(--fg) 14%, transparent);
border-color: color-mix(in srgb, var(--fg) 28%, var(--border));
}
.note-form-text-btn.note-form-save {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 45%, var(--border));
color: var(--accent, var(--red));
font-weight: 600;
}
.note-form-text-btn.note-form-save:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 28%, transparent);
}
.note-form-text-btn.danger:hover {
background: color-mix(in srgb, var(--red) 14%, transparent);
border-color: color-mix(in srgb, var(--red) 45%, var(--border));
color: var(--red);
}
.note-form-icon-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
border-radius: 4px;
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.65;
transition: opacity 0.15s, border-color 0.15s, background 0.15s, color 0.15s;
}
/* Force the inner icons to render at the inline-attribute size — some other
stylesheets clamp `svg { width: ... }` globally and that shrinks them. */
.note-form-icon-btn svg { width: 31px !important; height: 31px !important; }
.note-form-icon-btn:hover { opacity: 1; border-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
.note-form-delete-btn:hover { color: var(--red); border-color: color-mix(in srgb, var(--red) 50%, var(--border)); background: color-mix(in srgb, var(--red) 8%, transparent); }
/* Draw mode — canvas + small toolbar */
.note-form-draw-wrap {
display: flex;
flex-direction: column;
gap: 6px;
position: relative;
}
/* X overlay on the canvas — appears only when a photo is loaded as the
background (via _wireCanvas). Clicking it wipes the canvas back to white. */
.note-form-draw-bg-rm {
position: absolute;
top: 8px;
right: 8px;
z-index: 3;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 18px;
line-height: 1;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.note-form-draw-bg-rm:hover { background: rgba(0, 0, 0, 0.8); }
/* Draw mode hides the note's background-color picker and the standalone
image preview — they don't make sense alongside a canvas. Uses :has() so
it kicks in whenever the type-seg flips to .is-draw, in addition to the
JS that sets display:none. */
.note-form:has(.note-form-type-seg.is-draw) .note-color-picker,
.note-form:has(.note-form-type-seg.is-draw) .note-form-image-wrap {
display: none !important;
}
.note-form-canvas {
background: #ffffff;
border: 1px solid var(--border);
border-radius: 6px;
touch-action: none;
max-width: 100%;
cursor: crosshair;
}
.note-form-draw-toolbar {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.note-form-draw-tool {
display: inline-flex;
align-items: center;
height: 26px;
}
/* Color picker rendered as a flat circular swatch — the native chrome around
would clash with the rest of the toolbar. */
.note-form-draw-color {
appearance: none;
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid color-mix(in srgb, var(--fg) 25%, transparent);
background: none;
cursor: pointer;
padding: 0;
overflow: hidden;
box-sizing: border-box;
flex: 0 0 24px;
}
/* After attachColorPicker swaps the input to type=text + .cp-swatch-input,
re-pin the swatch to a 24px circle (otherwise it inherits text-input
sizing and becomes a giant rectangle). */
.note-form-draw-color.cp-swatch-input {
width: 24px !important;
height: 24px !important;
min-width: 24px !important;
max-width: 24px !important;
border-radius: 50%;
padding: 0;
box-sizing: border-box;
}
.note-form-draw-color::-webkit-color-swatch-wrapper { padding: 0; border-radius: 50%; }
.note-form-draw-color::-webkit-color-swatch { border: none; border-radius: 50%; }
.note-form-draw-color::-moz-color-swatch { border: none; border-radius: 50%; }
.note-form-draw-size-wrap { width: 110px; display: inline-flex; align-items: center; }
.note-form-photo-plus {
font-size: 16px;
line-height: 1;
font-weight: 600;
opacity: 0.8;
}
/* Mirrors .gallery-editor-container input[type=range] — slim pill track that
grows on interaction, red circular thumb. */
.note-form-draw-size {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: color-mix(in srgb, var(--fg) 25%, transparent);
border-radius: 999px;
accent-color: var(--red);
cursor: pointer;
transition: height 0.15s ease;
}
.note-form-draw-size:hover,
.note-form-draw-size:focus,
.note-form-draw-size:active { height: 10px; }
.note-form-draw-size::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease, height 0.12s ease;
}
.note-form-draw-size::-moz-range-thumb {
width: 12px; height: 12px;
border-radius: 50%;
background: var(--red);
border: none;
cursor: pointer;
transition: width 0.12s ease, height 0.12s ease;
}
.note-form-draw-size:hover::-webkit-slider-thumb,
.note-form-draw-size:focus::-webkit-slider-thumb,
.note-form-draw-size:active::-webkit-slider-thumb { width: 18px; height: 18px; }
.note-form-draw-size:hover::-moz-range-thumb,
.note-form-draw-size:focus::-moz-range-thumb,
.note-form-draw-size:active::-moz-range-thumb { width: 18px; height: 18px; }
/* Brush | Eraser segmented toggle — sliding pill, same recipe as Note/Todo. */
.note-form-draw-be {
display: inline-flex;
height: 32px;
/* Accent-colored box around the whole control so it's instantly readable
as "switch is on", and the sliding pill inside indicates which side. */
border: 2px solid var(--accent);
border-radius: 10px;
overflow: hidden;
position: relative;
flex-shrink: 0;
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.note-form-draw-be::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
background: var(--accent);
border-radius: 9px;
transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 0;
}
.note-form-draw-be.is-eraser::before { transform: translateX(100%); }
.note-form-draw-be-btn {
background: none;
border: none;
color: color-mix(in srgb, var(--fg) 50%, transparent);
cursor: pointer;
padding: 0 8px;
width: 34px;
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 1;
transition: color 0.18s;
}
.note-form-draw-be-btn:hover { color: var(--fg); }
/* Active side picks up the theme accent so brush vs eraser is unmistakable. */
.note-form-draw-be-btn.active { color: var(--accent-primary, var(--red)); }
.note-form-draw-be-btn:focus { outline: none; }
.note-form-draw-text,
.note-form-draw-line,
.note-form-draw-circle,
.note-form-draw-undo {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
border-radius: 4px;
height: 32px;
width: 34px;
font-family: inherit;
font-size: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0.7;
position: relative;
transition: opacity 0.15s, border-color 0.15s, background 0.15s;
}
.note-form-draw-text { font-weight: 700; font-family: Georgia, serif; font-size: 16px; }
/* Size badge in the bottom-right of T — empty until a size is chosen. */
.note-form-draw-text-badge {
position: absolute;
right: 3px;
bottom: 1px;
font-family: inherit;
font-weight: 700;
font-size: 8px;
line-height: 1;
letter-spacing: 0.3px;
/* Bare --accent is undefined here, was rendering invisible — use the
defined accent so the S/M/L size badge actually shows in theme colour. */
color: var(--accent-primary, var(--red));
}
.note-form-draw-text-badge:empty { display: none; }
.note-form-draw-shape-badge {
position: absolute;
right: 3px;
bottom: 1px;
font-family: inherit;
font-weight: 700;
font-size: 8px;
line-height: 1;
letter-spacing: 0.3px;
color: var(--accent-primary, var(--red));
}
.note-form-draw-shape-badge:empty { display: none; }
.note-form-draw-text:hover,
.note-form-draw-line:hover,
.note-form-draw-circle:hover,
.note-form-draw-undo:hover {
opacity: 1;
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 8%, transparent);
}
.note-form-draw-text.active,
.note-form-draw-line.active,
.note-form-draw-circle.active {
opacity: 1;
background: color-mix(in srgb, var(--accent) 18%, transparent);
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
}
/* Reflect the chosen size in the icon itself, so the user sees at a glance
which size (S/M/L) is active without needing the tiny badge. */
.note-form-draw-line.size-s svg line,
.note-form-draw-circle.size-s svg circle { stroke-width: 1.2; }
.note-form-draw-line.size-m svg line,
.note-form-draw-circle.size-m svg circle { stroke-width: 3; }
.note-form-draw-line.size-l svg line,
.note-form-draw-circle.size-l svg circle { stroke-width: 5; }
.note-form-draw-text.size-s { font-size: 13px; }
.note-form-draw-text.size-m { font-size: 18px; }
.note-form-draw-text.size-l { font-size: 23px; line-height: 1; }
/* Narrow widths: shrink the tag input to make room for the action group;
the hashtag-in-content shortcut still works, so the input can be tiny. */
@media (max-width: 600px) {
.note-form-meta .note-form-label { width: 78px; flex-shrink: 1; min-width: 0; }
}
.note-form-due {
background: transparent;
border: 1px dashed color-mix(in srgb, var(--border) 60%, transparent);
color: var(--fg);
font-size: 11px;
padding: 3px 6px;
border-radius: 4px;
outline: none;
opacity: 0.4;
transition: opacity 0.15s, border-color 0.15s;
}
.note-form-due:hover, .note-form-due:focus { opacity: 0.9; border-color: var(--border); }
.note-form-due:not([value=""]) { opacity: 0.85; border-style: solid; }
.note-form-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
}
.note-form-save,
.note-form-cancel,
.note-form-archive {
background: transparent;
border: 1px solid var(--border);
color: var(--fg);
font-size: 11px;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
transition: background 0.15s;
}
.note-form-save { background: color-mix(in srgb, var(--fg) 12%, transparent); font-weight: 600; }
.note-form-save:hover { background: color-mix(in srgb, var(--fg) 20%, transparent); }
.note-form-cancel:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
.note-form-archive:hover { background: color-mix(in srgb, var(--fg) 8%, transparent); }
/* Checklist inputs in form */
.note-checklist-inputs {
display: flex;
flex-direction: column;
gap: 4px;
}
.note-cl-row {
display: flex;
align-items: center;
gap: 6px;
}
.note-cl-dot {
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid color-mix(in srgb, var(--fg) 30%, transparent);
flex-shrink: 0;
cursor: pointer;
transition: all 0.15s;
}
.note-cl-dot:hover { border-color: var(--fg); transform: scale(1.2); }
.note-cl-row.done .note-cl-dot {
background: var(--accent);
border-color: var(--accent);
}
.note-cl-row.done .note-cl-text {
opacity: 0.4;
background: linear-gradient(currentColor, currentColor) no-repeat;
background-size: 0 1px;
background-position: 0 calc(50% - 1px);
animation: cl-strike 0.32s ease-out forwards;
transition: opacity 0.2s ease;
}
/* Draws the strikethrough line left-to-right when the row is marked
done, instead of snapping it in full-width on the same frame. */
@keyframes cl-strike {
to { background-size: 100% 1px; }
}
.note-cl-text {
background: transparent;
border: none;
border-bottom: 1px solid var(--border);
color: var(--fg);
font-size: 12px;
padding: 3px 0 3px 3px;
flex: 1 1 auto;
width: auto;
min-width: 0;
outline: none;
}
.note-cl-text:focus { border-color: var(--fg); }
.note-cl-rm {
background: none;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
font-size: 14px;
padding: 0;
width: 24px;
height: 14px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
line-height: 1;
}
.note-cl-rm:hover { opacity: 0.8; color: var(--red); }
.note-cl-add {
background: none;
border: none;
color: var(--fg);
opacity: 0.4;
cursor: pointer;
font-size: 11px;
padding: 4px 0;
text-align: left;
}
.note-cl-add:hover { opacity: 0.7; }
/* ── Calendar ── */
.cal-modal-content { width:min(680px, 94vw); max-height:88vh; overflow-x:hidden; }
/* Was overflow-y:auto with grid + day-detail flowing naturally. Now uses a
true splitter layout: the body is a flex column that doesn't scroll
itself; the grid takes the remaining space and scrolls if needed; the
day-detail has an explicit height (driven by the splitter drag via
--cal-detail-h) and shrinks the grid visually as it grows. */
#cal-body { display:flex; flex-direction:column; gap:8px; overflow:hidden; padding:0; min-height:0; }
#cal-body > .cal-grid { flex: 1 1 auto; min-height: 120px; overflow-y: auto; overflow-x: hidden; }
@media (max-width: 768px) {
/* Let the calendar pane shrink to nothing on mobile so the day-detail
splitter can be dragged all the way to the top, hiding the calendar
entirely. Without min-height:0 the grid (or the week-wrap below)
refuses to shrink past its built-in floor. */
#cal-body > .cal-grid,
#cal-body > .cal-wk-wrap {
min-height: 0;
max-height: none;
}
/* Let the week grid fill the calendar pane and scroll its hours internally
instead of overflowing #cal-body (which clips it and feels cramped). */
#cal-body > .cal-wk-wrap {
flex: 1 1 auto;
overflow: auto;
}
}
.cal-toolbar { display:flex; align-items:center; gap:6px; margin-bottom:8px; flex-wrap:wrap; line-height:1; max-width:100%; row-gap:6px; }
/* Quick-add bar: natural-language event creation. Sits directly under
the toolbar; focused with `Q`; opens the bespoke form pre-filled. */
.cal-quickadd-row {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 8px;
padding: 1px 10px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
transition: border-color 0.15s, background 0.15s;
position: relative;
}
/* Two-tone placeholder hint: "Quick add" in accent, the example in grey, both
at one low opacity. Overlays the (empty) input; hidden once the user types. */
.cal-quickadd-hint {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: var(--fg);
opacity: 0.5;
pointer-events: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: calc(100% - 20px);
}
.cal-quickadd-hint .qa-hint-accent { color: var(--accent, var(--red)); }
/* ↵ enter glyph in the hint — makes it obvious the field submits on Enter. */
.cal-quickadd-hint .qa-hint-enter { vertical-align: -2px; color: var(--accent, var(--red)); opacity: 0.8; }
/* Matching ↵ hint at the right of the event title while it's empty; hidden once
editing (the ✓ Done button takes over then). */
/* Title placeholder overlay: the prompt text with a ↵ enter glyph right after
it (accent), so it's clear the field submits on Enter. Shown only while the
title is empty; the native placeholder is hidden in favour of this. */
.cal-hero-title::placeholder { color: transparent; }
.cal-title-hint {
position: absolute;
left: 0;
top: 50%;
transform: translateY(calc(-50% - 2px));
display: none;
align-items: center;
gap: 7px;
font-size: 18px;
line-height: 1.2;
white-space: nowrap;
pointer-events: none;
color: color-mix(in srgb, var(--fg) 42%, transparent);
}
.cal-hero-title:placeholder-shown ~ .cal-title-hint { display: inline-flex; }
.cal-title-hint .cal-title-enter-ico { color: var(--accent, var(--red)); flex-shrink: 0; }
/* Pure-CSS toggle: hide the hint as soon as the field has real text (no
per-keystroke JS, so typing never triggers a re-render / focus loss). */
.cal-quickadd-input:not(:placeholder-shown) ~ .cal-quickadd-hint { display: none; }
.cal-quickadd-row:focus-within {
border-color: color-mix(in srgb, var(--accent, var(--fg)) 55%, var(--border));
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
}
.cal-quickadd-icon {
color: color-mix(in srgb, var(--accent, var(--fg)) 80%, transparent);
display: inline-flex;
align-items: center;
flex-shrink: 0;
opacity: 0.7;
}
.cal-quickadd-input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
outline: none;
color: var(--fg);
font-family: inherit;
font-size: 12px;
height: 22px;
padding: 0;
}
.cal-quickadd-input::placeholder { color: transparent; }
.cal-quickadd-status {
font-size: 10px;
opacity: 0.55;
font-variant-numeric: tabular-nums;
}
.cal-toolbar > *, .cal-toolbar-nav > *, .cal-toolbar-right > * { vertical-align:middle; }
.cal-toolbar-nav { display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; }
.cal-toolbar-right { display:inline-flex; align-items:center; gap:6px; margin-left:auto; flex-wrap:wrap; margin-top:6px; }
.cal-title { font-size:13px; font-weight:600; white-space:nowrap; height:24px; line-height:30px; padding:0 6px; display:inline-block; vertical-align:middle; box-sizing:border-box; }
button.cal-nav { background:color-mix(in srgb, var(--fg) 6%, transparent); border:1px solid var(--border); color:var(--fg); border-radius:5px; padding:0 8px; height:24px; line-height:22px; cursor:pointer; font-size:11px; font-family:inherit; box-sizing:border-box; vertical-align:middle; }
button.cal-nav:hover { background:color-mix(in srgb, var(--fg) 12%, transparent); }
button.cal-today-btn { font-size:10px; opacity:0.5; }
button.cal-add-btn { background:var(--accent); color:#fff; border:none; border-radius:50%; width:24px; height:24px; line-height:22px; font-size:18px; cursor:pointer; flex-shrink:0; padding:0; box-sizing:border-box; font-family:inherit; vertical-align:middle; text-align:center; }
button.cal-add-btn.cal-add-btn-text {
width:auto; min-width:0;
background: color-mix(in srgb, var(--fg) 8%, transparent);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 0 10px 0 6px;
display: inline-flex;
align-items: center;
gap: 4px;
height: 24px;
line-height: 1;
margin-top: 0;
transition: background 0.15s, border-color 0.15s;
}
/* Settings "+ Add server" matches the model-dir "+ Add" path button (22px). */
#cookbook-server-add.cal-add-btn-text { height: 21px; border-radius: 11px; position: relative; top: 3px; }
button.cal-add-btn.cal-add-btn-text:hover {
background: color-mix(in srgb, var(--fg) 14%, transparent);
border-color: var(--accent);
opacity: 1;
}
.cal-add-plus {
font-size: 16px;
line-height: 1;
color: var(--accent, var(--red));
font-weight: 500;
display: inline-block;
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
padding-bottom: 2px;
}
button.cal-add-btn.cal-add-btn-text:hover .cal-add-plus {
transform: rotate(180deg);
}
/* Mobile tap-spin: a gentle quarter-turn nudge before the form opens.
`.cal-add-plus` has a `padding-bottom: 2px` (and a translateY on the
small variant) that offsets the glyph for optical centering — those
shift the rotation pivot off the visual centre. Force the box to
match the glyph and pin the transform-origin during the spin. */
.cal-add-plus.cal-add-spinning {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
width: 1em;
height: 1em;
line-height: 1;
transform-origin: 50% 50%;
animation: cal-add-spin 0.28s ease-out forwards;
}
@keyframes cal-add-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(90deg); }
}
.cal-add-label { font-size:11px; font-weight:600; line-height:1; opacity:0.85; }
/* Small variant of the toolbar's +New button. Inherits the .cal-add-btn-text
styles (pill with "+" + "New" + spring-rotate on the +), so the
animation is identical. Just shrunk a bit. */
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm {
height: 20px;
border-radius: 10px;
/* At rest: tight, equal padding around the "+" so it sits centred
with no trailing gap. On hover the right pad and the flex gap grow
to make room for the revealed "New". */
padding: 0 5px;
gap: 0;
transition: padding 0.25s ease, gap 0.25s ease, background 0.15s, border-color 0.15s;
}
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm:hover {
padding: 0 8px 0 5px;
gap: 3px;
}
/* Don't touch .cal-add-plus on the small variant — the toolbar +New is
the reference and we want the same rotation centre / motion. The label
is shrunk and hidden until hover so the pill is compact at rest. */
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-label {
font-size: 10px;
max-width: 0;
margin-left: 0;
opacity: 0;
overflow: hidden;
white-space: nowrap;
transition: max-width 0.25s ease, opacity 0.18s ease, margin-left 0.25s ease;
}
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm:hover .cal-add-label {
max-width: 40px;
margin-left: 3px;
opacity: 0.85;
}
.cal-add-btn:hover { opacity:0.85; }
/* Mobile: bump the +New pill and the +/- zoom buttons up to a tap-friendly
size so they're easy to hit with a thumb. */
@media (max-width: 768px) {
/* Toolbar +New (relocated by JS into the quickadd row on mobile) */
button.cal-add-btn.cal-add-btn-text {
height: 36px;
border-radius: 18px;
padding: 0 14px 0 12px;
gap: 6px;
flex-shrink: 0;
}
/* Day-detail +New: just a "+" on mobile — drop the label entirely. */
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm {
width: 36px;
height: 36px;
border-radius: 50%;
padding: 0;
gap: 0;
justify-content: center;
}
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-label {
display: none;
}
.cal-add-plus { font-size: 22px; padding-bottom: 1px; }
.cal-add-label { font-size: 13px; }
.cal-wk-zoom {
width: 36px;
height: 36px;
font-size: 20px;
border-radius: 8px;
opacity: 0.85;
}
/* The +New button sits as a sibling of the quickadd row inside a flex
wrapper, so it's clearly outside the input's bordered box. The row
itself keeps its own styling. */
.cal-quickadd-wrap {
display: flex;
align-items: stretch;
gap: 8px;
margin: 0 0 8px;
}
.cal-quickadd-wrap > #cal-quickadd-row {
flex: 1;
min-width: 0;
margin: 0;
}
.cal-quickadd-wrap > .cal-add-btn-text {
align-self: stretch;
flex-shrink: 0;
}
/* Nudge the "+" glyph up one pixel on the round day-detail button so it
sits visually centred inside the circle. */
button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm .cal-add-plus {
transform: translateY(-1px);
}
/* Event form: bigger Create / Cancel buttons so they're easy to tap. */
.cal-form-actions button.cal-btn {
height: 44px;
padding: 0 20px;
font-size: 14px;
border-radius: 8px;
min-width: 96px;
}
/* Left-align the day/month + tags row in the day-detail panel and
the calendar/category chip row so they sit flush against the
left edge on a narrow screen. */
.cal-detail-header {
justify-content: flex-start;
gap: 12px;
}
.cal-filters {
justify-content: flex-start;
}
}
/* Refresh button spin — driven by JS toggling .cal-syncing on the button.
transform-box + transform-origin keep the rotation pivoted at the SVG's
own centre so it spins in place instead of orbiting / drifting up. */
#cal-sync svg,
#email-lib-refresh-btn svg { display: block; transform-box: fill-box; transform-origin: 50% 50%; }
/* Brief checkmark flash after a refresh completes. Lasts ~900ms in JS;
the small bounce + accent color give a clear "done" cue. */
#cal-sync.cal-sync-done svg,
#email-lib-refresh-btn.email-lib-refresh-done svg {
color: var(--accent, #2ea043);
animation: refresh-check-pop 0.32s ease-out;
}
@keyframes refresh-check-pop {
0% { transform: scale(0.55); opacity: 0.4; }
60% { transform: scale(1.18); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
#cal-sync.cal-syncing svg,
#email-lib-refresh-btn.email-lib-refreshing svg { animation: spin 0.6s linear infinite; }
.email-undone-toggle.active,
.email-attach-toggle.active {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
color: var(--accent, var(--red));
font-weight: 600;
}
/* Hide the gallery-style reader-btn labels on desktop — only the mobile
@media block re-displays them under each icon. Desktop keeps compact
icon-only reader buttons. */
.reader-btn-label {
display: inline-block;
font-size: 9px;
font-weight: 500;
line-height: 1;
letter-spacing: 0.02em;
white-space: nowrap;
opacity: 0.85;
}
/* Inline attachment-filter toggle nested inside the search input. The
input has padding-right:34px set inline so its text doesn't slide
under the button. */
.email-search-wrap { display: flex; align-items: center; }
/* Keep the email search input and the adjacent "Select" button identical in
height regardless of base-vs-mobile overrides — pin both explicitly. */
.email-search-row .memory-search-input {
height: 32px;
min-height: 32px;
box-sizing: border-box;
}
.email-search-row .email-search-select-btn {
height: 32px;
min-height: 32px;
box-sizing: border-box;
margin-top: 6px;
flex-shrink: 0;
}
/* Select moved into the dropdown row — dock it to the right and match the
selects' vertical nudge (.memory-sort-select has top:3px). */
.memory-category-filters .email-filter-select-btn,
.memory-category-filters .email-filter-refresh-btn {
position: relative;
top: 3px;
flex-shrink: 0;
}
.memory-category-filters .email-filter-refresh-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 8px;
}
.memory-category-filters .email-reminders-clear-btn {
position: relative;
top: -2px;
flex-shrink: 0;
}
.email-attach-toggle-inline {
position: absolute !important;
right: 4px !important;
top: 50%;
transform: translateY(calc(-50% - 3px));
width: 26px !important;
height: 26px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 6px !important;
background: transparent !important;
border: none !important;
opacity: 0.55;
}
.email-attach-toggle-inline:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
}
.email-attach-toggle-inline.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
color: var(--accent, var(--red)) !important;
}
/* Inline undone-toggle nested inside the search input, sits LEFT of the
attach toggle. Same visual treatment. */
.email-undone-toggle-inline {
position: absolute !important;
right: 34px !important;
top: 50%;
transform: translateY(calc(-50% - 3px));
width: 26px !important;
height: 26px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 6px !important;
background: transparent !important;
border: none !important;
opacity: 0.55;
}
.email-reminder-toggle-inline {
position: absolute !important;
right: 64px !important;
top: 50%;
transform: translateY(calc(-50% - 3px));
width: 26px !important;
height: 26px !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
border-radius: 6px !important;
background: transparent !important;
border: none !important;
opacity: 0.55;
}
.email-search-wrap.email-reminder-bell-hidden .memory-search-input {
padding-right: 66px !important;
}
.email-reminder-toggle-inline:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
}
.email-reminder-toggle-inline:hover svg {
animation: none !important;
transform: none !important;
}
.email-reminder-toggle-inline.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
color: var(--accent, var(--red)) !important;
}
.email-undone-toggle-inline:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 10%, transparent) !important;
}
.email-undone-toggle-inline.active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent) !important;
color: var(--accent, var(--red)) !important;
}
/* Round (circular) toggles in the search bar, matching the app's default icon
buttons. Always fully visible (not dimmed until hover). */
.email-attach-toggle-inline,
.email-undone-toggle-inline,
.email-reminder-toggle-inline { border-radius: 50% !important; opacity: 1 !important; }
.email-attach-toggle:not(.email-attach-toggle-inline):hover svg {
animation: email-undone-jiggle 0.45s ease-in-out;
transform-origin: 50% 50%;
}
@keyframes email-undone-jiggle {
0% { transform: rotate(0deg); }
20% { transform: rotate(-14deg); }
40% { transform: rotate(10deg); }
60% { transform: rotate(-6deg); }
80% { transform: rotate(3deg); }
100% { transform: rotate(0deg); }
}
.email-undone-toggle:not(.email-undone-toggle-inline):hover svg,
.email-compose-jiggle:hover svg {
animation: email-undone-jiggle 0.45s ease-in-out;
transform-origin: 50% 50%;
}
.email-accounts-row {
display: flex;
align-items: center;
gap: 10px;
padding: 0 0 6px;
}
.email-accounts-row .doclib-desc { flex-shrink: 0; }
/* Only the direct-child compose button gets pushed right; nested chips
inside #email-lib-accounts pack to the left as normal flex items. */
.email-accounts-row > .memory-toolbar-btn { flex-shrink: 0; margin-left: auto; }
#email-lib-accounts { justify-content: flex-start; }
/* Refresh button now lives top-right in the modal header next to the close X.
Borderless (matches the close X), and a fixed square box so the spin and the
refresh→checkmark icon swap never change its size or nudge neighbours. */
.email-lib-header-actions #email-lib-refresh-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
border: none;
background: none;
padding: 0;
width: 24px;
height: 24px;
}
/* Bump the icon up on mobile for a bigger visual — but keep the button BOX at
24px so it never exceeds the header's natural height (the title/close set
that). A taller box would grow the sticky header and push the list down. */
@media (max-width: 768px) {
.email-lib-header-actions #email-lib-refresh-btn svg {
width: 17px;
height: 17px;
}
}
.cal-view-toggle {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--border);
border-radius: 5px;
overflow: hidden;
vertical-align: middle;
box-sizing: border-box;
padding: 0;
margin: 0;
line-height: 1;
}
button.cal-view-btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--fg);
font-size: 11px;
font-family: inherit;
font-weight: 500;
padding: 2px 12px 2px 12px;
margin: 0;
cursor: pointer;
opacity: 0.45;
line-height: 1;
height: 24px;
box-sizing: border-box;
}
.cal-view-btn + .cal-view-btn { border-left: 1px solid var(--border); }
.cal-view-btn:hover { opacity: 0.75; }
.cal-view-btn.active {
background: color-mix(in srgb, var(--fg) 12%, transparent);
opacity: 1;
font-weight: 600;
}
/* Toolbar holds only the toggle button; the chip row renders below the
toolbar on its own line and wraps freely. */
.cal-filters { display:flex; gap:6px; margin:6px 0 8px; flex-wrap:wrap; }
/* Round the native color-input swatch in calendar settings. The outer
wraps a colored fill drawn by the browser; we need to round both
the wrapper and the fill so it actually appears circular. */
.cal-s-color { border-radius: 50%; overflow: hidden; }
.cal-s-color::-webkit-color-swatch-wrapper { padding: 0; border-radius: 50%; }
.cal-s-color::-webkit-color-swatch { border: none; border-radius: 50%; }
.cal-s-color::-moz-color-swatch { border: none; border-radius: 50%; }
.cal-filter-item { display:inline-flex; align-items:center; gap:4px; cursor:pointer; font-size:10px; padding:1px 8px; line-height:1.4; border-radius:10px; background:color-mix(in srgb, var(--fg) 5%, transparent); border:1px solid var(--border); transition:opacity 0.15s; }
.cal-filter-item:hover { background:color-mix(in srgb, var(--fg) 10%, transparent); }
.cal-filter-item.cal-filter-off { opacity:0.25; text-decoration:line-through; }
.cal-filter-item.cal-filter-important { font-weight:700; color:#e5a33a; border-color:color-mix(in srgb, #e5a33a 40%, transparent); }
.cal-filter-item.cal-filter-important.cal-filter-active { background:color-mix(in srgb, #e5a33a 18%, transparent); border-color:#e5a33a; }
/* Match the toolbar's other buttons (cal-nav, cal-view-btn, cal-add-btn-text):
24px tall, 11px font, 5px corners. */
.cal-filter-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--fg) 6%, transparent);
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
font-size: 11px;
font-family: inherit;
font-weight: 500;
padding: 0 10px;
height: 24px;
line-height: 1;
border-radius: 5px;
box-sizing: border-box;
vertical-align: middle;
opacity: 0.65;
position: relative;
top: -3px;
}
.cal-filter-toggle:hover { opacity: 1; background: color-mix(in srgb, var(--fg) 12%, transparent); }
.cal-filter-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; }
/* The grid is now a flex column: one row of weekday headers + six
.cal-week-row rows. Each row holds the 7 day cells in its own
internal grid plus the absolute-positioned multi-day overlays.
--bars on a week-row is the number of multi-day events overlapping
it; cells in that row use it to reserve top padding so the bar
overlays don't cover the day numbers or single-event rows. */
.cal-grid { display:flex; flex-direction:column; gap:1px; background:color-mix(in srgb, var(--fg) 8%, transparent); border-radius:6px; overflow:hidden; }
.cal-week-headers { display:grid; grid-template-columns:repeat(7,1fr); gap:1px; }
.cal-week-row { position:relative; display:grid; grid-template-columns:repeat(7,1fr); gap:1px; }
.cal-week-row > .cal-day { padding-top: calc(4px + var(--bars, 0) * 12px); }
@keyframes cal-slide-from-right {
from { opacity:0; transform:translateX(20px); }
to { opacity:1; transform:translateX(0); }
}
@keyframes cal-slide-from-left {
from { opacity:0; transform:translateX(-20px); }
to { opacity:1; transform:translateX(0); }
}
.cal-slide-in-right { animation: cal-slide-from-right 0.22s cubic-bezier(0.25, 0.8, 0.25, 1); }
.cal-slide-in-left { animation: cal-slide-from-left 0.22s cubic-bezier(0.25, 0.8, 0.25, 1); }
.cal-weekday { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); text-align:center; font-size:10px; font-weight:600; opacity:0.4; padding:5px 0; }
.cal-day { background:var(--bg); min-height:78px; padding:3px; cursor:pointer; position:relative; transition:background 0.12s; overflow:hidden; }
.cal-day:hover { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); }
.cal-day.cal-today { box-shadow:inset 0 0 0 2px var(--accent, var(--red)); background:color-mix(in srgb, var(--accent, var(--red)) 15%, var(--bg)); border-radius:8px; }
.cal-day.cal-today .cal-day-num { color:var(--bg); font-weight:800; background:var(--accent, var(--red)); border-radius:10px; padding:1px 6px; display:inline-block; opacity:1; line-height:1.3; }
/* Selected day — same square geometry as `.cal-today` so the two read as
peers, but rendered as an outline + softer fill instead of a solid
accent block. Today wins when both classes are on the same cell. */
.cal-day.cal-selected:not(.cal-today) {
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent, var(--fg)) 65%, transparent);
background: color-mix(in srgb, var(--accent, var(--fg)) 12%, var(--bg));
border-radius: 8px;
}
.cal-day.cal-selected:not(.cal-today) .cal-day-num {
color: var(--accent, var(--fg));
opacity: 1;
font-weight: 700;
}
.cal-day.cal-other { opacity:0.25; }
.cal-day.cal-drag-over { background:color-mix(in srgb, var(--accent) 18%, var(--bg)); }
.cal-day.cal-range-select { background:color-mix(in srgb, var(--accent, var(--red)) 25%, var(--bg)); box-shadow:inset 0 0 0 1px var(--accent, var(--red)); }
.cal-day-num { font-size:10px; font-weight:600; display:block; margin-bottom:1px; opacity:0.7; line-height:1.2; }
.cal-dots { display:flex; flex-direction:row; align-items:center; gap:2px; flex-wrap:nowrap; margin-bottom:1px; height:6px; }
.cal-dot { width:5px; height:5px; border-radius:50%; display:inline-block; cursor:grab; flex-shrink:0; }
.cal-dot-more { font-size:7px; opacity:0.4; }
.cal-event-row { display:flex; align-items:center; gap:3px; padding:1px 2px; border-radius:2px; cursor:grab; line-height:1.15; margin-bottom:1px; }
.cal-event-row:hover { background:color-mix(in srgb, var(--fg) 8%, transparent); }
.cal-event-row-dot { width:4px; height:4px; border-radius:50%; flex-shrink:0; }
.cal-event-row-time { font-size:9px; opacity:0.5; font-variant-numeric:tabular-nums; flex-shrink:0; }
.cal-event-row-name { font-size:9px; opacity:0.8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1; min-width:0; }
.cal-event-more { font-size:8px; opacity:0.4; padding-left:6px; }
/* Multi-day bar — positioned as an absolute overlay inside its
.cal-week-row so it spans uninterrupted across every column it
covers. --col is the starting column (0–6), --span is the number
of days it covers within the row, --slot stacks parallel bars. */
.cal-multiday {
position: absolute;
left: calc(var(--col, 0) * (100% / 7));
width: calc(var(--span, 1) * (100% / 7));
top: calc(2px + var(--slot, 0) * 12px);
height: 11px;
font-size: 8px;
line-height: 11px;
padding: 0 4px;
border-radius: 3px;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: grab;
opacity: 0.92;
z-index: 2;
pointer-events: auto;
}
.cal-dragging { opacity:0.3; }
/* Splitter between the month/week grid and the day-tasks panel — drag
vertically to give the day panel more room. Height clamped by the
`--cal-detail-h` variable set on #cal-body by the splitter drag JS. */
.cal-splitter {
height: 8px;
margin: 6px -10px 0;
cursor: row-resize;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
user-select: none;
touch-action: none;
}
/* Mobile: fat hitbox so the splitter is easy to grab with a thumb,
without changing how the grip itself looks. */
@media (max-width: 768px) {
.cal-splitter {
height: 28px;
margin-top: 0;
margin-bottom: 0;
}
.cal-splitter-grip {
width: 56px;
height: 4px;
}
}
.cal-splitter-grip {
width: 42px;
height: 3px;
border-radius: 2px;
background: color-mix(in srgb, var(--fg) 25%, transparent);
transition: background 0.12s;
}
.cal-splitter:hover .cal-splitter-grip,
.cal-splitter-dragging .cal-splitter-grip {
background: var(--accent, var(--red));
}
.cal-day-detail {
margin-top: 4px;
border-top: 1px solid var(--border);
padding-top: 8px;
height: var(--cal-detail-h, 240px);
overflow-y: auto;
overscroll-behavior: contain;
flex-shrink: 0;
}
.cal-detail-header { display:flex; justify-content:space-between; align-items:center; font-size:12px; font-weight:600; margin-bottom:6px; padding-right:8px; }
.cal-empty { font-size:11px; opacity:0.3; padding:4px 0; }
.cal-event-item { display:flex; gap:8px; padding:6px 8px; border-radius:5px; cursor:pointer; align-items:flex-start; }
.cal-event-item:hover { background:color-mix(in srgb, var(--fg) 6%, transparent); }
.cal-event-dot { width:7px; height:7px; border-radius:50%; flex-shrink:0; margin-top:4px; }
.cal-event-info { flex:1; min-width:0; }
.cal-event-name { font-size:12px; font-weight:500; }
.cal-event-tag {
display: inline-block;
font-size: 10px;
font-weight: 500;
padding: 1px 6px;
border-radius: 8px;
border: 1px solid;
margin-left: 6px;
vertical-align: 1px;
opacity: 0.85;
white-space: nowrap;
}
.cal-event-time { font-size:10px; opacity:0.4; }
.cal-event-loc { font-size:10px; opacity:0.3; }
.cal-event-loc a, .cal-event-time a { color:inherit; text-decoration:underline; text-decoration-color:color-mix(in srgb, currentColor 40%, transparent); }
.cal-event-loc a:hover, .cal-event-time a:hover { opacity:1; text-decoration-color:currentColor; }
/* ── Week view: hour grid ──────────────────────────────────────────
Vertical hour rail on the left, 7 day columns on the right with their
own all-day strip and an absolute-positioned hour grid below it. */
.cal-wk-wrap {
display: flex;
flex-direction: row;
gap: 0;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
/* The wrap itself scrolls so the hour rail and day columns move
together; column headers and the rail spacer are sticky-pinned. */
overflow: auto;
max-height: calc(100vh - 220px);
min-height: 360px;
position: relative;
}
.cal-wk-rail {
flex: 0 0 56px;
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
background: color-mix(in srgb, var(--fg) 2%, var(--bg));
position: sticky;
left: 0;
z-index: 5;
}
.cal-wk-rail-spacer {
/* Lines up the rail's first hour cell with the column's grid origin
(column header + all-day strip). 32 + 24 = 56 px. Doubles as the
zoom-control corner since the toolbar is crowded. */
height: 56px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
position: sticky;
top: 0;
background: color-mix(in srgb, var(--fg) 2%, var(--bg));
z-index: 6;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
padding: 0 4px;
}
.cal-wk-zoom {
width: 22px;
height: 22px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 5px;
font-family: inherit;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
opacity: 0.55;
transition: opacity 0.15s, background 0.15s, border-color 0.15s;
}
.cal-wk-zoom:hover {
opacity: 1;
background: color-mix(in srgb, var(--fg) 8%, var(--bg));
border-color: color-mix(in srgb, var(--accent, var(--fg)) 50%, var(--border));
}
.cal-wk-rail-cell {
font-size: 11px;
color: color-mix(in srgb, var(--fg) 55%, transparent);
padding: 4px 8px 0;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 5%, transparent);
flex-shrink: 0;
display: flex;
align-items: flex-start;
justify-content: flex-end;
font-variant-numeric: tabular-nums;
}
.cal-wk-cols {
flex: 1;
display: grid;
grid-template-columns: repeat(7, 1fr);
position: relative;
}
.cal-wk-col {
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
min-width: 0;
}
.cal-wk-col:first-child { border-left: none; }
.cal-wk-col-head {
height: 32px;
padding: 0 6px;
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
flex-shrink: 0;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
border-bottom: 1px solid color-mix(in srgb, var(--fg) 5%, transparent);
position: sticky;
top: 0;
z-index: 4;
}
.cal-wk-col.cal-wk-today .cal-wk-col-head { background: color-mix(in srgb, var(--accent, var(--red)) 12%, var(--bg)); }
.cal-wk-col.cal-wk-today .cal-wk-dn,
.cal-wk-col.cal-wk-today .cal-wk-dt { color: var(--accent, var(--red)); font-weight: 700; }
/* Sunday: a softer rest-day tint so the week edge reads visually, even
when today is mid-week. Falls back gracefully under the .cal-wk-today
rules above (today on a Sunday still gets the accent treatment). */
.cal-wk-col.cal-wk-sun .cal-wk-col-head { background: color-mix(in srgb, var(--fg) 7%, var(--bg)); }
.cal-wk-col.cal-wk-sun .cal-wk-grid { background: color-mix(in srgb, var(--fg) 2.5%, var(--bg)); }
.cal-wk-col.cal-wk-sun .cal-wk-allday { background: color-mix(in srgb, var(--fg) 3%, var(--bg)); }
.cal-wk-col.cal-wk-sun .cal-wk-dn,
.cal-wk-col.cal-wk-sun .cal-wk-dt { color: color-mix(in srgb, var(--fg) 60%, transparent); }
/* Today on a Sunday still wins the accent treatment. */
.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-col-head { background: color-mix(in srgb, var(--accent, var(--fg)) 12%, var(--bg)); }
.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-dn,
.cal-wk-col.cal-wk-today.cal-wk-sun .cal-wk-dt { color: var(--accent, var(--fg)); }
.cal-wk-dn { font-weight: 600; opacity: 0.6; }
.cal-wk-dt { font-variant-numeric: tabular-nums; opacity: 0.5; }
.cal-wk-allday {
height: 24px;
padding: 2px 4px;
display: flex;
flex-direction: row;
gap: 2px;
align-items: center;
background: color-mix(in srgb, var(--fg) 1.5%, var(--bg));
border-bottom: 1px solid var(--border);
flex-shrink: 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
position: sticky;
top: 32px;
z-index: 3;
}
.cal-wk-allday::-webkit-scrollbar { display: none; }
.cal-wk-allday-event {
font-size: 10px;
font-weight: 500;
padding: 2px 5px;
border-radius: 3px;
color: var(--fg);
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Hour grid body */
.cal-wk-grid {
position: relative;
flex: 0 0 auto;
cursor: cell;
user-select: none;
}
.cal-wk-cell {
border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
box-sizing: border-box;
}
.cal-wk-cell:hover { background: color-mix(in srgb, var(--fg) 3%, transparent); }
/* "Now" line */
.cal-wk-now {
position: absolute;
left: 0; right: 0;
height: 0;
border-top: 2px solid color-mix(in srgb, var(--accent, var(--red, #f87171)) 90%, transparent);
pointer-events: none;
z-index: 4;
}
.cal-wk-now::before {
content: '';
position: absolute;
left: -3px;
top: -4px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent, var(--red, #f87171));
}
/* Event blocks */
.cal-wk-block {
position: absolute;
left: 2px;
right: 2px;
border-left: 3px solid var(--fg);
border-radius: 4px;
padding: 4px 7px;
font-size: 11px;
line-height: 1.25;
cursor: pointer;
overflow: hidden;
z-index: 2;
box-shadow: 0 1px 2px color-mix(in srgb, var(--fg) 8%, transparent);
transition: filter 0.12s, transform 0.12s;
box-sizing: border-box;
}
.cal-wk-block:hover { filter: brightness(1.05); transform: translateY(-0.5px); }
.cal-wk-block-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 11px; }
.cal-wk-block-time { font-size: 10px; opacity: 0.75; font-variant-numeric: tabular-nums; margin-top: 1px; }
@media (max-width: 768px) {
/* Mobile week view: the hour rail already shows the time, so drop it from the
card and let the title wrap to fill the freed space (more readable text). */
.cal-wk-block-time { display: none; }
.cal-wk-block-name {
white-space: normal;
line-height: 1.15;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
}
/* Resize handle: a 6 px hit zone along the block's bottom edge. The
visible 2-px stripe only shows on hover so blocks at rest stay clean. */
.cal-wk-block-resize {
position: absolute;
left: 4px;
right: 4px;
bottom: 0;
height: 6px;
cursor: ns-resize;
z-index: 3;
}
.cal-wk-block:hover .cal-wk-block-resize::after {
content: '';
position: absolute;
left: 30%;
right: 30%;
bottom: 1px;
height: 2px;
border-radius: 2px;
background: color-mix(in srgb, var(--fg) 35%, transparent);
}
.cal-wk-block-resize:hover::after {
background: color-mix(in srgb, var(--fg) 65%, transparent);
}
/* Body of a block while it's being repositioned: lifted shadow + above
siblings so the drop target reads clearly. */
.cal-wk-block-ghost {
z-index: 6 !important;
box-shadow: 0 4px 12px color-mix(in srgb, var(--fg) 24%, transparent) !important;
cursor: grabbing;
}
.cal-wk-block { cursor: grab; }
.cal-wk-block:active { cursor: grabbing; }
/* Drag-to-create ghost */
.cal-wk-ghost {
position: absolute;
left: 2px;
right: 2px;
background: color-mix(in srgb, var(--accent, var(--fg)) 28%, transparent);
border: 1px dashed color-mix(in srgb, var(--accent, var(--fg)) 70%, transparent);
border-radius: 4px;
pointer-events: none;
z-index: 3;
font-size: 10px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
color: var(--accent, var(--fg));
font-variant-numeric: tabular-nums;
}
.cal-year { display:grid; grid-template-columns:repeat(4, 1fr); gap:10px; }
.cal-year-month { background:color-mix(in srgb, var(--fg) 3%, var(--bg)); border:1px solid var(--border); border-radius:6px; padding:6px; cursor:pointer; transition:background 0.15s, border-color 0.15s, transform 0.15s; }
.cal-year-month:hover { background:color-mix(in srgb, var(--fg) 8%, var(--bg)); border-color:var(--accent); transform:translateY(-1px); }
.cal-year-month-title { font-size:11px; font-weight:600; text-align:center; margin-bottom:4px; opacity:0.7; }
.cal-year-month:hover .cal-year-month-title { opacity:1; color:var(--accent); }
.cal-year-grid { display:grid; grid-template-columns:repeat(7, 1fr); gap:1px; }
.cal-year-wd { font-size:7px; text-align:center; opacity:0.3; padding:1px 0; }
.cal-year-cell { font-size:8px; text-align:center; padding:2px 0; min-height:14px; border-radius:2px; transition:transform 0.12s, background 0.12s; }
.cal-year-day { cursor:pointer; opacity:0.5; }
.cal-year-day:hover { background:var(--accent, var(--red)); color:#fff; opacity:1; transform:scale(1.15); font-weight:700; z-index:2; position:relative; }
.cal-year-today { color:var(--accent, var(--red)); font-weight:700; opacity:1; }
.cal-year-has { opacity:1; background:color-mix(in srgb, var(--accent) 15%, transparent); }
.cal-year-has.cal-year-today { background:color-mix(in srgb, var(--accent, var(--red)) 25%, transparent); }
.cal-loading { display:flex; justify-content:center; padding:40px 0; }
.cal-badge { background:var(--accent); border-radius:50%; width:6px; height:6px; margin-left:auto; display:inline-block; flex-shrink:0; }
/* Search input — in-panel variant lives inside the day-detail panel and
spans its width; no width animation since growing/shrinking on every
keystroke would jitter beside the events list. */
.cal-search-input { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); border:1px solid var(--border); border-radius:5px; padding:0 8px; height:24px; color:var(--fg); font-size:11px; font-family:inherit; width:120px; box-sizing:border-box; transition:width 0.15s; }
.cal-search-input:focus { outline:none; border-color:var(--accent); width:180px; }
.cal-search-input.cal-day-search { width:100%; margin-bottom:0; transition:none; padding-left: 28px; }
.cal-search-input.cal-day-search:focus { width:100%; }
/* Wrap for the search input so the magnifying-glass icon can be absolute-
positioned inside the field. */
.cal-search-wrap {
position: relative;
display: block;
margin-bottom: 6px;
}
.cal-search-wrap .cal-search-icon {
position: absolute;
left: 8px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
opacity: 0.45;
color: var(--fg);
}
.cal-day-search-meta { font-size:10px; opacity:0.5; margin:0 2px 6px; }
.cal-search-results { display:flex; flex-direction:column; gap:4px; }
.cal-search-count { font-size:10px; opacity:0.5; padding:4px 2px 8px; }
/* Agenda view */
.cal-agenda { display:flex; flex-direction:column; gap:10px; flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; }
.cal-agenda-day { display:flex; flex-direction:column; gap:2px; }
.cal-agenda-date { font-size:11px; font-weight:600; opacity:0.6; padding:4px 2px 2px; border-bottom:1px solid var(--border); }
.cal-agenda-day.is-today .cal-agenda-date { opacity: 1; color: var(--accent, var(--red)); border-bottom-color: color-mix(in srgb, var(--accent, var(--red)) 35%, var(--border)); }
.cal-agenda-today-badge {
display: inline-block;
margin-left: 6px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 1px 6px;
border-radius: 8px;
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
color: var(--accent, var(--red));
vertical-align: 1px;
}
.cal-agenda-empty { font-size:11px; opacity:0.4; padding:8px; font-style:italic; }
.cal-agenda-event { display:flex; gap:8px; padding:8px; border-radius:5px; cursor:pointer; align-items:center; position:relative; }
.cal-agenda-event:hover { background:color-mix(in srgb, var(--fg) 6%, transparent); }
.cal-agenda-event .cal-event-more,
.cal-event-item .cal-event-more { opacity:0; transition:opacity 0.15s; }
.cal-agenda-event:hover .cal-event-more,
.cal-event-item:hover .cal-event-more { opacity:0.6; }
.cal-event-item { position:relative; }
/* ── Personal Assistant ── */
.sidebar-assistant-entry { gap: 6px; min-height: 29px; box-sizing: border-box; align-items: center; }
.sidebar-assistant-entry .sidebar-action-icon { position: relative; left: -2px; }
#sidebar-assistant-gear:hover { opacity: 0.8 !important; }
.assistant-settings-form { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 2px; }
.assistant-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: color-mix(in srgb, var(--fg) 70%, transparent); }
.assistant-field > span { font-size: 11px; opacity: 0.6; }
.assistant-field input[type="text"],
.assistant-field input[type="time"],
.assistant-field select,
.assistant-field textarea {
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
border: 1px solid var(--border);
border-radius: 5px;
padding: 7px 10px;
color: var(--fg);
font-size: 12px;
font-family: inherit;
box-sizing: border-box;
}
.assistant-field textarea { resize: vertical; min-height: 60px; }
.assistant-field input:focus,
.assistant-field select:focus,
.assistant-field textarea:focus { outline: none; border-color: var(--accent, var(--red)); }
.assistant-field-row { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.assistant-field-row > .assistant-field { flex: 1 1 180px; }
.assistant-field-check { flex-direction: row !important; align-items: center; gap: 6px; padding-bottom: 7px; }
.assistant-checkins { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; padding-top: 8px; border-top: 1px solid var(--border); }
.assistant-checkins h5 { margin: 0 0 2px; font-size: 12px; font-weight: 600; opacity: 0.8; }
.assistant-checkin-row {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
}
.assistant-checkin-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.assistant-checkin-head input[type="text"],
.assistant-checkin-head input[type="time"] {
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
border: 1px solid var(--border);
border-radius: 4px;
padding: 4px 8px;
color: var(--fg);
font-size: 12px;
font-family: inherit;
}
.assistant-checkin-head input[type="text"] { flex: 1; min-width: 120px; }
.assistant-checkin-head input[type="time"] { width: 90px; }
.assistant-checkin-run {
background: none;
border: 1px solid var(--border);
color: var(--fg);
font-family: inherit;
font-size: 11px;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.15s;
}
.assistant-checkin-run:hover { opacity: 1; }
.assistant-checkin-prompt {
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 8px;
color: var(--fg);
font-size: 12px;
font-family: inherit;
resize: vertical;
min-height: 48px;
}
.assistant-checkin-prompt:focus { outline: none; border-color: var(--accent, var(--red)); }
.assistant-checkin-meta { font-size: 10px; opacity: 0.45; }
.assistant-settings-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.assistant-tools-grid {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 240px;
overflow-y: auto;
padding: 6px;
border: 1px solid var(--border);
border-radius: 6px;
margin-top: 4px;
}
.assistant-tool-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.assistant-tool-group-label {
font-size: 10px;
font-weight: 600;
opacity: 0.5;
text-transform: uppercase;
letter-spacing: 0.03em;
width: 100%;
margin-bottom: 1px;
}
.assistant-tool-item {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
padding: 2px 6px;
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
transition: background 0.1s, border-color 0.1s;
user-select: none;
}
.assistant-tool-item:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
}
.assistant-tool-item:has(input:checked) {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.assistant-tool-item input[type="checkbox"] {
width: 12px;
height: 12px;
margin: 0;
}
button.cal-event-more {
background:transparent;
border:none;
color:var(--fg);
width:20px; height:20px;
cursor:pointer;
padding:0;
display:inline-flex;
align-items:center;
justify-content:center;
font-family:inherit;
flex-shrink:0;
margin-left:auto;
align-self:center;
transform:translateY(-4px);
transition:opacity 0.15s;
}
button.cal-event-more:hover { opacity:1 !important; }
/* Empty state */
.cal-empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:60px 20px; text-align:center; gap:10px; }
.cal-empty-title { font-size:14px; font-weight:600; opacity:0.8; }
.cal-empty-msg { font-size:12px; opacity:0.5; max-width:320px; line-height:1.5; margin-bottom:8px; }
.cal-allday-switch { margin:0; flex-shrink:0; }
/* Stack the "All day" label above its toggle so it doesn't get squeezed /
truncated next to the date inputs in the row. */
.cal-allday-ctrl { display:flex; flex-direction:column; align-items:center; gap:3px; flex-shrink:0; }
.cal-allday-label { font-size:10px; opacity:0.5; white-space:nowrap; line-height:1; }
.cal-form { display:flex; flex-direction:column; gap:8px; }
.cal-form-title { font-size:13px; font-weight:600; margin-bottom:2px; }
.cal-title-wrap { position: relative; }
/* Calendar-picker select sits 2px higher; its border-left/background tint is
set in JS to the chosen calendar's colour. */
.cal-f-cal-select { position: relative; top: -4px; }
/* Day-detail "+ New" button nudged down 2px. */
#cal-add-day { position: relative; top: 2px; }
/* Accent the native date/time picker glyphs. We recolor the webkit indicator
by masking an accent fill through a calendar/clock SVG (same technique as the
mono icon picker). accent-color covers the spinner parts where supported. */
.cal-form-bespoke input[type="date"],
.cal-form-bespoke input[type="time"],
.cal-form-bespoke input[type="datetime-local"] { accent-color: var(--accent, var(--red)); }
.cal-form-bespoke input[type="date"]::-webkit-calendar-picker-indicator,
.cal-form-bespoke input[type="time"]::-webkit-calendar-picker-indicator,
.cal-form-bespoke input[type="datetime-local"]::-webkit-calendar-picker-indicator {
background-color: var(--accent, var(--red));
cursor: pointer;
width: 14px; height: 14px;
}
.cal-form-bespoke input[type="date"]::-webkit-calendar-picker-indicator,
.cal-form-bespoke input[type="datetime-local"]::-webkit-calendar-picker-indicator {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E") center/contain no-repeat;
}
.cal-form-bespoke input[type="time"]::-webkit-calendar-picker-indicator {
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpolyline points='12 6 12 12 16 14'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpolyline points='12 6 12 12 16 14'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* ── Bespoke event form ────────────────────────────────────────────
Big clock-face date/time hero with the title under it. Details panel
stays collapsed until the title gets focus or "Add details" is clicked.
*/
.cal-form-bespoke {
position: relative;
width: min(720px, calc(100% - 24px));
max-width: none;
margin: 12px auto;
padding: 18px 28px 14px;
box-sizing: border-box;
background: var(--bg);
/* Optional per-event tint set by JS via --ev-color. When unset, falls
back to the theme's neutral border so the card looks like normal. */
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 8px 24px color-mix(in srgb, var(--fg) 6%, transparent);
transition: border-color 0.18s, box-shadow 0.18s;
}
/* When a colour is picked, lift the card with a soft tinted border + glow
so the choice is unmistakable. */
.cal-form-bespoke[style*="--ev-color"] {
border-color: color-mix(in srgb, var(--ev-color) 55%, var(--border));
box-shadow: 0 8px 28px color-mix(in srgb, var(--ev-color) 22%, transparent);
}
/* Title underline + clock focus ring + primary button track --ev-color
when set; default to --accent / --fg otherwise. */
.cal-form-bespoke .cal-input.cal-hero-title:focus { border-bottom-color: var(--ev-color, var(--accent, color-mix(in srgb, var(--fg) 60%, transparent))); }
.cal-form-bespoke[style*="--ev-color"] .cal-input.cal-hero-title { border-bottom-color: color-mix(in srgb, var(--ev-color) 35%, var(--border)); }
.cal-form-bespoke .cal-hero-time:focus-visible,
.cal-form-bespoke .cal-hero-date:focus-visible { outline-color: var(--ev-color, var(--accent, color-mix(in srgb, var(--fg) 50%, transparent))); }
.cal-form-bespoke[style*="--ev-color"] .cal-btn-primary {
background: var(--ev-color);
border-color: var(--ev-color);
}
/* Custom-BG events paint the form card with the uploaded image. Make sure
the action row + buttons stay clearly readable on top of it. */
.cal-form-bespoke.cal-form-bg-image .cal-form-actions {
position: relative;
z-index: 2;
/* Solid backing strip so the Save/Cancel/Delete buttons don't fade into
the photo behind them. */
background: color-mix(in srgb, var(--panel) 92%, transparent);
margin: 8px -14px -14px;
padding: 10px 14px;
border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
border-radius: 0 0 8px 8px;
}
.cal-form-bespoke.cal-form-bg-image .cal-btn-primary {
background: var(--accent, var(--red));
border-color: var(--accent, var(--red));
color: var(--bg);
}
.cal-form-close {
position: absolute; top: 10px; right: 12px;
background: none; border: none; color: var(--fg);
opacity: 0.35; cursor: pointer; font-size: 20px; line-height: 1;
padding: 4px 8px; border-radius: 6px;
}
.cal-form-close:hover { opacity: 0.9; background: color-mix(in srgb, var(--fg) 8%, transparent); }
.cal-form-mobile-cancel { display: none; }
/* "Today is …" header pinned at the top of the form. Keeps you oriented
even when the event date you're picking is way off in the future. */
.cal-form-today {
text-align: center;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.45;
margin-bottom: 0;
}
.cal-form-today span { letter-spacing: 0.04em; text-transform: none; opacity: 0.85; font-weight: 500; }
/* Hero — clock face + date label. Spacing between the two lives entirely
on the clock's bottom margin so it's predictable. */
.cal-hero {
display: flex; flex-direction: column; align-items: center;
gap: 0; margin-bottom: 12px;
}
.cal-hero-time {
display: inline-flex; align-items: baseline; gap: 8px;
font-family: ui-monospace, SFMono-Regular, "Menlo", "Consolas", monospace;
font-variant-numeric: tabular-nums;
font-size: 56px;
letter-spacing: 0.02em;
font-weight: 200;
color: var(--fg);
line-height: 1;
/* Behaves like a button — strip native chrome but keep it tappable. */
background: none;
border: none;
/* Pill that reads as a real hit target for a 56-px clock face.
Top padding kept tight so the clock sits close to the "Today is" line. */
padding: 6px 38px 18px;
margin: 0 0 24px;
border-radius: 18px;
cursor: pointer;
line-height: 1;
transition: background 0.15s, color 0.15s;
}
.cal-hero-time .cal-hero-clock,
.cal-hero-time .cal-hero-ampm { line-height: 1; }
/* The clock is a `` and a global `button:hover` rule paints
every button's background on hover. Suppress it explicitly. */
.cal-hero-time:hover { background: transparent; border-color: transparent; }
.cal-hero-time:focus-visible { outline: 2px solid var(--accent, color-mix(in srgb, var(--fg) 50%, transparent)); outline-offset: 2px; }
.cal-hero-clock { font-feature-settings: "tnum"; display: inline-flex; align-items: baseline; }
/* Per-segment hover so it's clear hh vs mm are individually clickable. */
.cal-hero-clock-hh, .cal-hero-clock-mm {
display: inline-block;
border-radius: 6px;
padding: 0 2px;
transition: background 0.12s;
}
.cal-hero-clock-hh:hover, .cal-hero-clock-mm:hover {
background: color-mix(in srgb, var(--fg) 9%, transparent);
}
.cal-hero-sep { padding: 0 2px; opacity: 0.55; pointer-events: none; }
.cal-hero-ampm {
font-size: 14px;
letter-spacing: 0.12em;
opacity: 0.55;
font-weight: 500;
text-transform: uppercase;
}
.cal-hero-date {
font-size: 16px;
opacity: 0.6;
letter-spacing: 0.04em;
background: none;
border: none;
color: inherit;
font-family: inherit;
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
margin: 7px 0 0; /* extra nudge below the clock */
transition: background 0.15s, opacity 0.15s;
}
.cal-hero-date:hover { background: color-mix(in srgb, var(--fg) 6%, transparent); opacity: 0.85; }
.cal-hero-date:focus-visible { outline: 2px solid var(--accent, color-mix(in srgb, var(--fg) 50%, transparent)); outline-offset: 2px; }
/* Title input — flat, large, no chrome until focused */
.cal-input.cal-hero-title {
font-size: 18px;
text-align: left;
padding: 10px 12px 10px 0;
background: transparent;
border: 1px solid transparent;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
border-radius: 0;
margin-bottom: 4px;
}
.cal-input.cal-hero-title:focus {
border-color: transparent;
border-bottom-color: var(--accent, color-mix(in srgb, var(--fg) 60%, transparent));
}
/* Details panel — animated reveal via max-height + opacity. */
.cal-form-bespoke .cal-form-details {
display: block;
max-height: 0;
opacity: 0;
overflow: hidden;
transition: max-height 240ms ease, opacity 180ms ease, margin 220ms ease;
margin-top: 0;
}
.cal-form-bespoke.is-expanded .cal-form-details {
max-height: 720px;
opacity: 1;
margin-top: 4px;
}
/* The detail children themselves keep the existing flex/gap rules from
the regular .cal-form, so we don't restyle inputs/rows here. */
.cal-form-bespoke .cal-form-details > * + * { margin-top: 8px; }
.cal-form-bespoke .cal-form-actions { margin-top: 8px; justify-content: flex-end; }
.cal-form-bespoke .cal-form-row { margin-bottom: 8px; }
/* Location row: input + Apple Maps pin link. Pin only lights up when the
field has content; clicking opens maps.apple.com (native Maps app on
Apple devices, web fallback elsewhere). All theme vars. */
.cal-loc-row {
display: flex;
align-items: stretch;
gap: 6px;
}
.cal-loc-row > input { flex: 1; min-width: 0; }
.cal-loc-map {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
flex-shrink: 0;
border: 1px solid var(--border);
border-radius: 5px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
text-decoration: none;
transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}
.cal-loc-map:hover { background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg)); border-color: var(--accent, var(--red)); }
.cal-loc-map.is-disabled { opacity: 0.3; pointer-events: none; cursor: default; }
@media (max-width: 520px) {
.cal-hero-time { font-size: 44px; }
.cal-form-bespoke { margin: 16px 12px; padding: 32px 16px 14px; }
.cal-form-mobile-cancel {
display: inline-flex;
position: absolute;
top: 8px;
right: 8px;
width: 30px;
height: 30px;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--bg) 82%, transparent);
color: var(--fg);
opacity: 0.72;
cursor: pointer;
z-index: 3;
-webkit-tap-highlight-color: transparent;
}
.cal-form-mobile-cancel:active {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 14%, var(--bg));
border-color: color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
}
}
.cal-input { background:color-mix(in srgb, var(--fg) 5%, var(--bg)); border:1px solid var(--border); border-radius:5px; padding:7px 10px; color:var(--fg); font-size:12px; width:100%; box-sizing:border-box; }
.cal-input:focus { outline:none; border-color:var(--accent); }
.cal-input-time { width:auto; flex:1; }
select.cal-input { appearance:auto; }
textarea.cal-input { resize:vertical; font-family:inherit; }
.cal-form-row { display:flex; gap:8px; align-items:center; }
.cal-form-actions { display:flex; gap:8px; justify-content:flex-end; margin-top:4px; }
button.cal-btn { background:color-mix(in srgb, var(--fg) 8%, transparent); border:1px solid var(--border); color:var(--fg); border-radius:5px; padding:0 14px; height:28px; font-size:11px; font-weight:500; cursor:pointer; font-family:inherit; display:inline-flex; align-items:center; justify-content:center; box-sizing:border-box; transition:background 0.15s, border-color 0.15s; }
button.cal-btn:hover { background:color-mix(in srgb, var(--fg) 14%, transparent); }
button.cal-btn.cal-btn-primary { background:var(--accent, var(--red)); color:var(--bg); border-color:var(--accent, var(--red)); }
button.cal-btn.cal-btn-primary:hover { opacity:0.9; background:var(--accent, var(--red)); }
button.cal-btn.cal-btn-danger { color:var(--accent, var(--red)); border-color:var(--accent, var(--red)); background:transparent; }
button.cal-btn.cal-btn-danger:hover { background:var(--accent, var(--red)); color:var(--bg); }
@media (max-width: 600px) {
.cal-modal-container { max-width:100vw; width:100vw; border-radius:0; max-height:100vh; height:100vh; }
.cal-day { min-height:44px; padding:2px; }
.cal-day-num { font-size:9px; }
.cal-event-preview { display:none; }
.cal-multiday { font-size:7px; padding:0 2px; }
/* Two-row toolbar on mobile:
Row 1 — Title (full width)
Row 2 — [← Today →] [view toggle] [settings/sync/filters/+New]
Title gets its own row so it isn't squeezed to nothing when the
right-side group is wide. The .cal-toolbar-nav (which now only
contains the title since JS moved the date-nav to the right) is
width:100% so it forces a wrap before .cal-toolbar-right. */
.cal-toolbar {
gap: 4px;
flex-wrap: wrap;
row-gap: 4px;
}
.cal-toolbar-nav {
flex-wrap: nowrap;
flex: 1 1 100%;
min-width: 0;
justify-content: flex-start;
}
.cal-toolbar-right {
margin-top: 8px;
margin-left: 0;
flex-wrap: nowrap;
flex-shrink: 1;
overflow-x: auto;
scrollbar-width: none;
width: 100%;
}
.cal-toolbar-right::-webkit-scrollbar { display: none; }
.cal-title {
font-size: 13px;
padding: 0 4px;
}
.cal-nav { padding: 2px 5px; font-size: 10px; }
.cal-view-btn { padding: 2px 5px; font-size: 9px; }
.cal-filters { gap: 3px; margin-top: 4px; }
.cal-filter-item { font-size: 9px; padding: 1px 5px; }
.cal-year { grid-template-columns:repeat(3, 1fr); gap:6px; }
.cal-year-cell { font-size:7px; }
}
/* ═══ Research Panel ═══ */
body.research-panel-view #research-divider { display:none; }
/* .research-overlay inherits from .modal — no extra layout needed */
.research-pane {
display:flex; flex-direction:column;
padding:10px; box-sizing:border-box;
font-size:12px; letter-spacing:-0.015em;
position: relative;
isolation: isolate;
overflow: hidden;
}
/* Synapse signal traveling around the outer edge of the pane. The pane keeps a
flat background; only this thin border ring remains animated. */
.research-pane::after {
content: '';
position: absolute; inset: 0;
z-index: 1;
pointer-events: none;
border-radius: inherit;
padding: 2px;
background: conic-gradient(
from var(--research-orbit-angle, 0deg),
transparent 0deg,
transparent 300deg,
color-mix(in srgb, var(--accent, var(--red)) 70%, transparent) 335deg,
var(--accent, var(--red)) 350deg,
color-mix(in srgb, var(--accent, var(--red)) 70%, transparent) 358deg,
transparent 360deg
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask-composite: exclude;
}
@media (prefers-reduced-motion: reduce) {
.research-pane::after { animation: none; opacity: 0.4; }
}
.research-pane-header {
display:flex; justify-content:space-between; align-items:center;
margin-bottom:6px; cursor:grab; user-select:none;
}
.research-pane-header:active { cursor:grabbing; }
.research-pane-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.03em;
color: var(--red);
}
/* Match Library's inline SVG alignment inside the h4 */
.research-pane-header h4 svg {
vertical-align: -2px;
margin-right: 6px;
}
.research-pane-header-actions {
display: flex; align-items: center; gap: 2px;
margin-left: auto;
}
.research-pane-body {
flex: 1; min-height: 0; overflow: hidden;
display: flex; flex-direction: column;
/* Flat surface — matches Library's .modal-body (no inset sub-window).
The outer .research-pane already provides the 10px padding + border. */
padding: 0;
margin: 0;
background: transparent;
border: 0;
border-radius: 0;
font-size: 12px;
color: var(--fg);
}
/* Hide the in-body "Research" subtitle on mobile to save vertical space —
the toolbar/header carries enough context already. */
/* Match the .admin-card h2 styling used in Documents/Library so the
"Research" sub-title reads the same as those modals' titles. */
.research-new-job h2 {
font-size: 14px;
font-weight: 600;
letter-spacing: -0.03em;
}
@media (max-width: 600px) {
/* Keep the "Research" title visible on mobile (matches the Cookbook tab
titles, which show on mobile). It used to be hidden here. */
.research-new-job h2 {
font-size: 14px;
}
}
/* Match the .admin-card surface used in Cookbook (Download / Serve sections):
var(--panel) bg with var(--border) and rounded corners. */
.research-new-job {
/* Match the Cookbook download block (.admin-card) exactly. */
padding: 12px;
margin-bottom: 10px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
flex-shrink: 0;
}
.research-query {
width:100%; resize:vertical; min-height:80px; max-height:240px;
/* Match the rest of the app's inputs (var(--bg) page tone) rather than
the darker panel tone — keeps the input readable on the dark sub-window. */
background: var(--bg); color: var(--fg);
border:1px solid var(--border); border-radius:6px;
padding:8px 10px; font-size:12px; font-family:inherit;
box-sizing:border-box;
margin-top: 6px;
}
.research-query:focus { outline:none; border-color:var(--accent-primary, var(--red)); }
.research-settings-row {
display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap;
padding: 8px 10px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
border: 1px solid var(--border);
border-radius: 6px;
}
.research-setting {
display:flex; flex-direction:column; flex:1; min-width:90px;
}
.research-setting-label {
font-size:9px; text-transform:uppercase; letter-spacing:0.5px;
opacity:0.5; margin-bottom:2px;
}
.research-setting select {
font-size: 11px; padding: 4px 6px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border); border-radius: 4px;
}
.research-controls-row {
display:flex; align-items:center; gap:10px; margin-top:10px;
}
.research-start-btn {
margin-left:auto;
display:flex; align-items:center; gap:5px;
padding:6px 16px; border:none; border-radius:6px;
background:var(--accent-primary, var(--red)); color:#fff;
font-size:12px; font-weight:600; cursor:pointer;
transition:opacity 0.15s;
}
.research-start-btn:hover { opacity:0.85; }
.research-start-btn:disabled, .research-start-btn.research-start-busy {
opacity:0.7; cursor:wait;
}
.research-start-spinner {
display:inline-block; width:11px; height:11px; vertical-align:-1px; margin-right:4px;
border:2px solid currentColor; border-right-color:transparent; border-radius:50%;
animation: spin 0.7s linear infinite;
}
.research-category-row {
display:flex; gap:4px; margin-top:8px; flex-wrap:wrap;
}
/* Mobile: keep the category chips on ONE row (scroll horizontally) instead of
wrapping to two rows. */
@media (max-width: 600px) {
.research-category-row {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.research-category-row::-webkit-scrollbar { display: none; }
.research-cat { flex-shrink: 0; white-space: nowrap; position: relative; top: -8px;
}
}
/* Match .doclib-chip exactly so categories feel like document filter tags. */
.research-cat {
padding: 2px 10px;
border-radius: 12px;
font-size: 10px;
border: 1px solid var(--border);
background: transparent;
color: var(--fg-muted);
cursor: pointer;
user-select: none;
transition: background 0.15s, border-color 0.15s, color 0.15s;
position: relative;
top: -4px;
}
.research-cat:hover { border-color: var(--red); }
.research-cat.active {
background: color-mix(in srgb, var(--red) 15%, transparent);
border-color: color-mix(in srgb, var(--red) 40%, transparent);
color: var(--red);
}
/* Match the Cookbook "Trending models" toggle (a left-aligned memory-toolbar-btn
with an arrow + label) instead of the old tiny-uppercase borderless text. */
.research-settings-toggle {
display:flex; align-items:center; gap:6px;
width:100%; text-align:left;
height:26px; padding:0 8px; margin-top:23px;
background:none; border:1px solid var(--border); border-radius:4px;
color:color-mix(in srgb, var(--fg) 60%, transparent);
font-size:11px; font-family:inherit; cursor:pointer;
transition:all 0.15s;
}
.research-settings-toggle:hover { color:var(--fg); border-color:var(--fg); }
.research-settings-chevron { display:inline-flex; transition:transform 0.2s; margin-left:auto; }
.research-settings-toggle.collapsed .research-settings-chevron { transform:rotate(-90deg); }
.research-add-btn {
padding:6px 14px; border:1px solid var(--border); border-radius:6px;
background:transparent; color:var(--fg); font-size:12px; cursor:pointer;
transition:background 0.15s;
}
.research-add-btn:hover { background:color-mix(in srgb, var(--border) 30%, transparent); }
/* Popover anchored to the Start-All button — pick parallel vs sequential
without the heavy "Run N jobs" modal. Drops down by default, flips to
drop-up when there's no room below (the .rrm-up modifier is added by
the JS that positions it). */
.research-run-mode-popover {
position: fixed;
z-index: 12000;
min-width: 200px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
padding: 5px;
font-size: 13px;
animation: rrm-pop-in 0.12s ease-out both;
transform-origin: top right;
}
.research-run-mode-popover.rrm-up { transform-origin: bottom right; }
@keyframes rrm-pop-in {
from { opacity: 0; transform: scale(0.96) translateY(-2px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.research-run-mode-popover .research-run-mode-row {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
text-align: left;
background: none;
border: none;
color: var(--fg);
font-family: inherit;
padding: 10px 13px;
border-radius: 6px;
cursor: pointer;
}
.research-run-mode-popover .research-run-mode-row svg { flex-shrink: 0; opacity: 0.8; }
.research-run-mode-popover .research-run-mode-row:hover {
background: color-mix(in srgb, var(--accent-primary, var(--red)) 14%, transparent);
}
.research-run-mode-popover .rrm-title {
font-weight: 600;
font-size: 13px;
color: var(--fg);
}
.research-jobs-list {
flex:1; min-height:0; overflow-y:auto; padding:6px 0;
display: flex; flex-direction: column; gap: 6px;
}
.research-empty {
text-align:center; padding:30px 14px;
font-size:12px; opacity:0.4;
}
/* Cards match Library's .doclib-card: subtle fg 3% tint over var(--bg). */
.research-job-card {
margin: 0;
padding: 8px 10px;
background: color-mix(in srgb, var(--fg) 3%, transparent);
border: 1px solid var(--border);
border-radius: 8px;
transition: background 0.15s, border-color 0.15s;
}
.research-job-card:hover {
background: color-mix(in srgb, var(--fg) 5%, transparent);
border-color: color-mix(in srgb, var(--fg) 16%, transparent);
}
.research-job-card.running { border-left:3px solid var(--accent-primary, var(--red)); }
.research-job-card.queued { border-left:3px solid var(--fg-dim, #888); }
/* No colored left-accent bar on done/past research — it read as a generic
"AI default" card. The category/standard badge carries the meaning instead. */
.research-job-card.done { border-left:1px solid var(--border); cursor:pointer; }
/* Past (library) research used to be dimmed to 0.65 which read as "greyed out".
They now look the same as the rest — folding them under "Past research"
handles the de-clutter instead. */
.research-job-card.done.from-library { opacity:1; }
.research-job-card.error,
.research-job-card.cancelled { border-left:3px solid #f44336; }
/* Per-category theming — picks up a category accent, badge, and soft tint */
.research-job-card[data-category] { --cat-color: var(--accent, var(--red)); }
.research-job-card[data-category="product"] { --cat-color: #5b8abf; }
.research-job-card[data-category="comparison"] { --cat-color: #e5a33a; }
.research-job-card[data-category="howto"] { --cat-color: #82c882; }
.research-job-card[data-category="landscape"] { --cat-color: #a07ae0; }
.research-job-card[data-category="factcheck"] { --cat-color: var(--red); }
.research-job-card.done[data-category] {
background: color-mix(in srgb, var(--cat-color) 4%, transparent);
}
.research-job-card.done[data-category]:hover {
background: color-mix(in srgb, var(--cat-color) 7%, transparent);
}
/* Inline category label — sits beside the title in low-opacity colored
text instead of a separate pill. Picks up the per-card --cat-color. */
/* Uncategorized (default-format) research = "standard", shown in the same
green as its left border so the green reads as a labelled category. */
.research-cat-badge.research-cat-standard { color: var(--color-success); opacity: 0.75; }
.research-cat-badge {
font-size: 10px;
font-weight: 500;
text-transform: lowercase;
letter-spacing: 0;
padding: 0;
background: transparent;
border: 0;
border-radius: 0;
color: var(--cat-color, var(--accent));
opacity: 0.55;
flex-shrink: 0;
margin-left: 2px;
position: relative;
top: -1px;
}
.research-job-report-body h1, .research-job-report-body h2, .research-job-report-body h3 {
color: var(--cat-color, var(--accent, var(--fg)));
}
/* Category hero banner */
.research-hero {
display: flex; align-items: center; gap: 14px;
padding: 16px 18px; margin: 8px 0 14px;
border-radius: 10px;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--cat-color, var(--accent)) 18%, transparent) 0%,
color-mix(in srgb, var(--cat-color, var(--accent)) 5%, transparent) 100%
);
border-left: 4px solid var(--cat-color, var(--accent));
position: relative; overflow: hidden;
}
.research-hero::after {
content: ''; position: absolute; right: -40px; top: -40px;
width: 160px; height: 160px; border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--cat-color, var(--accent)) 15%, transparent) 0%, transparent 60%);
pointer-events: none;
}
.research-hero-icon {
flex-shrink: 0; width: 32px; height: 32px;
color: var(--cat-color, var(--accent));
display: flex; align-items: center; justify-content: center;
filter: drop-shadow(0 2px 4px color-mix(in srgb, var(--cat-color, var(--accent)) 40%, transparent));
}
.research-hero-icon svg { width: 100%; height: 100%; }
.research-hero-text { flex: 1; min-width: 0; }
.research-hero-label {
font-size: 10px; text-transform: uppercase; letter-spacing: 1.2px;
font-weight: 700; opacity: 0.7; margin-bottom: 3px;
color: var(--cat-color, var(--accent));
}
.research-hero-query {
font-size: 15px; font-weight: 600;
line-height: 1.3; color: var(--fg);
}
/* Product: callout-style bullets */
.research-body-product ul { list-style: none; padding-left: 0; }
.research-body-product ul li {
padding: 6px 10px 6px 28px; margin: 4px 0;
border-left: 2px solid color-mix(in srgb, #5b8abf 40%, transparent);
background: color-mix(in srgb, #5b8abf 4%, transparent);
border-radius: 0 4px 4px 0; position: relative;
}
.research-body-product ul li::before {
content: '▸'; position: absolute; left: 10px;
color: #5b8abf; font-weight: bold;
}
/* Comparison: styled table */
.research-body-comparison table {
width: 100%; border-collapse: collapse; margin: 12px 0;
border: 1px solid color-mix(in srgb, #e5a33a 25%, transparent);
border-radius: 6px; overflow: hidden;
}
.research-body-comparison th {
background: color-mix(in srgb, #e5a33a 18%, transparent);
color: #e5a33a; font-weight: 700;
padding: 8px 12px; text-align: left;
border-bottom: 2px solid color-mix(in srgb, #e5a33a 40%, transparent);
}
.research-body-comparison td {
padding: 8px 12px;
border-bottom: 1px solid color-mix(in srgb, #e5a33a 12%, transparent);
}
.research-body-comparison tr:nth-child(even) td {
background: color-mix(in srgb, #e5a33a 3%, transparent);
}
/* How-to: big numbered steps */
.research-body-howto ol { counter-reset: howto-step; list-style: none; padding-left: 0; }
.research-body-howto ol > li {
counter-increment: howto-step; position: relative;
padding: 10px 12px 10px 52px; margin: 8px 0;
background: color-mix(in srgb, #82c882 5%, transparent);
border-radius: 8px; border-left: 2px solid #82c882;
}
.research-body-howto ol > li::before {
content: counter(howto-step);
position: absolute; left: 10px; top: 14px;
width: 30px; height: 30px; border-radius: 50%;
background: #82c882; color: white;
font-weight: 700; font-size: 13px;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 6px color-mix(in srgb, #82c882 40%, transparent);
}
/* Landscape: section banners */
.research-body-landscape h3 {
padding: 8px 14px;
background: linear-gradient(90deg, color-mix(in srgb, #a07ae0 15%, transparent), transparent);
border-left: 3px solid #a07ae0;
border-radius: 0 6px 6px 0;
margin: 14px 0 8px;
}
/* Fact-check: verdict emphasis */
.research-body-factcheck blockquote {
border-left: 3px solid var(--red);
background: color-mix(in srgb, var(--red) 6%, transparent);
padding: 10px 14px; margin: 10px 0;
border-radius: 0 6px 6px 0;
}
.research-body-factcheck strong {
color: var(--red);
padding: 1px 6px; border-radius: 4px;
background: color-mix(in srgb, var(--red) 12%, transparent);
}
.research-job-header {
display:flex; align-items:center; gap:8px;
}
/* Active (running) card: nudge the title + text assets up 4px so they line up
with the chevron / Clear button on the right. */
.research-job-card.running .research-job-query,
.research-job-card.running .research-cat-badge,
.research-job-card.running .research-job-model,
.research-job-card.running .research-job-time {
position: relative;
top: -4px;
}
.research-job-query {
flex:1; font-size:11px; font-weight:600;
letter-spacing: -0.01em;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
color: var(--fg);
}
.research-job-time,
.research-job-meta {
font-size:10px; opacity:0.5; white-space:nowrap; font-family:monospace;
}
.research-job-status {
font-size:10px; text-transform:uppercase; opacity:0.6;
}
.research-job-cancel,
.research-job-remove,
.research-job-report-link {
background:transparent; border:none; color:var(--fg);
opacity:0.4; cursor:pointer; padding:2px; display:flex;
transition:opacity 0.15s;
}
.research-job-cancel:hover,
.research-job-remove:hover,
.research-job-report-link:hover { opacity:1; }
/* Minimize toggle for the per-job synapse "tree" visual. */
.research-synapse-toggle {
background:transparent; border:none; color:var(--fg);
opacity:0.4; cursor:pointer; padding:2px; display:flex;
transition:opacity 0.15s;
}
.research-synapse-toggle:hover,
.research-synapse-toggle.active { opacity:1; }
.research-job-synapse-host.synapse-collapsed { display:none; }
.research-job-model {
font-size:9px; opacity:0.4; white-space:nowrap;
max-width:120px; overflow:hidden; text-overflow:ellipsis;
}
.research-job-actions {
display:flex; gap:4px; margin-top:6px;
}
/* Push the first dim (dismiss/delete) button — and everything after it — to
the right edge of the actions row. `:first-of-type` would match the very
first in the row regardless of class; this immediate-sibling
combinator targets only a dim button that follows a non-dim one. */
.research-job-actions .research-job-action:not(.research-job-action-dim)
+ .research-job-action-dim { margin-left: auto; }
/* Edge case: all buttons in the row are dim — push first dim right. */
.research-job-actions > .research-job-action-dim:first-child { margin-left: auto; }
/* Match .doclib-toolbar-btn — same font, padding, hover */
.research-job-action {
display: inline-flex; align-items: center; gap: 4px;
padding: 5px 10px;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg-muted);
font-size: 11px;
font-family: inherit;
white-space: nowrap;
cursor: pointer;
transition: all 0.15s;
}
.research-job-action:hover {
color: var(--fg);
border-color: var(--fg);
}
.research-job-action-dim { opacity: 0.5; border-color: transparent; }
.research-job-action-dim:hover { opacity: 1; }
.research-job-action-copied {
color: var(--color-success) !important;
border-color: color-mix(in srgb, var(--color-success) 45%, var(--border)) !important;
opacity: 1 !important;
}
.research-job-phase {
font-size:11px; opacity:0.6; margin-top:4px;
}
.research-job-queued-meta {
font-size:10px; opacity:0.4; margin-top:2px;
}
.research-section-divider {
display:flex; align-items:center; gap:10px;
padding:6px 14px; font-size:10px; opacity:0.4;
text-transform:uppercase; letter-spacing:0.5px;
}
.research-section-divider::before,
.research-section-divider::after {
content:''; flex:1; height:1px;
background:var(--border);
}
/* Foldable job sections (Active / Past research) */
/* Collapsible section styled like the Cookbook download sub-blocks (.cookbook-card):
clean var(--bg) card, 8px radius, a real 13px/600 title. Chevron on the LEFT,
then title, count, and a status-color dot on the right (the dot stays visible
when folded so you get status at a glance). Keeps every modal consistent. */
.research-section {
margin-top: 6px;
/* Same surface as the research "window" (.research-new-job -> var(--panel)). */
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.research-section-body { display: flex; flex-direction: column; gap: 8px; padding: 12px; }
.research-section.collapsed .research-section-body { display: none; }
.research-section-header {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 10px 12px; cursor: pointer; user-select: none;
transition: background 0.15s;
}
.research-section:not(.collapsed) > .research-section-header { border-bottom: 1px solid var(--border); }
.research-section-header:hover { background: color-mix(in srgb, var(--fg) 4%, transparent); }
.research-section-title { font-size: 14px; font-weight: 600; letter-spacing: -0.03em; }
.research-section-chevron { flex-shrink: 0; opacity: 0.55; transition: transform 0.2s ease; }
.research-section.collapsed .research-section-chevron { transform: rotate(-90deg); }
.research-section-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; opacity: 0.9; }
/* Active section dot: pulsing accent glow (work in progress). */
.research-section-dot.pulsing { animation: research-dot-pulse 1.5s ease-in-out infinite; }
@keyframes research-dot-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent, var(--red)) 55%, transparent); }
70% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--accent, var(--red)) 0%, transparent); }
}
/* The top "Research" heading is a plain card-style title — no bar/chevron/dot. */
.research-main-header {
cursor: default; background: none; border: none;
padding: 6px 2px; font-size: 13px; font-weight: 600;
}
.research-main-header:hover { background: none; }
.research-section-count {
/* Match the panel header's "N research" chip — small, muted, no weight. */
font-size: 10px; opacity: 0.6; font-weight: normal;
font-variant-numeric: tabular-nums;
}
.research-job-error {
font-size:11px; color:#f44336; margin-top:4px; line-height:1.4;
word-break:break-word;
}
.research-progress-bar {
height:3px; background:var(--border); border-radius:2px; margin-top:6px;
overflow:hidden;
}
.research-progress-fill {
height:100%; background:var(--accent-primary, var(--red));
border-radius:2px; transition:width 0.4s ease;
}
.research-job-result {
margin-top:10px; border-top:1px solid var(--border); padding-top:10px;
}
.research-job-sources {
display:flex; flex-wrap:wrap; gap:4px; margin-bottom:8px;
}
.research-source-link {
font-size:10px; padding:2px 6px;
background:color-mix(in srgb, var(--accent-primary, var(--red)) 10%, transparent);
border-radius:3px; color:var(--accent-primary, var(--red));
text-decoration:none; white-space:nowrap;
max-width:200px; overflow:hidden; text-overflow:ellipsis;
}
.research-source-link:hover { text-decoration:underline; }
.research-source-more { font-size:10px; opacity:0.5; padding:2px 4px; }
.research-job-report-body {
font-size:12px; line-height:1.55; max-height:400px; overflow-y:auto;
}
.research-job-report-body h1,
.research-job-report-body h2,
.research-job-report-body h3 { font-size:13px; margin:12px 0 4px; }
.research-job-report-body p { margin:4px 0; }
.research-job-report-body pre { font-size:11px; }
.research-job-loading { font-size:11px; opacity:0.5; padding:8px 0; }
.research-library-section {
border-top:1px solid var(--border); flex-shrink:0;
}
.research-library-toggle {
width:100%; background:transparent; border:none; color:var(--fg);
padding:8px 14px; font-size:11px; text-align:left;
opacity:0.6; cursor:pointer; font-weight:500;
}
.research-library-toggle:hover { opacity:1; }
.research-library-list {
max-height:300px; overflow-y:auto;
}
.research-library-item {
display:flex; align-items:center; gap:6px; flex-wrap:wrap;
padding:8px 14px; font-size:11px;
border-bottom:1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.research-library-item:hover { background:color-mix(in srgb, var(--border) 20%, transparent); }
.research-lib-query {
flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:120px;
}
.research-lib-actions {
display:flex; gap:4px; margin-left:auto;
}
.research-lib-meta {
font-size:9px; opacity:0.4; white-space:nowrap; font-family:monospace;
}
.research-lib-open,
.research-lib-delete {
background:transparent; border:none; color:var(--fg);
opacity:0.3; cursor:pointer; padding:2px; display:flex;
transition:opacity 0.15s;
}
.research-lib-open:hover { opacity:1; }
.research-lib-delete:hover { opacity:1; color:#f44336; }
.research-badge {
background:var(--accent-primary, var(--red)); border-radius:50%;
width:6px; height:6px; margin-left:auto; display:inline-block; flex-shrink:0;
position: relative; left: -4px;
/* Breathe when research has finished — gentle scale + glow pulse to
draw the eye to the unread result without being a hard blink. */
animation: research-badge-breathe 2.4s ease-in-out infinite;
}
@keyframes research-badge-breathe {
0%, 100% {
transform: scale(1);
opacity: 0.85;
box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-primary, var(--red)) 55%, transparent);
}
50% {
transform: scale(1.35);
opacity: 1;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent-primary, var(--red)) 0%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.research-badge { animation: none; }
}
/* Sidebar Deep Research running indicator — text then dot, styled the same
as Cookbook's running status (no glow). */
.research-sb-running {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.research-sb-status {
font-size: 8px;
opacity: 0.5;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
top: 0;
left: -12px;
}
.research-sb-dot {
width: 6px;
height: 6px;
flex-shrink: 0;
border-radius: 50%;
background: var(--color-success, #4caf50);
animation: cookbook-notif-pulse 2s ease-in-out infinite;
position: relative;
top: -1px;
left: -4px;
}
@media (prefers-reduced-motion: reduce) {
.research-sb-dot { animation: none; }
}
/* ── In-house color picker ── */
/* A color-picker swatch input shows the chosen colour as its background; hide
the underlying hex text/caret so it doesn't read as "#a1b2c3" in the box.
(The themed/gallery variants set this too; this is the generic fallback for
swatches outside those scopes, e.g. the calendar-settings color dots.) */
input.cp-swatch-input {
color: transparent;
font-size: 0;
text-shadow: none;
caret-color: transparent;
user-select: none;
}
input.cp-swatch-input::selection { background: transparent; }
.cp-popover {
position: fixed; z-index: 10000;
display: none;
width: 240px;
background: var(--panel, #1a1a1a);
border: 1px solid var(--border, #333);
border-radius: 8px;
padding: 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.45);
font-family: inherit; color: var(--fg, #eee); font-size: 12px;
user-select: none;
}
.cp-sl {
position: relative;
width: 100%; height: 160px;
border-radius: 6px;
cursor: crosshair;
overflow: hidden;
touch-action: none;
}
.cp-sl-white {
position: absolute; inset: 0;
background: linear-gradient(to right, #fff, rgba(255,255,255,0));
pointer-events: none;
}
.cp-sl-black {
position: absolute; inset: 0;
background: linear-gradient(to top, #000, rgba(0,0,0,0));
pointer-events: none;
}
.cp-sl-handle {
position: absolute;
width: 12px; height: 12px;
margin: -6px 0 0 -6px;
border: 2px solid #fff;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 1px 3px rgba(0,0,0,0.5);
pointer-events: none;
}
.cp-hue {
position: relative;
margin-top: 10px;
height: 14px;
border-radius: 7px;
background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
cursor: pointer;
touch-action: none;
}
.cp-hue-handle {
position: absolute; top: 50%;
width: 10px; height: 18px;
margin: -9px 0 0 -5px;
border: 2px solid #fff;
border-radius: 3px;
box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 1px 3px rgba(0,0,0,0.5);
pointer-events: none;
}
.cp-row {
display: flex; align-items: center; gap: 6px; margin-top: 10px;
}
.cp-preview {
width: 24px; height: 24px;
border-radius: 50%;
border: 1px solid var(--border, #333);
flex-shrink: 0;
}
.cp-hex {
flex: 1; min-width: 0;
padding: 4px 6px;
background: var(--bg, #000); color: var(--fg, #eee);
border: 1px solid var(--border, #333);
border-radius: 4px;
font-family: ui-monospace, 'Fira Code', monospace;
font-size: 12px; text-transform: lowercase;
}
.cp-hex:focus { outline: none; border-color: var(--red, #e06c75); }
.cp-eyedropper {
width: 26px; height: 26px;
padding: 0;
background: transparent;
border: 1px solid var(--border, #333);
border-radius: 4px;
color: var(--fg, #eee);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
transition: background 0.1s;
}
.cp-eyedropper:hover:not(:disabled) { background: rgba(255,255,255,0.08); }
.cp-section-label {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.6px;
opacity: 0.5; margin-top: 10px; margin-bottom: 4px;
}
.cp-swatches {
display: flex; flex-wrap: wrap; gap: 4px;
}
.cp-swatch {
width: 20px; height: 20px;
border-radius: 50%;
border: 1px solid var(--border, #333);
cursor: pointer;
padding: 0;
transition: transform 0.08s;
}
.cp-swatch:hover { transform: scale(1.15); }
.cp-recent-empty {
font-size: 11px; opacity: 0.4; padding: 2px 0;
}
/* PDF form export modal + signature modals — match the app theme via the
existing modal/confirm-btn classes. Only a few atom-level styles below. */
.pdf-export-overlay .modal-content { padding: 12px 14px; }
.pdf-export-overlay #pdf-export-body { padding-right: 4px; }
.pdf-export-overlay .pdf-export-section {
margin-bottom: 14px;
}
.pdf-export-overlay .pdf-export-section-title {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--red);
opacity: 0.85;
margin-bottom: 6px;
}
.pdf-export-overlay .pdf-export-row {
display: grid;
grid-template-columns: 38% 62%;
gap: 8px;
align-items: center;
padding: 3px 0;
}
.pdf-export-overlay .pdf-export-row label {
font-size: 0.78rem;
color: var(--fg);
opacity: 0.85;
}
.pdf-export-input,
.pdf-export-overlay select.pdf-export-input,
.pdf-export-overlay input.pdf-export-input {
padding: 4px 6px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 4px;
font-size: 0.78rem;
font-family: inherit;
}
.pdf-export-input:focus {
outline: none;
border-color: var(--fg);
}
.sig-modal-overlay .modal-content { padding: 12px 14px; }
.sig-modal-overlay .sig-canvas {
display: block;
width: 100%;
height: 240px;
background: #fff;
cursor: crosshair;
border: 1px dashed var(--border);
border-radius: 5px;
/* Drawing uses pointer events; without this, a touch-drag on the canvas pans
the page / sheet instead of drawing ("the whole window moves"). */
touch-action: none;
}
.sig-modal-overlay .sig-name,
.sig-modal-overlay .sig-smoothness {
font-family: inherit;
}
.sig-modal-overlay .sig-name {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
border-radius: 4px;
font-size: 0.85rem;
}
.sig-modal-overlay .sig-tile {
position: relative;
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px;
cursor: pointer;
background: var(--bg);
}
.sig-modal-overlay .sig-tile:hover { border-color: var(--fg); }
.sig-modal-overlay .sig-tile img {
display: block;
width: 100%;
height: 80px;
object-fit: contain;
background: #fff;
border-radius: 3px;
}
.sig-modal-overlay .sig-tile-del {
position: absolute;
top: 1px;
right: 6px;
width: 20px;
height: 20px;
/* Center the × glyph inside the round bg — without this the button's default
text metrics push it past the right edge of the circle. */
display: flex;
align-items: center;
justify-content: center;
padding: 0 0 0 1px;
box-sizing: border-box;
font-size: 0.85rem;
line-height: 1;
border: 1px solid var(--accent, var(--red));
background: #fff;
color: var(--accent, var(--red));
border-radius: 50%;
cursor: pointer;
opacity: 1;
}
.sig-modal-overlay .sig-tile-del:hover {
background: color-mix(in srgb, var(--accent, var(--red)) 10%, #fff);
}
.sig-modal-overlay .sig-new-tile {
border: 2px dashed var(--border);
border-radius: 6px;
padding: 8px;
cursor: pointer;
background: var(--bg);
color: var(--fg);
display: flex;
align-items: center;
justify-content: center;
min-height: 106px;
font-weight: 600;
opacity: 0.85;
}
.sig-modal-overlay .sig-new-tile:hover {
opacity: 1;
border-color: var(--fg);
}
/* Form checkboxes — render as a circular dot, theme-aware. Used in the
Export PDF modal and the inline PDF view overlay. */
.pdf-export-overlay input[type="checkbox"],
#doc-pdf-view input[type="checkbox"] {
-webkit-appearance: none;
appearance: none;
margin: 0;
cursor: pointer;
border-radius: 50%;
background: #fff;
position: relative;
}
.pdf-export-overlay input[type="checkbox"] {
width: 14px;
height: 14px;
border: 1.5px solid var(--border);
background: var(--bg);
}
.pdf-export-overlay input[type="checkbox"]:checked {
background: var(--fg);
border-color: var(--fg);
}
/* PDF view overlays on a white page render — keep colors theme-independent
so the dot is always visible against the rendered PDF. */
#doc-pdf-view input[type="checkbox"] {
border: 1.5px solid #444;
}
#doc-pdf-view input[type="checkbox"]:checked {
background: #111;
border-color: #111;
}
/* ─────────────────────────────────────────────────────────────────────────
Frosted glass theme — applied when the user enables the "Glass" toggle
in theme settings (writes `body.theme-frosted`). Layers a semi-translucent
tint over every panel/modal/sidebar/dropdown and blurs whatever is
behind it, so the UI reads like layered glass.
───────────────────────────────────────────────────────────────────────── */
body.theme-frosted #sidebar,
body.theme-frosted .modal-content,
body.theme-frosted .doclib-modal-content,
body.theme-frosted .gallery-modal-content,
body.theme-frosted .notes-pane,
body.theme-frosted .research-pane,
body.theme-frosted .calendar-pane,
body.theme-frosted .cookbook-pane,
body.theme-frosted .doc-pane,
body.theme-frosted .doc-pane-floating,
body.theme-frosted .cp-popover,
body.theme-frosted .dropdown,
body.theme-frosted .overflow-menu,
body.theme-frosted .doc-tab-dropdown,
body.theme-frosted .email-reader,
body.theme-frosted .admin-card,
body.theme-frosted .gallery-detail-menu,
body.theme-frosted #theme-popup .modal-content {
/* Tinted base color (set separately so the shorthand can't swallow it);
much more transparent than before so what's behind actually shows
through. The blur catches whatever pixels are below. */
background-color: color-mix(in srgb, var(--panel, var(--bg)) 32%, transparent) !important;
background-image: linear-gradient(180deg,
color-mix(in srgb, var(--fg, #fff) 14%, transparent) 0%,
color-mix(in srgb, var(--fg, #fff) 4%, transparent) 26%,
transparent 55%) !important;
backdrop-filter: blur(24px) saturate(170%) !important;
-webkit-backdrop-filter: blur(24px) saturate(170%) !important;
border-color: color-mix(in srgb, var(--fg) 22%, transparent) !important;
}
/* The sidebar header + user bar paint their own opaque --sidebar-bg/--panel.
Under the frosted theme the sidebar is translucent glass, so those solid
bands stood out as a dark (near-black) strip over the title/avatar area.
Make them transparent so they blend into the frosted sidebar. */
body.theme-frosted .sidebar-header,
body.theme-frosted .sidebar-user-bar {
background: transparent !important;
}
/* Same problem on tool modals: the sticky .modal-header paints a solid
var(--panel) bar (so scrolled content doesn't bleed through). Over a
frosted-glass modal body that solid bar reads as a dark band across the
title area (Calendar, Tasks, Cookbook, Memory, Email…). Give the header
its own matching frosted glass — translucent tint + blur — so it stays
readable while scrolling but blends into the panel instead of a black band. */
body.theme-frosted .modal-header {
/* Transparent — NOT another panel tint. The header sits on top of the
already-frosted modal body, so any added tint stacks and the band
reads darker than its surroundings. Keep just the blur (which doesn't
darken) so scrolled content stays legible under the sticky header. */
background-color: transparent !important;
background-image: none !important;
backdrop-filter: blur(24px) !important;
-webkit-backdrop-filter: blur(24px) !important;
}
/* Deep Research's header isn't sticky (content doesn't scroll under it) and
its pane carries extra frosting — saturate + a top gradient highlight +
the synapse glow. The generic header blur above adds its OWN glass layer,
which renders a different shade than the pane behind it. Drop the header's
blur so the pane's frosted background shows through the header uniformly. */
body.theme-frosted .research-pane-header {
background-color: transparent !important;
background-image: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
body.theme-frosted .notes-mobile-grabber {
background-color: transparent !important;
background-image: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
/* Inner admin-cards inside frosted modals stack their transparency on top
of the modal's, which kills the see-through. Make the inner card nearly
fully transparent so the outer modal's blur dominates. */
body.theme-frosted .modal-content .admin-card,
body.theme-frosted .doclib-modal-content .admin-card,
body.theme-frosted .gallery-modal-content .admin-card,
body.theme-frosted #theme-popup .admin-card {
background-color: color-mix(in srgb, var(--panel, var(--bg)) 10%, transparent) !important;
background-image: none !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
/* Subtle inner light highlight + softer shadow on the heavier surfaces */
body.theme-frosted .modal-content,
body.theme-frosted .doclib-modal-content,
body.theme-frosted .gallery-modal-content,
body.theme-frosted .notes-pane,
body.theme-frosted .research-pane,
body.theme-frosted .calendar-pane,
body.theme-frosted .cookbook-pane,
body.theme-frosted .doc-pane,
body.theme-frosted .doc-pane-floating {
box-shadow:
0 14px 36px rgba(0, 0, 0, 0.5),
inset 0 1px 0 color-mix(in srgb, var(--fg, #fff) 14%, transparent),
inset 0 -1px 0 color-mix(in srgb, #000 14%, transparent) !important;
}
/* Backdrops should be MUCH less opaque so the blur effect reads through to
the actual page content behind the modal. */
body.theme-frosted .modal {
background: color-mix(in srgb, #000 12%, transparent) !important;
}
/* Mobile only: lift the Dependencies "Server" dropdown's selected text up ~2px. */
@media (max-width: 768px) {
#hwfit-deps-server { padding-bottom: 4px; line-height: 1; }
}
/* Emoji rendered as monochrome line icons (OpenMoji-black via /api/emoji proxy)
instead of colorful system glyphs — project rule: never colorful emoji. The
SVG is used as a mask filled with the current text color, so it tints to the
theme. Sized to sit on the text baseline. */
.emoji {
height: 1.15em;
width: 1.15em;
vertical-align: -0.18em;
margin: 0 0.05em;
display: inline-block;
background-color: currentColor;
-webkit-mask: var(--em) center / contain no-repeat;
mask: var(--em) center / contain no-repeat;
}
/* Nudge the X icon in the "Clear all" button down 2px. */
#research-clear-all svg { position: relative; top: 2px; }
/* RESEARCH SCROLL FORCE — guarantee the jobs list scrolls inside the bounded
pane (the pane is height-capped: 85vh desktop, 100dvh mobile). Each ancestor
needs min-height:0 for a nested flex scroll to engage. */
#research-pane { min-height: 0; }
/* The BODY is the single scroller — the list flows into it and the whole body
scrolls. (A nested list-scroller kept clipping instead of scrolling.) */
#research-pane .research-pane-body {
flex: 1 1 auto !important; min-height: 0 !important;
display: flex !important; flex-direction: column !important;
overflow-y: auto !important; -webkit-overflow-scrolling: touch;
}
#research-pane .research-jobs-list {
flex: 0 0 auto !important; min-height: 0 !important;
overflow: visible !important; max-height: none !important;
}
/* RESEARCH FULLSCREEN MOBILE — the pane wasn't opening to full height on mobile,
so its content overflowed with nowhere to scroll. Pin it to the full viewport
(id selector + !important beats the inherited modal-content sizing). */
@media (max-width: 768px) {
/* Bottom-sheet: 90dvh tall (a touch smaller than full-screen) anchored to the
bottom, so there's a small gap at the top and it reads as a sheet. The body
still scrolls within the bounded height. */
#research-overlay { align-items: flex-end !important; justify-content: stretch !important; }
#research-pane {
height: 90dvh !important;
max-height: 90dvh !important;
width: 100vw !important;
max-width: 100vw !important;
border-radius: 14px 14px 0 0 !important;
}
/* Bottom breathing room so the expanded Settings + Start button clear the
browser's bottom toolbar / safe area and can always be scrolled into view
(otherwise the form fits the viewport and there's no room to reach them). */
#research-pane .research-pane-body {
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 40px) !important;
touch-action: pan-y;
overscroll-behavior: contain;
}
}
/* Quick fade + up-nudge. */
.research-start-btn { position: relative; top: -6px; transition: opacity 0.16s ease; }
/* Right-side group of the section header: clear button (past), status dot, chevron. */
.research-section-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
/* "Clear all" in the Past header — styled like the cookbook-running clear btn. */
.research-section-clear {
font-size: 10px; padding: 0 8px; height: 22px;
border-radius: 6px; border: 1px solid var(--border);
color: color-mix(in srgb, var(--fg) 45%, transparent);
background: none; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;
font-family: inherit; white-space: nowrap; transition: all 0.15s;
position: relative; top: -2px;
}
.research-section-clear:hover { color: var(--red); border-color: var(--red); background: color-mix(in srgb, var(--red) 8%, transparent); }
.research-section-clear svg { width: 11px; height: 11px; }
/* Visual Report button tinted with the research type colour (--cat-color),
falling back to the accent for "standard" (uncategorized) research. */
.research-job-action.research-job-action-report {
color: var(--cat-color, var(--accent, var(--red)));
border-color: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 45%, var(--border));
}
.research-job-action.research-job-action-report:hover {
color: var(--cat-color, var(--accent, var(--red)));
border-color: var(--cat-color, var(--accent, var(--red)));
background: color-mix(in srgb, var(--cat-color, var(--accent, var(--red))) 10%, transparent);
}
/* Nudge the "+ Queue" button up 5px. */
#research-add-btn { position: relative; top: -5px; }
/* Nudge the Start play-icon and Queue plus-sign up 1px so they sit visually
centered with the button label text (their glyph baselines drop low). */
.research-start-btn svg { position: relative; top: -1px; }
.research-add-plus { position: relative; top: -1px; display: inline-block; }
/* Footer hint under Past research linking to the Library's Research tab. */
.research-library-hint {
/* full-width line in the header, pulled up with negative MARGIN (collapses
the gap so it moves up without making the header taller). */
width: 100%; flex-basis: 100%; margin: -22px 0 0; line-height: 1.2;
}
.research-library-link {
background: none; border: none; padding: 0; cursor: pointer;
font: inherit; color: var(--accent, var(--red)); text-decoration: underline;
}
.research-library-link:hover { opacity: 0.8; }
/* Nudge the Delete button 4px left. */
.research-job-action[data-action="delete"] { position: relative; right: 2px; }
/* "Standard" (uncategorized) research uses green everywhere — set its --cat-color
to the success green so the Visual Report button matches the green badge. */
.research-job-card.done:not([data-category]) { --cat-color: var(--color-success); }
/* Failed research (0 sources) — red flag + actionable note. */
.research-job-card.research-job-failed { border-color: color-mix(in srgb, #f44336 40%, var(--border)); }
.research-cat-badge.research-cat-failed { color: #f44336; display: inline-flex; align-items: center; gap: 3px; }
.research-cat-badge.research-cat-failed svg { width: 10px; height: 10px; }
.research-job-failnote { font-size: 11px; color: #f44336; opacity: 0.9; margin: 4px 0 6px; line-height: 1.35; }
/* Shared popup-menu row feedback. Many small menus use their own item class;
keep the hover light and consistent instead of giving one action a special
glow. */
:is(
.overflow-menu-item,
.dropdown-item,
.dropdown-item-compact,
.export-dropdown-item,
.sort-dropdown-item,
.note-reminder-menu-item,
.research-run-mode-popover .research-run-mode-row,
.ge-fx-menu-item
) {
transition:
background-color 0.14s ease,
color 0.14s ease,
opacity 0.14s ease,
transform 0.14s ease;
}
:is(
.overflow-menu-item,
.dropdown-item,
.dropdown-item-compact,
.export-dropdown-item,
.sort-dropdown-item,
.note-reminder-menu-item,
.research-run-mode-popover .research-run-mode-row,
.ge-fx-menu-item
):hover:not(:disabled) {
opacity: 1;
color: var(--fg);
background-color: color-mix(in srgb, var(--accent, var(--red)) 10%, transparent);
transform: translateX(1px);
}