This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
imagerepo/app/static/style.css
T
bvandeusen 5b7e033d0c feat(suggestions): auto-accept threshold + retroactive blocklist sweep
Two capabilities that work together to turn the suggestion system from
a 'review every noisy chip' UX into a 'curated tags only, configurable
auto-apply' UX for a solo-user library.

Auto-accept threshold
  Service (tag_suggestions.py):
    - New default auto_accept_general_threshold = 0.95 in _DEFAULTS
    - get_suggestions splits general-category hits at/above the threshold
      into a separate auto_accept_candidates list; the core 'character'/
      'copyright'/'general' keys stay the same shape for existing callers
    - get_bulk_suggestions ignores the new key (no side effects in bulk)
  Route (main.py GET /image/<id>/suggestions):
    - For each candidate: find-or-create Tag(kind='user', name=name), attach
      to image, log SuggestionFeedback(source='wd14'|etc, decision='accepted')
    - Response adds 'auto_accepted: [{id, name, display_name, kind,
      confidence, source}, ...]' so the modal can review + undo
  Config endpoints (main.py):
    GET  /api/suggestions/config/auto-accept-threshold
    POST /api/suggestions/config/auto-accept-threshold {threshold}
    Values > 1.0 effectively disable the feature
  UI (view-modal.js):
    - New renderAutoAccepted block at top of suggestions section with
      green-tinted chips carrying ✕ (undo for this image, logs rejection)
      and ⊘ (blocklist + remove from all images)
    - loadSuggestions refreshes the tag pill list when auto_accepted has
      items so the user sees them in the Tags section too
  Settings:
    - Maintenance tab gains a number input + save button backed by
      /api/suggestions/config/auto-accept-threshold

Retroactive blocklist sweep
  New Celery task app.tasks.maintenance.sweep_blocklisted_tag_from_images
  on the maintenance queue. Enqueued automatically when a name is added
  via POST /api/suggestions/blocklist or appears new in the bulk replace.
  Task body: finds kind='user' Tag matching the name, deletes its
  image_tags rows, deletes the Tag itself. Scope limited to kind='user'
  so deliberate character/fandom/artist tags sharing a blocklisted name
  aren't silently destroyed.

Verification (local dev):
  - Threshold GET/POST round-trip correct (default 0.95, persisted in
    tag_suggestion_config)
  - Image 633 at threshold=0.9: 4 general tags auto-applied, each with
    a SuggestionFeedback row, returned in auto_accepted
  - Blocklist add of 'sweeptestonly' with an attached test tag: Redis
    maintenance queue depth = 1, task body removes 1 image_tags row +
    the Tag row
  - get_bulk_suggestions still works (auto_accept_candidates key skipped)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 17:23:25 -04:00

3192 lines
67 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.
/* /app/static/style.css
==============================================================================
ImageRepo Global Styles
- Theme tokens
- Base & layout
- Header / Navbar
- Flash messages
- Pagination
- Gallery
- Modal viewer
- Tag overlays & editor
- Tag/Artist cards (tags_list.html)
- Settings
- Index / Landing
- Forms
==============================================================================*/
/*------------------------------------------------------------------------------
Theme tokens - Dark theme matching showcase
------------------------------------------------------------------------------*/
:root {
/* surfaces */
--bg: #0a0a0a;
--bg-elevated: #141414;
--panel: #1a1a1a;
--panel-alt: rgba(20, 20, 20, 0.95);
/* nav */
--nav-bg: rgba(0, 0, 0, 0.8);
--nav-border: rgba(255, 255, 255, 0.08);
/* text */
--text: #ffffff;
--text-dim: rgba(255, 255, 255, 0.7);
--text-muted: rgba(255, 255, 255, 0.5);
/* accents */
--link: #a78bfa;
--link-hover: #c4b5fd;
/* shadows */
--shadow-1: 0 2px 8px rgba(0, 0, 0, 0.4);
--shadow-2: 0 4px 12px rgba(0, 0, 0, 0.5);
--shadow-3: 0 8px 24px rgba(0, 0, 0, 0.6);
/* buttons */
--btn-primary: #6366f1;
--btn-primary-hover: #4f46e5;
--btn-danger: #dc2626;
--btn-danger-hover: #b91c1c;
/* flash */
--flash-info: #3b82f6;
--flash-success: #22c55e;
--flash-warning: #f59e0b;
--flash-danger: #ef4444;
/* filter pills */
--filter-bg: rgba(255, 255, 255, 0.08);
--filter-fg: #e2e8f0;
--filter-border: rgba(255, 255, 255, 0.15);
--filter-active-bg: #6366f1;
--filter-active-fg: #ffffff;
--filter-active-border: #818cf8;
}
/*------------------------------------------------------------------------------
Base & layout
------------------------------------------------------------------------------*/
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 0;
color: var(--text);
background-color: var(--bg);
min-height: 100vh;
}
.content {
padding: 1rem;
max-width: 1800px;
margin: 0 auto;
}
.mainview {
max-width: 100%;
margin: 0 auto;
background-color: transparent;
min-height: 100vh;
}
h1 {
font-weight: 500;
letter-spacing: -0.02em;
}
hr {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
margin: 0;
}
/*------------------------------------------------------------------------------
Header / Navbar (sticky with gradient fade)
------------------------------------------------------------------------------*/
header {
position: sticky;
top: 0;
z-index: 1000;
background: linear-gradient(to bottom, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.6) 70%, rgba(0,0,0,0) 100%);
padding-bottom: 1rem;
}
.navbar {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.25rem;
padding: 0.75rem 1rem;
background: transparent;
border: none;
}
.nav-button {
display: inline-block;
padding: 0.5rem 1rem;
color: var(--text);
text-decoration: none;
border-radius: 6px;
font-weight: 500;
font-size: 1rem;
transition: background 0.2s ease, transform 0.1s ease;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.nav-button:hover {
background: rgba(255,255,255,0.15);
transform: translateY(-1px);
}
.nav-button:active {
background: rgba(255,255,255,0.1);
transform: translateY(0);
}
/* Nav dropdown */
.nav-dropdown {
position: relative;
display: inline-block;
}
.nav-dropdown-trigger {
cursor: pointer;
border: none;
background: transparent;
}
.nav-dropdown-trigger::after {
content: " \25BC";
font-size: 0.65em;
opacity: 0.7;
}
.nav-dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
min-width: 150px;
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 1001;
padding: 0.5rem 0;
}
.nav-dropdown-menu a {
display: block;
padding: 0.5rem 1rem;
color: var(--text);
text-decoration: none;
font-size: 0.95rem;
transition: background 0.15s ease;
}
.nav-dropdown-menu a:hover {
background: rgba(255, 255, 255, 0.15);
}
.nav-dropdown:hover .nav-dropdown-menu,
.nav-dropdown:focus-within .nav-dropdown-menu {
display: block;
}
/*------------------------------------------------------------------------------
Flash messages
------------------------------------------------------------------------------*/
.flash-container {
max-width: 800px;
margin: 1rem auto;
padding: 0 1rem;
}
.flash {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 6px;
font-weight: 500;
color: #fff;
box-shadow: var(--shadow-1);
}
.flash-info { background-color: var(--flash-info); }
.flash-message,
.flash-success { background-color: var(--flash-success); }
.flash-warning { background-color: var(--flash-warning); }
.flash-danger,
.flash-error { background-color: var(--flash-danger); }
/*------------------------------------------------------------------------------
Pagination - Floating minimal bar
------------------------------------------------------------------------------*/
.floating-pagination {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(20, 20, 20, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 999px;
box-shadow: var(--shadow-3);
z-index: 100;
}
.fp-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
text-decoration: none;
font-size: 1.25rem;
transition: all 0.2s ease;
}
.fp-btn:hover:not(.disabled) {
background: rgba(255, 255, 255, 0.15);
color: white;
}
.fp-btn.disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
}
.fp-indicator {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0 0.5rem;
font-size: 0.9rem;
color: var(--text-dim);
}
.fp-current {
color: var(--text);
font-weight: 500;
}
.fp-sep {
color: var(--text-muted);
}
.fp-total {
color: var(--text-muted);
}
/* Legacy pagination (kept for other pages if needed) */
.pagination-container {
text-align: center;
margin: 2rem 0;
}
.pagination-link {
display: inline-block;
margin: 0 0.2rem;
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: var(--text-dim);
text-decoration: none;
background: rgba(255, 255, 255, 0.05);
transition: all 0.2s ease;
}
.pagination-link:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.25);
color: var(--text);
}
.pagination-link.active {
background: var(--btn-primary);
color: white;
border-color: var(--btn-primary);
}
.pagination-ellipsis { padding: 0 0.5rem; color: var(--text-muted); }
/*------------------------------------------------------------------------------
Gallery - Seamless grid with overlaid metadata
------------------------------------------------------------------------------*/
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 4px;
padding: 4px;
}
.gallery-item {
position: relative;
height: 320px;
background-color: var(--panel);
overflow: hidden;
cursor: pointer;
}
.gallery-thumb {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: transform 0.3s ease, filter 0.3s ease;
}
.gallery-item:hover .gallery-thumb {
transform: scale(1.03);
filter: brightness(1.1);
}
/* Date overlay at bottom */
.image-date {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 6px 8px;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.9);
background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 100%);
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
z-index: 2;
pointer-events: none;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.gallery-item:hover .image-date {
opacity: 1;
}
.gallery-item a {
color: var(--link);
font-weight: 500;
text-decoration: none;
font-size: 0.9rem;
}
.gallery-item a:hover { text-decoration: underline; color: var(--link-hover); }
/* video play glyph sits under tags, above image */
.play-overlay {
position: absolute;
inset: 0;
display: grid;
place-items: center;
z-index: 1;
pointer-events: none;
}
.play-overlay::before {
content: '▶';
font-size: 2rem;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 0 12px rgba(0,0,0,0.9), 0 2px 6px rgba(0,0,0,0.8);
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
padding-left: 4px; /* optical centering of triangle */
}
/*------------------------------------------------------------------------------
Modal viewer
------------------------------------------------------------------------------*/
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.92);
justify-content: center;
align-items: center;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.modal.active { display: flex; }
.modal-content {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1rem;
}
/* Modal body - horizontal layout with image on left, tags on right */
.modal-body {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 1rem;
width: 100%;
max-width: 1600px;
max-height: 100%;
}
/* On smaller screens, stack vertically */
@media (max-width: 900px) {
.modal-body {
flex-direction: column;
align-items: center;
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
}
/* Mobile landscape: prioritize image, minimize tag editor */
@media (max-width: 900px) and (orientation: landscape) {
.modal-body {
flex-direction: row;
align-items: stretch;
}
.modal-image-wrapper {
max-width: calc(100% - 200px);
max-height: calc(100vh - 2rem);
height: calc(100vh - 2rem);
}
.tag-editor {
width: 180px;
min-width: 180px;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
}
/* Image wrapper & zoom */
.modal-image-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: calc(100% - 300px); /* Account for tag editor */
height: calc(100vh - 140px);
max-height: calc(100vh - 140px);
overflow: hidden;
cursor: default;
}
.modal-image-wrapper.zoomed {
overflow: auto;
cursor: grab;
/* Enable scrolling by removing flex centering when zoomed */
display: block;
}
.modal-image-wrapper.zoomed img {
width: auto;
height: auto;
max-width: none;
max-height: none;
cursor: grab;
}
.modal-image-wrapper img {
max-width: 100%;
max-height: calc(100vh - 140px);
object-fit: contain;
border-radius: 4px;
}
.modal-image-wrapper video {
max-width: 100%;
max-height: calc(100vh - 140px);
border-radius: 4px;
}
/* Tall images */
.modal-image-wrapper.too-tall { overflow-y: auto; }
.modal-image-wrapper.too-tall img {
width: 90vw; height: auto; max-height: none;
}
/* On smaller screens (portrait), full width for image wrapper */
@media (max-width: 900px) and (orientation: portrait) {
.modal-image-wrapper {
max-width: 100%;
height: auto;
max-height: 55vh;
flex-shrink: 0;
}
.modal-image-wrapper img,
.modal-image-wrapper video {
max-height: 55vh;
}
}
/* Modal navigation buttons - cleaner pill style */
.modal-button {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
font-size: 1.25rem;
cursor: pointer;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.2s ease;
border-radius: 50%;
}
.modal-button:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
.modal-content:hover .modal-button { opacity: 1; }
.modal-prev { left: 1rem; }
.modal-next { right: 1rem; }
/* Mobile: always show nav buttons, larger touch targets */
@media (max-width: 900px) {
.modal-button {
opacity: 0.8;
width: 44px;
height: 44px;
}
.modal-prev { left: 0.5rem; }
.modal-next { right: 0.5rem; }
}
/* Close button */
.modal-close-btn {
position: absolute;
top: 1rem;
right: 1rem;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
color: rgba(255, 255, 255, 0.8);
font-size: 1.5rem;
cursor: pointer;
z-index: 1002;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
line-height: 1;
}
.modal-close-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
border-color: rgba(255, 255, 255, 0.3);
}
/* Close hint - hidden on mobile since we have a button */
.modal-close-hint {
position: absolute;
top: 1.5rem;
right: 4rem;
color: rgba(255, 255, 255, 0.4);
font-size: 0.8rem;
pointer-events: none;
}
@media (max-width: 640px) {
.modal-close-hint { display: none; }
}
/* Modal sidebar sections */
.modal-sidebar-section {
padding: 0.75rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.modal-sidebar-section:last-child {
border-bottom: none;
}
/* Series toggle button (for "Add to Series" collapsed state) */
.modal-series-toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
background: none;
border: none;
color: var(--text-muted);
font-size: 0.85rem;
cursor: pointer;
padding: 0.25rem 0;
transition: color 0.15s ease;
}
.modal-series-toggle:hover {
color: var(--text-dim);
}
.modal-series-toggle .toggle-arrow {
transition: transform 0.15s ease;
font-size: 0.9rem;
}
.modal-series-toggle.open .toggle-arrow {
transform: rotate(90deg);
}
.add-series-body {
margin-top: 0.5rem;
}
/* In-context feedback label */
.tag-feedback {
font-size: 0.75rem;
color: var(--flash-success);
opacity: 0;
transition: opacity 0.2s ease;
display: inline-block;
margin-left: 0.4rem;
}
.tag-feedback.visible {
opacity: 1;
}
.tag-feedback-inline {
display: block;
margin-left: 0;
margin-top: 0.3rem;
min-height: 1.1em;
}
/* Tag chip remove button in modal — hidden at rest, visible on hover */
.tag-editor .tag-chip .x {
opacity: 0;
transition: opacity 0.15s ease;
margin-left: 4px;
}
.tag-editor .tag-chip:hover .x {
opacity: 1;
}
/*------------------------------------------------------------------------------
Tag overlays & editor
------------------------------------------------------------------------------*/
/* Pills used inside overlays and editors */
.tags {
margin-top: .4rem;
display: flex;
flex-wrap: wrap;
gap: .25rem;
}
/* Tag chips (truncate long names nicely) */
.tag-chip {
display: inline-block; /* required for ellipsis */
max-width: clamp(8rem, 22vw, 14rem); /* responsive cap */
padding: 2px 8px;
border-radius: 999px;
background: rgba(255,255,255,0.92);
color: #334155;
text-decoration: none;
border: 1px solid rgba(199,210,254,0.9);
white-space: nowrap; /* single line */
overflow: hidden; /* hide overflow */
text-overflow: ellipsis; /* … */
vertical-align: middle;
font: 12px/1.6 system-ui, sans-serif;
}
.tag-chip:hover { background: rgba(255,255,255,1); }
/* Modal tag editor - side panel layout */
.tag-editor {
width: 280px;
min-width: 280px;
max-height: calc(100vh - 40px);
overflow-y: auto;
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
/* On smaller screens (portrait), make tag editor full width and scrollable */
@media (max-width: 900px) and (orientation: portrait) {
.tag-editor {
width: 100%;
max-width: 500px;
min-width: unset;
max-height: 35vh;
overflow-y: auto;
flex-shrink: 0;
}
}
/* Series info in modal tag editor */
.modal-series-info {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.series-info-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.4rem;
}
.series-info-row:last-child {
margin-bottom: 0;
}
.series-info-label {
color: var(--text-muted);
font-size: 0.85rem;
}
.series-info-link {
color: var(--accent);
text-decoration: none;
font-size: 0.9rem;
}
.series-info-link:hover {
text-decoration: underline;
}
.series-page-input {
width: 60px;
padding: 0.25rem 0.4rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
color: var(--text);
font-size: 0.85rem;
}
.btn-small {
padding: 0.25rem 0.5rem;
background: var(--btn-secondary);
border: none;
border-radius: 4px;
color: var(--text);
font-size: 0.8rem;
cursor: pointer;
}
.btn-small:hover {
background: var(--btn-secondary-hover);
}
.btn-small:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-update-status {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Add to series section in modal */
.modal-add-to-series {
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.add-series-header {
margin-bottom: 0.5rem;
}
.add-series-controls {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.series-select {
flex: 1;
min-width: 120px;
padding: 0.3rem 0.5rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
color: var(--text);
font-size: 0.85rem;
cursor: pointer;
}
.series-select option {
background: var(--panel);
color: var(--text);
}
.tag-editor .tags {
margin: 0 0 0.5rem 0;
min-height: 28px;
}
/* Tag chips in modal - with remove button */
.tag-editor .tag-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: var(--text);
}
.tag-editor .tag-chip:hover {
background: rgba(255, 255, 255, 0.18);
}
.tag-editor .tag-chip .x {
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
padding: 0;
margin-left: 2px;
font-size: 1rem;
line-height: 1;
}
.tag-editor .tag-chip .x:hover {
color: #ef4444;
}
/* Modal provenance section — system-generated, non-editable tags (artist, post) */
.modal-provenance-section {
border-bottom: none;
}
.modal-provenance-section .provenance-list {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.provenance-chip {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 2px 8px 2px 10px;
border-radius: 999px;
background: transparent;
color: var(--text-dim);
text-decoration: none;
border: 1px solid rgba(255, 255, 255, 0.15);
border-left-width: 2px;
border-left-color: rgba(255, 255, 255, 0.4);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font: 12px/1.6 system-ui, sans-serif;
max-width: clamp(8rem, 22vw, 14rem);
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.provenance-chip:hover {
border-color: rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
}
.provenance-chip[data-platform="patreon"] {
border-left-color: #f96854;
}
.provenance-chip[data-platform="subscribestar"] {
border-left-color: #009cde;
}
.provenance-chip[data-platform="hentaifoundry"] {
border-left-color: #c8232c;
}
.provenance-chip .provenance-arrow {
opacity: 0.6;
font-size: 0.85em;
}
/* Modal suggestions section — ML-backed chips with accept/reject */
.modal-suggestions-section .section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.4rem;
}
.modal-suggestions-section .section-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-dim);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.modal-suggestions-section .suggestions-refresh {
background: transparent;
border: none;
color: var(--text-dim);
cursor: pointer;
font-size: 0.9rem;
padding: 0 0.25rem;
}
.modal-suggestions-section .suggestions-refresh:hover {
color: var(--text);
}
.suggestions-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.suggestions-category {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.suggestions-category-label {
font-size: 0.7rem;
color: var(--text-dim);
opacity: 0.65;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.suggestions-chips {
display: flex;
flex-direction: column;
gap: 4px;
}
.suggestion-chip {
display: flex;
align-items: stretch;
gap: 6px;
min-height: 36px;
padding: 0;
border-radius: 6px;
background: rgba(255, 255, 255, 0.02);
color: var(--text-dim);
border: 1px dashed rgba(255, 255, 255, 0.18);
font: 13px/1.3 system-ui, sans-serif;
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, opacity 0.15s ease, transform 0.18s ease;
}
.suggestion-chip:hover {
border-color: rgba(255, 255, 255, 0.35);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
}
.suggestion-chip .sugg-body {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
padding: 0 2px;
}
.suggestion-chip .sugg-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.suggestion-chip .sugg-confidence {
font-size: 0.72rem;
opacity: 0.6;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.suggestion-chip .sugg-btn {
flex: 0 0 auto;
width: 32px;
min-height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 4px;
color: inherit;
cursor: pointer;
padding: 0;
font-size: 0.95rem;
line-height: 1;
margin: 1px;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.suggestion-chip .sugg-btn.sugg-accept {
color: #8be78b;
border-color: rgba(139, 231, 139, 0.35);
}
.suggestion-chip .sugg-btn.sugg-accept:hover {
background: rgba(139, 231, 139, 0.12);
border-color: rgba(139, 231, 139, 0.6);
}
.suggestion-chip .sugg-btn.sugg-reject {
color: #e78b8b;
border-color: rgba(231, 139, 139, 0.35);
}
.suggestion-chip .sugg-btn.sugg-reject:hover {
background: rgba(231, 139, 139, 0.12);
border-color: rgba(231, 139, 139, 0.6);
}
.suggestion-chip .sugg-btn.sugg-block {
color: #ffb400;
border-color: rgba(255, 180, 0, 0.35);
}
.suggestion-chip .sugg-btn.sugg-block:hover {
background: rgba(255, 180, 0, 0.12);
border-color: rgba(255, 180, 0, 0.6);
}
/* Auto-accepted suggestion block (top of suggestions section) */
.suggestions-auto-accepted {
margin-bottom: 0.75rem;
padding: 0.5rem;
background: rgba(139, 231, 139, 0.04);
border: 1px solid rgba(139, 231, 139, 0.2);
border-radius: 6px;
}
.suggestions-auto-accepted .suggestions-category-label {
color: #8be78b;
font-weight: 600;
}
.suggestion-chip.suggestion-chip-auto {
border-color: rgba(139, 231, 139, 0.3);
background: rgba(139, 231, 139, 0.08);
}
.suggestion-chip .sugg-btn.sugg-undo {
color: #e78b8b;
border-color: rgba(231, 139, 139, 0.35);
}
.suggestion-chip .sugg-btn.sugg-undo:hover {
background: rgba(231, 139, 139, 0.12);
border-color: rgba(231, 139, 139, 0.6);
}
/* Settings: threshold number input + blocklist textarea */
.settings-input {
padding: 0.4rem 0.6rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-size: 0.95rem;
}
.settings-input:focus {
outline: none;
border-color: var(--btn-primary);
background: rgba(255, 255, 255, 0.12);
}
.settings-textarea {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-family: var(--mono, monospace);
font-size: 0.9rem;
resize: vertical;
min-height: 8rem;
}
.settings-textarea:focus {
outline: none;
border-color: var(--btn-primary);
background: rgba(255, 255, 255, 0.12);
}
.suggestion-chip.leaving {
opacity: 0;
transform: translateX(6px);
}
/* Tag form with autocomplete */
.tag-form {
display: flex;
gap: 0.5rem;
position: relative;
}
.tag-form-wrapper {
flex: 1;
position: relative;
}
.tag-form input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-size: 16px; /* Must be 16px+ to prevent iOS auto-zoom on focus */
}
.tag-form input::placeholder {
color: var(--text-muted);
}
.tag-form input:focus {
outline: none;
border-color: var(--btn-primary);
background: rgba(255, 255, 255, 0.12);
}
.tag-form button {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
background: var(--btn-primary);
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background 0.2s ease;
white-space: nowrap;
}
.tag-form button:hover {
background: var(--btn-primary-hover);
}
/* Autocomplete dropdown - uses fixed positioning to escape overflow containers */
.tag-autocomplete {
position: fixed;
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
max-height: 240px;
overflow-y: auto;
z-index: 10100;
display: none;
box-shadow: var(--shadow-3);
min-width: 200px;
max-width: calc(100vw - 16px);
box-sizing: border-box;
}
.tag-autocomplete.active {
display: block;
}
/* Inline variant: the modal uses a reserved area that sits below the add-tag
input instead of floating over provenance/suggestions. Height fits ~5 rows;
overflow scrolls when more matches are loaded. */
.tag-autocomplete-inline {
display: block;
position: static;
margin-top: 0.5rem;
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
height: 11.5rem;
max-height: none;
min-width: 0;
max-width: none;
overflow-y: auto;
box-shadow: none;
z-index: auto;
}
.tag-autocomplete-item {
padding: 0.5rem 0.75rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
line-height: 1.2;
color: var(--text);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.tag-autocomplete-item:last-child {
border-bottom: none;
}
.tag-autocomplete-item:hover,
.tag-autocomplete-item.selected {
background: rgba(255, 255, 255, 0.1);
}
.tag-autocomplete-item .tag-kind {
font-size: 0.75rem;
color: var(--text-muted);
margin-left: auto;
}
.tag-autocomplete-empty {
padding: 0.5rem 0.75rem;
color: var(--text-muted);
font-size: 0.85rem;
font-style: italic;
}
/* Tag overlay bar at the top of the image */
.tag-overlay {
position: absolute;
top: 0; left: 0; right: 0;
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 6px;
z-index: 2;
background: linear-gradient(to bottom, rgba(0,0,0,0.45), rgba(0,0,0,0));
}
/*------------------------------------------------------------------------------
Tag cards (tags_list.html) - New design
------------------------------------------------------------------------------*/
.tag-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
padding: 0.5rem;
}
.tag-card {
position: relative;
background: var(--panel);
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.tag-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-3);
}
.tag-card-link {
text-decoration: none;
color: inherit;
display: block;
}
.tag-preview {
display: flex;
gap: 2px;
height: 140px;
overflow: hidden;
}
.tag-thumb {
flex: 1;
min-width: 0;
height: 100%;
object-fit: cover;
background-color: var(--bg-elevated);
transition: filter 0.2s ease;
}
.tag-card:hover .tag-thumb {
filter: brightness(1.08);
}
.tag-thumb-empty {
background: linear-gradient(135deg, var(--bg-elevated) 0%, var(--panel) 100%);
}
.tag-card-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: linear-gradient(to top, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.2) 100%);
}
.tag-icon {
font-size: 1.1rem;
flex-shrink: 0;
}
.tag-name {
flex: 1;
font-weight: 500;
font-size: 0.95rem;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tag-count {
font-size: 0.8rem;
color: var(--text-muted);
background: rgba(255,255,255,0.1);
padding: 0.2rem 0.5rem;
border-radius: 999px;
flex-shrink: 0;
}
.tag-edit-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(0,0,0,0.6);
border: 1px solid rgba(255,255,255,0.1);
color: var(--text-dim);
cursor: pointer;
opacity: 0;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
}
.tag-card:hover .tag-edit-btn {
opacity: 1;
}
.tag-edit-btn:hover {
background: rgba(0,0,0,0.8);
color: var(--text);
border-color: rgba(255,255,255,0.2);
}
.empty-state {
text-align: center;
color: var(--text-muted);
padding: 3rem 1rem;
font-size: 1.1rem;
}
/* Tag edit modal specific */
.tag-edit-modal-content {
max-width: 400px;
padding: 2rem;
background: var(--panel);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.1);
}
.tag-edit-modal-content h2 {
margin: 0 0 1.5rem 0;
text-align: center;
color: var(--text);
}
.tag-edit-form .form-group {
margin-bottom: 1rem;
}
.tag-edit-form .form-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
}
.tag-edit-form .form-actions .btn {
flex: 1;
}
/* Legacy artist grid (kept for backward compatibility) */
.artist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 6px;
padding: 0.5rem;
}
.artist-card-link {
text-decoration: none;
color: inherit;
display: block;
transition: transform 0.3s ease;
}
.artist-card-link:hover .artist-card {
transform: scale(1.02);
box-shadow: var(--shadow-3);
}
.artist-card {
background-color: var(--panel);
padding: 0;
border-radius: 4px;
overflow: hidden;
box-shadow: var(--shadow-2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.artist-card h3 {
margin: 0;
padding: 0.75rem;
text-align: center;
color: var(--text);
font-weight: 500;
font-size: 0.95rem;
background: rgba(0,0,0,0.3);
}
.artist-preview {
display: flex;
gap: 2px;
justify-content: center;
overflow: hidden;
}
.artist-thumb {
flex: 1;
height: 280px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transition: transform 0.3s ease, filter 0.3s ease;
}
.artist-card-link:hover .artist-thumb {
filter: brightness(1.1);
}
/* Tag search bar */
.tag-search-bar {
display: flex;
align-items: center;
gap: 1rem;
max-width: 500px;
margin: 0 auto 1rem;
}
.tag-search-input {
flex: 1;
padding: 0.6rem 1rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.tag-search-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
}
.tag-search-input::placeholder {
color: var(--muted);
}
.tag-search-count {
color: var(--muted);
font-size: 0.9rem;
white-space: nowrap;
}
/* Filter pills (top of tags_list) */
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0 1.5rem;
justify-content: center;
}
.filter-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
border-radius: 999px;
border: 1px solid var(--filter-border);
background: var(--filter-bg);
color: var(--filter-fg);
text-decoration: none;
font: 14px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
transition: all 0.2s ease;
}
.filter-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.25);
}
.filter-btn.is-active {
background: var(--filter-active-bg);
color: var(--filter-active-fg);
border-color: var(--filter-active-border);
}
.filter-btn:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/*------------------------------------------------------------------------------
Settings
------------------------------------------------------------------------------*/
/* Settings page title */
.settings-title {
text-align: center;
margin-top: 1rem;
margin-bottom: 1rem;
}
/* Settings tab bar */
.settings-tab-bar {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
max-width: 1600px;
margin-left: auto;
margin-right: auto;
padding: 0 1.5rem;
}
.settings-tab-btn {
display: inline-block;
text-decoration: none;
padding: 0.4rem 1.1rem;
border-radius: 999px;
border: 1px solid var(--filter-border);
background: var(--filter-bg);
color: var(--filter-fg);
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
.settings-tab-btn:hover {
background: rgba(255, 255, 255, 0.12);
}
.settings-tab-btn.is-active {
background: var(--filter-active-bg);
border-color: var(--filter-active-border);
color: var(--filter-active-fg);
}
/* Settings tab panels — always visible, server renders only the active one */
.settings-tab-panel {
display: block;
}
/* Maintenance tab — visually subdued */
.settings-tab-panel[data-tab="maintenance"] .settings-container {
opacity: 0.8;
transition: opacity 0.2s ease;
}
.settings-tab-panel[data-tab="maintenance"] .settings-container:hover {
opacity: 1;
}
/* Two-column layout for Import tab */
.settings-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
align-items: start;
}
@media (max-width: 900px) {
.settings-two-col {
grid-template-columns: 1fr;
}
}
.settings-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
max-width: 1600px;
margin: 1.5rem auto;
padding: 0 1.5rem;
align-items: start;
}
.settings-grid-full {
max-width: 1600px;
margin: 1.5rem auto;
padding: 0 1.5rem;
}
.settings-column {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (max-width: 768px) {
.settings-grid {
grid-template-columns: 1fr;
padding: 0 1rem;
}
.settings-grid-full {
padding: 0 1rem;
}
}
.settings-container {
padding: 1.5rem;
background: var(--panel);
border-radius: 8px;
box-shadow: var(--shadow-2);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.settings-section h2,
.settings-info h2 {
margin-bottom: 1rem;
text-align: center;
color: var(--text);
font-weight: 500;
}
.settings-actions {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.btn {
padding: 0.75rem 2rem;
font-size: 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
min-width: 200px;
}
.btn:hover {
transform: translateY(-1px);
}
.primary-btn { background-color: var(--btn-primary); color: #fff; }
.primary-btn:hover { background-color: var(--btn-primary-hover); }
.warning-btn { background-color: var(--flash-warning); color: #fff; }
.warning-btn:hover { background-color: #d97706; }
.danger-btn { background-color: var(--btn-danger); color: #fff; }
.danger-btn:hover { background-color: var(--btn-danger-hover); }
.settings-info {
font-size: 0.9rem;
color: var(--text-muted);
text-align: center;
line-height: 1.5;
}
/*------------------------------------------------------------------------------
Showcase - Full-bleed Masonry Collage
------------------------------------------------------------------------------*/
.showcase-body {
margin: 0;
padding: 0;
background: #0a0a0a;
min-height: 100vh;
overflow-x: hidden;
}
/* Floating navbar overlay */
.showcase-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.75rem 1rem;
background: linear-gradient(to bottom, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.6) 70%, rgba(0,0,0,0) 100%);
pointer-events: none;
padding-bottom: 1rem;
}
.showcase-nav-links {
display: flex;
justify-content: center;
gap: 0.25rem;
pointer-events: auto;
}
.showcase-nav .nav-link {
pointer-events: auto;
color: #fff;
text-decoration: none;
padding: 0.5rem 1rem;
font-size: 1rem;
font-weight: 500;
border-radius: 6px;
transition: background 0.2s ease;
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
}
.showcase-nav .nav-link:hover {
background: rgba(255,255,255,0.15);
}
/* Dropdown within showcase nav */
.showcase-nav .nav-dropdown {
position: relative;
display: inline-block;
pointer-events: auto;
}
.showcase-nav .nav-dropdown-trigger {
cursor: pointer;
border: none;
background: transparent;
font-family: inherit;
}
.showcase-nav .nav-dropdown-menu {
left: 50%;
transform: translateX(-50%);
}
.showcase-nav .nav-hint {
opacity: 0;
transition: opacity 0.2s ease;
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 0.25rem;
pointer-events: none;
}
.showcase-nav:hover .nav-hint {
opacity: 1;
}
/* Masonry container - JS-managed columns */
.masonry-container {
display: flex;
gap: 4px;
padding: 60px 4px 4px 4px; /* top padding for navbar */
min-height: 100vh;
align-items: flex-start;
max-width: 2200px; /* prevent columns from getting too wide on ultrawide */
margin: 0 auto;
}
.masonry-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
/* Masonry item */
.masonry-item {
position: relative;
cursor: pointer;
overflow: hidden;
background: #1a1a1a;
}
.masonry-item img {
display: block;
width: 100%;
height: auto;
transition: transform 0.3s ease, filter 0.3s ease;
}
.masonry-item:hover img {
transform: scale(1.03);
filter: brightness(1.1);
}
/* Tag overlay on masonry items */
.masonry-item .tag-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
padding: 6px;
background: linear-gradient(to bottom, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0) 100%);
display: flex;
flex-wrap: wrap;
gap: 4px;
z-index: 2;
}
/* Ghost tag count badge on showcase items */
.tag-count-badge {
position: absolute;
bottom: 8px;
right: 8px;
padding: 2px 7px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.55);
font-size: 0.7rem;
font-weight: 500;
z-index: 2;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s ease;
}
.masonry-item:hover .tag-count-badge {
opacity: 1;
}
/* Touch devices: always show badge */
@media (hover: none) {
.tag-count-badge {
opacity: 1;
}
}
/* Loading state for shuffle */
.masonry-container.loading {
opacity: 0.5;
pointer-events: none;
}
.masonry-container {
transition: opacity 0.3s ease;
}
/* Stagger entry animation for showcase items */
@keyframes itemFadeIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.masonry-item.animating-in {
animation: itemFadeIn 0.25s ease forwards;
animation-delay: calc(var(--stagger-index, 0) * 60ms);
opacity: 0; /* Start hidden until animation fires */
}
@media (prefers-reduced-motion: reduce) {
.masonry-item.animating-in {
animation: none;
opacity: 1;
}
}
/*------------------------------------------------------------------------------
Forms
------------------------------------------------------------------------------*/
.form-container {
max-width: 400px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--panel);
border-radius: 8px;
box-shadow: var(--shadow-2);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.form-card { display: flex; flex-direction: column; gap: 1rem; }
.form-group { display: flex; flex-direction: column; }
.form-label { margin-bottom: 0.5rem; font-weight: 500; color: var(--text); }
.form-input {
padding: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-size: 1rem;
}
.form-input:focus {
outline: none;
border-color: var(--btn-primary);
background: rgba(255, 255, 255, 0.12);
}
/* Select dropdowns - ensure dark theme for options */
select.form-input {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.5rem;
}
select.form-input option,
select.form-input optgroup {
background: #1a1a1a;
color: #ffffff;
}
select.form-input optgroup {
font-weight: 600;
color: #a0a0a0;
}
.form-button {
padding: 0.75rem;
background-color: var(--btn-primary);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s ease;
}
.form-button:hover { background-color: var(--btn-primary-hover); }
.form-footer-text { margin-top: 1rem; text-align: center; color: var(--text-dim); }
/* Fixed-height, 2-line title area so all cards match height */
.card-title {
/* reserve exactly two lines of space for the title */
line-height: 1.3;
min-height: calc(1.3em * 2);
max-height: calc(1.3em * 2);
overflow: hidden;
margin-bottom: 0.5rem;
}
/* Multi-line clamp with ellipsis (where supported) */
.card-title-text {
margin: 0;
font-size: 1.1rem;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2; /* clamp to 2 lines */
-webkit-box-orient: vertical;
overflow: hidden;
/* good fallbacks for very long single words / no spaces */
overflow-wrap: anywhere; /* allows breaking long tokens */
word-break: break-word;
}
/*------------------------------------------------------------------------------
Gallery Infinite Scroll Layout
------------------------------------------------------------------------------*/
.gallery-infinite-container {
display: flex;
min-height: 100vh;
max-width: 1800px;
margin: 0 auto;
position: relative;
}
/* Timeline Sidebar - positioned relative to gallery container */
.timeline-sidebar {
position: sticky;
top: 60px;
height: calc(100vh - 60px);
width: 70px;
min-width: 70px;
background: rgba(0,0,0,0.3);
border-right: 1px solid rgba(255,255,255,0.08);
z-index: 50;
display: flex;
flex-direction: column;
padding: 0.5rem 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.2) transparent;
}
.timeline-sidebar::-webkit-scrollbar {
width: 4px;
}
.timeline-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.timeline-sidebar::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.2);
border-radius: 2px;
}
.timeline-track {
display: flex;
flex-direction: column;
padding: 0 6px;
}
.timeline-loading {
font-size: 0.7rem;
color: var(--text-muted);
padding: 0.5rem;
text-align: center;
}
.timeline-year-group {
margin-bottom: 0.5rem;
}
.timeline-year {
font-size: 0.8rem;
font-weight: 600;
color: var(--text);
padding: 0.5rem 0.25rem 0.25rem;
text-align: center;
position: sticky;
top: 0;
background: linear-gradient(to bottom, rgba(10,10,10,0.95), rgba(10,10,10,0.8));
z-index: 1;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.timeline-months {
display: flex;
flex-direction: column;
gap: 2px;
padding-top: 4px;
}
.timeline-month {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.35rem 0.5rem;
font-size: 0.7rem;
color: var(--text-muted);
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
}
.timeline-month:hover {
background: rgba(255,255,255,0.12);
color: var(--text);
}
.timeline-month.active {
background: var(--btn-primary);
color: white;
}
.timeline-month.loaded {
color: var(--text-dim);
}
.timeline-month-label {
font-weight: 500;
}
.timeline-month-count {
font-size: 0.6rem;
opacity: 0.7;
}
/* Main gallery area - flex child next to timeline */
.gallery-main {
flex: 1;
padding: 0 0.5rem;
min-width: 0;
}
.gallery-header {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem 0 1rem;
gap: 0.5rem;
}
.gallery-header h1 {
margin: 0;
font-size: 1.5rem;
}
.active-filter {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255,255,255,0.08);
border-radius: 999px;
font-size: 0.9rem;
color: var(--text-dim);
}
.filter-tag {
color: var(--link);
font-weight: 500;
}
.clear-filter {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255,255,255,0.1);
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
line-height: 1;
transition: all 0.15s ease;
}
.clear-filter:hover {
background: rgba(255,255,255,0.2);
color: var(--text);
}
/* Post Info Header (shown when filtering by post tag) */
.post-info-header {
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
margin: 0 0 1.5rem 0;
overflow: hidden;
}
.post-info-content {
padding: 1.25rem 1.5rem;
}
.post-title {
margin: 0 0 0.75rem 0;
font-size: 1.35rem;
font-weight: 600;
color: var(--text);
line-height: 1.3;
}
.post-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 1rem;
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: var(--text-dim);
}
.post-artist {
font-weight: 500;
color: var(--link);
}
.post-platform {
display: inline-flex;
align-items: center;
padding: 0.2rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: capitalize;
background: rgba(255, 255, 255, 0.1);
color: var(--text-dim);
}
.post-platform-patreon {
background: rgba(255, 66, 77, 0.2);
color: #ff6b6b;
}
.post-platform-subscribestar {
background: rgba(255, 153, 0, 0.2);
color: #ffaa33;
}
.post-platform-hentaifoundry {
background: rgba(139, 92, 246, 0.2);
color: #a78bfa;
}
.post-date {
color: var(--text-muted);
}
.post-count {
color: var(--text-muted);
}
.post-count::before {
content: "•";
margin-right: 0.5rem;
}
.post-description {
margin: 0.75rem 0;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-dim);
max-height: 200px;
overflow-y: auto;
}
.post-description:empty {
display: none;
}
/* Style any HTML content in descriptions */
.post-description a {
color: var(--link);
}
.post-description a:hover {
color: var(--link-hover);
}
.post-description p {
margin: 0 0 0.75rem 0;
}
.post-description p:last-child {
margin-bottom: 0;
}
.post-description img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.post-source-link {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(99, 102, 241, 0.15);
border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 6px;
color: var(--link);
text-decoration: none;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
}
.post-source-link:hover {
background: rgba(99, 102, 241, 0.25);
border-color: rgba(99, 102, 241, 0.5);
color: var(--link-hover);
}
.post-source-link::after {
content: "↗";
font-size: 0.9em;
}
/* Series Header (shown when filtering by series tag) */
.series-header {
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
margin: 0 0 1.5rem 0;
overflow: hidden;
}
.series-header-content {
padding: 1rem 1.25rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.series-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.series-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
}
.series-page-count {
color: var(--text-muted);
font-size: 0.9rem;
background: rgba(255, 255, 255, 0.08);
padding: 0.25rem 0.6rem;
border-radius: 4px;
}
.series-header-actions {
display: flex;
gap: 0.5rem;
}
/* Series Editor Panel */
.series-editor-panel {
padding: 1rem 1.25rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
}
.series-editor-info {
margin-bottom: 0.75rem;
}
.series-editor-info p {
margin: 0;
color: var(--text-dim);
font-size: 0.85rem;
}
.series-editor-controls {
display: flex;
align-items: flex-end;
gap: 1rem;
flex-wrap: wrap;
}
.series-editor-input {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.series-editor-input label {
font-size: 0.8rem;
color: var(--text-muted);
}
.series-editor-input input {
width: 100px;
padding: 0.4rem 0.6rem;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: var(--text);
font-size: 0.9rem;
}
.series-editor-feedback {
margin-top: 0.75rem;
}
.series-editor-feedback .feedback-success {
padding: 0.5rem 0.75rem;
background: rgba(34, 197, 94, 0.15);
border: 1px solid rgba(34, 197, 94, 0.3);
border-radius: 6px;
color: #22c55e;
font-size: 0.85rem;
}
.series-editor-feedback .feedback-error {
padding: 0.5rem 0.75rem;
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
color: #ef4444;
font-size: 0.85rem;
}
#seriesEditModeBtn.active {
background: var(--btn-primary);
color: white;
}
@media (max-width: 768px) {
.series-header-content {
flex-direction: column;
align-items: flex-start;
}
.series-editor-controls {
flex-direction: column;
align-items: stretch;
}
.series-editor-input {
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
}
@media (max-width: 768px) {
.post-info-content {
padding: 1rem;
}
.post-title {
font-size: 1.15rem;
}
.post-meta {
font-size: 0.8rem;
}
.post-description {
max-height: 150px;
font-size: 0.85rem;
}
}
/* Gallery content area */
.gallery-infinite {
min-height: 50vh;
}
.gallery-empty {
text-align: center;
padding: 4rem 1rem;
color: var(--text-muted);
}
/* Date sections */
.date-section {
margin-bottom: 1.5rem;
}
.date-divider {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 0.5rem;
margin: 0 0 0.5rem 0;
border-bottom: 1px solid rgba(255,255,255,0.1);
position: sticky;
top: 50px;
background: linear-gradient(to bottom, var(--bg) 0%, var(--bg) 85%, transparent 100%);
z-index: 10;
}
.date-divider-text {
font-size: 1.1rem;
font-weight: 500;
color: var(--text);
}
.date-divider-count {
font-size: 0.8rem;
color: var(--text-muted);
background: rgba(255,255,255,0.08);
padding: 0.2rem 0.6rem;
border-radius: 999px;
}
/* Loading indicator */
.gallery-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 2rem;
color: var(--text-muted);
}
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255,255,255,0.2);
border-top-color: var(--btn-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.gallery-end {
text-align: center;
padding: 2rem;
color: var(--text-muted);
font-style: italic;
border-top: 1px solid rgba(255,255,255,0.05);
}
/* Infinite scroll loading indicator */
.loading-indicator {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 2rem;
color: var(--text-muted);
}
.loading-indicator .spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid rgba(255,255,255,0.2);
border-top-color: var(--btn-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* Scroll sentinel for IntersectionObserver */
.scroll-sentinel {
height: 1px;
width: 100%;
}
/* Mobile: hide timeline sidebar */
@media (max-width: 768px) {
.timeline-sidebar {
display: none;
}
.date-divider {
top: 45px;
}
}
/*------------------------------------------------------------------------------
Floating Select Button (Gallery)
------------------------------------------------------------------------------*/
.floating-select-btn {
position: fixed;
top: 0.75rem;
right: 1rem;
z-index: 1001;
}
.floating-select-btn .btn {
box-shadow: var(--shadow-2);
}
@media (max-width: 640px) {
.floating-select-btn {
top: 0.5rem;
right: 0.5rem;
}
}
/*------------------------------------------------------------------------------
Bulk Select Mode
------------------------------------------------------------------------------*/
/* Select mode body state */
body.select-mode .img-clickable {
cursor: pointer;
position: relative;
}
/* Selection checkbox overlay */
body.select-mode .img-clickable::before {
content: '';
position: absolute;
top: 8px;
left: 8px;
width: 24px;
height: 24px;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 4px;
background: rgba(0, 0, 0, 0.4);
z-index: 10;
transition: all 0.15s ease;
}
body.select-mode .img-clickable.selected::before {
background: var(--btn-primary);
border-color: var(--btn-primary);
}
body.select-mode .img-clickable.selected::after {
content: '\2713';
position: absolute;
top: 8px;
left: 8px;
width: 24px;
height: 24px;
color: white;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 11;
}
/* Selected image highlight */
body.select-mode .img-clickable.selected {
outline: 3px solid var(--btn-primary);
outline-offset: -3px;
}
/* Prevent hover effects in select mode */
body.select-mode .gallery-item:hover {
transform: none;
}
/* Select button active state */
#selectModeBtn.active {
background: var(--btn-primary);
color: white;
}
/* Push gallery content left when bulk editor panel is visible */
body.select-mode .gallery-infinite-container {
margin-right: 320px;
transition: margin-right 0.3s ease;
}
@media (max-width: 768px) {
/* On mobile, don't shift content - panel will overlay */
body.select-mode .gallery-infinite-container {
margin-right: 0;
}
}
/*------------------------------------------------------------------------------
Bulk Editor Panel (slide-in sidebar)
------------------------------------------------------------------------------*/
.bulk-editor-panel {
position: fixed;
top: 0;
right: 0;
width: 320px;
height: 100vh;
background: var(--panel);
border-left: 1px solid rgba(255, 255, 255, 0.1);
z-index: 1100;
transform: translateX(100%);
transition: transform 0.3s ease;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-3);
overflow-y: auto;
overflow-x: hidden;
}
.bulk-editor-panel.active {
transform: translateX(0);
}
.bulk-editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.bulk-editor-header h3 {
margin: 0;
font-size: 1.1rem;
}
.bulk-editor-stats {
padding: 0.75rem 1rem;
background: rgba(255, 255, 255, 0.05);
font-size: 0.9rem;
color: var(--text-dim);
}
.bulk-editor-section {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.bulk-editor-section h4 {
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.bulk-editor-section .form-help {
margin: 0 0 0.5rem 0;
font-size: 0.8rem;
color: var(--text-muted);
}
.bulk-editor-section .tags {
min-height: 32px;
}
.bulk-editor-actions {
padding: 1rem;
margin-top: auto;
}
/* Removable tag chips in bulk editor */
.bulk-editor-section .tag-chip.removable {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.15);
color: var(--text);
cursor: default;
}
.bulk-editor-section .tag-chip.removable:hover {
background: rgba(255, 255, 255, 0.18);
}
.bulk-editor-section .tag-chip .x {
background: none;
border: none;
color: rgba(255, 255, 255, 0.5);
cursor: pointer;
padding: 0;
margin-left: 2px;
font-size: 1rem;
line-height: 1;
}
.bulk-editor-section .tag-chip .x:hover {
color: #ef4444;
}
/* Series section in bulk editor */
.bulk-editor-series {
background: rgba(100, 100, 255, 0.05);
border-top: 1px solid rgba(100, 100, 255, 0.15);
}
.bulk-editor-series h4 {
color: var(--text);
}
.bulk-editor-series .series-editor-controls {
margin-top: 0.5rem;
}
.bulk-editor-series .series-editor-feedback {
margin-top: 0.5rem;
}
/* Text muted helper */
.text-muted {
color: var(--text-muted);
font-size: 0.85rem;
}
/* Selection order badge on gallery items */
.selection-order-badge {
position: absolute;
top: 4px;
left: 4px;
width: 24px;
height: 24px;
background: var(--accent);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
z-index: 10;
pointer-events: none;
}
/* Mobile: full-width panel from bottom */
@media (max-width: 640px) {
.bulk-editor-panel {
width: 100%;
height: auto;
max-height: 60vh;
top: auto;
bottom: 0;
transform: translateY(100%);
border-left: none;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px 16px 0 0;
}
.bulk-editor-panel.active {
transform: translateY(0);
}
}
/*------------------------------------------------------------------------------
Series Reader View
------------------------------------------------------------------------------*/
.reader-container {
display: flex;
flex-direction: column;
height: calc(100vh - 60px);
background: var(--bg);
margin: -1rem;
}
.reader-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--panel);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
}
.reader-header.hidden {
opacity: 0;
pointer-events: none;
transform: translateY(-4px);
}
.reader-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
}
.reader-back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--text);
text-decoration: none;
font-size: 1.2rem;
transition: all 0.2s ease;
}
.reader-back-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.reader-title {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
color: var(--text);
}
.reader-page-count {
color: var(--text-muted);
font-size: 0.85rem;
}
.reader-header-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.reader-jump {
display: flex;
align-items: center;
gap: 0.5rem;
}
.reader-jump label {
color: var(--text-muted);
font-size: 0.85rem;
}
.reader-jump-select {
padding: 0.4rem 0.75rem;
background: var(--surface);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: var(--text);
font-size: 0.85rem;
cursor: pointer;
}
.reader-jump-select option {
background: var(--surface);
color: var(--text);
}
.reader-toggle-nav {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 8px;
color: var(--text);
font-size: 1.1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.reader-toggle-nav:hover {
background: rgba(255, 255, 255, 0.2);
}
.reader-body {
display: flex;
flex: 1;
overflow: hidden;
position: relative;
}
/* Navigation Sidebar */
.reader-nav {
position: fixed;
top: 0;
left: 0;
width: 200px;
height: 100vh;
background: var(--panel);
border-right: 1px solid rgba(255, 255, 255, 0.1);
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
.reader-nav.open {
transform: translateX(0);
}
.reader-nav-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.reader-nav-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 500;
}
.reader-nav-close {
width: 28px;
height: 28px;
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 6px;
color: var(--text);
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.reader-nav-close:hover {
background: rgba(255, 255, 255, 0.2);
}
.reader-nav-thumbs {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.reader-nav-thumb {
position: relative;
border-radius: 6px;
overflow: hidden;
transition: opacity 0.2s ease;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.reader-nav-thumb img {
width: 100%;
height: auto;
display: block;
}
.thumb-page-num {
position: absolute;
bottom: 4px;
right: 4px;
background: rgba(0, 0, 0, 0.75);
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
}
/* Main Reader Content */
.reader-main {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.reader-content {
flex: 1;
overflow-y: auto;
scroll-behavior: smooth;
padding: 0.25rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
background: var(--bg);
}
.reader-page {
position: relative;
width: 100%;
display: flex;
justify-content: center;
}
.reader-page img {
max-width: min(100%, 1200px);
height: auto;
display: block;
}
.page-indicator {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.75);
color: white;
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
opacity: 0;
transition: opacity 0.2s ease;
}
.reader-page:hover .page-indicator {
opacity: 1;
}
.reader-empty {
text-align: center;
padding: 4rem 1rem;
color: var(--text-muted);
}
/* Reading progress bar */
.reader-progress-bar {
position: fixed;
top: 0;
left: 0;
height: 2px;
width: 0%;
background: var(--btn-primary);
z-index: 1100;
transition: width 0.1s linear;
pointer-events: none;
}
/* Active nav thumbnail */
.reader-nav-thumb.active {
box-shadow: inset 3px 0 0 var(--btn-primary);
opacity: 1;
}
.reader-nav-thumb:not(.active) {
opacity: 0.5;
}
.reader-nav-thumb:hover {
opacity: 1;
}
/* Floating page indicator */
.floating-page-indicator {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 4px 10px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 999px;
font-size: 0.8rem;
color: var(--text-muted);
z-index: 100;
pointer-events: none;
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
/* Quick nav buttons */
.reader-quick-nav {
position: fixed;
right: 1.5rem;
bottom: 5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 100;
}
.reader-quick-btn {
padding: 0.5rem 0.75rem;
background: var(--panel);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: var(--text-dim);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
}
.reader-quick-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--text);
}
/* Mobile adjustments */
@media (max-width: 768px) {
.reader-header {
flex-wrap: wrap;
gap: 0.5rem;
}
.reader-header-left {
flex: 1;
}
.reader-title {
font-size: 0.95rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.reader-jump label {
display: none;
}
.reader-nav {
width: 100%;
max-width: 280px;
}
.reader-content {
padding: 0.5rem;
}
.floating-page-indicator {
bottom: 1rem;
font-size: 0.8rem;
}
.reader-quick-nav {
right: 1rem;
bottom: 4rem;
}
}
.null-fandom-counter-link {
display: inline-block;
padding: 0.35rem 0.75rem;
margin: 0.5rem 0;
background: rgba(255, 180, 0, 0.12);
border: 1px solid rgba(255, 180, 0, 0.3);
border-radius: 0.25rem;
color: #ffb400;
text-decoration: none;
font-size: 0.9rem;
}
.null-fandom-counter-link:hover {
background: rgba(255, 180, 0, 0.2);
}
.null-fandom-filter-active {
padding: 0.5rem 0.75rem;
margin: 0.5rem 0;
background: rgba(255, 180, 0, 0.08);
border-left: 3px solid #ffb400;
font-size: 0.9rem;
}
.character-no-fandom-chip {
display: inline-block;
padding: 0.1rem 0.4rem;
margin-left: 0.5rem;
font-size: 0.75rem;
background: rgba(255, 180, 0, 0.15);
color: #ffb400;
border-radius: 0.2rem;
cursor: help;
}
/* Fandom picker (Task 22 - fandom association during character tag creation) */
.fandom-picker-wrapper {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 180ms ease-out, opacity 180ms ease-out;
margin-top: 0;
}
.fandom-picker-wrapper.visible {
max-height: 16rem;
opacity: 1;
margin-top: 0.5rem;
}
.fandom-picker-input-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Match the main .tag-form input styling so the picker feels like the
same control as the add-tag field above it. */
.fandom-picker-input {
flex: 1;
padding: 0.5rem 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 6px;
background: rgba(255, 255, 255, 0.08);
color: var(--text);
font-size: 16px;
}
.fandom-picker-input::placeholder {
color: var(--text-muted);
}
.fandom-picker-input:focus {
outline: none;
border-color: var(--btn-primary);
background: rgba(255, 255, 255, 0.12);
}
/* Desktop: the form's submit button is redundant — Enter on the tag input
(and Enter in the fandom picker when no row is highlighted) already
submits. Keep the button visible on mobile where virtual keyboards make
Enter-to-submit less reliable. */
@media (min-width: 768px) {
.tag-form > button[type="submit"] {
display: none;
}
}
/* Ambiguous-candidate picker (Task 24) */
.suggestion-ambiguous-picker {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
padding: 0.5rem;
margin-top: 0.3rem;
background: rgba(255, 180, 0, 0.06);
border: 1px solid rgba(255, 180, 0, 0.25);
border-radius: 0.25rem;
font-size: 0.85rem;
}
.suggestion-ambiguous-picker .ambiguous-label {
width: 100%;
font-weight: 600;
color: #ffb400;
margin-bottom: 0.2rem;
}
.suggestion-ambiguous-picker .ambiguous-candidate {
padding: 0.25rem 0.5rem;
background: var(--button-bg, #222);
border: 1px solid var(--button-border, #444);
border-radius: 0.25rem;
color: var(--fg, #eee);
cursor: pointer;
font-size: 0.85rem;
}
.suggestion-ambiguous-picker .ambiguous-candidate:hover {
background: var(--button-hover-bg, #333);
}
.suggestion-ambiguous-picker .ambiguous-candidate:disabled {
opacity: 0.5;
cursor: wait;
}
.suggestion-ambiguous-picker .ambiguous-cancel {
padding: 0.25rem 0.5rem;
background: transparent;
border: none;
color: var(--muted, #888);
cursor: pointer;
margin-left: auto;
}