/* 13-image-editor - part of the former static/style.css.
*
* The stylesheet is split by area, but the split is mechanical: rules were
* moved verbatim and NOT regrouped. The numeric prefix is the load order, and
* that order is what preserves the cascade. A rule sits in the file its
* original position fell into, so an area's rules are not all in its file and
* a file is not only that area's rules.
*
* Do not reorder the tags in static/index.html, and do not move a rule
* between files: with 1,151 redeclarations and 1,886 !important in this
* cascade, either can change which declaration wins.
*
* static/index.html and the PRECACHE list in static/sw.js must agree on this
* order. tests/test_static_stylesheet_manifest.py and
* tests/test_css_computed_style_snapshot.py fail if that drifts.
*/
.ge-draft-status[data-state="pending"],
.ge-draft-status[data-state="saving"] { color: color-mix(in srgb, var(--accent, var(--red)) 75%, var(--fg)); }
.ge-draft-status[data-state="saved"] { color: color-mix(in srgb, var(--color-success, #4ade80) 78%, var(--fg)); }
.ge-draft-status[data-state="error"] { color: var(--color-error, #ef4444); }
/* "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;
}
.ge-editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 34px;
padding: 3px 10px;
box-sizing: border-box;
flex: 0 0 auto;
border-top: 1px solid var(--border);
background: var(--panel);
}
.ge-editor-footer .ge-canvas-size {
min-width: 0;
font-size: 11px;
color: color-mix(in srgb, var(--fg) 65%, transparent);
}
.ge-footer-zoom {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
white-space: nowrap;
}
.ge-footer-zoom > .ge-btn { min-width: 28px; min-height: 26px; }
.ge-footer-zoom .ge-zoom-stack { flex-direction: row; gap: 5px; min-width: 56px; }
.ge-footer-zoom .ge-zoom-label { top: 0; left: 0; font-size: 11px; letter-spacing: 0; opacity: 0.8; }
.ge-footer-zoom .ge-zoom-glyph { left: 0; }
.ge-footer-zoom .ge-stacked-btn { min-width: 48px; }
.gallery-editor-container {
flex: 1;
min-height: 0;
display: none;
}
.gallery-editor-container input[type="number"] {
-moz-appearance: textfield;
appearance: textfield;
}
.gallery-editor-container input[type="number"]::-webkit-inner-spin-button,
.gallery-editor-container input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
appearance: none;
margin: 0;
}
.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: 64px;
flex-shrink: 0;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
.ge-toolbar::-webkit-scrollbar { display: none; }
.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 { 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.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. */
}
/* 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: 10px; 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;
}
/* 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-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-ruler {
position: absolute;
z-index: 16;
touch-action: none;
user-select: none;
}
.ge-ruler-horizontal {
top: 0;
left: 18px;
right: 0;
width: calc(100% - 18px);
height: 18px;
cursor: row-resize;
}
.ge-ruler-vertical {
top: 18px;
bottom: 0;
left: 0;
width: 18px;
height: calc(100% - 18px);
cursor: col-resize;
}
.ge-ruler-corner {
position: absolute;
top: 0;
left: 0;
z-index: 17;
width: 18px;
height: 18px;
background: var(--panel);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
box-sizing: border-box;
}
.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;
}
.ge-ai-command {
position: absolute;
left: 50%;
bottom: 14px;
z-index: 18;
width: min(430px, calc(100% - 28px));
transform: translateX(-50%);
padding: 7px;
box-sizing: border-box;
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
border-radius: 8px;
background: color-mix(in srgb, var(--panel, var(--bg)) 94%, transparent);
box-shadow: 0 8px 22px color-mix(in srgb, #000 26%, transparent);
backdrop-filter: blur(8px);
transition: bottom 0.16s ease, opacity 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease;
}
.ge-ai-command-toggle {
width: 100%;
height: 28px;
display: none;
align-items: center;
justify-content: center;
gap: 5px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--accent, var(--red));
font: inherit;
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.ge-ai-command-toggle-caret {
opacity: 0.7;
flex-shrink: 0;
}
.ge-ai-command-head {
display: flex;
align-items: center;
min-height: 24px;
padding: 0 2px 5px;
color: var(--fg);
}
.ge-ai-command-title {
margin-left: 5px;
font-size: 11px;
font-weight: 650;
}
.ge-ai-command-scope {
margin-left: 7px;
padding-left: 7px;
border-left: 1px solid var(--border);
color: var(--fg-muted);
font-size: 9px;
line-height: 1;
text-transform: uppercase;
}
.ge-ai-command-collapsed {
bottom: -16px;
width: auto;
min-width: 102px;
padding: 2px 9px;
opacity: 0.32;
border-color: color-mix(in srgb, var(--accent, var(--red)) 18%, var(--border));
box-shadow: 0 3px 10px color-mix(in srgb, #000 16%, transparent);
}
.ge-canvas-area:hover .ge-ai-command-collapsed,
.ge-ai-command-collapsed:hover,
.ge-ai-command-collapsed:focus-within {
bottom: 10px;
opacity: 0.94;
border-color: color-mix(in srgb, var(--accent, var(--red)) 42%, var(--border));
box-shadow: 0 8px 22px color-mix(in srgb, #000 24%, transparent);
}
.ge-ai-command-form {
display: flex;
align-items: center;
gap: 6px;
}
.ge-ai-command-input {
flex: 1 1 auto;
min-width: 0;
height: 32px;
box-sizing: border-box;
padding: 0 9px;
border: 1px solid var(--border);
border-radius: 8px;
background: color-mix(in srgb, var(--bg) 76%, #000 24%);
color: var(--fg);
font: inherit;
font-size: 12px;
}
.ge-ai-command-input:focus {
outline: none;
border-color: var(--accent, var(--red));
}
.ge-ai-command-clear {
position: relative;
top: -2px;
width: 26px;
height: 32px;
flex: 0 0 26px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: -38px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--fg-muted);
cursor: pointer;
padding: 0;
z-index: 1;
}
.ge-ai-command-clear:hover {
color: var(--fg);
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.ge-ai-command-run {
position: relative;
top: -2px;
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;
flex: 0 0 auto;
transition: background 0.25s, opacity 0.12s;
overflow: hidden;
}
.ge-ai-command-run:hover {
background: var(--send-btn-hover, color-mix(in srgb, var(--red) 80%, white));
}
.ge-ai-command-busy .ge-ai-command-run {
opacity: 0.58;
cursor: wait;
}
.ge-ai-command-busy .ge-ai-command-run svg {
display: none;
}
.ge-ai-command-close {
width: 28px;
height: 32px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--fg);
opacity: 0.52;
cursor: pointer;
padding: 0;
margin-left: auto;
}
.ge-ai-command-close:hover {
opacity: 0.9;
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.ge-ai-command-suggestions {
position: absolute;
left: 7px;
right: 7px;
bottom: calc(100% - 2px);
z-index: 2;
display: flex;
flex-direction: column;
gap: 2px;
max-height: 236px;
overflow: auto;
padding: 5px;
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 34%, var(--border));
border-radius: 8px;
background: color-mix(in srgb, var(--panel, var(--bg)) 97%, #000 3%);
box-shadow: 0 10px 28px color-mix(in srgb, #000 28%, transparent);
}
.ge-ai-command-suggestion {
width: 100%;
display: grid;
grid-template-columns: 38px minmax(82px, auto) minmax(0, 1fr);
align-items: center;
gap: 8px;
min-height: 28px;
padding: 4px 7px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--fg);
text-align: left;
cursor: pointer;
font: inherit;
}
.ge-ai-command-suggestion:hover,
.ge-ai-command-suggestion.active {
background: color-mix(in srgb, var(--accent, var(--red)) 16%, transparent);
}
.ge-ai-command-suggestion-kind {
min-width: 0;
overflow: hidden;
color: var(--accent, var(--red));
font-size: 8px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.ge-ai-command-suggestion-main {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
font-weight: 650;
}
.ge-ai-command-suggestion-hint {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--fg-muted);
font-size: 10px;
}
.ge-ai-command-status {
margin-top: 4px;
font-size: 10px;
line-height: 1.3;
color: var(--fg-muted);
}
.ge-ai-command-status[data-kind="running"] {
color: var(--accent, var(--red));
}
.ge-ai-command-status[data-kind="done"] {
color: var(--green, #4caf50);
}
.ge-ai-command-status[data-kind="error"] {
color: var(--red, #e5484d);
}
/* 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;
}
.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;
}
.ge-selection-overlay {
position: absolute;
image-rendering: pixelated;
pointer-events: none;
z-index: 4;
}
.ge-quick-mask-bar {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
z-index: 18;
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 4px 5px 4px 10px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel);
box-shadow: 0 4px 16px rgba(0,0,0,0.24);
color: var(--fg);
font-size: 11px;
white-space: nowrap;
}
.ge-quick-mask-label { font-weight: 600; }
.ge-quick-mask-tools { display: flex; }
.ge-quick-mask-tool,
.ge-quick-mask-done {
min-height: 24px;
padding: 2px 8px;
border: 1px solid var(--border);
background: var(--bg);
color: inherit;
font: inherit;
cursor: pointer;
}
.ge-quick-mask-tool:first-child { border-radius: 4px 0 0 4px; }
.ge-quick-mask-tool:last-child { border-radius: 0 4px 4px 0; border-left: 0; }
.ge-quick-mask-tool.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.ge-quick-mask-done { border-radius: 4px; }
.ge-marquee-constraint-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 5px 7px;
font-size: 10px;
}
.ge-marquee-constraint-row > select {
min-width: 0;
height: 26px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-marquee-dimensions {
grid-column: 1 / -1;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 26px;
gap: 5px;
align-items: center;
}
.ge-marquee-dimensions label {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 4px;
}
.ge-marquee-dimensions input {
width: 100%;
min-width: 0;
height: 26px;
padding: 2px 5px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-marquee-dimensions .ge-icon-btn { width: 26px; height: 26px; }
/* Right panel */
.ge-right-panel {
width: 250px;
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;
}
.ge-layer-geometry-section {
display: flex;
flex-direction: column;
gap: 6px;
}
#ge-layer-geometry-name {
margin-left: auto;
min-width: 0;
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-muted);
font-size: 9px;
font-weight: 400;
text-transform: none;
}
.ge-layer-geometry-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 5px;
}
.ge-layer-geometry-grid label {
display: grid;
grid-template-columns: 12px minmax(0, 1fr);
align-items: center;
gap: 2px;
min-width: 0;
color: var(--color-muted);
font-size: 9px;
}
.ge-layer-geometry-grid input {
width: 100%;
height: 26px;
box-sizing: border-box;
min-width: 0;
padding: 3px 4px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
text-align: right;
appearance: textfield;
}
.ge-layer-geometry-grid input:focus { outline: none; border-color: var(--red); }
.ge-layer-geometry-grid input[readonly] { opacity: 0.58; cursor: default; }
@media (max-width: 700px) {
.ge-layer-geometry-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
/* 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: 9px;
}
.ge-brush-pressure-row { justify-content: space-between; gap: 6px; }
.ge-pressure-option { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; opacity: 0.7; }
.ge-pressure-option .toggle-switch { width: 24px; height: 14px; }
.ge-pressure-option .toggle-slider::before { width: 10px; height: 10px; }
.ge-pressure-option .toggle-switch input:checked + .toggle-slider::before { transform: translateX(10px); }
.ge-brush-preset-row select { flex: 1 1 auto; min-width: 0; }
.ge-brush-preset-row .ge-btn { flex: 0 0 auto; }
.ge-text-section,
.ge-shape-section {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.ge-shape-gradient-extra-stops {
display: flex;
flex-direction: column;
gap: 6px;
}
.ge-gradient-extra-stop {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
gap: 6px;
align-items: end;
}
.ge-gradient-stop-remove {
min-width: 28px;
height: 28px;
padding: 0 6px;
}
.ge-text-section textarea,
.ge-text-section select,
.ge-text-section input[type="number"],
.ge-shape-section input[type="number"] {
box-sizing: border-box;
min-width: 0;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-text-section textarea {
width: 100%;
min-height: 58px;
resize: vertical;
padding: 6px 7px;
line-height: 1.35;
}
.ge-text-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(56px, 0.42fr) auto;
gap: 6px;
align-items: end;
}
.ge-text-field {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
font-size: 9px;
color: var(--color-muted);
}
.ge-text-field select,
.ge-text-field input { width: 100%; height: 28px; padding: 3px 6px; }
.ge-text-format-row { gap: 4px; }
.ge-text-toggle {
width: auto;
min-width: 24px;
flex: 1 1 24px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
color: var(--fg);
cursor: pointer;
}
.ge-text-toggle:hover { background: color-mix(in srgb, var(--fg) 7%, transparent); }
.ge-text-toggle.active {
color: var(--red);
border-color: color-mix(in srgb, var(--red) 55%, var(--border));
background: color-mix(in srgb, var(--red) 12%, transparent);
}
.ge-text-format-sep { width: 1px; height: 20px; background: var(--border); margin: 0 2px; }
.ge-shape-types { display: flex; gap: 4px; }
.ge-shape-sides-field { max-width: 90px; }
.ge-direct-text-editor {
position: fixed;
z-index: 10020;
box-sizing: border-box;
padding: 4px;
overflow: hidden;
resize: both;
border: 1px dashed var(--red);
outline: 1px solid color-mix(in srgb, var(--red) 24%, transparent);
border-radius: 2px;
background: color-mix(in srgb, var(--bg) 8%, transparent);
caret-color: var(--red);
transform-origin: top left;
box-shadow: none;
}
/* 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: 10020;
display: flex;
flex-direction: column;
gap: 4px;
padding: 0 0 8px;
background: color-mix(in srgb, var(--panel) 98%, var(--bg));
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 42%, 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-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; }
.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-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;
}
/* 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); }
/* 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);
}
/* 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;
}
.ge-transform-preserve-field {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 0 12px;
font-size: 10px;
color: var(--fg-muted);
cursor: pointer;
}
.ge-transform-preserve-field input {
accent-color: var(--accent, var(--red));
margin: 0;
}
/* 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;
}
.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;
}
/* attachColorPicker swaps the native for a styled
text input with the same `.ge-color-picker` class plus
`.cp-swatch-input`. Force the 24×24 circular swatch back on it so
it doesn't render as a wide text box stretched across the row. */
.ge-color-picker.cp-swatch-input {
width: 24px;
height: 24px;
flex: 0 0 24px;
border: 1px solid var(--border);
border-radius: 50%;
padding: 0;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
/* Hide the (read-only) text caret + selection so the element reads
as a swatch, not a text field. */
color: transparent;
text-shadow: none;
caret-color: transparent;
font-size: 0;
user-select: none;
}
.ge-color-picker.cp-swatch-input: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-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;
}
/* 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;
}
/* 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 .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;
}
/* 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); }
.ge-add-layer-menu {
position: fixed;
z-index: 10004;
width: 218px;
padding: 5px;
border-radius: 7px;
display: flex;
flex-direction: column;
gap: 1px;
}
.ge-add-layer-menu button {
height: 30px;
padding: 0 9px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--fg);
display: flex;
align-items: center;
gap: 8px;
text-align: left;
font: inherit;
font-size: 11px;
cursor: pointer;
}
.ge-add-layer-menu button:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
.ge-add-layer-symbol {
width: 16px;
display: inline-flex;
justify-content: center;
color: var(--accent, var(--red));
font-size: 13px;
}
.ge-add-layer-divider { height: 1px; margin: 4px 5px; background: color-mix(in srgb, var(--fg) 10%, transparent); }
/* 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-head-btns { margin-left: auto; }
.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-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-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; }
/* 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;
}
.memory-item-focus {
animation: anchor-item-flash 2.2s ease-out;
outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 45%, transparent);
outline-offset: 2px;
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;
}
/* 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-preset-field {
display: grid;
grid-template-columns: 46px 1fr;
align-items: center;
gap: 8px;
margin: 8px 12px 0;
color: color-mix(in srgb, var(--fg) 65%, transparent);
font-size: 10px;
}
.ge-adj-preset-select {
min-width: 0;
width: 100%;
padding: 5px 7px;
border: 1px solid var(--border);
border-radius: 6px;
background: color-mix(in srgb, var(--panel) 90%, transparent);
color: var(--fg);
font: inherit;
}
.ge-adj-preset-select:focus { outline: none; border-color: var(--accent, var(--red)); }
.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);
}
.ge-adj-foot-spacer { flex: 1 1 auto; }
.ge-adj-compare-btn.is-active {
color: var(--accent, var(--red));
border-color: color-mix(in srgb, var(--accent, var(--red)) 65%, transparent);
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
}
.ge-adj-channel-field {
display: grid;
grid-template-columns: 64px 1fr;
align-items: center;
gap: 8px;
margin: 0 0 8px;
color: color-mix(in srgb, var(--fg) 65%, transparent);
font-size: 10px;
}
.ge-adj-channel-select,
.ge-adj-selective-range {
width: 100%;
min-width: 0;
height: 27px;
padding: 0 25px 0 7px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--panel);
color: var(--fg);
font: inherit;
}
.ge-gradient-map-preview {
height: 24px;
margin-bottom: 9px;
border: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
border-radius: 4px;
background: linear-gradient(90deg, var(--ge-gradient-start), var(--ge-gradient-end));
}
.ge-gradient-color-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 8px;
}
.ge-gradient-color-row label {
display: grid;
grid-template-columns: 1fr 30px;
align-items: center;
gap: 6px;
font-size: 10px;
opacity: .75;
}
.ge-gradient-color-row input[type="color"] {
width: 30px;
height: 24px;
padding: 1px;
border: 1px solid var(--border);
border-radius: 4px;
background: transparent;
}
.ge-adj-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 28px;
font-size: 10px;
color: color-mix(in srgb, var(--fg) 70%, transparent);
}
.ge-adj-toggle-row input { accent-color: var(--red); }
.ge-curves-wrap {
width: 100%;
aspect-ratio: 14 / 9;
border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
background: #0b0d10;
overflow: hidden;
}
.ge-curves-canvas { display: block; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
.ge-adj-help { margin: 7px 0 0; font-size: 9px; line-height: 1.35; opacity: .5; }
.ge-adjustment-layer-item .ge-layer-adjustment-marker { color: var(--accent, var(--red)); }
.ge-adjustment-layer-item .ge-layer-adjustment-marker svg { width: 12px; height: 12px; }
/* 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;
}
/* Let the slider consume the space left after the label and value chip.
A fixed 160px track overflows the 320px phone sheet once the popup's
horizontal padding and controls are included. */
.ge-adj-row > label {
flex: 0 0 clamp(72px, 26vw, 96px);
min-width: 0;
}
.ge-adj-row input[type="range"] {
flex: 1 1 auto;
width: auto;
min-width: 56px;
max-width: none;
}
}
@media (max-width: 360px) {
/* Give Gradient Map's color labels and swatches a usable tap width on
narrow phones; the popup remains scrollable if the extra row is needed. */
.ge-gradient-color-row {
grid-template-columns: 1fr;
gap: 6px;
}
}
.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-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[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-clip-status {
margin-top: 3px;
color: var(--fg-muted);
font-size: 10px;
line-height: 1.25;
font-variant-numeric: tabular-nums;
}
.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-clone-source-status {
display: flex;
align-items: center;
gap: 6px;
min-height: 24px;
margin: 2px 0 5px;
padding: 0 6px;
color: var(--fg);
opacity: 0.68;
font-size: 10px;
}
.ge-clone-source-status span { flex: 1; min-width: 0; }
.ge-clone-source-status .ge-icon-btn { margin-left: auto; }
.ge-mask-inline-thumb {
width: 34px;
height: 26px;
flex: 0 0 34px;
opacity: 0.78;
}
.ge-eraser-preview {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--fg);
flex-shrink: 0;
display: inline-block;
position: relative;
}
.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; }
/* 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 svg { display: block; }
.ge-stacked-btn .ge-stacked-caret {
margin-left: 2px;
font-size: 7px;
line-height: 1;
opacity: 0.55;
}
.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; }
.ge-layer-blend-row {
display: grid;
grid-template-columns: 38px minmax(0, 1fr);
align-items: center;
gap: 6px;
padding: 4px 7px;
border-bottom: 1px solid color-mix(in srgb, var(--fg) 8%, transparent);
font-size: 10px;
color: color-mix(in srgb, var(--fg) 62%, transparent);
}
.ge-layer-blend-row select {
width: 100%;
min-width: 0;
height: 25px;
padding: 0 24px 0 7px;
border-radius: 4px;
font-size: 10px;
}
.ge-layer-selection-bar {
min-height: 30px;
display: flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-bottom: 1px solid var(--border);
background: color-mix(in srgb, var(--accent, var(--red)) 8%, var(--bg-secondary));
color: var(--fg);
font-size: 10px;
}
.ge-layer-selection-bar > span { flex: 1; opacity: 0.72; }
.ge-layer-selection-bar .ge-icon-btn {
width: 24px;
height: 24px;
min-width: 24px;
border: 0;
background: transparent;
color: inherit;
opacity: 0.72;
}
.ge-layer-selection-bar .ge-icon-btn.danger:hover { color: var(--red); }
/* 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;
flex-shrink: 0;
}
.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-state-icon { display: none; }
.ge-save-label { white-space: nowrap; min-width: 0; }
.ge-save-state-saved { color: var(--fg); }
.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-view-wrap,
.ge-image-wrap,
.ge-selection-wrap,
.ge-filter-wrap { position: relative; display: inline-block; }
.ge-resize-menu,
.ge-edge-menu,
.ge-view-menu,
.ge-image-menu,
.ge-selection-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-view-menu,
.ge-image-menu,
.ge-selection-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-view-menu { min-width: 174px; }
.ge-image-menu { min-width: 180px; }
.ge-selection-menu { min-width: 224px; }
.ge-filter-menu { min-width: 180px; }
.ge-resize-menu .dropdown-item-compact,
.ge-view-menu .dropdown-item-compact,
.ge-image-menu .dropdown-item-compact,
.ge-selection-menu .dropdown-item-compact,
.ge-filter-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
}
.ge-view-toggle .ge-view-check { opacity: 0; }
.ge-view-toggle.active .ge-view-check { opacity: 1; color: var(--red); }
.ge-view-grid-size {
display: grid;
grid-template-columns: 1fr 52px auto;
align-items: center;
gap: 5px;
padding: 4px 8px;
font-size: 10px;
color: var(--color-muted);
}
.ge-view-grid-size input {
width: 52px;
height: 24px;
box-sizing: border-box;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
text-align: right;
}
/* 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-selection-menu .dropdown-item-compact:disabled,
.ge-filter-menu .dropdown-item-compact:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ge-selection-save-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 28px;
gap: 5px;
padding: 3px 4px;
}
.ge-selection-save-row input {
min-width: 0;
height: 27px;
padding: 3px 7px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-saved-selection-list { display: flex; flex-direction: column; gap: 1px; }
.ge-saved-selection-row { display: grid; grid-template-columns: minmax(0, 1fr) 26px; align-items: center; }
.ge-saved-selection-load,
.ge-saved-selection-delete {
min-width: 0;
height: 26px;
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.ge-saved-selection-load { padding: 3px 8px; overflow: hidden; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
.ge-saved-selection-delete { opacity: 0.45; font-size: 15px; }
.ge-saved-selection-load:hover,
.ge-saved-selection-delete:hover { background: var(--hover); opacity: 1; }
.ge-saved-selection-empty { padding: 4px 8px 6px; font-size: 10px; opacity: 0.45; }
/* 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-row input[type="color"] {
width: 100%;
height: 24px;
padding: 1px;
border: 1px solid var(--border, #333);
border-radius: 4px;
background: transparent;
cursor: pointer;
}
.ge-filter-modal-actions {
display: flex;
gap: 6px;
justify-content: flex-end;
margin-top: 4px;
}
/* Controlled image export. */
.ge-export-overlay {
position: fixed;
inset: 0;
z-index: 10020;
display: grid;
place-items: center;
padding: 16px;
background: rgba(0, 0, 0, 0.56);
}
.ge-export-dialog {
width: min(720px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
background: var(--panel);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 18px 54px rgba(0, 0, 0, 0.48);
}
.ge-export-head,
.ge-export-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
}
.ge-export-head { border-bottom: 1px solid var(--border); }
.ge-export-head h2 {
margin: 0;
font-size: 13px;
letter-spacing: 0;
}
.ge-export-close {
border: 0;
background: transparent;
color: var(--fg);
width: 28px;
height: 28px;
padding: 0;
font-size: 20px;
line-height: 1;
cursor: pointer;
opacity: 0.6;
}
.ge-export-close:hover { opacity: 1; }
.ge-export-body {
display: grid;
grid-template-columns: minmax(260px, 1.25fr) minmax(220px, 0.75fr);
gap: 14px;
padding: 14px;
}
.ge-export-preview-wrap {
min-height: 300px;
display: grid;
place-items: center;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 6px;
background-color: #22262d;
background-image:
linear-gradient(45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255,255,255,0.06) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255,255,255,0.06) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255,255,255,0.06) 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
}
.ge-export-preview {
display: block;
max-width: calc(100% - 24px);
max-height: 420px;
box-shadow: 0 4px 18px rgba(0,0,0,0.38);
}
.ge-export-fields {
display: flex;
flex-direction: column;
gap: 10px;
}
.ge-export-field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
font-size: 10px;
opacity: 0.78;
}
.ge-export-field > span {
display: flex;
justify-content: space-between;
align-items: center;
}
.ge-export-field input[type="text"],
.ge-export-field input[type="number"] {
width: 100%;
box-sizing: border-box;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 7px 8px;
font: inherit;
font-size: 11px;
}
.ge-export-field input:focus { outline: none; border-color: var(--red); }
.ge-export-format {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1px solid var(--border);
border-radius: 5px;
overflow: hidden;
}
.ge-export-format button {
min-height: 30px;
border: 0;
border-right: 1px solid var(--border);
background: var(--bg);
color: var(--fg);
font: inherit;
font-size: 10px;
cursor: pointer;
}
.ge-export-format button:last-child { border-right: 0; }
.ge-export-format button.active {
background: color-mix(in srgb, var(--red) 18%, var(--bg));
color: var(--red);
}
.ge-export-dimensions {
display: grid;
grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr);
gap: 5px;
align-items: end;
}
.ge-export-link {
width: 28px;
height: 29px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
color: var(--fg);
opacity: 0.45;
cursor: pointer;
}
.ge-export-link.active { opacity: 1; color: var(--red); border-color: var(--red); }
.ge-export-quality input { width: 100%; accent-color: var(--red); }
.ge-export-surface-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
border-top: 1px solid var(--border);
padding-top: 10px;
}
.ge-export-toggle,
.ge-export-matte {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
}
.ge-export-matte.disabled { opacity: 0.35; pointer-events: none; }
.ge-export-footer {
border-top: 1px solid var(--border);
font-size: 10px;
}
@media (max-width: 700px) {
.ge-export-overlay { padding: 8px; place-items: end center; }
.ge-export-dialog { width: 100%; max-height: calc(100dvh - 16px); }
.ge-export-body { grid-template-columns: 1fr; padding: 10px; }
.ge-export-preview-wrap { min-height: 190px; max-height: 260px; }
.ge-export-preview { max-height: 230px; }
}
.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: 430px;
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;
position: relative;
top: 8px;
}
.ge-canvas-prompt-hint {
margin: 0 auto 0 0;
font-size: 11px;
opacity: 0.5;
line-height: 1.35;
text-align: left;
}
.ge-canvas-prompt-section-label {
display: flex;
align-items: center;
gap: 5px;
margin: 2px 0 7px;
font-size: 10px;
font-weight: 650;
letter-spacing: .04em;
text-transform: uppercase;
opacity: .62;
}
.ge-canvas-units-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 8px 0 2px;
font-size: 11px;
opacity: .75;
}
.ge-canvas-control-label {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
}
.ge-canvas-prompt-icon,
.ge-canvas-preset-icon {
flex: 0 0 auto;
opacity: .68;
}
.ge-canvas-units-field select,
.ge-canvas-interpolation-field select {
min-width: 112px;
padding: 5px 7px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font: inherit;
}
.ge-canvas-anchor-label {
margin: 14px 0 6px;
font-size: 10px;
font-weight: 650;
letter-spacing: .04em;
text-transform: uppercase;
opacity: .62;
text-align: center;
}
.ge-canvas-anchor-grid {
display: grid;
grid-template-columns: repeat(3, 22px);
gap: 5px;
justify-content: center;
}
.ge-canvas-anchor {
width: 22px;
height: 22px;
padding: 0;
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
border-radius: 4px;
background: color-mix(in srgb, var(--fg) 6%, transparent);
cursor: pointer;
position: relative;
}
.ge-canvas-anchor:hover,
.ge-canvas-anchor.active {
border-color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
}
.ge-canvas-resize-options {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
}
.ge-canvas-lock-option {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
opacity: .72;
cursor: pointer;
}
.ge-canvas-interpolation-field {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
opacity: .72;
}
.ge-canvas-interpolation-field select {
min-width: 92px;
padding: 4px 6px;
border: 1px solid var(--border);
border-radius: 5px;
background: var(--bg);
color: var(--fg);
font: inherit;
}
.ge-canvas-preset-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
}
.ge-canvas-preset {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
min-width: 0;
padding: 8px 9px;
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
border-radius: 6px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
color: var(--fg);
font: inherit;
text-align: left;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
}
.ge-canvas-preset:hover,
.ge-canvas-preset.active {
border-color: var(--red);
background: color-mix(in srgb, var(--red) 12%, transparent);
}
.ge-canvas-preset span,
.ge-canvas-preset strong {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ge-canvas-preset span { font-size: 11px; font-weight: 600; }
.ge-canvas-preset strong { font-size: 10px; font-weight: 500; opacity: .62; }
.ge-canvas-prompt-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
padding-top: 8px;
border-top: 1px solid var(--border);
margin-top: 8px;
}
.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);
}
/* 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.visible { opacity: 1; }
/* 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-layers-header {
display: flex;
flex-wrap: wrap;
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-header #ge-add-layer { flex: 0 0 auto; white-space: nowrap; }
.ge-layer-header-tools {
display: flex;
flex: 0 0 100%;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.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 .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;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 0 0 6px;
}
.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);
}
.ge-layer-item:focus-visible,
.ge-layer-group-row:focus-visible {
outline: 1px solid var(--accent, var(--red));
outline-offset: -1px;
}
@media (min-width: 701px) {
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) {
display: grid;
grid-template-columns: 12px 22px 34px minmax(0, 1fr) 24px;
row-gap: 5px;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-drag {
grid-column: 1;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-vis {
grid-column: 2;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-inline-thumb {
grid-column: 3;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name,
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name-input {
grid-column: 4;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-lock-btn {
grid-column: 5;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
grid-column: 1 / -1;
grid-row: 2;
width: 100%;
}
.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;
justify-content: flex-end;
min-width: 0;
}
}
.ge-layer-inline-thumb {
display: block;
width: 34px;
height: 26px;
flex: 0 0 34px;
object-fit: contain;
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
border-radius: 3px;
background: #303030;
image-rendering: auto;
}
/* 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(--accent, var(--red)) 52%, var(--bg-secondary, var(--bg)));
border-left-color: var(--accent, var(--red));
box-shadow: inset 3px 0 0 var(--accent, var(--red)), inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 80%, transparent);
}
.ge-layer-item.active > .ge-layer-name,
.ge-layer-group-row.active > .ge-layer-group-name { font-weight: 700; }
/* "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-item.selected:not(.active):not(.active-parent) {
background: color-mix(in srgb, var(--accent, var(--red)) 22%, var(--bg));
box-shadow: inset 2px 0 0 color-mix(in srgb, var(--accent, var(--red)) 55%, transparent);
}
.ge-layer-group-row {
min-height: 36px;
display: grid;
grid-template-columns: 12px 18px 24px 34px minmax(0, 1fr) 24px;
grid-template-rows: auto auto auto;
align-items: center;
gap: 3px;
padding: 3px 7px 3px calc(7px + var(--group-depth, 0) * 14px);
border-bottom: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 78%, var(--bg));
color: var(--fg);
width: 100%;
box-sizing: border-box;
}
.ge-layer-group-row .ge-layer-group-drag { grid-column: 1; grid-row: 1; }
.ge-layer-group-row .ge-layer-group-toggle { grid-column: 2; grid-row: 1; }
.ge-layer-group-row .ge-layer-vis { grid-column: 3; grid-row: 1; }
.ge-layer-group-row .ge-group-inline-thumb { grid-column: 4; grid-row: 1; }
.ge-layer-group-row .ge-layer-group-name { grid-column: 5; grid-row: 1; }
.ge-layer-group-row .ge-layer-lock-btn { grid-column: 6; grid-row: 1; }
.ge-layer-group-row .ge-layer-opacity-row {
grid-column: 1 / -1;
grid-row: 2;
display: flex;
width: 100%;
min-width: 0;
}
.ge-layer-group-row .ge-layer-opacity-row .ge-layer-opacity {
flex: 1 1 auto;
width: 100%;
min-width: 0;
}
.ge-layer-opacity-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.ge-layer-opacity-row .ge-layer-opacity { flex: 1 1 auto; width: auto; }
.ge-layer-group-row .ge-layer-controls {
grid-column: 1 / -1;
grid-row: 3;
justify-content: flex-end;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
grid-column: 1 / -1;
grid-row: 2;
width: 100%;
}
.ge-layer-group-drag { touch-action: none; }
.ge-layer-group-row.group-dragging {
opacity: 0.62;
outline: 1px solid color-mix(in srgb, var(--accent, var(--red)) 65%, transparent);
}
.ge-group-drop-line {
position: absolute;
z-index: 20;
left: 6px;
right: 6px;
height: 2px;
pointer-events: none;
background: var(--accent, var(--red));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--bg) 65%, transparent);
}
.ge-layer-group-row.active {
background: color-mix(in srgb, var(--accent, var(--red)) 38%, var(--bg-secondary));
box-shadow: inset 2px 0 0 var(--accent, var(--red)), inset 0 0 0 1px color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
}
.ge-layer-group-toggle {
width: 18px;
height: 22px;
padding: 0;
border: 0;
background: transparent;
color: inherit;
opacity: 0.62;
}
.ge-layer-group-toggle svg { transition: transform 120ms ease; }
.ge-layer-group-toggle.expanded svg { transform: rotate(90deg); }
.ge-layer-group-name {
min-width: 0;
display: flex;
align-items: center;
gap: 5px;
overflow: hidden;
font-size: 11px;
font-weight: 600;
}
.ge-layer-group-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ge-group-mask-sub-item { padding-left: calc(20px + var(--group-mask-depth, 1) * 14px); }
.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-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.ge-layer-clipped-marker {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
color: var(--accent, var(--red));
opacity: 0.82;
}
.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;
}
.ge-layer-controls {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.ge-layer-controls .ge-mask-inspect-btn.active {
color: var(--accent, var(--red));
background: color-mix(in srgb, var(--accent, var(--red)) 14%, transparent);
}
.ge-mask-properties {
position: relative;
flex: 0 0 22px;
}
.ge-mask-properties > summary:hover,
.ge-mask-properties[open] > summary {
opacity: 0.8;
}
.ge-mask-properties[open] > summary { color: var(--accent, var(--red)); opacity: 0.95; }
.ge-mask-properties-panel {
position: fixed;
box-sizing: border-box;
margin: 0;
bottom: auto;
overflow: auto;
color: var(--fg);
z-index: 9999;
right: auto;
top: 0;
width: 190px;
padding: 8px;
display: grid;
gap: 7px;
border: 1px solid color-mix(in srgb, var(--border) 85%, transparent);
border-radius: 5px;
background: var(--bg-secondary, var(--bg));
box-shadow: 0 8px 24px rgba(0,0,0,.28);
}
.ge-mask-properties-panel:not(:popover-open) { display: none; }
.ge-mask-property-row {
display: grid;
grid-template-columns: 52px minmax(0, 1fr) 35px;
align-items: center;
gap: 6px;
font-size: 10px;
}
.ge-mask-property-row input { min-width: 0; width: 100%; }
.ge-mask-apply-btn {
width: 100%;
min-height: 26px;
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 55%, var(--border));
border-radius: 4px;
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
color: var(--fg);
cursor: pointer;
font-size: 10px;
}
.ge-mask-apply-btn:hover { background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent); }
.ge-layer-tools {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2px;
min-height: 34px;
padding: 3px 7px;
border-top: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
background: color-mix(in srgb, var(--bg-secondary) 72%, var(--bg));
}
.ge-layer-tools .ge-layer-controls {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
width: 100%;
}
.ge-eyedropper-live {
display: flex;
align-items: center;
gap: 7px;
min-height: 28px;
padding: 3px 6px;
border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
border-radius: 5px;
background: color-mix(in srgb, var(--fg) 4%, transparent);
}
.ge-eyedropper-loupe {
width: 42px;
height: 42px;
flex: 0 0 42px;
border-radius: 4px;
image-rendering: pixelated;
background: var(--bg);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 22%, transparent);
}
.ge-eyedropper-live-swatch {
width: 20px;
height: 20px;
flex: 0 0 20px;
border-radius: 4px;
background: transparent;
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--fg) 22%, transparent);
}
.ge-eyedropper-live-values {
display: grid;
gap: 1px;
min-width: 0;
font-size: 10px;
line-height: 1.25;
}
.ge-eyedropper-live-values > span { opacity: .62; font-variant-numeric: tabular-nums; }
.ge-eyedropper-live-swatch.empty {
background: repeating-linear-gradient(135deg, transparent 0 4px, color-mix(in srgb, var(--fg) 18%, transparent) 4px 6px);
}
.ge-eyedropper-live code {
font-size: 11px;
font-variant-numeric: tabular-nums;
opacity: .75;
}
/* 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;
touch-action: none;
user-select: none;
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.active { color: var(--accent, var(--red)); opacity: 0.95; }
.ge-layer-btn:disabled { cursor: default; opacity: 0.16; }
.ge-layer-style-btn { font-style: italic; font-weight: 700; font-size: 13px; }
.ge-layer-style-menu {
animation: none;
position: fixed;
inset: auto;
margin: 0;
padding: 4px;
min-width: 170px;
max-width: calc(100vw - 12px);
max-height: calc(100dvh - 12px);
overflow-y: auto;
box-sizing: border-box;
background: var(--panel, var(--bg));
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
box-shadow: 0 8px 24px #0005;
}
.ge-layer-style-menu:not(:popover-open) { display: none; }
.ge-layer-style-menu:popover-open { display: block; }
.ge-layer-style-menu > button { width: 100%; border: 0; background: none; font: inherit; text-align: left; }
.ge-layer-btn.danger:hover { color: var(--red); opacity: 1; }
.ge-layer-lock-menu {
position: fixed;
z-index: 10004;
width: 176px;
padding: 4px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--panel, var(--bg));
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}
.ge-layer-lock-option {
width: 100%;
height: 30px;
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
align-items: center;
gap: 6px;
padding: 0 8px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--fg);
font: inherit;
font-size: 11px;
text-align: left;
cursor: pointer;
}
.ge-layer-lock-option:hover,
.ge-layer-lock-option:focus-visible {
outline: none;
background: color-mix(in srgb, var(--fg) 8%, transparent);
}
.ge-layer-align-menu { width: 218px; }
.ge-layer-lock-check {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid color-mix(in srgb, var(--fg) 28%, transparent);
border-radius: 3px;
color: var(--accent, var(--red));
font-size: 11px;
line-height: 1;
}
@media (max-width: 700px) {
.ge-layer-lock-menu { width: auto; }
.ge-layer-lock-option { height: 44px; font-size: 13px; }
}
/* 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-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: 2147483647;
border: 1.5px solid #fff; border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0,0,0,0.72);
display: none;
box-sizing: border-box;
}
.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;
}
.ge-layers-list {
overflow-x: clip;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) {
display: grid;
grid-template-columns: 12px 40px 34px minmax(0, 1fr) 40px;
grid-template-rows: 40px auto 40px;
align-items: center;
row-gap: 3px;
}
.ge-layers-list > .ge-layer-group-row {
grid-template-columns: 12px 40px 40px 34px minmax(0, 1fr) 40px;
grid-template-rows: 40px auto 40px;
align-items: center;
row-gap: 3px;
}
.ge-layer-group-row .ge-layer-group-drag { grid-column: 1; grid-row: 1; }
.ge-layer-group-row .ge-layer-group-toggle { grid-column: 2; grid-row: 1; }
.ge-layer-group-row .ge-layer-vis { grid-column: 3; grid-row: 1; }
.ge-layer-group-row .ge-group-inline-thumb { grid-column: 4; grid-row: 1; }
.ge-layer-group-row .ge-layer-group-name { grid-column: 5; grid-row: 1; }
.ge-layer-group-row .ge-layer-lock-btn { grid-column: 6; grid-row: 1; }
.ge-layer-group-row .ge-layer-opacity-row {
grid-column: 1 / -1;
grid-row: 2;
margin-left: 52px;
width: auto;
}
.ge-layer-group-row .ge-layer-controls {
grid-column: 1 / -1;
grid-row: 3;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-drag {
grid-column: 1;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-vis {
grid-column: 2;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-inline-thumb {
grid-column: 3;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name,
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-name-input {
grid-column: 4;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-lock-btn {
grid-column: 5;
grid-row: 1;
}
.ge-layers-list > .ge-layer-item:not(.ge-adj-sub-item):not(.ge-mask-sub-item) > .ge-layer-opacity-row {
grid-column: 1 / -1;
grid-row: 2;
display: flex;
width: auto;
min-width: 0;
margin-left: 52px;
}
.ge-mask-sub-item {
display: grid;
grid-template-columns: 40px 34px minmax(80px, 1fr) auto;
grid-template-rows: 40px;
align-items: center;
padding-left: 8px;
}
.ge-mask-sub-item > .ge-layer-vis { grid-column: 1; grid-row: 1; }
.ge-mask-sub-item > .ge-mask-inline-thumb { grid-column: 2; grid-row: 1; }
.ge-mask-sub-item > .ge-layer-name {
grid-column: 3;
grid-row: 1;
display: flex !important;
min-width: 0;
}
.ge-mask-sub-item > .ge-layer-controls {
grid-column: 4;
grid-row: 1;
min-width: 0;
justify-content: flex-start;
}
.ge-mask-sub-item > .ge-layer-controls .ge-layer-btn {
width: 30px !important;
min-width: 30px !important;
height: 30px !important;
font-size: 14px !important;
}
/* 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 into precision rows on mobile:
Row 1: X + Y
Row 2: W + H
Row 3: ↻ rotate + quick actions + Apply/Cancel */
.ge-transform-popup-body {
flex-wrap: wrap !important;
align-items: center;
row-gap: 8px;
column-gap: 6px;
}
.ge-transform-popup-body #ge-transform-rot {
width: 3.5ch !important;
min-width: 0 !important;
}
/* 3-row layout, enforced by explicit `.ge-row-break` elements in the
DOM (the only reliable way to force a flex-wrap line break).
Row 1: W input + H input
Row 2: aspect lock + ↻ rotate input
Row 3: flip-h, flip-v, rot-90 + Cancel + Apply */
.ge-transform-popup-body .ge-row-break {
display: block;
flex-basis: 100%;
height: 0;
width: 100%;
}
/* Flip buttons slightly bigger so they're easier to tap. */
.ge-transform-quick-btn {
padding: 8px 10px !important;
min-width: 40px;
min-height: 40px;
}
.ge-transform-quick-btn svg {
width: 18px;
height: 18px;
}
/* Cancel + Apply share the right side of row 2, same size, paired. */
.ge-transform-popup-body #ge-transform-cancel-btn {
margin-left: auto;
}
.ge-transform-popup-body #ge-transform-cancel-btn,
.ge-transform-popup-body #ge-transform-apply {
padding: 8px 16px !important;
font-size: 13px !important;
min-width: 80px;
text-align: center;
}
/* Stepper-style number control on mobile:
[ − ] [ input ] [ + ]
Stack reverses so the − sits visually on the LEFT of the input, +
on the RIGHT. Big finger-sized squares, big +/- glyphs, both
buttons support tap-and-hold auto-repeat (JS) for fast scrubbing. */
.ge-transform-field {
display: inline-flex !important;
align-items: stretch;
gap: 0;
}
.ge-transform-field > label {
display: inline-flex;
align-items: center;
padding-right: 6px;
font-weight: 600;
}
.ge-transform-field:not(:has(#ge-transform-rot)) > label {
position: relative;
top: -1px;
}
.ge-transform-spin {
display: inline-flex !important;
flex-direction: row !important;
/* Use grid-order to swap positions of [−][+]: the − should sit
BEFORE the input, the + AFTER it. Achieve this by absolute
order — first child styled differently, second styled
differently, then we move the whole .ge-transform-spin's first
button visually before the input via reverse-flex. */
order: 0;
width: auto !important;
}
.ge-transform-field {
flex-direction: row;
}
/* Re-flow: label + [-] + input + [+]
We split the .ge-transform-spin's two buttons across the field's
flex line using order: order:0 for −, order:2 for input, order:3
for +. The spin itself gets `display: contents` so its
children can be reordered independently against the input. */
.ge-transform-spin {
display: contents !important;
}
.ge-transform-field > input.ge-transform-popup-input {
order: 2;
text-align: center;
}
.ge-transform-spin button[data-spin="down"] {
order: 1;
}
.ge-transform-spin button[data-spin="up"] {
order: 3;
}
/* The buttons themselves — chunky, square, identical in size. Reset
the base `:first-child` rule (which strips the bottom border for
the old stacked layout) so both buttons render with the same
visual box in the new row layout. */
.ge-transform-spin button {
box-sizing: border-box !important;
width: 36px !important;
height: 36px !important;
min-width: 36px;
min-height: 36px;
padding: 0 0 0 0 !important;
font-size: 18px !important;
font-weight: 700;
line-height: 1 !important;
border: 1px solid var(--border) !important;
border-radius: 6px !important;
flex-shrink: 0;
display: inline-flex !important;
align-items: center;
justify-content: center;
position: relative;
top: -4px;
}
.ge-transform-spin button:first-child,
.ge-transform-spin button:last-child {
border-radius: 6px !important;
border-bottom: 1px solid var(--border) !important;
}
/* Graphic-editor adjustment buttons use text glyphs that sit visually low —
lift them to match the +/- button alignment. */
.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-selection-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;
}
.ge-canvas-prompt-footer {
flex-wrap: wrap;
}
.ge-canvas-prompt-footer .ge-canvas-prompt-hint {
flex: 1 0 100%;
margin-bottom: 2px;
}
/* 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);
}