Files
odysseus/static/css/09-gallery.css
T
Léo a4715b77a5 refactor(css): name the fragments by area instead of by number
style-part-01..08 were arbitrary 6,000-line cuts, so the names said nothing
and a reader had no way to guess which file held a rule.

The original stylesheet turns out to be roughly area-grouped already, so
cutting where the content changes rather than every 6,000 lines produces
boundaries worth naming. Classifying every top-level rule by selector prefix
and smoothing over a 40-rule window gives 17 stable runs: agent-chat, compare,
memory, documents, admin-settings, skills, gallery, cookbook, tasks,
image-editor, email, notes, calendar, research.

The numeric prefix stays because load order is load-bearing, and it also
disambiguates the areas that appear twice: the original interleaves, and
merging non-adjacent blocks of the same area would reorder the cascade.

The names describe where a file sits, not a claim that it holds every rule for
that area or only rules for it. The header in each file says so, because that
is the misreading this naming invites.

Reconstruction was checked against lab's style.css before rewriting: identical
after whitespace normalisation, all 47,528 lines accounted for. The computed
style baseline still reproduces exactly.
2026-09-29 17:50:04 +02:00

1383 lines
39 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 09-gallery - part of the former static/style.css.
*
* The stylesheet is split by area, but the split is mechanical: rules were
* moved verbatim and NOT regrouped. The numeric prefix is the load order, and
* that order is what preserves the cascade. A rule sits in the file its
* original position fell into, so an area's rules are not all in its file and
* a file is not only that area's rules.
*
* Do not reorder the <link> tags in static/index.html, and do not move a rule
* between files: with 1,151 redeclarations and 1,886 !important in this
* cascade, either can change which declaration wins.
*
* static/index.html and the PRECACHE list in static/sw.js must agree on this
* order. tests/test_static_stylesheet_manifest.py and
* tests/test_css_computed_style_snapshot.py fail if that drifts.
*/
.email-reader-actions-inline.email-reader-actions-compact > .reader-action-overflowed {
display: none !important;
}
/* ── Archive browser ── */
.archive-list {
grid-template-columns: 1fr !important;
gap: 4px !important;
}
.archive-row {
flex-direction: row !important;
}
.archive-row .doclib-card-header {
flex: 1;
padding: 8px 10px;
}
.archive-row .doclib-card-title {
flex: 1;
min-width: 0;
}
/* Archive column layout */
.archive-header {
display: flex;
align-items: center;
padding: 4px 10px;
gap: 6px;
font-size: 9px;
font-weight: 600;
opacity: 0.35;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.archive-col-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: -4px;
}
.archive-col-msgs {
width: 40px;
flex-shrink: 0;
text-align: left;
margin-left: -8px;
}
.archive-col-model {
width: 90px;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.archive-col-time {
width: 55px;
flex-shrink: 0;
text-align: right;
}
.archive-col-menu {
width: 24px;
flex-shrink: 0;
}
.archive-menu-btn {
background: transparent;
border: none;
color: var(--fg);
opacity: 0.3;
cursor: pointer;
padding: 4px;
line-height: 0;
border-radius: 4px;
transition: background .15s, opacity .15s;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-left: 4px;
vertical-align: middle;
height: 100%;
}
.archive-menu-btn svg {
display: block;
margin-top: -6px;
}
.archive-menu-btn:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
opacity: 1;
}
/* ── Gallery (image library) ── */
.gallery-modal-content {
width: min(820px, 94vw);
max-height: 92vh;
font-size: 12px;
}
/* Keep the Gallery frame anchored while tabs or bulk-selection controls
change the content height. Centering the frame made every shorter tab move
the whole window vertically. */
@media (min-width: 769px) {
#calendar-modal,
#gallery-modal,
#tasks-modal,
#memory-modal,
#doclib-modal,
#compare-model-overlay,
#research-overlay,
#theme-modal,
#settings-modal,
#email-lib-modal,
#cookbook-modal {
align-items: flex-start;
padding-top: 4vh;
box-sizing: border-box;
}
}
/* While the Edit tab is active the editor needs a *definite* height so
the right-side tool panel (`.ge-right-panel { overflow-y:auto }`)
scrolls INTERNALLY. Without this the modal only has `max-height`, so
`.gallery-editor { height:100% }` can't resolve — the editor grows to
its full content height, overflows the modal body, and the modal-body
scrollbar that appears can't be wheel-scrolled because the editor's
`overscroll-behavior:contain` blocks the chain. Pinning a real height
here makes the panel bounded and scrollable as designed. Scoped to
the editor view (matched via the container's inline `display:flex`)
so the Photos/Albums views keep sizing to their content. */
.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) {
height: 92vh;
}
/* The editor tab owns the modal body's scroll context. Keep the landing
chooser and the mounted editor inside the available sheet height so a long
saved-project list cannot push the launch controls below the viewport. */
.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) > .modal-body {
display: flex;
min-height: 0;
overflow: hidden;
}
.gallery-modal-content:has(#gallery-editor-container[style*="flex"]) #gallery-editor-container {
min-height: 0;
overflow: hidden;
}
/* Photo-detail view sizing (issue #314).
The detail view is rendered as a `position:absolute; inset:0` overlay
*inside* `.gallery-images-container`, painted over the photo grid. Because
it's absolutely positioned it can't contribute to the container's height —
the container (and therefore the overlay's `inset:0` box) collapses to the
height of the grid sitting behind it. When the library only has a few
photos that grid is short, so the detail view is crushed: the image is
clipped and the metadata sidebar (`overflow-y:auto`) is squeezed into a
tiny, internally-scrolling strip. (With a large library the grid is tall,
which is why it looked fine in the demo video but cramped for users with
few photos.)
Fix: when the detail view is open, hide the grid-view siblings and drop the
overlay into normal flow. The container — and the window, up to its 92vh
max-height — then sizes to the detail's own content (image + metadata), so
nothing is clipped or squeezed regardless of how many photos exist. Scoped
via the detail element's inline `display:flex` so the grid / albums views
keep sizing to their own content. Works on both desktop and the mobile
full-screen sheet. */
#gallery-images-container:has(> #gallery-detail[style*="flex"]) > *:not(#gallery-detail) {
display: none !important;
}
#gallery-images-container:has(> #gallery-detail[style*="flex"]) > #gallery-detail {
position: static;
}
/* Containing block for the photo-detail overlay — keeps it inside the body
so it sits below the modal header and the tab strip instead of covering them. */
.gallery-images-container {
position: relative;
}
.gallery-modal-content .modal-header h4 {
font-size: 1rem;
}
.gallery-stats {
font-size: 10px;
color: color-mix(in srgb, var(--fg) 50%, transparent);
margin-bottom: 4px;
}
.gallery-toolbar {
display: flex;
gap: 6px;
margin-bottom: 4px;
align-items: center;
}
.gallery-toolbar-break { display: none; }
/* Keep the hint anchored while independently seating the input in the row. */
.gallery-search-wrap { position: relative; flex: 1; min-width: 0; display: flex; top: 0; }
.gallery-search {
position: relative;
top: 4px;
flex: 1;
width: 100%;
padding: 4px 8px;
padding-right: 78px; /* room for the enter hint */
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
font: inherit;
font-size: 12px;
outline: none;
}
.gallery-search-enter-hint {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
color: var(--accent, var(--red));
pointer-events: none;
opacity: 0;
transition: opacity 0.12s;
white-space: nowrap;
}
/* Show the hint only once the user has typed something (placeholder gone). */
.gallery-search:not(:placeholder-shown) ~ .gallery-search-enter-hint { opacity: 0.95; }
.gallery-search:focus {
border-color: var(--red);
}
.gallery-model-filter,
.gallery-sort {
padding: 4px 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
font: inherit;
font-size: 11px;
cursor: pointer;
position: relative;
top: 4px;
}
.gallery-selection-bar {
margin-bottom: 4px;
position: relative;
top: 2px;
padding-right: 18px;
}
.gallery-tag-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 4px;
}
.gallery-chip {
height: 28px;
box-sizing: border-box;
padding: 0 13px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
border-radius: 14px;
font-size: 12px;
line-height: 1;
border: 1px solid var(--border);
background: none;
color: var(--fg);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.gallery-chip:hover {
border-color: var(--red);
}
.gallery-chip.active {
background: color-mix(in srgb, var(--red) 15%, transparent);
border-color: color-mix(in srgb, var(--red) 40%, transparent);
color: var(--red);
}
/* Album chips row */
.gallery-album-chips {
display: flex; gap: 4px; flex-wrap: wrap; padding: 0 0 3px;
}
.gallery-chip-fav { color: var(--red); }
.gallery-chip-fav.active { background: color-mix(in srgb, var(--red) 15%, transparent); border-color: var(--red); }
.gallery-chip-add { opacity: 0.5; font-size: 14px; padding: 2px 10px; }
/* Active-album indicator — appears when the Photos grid is filtered to one album. */
.gallery-chip-active-album,
.gallery-active-filter-pill {
display: inline-flex; align-items: center; gap: 4px;
background: color-mix(in srgb, var(--red) 14%, transparent);
border-color: color-mix(in srgb, var(--red) 45%, transparent);
position: relative;
top: 6px;
}
.gallery-active-filter-pill {
background: color-mix(in srgb, var(--fg) 7%, transparent);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
color: color-mix(in srgb, var(--fg) 86%, transparent);
}
.gallery-clear-all-filters {
height: 28px;
border-style: dashed;
opacity: 0.72;
}
.gallery-clear-all-filters:hover {
opacity: 1;
color: var(--red);
}
.gallery-chip-clear {
background: none;
border: none;
color: inherit;
opacity: 0.6;
cursor: pointer;
padding: 0;
margin-left: 1px;
width: 12px;
height: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
position: relative;
top: -4px;
}
.gallery-chip-clear:hover { opacity: 1; }
/* Favorite button on card */
/* Video cards: <video> fills the same slot as <img>, plus a small play badge
so it's clear the thumbnail represents a video. */
.gallery-card video {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
background: #000;
display: block;
}
.gallery-card-play {
position: absolute;
left: 50%; top: 50%;
transform: translate(-50%, -50%);
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
border-radius: 50%;
pointer-events: none;
z-index: 1;
}
.gallery-card-play svg { margin-left: 2px; }
.gallery-fav-btn {
position: absolute; top: 0px; right: 4px; z-index: 2;
background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
width: 26px; height: 26px; font-size: 14px;
color: rgba(255,255,255,0.6); cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s;
padding: 0;
}
.gallery-fav-btn.gallery-fav-active { opacity: 1; color: var(--red); }
.gallery-dl-btn {
position: absolute; top: 0px; left: 4px; z-index: 2;
background: rgba(0,0,0,0.4); border: none; border-radius: 50%;
width: 26px; height: 26px;
color: rgba(255,255,255,0.75); cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0; transition: opacity 0.15s, background 0.15s;
padding: 0;
}
.gallery-dl-btn:hover { background: rgba(0,0,0,0.7); color: #fff; }
/* In select mode the per-thumbnail hover buttons (favorite + download) just
get in the way of picking — hide them so the card is a clean select target.
`body.gallery-selecting` is the authoritative signal (one class for the
whole grid) — the per-card .gallery-card-selectable is kept as a backup
in case the body class is missed. */
body.gallery-selecting .gallery-fav-btn,
body.gallery-selecting .gallery-dl-btn,
.gallery-card-selectable .gallery-fav-btn,
.gallery-card-selectable .gallery-dl-btn {
display: none !important;
}
/* AI tag chips in detail view */
.gallery-ai-tags { display: flex; gap: 4px; flex-wrap: wrap; }
.gallery-ai-chip {
background: color-mix(in srgb, var(--accent, var(--red)) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--accent, var(--red)) 25%, transparent);
border-radius: 10px; padding: 3px 8px; font-size: 10px;
color: var(--fg); opacity: 0.8;
font: inherit; font-size: 10px;
cursor: pointer;
transition: background 0.12s, opacity 0.12s, border-color 0.12s;
}
.gallery-ai-chip:hover {
opacity: 1;
background: color-mix(in srgb, var(--accent, var(--red)) 22%, transparent);
border-color: color-mix(in srgb, var(--accent, var(--red)) 50%, transparent);
}
/* AI-generated tags: muted neutral with a tiny sparkle marker so the
user can tell at a glance these came from the model, not them. */
.gallery-aitag-chip {
background: color-mix(in srgb, var(--fg) 6%, transparent);
border-color: color-mix(in srgb, var(--fg) 18%, transparent);
display: inline-flex;
align-items: center;
gap: 4px;
}
.gallery-aitag-chip:hover {
background: color-mix(in srgb, var(--fg) 12%, transparent);
border-color: color-mix(in srgb, var(--fg) 35%, transparent);
}
.gallery-aitag-mark {
font-size: 8px;
opacity: 0.55;
color: var(--accent, var(--red));
}
/* User-applied tags keep the accent-colored chip — they're the personal/
curated tags, so they get the strongest visual weight. */
.gallery-user-chip {
font-weight: 600;
}
/* × to remove a user tag (appears inside the chip). */
.gallery-tag-x {
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 5px;
width: 13px;
height: 13px;
border-radius: 50%;
font-size: 12px;
line-height: 1;
opacity: 0.55;
transition: opacity 0.12s, background 0.12s, color 0.12s;
}
.gallery-tag-x:hover {
opacity: 1;
background: color-mix(in srgb, var(--red, #ff5555) 22%, transparent);
color: var(--red, #ff5555);
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
align-content: start;
gap: 8px;
max-height: calc(92vh - 165px);
overflow-y: auto;
padding: 2px;
transition: border-color 0.15s, background 0.15s;
}
.gallery-grid.gallery-dragover {
border: 2px dashed var(--red);
border-radius: 8px;
background: color-mix(in srgb, var(--red) 5%, transparent);
}
.gallery-card {
position: relative;
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
border: 1px solid var(--border);
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s;
}
.gallery-card.gallery-card-focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 36%, transparent);
}
/* Upload-affordance tile pinned to the top-left of the Photos grid. Matches
the Upload album tile in the Albums tab — dashed border, centered icon. */
.gallery-card-upload {
border-style: dashed;
background: color-mix(in srgb, var(--fg) 3%, var(--bg));
opacity: 0.75;
transition: opacity 0.12s, border-color 0.15s, transform 0.15s;
}
/* The "Start AI tag" button turns into a Cancel control during a tag run. */
.gallery-tag-cancelling {
color: var(--red) !important;
border-color: color-mix(in srgb, var(--red) 45%, var(--border)) !important;
}
/* Skeleton/shimmer placeholder tiles shown while the first page of photos loads. */
.gallery-card-skeleton {
cursor: default;
background: linear-gradient(100deg,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
background-size: 200% 100%;
animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
}
.gallery-card-skeleton:hover { transform: none; box-shadow: none; border-color: var(--border); }
/* Chat attachment image: shimmer skeleton + centered whirlpool shown while a
sent photo uploads / its thumbnail loads (see chatRenderer buildAttachCards). */
.attach-image-skeleton {
position: relative;
width: 160px; height: 120px;
border-radius: 6px;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(100deg,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 30%,
color-mix(in srgb, var(--fg) 12%, var(--panel)) 50%,
color-mix(in srgb, var(--fg) 4%, var(--panel)) 70%);
background-size: 200% 100%;
animation: gallery-skeleton-shimmer 1.25s ease-in-out infinite;
}
.attach-image-skeleton > .spinner-whirlpool { margin: 0 !important; }
/* Corner "Aa" button on a chat photo thumbnail — opens the vision/OCR editor
so the user can correct what the vision model fed to the LLM. */
.attach-image-preview { position: relative; }
.attach-vision-model,
.attach-image-name {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attach-vision-model {
font-size: 10px;
color: var(--accent, var(--red));
opacity: 0.85;
margin-top: 3px;
}
.attach-image-name {
font-size: 10px;
opacity: 0.5;
margin-top: 1px;
}
.attach-ocr-btn {
position: absolute; top: 4px; right: 4px;
z-index: 5; /* sit above msg-action overlays so the click lands here */
height: 22px;
background: rgba(0,0,0,0.55); color: #fff;
border: 1px solid rgba(255,255,255,0.2); border-radius: 4px;
padding: 0 8px 0 6px; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: 5px;
font-size: 11px; font-weight: 500; line-height: 1;
opacity: 0.75; transition: opacity 0.15s;
}
.attach-ocr-btn:hover { opacity: 1; }
.attach-ocr-btn svg { display: block; }
/* On mobile keep just the (larger) edit icon — the "Caption" label crowds
the corner of the small chat photo. */
@media (max-width: 768px) {
.attach-ocr-btn .attach-ocr-label { display: none; }
.attach-ocr-btn {
width: 28px; height: 28px;
padding: 0;
}
.attach-ocr-btn svg { width: 16px; height: 16px; }
}
/* Vision/OCR text editor modal — opened from the "Aa" button above. */
.vision-editor-overlay {
position: fixed; inset: 0; z-index: 9999;
background: rgba(0,0,0,0.55);
display: flex; align-items: center; justify-content: center;
padding: 16px;
}
.vision-editor-panel {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px;
width: min(560px, 92vw);
max-height: 88vh;
display: flex; flex-direction: column; gap: 8px;
box-shadow: 0 14px 40px rgba(0,0,0,0.4);
}
.vision-editor-title {
font-size: 13px; font-weight: 600; color: var(--fg);
display: flex; align-items: center; gap: 6px;
}
.vision-editor-desc { font-size: 11px; opacity: 0.6; line-height: 1.4; }
.vision-editor-text {
width: 100%; min-height: 180px;
background: var(--panel); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 8px 10px;
font-family: inherit; font-size: 12px; line-height: 1.5;
resize: vertical; outline: none;
}
.vision-editor-text:focus { border-color: var(--accent-primary, var(--red)); }
.vision-editor-hint { font-size: 10.5px; opacity: 0.55; line-height: 1.4; }
.vision-editor-actions {
display: flex; gap: 8px; justify-content: flex-end;
margin-top: 4px;
}
.vision-editor-btn {
background: var(--panel); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 14px; font-size: 12px; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
display: inline-flex; align-items: center; gap: 5px;
}
.vision-editor-btn svg { display: block; }
/* Optical nudge: the button text sits 1px higher than the SVG glyphs
alongside it. Drop the text down 1px so they line up. */
.vision-editor-btn .vision-btn-label { position: relative; top: 1px; }
.vision-editor-btn:hover { border-color: color-mix(in srgb, var(--fg) 40%, var(--border)); }
.vision-editor-btn-primary {
background: var(--accent-primary, var(--red));
color: #fff; border-color: transparent;
}
.vision-editor-btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
/* Quick full-size lightbox shown when the user taps a chat photo thumbnail. */
.attach-lightbox {
position: fixed; inset: 0; z-index: 9998;
background: rgba(0, 0, 0, 0.85);
display: flex; align-items: center; justify-content: center;
padding: 16px; cursor: zoom-out;
animation: attach-lightbox-fade 0.12s ease-out;
}
.attach-lightbox img {
max-width: 100%; max-height: 100%;
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
object-fit: contain;
}
.attach-lightbox-err {
position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
background: var(--bg); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 6px 10px; font-size: 12px;
}
@keyframes attach-lightbox-fade {
from { opacity: 0; }
to { opacity: 1; }
}
.gallery-card-upload:hover {
opacity: 1;
border-color: var(--red);
transform: translateY(-1px);
}
.gallery-card-upload-inner {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
color: var(--fg);
}
.gallery-card-upload-label {
font-size: 12px;
font-weight: 500;
opacity: 0.8;
}
.gallery-card:hover {
border-color: var(--red);
box-shadow: 0 2px 8px color-mix(in srgb, var(--red) 15%, transparent);
transform: translateY(-1px);
}
.gallery-card-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px;
background: linear-gradient(180deg, rgba(0,0,0,0.42), transparent 34%, rgba(0,0,0,0.22));
color: #fff;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
}
.gallery-card-badges {
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: flex-end;
justify-content: flex-end;
margin-top: auto;
margin-bottom: 34px;
}
.gallery-card-badge {
padding: 2px 6px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.55);
border: 1px solid rgba(255, 255, 255, 0.14);
color: rgba(255, 255, 255, 0.88);
font-size: 9px;
font-weight: 600;
line-height: 1.1;
}
.gallery-card-badge-fav {
color: #ff5f6f;
}
.gallery-select-dot {
position: absolute; top: 6px; left: 6px; z-index: 2;
display: block;
width: 10px; height: 10px; border-radius: 50%; cursor: pointer;
background: color-mix(in srgb, var(--fg) 30%, transparent);
border: 1px solid color-mix(in srgb, var(--fg) 50%, transparent);
transition: background 0.15s;
}
.gallery-select-dot.selected { background: var(--red); border-color: var(--red); }
.gallery-select-dot:hover { background: color-mix(in srgb, var(--red) 50%, transparent); }
.gallery-select-btn {
padding: 9px 10px 11px; background: transparent; color: var(--fg);
border: 1px solid var(--border); border-radius: 6px; cursor: pointer;
font-size: 11px; font-family: inherit; opacity: 0.6; transition: all 0.15s;
margin-top: -4px !important;
}
.gallery-select-btn:hover { opacity: 1; }
/* Match the library Select toggle's red tint (.memory-toolbar-btn.active).
Bumped contrast so the toggled state actually reads on mobile — the prior
15% red on transparent was nearly invisible against the page background. */
.gallery-select-btn.active {
background: color-mix(in srgb, var(--red) 28%, transparent);
color: var(--red);
border-color: var(--red);
font-weight: 600;
opacity: 1;
}
/* Toolbar action buttons sit 2px lower than the generic select-btn so
they baseline-align with the toolbar's selects/inputs instead of
floating above them. */
.gallery-toolbar-action {
margin-top: 0 !important;
display: inline-flex;
align-items: center;
gap: 2px;
}
.gallery-bulk-bar {
display: flex; align-items: center; gap: 8px; padding: 6px 0; font-size: 12px;
justify-content: flex-end;
}
.gallery-bulk-bar.hidden { display: none; }
.gallery-bulk-all {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11px; opacity: 0.75; cursor: pointer; margin-right: auto;
}
.gallery-bulk-all input { accent-color: var(--accent, var(--red)); cursor: pointer; }
.gallery-bulk-delete {
padding: 3px 10px; background: transparent; color: var(--red);
border: 1px solid var(--red); border-radius: 4px; cursor: pointer;
font-size: 11px; font-family: inherit;
}
.gallery-bulk-delete:hover { background: var(--red); color: #fff; }
.gallery-bulk-cancel {
padding: 3px 10px; background: transparent; color: var(--fg);
border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
font-size: 11px; font-family: inherit; opacity: 0.6;
}
.gallery-bulk-cancel:hover { opacity: 1; }
.gallery-card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.gallery-card-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px 8px;
background: linear-gradient(transparent, rgba(0,0,0,0.75));
color: #fff;
}
.gallery-card-prompt {
font-size: 10px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gallery-card-meta {
display: flex;
align-items: center;
gap: 6px;
margin-top: 2px;
font-size: 9px;
opacity: 0.8;
}
.gallery-card-model {
padding: 1px 5px;
border-radius: 6px;
background: rgba(255,255,255,0.15);
}
.gallery-empty {
text-align: center;
color: color-mix(in srgb, var(--fg) 35%, transparent);
padding: 32px 16px;
font-size: 12px;
font-style: italic;
}
.gallery-load-more {
display: block;
margin: 10px auto 0;
padding: 6px 16px;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 11px;
transition: border-color 0.15s, color 0.15s;
}
.gallery-load-more:hover {
border-color: var(--red);
color: var(--red);
}
/* Gallery detail overlay */
.gallery-detail {
position: absolute;
inset: 0;
background: var(--panel);
z-index: 10;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.gallery-detail-header {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 8px 4px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
/* Pin to top so a tall (portrait) photo can't cover the Back / ⋮
menu when the detail body grows past the visible area. */
position: sticky;
top: 0;
background: var(--panel);
/* Above the image's rotate/nav buttons (z-index:3) so they scroll cleanly
behind the header instead of colliding with it. */
z-index: 5;
min-height: 0;
}
.gallery-detail-title {
max-width: min(360px, 40vw);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
font-weight: 600;
opacity: 0.82;
position: relative;
top: 1px;
}
.gallery-detail-back {
background: none;
border: none;
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 12px;
padding: 4px 8px;
border-radius: 4px;
position: relative;
top: -2px;
}
/* Slightly larger header icons (back/edit/heart/⋮) — override the inline 14px. */
.gallery-detail-header svg { width: 16px; height: 16px; }
.gallery-detail-back:hover {
background: color-mix(in srgb, var(--fg) 10%, transparent);
}
.gallery-detail-action {
background: none;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 11px;
padding: 3px 8px;
border-radius: 4px;
transition: border-color 0.15s;
}
.gallery-detail-action:hover {
border-color: var(--fg);
}
.gallery-detail-action.danger {
color: var(--color-error, #e55);
}
/* Overflow menu — replaces the seven individual action buttons that used
to crowd the right side of the detail header. */
.gallery-detail-menu-wrap { position: relative; }
.gallery-detail-menu-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 26px;
padding: 0;
position: relative;
top: -2px;
}
.gallery-detail-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 150px;
padding: 3px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 4px 14px color-mix(in srgb, var(--fg) 18%, transparent);
z-index: 12;
display: flex;
flex-direction: column;
gap: 0;
}
.gallery-detail-menu[hidden] { display: none; }
.gallery-detail-menu .dropdown-item-compact {
width: 100%;
background: none;
border: none;
text-align: left;
font: inherit;
/* Tighter than the global default — the photo-detail menu has 8
items so every pixel of vertical padding adds up. */
padding: 4px 8px;
font-size: 11px;
gap: 8px;
line-height: 1.2;
}
.gallery-detail-menu .dropdown-item-compact .dropdown-icon,
.gallery-detail-menu .dropdown-item-compact .dropdown-icon svg {
width: 12px;
height: 12px;
}
.gallery-detail-body {
display: flex;
gap: 16px;
padding: 12px;
flex: 1;
min-height: 0;
overflow: hidden;
}
.gallery-detail-image {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-width: 0;
min-height: 0;
height: 100%;
padding-inline: 56px;
box-sizing: border-box;
overflow: hidden;
}
/* Shrink-wraps the actual image so overlay children (heart, face boxes)
anchor to the IMAGE bounds, not the wider letterboxed wrapper. */
.gallery-detail-img-frame {
position: relative;
display: inline-flex;
max-width: 100%;
max-height: 100%;
}
.gallery-detail-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.85);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
z-index: 3;
padding: 0;
}
.gallery-detail-nav:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
.gallery-detail-nav-prev { left: 8px; }
.gallery-detail-nav-next { right: 8px; }
/* Rotate buttons — same pattern as the prev/next arrows but pinned to the
top corners. Hover-revealed so they don't clutter the image. */
.gallery-detail-rotate {
position: absolute;
top: 8px;
background: rgba(0, 0, 0, 0.45);
color: rgba(255, 255, 255, 0.85);
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
z-index: 3;
padding: 0;
}
.gallery-detail-rotate:hover { opacity: 1 !important; background: rgba(0, 0, 0, 0.7); }
.gallery-detail-rotate-ccw { left: 8px; }
.gallery-detail-rotate-cw { right: 8px; }
/* Inline heart that lives next to the "Date" label in the sidebar.
Tiny outline icon by default; fills red when favorited. */
.gallery-detail-date-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.gallery-detail-fav-inline {
background: none;
border: none;
padding: 0;
/* Nudge up 8px so the heart sits visually above the Date label
baseline rather than centred on it. */
margin-top: -8px;
border-radius: 4px;
color: var(--fg-muted);
opacity: 0.6;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: opacity 0.15s, color 0.15s;
}
/* Shrink the SVG so the heart matches the height of the small uppercase
Date label sitting next to it. */
.gallery-detail-fav-inline svg {
width: 12px;
height: 12px;
display: block;
}
.gallery-detail-fav-inline:hover {
opacity: 1;
}
.gallery-detail-fav-inline.active {
opacity: 1;
color: var(--red);
}
.gallery-detail-sidebar {
width: 290px;
flex-shrink: 0;
/* Vertical scroll for long metadata, but never horizontal — overflow:auto
alone would let a wide child (e.g. long album name in the select) push
the sidebar into horizontal-scroll mode. */
overflow-x: hidden;
overflow-y: auto;
min-width: 0;
}
.gallery-detail-inspector-head {
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}
.gallery-detail-eyebrow {
font-size: 9px;
font-weight: 700;
color: var(--accent, var(--red));
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 3px;
}
.gallery-detail-inspector-title {
font-size: 14px;
font-weight: 650;
line-height: 1.25;
word-break: break-word;
}
.gallery-detail-meta-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 6px;
margin: 4px 0 12px;
}
.gallery-detail-meta-card {
min-width: 0;
border: 1px solid color-mix(in srgb, var(--border) 82%, transparent);
background: color-mix(in srgb, var(--fg) 4%, transparent);
border-radius: 7px;
padding: 7px 8px;
}
.gallery-detail-meta-card span {
display: block;
font-size: 9px;
font-weight: 700;
color: color-mix(in srgb, var(--fg) 48%, transparent);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 3px;
}
.gallery-detail-meta-card strong {
display: block;
min-width: 0;
font-size: 11px;
font-weight: 600;
line-height: 1.25;
word-break: break-word;
}
.gallery-detail-inline-link {
appearance: none;
background: none;
border: none;
color: var(--accent, var(--red));
padding: 0;
font: inherit;
font-weight: inherit;
cursor: pointer;
text-align: left;
}
.gallery-detail-mini-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 7px;
}
.gallery-detail-mini-btn {
background: color-mix(in srgb, var(--fg) 5%, transparent);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 8px;
font: inherit;
font-size: 10.5px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.gallery-detail-mini-btn:hover {
border-color: color-mix(in srgb, var(--fg) 42%, var(--border));
}
/* Constrain any child that could otherwise stretch the sidebar wider than
its declared width (selects with long option text, long URLs, etc.). */
.gallery-detail-sidebar select,
.gallery-detail-sidebar input,
.gallery-detail-sidebar .gallery-detail-prompt {
max-width: 100%;
box-sizing: border-box;
}
.gallery-date-rel {
opacity: 0.55;
margin-left: 4px;
font-size: 11px;
font-style: italic;
}
.gallery-detail-section label {
display: block;
font-size: 10px;
font-weight: 600;
opacity: 0.6;
margin-bottom: 3px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.gallery-detail-section div {
font-size: 12px;
word-break: break-word;
}
.gallery-detail-prompt {
white-space: pre-wrap;
line-height: 1.4;
}
.gallery-tag-input,
.gallery-detail-name-input {
width: 100%;
padding: 5px 8px;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 4px;
font: inherit;
font-size: 11px;
margin-bottom: 4px;
}
.gallery-tag-input:focus,
.gallery-detail-name-input:focus {
border-color: var(--red);
outline: none;
}
/* ↵ enter hint inside the Add-a-tag field (accent), replacing the "press Enter"
placeholder text. */
.gallery-tag-input-wrap { position: relative; }
.gallery-tag-enter-hint {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
margin-top: -2px; /* the input has 4px bottom margin; center on the box */
color: var(--accent, var(--red));
font-size: 13px;
line-height: 1;
pointer-events: none;
opacity: 0.8;
}
.gallery-detail-name-input { font-size: 13px; font-weight: 500; }
/* ↵ enter hint on the image name field — accent, shown while editing to signal
that Enter saves. */
.gallery-name-wrap { position: relative; }
.gallery-name-enter {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
margin-top: -2px;
color: var(--accent, var(--red));
opacity: 0.85;
pointer-events: none;
display: none;
}
.gallery-tag-save {
background: none;
border: 1px solid var(--border);
color: var(--fg);
cursor: pointer;
font: inherit;
font-size: 10px;
padding: 3px 10px;
border-radius: 4px;
transition: border-color 0.15s, color 0.15s;
}
.gallery-tag-save:hover {
border-color: var(--red);
color: var(--red);
}
@media (max-width: 600px) {
.gallery-grid {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 6px;
}
/* Single-row toolbar on mobile: search + Sources + Recent (sort) all share
one row. The search bar shrinks so the dropdowns fit beside it. */
.gallery-toolbar {
flex-wrap: nowrap;
gap: 4px;
}
.gallery-search-wrap {
flex: 1 1 0;
min-width: 0;
}
.gallery-search {
/* Drop the desktop right-padding reserved for the "enter to tag" hint —
hide the hint on mobile (below) so the input can use its full width. */
padding-right: 8px;
}
.gallery-search-enter-hint { display: none; }
/* Disable the row break so the filters stay inline with search. */
.gallery-toolbar-break { display: none; }
.gallery-model-filter,
.gallery-sort {
flex: 0 0 auto;
/* Cap each dropdown so search keeps a usable amount of width. */
max-width: 100px;
font-size: 11px;
padding: 4px 6px;
/* Keep the same vertical alignment as the desktop toolbar. */
position: relative;
top: 4px;
}
.gallery-select-btn {
padding: 6px 8px 8px;
font-size: 9px;
flex-shrink: 0;
/* Push Select to the far right of the single-row toolbar, regardless
of its DOM position (sits between search and the dropdowns). */
order: 99;
margin-left: auto;
}
/* Tabs scroll horizontally rather than wrapping when narrow. */
.gallery-tabs {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.gallery-tabs::-webkit-scrollbar { display: none; }
.gallery-tab { flex-shrink: 0; }
/* Album chips also scroll horizontally so they don't stack. */
.gallery-album-chips {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.gallery-album-chips::-webkit-scrollbar { display: none; }
.gallery-chip { flex-shrink: 0; }
/* Tasks filter chips — same horizontal-swipe behaviour as the gallery
albums / library chips on mobile: one row, slide-to-see-more. */
.tasks-activity-filters {
overflow-x: auto;
flex-wrap: nowrap !important;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
padding-bottom: 4px;
}
.tasks-activity-filters::-webkit-scrollbar { display: none; }
.tasks-activity-filters > * { flex-shrink: 0; }
/* Detail view */
.gallery-detail-body {
flex-direction: column;
overflow: visible;
}
.gallery-detail-image {
flex: 0 0 auto;
width: 100%;
height: clamp(280px, 58vh, 520px);
padding-inline: 48px;
}
.gallery-detail-sidebar {
width: 100%;
}
.gallery-detail-title {
display: none;
}
.gallery-detail-meta-grid {
grid-template-columns: 1fr;
}
/* Album grid: smaller tile minimum on mobile. */
.gallery-albums-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
}
/* ── Scoreboard table ── */
.scoreboard-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88em;
}
.scoreboard-table th {
text-align: left;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
font-weight: 600;
font-size: 0.85em;
color: color-mix(in srgb, var(--fg) 55%, transparent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.scoreboard-table th:not(:first-child) {
text-align: center;
}
.scoreboard-table td {
padding: 5px 10px;
border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
text-align: center;
}
.scoreboard-table td.scoreboard-model {
text-align: left;
font-weight: 500;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.scoreboard-table td.scoreboard-pct {
font-weight: 600;
color: var(--red);
}