34233 lines
1.0 MiB
34233 lines
1.0 MiB
/* ============================================ */
|
||
/* 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.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 + <pre>; 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 <pre> (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 <pre> 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 <pre> (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 <pre> / 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 <svg> 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-paused-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--orange, #ff9800);
|
||
background: color-mix(in srgb, var(--orange, #ff9800) 12%, transparent);
|
||
padding: 2px 6px;
|
||
border-radius: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.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: '<percentage>';
|
||
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;
|
||
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 <option>, 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 <select>
|
||
— <option>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,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239cdef2' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 15 12 9 18 15'/></svg>");
|
||
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,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%232b2b2b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 15 12 9 18 15'/></svg>");
|
||
}
|
||
/* 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 <span>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-<uid>); 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 <button> 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 <select> */
|
||
.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;
|
||
}
|
||
.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,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23e06c75' stroke-opacity='0.85' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='8'/><line x1='21' y1='21' x2='16.65' y2='16.65'/></svg>");
|
||
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 .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;
|
||
}
|
||
/* 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 <pre>
|
||
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: <video> fills the same slot as <img>, 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 <button> tags (Install / Installed) are the EXACT
|
||
same height as the sibling <span> 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 {
|
||
color: color-mix(in srgb, var(--fg) 35%, transparent);
|
||
}
|
||
.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 <button> 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 <whirlpool>" 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-<id>` 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 <select>'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 <select> 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 <input type="color"> 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 <input type="color"> 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 <span> 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;
|
||
}
|
||
/* 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;
|
||
}
|
||
.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 `<span style="background:yellow">…</span>` to <mark>. */
|
||
.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 <head> 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 (<font size>, <big>, <small>, 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 <div style="width:600px">
|
||
or <table width="600">. 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 (<pre> 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 <mark.doc-find-mark> 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); }
|
||
/* 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 <input> 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 <input> 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,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23e06c75' stroke-opacity='0.85' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='11' cy='11' r='8'/><line x1='21' y1='21' x2='16.65' y2='16.65'/></svg>");
|
||
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
|
||
<input type=color> 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
|
||
<input> 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 `<button>` 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 <button> 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);
|
||
}
|