merge: reconcile PR 40 with current lab

Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email
backend/frontend, modular settings, split stylesheets (static/style.css
stays deleted), procfs compatibility, and request-scoped TurnContract
authority win; PR #40's routing classifiers, editor/email/task features,
and style.css changes are ported into lab's module and stylesheet homes.

Integration fixes:
- settings/api.js imports ui.js under its canonical versioned URL
- browser observations keep legacy CAPTCHA/access-block evidence
- artifact turns do not re-trigger broad-web research recovery
- env reference documents PR test-tool variables; page regenerated

PR #40 defects surfaced by lab gates and fixed here:
- web_fetch generic schema drops top-level anyOf (OpenAI contract);
  the compact preview contract still requires url or urls
- get_weather registered as a brokered network read
- new lazy editor modules precached for offline use
- SearXNG pin mirrored into GPU standalone compose files
- image model picker again skips offline endpoints

Tests updated where PR #40 changed behaviour on purpose, and PR tests
moved onto lab's document_source helpers.
This commit is contained in:
Alexandre Teixeira
2026-10-01 05:03:58 +01:00
337 changed files with 90287 additions and 75556 deletions
+183
View File
@@ -0,0 +1,183 @@
/* 00-tokens - part of the former static/style.css.
*
* The stylesheet is split by area, but the split is mechanical: rules were
* moved verbatim and NOT regrouped. The numeric prefix is the load order, and
* that order is what preserves the cascade. A rule sits in the file its
* original position fell into, so an area's rules are not all in its file and
* a file is not only that area's rules.
*
* Do not reorder the <link> tags in static/index.html, and do not move a rule
* between files: with 1,151 redeclarations and 1,886 !important in this
* cascade, either can change which declaration wins.
*
* static/index.html and the PRECACHE list in static/sw.js must agree on this
* order. tests/test_static_stylesheet_manifest.py and
* tests/test_css_computed_style_snapshot.py fail if that drifts.
*/
/* ============================================ */
/* Odysseus UI — Consolidated Stylesheet */
/* ============================================ */
/* ── Variables ──
*
* Theme-public variables (override via theme.js or custom themes):
* Core: --bg, --fg, --panel, --border, --red
* Syntax: --hl-keyword, --hl-string, --hl-comment, --hl-function,
* --hl-number, --hl-builtin, --hl-variable, --hl-params,
* --hl-bg, --hl-fg
* Accents: --accent-primary, --accent-error (set by theme.js)
* Semantic: --color-error, --color-success, --color-warning, --color-danger,
* --color-accent, --color-muted, --color-muted-alt
*/
:root {
/* Core palette */
--bg: #282c34;
--fg: #9cdef2;
--panel: #111;
--border: #355a66;
--red: #e06c75;
/* Were `var(--green)` / `var(--warn)` — self-referential, so they
resolved to invalid and every site fell back to its own literal
(or, for sites with no fallback, painted as transparent/inherit).
Anchor them to real hex so the token layer actually works. */
--green: #50fa7b;
--warn: #f0ad4e;
/* Syntax highlighting */
--hl-bg: #1e2228;
--hl-fg: #9cdef2;
--hl-keyword: #c678dd;
--hl-string: #e5c07b;
--hl-comment: #828997;
--hl-function: #61afef;
--hl-number: #d19a66;
--hl-builtin: #56b6c2;
--hl-variable: #abb2bf;
--hl-params: #a8c0d4;
/* Semantic colors */
--color-error: #ff4444;
--color-error-light: #ff6666;
--color-success: #4caf50;
--color-warning: #f0ad4e;
--color-danger: #c0392b;
--color-recording: #ff3b30;
--color-recording-hover: #d63031;
--color-muted: #888;
--color-muted-alt: #6b7280;
--color-accent: #00aaff;
--color-agent-active: #00ff00;
--color-brand-blue: #3b82f6;
--color-blind-orange: #ff9800;
--color-save-green: var(--color-success);
--color-link-hover: #66c7ff;
--color-subheader: #6b8a94;
--select-bg: var(--bg);
--select-fg: var(--fg);
--select-option-bg: color-mix(in srgb, var(--panel) 74%, var(--bg));
--select-option-fg: var(--fg);
--select-option-active-bg: color-mix(in srgb, var(--accent, var(--red)) 24%, var(--panel));
/* Warm accent — used by the Goals/Today UI in Notes. Lives as a token so
themes can override without touching the goal CSS. */
--accent-warm: #d19a66;
}
:root.light {
--bg: #f5f5f5;
--fg: #2b2b2b;
--panel: #fff;
--border: #bbb;
--hl-bg: #f9f9f9;
--hl-fg: #2b2b2b;
--hl-keyword: #7928a1;
--hl-string: #986801;
--hl-comment: #6a737d;
--hl-function: #005cc5;
--hl-number: #986801;
--hl-builtin: #0070a0;
--hl-variable: #383a42;
--hl-params: #4a4f5c;
--select-bg: #eaeaea;
--select-fg: var(--fg);
--select-option-bg: var(--panel);
--select-option-fg: var(--fg);
--select-option-active-bg: color-mix(in srgb, var(--red) 16%, var(--panel));
}
/* ── Reset & Base ── */
* { box-sizing: border-box; }
html, body { overflow-x: hidden; height: 100%; margin: 0; overscroll-behavior: none; }
body {
background-color: var(--bg);
color: var(--fg);
/* Keep the base padding transition for older layout paths that still adjust
the body directly. Edge docks reserve workspace room on the flex panes
below so left + right docks can coexist without skewing the whole body. */
transition: padding-left 160ms cubic-bezier(0.22, 0.61, 0.36, 1),
padding-right 160ms cubic-bezier(0.22, 0.61, 0.36, 1);
font-family: var(--font-family, 'Fira Code', monospace);
display: flex;
height: 100%;
height: 100dvh; /* dynamic viewport height — adapts when mobile keyboard opens */
overflow: hidden;
}
/* Self-hosted Fira Code font */
@font-face { font-family: 'Fira Code'; font-weight: 300; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Light.woff2') format('woff2'); }
@font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); }
@font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); }
/* GohuFont is the default UI face across themes; keep the shipped font local
so the default is available before custom-font discovery completes. */
@font-face { font-family: 'GohuFont'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/custom/GohuFont.ttf') format('truetype'); }
/* Self-hosted OpenDyslexic — dyslexia-friendly accessibility font option (SIL OFL 1.1) */
@font-face { font-family: 'OpenDyslexic'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Regular.woff2') format('woff2'); }
@font-face { font-family: 'OpenDyslexic'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Bold.woff2') format('woff2'); }
/* Code block baseline */
pre, code, .hljs {
font-size: 11px;
line-height: 1.5;
}
/* Scrollbar styling */
@supports selector(::-webkit-scrollbar) {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--panel);
}
::-webkit-scrollbar-thumb {
background-color: var(--red);
border-radius: 4px;
border: 2px solid var(--panel);
}
::-webkit-scrollbar-thumb:hover {
background-color: color-mix(in srgb, var(--red) 80%, white);
}
}
html {
scrollbar-color: var(--red) var(--panel);
scrollbar-width: thin;
}
/* Utility */
.red-text { color: var(--red); }
/* ── Density Overrides ── */
:root.density-compact { font-size: 13px; }
:root.density-compact .msg { padding: 6px 10px; margin-bottom: 4px; }
:root.density-compact .list-item { padding: 4px 8px; }
:root.density-compact .sidebar .section { padding: 0; }
:root.density-spacious { font-size: 16px; }
:root.density-spacious .msg { padding: 14px 18px; margin-bottom: 12px; }
:root.density-spacious .list-item { padding: 8px 12px; }
:root.density-spacious .sidebar .section { padding: 0; }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+839
View File
@@ -0,0 +1,839 @@
/* 12-gallery - part of the former static/style.css.
*
* The stylesheet is split by area, but the split is mechanical: rules were
* moved verbatim and NOT regrouped. The numeric prefix is the load order, and
* that order is what preserves the cascade. A rule sits in the file its
* original position fell into, so an area's rules are not all in its file and
* a file is not only that area's rules.
*
* Do not reorder the <link> tags in static/index.html, and do not move a rule
* between files: with 1,151 redeclarations and 1,886 !important in this
* cascade, either can change which declaration wins.
*
* static/index.html and the PRECACHE list in static/sw.js must agree on this
* order. tests/test_static_stylesheet_manifest.py and
* tests/test_css_computed_style_snapshot.py fail if that drifts.
*/
.research-synapse.rs-phase-reading .rs-phase-chip,
.research-synapse.rs-phase-analyzing .rs-phase-chip { color: var(--gold, #d4a72c); }
.research-synapse.rs-phase-writing .rs-phase-chip { color: var(--color-success, #4caf50); }
@media (prefers-reduced-motion: reduce) {
.research-synapse .rs-live-dot,
.research-synapse .rs-pulse,
.research-synapse .rs-node-new,
.research-synapse .rs-node-expanded,
.research-synapse .rs-edge-firing { animation: none !important; }
}
/* ── Raw findings items (inside sources-style box) ── */
.finding-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.finding-item .source-link {
border-radius: 6px 6px 0 0;
}
.finding-summary {
padding: 6px 8px 8px;
font-size: 0.82em;
line-height: 1.5;
color: var(--fg-dim, var(--fg));
opacity: 0.85;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 6%, transparent);
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-word;
}
.finding-item:last-child .finding-summary {
border-bottom: none;
margin-bottom: 0;
}
/* ═══════════════════════════════════════════════════════════
Gallery Editor
═══════════════════════════════════════════════════════════ */
/* Tab bar */
.gallery-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border);
padding: 0 16px;
background: var(--panel);
}
.gallery-tab {
padding: 8px 18px;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--fg);
opacity: 0.6;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: opacity 0.15s, border-color 0.15s;
}
.gallery-tab:hover { opacity: 0.85; }
.gallery-tab.active {
opacity: 1;
border-bottom-color: var(--red);
}
/* Narrow modal tab strips should stay on one row. Resized docked windows can
be much narrower than the viewport, so this cannot live only in mobile media
queries. */
.cookbook-tabs,
.memory-tabs,
.admin-tabs,
.lib-tabs,
.gallery-tabs,
.preset-tabs {
flex-wrap: nowrap !important;
overflow-x: auto !important;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.cookbook-tabs::-webkit-scrollbar,
.memory-tabs::-webkit-scrollbar,
.admin-tabs::-webkit-scrollbar,
.lib-tabs::-webkit-scrollbar,
.gallery-tabs::-webkit-scrollbar,
.preset-tabs::-webkit-scrollbar {
display: none;
}
.cookbook-tabs > *,
.memory-tabs > *,
.admin-tabs > *,
.lib-tabs > *,
.gallery-tabs > *,
.preset-tabs > * {
flex: 0 0 auto;
}
.cookbook-tab,
.memory-tab,
.admin-tab,
.lib-tab,
.gallery-tab,
.preset-tab {
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
line-height: 1;
}
.gallery-tab {
gap: 6px;
}
@container (max-width: 360px) {
.cookbook-tab:has(svg),
.memory-tab:has(svg),
.admin-tab:has(svg),
.lib-tab:has(svg),
.gallery-tab:has(svg),
.preset-tab:has(svg) {
width: 34px;
min-width: 34px;
padding-left: 0;
padding-right: 0;
font-size: 0;
}
.cookbook-tab:has(svg) svg,
.memory-tab:has(svg) svg,
.admin-tab:has(svg) svg,
.lib-tab:has(svg) svg,
.gallery-tab:has(svg) svg,
.preset-tab:has(svg) svg {
width: 14px;
height: 14px;
margin-right: 0 !important;
vertical-align: middle !important;
}
.memory-tab:has(svg) .memory-count,
.gallery-tab:has(svg) .gallery-tab-label,
.gallery-tab:has(svg) .gallery-tab-close,
.cookbook-tab:has(svg) .cookbook-tab-count,
.preset-tab:has(svg) .preset-count {
display: none !important;
}
}
/* Icon + label layout inside each tab. */
.gallery-tab {
display: inline-flex;
align-items: center;
gap: 6px;
}
.gallery-tab-icon {
display: inline-flex;
align-items: center;
opacity: 0.85;
}
.gallery-tab.active .gallery-tab-icon { opacity: 1; }
/* Close × on the Edit tab — appears on hover. */
.gallery-tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 2px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
/* Use a slightly larger glyph at a larger line-height so the × sits
visually centered — the U+00D7 character has uneven em-box metrics. */
font-size: 16px;
line-height: 16px;
font-weight: 500;
cursor: pointer;
padding-bottom: 2px;
box-sizing: border-box;
}
.gallery-tab:hover .gallery-tab-close,
.gallery-tab.active .gallery-tab-close { opacity: 0.65; }
/* Hide the close × entirely on the Edit tab when there's no edit open. */
.gallery-tab[data-tab="editor"]:not(.has-edit) .gallery-tab-close { display: none !important; }
.gallery-tab-close:hover { opacity: 1 !important; background: color-mix(in srgb, var(--red) 25%, transparent); color: var(--red); }
/* Inline rename input shown when the Edit tab is double-clicked. */
.gallery-tab-rename-input {
background: var(--bg);
border: 1px solid var(--red);
border-radius: 3px;
color: var(--fg);
font: inherit;
font-size: 13px;
padding: 1px 4px;
width: 140px;
outline: none;
}
/* Albums tab — grid of album cards with cover thumbnails. */
.gallery-albums-container {
padding: 12px 4px;
max-height: calc(92vh - 120px);
overflow-y: auto;
border: 2px dashed transparent;
border-radius: 8px;
transition: border-color 0.15s, background 0.15s;
}
.gallery-settings-container {
padding: 8px 4px;
max-height: calc(92vh - 120px);
overflow-y: auto;
}
.gallery-albums-container.gallery-dragover {
border-color: var(--red);
background: color-mix(in srgb, var(--red) 5%, transparent);
}
.gallery-albums-empty {
display: flex; flex-direction: column; align-items: center; gap: 12px;
padding: 48px 16px;
opacity: 0.7;
font-size: 13px;
}
.gallery-albums-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.gallery-album-card {
position: relative;
display: flex;
flex-direction: column;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.gallery-album-card:hover {
transform: translateY(-2px);
border-color: var(--red);
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 12%, transparent);
}
.gallery-album-menu-btn {
position: absolute;
top: 6px; right: 6px;
width: 24px; height: 24px;
display: flex; align-items: center; justify-content: center;
background: color-mix(in srgb, var(--bg) 70%, transparent);
color: var(--fg);
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
border-radius: 6px;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
z-index: 2;
backdrop-filter: blur(4px);
/* Plain "…" text — the previous SVG was rendering as a flat black
block on some themes when its currentColor didn't contrast. */
font-size: 16px;
line-height: 1;
font-weight: 600;
padding: 0 0 4px;
}
.gallery-album-card:hover .gallery-album-menu-btn,
.gallery-album-menu-btn:focus-visible { opacity: 1; }
.gallery-album-menu-btn:hover { background: var(--bg); }
.gallery-album-menu-pop {
position: absolute;
top: 34px; right: 6px;
min-width: 140px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
display: flex; flex-direction: column;
padding: 4px;
z-index: 3;
}
.gallery-album-menu-pop[hidden] { display: none; }
.gallery-album-cover {
aspect-ratio: 4 / 3;
background: color-mix(in srgb, var(--fg) 5%, var(--bg));
display: flex; align-items: center; justify-content: center;
overflow: hidden;
}
.gallery-album-placeholder {
opacity: 0.4;
font-size: 40px;
display: flex; align-items: center; justify-content: center;
width: 100%; height: 100%;
}
.gallery-album-info {
padding: 8px 10px;
display: flex; flex-direction: column; gap: 2px;
}
.gallery-album-name {
font-size: 13px;
font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.gallery-album-count {
font-size: 11px;
opacity: 0.55;
}
.gallery-album-card-add {
border-style: dashed;
opacity: 0.7;
}
.gallery-album-card-add:hover { opacity: 1; }
.gallery-album-action-tile {
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
}
/* Edit-tab empty state — shown before any image is loaded. */
.gallery-editor-landing {
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 10px;
padding: 28px 16px;
flex: 1;
text-align: center;
color: var(--fg);
}
.gallery-editor-landing h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.gallery-editor-heading {
display: inline-flex;
align-items: center;
gap: 7px;
}
.gallery-editor-heading > svg { opacity: 0.6; flex: 0 0 auto; }
/* "vision model" link in the AI-tagging description → opens Settings. */
.ge-vision-link {
color: var(--accent, var(--red));
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.ge-vision-link:hover { opacity: 0.8; }
.ge-alpha-tag {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
vertical-align: middle;
padding: 1px 5px;
border-radius: 4px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 15%, transparent);
position: relative;
top: -1px;
}
.gallery-editor-landing-actions {
display: flex; gap: 10px;
margin-top: 8px;
}
/* Bigger primary action buttons specifically inside the editor landing —
the gallery-wide .gallery-select-btn is a compact toolbar style which is
too small for the empty-state hero. */
.gallery-editor-landing-actions .gallery-select-btn {
padding: 7px 22px 17px;
font-size: 13px;
opacity: 0.85;
}
/* Visible template picker: every canvas ratio is inspectable without opening
a native menu, and its SVG outline previews the resulting canvas shape. */
.gallery-editor-template-picker {
width: min(760px, 96%);
margin-top: 10px;
}
.gallery-editor-template-picker h4 {
margin: 0 0 7px;
font-size: 11px;
opacity: 0.55;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.gallery-editor-template-options {
display: grid;
grid-template-columns: repeat(4, minmax(105px, 1fr));
gap: 6px;
}
.gallery-editor-template-option {
height: 72px;
min-width: 0;
padding: 6px 7px;
display: grid;
grid-template-rows: 28px auto auto;
justify-items: center;
align-items: center;
background: var(--panel);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 5px;
font: inherit;
font-size: 11px;
cursor: pointer;
overflow: hidden;
}
.gallery-editor-template-option svg { color: var(--accent, var(--red)); }
.gallery-editor-template-option:hover,
.gallery-editor-template-option:focus-visible {
outline: none;
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 7%, var(--panel));
}
.gallery-editor-template-option span {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
}
.gallery-editor-template-option small {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 9px;
opacity: 0.52;
}
@media (max-width: 620px) {
.gallery-editor-landing {
justify-content: flex-start;
gap: 7px;
padding: 18px 12px;
overflow-y: auto;
}
.gallery-editor-landing-actions {
width: 100%;
gap: 6px;
justify-content: center;
}
.gallery-editor-landing-actions .gallery-select-btn {
flex: 1 1 0;
min-width: 0;
max-width: 180px;
padding: 6px 8px 10px;
font-size: 11px;
}
.gallery-editor-template-picker {
width: 100%;
margin-top: 4px;
}
.gallery-editor-template-options {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* The common square, widescreen, portrait, and Instagram presets are
enough to start on a phone. Keep the print and 4K presets on desktop. */
.gallery-editor-template-option:nth-child(n + 5) {
display: none;
}
}
/* Saved-drafts grid on the editor landing. Surfaces every persisted
in-progress project so the user can resume without re-opening the
original photo. Each card: thumbnail + name + last-saved hint + ×. */
.gallery-editor-drafts {
width: min(720px, 92%);
margin: 28px auto 0; /* centered */
padding-top: 18px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 10px;
}
.gallery-editor-drafts {
position: relative;
}
.gallery-editor-drafts-loading {
position: absolute;
inset: 30px 0 0 0;
z-index: 4;
display: flex;
align-items: center;
justify-content: center;
background: var(--panel);
border-radius: 6px;
min-height: 80px;
}
.gallery-editor-drafts-header {
display: flex;
align-items: center;
justify-content: center; /* center the title / search / select */
gap: 8px;
margin-bottom: 8px;
}
.gallery-editor-drafts-title {
margin: 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.55;
font-weight: 600;
text-align: left;
flex-shrink: 0;
}
.gallery-editor-drafts-search {
flex: 1 1 auto;
min-width: 0;
max-width: none;
height: 26px; /* same thickness as the Select button */
box-sizing: border-box;
padding: 0 8px;
font-size: 12px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
}
.gallery-editor-drafts-search:focus { outline: none; border-color: var(--red); }
/* The shared .gallery-select-btn has a -4px top margin + tall asymmetric padding
meant for the main gallery toolbar; in this header it makes the button a
different height / offset from the search box. Normalize so it lines up. */
.gallery-editor-drafts-header .gallery-select-btn {
margin-top: 0 !important;
height: 26px; /* match the search bar */
box-sizing: border-box;
padding: 0 10px;
font-size: 12px;
line-height: 1;
flex-shrink: 0;
}
.cookbook-official-filter {
display: inline-flex;
align-items: center;
gap: 4px;
flex: 0 1 auto;
min-width: 0;
max-width: 100%;
color: var(--fg);
font-size: 10px;
opacity: 0.72;
white-space: nowrap;
position: relative;
top: 1px;
}
.cookbook-official-filter > span:first-child {
position: relative;
top: 1px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 9px;
}
.cookbook-official-filter .toggle {
width: 26px;
height: 14px;
}
.cookbook-official-filter .toggle .slider:before {
width: 10px;
height: 10px;
left: 2px;
bottom: 2px;
}
.cookbook-official-filter .toggle input:checked + .slider:before {
transform: translateX(12px);
}
#gallery-editor-drafts-select { margin-left: auto; flex-shrink: 0; }
#gallery-editor-drafts-select.active {
background: color-mix(in srgb, var(--accent, var(--red)) 18%, transparent);
border-color: var(--accent, var(--red));
color: var(--accent, var(--red));
}
/* Graceful exit when a project card is deleted — fade + shrink before re-render. */
.gallery-editor-draft-card.gallery-draft-removing {
opacity: 0;
transform: scale(0.92);
transition: opacity 0.24s ease, transform 0.24s ease;
pointer-events: none;
}
/* Draft select dot uses the standard small .gallery-select-dot look (same as the
photo grid) — no oversized white-ring checkbox. */
.gallery-editor-drafts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
}
.gallery-drafts-just-opened > .gallery-editor-draft-card {
animation: section-domino-in 0.36s cubic-bezier(0.22, 1.61, 0.36, 1) backwards;
}
.gallery-drafts-just-opened > :nth-child(1) { animation-delay: 0.02s; }
.gallery-drafts-just-opened > :nth-child(2) { animation-delay: 0.04s; }
.gallery-drafts-just-opened > :nth-child(3) { animation-delay: 0.06s; }
.gallery-drafts-just-opened > :nth-child(4) { animation-delay: 0.08s; }
.gallery-drafts-just-opened > :nth-child(5) { animation-delay: 0.10s; }
.gallery-drafts-just-opened > :nth-child(6) { animation-delay: 0.12s; }
.gallery-drafts-just-opened > :nth-child(7) { animation-delay: 0.14s; }
.gallery-drafts-just-opened > :nth-child(8) { animation-delay: 0.16s; }
.gallery-drafts-just-opened > :nth-child(9) { animation-delay: 0.18s; }
.gallery-drafts-just-opened > :nth-child(10) { animation-delay: 0.20s; }
.gallery-drafts-just-opened > :nth-child(11) { animation-delay: 0.22s; }
.gallery-drafts-just-opened > :nth-child(12) { animation-delay: 0.24s; }
.gallery-drafts-just-opened > :nth-child(13) { animation-delay: 0.26s; }
.gallery-drafts-just-opened > :nth-child(14) { animation-delay: 0.28s; }
.gallery-drafts-just-opened > :nth-child(15) { animation-delay: 0.30s; }
.gallery-drafts-just-opened > :nth-child(16) { animation-delay: 0.32s; }
.gallery-drafts-just-opened > :nth-child(17) { animation-delay: 0.34s; }
.gallery-drafts-just-opened > :nth-child(18) { animation-delay: 0.36s; }
.gallery-drafts-just-opened > :nth-child(19) { animation-delay: 0.38s; }
.gallery-drafts-just-opened > :nth-child(20) { animation-delay: 0.40s; }
.gallery-drafts-just-opened > :nth-child(n+21) { animation-delay: 0.42s; }
.gallery-drafts-view-toggle {
display: inline-flex;
position: relative;
top: 0;
height: 24px;
flex: 0 0 auto;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 5px;
background: var(--bg);
}
.gallery-drafts-view-toggle button {
width: 25px;
height: 22px;
margin: 0;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-right: 1px solid var(--border);
background: transparent;
color: color-mix(in srgb, var(--fg) 55%, transparent);
cursor: pointer;
line-height: 0;
}
.gallery-drafts-view-toggle button:last-child { border-right: 0; }
.gallery-drafts-view-toggle button:hover { color: var(--fg); }
.gallery-drafts-view-toggle button.active {
height: 22px;
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-thumb {
width: 44px;
height: 44px;
aspect-ratio: auto;
}
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete {
position: static;
opacity: 0.55;
background: transparent;
color: var(--fg);
}
.gallery-editor-draft-delete-trash { display: none; }
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete-trash {
display: block;
width: 18px;
height: 18px;
position: relative;
top: -2px;
}
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete:hover {
opacity: 1;
color: var(--color-error, #f44);
background: color-mix(in srgb, var(--color-error, #f44) 10%, transparent);
}
.gallery-editor-draft-card {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--panel) 70%, transparent);
cursor: pointer;
transition: border-color 0.15s, background 0.15s, transform 0.15s;
text-align: left;
}
.gallery-editor-draft-card:hover {
border-color: var(--accent, var(--red));
background: color-mix(in srgb, var(--panel) 90%, transparent);
}
.gallery-editor-draft-thumb {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: 4px;
background: var(--bg);
display: block;
}
.gallery-editor-draft-thumb-empty {
background: repeating-linear-gradient(
45deg,
color-mix(in srgb, var(--fg) 4%, transparent),
color-mix(in srgb, var(--fg) 4%, transparent) 6px,
transparent 6px,
transparent 12px
);
}
.gallery-editor-draft-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.gallery-editor-draft-name {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-editor-draft-meta {
font-size: 10px;
opacity: 0.55;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-editor-draft-delete {
position: absolute;
top: -2px;
right: 8px;
width: 22px;
height: 22px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 14px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s, background 0.12s;
}
.gallery-editor-draft-delete {
padding-bottom: 2px;
}
.gallery-editor-draft-delete:hover { background: var(--red); }
/* On very narrow phones, give project search its own row instead of
compressing the title and view controls into an overflowing header. */
@media (max-width: 360px) {
.gallery-editor-drafts-header {
flex-wrap: wrap;
justify-content: flex-start;
gap: 6px;
}
.gallery-editor-drafts-title {
flex: 1 1 auto;
}
.gallery-editor-drafts-search {
order: 10;
flex: 1 1 100%;
width: 100%;
max-width: none;
}
#gallery-editor-drafts-select {
margin-left: 0;
}
}
/* Editor layout */
.gallery-editor {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
/* Prevent the editor's own scroll/touch from chaining out to the
gallery modal or the page body — critical on mobile so touching
the canvas doesn't slide the whole page. */
overscroll-behavior: contain;
touch-action: pan-y;
}
/* Pin the topbar to the top of the editor so its action buttons remain
visible if the gallery modal body itself ends up scrolling. */
.ge-topbar { position: sticky; top: 0; z-index: 5; }
/* Top bar */
.ge-topbar {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: var(--panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
gap: 8px;
}
.ge-topbar-left, .ge-topbar-right {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 4px;
}
.ge-topbar-right > * {
flex-shrink: 0;
}
.ge-topbar .dropdown[popover] {
position: fixed;
margin: 0;
bottom: auto;
max-height: calc(100vh - 80px);
overflow-y: auto;
}
.ge-draft-status {
flex: 0 1 auto;
min-width: 0;
max-width: 92px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: color-mix(in srgb, var(--fg) 52%, transparent);
font-size: 10px;
line-height: 1;
transition: color 0.15s, opacity 0.15s;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+980
View File
@@ -0,0 +1,980 @@
/* Document library and editor, image gallery, image editor.
*
* This is an eagerly loaded tail segment of the app stylesheet cascade. The
* former static/style.css now lives in the numbered 00-17 fragments; these
* three pre-existing panel files follow those fragments without changing
* their relative order. Nothing here is lazy-loaded.
*
* LOAD ORDER IS PART OF THE CONTRACT. static/index.html and the PRECACHE list
* in static/sw.js must agree:
* 1. static/css/00-tokens.css through static/css/17-research.css,
* in numeric order
* 2. static/css/documents-gallery-editor.css
* 3. static/css/email-calendar-notes-tasks.css
* 4. static/css/cookbook-research-memory-settings.css
*
* Rules in this file retain their existing cascade position. Shared rules
* formerly kept in static/style.css now live in the numbered fragments.
*/
/* Document library search-term highlight — clear, high-contrast. */
mark.doclib-search-hl {
background: var(--accent, #e8a830);
color: var(--bg, #1a1a1a);
border-radius: 2px;
padding: 0 2px;
font-weight: 600;
}
/* 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;
}
/* 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;
}
.doc-divider.doc-divider-handle-idle .doc-divider-collapse {
opacity: 0;
pointer-events: none;
}
.doc-divider:hover .doc-divider-collapse { opacity: 0.92; }
/* 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); }
}
.doc-editor-pane.doc-pane-leaving {
pointer-events: none;
}
@keyframes doc-pane-leave {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(24px); opacity: 0; }
}
.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-right,
#doc-tab-left {
position: relative;
top: 3px;
}
/* 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.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: var(--red);
}
@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; }
}
.doc-langpicker-trigger.doc-langpicker-email-compact {
width: 28px;
min-width: 28px;
padding: 0 0 0 6px;
justify-content: flex-start;
gap: 0;
}
/* 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-overflow-wrapper { order: -1; }
.doc-overflow-toggle { opacity: 0.5; }
.doc-md-toolbar.md-preview-active,
.doc-md-toolbar.md-write-active {
overflow: visible;
-webkit-mask-image: none;
mask-image: none;
z-index: 3;
}
.doc-md-toolbar #doc-render-view-toggle .md-view-opt {
height: 28px;
}
/* Some toolbar buttons baked in svg{opacity:0.7} via inline styles, which
compounded with the button's own opacity to look almost invisible.
Force the inline svg opacity back to 1 inside the toolbar — the button
wrapper's 0.85 is the only fade we need. */
.doc-md-toolbar button svg[style*="opacity"] { opacity: 1 !important; }
/* 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;
}
/* Pull the icon down toward the "sign" label so they read as one unit. */
#doc-pdf-add-sign-btn svg { transform: translateY(2px); }
.doc-outline-count {
opacity: 0.48;
font-variant-numeric: tabular-nums;
}
.doc-outline-target {
animation: doc-outline-target-flash 1.05s ease-out;
}
@keyframes doc-outline-target-flash {
0%, 28% { background-color: color-mix(in srgb, var(--accent, var(--red)) 24%, transparent); }
100% { background-color: transparent; }
}
.doc-rich-slash-count {
opacity: 0.48;
font-variant-numeric: tabular-nums;
}
.doc-rich-slash-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 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);
}
.doc-editor-highlight::-webkit-scrollbar { display: none; }
/* 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-close::before {
content: '';
width: 10px;
height: 10px;
display: block;
background: currentColor;
-webkit-mask: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 2L8 8M8 2L2 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M2 2L8 8M8 2L2 8' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.doc-suggestion-accept:hover {
filter: brightness(1.15);
}
.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); }
}
.doc-diff-content { }
@media (hover: none) {
.doc-preview-hover-edit {
opacity: 1;
pointer-events: auto;
transform: none;
}
}
#doclib-research-search { position: relative; top: -1.5px; }
/* Restore sits on the right of the Delete in archive previews. */
.doclib-chat-restore-btn { margin-left: auto; }
/* 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; }
@keyframes doclib-expand {
from { opacity: 0.5; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Hide the section headers while a card is expanded (full-screen reader). */
.doclib-grid:has(.doclib-card-expanded) > .skills-section-header { display: none; }
.skill-card:not(.doclib-card-expanded) .skill-card-tags { display: none; }
.doclib-chip-scroll-frame > .chip-scroll-strip:empty {
display: none;
}
.skill-card.doclib-card-expanded .skill-kebab-btn { display: none; }
.skill-card.doclib-card-expanded .skill-chevron-up { display: inline-flex; }
/* 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;
}
.doclib-inline-load-more::before {
display: none;
}
#doclib-selected-count {
color: color-mix(in srgb, var(--fg) 50%, transparent);
font-size: 10px;
}
.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) .gallery-editor-landing {
width: 100%;
min-height: 0;
box-sizing: border-box;
}
#gallery-bulk-count,
#gallery-editor-drafts-bulk-count {
top: -1px !important;
}
#gallery-select-btn { position: relative; top: 4px !important; font-size: 12px; padding: 10px 11px 12px; min-width: 76px; justify-content: center; }
#gallery-albums-search { top: -4px; }
#gallery-albums-select-btn { top: -4px !important; }
#gallery-bulk-delete,
#gallery-albums-bulk-delete,
#gallery-editor-drafts-bulk-delete {
color: var(--color-error, #f44) !important;
}
.gallery-detail-image video {
max-width: 100%;
max-height: 70vh;
display: block;
}
.gallery-card:hover .gallery-fav-btn { opacity: 1; }
.gallery-card:hover .gallery-dl-btn { opacity: 1; }
@media (hover: none) {
.gallery-dl-btn { opacity: 0.55; }
}
/* Space the user's tag chips off the "add tag" input below them. */
#gallery-user-tag-chips { margin-bottom: 4px; }
.gallery-user-chip:hover .gallery-tag-x { opacity: 0.85; }
@keyframes gallery-skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.gallery-card:hover .gallery-card-overlay,
.gallery-card.gallery-card-focus .gallery-card-overlay {
opacity: 1;
}
/* Strong accent ring on the whole photo when selected. Paint it as a
foreground pseudo-element so the thumbnail cannot cover the feedback. */
.gallery-card:has(.gallery-select-dot.selected) {
border-color: var(--accent-primary, var(--red));
}
.gallery-card:has(.gallery-select-dot.selected)::after {
content: '';
position: absolute;
inset: 0;
z-index: 3;
border: 3px solid var(--accent-primary, var(--red));
border-radius: inherit;
box-sizing: border-box;
pointer-events: none;
}
.gallery-card:has(.gallery-select-dot.selected) .gallery-select-dot {
z-index: 4;
}
.gallery-detail-action.danger:hover {
border-color: var(--color-error, #e55);
}
.gallery-detail-image img,
.gallery-detail-image video,
.gallery-detail-img-frame img,
.gallery-detail-img-frame video {
max-width: 100%;
max-height: 100%;
border-radius: 6px;
object-fit: contain;
display: block;
}
.gallery-detail-image:hover .gallery-detail-nav { opacity: 0.85; }
@media (min-width: 769px) {
.gallery-modal-content:has(#gallery-detail[style*="flex"]) {
height: 92vh;
}
.gallery-modal-content:has(#gallery-detail[style*="flex"]) > .modal-body,
.gallery-images-container:has(> #gallery-detail[style*="flex"]),
#gallery-images-container:has(> #gallery-detail[style*="flex"]) > #gallery-detail {
height: 100%;
min-height: 0;
}
}
.gallery-detail-image:hover .gallery-detail-rotate { opacity: 0.85; }
.gallery-detail-nav-disabled {
opacity: 0 !important;
pointer-events: none;
}
@media (hover: none) {
.gallery-detail-nav { opacity: 0.7; }
}
.gallery-detail-inline-link:hover { text-decoration: underline; }
.gallery-detail-section {
margin-bottom: 12px;
}
.gallery-tag-input-wrap .gallery-tag-input { padding-right: 24px; }
.gallery-name-wrap:focus-within .gallery-name-enter { display: inline-flex; }
.gallery-name-wrap:focus-within .gallery-detail-name-input { padding-right: 28px; }
.gallery-album-cover img {
width: 100%; height: 100%;
object-fit: cover;
display: block;
}
.gallery-editor-landing p {
margin: 0;
opacity: 0.6;
font-size: 13px;
max-width: 320px;
}
.gallery-editor-landing-actions .gallery-select-btn:hover { opacity: 1; }
/* Saved-project selection uses the same bulk-bar rhythm as Photos. */
#gallery-editor-drafts-bulk {
margin-top: 0;
}
.gallery-editor-draft-card.selected {
outline: 2px solid var(--accent, var(--red));
outline-offset: -2px;
}
.gallery-editor-draft-card.select-mode { cursor: pointer; }
.gallery-drafts-view-toggle button svg {
display: block;
position: static;
transform: none;
}
.gallery-editor-drafts-grid.list-view {
display: flex;
flex-direction: column;
gap: 4px;
}
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-card {
display: grid;
grid-template-columns: 44px minmax(0, 1fr) 24px;
align-items: center;
gap: 9px;
min-height: 48px;
padding: 4px 6px;
}
.gallery-editor-drafts-grid.list-view .gallery-editor-draft-delete-x { display: none; }
.gallery-editor-draft-card:focus-visible {
outline: 2px solid var(--accent, var(--red));
outline-offset: 2px;
}
.gallery-editor-draft-card:hover .gallery-editor-draft-delete { opacity: 1; }
.ge-topbar.ge-topbar-menu-open { z-index: 10006; }
/* Toolbar (left) */
@keyframes ge-layer-action-flash {
from { box-shadow: inset 0 0 0 2px var(--red); background-color: color-mix(in srgb, var(--red) 24%, transparent); }
to { box-shadow: inset 0 0 0 2px transparent; }
}
.ge-layer-action-flash { animation: ge-layer-action-flash 650ms ease-out; }
@media (prefers-reduced-motion: reduce) {
.ge-layer-action-flash { animation-duration: 1ms; }
}
.ge-tool-btn .ge-tool-label {
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.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::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;
}
.ge-btn-ai:hover .ge-btn-ai-mark { opacity: 1; }
.ge-tool-btn.has-selection .ge-tool-clear { display: inline-flex; }
.ge-canvas-area.ge-rulers-visible {
padding: 18px 0 0 18px;
box-sizing: border-box;
}
.ge-main-canvas {
touch-action: none;
}
.ge-canvas-area.ge-pan-ready,
.ge-canvas-area.ge-pan-ready .ge-main-canvas {
cursor: grab !important;
}
.ge-canvas-area.ge-panning,
.ge-canvas-area.ge-panning .ge-main-canvas {
cursor: grabbing !important;
user-select: none;
}
.ge-ai-command-collapsed .ge-ai-command-toggle {
display: inline-flex;
}
.ge-ai-command-collapsed .ge-ai-command-head,
.ge-ai-command-collapsed .ge-ai-command-form,
.ge-ai-command-collapsed .ge-ai-command-suggestions,
.ge-ai-command-collapsed .ge-ai-command-status {
display: none;
}
@media (hover: none) {
.ge-ai-command-collapsed {
bottom: 8px;
opacity: 0.72;
}
}
.ge-ai-command-clear[hidden] {
display: none;
}
.ge-ai-command-busy .ge-ai-command-run::after {
content: '';
width: 12px;
height: 12px;
border: 2px solid color-mix(in srgb, #fff 42%, transparent);
border-top-color: #fff;
border-radius: 50%;
animation: ge-ai-command-spin 0.7s linear infinite;
}
@keyframes ge-ai-command-spin {
to { transform: rotate(360deg); }
}
.ge-ai-command-suggestions[hidden] {
display: none;
}
.ge-ai-command-status:empty {
display: none;
}
/* 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-quick-mask-bar[hidden] { display: none; }
.ge-marquee-dimensions[hidden] { display: none; }
.ge-layer-geometry-section .ge-section-title {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.ge-layer-geometry-grid input::-webkit-inner-spin-button,
.ge-layer-geometry-grid input::-webkit-outer-spin-button { appearance: none; }
#ge-brush-controls select,
#ge-eyedropper-section select {
height: 26px;
min-width: 0;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-gradient-extra-stop .ge-text-field { min-width: 0; }
.ge-shape-gradient-add-stop { align-self: flex-start; }
.ge-text-font-field { grid-column: span 1; }
#ge-text-rasterize { align-self: flex-end; }
.ge-shape-types .ge-text-toggle { flex: 1 1 25%; }
#ge-shape-rasterize { align-self: flex-end; }
.ge-direct-text-editor:focus { outline: 1px solid color-mix(in srgb, var(--red) 55%, transparent); }
.ge-transform-popup.ge-transform-popup-dragging .ge-transform-popup-head { 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-minimised .ge-transform-popup-body,
.ge-transform-popup-minimised .ge-transform-popup-hint {
display: none;
}
.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;
}
.ge-transform-position-field .ge-transform-popup-input { width: 68px; }
.ge-transform-input-locked {
opacity: 0.4;
cursor: not-allowed;
}
.ge-transform-input-locked + .ge-transform-spin { display: none; }
.ge-transform-preserve-field[hidden] { display: none; }
/* 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;
}
/* 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%;
}
.ge-color-picker.cp-swatch-input::selection { background: transparent; }
.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-eraser-row label > span[id$="-label"]:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.ge-eraser-row > span[id$="-label"].ge-slider-value:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
opacity: 1;
}
.ge-merge-menu[hidden] { display: none; }
/* 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; }
.ge-adj-popup .ge-adj-title { flex: 1 1 auto; }
.ge-adj-popup .ge-adj-head:active { cursor: grabbing; }
.ge-fx-dock-label { overflow: hidden; text-overflow: ellipsis; }
.ge-history-head:active { cursor: grabbing; }
@keyframes ge-canvas-spin { to { transform: rotate(360deg); } }
.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; }
.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-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-mask-sub-item.active .ge-layer-name { opacity: 1; font-style: normal; font-weight: 600; }
.ge-mask-sub-item.active .ge-mask-inline-thumb { opacity: 1; }
/* 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-mask-vis-btn.visible:hover { opacity: 1; }
.ge-topbar .ge-btn,
.ge-topbar .ge-btn-sm { text-transform: uppercase; }
.ge-stacked-btn .ge-stacked-glyph {
font-size: 14px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
height: 14px;
}
.ge-layer-selection-bar[hidden] { display: none; }
.ge-layer-selection-bar .ge-icon-btn:hover { opacity: 1; }
.ge-save-menu[hidden] { display: none; }
.ge-stacked-btn .ge-stacked-glyph .ge-save-state-icon[hidden] { display: none !important; }
.ge-stacked-btn .ge-stacked-glyph .ge-save-state-icon:not([hidden]) { display: block; }
.ge-resize-menu[hidden],
.ge-edge-menu[hidden],
.ge-view-menu[hidden],
.ge-image-menu[hidden],
.ge-selection-menu[hidden],
.ge-filter-menu[hidden] { display: none; }
.ge-selection-save-row .ge-icon-btn { width: 28px; height: 27px; }
.ge-export-matte .ge-color-picker { width: 28px; height: 28px; }
.ge-export-footer > div { display: flex; gap: 6px; }
.ge-export-estimate { opacity: 0.6; font-variant-numeric: tabular-nums; }
.ge-canvas-prompt-custom-label { margin-top: 14px; }
.ge-canvas-anchor::after {
content: '';
position: absolute;
inset: 6px;
border-radius: 1px;
background: color-mix(in srgb, var(--fg) 38%, transparent);
}
.ge-canvas-anchor.active::after { background: var(--red); }
.ge-canvas-lock-option input { accent-color: var(--red); }
.ge-canvas-preset-icon { margin-bottom: 1px; }
/* 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;
}
.ge-slider-bubble[hidden] { display: none; }
/* 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%);
}
.ge-layer-thumb canvas {
display: block;
border-radius: 4px;
}
.ge-layers-merge-menu[hidden] { display: none; }
.ge-layer-item:not(.active) .ge-layer-inline-thumb { opacity: 0.86; }
.ge-layer-opacity-row .ge-layer-lock-btn { flex: 0 0 24px; width: 24px; }
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-controls {
grid-column: 1 / -1;
grid-row: 3;
}
.ge-layer-item.grouped { padding-left: calc(20px + (var(--layer-group-depth, 1) - 1) * 14px); }
.ge-layer-vis.visible:hover { opacity: 1; }
.ge-layer-item.clipped .ge-layer-name {
display: flex;
align-items: center;
gap: 4px;
}
/* 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 .ge-mask-density {
width: 26px;
min-width: 24px;
height: 4px;
flex: 0 0 26px;
align-self: center;
}
.ge-mask-properties > summary {
box-sizing: border-box;
width: 22px;
height: 22px;
padding: 0;
min-height: 0;
display: inline-flex;
align-items: center;
justify-content: center;
list-style: none;
cursor: pointer;
color: var(--fg);
opacity: 0.4;
background: none;
border: none;
border-radius: 4px;
transition: opacity 0.1s;
}
.ge-mask-properties > summary::-webkit-details-marker { display: none; }
.ge-mask-properties > summary > svg { width: 12px; height: 12px; flex: none; }
.ge-mask-sub-item:has(.ge-mask-properties[open]) {
position: relative;
z-index: 50;
}
.ge-mask-property-row output { text-align: right; opacity: .7; }
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-controls,
.ge-layers-list > .ge-layer-group-row > .ge-layer-controls {
display: none;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item).active > .ge-layer-controls,
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item).active-parent > .ge-layer-controls,
.ge-layers-list > .ge-layer-group-row.active > .ge-layer-controls {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
padding-top: 4px;
border-top: 1px solid color-mix(in srgb, var(--border) 75%, transparent);
}
.ge-layer-tools[hidden] { display: none; }
.ge-layer-tools .ge-layer-btn { min-width: 26px; }
@media (max-width: 700px) {
/* Preserve the compact grip while giving long-press reorder a usable
touch target. */
.ge-layer-drag {
width: 28px;
min-height: 30px;
margin-left: -6px;
margin-right: -4px;
}
}
.ge-layer-align-menu .ge-layer-lock-option { grid-template-columns: minmax(0, 1fr); }
.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-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%;
}
/* 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-email-header:not(.doc-email-header-collapsed) .doc-email-collapse-summary {
opacity: 0.45;
}
.doc-email-header.doc-email-header-collapsed {
gap: 0;
padding-top: 5px;
padding-bottom: 5px;
}
.doc-email-header.doc-email-header-collapsed .doc-email-fields {
display: none;
}
/* 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-save-button[data-save-state="dirty"] .doc-save-state-dirty,
.doc-save-button[data-save-state="saving"] .doc-save-state-saving,
.doc-save-button[data-save-state="saved"] .doc-save-state-saved,
.doc-save-button[data-save-state="error"] .doc-save-state-error { display: block; }
.doc-save-button[data-save-state="saving"] .doc-save-state-saving {
animation: doc-save-spin 800ms linear infinite;
}
@keyframes doc-save-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
.doc-save-button[data-save-state="saving"] .doc-save-state-saving { animation: none; }
}
.doc-stats-popover[hidden] { display: none; }
.doc-stats-row strong {
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.doc-email-richbody:empty::before {
content: "Write your email\2026";
opacity: 0.4;
pointer-events: none;
}
@media (max-width: 768px) {
.doc-rich-empty-import {
top: 106px;
}
}
.doc-email-richbody.richtext-mode:empty::before {
content: "Start writing\2026";
}
.doc-email-richbody.richtext-mode ul,
.doc-email-richbody.richtext-mode ol {
padding-left: 1.75em;
}
.doc-email-richbody.richtext-mode ul:not(.rich-checklist) > li,
.doc-email-richbody.richtext-mode ol > li {
padding-inline-start: 0.2em;
}
.doc-email-richbody.richtext-mode pre code {
padding: 0;
border: 0;
background: transparent;
font: inherit;
}
.doc-email-richbody figure.richtext-image .richtext-image-caption {
margin-top: 6px;
max-width: 100%;
color: color-mix(in srgb, var(--fg) 68%, transparent);
font-size: 0.84em;
line-height: 1.4;
text-align: center;
overflow-wrap: anywhere;
}
.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 p { margin: 0.5em 0; }
#doc-email-more-btn svg { opacity: 0.8; }
@media (min-width: 769px) {
#doc-email-header #doc-email-collapse-btn.doc-email-collapse-btn {
display: none !important;
}
#doc-email-header.doc-email-header-collapsed .doc-email-fields {
display: flex !important;
}
}
.doclib-section-title-icon {
width: 14px;
height: 14px;
margin-right: 6px;
color: var(--accent, var(--red));
vertical-align: -2px;
}
File diff suppressed because it is too large Load Diff