/* ============================================================================
 * app.css — Web Viewer App stylesheet
 *
 * Extracted from inline <style> in index.php (2026-06 split). One file
 * because the CSS is a tight, single-app stylesheet — splitting by topic
 * would add HTTP round-trips without making maintenance easier.
 *
 * Loaded via <link rel="stylesheet" href="css/app.css?v=N"> in index.php.
 * The ?v=N query bumps in lockstep with the JS asset versions in session.md.
 * ========================================================================= */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* Kill the browser tap-highlight — the translucent blue the browser flashes on a
   tapped element. It was flashing on the clicked grid cell for a frame before the
   lightbox opened on mobile (the "flash" chased in docs/lightbox.md). The property
   is inherited, so setting it on html covers every tap target app-wide (native-app
   feel). */
html {
  -webkit-tap-highlight-color: transparent;
}

:root {
  color-scheme: dark;
  --bg: #141414;
  --surface: #1e1e1e;
  --surface2: #2a2a2a;
  --border: #333;
  --text: #ddd;
  --text-dim: #aaa;
  --accent: #0d84ff;
  --accent-bg: rgba(13, 132, 255, 0.18);
  --danger: #e04040;
  --danger-bg: rgba(224, 64, 64, 0.18);
  /* Liked-heart red (the "Like" marker; internally still "star"). */
  --heart: #e0245e;
  --heart-border: rgba(224, 36, 94, 0.55);
  --sidebar-w: 210px;
  --toolbar-h: 48px;
  --status-h: 34px;
  /* Anon justified gallery: row height (driven by thumb-size slider).
     Bleed is 0 now that flex-grow fills each row — kept as a knob in case
     we want the tease back later. */
  --tile-row-h: 200px;
  --tile-bleed: 0px;
}

html,
body {
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font:
    13px/1.4 system-ui,
    -apple-system,
    sans-serif;
}

#app {
  display: grid;
  height: 100vh;
  grid-template-rows: var(--toolbar-h) 1fr var(--status-h);
}
#main {
  display: grid;
  grid-template-columns: var(--sidebar-w) 4px 1fr;
  overflow: hidden;
  min-width: 0;
  /* Containing block for the absolutely-positioned pull-to-refresh spinner
     (#ptr). overflow:hidden also clips it when parked above the top edge. */
  position: relative;
}

/* ── PULL-TO-REFRESH (mobile) ──
   Floating circular spinner that drops from under the toolbar as you pull
   the grid down past its top. Driven by touch handlers in gallery-mobile.js;
   inert on desktop (no touch → never shown). Parked off the top edge by
   default; JS sets transform/opacity during the pull, then adds .refreshing
   for the continuous spin while refreshCurrentView() runs. */
#ptr {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -150%);
  opacity: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  /* Translucent dark disc behind the white spinner so it stays legible over
     bright photos. */
  background: rgba(0, 0, 0, 0.55);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
  pointer-events: none;
}
.ptr-spinner {
  width: 34px;
  height: 34px;
  border: 4px solid rgba(255, 255, 255, 0.3);
  border-top-color: #fff;
  border-radius: 50%;
}
#ptr.refreshing .ptr-spinner {
  animation: ptr-spin 0.7s linear infinite;
}
@keyframes ptr-spin {
  to { transform: rotate(360deg); }
}

/* ── RESIZE HANDLE ── */
#resize-handle {
  cursor: col-resize;
  background: var(--border);
  transition: background 0.15s;
  position: relative;
  z-index: 10;
}
#resize-handle:hover,
#resize-handle.dragging {
  background: var(--accent);
}

/* ── TOOLBAR ── */
#toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  min-width: 0;
}
#toolbar h1 {
  font-size: 15px;
  font-weight: 700;
  color: #fff;
  flex-shrink: 0;
}
#toolbar h1 .app-logo {
  height: 18px;
  width: 18px;
  vertical-align: -4px;
  margin-right: 2px;
}
#toolbar .divider {
  width: 1px;
  height: 20px;
  background: var(--border);
  flex-shrink: 0;
}
/* Active-view label. Now a <button> (mobile uses it to open a nav subset; see
   mobile.css). The reset below strips native button chrome so on desktop it
   reads as the same plain dim text label as before. */
#toolbar-folder {
  font-size: 12px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}

.tb-btn {
  padding: 4px 11px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
  transition: background 0.1s;
  flex-shrink: 0;
}
.tb-btn:hover {
  background: #333;
}
.tb-btn:active {
  background: #444;
}
.tb-btn.danger {
  border-color: var(--danger);
  color: var(--danger);
}
.tb-btn.danger:hover {
  background: var(--danger-bg);
}
.tb-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
/* Small notification dot on the gear button when a storage compression
   job is running or sampling. Pulses gently so it draws the eye but
   doesn't shout. gallery-storage-poll.js toggles the class. */
#btn-config {
  position: relative;
}
#btn-config.has-storage-job::after {
  content: '';
  position: absolute;
  top: 2px;
  right: 2px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #3b82f6;
  box-shadow: 0 0 0 2px var(--bg);
  animation: bf-storage-dot 1.6s ease-in-out infinite;
}
@keyframes bf-storage-dot {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}
/* Subtle barber-pole stripes on the storage progress bar while running.
   The bar's inline style sets background:var(--accent); this rule layers
   a translucent diagonal stripe gradient over it and slides the gradient
   position to animate. Solid-color fallback if animation is disabled. */
#storage-progress-bar.running {
  background-image: linear-gradient(
    45deg,
    rgba(255, 255, 255, 0.18) 25%,
    transparent 25%,
    transparent 50%,
    rgba(255, 255, 255, 0.18) 50%,
    rgba(255, 255, 255, 0.18) 75%,
    transparent 75%,
    transparent
  );
  background-size: 16px 16px;
  animation: bf-storage-stripes 1.2s linear infinite;
}
@keyframes bf-storage-stripes {
  0% {
    background-position: 0 0;
  }
  100% {
    background-position: 16px 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  #storage-progress-bar.running {
    animation: none;
  }
}
.tb-btn.active-mode {
  border-color: var(--accent);
  color: var(--accent);
}

#admin-toolbar {
  display: none;
  align-items: center;
  gap: 6px;
}
#admin-toolbar.visible {
  display: flex;
}

/* ── SPLIT BUTTON ── */
.split-btn {
  position: relative;
  display: flex;
  flex-shrink: 0;
}
.split-btn .split-main {
  border-right: none;
  border-radius: 5px 0 0 5px;
  padding-right: 9px;
}
.split-btn .split-arrow {
  border-radius: 0 5px 5px 0;
  padding: 4px 7px;
  border-left: 1px solid var(--bg);
}
.split-menu {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  z-index: 200;
  min-width: 160px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.split-menu.open {
  display: block;
}
.split-menu-item {
  padding: 8px 14px;
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
  color: var(--text);
  background: transparent;
  border: none;
  text-align: left;
  width: 100%;
  font-family: inherit;
}
.split-menu-item:hover:not(:disabled) {
  background: var(--surface2);
}
.split-menu-item:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* ── SIDEBAR ── */
#sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  container-type: inline-size;
  container-name: sidebar;
}
#sb-folders {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* 50/50 split between pinned (system items + Faces) and Albums tree.
   Each half scrolls independently; neither can push the other off-screen.
   min-height:0 lets the flex children actually shrink below content size. */
#folder-list-top {
  flex: 1 1 50%;
  min-height: 0;
  overflow-y: auto;
  box-shadow:
    inset 0 1px 0 0 #7d7d7d,
    0 1px 0 0 #7d7d7d;
  transition: flex-basis 0.25s ease;
}
#folder-list {
  flex: 1 1 50%;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 8px;
  transition: flex-basis 0.25s ease;
}
/* Focus-weighted split (gallery-sidebar.js sets sb-focus-top/-bottom on
   #sidebar from the active view): whichever panel the user is working in
   gets ~70% of the height, the other ~30%. Falls back to 50/50 if neither
   class is present. */
#sidebar.sb-focus-top #folder-list-top:not(.sb-top-fit) {
  flex-basis: 70%;
}
#sidebar.sb-focus-top #folder-list {
  flex-basis: 30%;
}
#sidebar.sb-focus-bottom #folder-list-top:not(.sb-top-fit) {
  flex-basis: 30%;
}
#sidebar.sb-focus-bottom #folder-list {
  flex-basis: 70%;
}
/* When the top panel has no expandable subtree visible (Faces collapsed,
   no named groups, admin view), shrink to content so Albums claims the
   freed space instead of locking a percentage gap. Declared AFTER the
   focus rules and kept id-qualified so it always wins. */
#sidebar #folder-list-top.sb-top-fit {
  flex: 0 1 auto;
}
/* …but when the TOP zone owns the focus (active view is a vFolder — incl.
   the admin rows Duplicates/…/Stats), a fit-sized top panel must not be
   squeezed by the Albums panel's 50% basis: hold its content height (Trash
   stays visible) up to the same 70% the focus split grants, then scroll. */
#sidebar.sb-focus-top #folder-list-top.sb-top-fit {
  flex-shrink: 0;
  max-height: 70%;
}
/* Pin Starred to the top of the scrolling top panel so it stays reachable
   when the Faces vFolder is expanded (Faces sits below Starred and makes the
   panel scroll, otherwise carrying Starred off the top). Inert when the panel
   fits its content (sb-top-fit / Faces collapsed) — nothing scrolls then.
   Needs an opaque background so scrolling face rows don't show through; the
   .active override keeps the selected-row highlight when Starred itself is
   the active view. */
#folder-list-top .sb-item[data-folder="__starred__"] {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
}
#folder-list-top .sb-item[data-folder="__starred__"].active {
  /* --accent-bg is translucent (0.18 alpha); on a pinned row that lets the
     scrolling face rows bleed through. Composite the tint over an opaque
     surface base so it occludes while looking identical to the normal tint. */
  background-color: var(--surface);
  background-image: linear-gradient(var(--accent-bg), var(--accent-bg));
}
/* Shared Expand-All button (Faces header + depth-0 anon Album rows).
   Text by default, icon when sidebar is narrow. Marker classes
   (.sb-faces-expand-all, .sb-folder-expand-all) carry behavior only. */
.sb-expand-all {
  background: var(--surface2, #141414);
  border: 1px solid var(--border, #444);
  border-radius: 4px;
  color: var(--text-dim);
  cursor: pointer;
  padding: 2px 8px;
  font-size: 11px;
  line-height: 1.2;
}
.sb-expand-all:hover {
  color: var(--text);
}
.sb-expand-all .sb-eax-icon {
  display: none;
}
@container sidebar (max-width: 200px) {
  .sb-expand-all .sb-eax-text {
    display: none;
  }
  .sb-expand-all .sb-eax-icon {
    display: inline;
  }
}
/* Folder-row Expand All: zero footprint until row hover, and on hover it
   REPLACES the file count (same slot after the flex-1 name) so the row
   doesn't grow. Faces-header button (.sb-faces-expand-all) unaffected. */
.sb-item .sb-folder-expand-all {
  display: none;
}
.sb-item:hover .sb-folder-expand-all {
  display: inline-block;
}
.sb-item:hover:has(.sb-folder-expand-all) .sb-count {
  display: none;
}
.sb-heading {
  padding: 10px 12px 5px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  display: flex;
  align-items: center;
}
.sb-new-folder {
  margin-left: auto;
  background: transparent;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
  border-radius: 3px;
}
.sb-new-folder:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}
/* Both heading buttons are .sb-new-folder with margin-left:auto; only the first
   should claim the free space, else the autos split it and the buttons scatter.
   Zero the auto on any following button so they group together at the right. */
.sb-new-folder + .sb-new-folder {
  margin-left: 4px;
}
#btn-manage-folders {
  font-size: 17px;
}
#folder-search-wrap {
  display: flex;
  align-items: center;
  margin: 4px 8px 6px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0 6px;
}
#folder-search {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 12px;
  padding: 5px 4px;
}
#folder-search::placeholder {
  color: var(--text-dim);
}
#folder-search-clear {
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 11px;
  padding: 2px;
  display: none;
}
#folder-search-clear:hover {
  color: var(--text);
}
.sb-search-item {
  padding: 6px 10px;
  cursor: pointer;
  border-radius: 4px;
  margin: 1px 4px;
}
.sb-search-item:hover {
  background: var(--surface2);
}
.sb-search-item .sb-sr-name {
  font-size: 13px;
  color: var(--text);
}
.sb-search-item .sb-sr-path {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 1px;
}

.sb-item {
  display: flex;
  align-items: center;
  padding: 5px 12px;
  cursor: pointer;
  font-size: 12px;
  user-select: none;
  border-left: 2px solid transparent;
}
.sb-item:hover {
  background: var(--surface2);
}
.sb-item.active {
  border-left-color: var(--accent);
  background: var(--accent-bg);
  color: #fff;
}
/* Secondary highlight: folder containing the last-clicked selected photo
   in the right pane. Distinct from .active (which means "currently being
   filtered"). Used to follow selection from vfolders (Duplicates/Starred/
   Faces) back to the source gallery folder without changing the filter. */
.sb-item.contains-selected:not(.active) {
  border-left-color: var(--accent);
  background: rgba(255, 255, 255, 0.04);
}
.sb-item .sb-count {
  color: var(--text-dim);
  font-size: 11px;
  flex-shrink: 0;
}
.sb-item.active .sb-count {
  color: rgba(255, 255, 255, 0.6);
}
/* Dual-count rows "own (subtotal)": colors SWAPPED 2026-06-10 (user) — the
   own count is the accent (orange), the subtree total goes dim. Single-count
   rows keep the plain dim color via the base rule above. */
.sb-item .sb-count:has(.sb-count-sub) {
  color: rgba(217, 168, 106, 0.7);
}
.sb-item .sb-count-sub {
  color: var(--text-dim);
  opacity: 0.85;
  margin-left: 2px;
  font-size: 12px;
}
.sb-item .sb-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Large inline icon prefix for system rows (Starred, etc.) so they sit
   at roughly the same visual height as the top-level face thumbs. */
.sb-row-icon {
  display: inline-block;
  font-size: 22px;
  line-height: 1;
  width: 24px;
  text-align: center;
  margin-right: 6px;
  vertical-align: middle;
}
/* Tighter vertical spacing for face vFolder children (anon view). */
.sb-face-item {
  padding-top: 2px;
  padding-bottom: 2px;
}
/* Top-level (depth 0) face rows carry a face-crop thumb, so they get
   more breathing room. Nested children stay compact. */
.sb-face-item.sb-face-top {
  padding-top: 6px;
  padding-bottom: 6px;
}
.sb-face-thumb {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  background-color: var(--surface2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  margin-right: 8px;
}
/* Nested face children get half-size thumbs so the parent row still
   reads as the primary anchor of the subtree. */
.sb-face-thumb.sb-face-thumb-sm {
  width: 18px;
  height: 18px;
  border-radius: 5px;
  margin-right: 6px;
}
/* Anon-only cover thumbs on gallery folder rows. Mirror the face-thumb
   sizing — 28px on top-level (e.g. "Stefs Photos"), 18px on every nested
   level. Cover photo is picked subtree-wide (most-starred, fallback newest
   mtime) and hydrated as a background-image post-render. */
.sb-folder-thumb {
  flex-shrink: 0;
  width: 28px;
  height: 28px;
  border-radius: 7px;
  background-color: var(--surface2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  margin-right: 8px;
}
.sb-folder-thumb.sb-folder-thumb-sm {
  width: 18px;
  height: 18px;
  border-radius: 5px;
  margin-right: 6px;
}
.sb-chevron {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 8px;
  padding: 0;
  margin-right: 2px;
  border-radius: 3px;
  line-height: 1;
}
.sb-chevron:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}
.sb-item.active .sb-chevron {
  color: rgba(255, 255, 255, 0.6);
}
.sb-chevron-gap {
  flex-shrink: 0;
  display: inline-block;
  width: 16px;
  margin-right: 2px;
}
/* Admin folder action buttons */
.sb-admin-btn,
.sb-detect {
  flex-shrink: 0;
  padding: 1px 5px;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 10px;
  display: none;
  margin-left: 3px;
}
.sb-item:hover .sb-admin-btn,
.sb-item:hover .sb-detect {
  display: inline-block;
}
.sb-item.active .sb-admin-btn,
.sb-item.active .sb-detect {
  border-color: rgba(255, 255, 255, 0.3);
  color: rgba(255, 255, 255, 0.7);
}
.sb-admin-btn.danger {
  color: var(--danger);
}
.sb-admin-btn.danger:hover {
  border-color: var(--danger);
  background: var(--danger-bg);
}
/* Admin lock icon — toggle folder privacy. Always-visible (unlike admin-btn
   which is hover-revealed) so admins can scan the tree at a glance to see
   which folders are private. White (subtle) when public, red when private. */
.sb-lock-btn {
  flex-shrink: 0;
  padding: 0 4px;
  margin-left: 4px;
  background: transparent;
  border: none;
  cursor: pointer;
  color: rgba(255, 255, 255, 0.45);
  display: inline-flex;
  align-items: center;
  line-height: 1;
  transition: color 0.12s;
}
.sb-lock-btn svg {
  display: block;
}
.sb-lock-btn:hover {
  color: #fff;
}
.sb-lock-btn.is-private {
  color: var(--danger);
}
.sb-lock-btn.is-private:hover {
  color: var(--danger);
}
/* Public folder that has a private folder somewhere below it: light orange,
   so the tree shows at a glance that something private lives under it.
   Hover nudges brighter; red (.is-private) always wins over this. */
.sb-lock-btn.has-private-desc {
  color: #e8a13c;
}
.sb-lock-btn.has-private-desc:hover {
  color: #ffb84d;
}
.sb-item.active .sb-lock-btn {
  color: rgba(255, 255, 255, 0.7);
}
.sb-item.active .sb-lock-btn.is-private {
  color: var(--danger);
}
.sb-item.active .sb-lock-btn.has-private-desc {
  color: #e8a13c;
}
.sb-trash .sb-name {
  color: var(--text-dim);
}
.sb-trash.active .sb-name {
  color: #fff;
}
.sb-trash:hover .sb-name {
  color: var(--danger);
}

/* ── GRID ── */
/* overflow-x: clip lets the anon justified grid bleed past the right edge
   intentionally — the tile on the right is half-visible as a "more here" tease.
   Admin grid stays inside the viewport, so clip is a no-op for them. */
#content {
  overflow-y: auto;
  overflow-x: clip;
  scrollbar-gutter: stable;
  padding: 0 10px 10px;
}
#grid {
  display: grid;
  gap: 4px;
  grid-template-columns: repeat(
    auto-fill,
    minmax(var(--thumb-size, 168px), 1fr)
  );
}

/* ── JUSTIFIED GRID (anon only) ──
   Fixed row height (--tile-row-h, driven by the thumb-size slider).
   Each tile's width = rowH × (originalW / originalH) — set inline per cell.
   Container is widened past 100% by --tile-bleed so flex-wrap packs an
   extra partial tile that hangs off the right edge. --tile-bleed = 0 on
   mobile so narrow screens don't suffer the bleed. */
#grid.justified {
  display: flex;
  flex-wrap: wrap;
  /* gap: <row> <column> — vertical (between rows) stays at 1px,
     horizontal (between tiles in a row) is 2x. */
  gap: 4px 1px;
  grid-template-columns: unset;
  width: calc(100% + var(--tile-bleed, 0px));
}
#grid.justified .cell {
  aspect-ratio: unset;
  height: var(--tile-row-h, 200px);
  /* Base width = row height × per-cell aspect (set inline as --aspect).
     flex-grow is proportional to aspect so any leftover row-space is
     distributed across all tiles in the row, preserving their relative
     widths and eliminating right-edge gaps. max-width caps the stretch
     at 1.6× so a sparse last row doesn't blow up a single tile. */
  flex: var(--aspect, 1) 0 calc(var(--tile-row-h, 200px) * var(--aspect, 1));
  max-width: calc(var(--tile-row-h, 200px) * var(--aspect, 1) * 1.6);
  /* Anon doesn't select — kill the 2px transparent border so adjacent
     tiles sit truly 1px apart (otherwise gap+border = 5px visible). */
  border: 0;
  border-radius: 0;
}
#grid.justified .cell:hover {
  border: 0;
}

/* ── STORIES GRID VIEW (thumbnail tiles + expand-in-place) ── */
.story-tile {
  aspect-ratio: 1 / 1;
  background: #111 center/cover no-repeat;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  position: relative;
}
.story-tile:hover {
  border-color: var(--accent);
}
.story-tile.open {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-color: var(--accent);
}
.story-tile.missing {
  display: flex;
  align-items: center;
  justify-content: center;
}
.story-tile-warn {
  font-size: 22px;
  color: #e9b04b;
}
/* Corner status badge (All tab only) */
.story-tile-badge {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
  color: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}
.story-tile-badge.st-approved, .story-tab-ico.st-approved { background: #16a34a; }
.story-tile-badge.st-rejected, .story-tab-ico.st-rejected { background: #dc2626; }
.story-tile-badge.st-review,   .story-tab-ico.st-review   { background: #eab308; color: #1a1a1a; }
.story-tile-badge.st-pending,  .story-tab-ico.st-pending  { background: #3b82f6; }
.story-tile-badge.st-pending  { font-size: 18px; }

/* Inline status icon on the moderation status tabs — mirrors the tile corner
   badge, shares the .st-* color classes above. */
.story-tab-ico {
  display: inline-flex;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  color: #fff;
  margin-right: 5px;
  vertical-align: middle;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
}
.story-tab-ico.st-pending { font-size: 15px; }

/* ── DUPLICATE GROUPS ── */
.dup-group {
  margin-bottom: 10px;
  background: var(--surface2);
  border-radius: 6px;
  overflow: hidden;
  grid-column: 1 / -1;
}
.dup-header {
  padding: 5px 10px;
  font-size: 11px;
  color: var(--text-dim);
  border-bottom: 1px solid var(--border);
}
.dup-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 8px;
}
.dup-thumbs .cell {
  width: 210px;
  height: 210px;
  flex-shrink: 0;
  aspect-ratio: unset;
}
/* Duplicates labels show a long, wrapping folder path; the shared .cell .info
   gradient leaves the upper lines over near-transparent backing → unreadable on
   light photos. Give the dup label a solid translucent panel instead. */
.dup-thumbs .cell .info {
  background: rgba(0, 0, 0, 0.68);
}
/* Per-row "Delete Selected" — sits in the free space after the thumbs and
   appears once a copy in that row is selected. Visibility is driven purely by
   :has() off the .selected class, which every selection path already maintains,
   so no JS toggling is needed. Saves reaching for the top-toolbar Delete. */
.dup-row-actions {
  display: none;
  align-self: center;
  margin-left: 4px;
}
.dup-group:has(.cell.selected) .dup-row-actions {
  display: flex;
}
/* Optimistic-delete notice: shown in the action panel for a group on its way
   out (fell below 2 copies). Forced visible regardless of selection. */
.dup-row-actions.removing {
  display: flex;
}
.dup-removing-msg {
  align-self: center;
  color: var(--text-dim);
  font-size: 13px;
  font-style: italic;
  white-space: nowrap;
}
/* "Compare folders" button in a dup-set header. */
.dup-header {
  display: flex;
  align-items: center;
  gap: 6px;
}
.dup-compare-btn {
  margin-left: auto;
  background: none;
  border: 1px solid var(--border);
  color: var(--accent, #6ea8fe);
  border-radius: 5px;
  padding: 2px 9px;
  font-size: 12px;
  cursor: pointer;
}
.dup-compare-btn:hover {
  background: #333;
}
.dup-samefolder {
  margin-left: auto;
  color: var(--text-dim);
  font-size: 12px;
  font-style: italic;
}

/* ── Compare Folders modal ─────────────────────────────────────────────────── */
.dup-compare-overlay {
  position: fixed;
  inset: 0;
  z-index: 10050;
  background: var(--bg, #111);
  display: none;
  flex-direction: column;
}
.dup-compare-overlay.open {
  display: flex;
}
.dcmp-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.dcmp-sub {
  color: var(--text-dim);
  font-size: 12px;
}
.dcmp-body {
  flex: 1 1 auto;
  display: flex;
  gap: 1px;
  min-height: 0;
  overflow: hidden;
  background: var(--border);
}
.dcmp-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}
.dcmp-col-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  flex-wrap: wrap;          /* narrow windows: buttons drop to a 2nd row */
  gap: 8px;
  row-gap: 6px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
}
/* Compact icon-only button (trash "Delete folder"). Square, glyph centered;
   inherits .tb-btn.danger red via currentColor. */
.dcmp-icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 4px 7px;
}
.dcmp-icon-btn svg { display: block; }
/* Per-column Delete is always present and just disabled (greyed via .tb-btn:disabled)
   when the column has no selection — keeps its slot so it never shoves "Select all"
   sideways, and reads as an inactive button rather than an unexplained gap.
   min-widths fix both buttons' widths so the disable/label toggles don't reflow. */
.dcmp-col-del { min-width: 96px; }
.dcmp-select-dupes { min-width: 168px; }
/* Hover preview: floating label with the 480px scan thumb + the file's
   name/dims/size in one popup. pointer-events:none so it never eats clicks;
   sizes to whatever image loads (capped to the viewport). */
.dcmp-preview {
  position: fixed;
  z-index: 10055;            /* above .dup-compare-overlay (10050) */
  pointer-events: none;
  display: none;
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.dcmp-preview.open { display: block; }
.dcmp-preview-imgwrap {
  position: relative;
  display: inline-block;
  line-height: 0;
}
.dcmp-preview img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(480px, 84vw);
  max-height: 72vh;
  border-radius: 6px;
}
/* Caption overlaid on the bottom of the image, translucent black. Two lines,
   each clipped to the image width so nothing runs off-screen. */
.dcmp-preview-cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  box-sizing: border-box;
  padding: 6px 8px;
  background: rgba(0, 0, 0, 0.85);
  color: #fff;
  font-size: 12px;
  line-height: 1.35;
  border-radius: 0 0 6px 6px;
}
.dcmp-preview-l1 {
  display: flex;
  min-width: 0;
  white-space: nowrap;
}
/* Path shrinks first (huge flex-shrink) and ellipsizes; filename only shrinks
   if it alone is wider than the image. */
.dcmp-prev-path {
  flex: 0 9999 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.8;
}
.dcmp-prev-name {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dcmp-preview-l2 {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.85;
}
/* Twin inlay: the OTHER folder's copy stats + a larger/smaller comparison,
   pinned to a top corner with an arrow pointing toward that column. */
.dcmp-twin {
  position: absolute;
  top: 0;
  max-width: 70%;
  padding: 5px 8px;
  background: rgba(0, 0, 0, 0.88);
  color: #fff;
  font-size: 12px;
  line-height: 1.3;
}
.dcmp-twin[hidden] { display: none; }
.dcmp-twin-l { left: 0; border-radius: 6px 0 6px 0; text-align: left; }
.dcmp-twin-r { right: 0; border-radius: 0 6px 0 6px; text-align: right; }
.dcmp-twin { overflow: hidden; }
.dcmp-twin-item + .dcmp-twin-item {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid rgba(255, 255, 255, 0.18);
}
.dcmp-twin-hdr {
  font-weight: 600;
  opacity: 0.95;
  margin-bottom: 1px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dcmp-twin-stat { white-space: nowrap; opacity: 0.85; }
.dcmp-twin-cmp { opacity: 0.95; }
/* Detail lines indented to align under the header label, past the arrow. */
.dcmp-twin-l .dcmp-twin-body { padding-left: 1.2em; }
.dcmp-twin-r .dcmp-twin-body { padding-right: 1.2em; }
.dcmp-twin-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: 0.95; }
.dcmp-twin-arrow { font-weight: 700; }
.cmp-up   { color: #34d399; font-weight: 600; }   /* twin is larger */
.cmp-down { color: #f59e0b; font-weight: 600; }   /* twin is smaller */
.cmp-eq   { color: #cbd5e1; }                      /* same */
/* Right-click "replace the other copy" menu. Above the preview (10055). */
.dcmp-ctx {
  position: fixed;
  z-index: 10057;
  display: none;
  min-width: 200px;
  max-width: 340px;
  padding: 4px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.dcmp-ctx.open { display: block; }
.dcmp-ctx-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  color: var(--text);
  font-size: 13px;
  padding: 7px 10px;
  border-radius: 4px;
  cursor: pointer;
}
.dcmp-ctx-item:hover:not(:disabled) { background: #333; }
.dcmp-ctx-item:disabled { opacity: 0.5; cursor: default; }
.dcmp-col-path {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dcmp-col-meta {
  color: var(--text-dim);
  font-size: 11px;
  white-space: nowrap;
}
.dcmp-col-grid {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  gap: 4px;
  padding: 8px;
}
.dcmp-cell {
  position: relative;
  width: 84px;
  height: 84px;
  flex: 0 0 auto;
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  border: 2px solid transparent;
}
.dcmp-cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.dcmp-cell.shared {
  border-color: var(--accent, #6ea8fe);
}
.dcmp-cell.selected {
  outline: 3px solid var(--danger);
  outline-offset: -3px;
}
.dcmp-cell .dcmp-check {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  font-size: 12px;
  line-height: 18px;
  text-align: center;
  display: none;
}
.dcmp-cell.selected .dcmp-check {
  display: block;
}
.dcmp-cell.dcmp-oos {
  cursor: default;
  opacity: 0.5;
}
.dcmp-empty {
  color: var(--text-dim);
  font-size: 12px;
  padding: 12px;
}

/* Free-text filter over the dup sets. */
.dup-filter-bar {
  grid-column: 1 / -1;
  margin-bottom: 10px;
}
#dup-filter {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 11px;
  font-size: 13px;
  color: var(--text);
  background: var(--surface, #1e1e1e);
  border: 1px solid var(--border);
  border-radius: 6px;
}
#dup-filter:focus {
  outline: none;
  border-color: var(--accent, #6ea8fe);
}

/* Clickable, lightly embossed offender rows in the duplicates panel. */
.dup-off-hint {
  font-size: 11px;
  color: var(--text-dim);
  margin-bottom: 8px;
}
.dup-off-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 9px;
  margin-top: 4px;
  font-size: 12px;
  cursor: pointer;
  background: var(--surface, #1e1e1e);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
  transition: background 0.12s, border-color 0.12s, transform 0.04s;
}
.dup-off-row:hover {
  background: #2c2c2c;
  border-color: var(--accent, #6ea8fe);
}
.dup-off-row:active {
  transform: translateY(1px);
}
.dor-rank {
  color: var(--text-dim);
  min-width: 20px;
}
.dor-folder {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dor-meta {
  color: var(--text-dim);
  white-space: nowrap;
}

/* ── Folder-choice dialog (offenders picker / big-set folder pick) ──────────── */
.dup-choice-overlay {
  position: fixed;
  inset: 0;
  z-index: 10060;
  background: rgba(0, 0, 0, 0.6);
  display: none;
  align-items: flex-start;
  justify-content: center;
  padding: 60px 16px;
}
.dup-choice-overlay.open {
  display: flex;
}
.dch-box {
  width: min(680px, 100%);
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface2, #1e1e1e);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.dch-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.dch-title {
  flex: 1;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.dch-sub {
  padding: 8px 14px;
  border-bottom: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.dch-hint {
  padding: 8px 14px 2px;
  color: var(--text-dim);
  font-size: 12px;
}
.dch-list {
  overflow-y: auto;
  padding: 6px;
}
/* Same lightly-embossed clickable card as the offenders rows. */
.dch-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 11px;
  margin-top: 4px;
  background: var(--surface, #1e1e1e);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25);
  color: var(--text);
  text-align: left;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.12s, border-color 0.12s, transform 0.04s;
}
.dch-row:hover {
  background: #2c2c2c;
  border-color: var(--accent, #6ea8fe);
}
.dch-row:active {
  transform: translateY(1px);
}
.dch-path {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dch-meta {
  color: var(--text-dim);
  font-size: 12px;
  white-space: nowrap;
}
.dch-empty {
  padding: 18px 14px;
  color: var(--text-dim);
  font-size: 13px;
}

.cell {
  position: relative;
  background: var(--surface2);
  border-radius: 4px;
  overflow: hidden;
  cursor: pointer;
  user-select: none;
  border: 2px solid transparent;
  transition: border-color 0.1s;
  aspect-ratio: 1;
}
.cell:hover {
  border-color: #555;
}
.cell.focused {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.cell.selected {
  border-color: var(--accent);
}
.cell.selected .check {
  opacity: 1;
}
/* Brief pulse used when navigating from the Problem Reports panel to a
   photo — draws the eye to the highlighted cell after the lightbox closes. */
.cell.rep-flash {
  animation: cellFlash 1.8s ease-out 1;
}
@keyframes cellFlash {
  0% {
    box-shadow: 0 0 0 4px rgba(251, 191, 36, 0);
  }
  15% {
    box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.85);
  }
  100% {
    box-shadow: 0 0 0 4px rgba(251, 191, 36, 0);
  }
}
.cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: #111;
}
.cell .info {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.75));
  padding: 16px 6px 5px;
  font-size: 10px;
  color: #ccc;
  line-height: 1.3;
}
.cell .info .fname {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.cell .info .fsize {
  color: var(--text-dim);
}
.check {
  position: absolute;
  top: 5px;
  right: 5px;
  opacity: 0;
  transition: opacity 0.1s;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: #fff;
  font-weight: 700;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.cell-preview {
  position: absolute;
  top: 5px;
  left: 5px;
  opacity: 0;
  transition: opacity 0.1s;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
  padding: 0;
}
.cell:hover .cell-preview {
  opacity: 1;
}
/* Star-count pill, top-left (was a bottom gradient bar — moved so the
   bottom hover info matches the Enhanced panel's layout). */
/* Shared heart icon (the "Like"/Liked marker — emitted by heartIcon() in
   gallery-core.js). 1em so the host element's font-size drives scale, like the
   old ⭐/☆ glyph. fill/stroke use currentColor so existing color rules
   (.is-starred gold, badge accent, row-icon, etc.) carry straight over. */
.heart-svg {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.125em;
  fill: currentColor;
}
.heart-svg.heart-outline {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}
/* Open-book outline icon (anon Stories vFolder row + toolbar). White-outline
   like the lightbox .ctrl-ico, but inline-block so it sits correctly in the
   sidebar row + toolbar text flow (.ctrl-ico is display:block for the modal). */
.book-svg {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.125em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Tree + gallery outlines (treeIcon / galleryIcon) — same stroke family as .book-svg. */
.tree-svg, .gallery-svg {
  width: 1em;
  height: 1em;
  display: inline-block;
  vertical-align: -0.125em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Traced profile-portrait Faces icon (faceIcon() in gallery-core.js). A
   CSS-masked span: background-color is the ink (currentColor → white in the
   sidebar/pill), the external SVG is the mask. ~24px tall to sit in line with
   the 22px glyph icons; mask keeps aspect-ratio (viewBox is tall, so the box is
   height-limited). url() is relative to css/app.css → ../icons/. */
.face-svg {
  display: inline-block;
  width: 24px;
  height: 24px;
  vertical-align: middle;
  background-color: currentColor;
  -webkit-mask: url('../icons/face_profile3.svg?v=1') no-repeat center / contain;
          mask: url('../icons/face_profile3.svg?v=1') no-repeat center / contain;
}
/* Toolbar-folder label icons for a plain gallery folder (cover thumb) and a named
   face group (face crop). Set in the dispatchers (gallery-core-helpers.js); shown
   inline on desktop and wrapped into .tf-ico on mobile. The cover's image is an
   inline background-image; the face crop is positioned by cropDivToFace. */
.tf-cover-thumb,
.tf-facecrop {
  display: inline-block;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 5px;
  vertical-align: middle;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
/* Decorative / always-filled hearts (grid badge, sidebar row, headings, log
   icons) render in the liked red regardless of the surrounding text color. */
.heart-red {
  color: var(--heart);
}
/* Option B (2026-06-23): no backdrop — white heart-outline + count anchored
   bottom-left, kept legible on any photo by a doubled text-shadow (a tight
   halo + a soft drop). No pill/box-shadow. */
.cell .stars-badge {
  position: absolute;
  bottom: 3px;
  left: 4px;
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 500;
  color: #fff;
  line-height: 1;
  /* Tappable → "who liked this" popover. */
  pointer-events: auto;
  cursor: pointer;
  /* Legible on ANY photo without a backdrop: a darker, wider shadow. The count
     is text (text-shadow), the heart is an SVG (needs filter drop-shadow), so
     both layers are needed — a tight dark halo plus a softer wider one. */
  text-shadow: 0 1px 2px rgba(0, 0, 0, 1), 0 0 5px rgba(0, 0, 0, 1);
  filter:
    drop-shadow(0 1px 1px rgba(0, 0, 0, 0.95))
    drop-shadow(0 0 3px rgba(0, 0, 0, 0.95));
}
/* Enlarge the tap target well beyond the small visible pill (the heart + number
   is tiny on a thumb). A transparent ::after extends the clickable box; clicks
   on it still register on .stars-badge, so the popover opens easily on mobile. */
.cell .stars-badge::after {
  content: '';
  position: absolute;
  top: -12px;
  right: -16px;
  bottom: -8px;
  left: -8px;
}
.cell .stars-badge .star {
  display: inline-flex;
}
/* Story badge — top-left book icon on each thumb that has >=1 approved story.
   No count (read the stories to see them); just a presence indicator. Subtle
   backdrop so it reads on light photos. Not interactive. */
.cell .story-badge {
  position: absolute;
  top: 4px;
  left: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  pointer-events: none;
}
.cell .story-badge .book-svg {
  width: 13px;
  height: 13px;
}
/* "Who liked this" popover (gallery-stars.js). Floating list of liker names. */
.likers-popover {
  position: fixed;
  z-index: 1200;
  min-width: 150px;
  max-width: 240px;
  background: var(--surface2, #1b1b1d);
  border: 1px solid var(--border, #333);
  border-radius: 10px;
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.55);
  padding: 10px 12px;
  font-size: 13px;
  color: var(--text, #eee);
}
.likers-popover.lp-centered {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  min-width: 220px;
}
.likers-popover .lp-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 600;
  margin-bottom: 8px;
}
.likers-popover .lp-head .heart-red {
  display: inline-flex;
}
.likers-popover .lp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Show ~5½ rows then scroll — the half-row peek cues that more names follow.
     Row ≈ 26px (17px line + 8px padding + 1px border). Scroll lives here, not on
     the popover, so the "Liked by · N" header stays pinned above it. */
  max-height: calc(5.5 * 26px);
  overflow-y: auto;
}
.likers-popover .lp-list li {
  padding: 4px 0;
  line-height: 17px;
  border-top: 1px solid var(--border, #2a2a2c);
}
.likers-popover .lp-list li:first-child {
  border-top: none;
}
.likers-popover .lp-list li.lp-me {
  font-weight: 600;
  color: var(--accent, #6aa6ff);
}
.likers-popover .lp-empty {
  color: var(--text-dim, #999);
}

/* ── PAGINATION ── */
/* Sticky top of #content scroll container — always reachable while
   scrolling the grid. Background matches the page so thumbs underneath
   don't bleed through. Margin pulls it into #content's top padding. */
#pagination-top {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0 8px;
  margin: 0 -10px;
  padding-left: 10px;
  padding-right: 10px;
  background: var(--bg);
  color: var(--text-dim);
  font-size: 13px;
}
#pagination-top.hidden {
  display: none;
}

/* Grid search box (left side of pagination-top row). 3-zone flex layout:
   search-wrap (flex:1, capped input inside), centered pagination controls,
   right spacer (flex:1) — equal-width flex zones keep the controls truly
   centered while the search stays left-aligned. Native type="search" gives
   the ✕ clear control in webkit; we don't render our own. */
#grid-search-wrap {
  flex: 1;
  display: flex;
  align-items: center;
  min-width: 0;
}
#pagination-top-controls {
  display: flex;
  align-items: center;
  gap: 10px;
}
#pagination-top-spacer {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
#grid-search {
  width: 100%;
  max-width: 360px;
  /* Shrinkable so the mobile refresh button can sit beside it on the
     full-width search row without overflowing. */
  flex: 1 1 auto;
  min-width: 0;
  padding: 5px 10px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  font-size: 12px;
  outline: none;
}
#grid-search::placeholder {
  color: var(--text-dim);
}

/* Curly-arrow "refresh this folder" button. Two copies exist (mobile in the
   search row, desktop in the right spacer); both share this class and the
   refreshCurrentView() handler. The mobile copy is hidden on desktop and
   revealed by mobile.css at ≤600px; the desktop copy hides on mobile because
   its spacer is display:none there. */
.grid-refresh-btn,
.grid-fs-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--surface2);
  color: var(--text-dim);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
}
.grid-refresh-btn:hover,
.grid-fs-btn:hover {
  color: #fff;
  border-color: var(--accent);
}
/* The button itself does NOT spin — feedback is the top-center spinner (#ptr).
   While a refresh runs the button is disabled + dimmed to block double-fires. */
.grid-refresh-btn:disabled {
  opacity: 0.45;
  cursor: default;
}
/* Fullscreen toggle: always white. The icon itself swaps between corners-out
   (enter) and corners-in (exit) via the fullscreenchange handler in
   savers/core.js — color does NOT change with state (.is-fs is still toggled
   but no longer styled). */
.grid-fs-btn {
  color: #fff;
}
.grid-fs-btn svg {
  display: block;
}
/* Desktop default: the mobile fullscreen toggle is hidden (mobile.css reveals
   it). The desktop search row keeps its ⟳ refresh button instead. */
#btn-grid-fullscreen-m {
  display: none;
}
#faces-tuning-row.hidden {
  display: none !important;
}
.fg-pot-block {
  margin: 0 0 10px 0;
}
.fg-pot-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 6px 12px;
  margin-left: 16px;
  background: var(--surface2);
  border: 1px dashed var(--border);
  border-left: 4px dashed var(--accent);
  border-radius: 4px;
  color: var(--text-dim);
  font-size: 12px;
  font-style: italic;
  cursor: pointer;
  text-align: left;
}
.fg-pot-toggle:hover {
  background: var(--surface);
  color: var(--text);
}
.fg-pot-toggle .fg-pot-arrow {
  display: inline-block;
  width: 10px;
  text-align: center;
  font-style: normal;
  color: var(--accent);
}
.fg-pot-toggle strong {
  color: var(--text);
  font-weight: 600;
  font-style: normal;
}
.fg-pot-children {
  margin-top: 6px;
}
.fg-pot-children.hidden {
  display: none;
}
.fg-nest-children {
  margin-top: 6px;
}
.fg-nest-children.hidden {
  display: none;
}
.page-btn {
  padding: 5px 14px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  cursor: pointer;
  font-size: 12px;
  transition: background 0.1s;
}
.page-btn:hover:not(:disabled) {
  background: #333;
}
.page-btn:disabled {
  opacity: 0.4;
  cursor: default;
}
#page-info-top {
  font-size: 12px;
  min-width: 90px;
  text-align: center;
}

/* ── STATUS BAR ── */
#statusbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 14px;
  background: var(--surface);
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-dim);
  min-width: 0;
}
#status-sel {
  color: var(--accent);
  font-weight: 600;
  flex-shrink: 0;
}
#status-info {
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  flex: 1;
}
#status-hint {
  margin-left: auto;
  font-size: 11px;
  flex-shrink: 0;
}

/* ── LIGHTBOX ── */
#screensaver {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 900;
  background: #000;
  overflow: hidden;
  cursor: pointer;
}
#ss-hint {
  position: absolute;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, 0.35);
  font-size: 13px;
  pointer-events: none;
  white-space: nowrap;
}
.ss-card {
  position: absolute;
  top: 0;
  left: 0;
  border-radius: 6px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8);
  overflow: hidden;
  pointer-events: auto;
  cursor: pointer;
  will-change: transform, opacity;
  transition: box-shadow 0.15s;
}
.ss-card img {
  display: block;
  height: auto;
}
.ss-card.ss-hovered {
  opacity: 1 !important;
  box-shadow:
    0 0 0 3px #fff,
    0 8px 40px rgba(0, 0, 0, 0.9);
  z-index: 100;
  cursor: grab;
}
#ss-hud {
  position: absolute;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 20px;
  background: rgba(0, 0, 0, 0.65);
  padding: 10px 22px;
  border-radius: 28px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  opacity: 0;
  transition: opacity 0.4s;
  pointer-events: none;
  white-space: nowrap;
  z-index: 200;
}
#ss-hud.visible {
  opacity: 1;
  pointer-events: auto;
}
.sb-divider {
  height: 1px;
  background: var(--border);
  margin: 6px 10px;
}
#ss-folder-label {
  position: absolute;
  top: 20px;
  left: 24px;
  color: rgba(255, 255, 255, 0.7);
  font-size: 13px;
  background: rgba(0, 0, 0, 0.45);
  padding: 5px 14px;
  border-radius: 20px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  pointer-events: none;
  z-index: 200;
  display: none;
}
.ss-hud-group {
  display: flex;
  align-items: center;
  gap: 6px;
}
.ss-hud-label {
  color: rgba(255, 255, 255, 0.5);
  font-size: 12px;
}
.ss-hud-btn {
  background: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.2);
  color: #fff;
  border-radius: 14px;
  padding: 4px 11px;
  font-size: 13px;
  cursor: pointer;
}
.ss-hud-btn.ss-active {
  background: rgba(255, 255, 255, 0.28);
  border-color: rgba(255, 255, 255, 0.65);
}
.ss-hud-btn:hover {
  background: rgba(255, 255, 255, 0.22);
}
.ss-hud-sep {
  width: 1px;
  background: rgba(255, 255, 255, 0.2);
  align-self: stretch;
  margin: 0 4px;
}
/* Screensaver mode picker (fly-out menu under toolbar button). Positioned
   by JS on open so it follows the button's current viewport position. */
#ss-menu {
  position: fixed;
  display: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
  padding: 6px;
  z-index: 1500;
  min-width: 260px;
}
#ss-menu.open {
  display: block;
}
/* ✕ close — top-right corner of the picker. First option gets extra right
   padding so its description can't run under the button. */
#ss-menu-close {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 26px;
  height: 26px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 50%;
  color: var(--text-dim);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  z-index: 1;
}
#ss-menu-close:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.12);
}
#ss-menu-close + .ss-menu-opt {
  padding-right: 40px;
}
.ss-menu-opt {
  display: flex;
  flex-direction: column;
  background: transparent;
  border: 0;
  color: var(--text);
  padding: 10px 14px;
  border-radius: 6px;
  cursor: pointer;
  width: 100%;
  text-align: left;
  font: inherit;
}
.ss-menu-opt + .ss-menu-opt {
  margin-top: 2px;
}
.ss-menu-opt:hover {
  background: var(--surface2);
}
.ss-menu-opt.active {
  background: rgba(255, 255, 255, 0.06);
}
.ss-menu-opt .ss-menu-title {
  font-weight: 600;
  font-size: 13px;
}
.ss-menu-opt .ss-menu-desc {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 2px;
}
/* Menu speed row (S/M/F) — sets speed BEFORE the saver starts. On mobile
   it's the ONLY speed control (HUD hidden below 600px). */
.ss-menu-speed {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px 8px;
  margin-top: 4px;
  border-top: 1px solid var(--border);
}
.ss-menu-speed-label {
  font-size: 12px;
  color: var(--text-dim);
}
/* Row layout: "Fullscreen ⛶ ......... Speed S M F" — the ⛶ button takes
   the auto margin so the Speed group sits flush right. */
#ss-menu-fs {
  margin-right: auto;
}
.ss-menu-speed-btn {
  background: var(--surface2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 14px;
  padding: 4px 14px;
  font-size: 13px;
  cursor: pointer;
}
.ss-menu-speed-btn:hover {
  background: rgba(255, 255, 255, 0.12);
}
.ss-menu-speed-btn.active {
  background: rgba(255, 255, 255, 0.22);
  border-color: rgba(255, 255, 255, 0.6);
}
/* Mobile screensaver: no HUD at all — it rendered offscreen anyway. Tap
   anywhere exits, size is forced S, speed comes from the ✦ menu. The menu
   itself becomes a centered sheet (the toolbar anchor button is
   display:none on mobile, so JS skips its rect-based positioning).
   Matches ssIsMobile() in savers/core.js: smaller viewport dimension
   ≤ 600px, so landscape phones (~900×412) count as mobile too. */
@media (max-width: 600px), (max-height: 600px) {
  #ss-hud {
    display: none !important;
  }
  /* Folder pill hugs the corner on small screens — desktop's 20/24px
     offsets eat too much of a phone viewport. */
  #ss-folder-label {
    top: 6px;
    left: 8px;
  }
  /* Pin BOTH horizontal edges instead of left:50% + translateX(-50%) —
     the transform approach rendered shifted half-off the left edge on
     real devices. Edge-pinning can't miscenter. */
  #ss-menu {
    left: 4vw !important;
    right: 4vw !important;
    top: 45% !important;
    transform: translateY(-50%);
    width: auto;
    box-shadow: 0 12px 48px rgba(0, 0, 0, 0.75);
  }
}
/* Polaroid mode — white-framed card, photos drop in via CSS transition.
   Padding leaves the classic bottom strip below the image. */
/* .ss-polaroid-pos = translating wrapper (absolute on the screensaver).
   .ss-polaroid = inner visible polaroid that rotates. Split exists so
   translate and rotate can run with different timing functions during
   the entry animation — translate ease-out (snap into place), rotate
   ease-in (twist concentrates near the END, like a card flipped onto
   a table). CSS transition:transform applies ONE timing to the whole
   matrix, so nesting is the only way to give the two motions distinct
   curves. position:relative on .ss-polaroid so the caption div can
   anchor to it. */
/* z-index:0 (not auto) so every card has its own stacking context from
   birth — topplers later jump to z 100, and pre-creating the context makes
   that a pure compositor reorder instead of a re-rasterize (which showed
   as a shadow flicker at bump time). DOM order still ranks equals. */
.ss-polaroid-pos {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
  z-index: 0;
}
.ss-polaroid {
  position: relative;
  background: #f5f5f0;
  padding: 12px 12px 48px 12px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.7);
  border-radius: 2px;
  cursor: pointer;
  will-change: transform;
}
/* NO will-change here — promoting every photo to its own compositor layer
   (tried for the toppler z-bump flicker) made Chrome rebuild the layer tree
   at every exit-animation start, which blinked WORSE. The blank-frame fix
   lives in JS instead: img.decoding='sync' on the preloaded element. */
.ss-polaroid img {
  display: block;
  width: 100%;
  height: auto;
}
.ss-polaroid:hover {
  box-shadow:
    0 0 0 2px #fff,
    0 12px 40px rgba(0, 0, 0, 0.9);
}
/* Polaroid caption — handwritten names of the people in the photo, sat in
   the polaroid's bottom strip (the 48px-tall area below the image, in the
   padding zone). Font-size set inline per-polaroid so it scales with size.
   The 2-line clamp handles "John, Mary, and Jane" on small polaroids; the
   outer flex centers vertically inside the strip. pointer-events:none so
   clicks pass through to the polaroid (which routes to its folder). */
/* overflow:hidden on the CAPTION div (not just the span) — `align-items:
   center` lets a content-sized span overhang both top + bottom of the
   strip if its text wraps to 2 tall lines. Clipping at the strip's box
   guarantees nothing escapes the polaroid's bottom. */
.ss-polaroid-caption {
  position: absolute;
  left: 12px;
  right: 12px;
  bottom: 0;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 10px;
  pointer-events: none;
  overflow: hidden;
}
/* line-height 0.9 — Caveat's EM box is loose, glyphs sit well inside it,
   so lines can pack tighter than the default 1.0-1.2 without descender
   collision. Two lines of 24px text at 0.9 = 43.2px, fits the 48px strip
   with breathing room. padding-right gives Caveat's handwriting tails
   (g, y, e, etc.) safe space before the line-clamp overflow:hidden clips. */
.ss-polaroid-caption span {
  font-family: 'Caveat', cursive;
  font-weight: 500;
  color: #1c1c1c;
  text-align: center;
  line-height: 0.9;
  max-height: 100%;
  padding: 0 6px 0 2px;
  overflow: hidden;
  word-break: break-word;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
}
/* Conveyor mode — 4 rows pinned to the viewport, each is a relative
   container; cells are absolutely positioned and translated by JS each
   frame. overflow:hidden clips off-screen cells while they enter/exit
   so we don't have to mask globally. pointer-events on the row so hover
   pause-on-this-row works without bubbling to siblings. */
.ss-conveyor-row {
  position: absolute;
  left: 0;
  right: 0;
  overflow: hidden;
  pointer-events: auto;
}
.ss-conveyor-cell {
  position: absolute;
  top: 0;
  left: 0;
  cursor: pointer;
  will-change: transform;
  overflow: hidden;
}
.ss-conveyor-cell img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.ss-conveyor-cell:hover {
  box-shadow: 0 0 0 2px #fff;
  z-index: 5;
}
/* Faces showcase — two crossfading SQUARE frames, each background-image
   positioned by cropDivToFace so the face fills the frame with some context
   padding. Square because cropDivToFace forces square crops; 80vmin keeps
   the frame inside both viewport dimensions on any aspect. Large name
   overlay sits below the frame. No corner badge — the face IS the
   showcase. */
#ss-faces-stage {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.ss-faces-frame {
  position: absolute;
  width: 80vmin;
  height: 80vmin;
  background-color: var(--surface2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 1.2s ease-in-out;
  border-radius: 14px;
  box-shadow: 0 24px 72px rgba(0, 0, 0, 0.75);
}
.ss-faces-name {
  position: absolute;
  left: 0;
  right: 0;
  bottom: max(4vh, 32px);
  text-align: center;
  color: #fff;
  font-size: clamp(24px, 4vw, 44px);
  font-weight: 600;
  letter-spacing: 0.5px;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.9);
  z-index: 200;
  pointer-events: none;
}
.ss-faces-notice {
  color: rgba(255, 255, 255, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  font-size: 18px;
  padding: 24px;
  text-align: center;
}
#modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 500;
  background: rgba(0, 0, 0, 0.88);
  align-items: center;
  justify-content: center;
  touch-action: none; /* JS owns pinch/pan/swipe (gallery-lightbox.js + gallery-mobile.js) */
}
/* Peek-from-screensaver: modal must paint above #screensaver (z 900). The
   modal creates its own stacking context (position:fixed + z-index), so
   bumping #modal alone brings every child along — including modal-star,
   modal-faces-toggle, modal-all-rects, etc. Stays below post-enhance
   (10001) and compare (10002). */
#modal.ss-peek {
  z-index: 1100;
}
/* While zoomed (class toggled in applyZoom), hide nav arrows + star +
   report. visibility (not display) so it overrides the inline display
   from showModalAt and the .show logic without fighting them. */
#modal.zoomed #modal-prev,
#modal.zoomed #modal-next,
#modal.zoomed #modal-star,
#modal.zoomed #modal-report,
#modal.zoomed #modal-story {
  visibility: hidden;
}
/* While zoomed, the file-info pill swaps for a "Tap to return" hint
   (same pill styling as #modal-info). */
#modal-zoom-hint {
  display: none;
  position: fixed;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, 0.7);
  padding: 6px 14px;
  border-radius: 20px;
  font-size: 12px;
  color: #ddd;
  white-space: nowrap;
  pointer-events: none;
}
#modal.zoomed #modal-info {
  display: none;
}
#modal.zoomed #modal-zoom-hint {
  display: block;
}
/* While zoomed, the Enhanced/Original toggle pins to the bottom edge
   (its JS anchor is photo-relative and stale once the photo is scaled —
   positionOriginalToggleBtn bails at mScale !== 1). !important overrides
   the inline top/left that JS set at 1×. Tap-to-exit exempts this button
   (gallery-lightbox.js) so Enhanced/Original can be compared while zoomed. */
#modal.zoomed #modal-original-toggle {
  top: auto !important;
  left: 50% !important;
  bottom: 16px;
  transform: translateX(-50%);
}
/* …and the hint sits directly above it (sibling selector — hint markup
   follows the toggle in index.php). Only when the toggle is shown. */
#modal.zoomed #modal-original-toggle.show ~ #modal-zoom-hint {
  bottom: 56px;
}
/* Clean view (single tap/click at 1×, JS toggleChrome) — hide every control
   except the close ×, plus the meta block, and surface the "Tap to return"
   hint. Lets the photo be viewed uncluttered without zooming. */
#modal.chrome-hidden button:not(#modal-close) {
  visibility: hidden;
}
#modal.chrome-hidden #modal-info {
  display: none;
}
#modal.chrome-hidden #modal-zoom-hint {
  display: block;
}
#modal.open {
  display: flex;
}
#modal-img {
  max-width: 92vw;
  max-height: 88vh;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}
/* Loading state — while the new photo's bytes are in flight, show a centered
   swirl and hide the image + the photo-anchored chrome (star/report/story/
   Enhanced pill/meta) so nothing floats on black. The close × and ‹ › nav stay
   visible so a slow load is never a trap. Driven by the `img-loading` class on
   #modal (added before each src change, removed on load/error in
   gallery-lightbox.js). */
#modal-spinner {
  display: none;
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 5;
}
#modal-spinner .spinner {
  width: 44px;
  height: 44px;
  border-width: 4px;
}
#modal.img-loading #modal-spinner {
  display: block;
}
#modal.img-loading #modal-img {
  opacity: 0;
}
#modal.img-loading button:not(#modal-close):not(#modal-prev):not(#modal-next) {
  visibility: hidden;
}
#modal.img-loading #modal-info,
#modal.img-loading #modal-stories-chip {
  display: none;
}

/* Upgrade spinner (mobile). Shown while a sharper tier streams in OVER the
   still-visible current image — the 480 display tier behind the grid-thumb
   placeholder at open, or the full-res original on zoom — so unlike #modal-spinner
   it does NOT hide #modal-img or the chrome. Small + WHITE with a drop-shadow so it reads on
   both dark and light photos. JS-anchored to the photo's top edge (top/left set by
   positionUpgradeSpinner, clamped into the viewport + centred on the photo), so the
   inline top:8px/left:50% here is just a pre-paint fallback. Driven by `lb-upgrading`
   on #modal (armed ~150ms into the fetch in gallery-lightbox.js _lbUpgradeToFull;
   removed on swap/error/nav/close). */
#modal-upgrade-spinner {
  display: none;
  position: fixed;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6));
}
#modal-upgrade-spinner .spinner {
  width: 52px;
  height: 52px;
  border: 4px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
}
#modal.lb-upgrading #modal-upgrade-spinner {
  display: block;
}
#modal-info {
  position: fixed;
  bottom: 16px;
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 8px;
  font-size: 12px;
  color: #eee;
  white-space: nowrap;
  /* No backdrop box — the meta sits on pure black almost always. A doubled
     text-shadow (tight halo + soft drop, mirroring .stars-badge) keeps it
     legible in the rare case it overlaps the photo (tall image + chip +
     Enhanced toggle pushing it up). */
  text-shadow: 0 1px 3px rgba(0, 0, 0, 1), 0 0 2px rgba(0, 0, 0, 0.95);
}
#modal-info a {
  color: var(--accent);
  text-decoration: none;
  margin-left: 10px;
}
#modal-info a:hover {
  text-decoration: underline;
}
/* "📖 N stories" chip — leads the meta pill (#modal-info) whenever the photo
   has approved stories. Tapping opens the stories reader. A subtle inline pill
   so it reads as tappable against the meta pill's dark background. */
#modal-stories-chip {
  /* Own centered row at the TOP of the meta pill (not inline with the
     filename). Block-level (flex) forces the meta text onto the next line;
     width:fit-content + margin:auto centers it. The Enhanced/Original toggle
     dodges this automatically — it measures #modal-info's top, which now
     includes this row. */
  display: flex;
  align-items: center;
  gap: 4px;
  width: fit-content;
  margin: 0 auto 7px;
  padding: 3px 12px 3px 10px;
  border-radius: 999px;
  /* Dark underlay (not a light tint): the chip can sit ON the photo when the
     image is tall, and a light fill washes out over bright areas. Matches the
     report/story corner pills so white text + book icon stay legible anywhere. */
  background: rgba(0, 0, 0, 0.62);
  border: 1px solid rgba(255, 255, 255, 0.28);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.5;
  cursor: pointer;
}
#modal-stories-chip:hover {
  background: rgba(0, 0, 0, 0.8);
}
#modal-stories-chip .book-svg {
  width: 13px;
  height: 13px;
}
#modal-stories-chip .msc-label {
  font-weight: 500;
  opacity: 0.92;
}

/* ── Stories reader — tap-to-reveal panel ──────────────────────────────────
   Right-side panel on desktop, bottom sheet on mobile (<=600px, the app's
   breakpoint). Sits above the lightbox; Esc/backdrop/× close it. */
.stories-reader {
  position: fixed;
  inset: 0;
  z-index: 10040;
  display: flex;
  justify-content: flex-end;
  background: rgba(0, 0, 0, 0.45);
  opacity: 0;
  transition: opacity 0.2s ease;
}
.stories-reader.open {
  opacity: 1;
}
.stories-reader .sr-panel {
  display: flex;
  flex-direction: column;
  width: 380px;
  max-width: 92vw;
  height: 100%;
  background: #1a1a1d;
  color: #eee;
  border-left: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 40px rgba(0, 0, 0, 0.6);
  transform: translateX(24px);
  transition: transform 0.2s ease;
}
.stories-reader.open .sr-panel {
  transform: translateX(0);
}
.stories-reader .sr-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  padding: 14px 16px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.stories-reader .sr-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  font-weight: 600;
}
.stories-reader .sr-title .book-svg {
  width: 18px;
  height: 18px;
}
.stories-reader .sr-close {
  background: none;
  border: none;
  color: #aaa;
  font-size: 18px;
  line-height: 1;
  padding: 4px;
  cursor: pointer;
}
.stories-reader .sr-close:hover {
  color: #fff;
}
.stories-reader .sr-body {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 4px 16px 16px;
  -webkit-overflow-scrolling: touch;
}
.stories-reader .sr-card {
  padding: 14px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}
.stories-reader .sr-card:last-child {
  border-bottom: none;
}
.stories-reader .sr-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--text-dim, #999);
}
.stories-reader .sr-author {
  color: #ddd;
  font-weight: 600;
}
.stories-reader .sr-voice {
  color: var(--accent);
}
.stories-reader .sr-when {
  margin-left: auto;
}
.stories-reader .sr-card-title {
  margin-bottom: 5px;
  font-size: 15px;
  font-weight: 600;
}
.stories-reader .sr-card-body {
  font-size: 14px;
  line-height: 1.55;
  color: #ddd;
  word-wrap: break-word;
}
.stories-reader .sr-audio {
  width: 100%;
  margin: 8px 0 4px;
}
.stories-reader .sr-empty,
.stories-reader .sr-loading {
  padding: 16px 0;
  font-size: 13px;
  color: #999;
}
@media (max-width: 600px), (max-height: 600px) {
  .stories-reader {
    justify-content: stretch;
    align-items: flex-end;
  }
  .stories-reader .sr-panel {
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: 82vh;
    border-left: none;
    border-radius: 16px 16px 0 0;
    transform: translateY(28px);
  }
  .stories-reader.open .sr-panel {
    transform: translateY(0);
  }
}
/* When the desktop reader (side panel) is open, slide the centered photo into
   the free space to its left so the 380px panel never covers it. The photo is
   flex-centered in #modal, so right-padding re-centers it; the viewport-fixed
   meta gets a matching translate. Desktop only — the mobile reader is a bottom
   sheet (no horizontal overlap). 380px must match .sr-panel's desktop width;
   gallery-stories.js re-anchors the photo's overlay pills after the shift. */
@media (min-width: 601px) and (min-height: 601px) {
  #modal.reader-shift {
    padding-right: 380px;
  }
  #modal.reader-shift #modal-info {
    transform: translateX(calc(-50% - 190px));
  }
}
#modal-select {
  display: none;
  position: fixed;
  bottom: 16px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-select.show {
  display: block;
}
#modal-select.is-selected {
  border-color: var(--accent);
  color: var(--accent);
}
#modal-copy-path {
  display: none;
  position: fixed;
  bottom: 54px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-copy-path.show {
  display: block;
}
#modal-copy-path:hover {
  border-color: var(--accent);
  color: var(--accent);
}
#modal-enhance {
  display: none;
  position: fixed;
  bottom: 92px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-enhance.show {
  display: block;
}
#modal-enhance:hover {
  border-color: var(--accent);
  color: var(--accent);
}
#modal-enhance:disabled {
  opacity: 0.6;
  cursor: default;
}
#modal-original-toggle {
  display: none;
  position: fixed;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 16px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
  z-index: 5;
  white-space: nowrap;
}
#modal-original-toggle.show {
  display: block;
}
#modal-original-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}
#modal-original-toggle .mot-sep {
  opacity: 0.5;
  margin: 0 6px;
}
#modal-original-toggle .mot-action {
  font-weight: 600;
}
#modal-replace {
  display: none;
  position: fixed;
  bottom: 130px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-replace.show {
  display: block;
}
#modal-replace:hover {
  border-color: var(--accent);
  color: var(--accent);
}
#modal-replace:disabled {
  opacity: 0.6;
  cursor: default;
}
#modal-faces-toggle {
  display: none;
  position: fixed;
  bottom: 168px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-faces-toggle.show {
  display: block;
}
#modal-faces-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}
/* Admin "🏷 Tags" pill — next slot up the right-edge stack. Opens the AI-tag
   info card (#modal-tags-panel). .is-open mirrors the accent hover so the
   button reads as toggled while the card is showing. */
#modal-tags {
  display: none;
  position: fixed;
  bottom: 244px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.25);
  color: #ddd;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-tags.show {
  display: block;
}
#modal-tags:hover,
#modal-tags.is-open {
  border-color: var(--accent);
  color: var(--accent);
}
/* AI-tag info card — left side so it never covers the right-edge button stack.
   Shown only while an admin has the Tags button toggled on. */
#modal-tags-panel {
  display: none;
  position: fixed;
  top: 64px;
  left: 20px;
  width: 320px;
  max-width: calc(100vw - 40px);
  max-height: 78vh;
  overflow-y: auto;
  background: rgba(18, 18, 20, 0.94);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 12px;
  padding: 14px 16px 16px;
  color: #ddd;
  font-size: 13px;
  line-height: 1.5;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
  z-index: 700;
}
#modal-tags-panel.show {
  display: block;
}
#modal-tags-close {
  position: absolute;
  top: 8px;
  right: 10px;
  background: none;
  border: none;
  color: #999;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}
#modal-tags-close:hover {
  color: #fff;
}
#modal-tags-edit-syn {
  margin-top: 12px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.16);
  color: #cdd6e3;
  font-size: 12px;
  cursor: pointer;
  padding: 5px 11px;
  border-radius: 14px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-tags-edit-syn:hover {
  border-color: var(--accent);
  color: var(--accent);
}
/* Masked secret fields (API keys, admin token) — plain text inputs visually
   masked with dots, WITHOUT being type=password, so Chrome's login heuristics
   and password managers (LastPass) ignore them. The 👁 reveal toggles this
   class instead of the input type. */
.secret-mask {
  -webkit-text-security: disc;
}
#modal-tags-body .mt-caption {
  font-style: italic;
  color: #fff;
  margin: 2px 26px 12px 0;
}
#modal-tags-body .mt-meta {
  color: #9aa0aa;
  font-size: 12px;
  margin-bottom: 12px;
}
#modal-tags-body .mt-row {
  margin-bottom: 10px;
}
#modal-tags-body .mt-label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #8a8f98;
  margin-bottom: 4px;
}
#modal-tags-body .mt-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
#modal-tags-body .mt-chip {
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 12px;
  padding: 2px 9px;
  font-size: 12px;
  color: #e3e3e6;
}
#modal-tags-body .mt-chip.mt-vocab {
  border-color: rgba(96, 165, 250, 0.5);
}
#modal-tags-body .mt-chip.mt-face {
  border-color: rgba(52, 211, 153, 0.55);
  background: rgba(52, 211, 153, 0.12);
}
#modal-tags-body .mt-empty {
  color: #777;
  font-size: 12px;
}
#modal-tags-body .mt-foot {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  color: #777;
  font-size: 11px;
}
#modal-mark-orphans {
  display: none;
  position: fixed;
  bottom: 206px;
  right: 20px;
  background: rgba(0, 0, 0, 0.65);
  border: 1px solid rgba(239, 68, 68, 0.5);
  color: #fca5a5;
  font-size: 12px;
  cursor: pointer;
  padding: 6px 14px;
  border-radius: 20px;
  transition:
    border-color 0.15s,
    color 0.15s;
}
#modal-mark-orphans.show {
  display: block;
}
#modal-mark-orphans:hover {
  border-color: rgba(239, 68, 68, 0.9);
  color: #fecaca;
}
/* Show-all overlay layered atop the lightbox photo. JS positions it to match
   #modal-img's getBoundingClientRect() (mirrors #modal-original-toggle). */
#modal-all-rects {
  display: none;
  position: fixed;
  pointer-events: none;
  z-index: 6;
}
#post-enhance-overlay {
  display: none;
  position: fixed;
  inset: 0;
  /* Above compare (10002) + the enhance dialog (10010). */
  z-index: 10011;
  background: rgba(0, 0, 0, 0.92);
  align-items: center;
  justify-content: center;
}
#post-enhance-overlay.open {
  display: flex;
}
#pe-img {
  max-width: 92vw;
  max-height: 80vh;
  object-fit: contain;
  border-radius: 4px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6);
}
#pe-hint {
  position: fixed;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(20, 20, 20, 0.85);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #ddd;
  font-size: 13px;
  padding: 8px 16px;
  border-radius: 6px;
  max-width: 92vw;
  text-align: center;
}
#pe-hint strong {
  color: #fff;
}
#pe-close {
  position: fixed;
  top: 14px;
  right: 18px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 22px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#pe-close:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
#compare-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 10002;
  background: rgba(0, 0, 0, 0.96);
  flex-direction: column;
  padding: 50px 20px 12px;
  box-sizing: border-box;
}
#compare-overlay.open {
  display: flex;
}
#cmp-close {
  position: fixed;
  top: 14px;
  right: 18px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 22px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}
#cmp-close:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
#cmp-zen {
  position: fixed;
  top: 14px;
  right: 62px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 16px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}
#cmp-zen:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
#cmp-star {
  position: fixed;
  top: 14px;
  right: 106px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 18px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}
#cmp-star:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
#cmp-star.is-starred {
  color: var(--heart);
}
#cmp-enhance {
  position: fixed;
  top: 14px;
  right: 150px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 17px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 4;
}
#cmp-enhance:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
/* Prev/next photo nav — overlaid on the stage edges so they work in zen
   mode too. JS hides each at its end of the list (display:none inline). */
#cmp-prev,
#cmp-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 4;
  width: 40px;
  height: 72px;
  border: none;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.45);
  color: #ccc;
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 0 4px;
}
#cmp-prev {
  left: 10px;
}
#cmp-next {
  right: 10px;
}
#cmp-prev:hover,
#cmp-next:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.18);
}
#compare-overlay.cmp-zen-on #cmp-prev,
#compare-overlay.cmp-zen-on #cmp-next {
  opacity: 0.35;
}
#compare-overlay.cmp-zen-on #cmp-prev:hover,
#compare-overlay.cmp-zen-on #cmp-next:hover {
  opacity: 1;
}
/* Zen mode — stage edge-to-edge, every control hidden. Hiding #cmp-actions
   zeroes its clientWidth, which self-disables placeSides/clampPctToButtons
   in gallery-enhance.js (both early-return on barW=0). */
#compare-overlay.cmp-zen-on {
  padding: 0;
}
#compare-overlay.cmp-zen-on #cmp-title,
#compare-overlay.cmp-zen-on #cmp-pickers,
#compare-overlay.cmp-zen-on #cmp-actions {
  display: none;
}
#compare-overlay.cmp-zen-on #cmp-zen,
#compare-overlay.cmp-zen-on #cmp-star,
#compare-overlay.cmp-zen-on #cmp-enhance,
#compare-overlay.cmp-zen-on #cmp-close {
  opacity: 0.35;
}
#compare-overlay.cmp-zen-on #cmp-zen:hover,
#compare-overlay.cmp-zen-on #cmp-star:hover,
#compare-overlay.cmp-zen-on #cmp-enhance:hover,
#compare-overlay.cmp-zen-on #cmp-close:hover {
  opacity: 1;
}
#cmp-title {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  color: #ddd;
  font-size: 13px;
  max-width: 60vw;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#cmp-stage {
  position: relative;
  flex: 1;
  width: 100%;
  min-height: 0;
  user-select: none;
  touch-action: none;
  cursor: ew-resize;
  overflow: hidden;
}
/* Zoom frame: positioned by JS to the photo's rendered (contain) rect — the
   zoom "window". overflow:hidden clips zoomed content at the photo's edges
   instead of letting it bleed into the letterbox bars. The before-side wipe
   lives on #cmp-clip-before (OUTSIDE the img transform) so the divider stays
   fixed in screen space while the images zoom/pan under it. */
#cmp-frame {
  position: absolute;
  inset: 0;
  overflow: hidden;
}
#cmp-clip-before {
  position: absolute;
  inset: 0;
  clip-path: inset(0 50% 0 0);
  z-index: 2;
  pointer-events: none;
}
#cmp-img-before,
#cmp-img-after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  pointer-events: none;
  transform-origin: 50% 50%;
}
#cmp-img-after {
  z-index: 1;
}
#cmp-stage.cmp-zoomed {
  cursor: grab;
}
#cmp-stage.cmp-zoomed:active {
  cursor: grabbing;
}
#cmp-divider {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  background: rgba(255, 255, 255, 0.3);
  transform: translateX(-0.5px);
  z-index: 3;
  pointer-events: none;
  box-shadow: 0 0 4px rgba(0, 0, 0, 0.45);
}
#cmp-stage.snapping #cmp-divider {
  transition: left 0.15s ease-out;
}
#cmp-stage.snapping #cmp-clip-before {
  transition: clip-path 0.15s ease-out;
}
#cmp-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 42px;
  height: 42px;
  border-radius: 50%;
  background: #fff;
  color: #111;
  font-size: 16px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
  pointer-events: auto;
  cursor: ew-resize;
  touch-action: none;
}
.cmp-label {
  display: none;
}
.cmp-dims {
  font-size: 10px;
  letter-spacing: 0;
  color: #bbb;
  margin-top: 2px;
  font-weight: normal;
}
.cmp-label-before {
  left: 12px;
}
.cmp-label-after {
  right: 12px;
}
/* BEFORE / AFTER chip pickers above the stage. */
#cmp-pickers {
  display: flex;
  /* BEFORE group hugs the left, AFTER group hugs the right (both above the
     image). Wraps to two lines only if too narrow to fit side by side. */
  flex-direction: row;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 4px 16px;
  padding: 6px 0 8px;
  flex-shrink: 0;
}
.cmp-picker-row {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
}
/* AFTER row (2nd) mirrors BEFORE: chips first, the AFTER label on the far-right
   edge so the two labels anchor the left/right extremes. */
#cmp-pickers .cmp-picker-row:last-child {
  flex-direction: row-reverse;
}
.cmp-picker-label {
  font-size: 10px;
  color: var(--text-dim);
  width: 50px;
  flex-shrink: 0;
  letter-spacing: 1px;
}
.cmp-chip {
  font-size: 11px;
  padding: 4px 10px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: #1a1a1c;
  color: #ccc;
  cursor: pointer;
}
.cmp-chip:hover {
  border-color: rgba(255, 255, 255, 0.35);
}
.cmp-chip.active {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
/* Action bar: relatively-positioned canvas. JS pins each side's inner
   edge to the divider with a fixed gap; clamps so neither group hijacks
   off the bar's left/right edge. BEFORE anchors by `right`, AFTER by
   `left` — JS sets the pixel values; the defaults here are only a
   pre-paint fallback. */
#cmp-actions {
  position: relative;
  padding: 8px 0 4px;
  min-height: 50px;
}
/* Negative `top` lifts each group so the label overlaps the bottom edge
   of the image above, and the buttons land just under it. */
.cmp-side {
  position: absolute;
  top: -44px;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  transition:
    left 0.04s linear,
    right 0.04s linear;
}
.cmp-side-before {
  right: 50%;
}
.cmp-side-after {
  left: 50%;
}
.cmp-side-label {
  color: #fff;
  font-size: 13px;
  font-weight: 600;
  text-align: center;
  letter-spacing: 0.5px;
  white-space: nowrap;
  background: rgba(0, 0, 0, 0.6);
  padding: 4px 10px;
  border-radius: 4px;
}
.cmp-side-dims {
  font-size: 10px;
  letter-spacing: 0;
  color: #bbb;
  margin-top: 2px;
  font-weight: normal;
}
.cmp-side-buttons {
  display: flex;
  gap: 8px;
}
#cmp-actions button {
  font-size: 13px;
  padding: 8px 16px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: #2a2a2c;
  color: #ddd;
  cursor: pointer;
}
#cmp-actions button:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
#cmp-actions button[id^='cmp-select']:not(:disabled) {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
#cmp-actions button[id^='cmp-delete']:not(:disabled) {
  color: #d99;
}
#cmp-actions #cmp-approve {
  border-color: var(--accent);
  background: var(--accent);
  color: #fff;
}
/* 📄 prompt button inside a compare side label (enhanced versions only). */
.cmp-prompt-btn {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  font-size: 11px;
  font-weight: normal;
  letter-spacing: 0;
  line-height: 1.4;
  vertical-align: middle;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: #2a2a2c;
  color: #cfcfcf;
  cursor: pointer;
}
.cmp-prompt-btn:hover {
  border-color: var(--accent);
  color: #fff;
}
/* ── Drag-modal pattern (desktop only) ────────────────────────────────────
   Opt-in: .drag-modal overlay + .dm-box inner box + .dm-handle grab area,
   wired by js/gallery-drag-modal.js. Dragging the handle adds .dm-dragged,
   which clears the backdrop tint so the photo behind is fully visible — the
   modal stays blocking + click-to-dismiss, only the paint changes. All gated
   to desktop; mobile keeps the plain centered modal. */
@media (hover: hover) and (pointer: fine) {
  .dm-handle {
    cursor: move;
    touch-action: none;
    -webkit-user-select: none;
    user-select: none;            /* dragging from a label must not select its text */
  }
  /* …but the whole-box handle (story/report) still holds editable fields — keep
     them selectable + correctly cursored; buttons keep the pointer cursor. */
  .dm-handle input,
  .dm-handle textarea {
    -webkit-user-select: text;
    user-select: text;
    cursor: text;
  }
  .dm-handle button,
  .dm-handle a { cursor: pointer; }
  .drag-modal.dm-dragged {
    background: transparent !important;
  }
  .drag-modal.dm-dragged .dm-box {
    box-shadow: 0 16px 50px rgba(0, 0, 0, 0.6);
  }
}
/* Prompt-used modal — opened from a side label's 📄 button. Above the compare
   overlay (10002) and enhance dialog (10010). */
#cmp-prompt-modal {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 10065;
  background: rgba(0, 0, 0, 0.6);
  align-items: center;
  justify-content: center;
}
#cmp-prompt-modal.open {
  display: flex;
}
#cmp-prompt-modal .cpm-box {
  display: flex;
  flex-direction: column;
  width: 560px;
  max-width: 92vw;
  max-height: 80vh;
  padding: 18px 20px;
  background: #1d1d1f;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
#cmp-prompt-modal .cpm-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
#cmp-prompt-modal .cpm-title {
  font-size: 15px;
  font-weight: 600;
  color: #eee;
}
#cmp-prompt-modal .cpm-close-x {
  padding: 0;
  border: none;
  background: none;
  color: #aaa;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
#cmp-prompt-modal .cpm-close-x:hover {
  color: #fff;
}
#cmp-prompt-modal .cpm-meta {
  margin: 4px 0 12px;
  font-size: 11px;
  color: #999;
}
/* Structured breakdown: freeform "Custom note" + numbered block list. */
#cmp-prompt-modal .cpm-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
#cmp-prompt-modal .cpm-sec-label {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: #888;
}
#cmp-prompt-modal .cpm-freeform {
  margin-bottom: 14px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: #ddd;
  white-space: pre-wrap;
  word-break: break-word;
  background: #141416;
  border: 1px solid var(--border);
  border-radius: 6px;
}
#cmp-prompt-modal .cpm-blocks {
  margin: 0;
  padding-left: 22px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
#cmp-prompt-modal .cpm-blocks li {
  color: #888;
  font-size: 12px;
}
#cmp-prompt-modal .cpm-block-chip {
  display: inline-block;
  padding: 3px 10px;
  font-size: 12px;
  font-weight: 600;
  color: #dfe8ff;
  background: rgba(80, 120, 255, 0.18);
  border: 1px solid var(--accent);
  border-radius: 12px;
}
#cmp-prompt-modal .cpm-block-text {
  color: #ccc;
  font-size: 13px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}
#cmp-prompt-modal .cpm-empty {
  color: #888;
  font-size: 13px;
  font-style: italic;
}
#cmp-prompt-modal .cpm-prompt {
  flex: 0 0 auto;
  max-height: 30vh;
  margin: 12px 0 0;
  padding: 12px 14px;
  overflow-y: auto;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.5;
  color: #ddd;
  white-space: pre-wrap;
  word-break: break-word;
  background: #141416;
  border: 1px solid var(--border);
  border-radius: 6px;
}
#cmp-prompt-modal .cpm-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 14px;
}
#cmp-prompt-modal .cpm-foot button.cpm-toggle {
  margin-right: auto;
  padding: 8px 4px;
  background: none;
  border: none;
  color: #9ab;
  text-decoration: underline;
  cursor: pointer;
}
#cmp-prompt-modal .cpm-foot button.cpm-toggle:hover {
  color: #cde;
}
#cmp-prompt-modal .cpm-foot button {
  padding: 8px 16px;
  font-size: 13px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: #2a2a2c;
  color: #ddd;
  cursor: pointer;
}
#cmp-prompt-modal .cpm-foot button.cpm-copy {
  border-color: var(--accent);
}
#enhance-dialog {
  display: none;
  position: fixed;
  inset: 0;
  /* Above the compare overlay (10002) so enhancing from compare keeps it open
     behind the dialog. */
  z-index: 10010;
  background: rgba(0, 0, 0, 0.6);
  align-items: center;
  justify-content: center;
}
#enhance-dialog.open {
  display: flex;
}
#enhance-dialog .ed-box {
  background: #1d1d1f;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 10px;
  padding: 18px 20px;
  width: 760px;
  max-width: 94vw;
  /* Fixed (generous) height so the panes don't grow/shrink with block count.
     The library list and the block stack each fill this and scroll internally
     instead of resizing the dialog. Caps to the viewport on short screens. */
  height: 720px;
  max-height: 90vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
#enhance-dialog h3 {
  margin: 0 0 4px;
  font-size: 15px;
  color: #eee;
}
#enhance-dialog .ed-sub {
  margin: 0 0 12px;
  font-size: 12px;
  color: #999;
  word-break: break-all;
}
#enhance-dialog .ed-body {
  display: flex;
  gap: 12px;
  flex: 1;
  min-height: 0;
}
#enhance-dialog .ed-library {
  width: 260px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 6px;
  background: #111;
  min-height: 280px;
}
#enhance-dialog .ed-lib-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 12px;
  color: #aaa;
}
#enhance-dialog .ed-lib-head-btns {
  display: inline-flex;
  gap: 4px;
}
#enhance-dialog .ed-lib-head button {
  font-size: 11px;
  padding: 3px 8px;
  background: #2a2a2c;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  color: #ddd;
  cursor: pointer;
}
#enhance-dialog .ed-lib-head button:hover:not(:disabled) {
  border-color: rgba(255, 255, 255, 0.35);
  color: #fff;
}
#enhance-dialog .ed-lib-head button:disabled {
  opacity: 0.4;
  cursor: default;
}
#enhance-dialog .ed-lib-tools {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 5px 8px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
#enhance-dialog .ed-lib-tools-label {
  font-size: 11px;
  color: rgba(255, 255, 255, 0.45);
  margin-right: auto;
}
#enhance-dialog .ed-lib-tools button {
  font-size: 11px;
  padding: 3px 10px;
  background: #2a2a2c;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  color: #ddd;
  cursor: pointer;
}
#enhance-dialog .ed-lib-tools button:hover {
  border-color: rgba(255, 255, 255, 0.35);
  color: #fff;
}
#enhance-dialog .ed-lib-list {
  flex: 1;
  overflow-y: auto;
  padding: 4px;
}
#enhance-dialog .ed-row {
  border-radius: 4px;
  margin-bottom: 2px;
}
#enhance-dialog .ed-row:hover {
  background: rgba(255, 255, 255, 0.04);
}
#enhance-dialog .ed-row-disabled .ed-row-main {
  opacity: 0.4;
}
#enhance-dialog .ed-row-disabled input[type='checkbox'] {
  cursor: not-allowed;
}
#enhance-dialog .ed-row-main {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 6px;
  cursor: pointer;
  font-size: 12px;
  color: #ddd;
}
#enhance-dialog .ed-row-main input[type='checkbox'] {
  margin: 0;
  flex-shrink: 0;
}
#enhance-dialog .ed-row-grab {
  color: #555;
  flex-shrink: 0;
  cursor: grab;
  user-select: none;
  font-size: 11px;
  letter-spacing: -2px;
  width: 12px;
  text-align: center;
}
#enhance-dialog .ed-row:hover .ed-row-grab {
  color: #888;
}
#enhance-dialog .ed-row.dragging {
  opacity: 0.4;
}
#enhance-dialog .ed-row.drag-over {
  border-top: 2px solid var(--accent);
}
#enhance-dialog .ed-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#enhance-dialog .ed-name-input {
  flex: 1;
  background: #1a1a1c;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 3px;
  color: #ddd;
  font-size: 12px;
  padding: 2px 4px;
  font-family: inherit;
}
#enhance-dialog .ed-row-actions {
  display: inline-flex;
  gap: 3px;
  margin-left: auto;
  flex-shrink: 0;
  opacity: 0;
  transition: opacity 0.1s;
}
#enhance-dialog .ed-row:hover .ed-row-actions,
#enhance-dialog .ed-row.editing .ed-row-actions {
  opacity: 1;
}
#enhance-dialog .ed-row-actions button {
  font-size: 10px;
  line-height: 1;
  padding: 2px 5px;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 3px;
  color: #aaa;
  cursor: pointer;
}
#enhance-dialog .ed-row-actions button:hover {
  color: #fff;
  border-color: rgba(255, 255, 255, 0.3);
}
#enhance-dialog .ed-row-actions .ed-del {
  color: #d77;
  border-color: rgba(220, 100, 100, 0.25);
}
#enhance-dialog .ed-row-actions .ed-del:hover {
  color: #f88;
  border-color: rgba(220, 100, 100, 0.5);
}
#enhance-dialog .ed-row-body {
  display: none;
  width: calc(100% - 12px);
  margin: 0 6px 6px;
  box-sizing: border-box;
  min-height: 110px;
  max-height: 260px;
  background: #1a1a1c;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  color: #ddd;
  font-size: 12px;
  padding: 6px;
  font-family: inherit;
  resize: vertical;
}
#enhance-dialog .ed-row.editing .ed-row-body {
  display: block;
}
#enhance-dialog .ed-preview {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  gap: 6px;
}
#enhance-dialog .ed-prev-head {
  font-size: 12px;
  color: #aaa;
  margin: 0;
}
#enhance-dialog .ed-blocks-head {
  margin-top: 4px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
#enhance-dialog .ed-blocks-head-btns {
  display: inline-flex;
  gap: 4px;
  flex-shrink: 0;
}
#enhance-dialog .ed-blocks-head-btns button {
  font-size: 11px;
  padding: 3px 10px;
  background: #2a2a2c;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 4px;
  color: #ddd;
  cursor: pointer;
}
#enhance-dialog .ed-blocks-head-btns button:hover {
  border-color: rgba(255, 255, 255, 0.35);
  color: #fff;
}
#enhance-dialog textarea#ed-freeform {
  width: 100%;
  box-sizing: border-box;
  min-height: 70px;
  max-height: 160px;
  resize: vertical;
  background: #111;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 6px;
  color: #ddd;
  font-size: 13px;
  padding: 8px 10px;
  font-family: inherit;
  flex-shrink: 0;
}
#enhance-dialog .ed-blocks {
  flex: 1;
  min-height: 120px;
  overflow-y: auto;
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 6px;
  background: #0e0e10;
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
#enhance-dialog .ed-blocks-empty {
  color: #666;
  font-size: 12px;
  padding: 16px 12px;
  text-align: center;
  font-style: italic;
}
#enhance-dialog .ed-block {
  background: #1a1a1c;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 4px;
  padding: 6px 8px;
}
#enhance-dialog .ed-block.dragging {
  opacity: 0.4;
}
#enhance-dialog .ed-block.drag-over {
  border-color: var(--accent);
}
#enhance-dialog .ed-block-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: #ddd;
  cursor: grab;
}
#enhance-dialog .ed-block-grab {
  color: #666;
  flex-shrink: 0;
  user-select: none;
  font-size: 11px;
  letter-spacing: -2px;
  width: 12px;
  text-align: center;
}
#enhance-dialog .ed-block-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#enhance-dialog .ed-block-toggle {
  background: transparent;
  border: none;
  color: #999;
  cursor: pointer;
  font-size: 11px;
  padding: 2px 6px;
}
#enhance-dialog .ed-block-toggle:hover {
  color: #fff;
}
#enhance-dialog .ed-block-body {
  display: none;
  font-size: 11px;
  color: #aaa;
  padding: 6px 4px 0;
  white-space: pre-wrap;
  max-height: 160px;
  overflow-y: auto;
  line-height: 1.4;
}
#enhance-dialog .ed-block.expanded .ed-block-body {
  display: block;
}
#enhance-dialog .ed-footer {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  margin-top: 14px;
  gap: 12px;
}
#enhance-dialog .ed-opts-wrap {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
#enhance-dialog .ed-prov,
#enhance-dialog .ed-prov-opts {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 12px;
  color: #ccc;
  flex-wrap: wrap;
}
#enhance-dialog .ed-prov span,
#enhance-dialog .ed-prov-opts span {
  color: #aaa;
}
#enhance-dialog .ed-prov label,
#enhance-dialog .ed-prov-opts label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
#enhance-dialog .ed-prov input,
#enhance-dialog .ed-prov-opts input {
  margin: 0;
}
#enhance-dialog .ed-res {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: #ccc;
}
#enhance-dialog .ed-res span {
  color: #aaa;
}
#enhance-dialog .ed-res label {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
}
#enhance-dialog .ed-res input {
  margin: 0;
}
#enhance-dialog .ed-aspect {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: #ccc;
  cursor: pointer;
}
#enhance-dialog .ed-aspect input {
  margin: 0;
}
#enhance-dialog .ed-actions {
  display: flex;
  gap: 8px;
}
#enhance-dialog .ed-footer button {
  font-size: 13px;
  cursor: pointer;
  padding: 7px 16px;
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: #2a2a2c;
  color: #ddd;
}
#enhance-dialog .ed-footer button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
#enhance-dialog .ed-footer button:disabled {
  opacity: 0.6;
  cursor: default;
}
#modal-close {
  position: fixed;
  top: 14px;
  right: 18px;
  background: rgba(0, 0, 0, 0.5);
  border: none;
  color: #ccc;
  font-size: 22px;
  cursor: pointer;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
}
#modal-close:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.15);
}
/* Anchored inline by JS to the image's rendered top-left corner so it
   sits inside the photo, not at the viewport edge (image is centered +
   letterboxed by max-width/max-height + object-fit:contain). */
#modal-star {
  display: none;
  position: fixed;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #eee;
  font-size: 24px;
  cursor: pointer;
  border-radius: 50%;
  width: 34px;
  height: 34px;
  align-items: center;
  justify-content: center;
  transition:
    color 0.15s,
    background 0.15s,
    transform 0.12s,
    border-color 0.15s;
  z-index: 510;
}
#modal-star.show {
  display: flex;
}
#modal-star:hover {
  color: var(--heart);
  background: rgba(0, 0, 0, 0.78);
  border-color: var(--heart-border);
  transform: scale(1.08);
}
#modal-star.is-starred {
  color: var(--heart);
  border-color: var(--heart-border);
}
/* Report-problem pill — JS-anchored to #modal-img's bottom-right (mirrors
   #modal-star top-left). Anon-only; admins don't see it. Amber hue to read
   as a warning without competing with the star's gold accent. */
#modal-report {
  display: none;
  position: fixed;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #eee;
  font-size: 18px;
  cursor: pointer;
  border-radius: 50%;
  width: 34px;
  height: 34px;
  align-items: center;
  justify-content: center;
  transition:
    color 0.15s,
    background 0.15s,
    transform 0.12s,
    border-color 0.15s;
  z-index: 510;
  line-height: 1;
}
#modal-report.show {
  display: flex;
}
#modal-report:hover {
  color: #fbbf24;
  background: rgba(0, 0, 0, 0.78);
  border-color: rgba(251, 191, 36, 0.55);
  transform: scale(1.08);
}
/* Add-a-story pill — JS-anchored to #modal-img's bottom-LEFT (mirrors the
   report pill on the right). Anon-only; admins manage stories from the
   sidebar. Indigo hue to read as "write / contribute". */
#modal-story {
  display: none;
  position: fixed;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #eee;
  font-size: 18px;
  cursor: pointer;
  border-radius: 50%;
  width: 34px;
  height: 34px;
  align-items: center;
  justify-content: center;
  transition:
    color 0.15s,
    background 0.15s,
    transform 0.12s,
    border-color 0.15s;
  z-index: 510;
  line-height: 1;
}
#modal-story.show {
  display: flex;
}
#modal-story:hover {
  color: #818cf8;
  background: rgba(0, 0, 0, 0.78);
  border-color: rgba(129, 140, 248, 0.55);
  transform: scale(1.08);
}
/* Shared outline icon for the report + story modal controls — matches the
   heart's white-outline treatment so all three controls read as one set.
   1em so each button's font-size drives scale; currentColor lets the hover
   hue carry straight through. */
.ctrl-ico {
  width: 1em;
  height: 1em;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Faces preview overlay star — mirrors #modal-star but sits above the
   faces overlay (z-index 9999). JS-anchored to #fp-img top-left. */
#fp-star {
  display: none;
  position: fixed;
  background: rgba(0, 0, 0, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #eee;
  font-size: 20px;
  cursor: pointer;
  border-radius: 50%;
  width: 34px;
  height: 34px;
  align-items: center;
  justify-content: center;
  transition:
    color 0.15s,
    background 0.15s,
    transform 0.12s,
    border-color 0.15s;
  z-index: 10000;
}
#fp-star.show {
  display: flex;
}
#fp-star:hover {
  color: var(--heart);
  background: rgba(0, 0, 0, 0.78);
  border-color: var(--heart-border);
  transform: scale(1.08);
}
#fp-star.is-starred {
  color: var(--heart);
  border-color: var(--heart-border);
}
.modal-nav {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0, 0, 0, 0.45);
  border: none;
  color: #ccc;
  font-size: 28px;
  cursor: pointer;
  padding: 12px 8px;
  border-radius: 4px;
  line-height: 1;
  z-index: 12;
}
.modal-nav:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
}
#modal-prev {
  left: 10px;
}
#modal-next {
  right: 10px;
}

/* ── MODALS (shared) ── */
.overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 600;
  background: rgba(0, 0, 0, 0.7);
  align-items: center;
  justify-content: center;
}
.overlay.open {
  display: flex;
}
.modal-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.modal-box h3 {
  font-size: 14px;
  font-weight: 700;
}
.modal-box p {
  font-size: 12px;
  color: var(--text-dim);
}
.form-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.form-row label {
  font-size: 11px;
  color: var(--text-dim);
}
.form-row input {
  padding: 7px 10px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  font-size: 13px;
  width: 100%;
}
.form-row input:focus {
  outline: none;
  border-color: var(--accent);
}
.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 4px;
}
/* ── Account banner (Preferences) ─────────────────────────────────────────
   Shown to signed-in non-admin users on desktop + mobile (renderAccountBanner,
   gallery-user-auth.js). Lives in app.css (not mobile.css) so it styles on
   desktop too — mobile.css only loads ≤600px. Inline Edit-name / Change-password
   forms (#cab-edit-form / #cab-pw-form) toggle in place. JS sets display. */
#config-account-banner {
  margin: 0 0 14px;
  padding: 0 0 14px;
  border-bottom: 1px solid var(--border);
}
#config-account-banner .cab-name   { font-size: 16px; font-weight: 600; color: var(--text); }
#config-account-banner .cab-email  { margin-top: 2px; font-size: 12px; color: var(--text-dim); word-break: break-all; }
#config-account-banner .cab-status { margin-top: 4px; font-size: 12px; }
#config-account-banner .cab-actions { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
#config-account-banner .cab-form { margin-top: 12px; max-width: 360px; }
#config-account-banner .cab-form .form-row { margin-bottom: 8px; }
#config-account-banner .cab-form .pw-field { display: flex; align-items: center; gap: 6px; }
#config-account-banner .cab-form-error { margin-top: 6px; font-size: 12px; color: var(--danger); }
/* ── Peek-modal pattern (mobile) ──────────────────────────────────────────
   Opt-in: .peek-modal overlay + a .pm-peek trigger button, wired by
   js/gallery-peek-modal.js. Tapping Peek hides the dialog and reveals the photo
   behind it (the lightbox); the floating #pm-return brings it back. Mobile only
   — the Peek button is display:none above 600px. */
.pm-peek {
  display: none;
  font-size: 13px;
  padding: 8px 14px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  cursor: pointer;
}
@media (max-width: 600px), (max-height: 600px) {
  .pm-peek { display: inline-block; margin-right: auto; }
}
.peek-modal.pm-peeking {
  background: transparent;
}
.peek-modal.pm-peeking .modal-box {
  display: none;
}
#pm-return {
  display: none;
  position: fixed;
  left: 50%;
  /* Default fallback; JS (positionReturn) anchors it just above #modal-info. */
  bottom: 88px;
  transform: translateX(-50%);
  z-index: 10090;
  align-items: center;
  gap: 8px;
  padding: 14px 30px;
  font-size: 17px;
  font-weight: 600;
  color: #fff;
  background: var(--accent);
  border: none;
  border-radius: 26px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}
#pm-return.show {
  display: inline-flex;
}
.ef-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--surface2);
  border-radius: 5px;
  padding: 6px 10px;
  font-size: 12px;
  gap: 8px;
}
.ef-row span {
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
}
.ef-row button {
  flex-shrink: 0;
}
.btn-primary {
  padding: 6px 14px;
  border-radius: 5px;
  border: 1px solid var(--accent);
  background: transparent;
  color: var(--accent);
  cursor: pointer;
  font-size: 12px;
}
.btn-primary:hover {
  background: var(--accent-bg);
}
.btn-primary:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.btn-primary:disabled:hover {
  background: transparent;
}
.btn-cancel {
  padding: 6px 14px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 12px;
}
.btn-cancel:hover {
  background: var(--surface2);
  color: var(--text);
}
.btn-danger {
  padding: 6px 14px;
  border-radius: 5px;
  border: 1px solid var(--danger);
  background: transparent;
  color: var(--danger);
  cursor: pointer;
  font-size: 12px;
}
.btn-danger:hover {
  background: var(--danger-bg);
}

/* ── LOGIN MODAL ── */
#login-modal .modal-box {
  width: 320px;
}

/* ── SETTINGS MODAL ── */
#settings-modal .modal-box {
  width: 380px;
}
.settings-row {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.settings-row:last-of-type {
  border-bottom: none;
}
.settings-row-left {
  flex: 1;
  min-width: 0;
}
.settings-label {
  font-size: 13px;
  color: var(--text);
}
.settings-desc {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 3px;
  line-height: 1.4;
}
.settings-control {
  flex-shrink: 0;
}
.thumb-slider-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
}
.thumb-slider-wrap input[type='range'] {
  width: 140px;
  accent-color: var(--accent);
  cursor: pointer;
}
.thumb-slider-val {
  font-size: 12px;
  color: var(--text-dim);
  min-width: 36px;
  text-align: right;
}
.settings-control select {
  padding: 5px 8px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  min-width: 80px;
}
.settings-control select:focus {
  outline: none;
  border-color: var(--accent);
}

/* ── HELP MODAL ── */
#help-modal .modal-box {
  width: 480px;
  max-height: 80vh;
  overflow-y: auto;
}

/* ── CONFIG MODAL ── fixed size so switching sections doesn't resize/recenter
   the box; the sidebar stays pinned and only the content pane changes. */
#config-modal .modal-box {
  width: 900px;
  max-width: 92vw;
  height: 670px;
  max-height: 92vh;
}
/* Mobile Config: the desktop two-column layout (150px nav | content)
   squeezes content into a sliver on phones. Stack instead — nav becomes a
   horizontal chip row on top, settings rows stack label-over-control.
   !important needed: the partial uses inline styles. */
@media (max-width: 600px), (max-height: 600px) {
  #config-modal .modal-box > div:has(#config-nav) {
    flex-direction: column !important;
    gap: 10px !important;
  }
  #config-nav {
    flex: 0 0 auto !important;
    flex-direction: row !important;
    flex-wrap: wrap;
    border-right: none !important;
    border-bottom: 1px solid var(--border);
    padding: 0 0 8px !important;
  }
  .config-nav-item {
    border-left: none !important;
    padding: 6px 10px !important;
  }
  .settings-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
  .settings-control {
    width: 100%;
  }
  .thumb-slider-wrap {
    width: 100%;
  }
  .thumb-slider-wrap input[type='range'] {
    flex: 1;
    width: auto;
  }
}
.help-section {
  margin-top: 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.help-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  background: var(--surface2);
  border: none;
  color: var(--text);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  cursor: pointer;
  text-align: left;
}
.help-toggle:hover {
  background: #333;
}
.help-toggle .help-chevron {
  font-size: 10px;
  color: var(--text-dim);
  transition: transform 0.2s;
}
.help-toggle.open .help-chevron {
  transform: rotate(180deg);
}
.help-body {
  display: none;
  padding: 10px 12px 6px;
}
.help-body.open {
  display: block;
}
.help-row {
  display: flex;
  gap: 10px;
  font-size: 12px;
  margin-bottom: 6px;
  line-height: 1.4;
}
.help-key {
  flex-shrink: 0;
  width: 110px;
  font-weight: 600;
  color: var(--text);
}
.help-desc {
  color: var(--text-dim);
}

/* ── UPLOAD MODAL ── */
#upload-modal .modal-box {
  width: 420px;
}
.progress-bar-wrap {
  background: var(--surface2);
  border-radius: 4px;
  height: 8px;
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 4px;
  transition: width 0.2s;
  width: 0%;
}
#upload-current {
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#upload-slots {
  margin-top: 6px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: var(--surface2);
  border-radius: 4px;
  padding: 6px 8px;
  font-size: 10.5px;
  color: var(--text-dim);
}
.upload-slot-row {
  display: flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  overflow: hidden;
}
.upload-slot-row .slot-dot {
  font-size: 9px;
  color: var(--accent);
  flex-shrink: 0;
  transition: opacity 0.2s;
  opacity: 0.25;
}
.upload-slot-row.active .slot-dot {
  opacity: 1;
}
.upload-slot-row .slot-name {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── DELETE CONFIRM ── */
#delete-modal .modal-box {
  width: 400px;
}
#delete-list {
  max-height: 160px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 10px;
  font-size: 11px;
  color: var(--text-dim);
  line-height: 1.8;
}

/* ── RENAME MODAL ── */
#rename-modal .modal-box {
  width: 380px;
}

/* ── SHARED FOLDER PICKER (used by move + upload flows) ── */
/* Must sit above other .overlay modals (e.g. the Config modal — same .overlay
   z-index but it renders later in the DOM, so it would otherwise cover the
   picker when launched from Config → Tagging). */
#folder-picker-modal {
  z-index: 11000;
}
#folder-picker-modal .modal-box {
  width: 420px;
}
#folder-picker-search {
  width: 100%;
  box-sizing: border-box;
  padding: 8px 12px;
  margin-bottom: 10px;
  border-radius: 5px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  font-size: 13px;
}
#folder-picker-search:focus {
  outline: none;
  border-color: var(--accent);
}
#folder-picker-tree {
  border: 1px solid var(--border);
  border-radius: 4px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--surface2);
  padding: 4px 0;
}
.fp-node {
  display: block;
}
.fp-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 13px;
  user-select: none;
}
.fp-row:hover {
  background: rgba(255, 255, 255, 0.06);
}
.fp-row.selected {
  background: var(--accent-bg);
  color: #fff;
}
.fp-chevron {
  flex-shrink: 0;
  width: 14px;
  text-align: center;
  font-size: 10px;
  color: var(--text-dim);
  transition: transform 0.12s ease;
  display: inline-block;
}
.fp-chevron.leaf {
  visibility: hidden;
}
.fp-node.expanded > .fp-row > .fp-chevron:not(.leaf) {
  transform: rotate(90deg);
}
.fp-icon {
  flex-shrink: 0;
  font-size: 13px;
}
.fp-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fp-children {
  display: none;
  padding-left: 16px;
  border-left: 1px dotted rgba(255, 255, 255, 0.08);
  margin-left: 14px;
}
.fp-node.expanded > .fp-children {
  display: block;
}
.fp-row.fp-search-hidden {
  display: none;
}
.fp-empty {
  padding: 14px 12px;
  font-size: 12px;
  color: var(--text-dim);
  text-align: center;
}
#folder-picker-new-area {
  margin-top: 10px;
}
#btn-folder-picker-new {
  background: none;
  border: 1px dashed var(--border);
  color: var(--text-dim);
  padding: 6px 10px;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
  width: 100%;
}
#btn-folder-picker-new:hover {
  color: var(--text);
  border-color: var(--accent);
}
#folder-picker-new-row {
  display: none;
  gap: 6px;
  align-items: center;
}
#folder-picker-new-row.open {
  display: flex;
}
#folder-picker-new-input {
  flex: 1;
  padding: 6px 10px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  font-size: 12px;
}
#folder-picker-new-input:focus {
  outline: none;
  border-color: var(--accent);
}
.fp-new-btn {
  padding: 6px 10px;
  border-radius: 4px;
  border: 1px solid var(--border);
  background: var(--surface2);
  color: var(--text);
  cursor: pointer;
  font-size: 12px;
}
.fp-new-btn:hover {
  background: rgba(255, 255, 255, 0.08);
}

/* ── EMPTY / LOADING ── */
#empty {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 60vh;
  color: var(--text-dim);
  gap: 8px;
  font-size: 14px;
}
#empty.show {
  display: flex;
}
#empty .big {
  font-size: 40px;
}
#loading {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 60vh;
  color: var(--text-dim);
  gap: 10px;
}
#loading.show {
  display: flex;
}
.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

/* ── FACES PANEL ── */
#faces-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0 12px;
  flex-wrap: wrap;
}
/* Sticky header wrapper. Pins the toolbar to the top of the scroll container
   (#content) so Rescan / Re-cluster / Hide-named / Reset stay reachable while
   triaging long face-group lists. During a scan, #faces-scan-row (status +
   progress) is rendered INSIDE this wrapper, so the toolbar + progress card pin
   together as one block (survives toolbar flex-wrap with no magic offset) and
   only the groups list scrolls beneath. When idle, the scan row is rendered
   OUTSIDE this wrapper and scrolls normally. */
#faces-sticky-head {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
}
#faces-progress-wrap {
  background: var(--surface2);
  border-radius: 4px;
  height: 6px;
  overflow: hidden;
  margin-bottom: 4px;
}
#faces-progress-bar {
  height: 100%;
  background: var(--accent);
  border-radius: 4px;
  width: 0%;
  transition: width 0.3s;
}
#faces-status {
  font-size: 11px;
  color: var(--text-dim);
}
#faces-merge-bar {
  display: none;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
  /* Lives inside #faces-sticky-head, so it pins under the toolbar when visible
     (2+ groups selected). The sticky head supplies the bottom spacing/border. */
  margin-bottom: 0;
}
#faces-merge-bar.visible {
  display: flex;
}
#faces-merge-count {
  font-size: 11px;
  color: var(--text-dim);
}

.face-group {
  background: var(--surface2);
  border-radius: 6px;
  margin-bottom: 10px;
  border: 2px solid transparent;
  overflow: hidden;
}
.face-group.fg-selected {
  border-color: var(--accent);
}
/* Subtle hue tints over the grey base: Orphaned = warm/orange hint,
   Strangers (system) = cool/blue hint. Just a whisper of color. */
.face-group.fg-orphans {
  background: #2f2a24;
}
.face-group.fg-system {
  background: #24272f;
}
.face-group.fg-potentially {
  border-left: 4px dashed var(--accent);
  margin-left: 16px;
  opacity: 0.95;
}
.face-group.fg-potentially .fg-suggest-prefix {
  font-style: italic;
  font-weight: 400;
  color: var(--text-dim);
  margin-right: 4px;
}
.face-group.fg-potentially .fg-suggest-name {
  font-weight: 600;
}
.fg-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
}
.fg-merge-label {
  cursor: pointer;
  flex-shrink: 0;
}
.fg-thumbs {
  display: flex;
  gap: 4px;
  flex-wrap: nowrap;
  flex-shrink: 0;
}
.fg-crop {
  width: 60px;
  height: 60px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--border);
  flex-shrink: 0;
}
.fg-thumb {
  width: 72px;
  height: 60px;
  border-radius: 4px;
  background: var(--border) center / cover no-repeat;
  flex-shrink: 0;
  overflow: hidden;
  cursor: pointer;
  transition: outline 0.1s;
}
.fg-thumb:hover {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.fg-meta {
  flex: 1;
  min-width: 0;
}
.fg-label {
  font-size: 16px;
  font-weight: 600;
}
/* Line 1 of a group card: name + nest indicator + count + edit controls, all on
   one row (line 2 is .fg-actions-row2). Two lines keeps the card height ≈ the
   60px thumb height, with the header vertically centered. */
.fg-line1 {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.fg-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}
.fg-actions-row2 {
  margin-top: 6px;
}
.fg-name-btn,
.fg-nest-btn {
  padding: 3px 8px !important;
  font-size: 11px !important;
}
.fg-count {
  font-size: 11px;
  color: var(--text-dim);
}
.fg-expand-btn {
  padding: 3px 8px !important;
  font-size: 11px !important;
}
.fg-delete-btn {
  padding: 3px 7px;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 11px;
}
.fg-delete-btn:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: var(--danger-bg);
}

.fg-photos {
  display: none;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 12px 12px;
  border-top: 1px solid var(--border);
}
.fg-photos.open {
  display: flex;
}
.fg-photo-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100px;
}
.fg-photo-item img {
  width: 100px;
  height: 100px;
  object-fit: cover;
  border-radius: 4px;
  background: var(--border);
}
.fg-photo-name {
  font-size: 9px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
  text-align: center;
}
.fg-remove-btn {
  padding: 2px 6px;
  font-size: 10px;
  border-radius: 3px;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}
.fg-remove-btn:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: var(--danger-bg);
}
.fg-empty {
  font-size: 12px;
  color: var(--text-dim);
  padding: 8px;
}

/* ── TOAST ── */
#toast {
  position: fixed;
  bottom: 50px;
  left: 50%;
  transform: translateX(-50%);
  background: #222;
  border: 1px solid #444;
  border-radius: 20px;
  padding: 13px 24px;
  font-size: 16px;
  line-height: 1.4;
  font-weight: 500;
  color: #f0f0f0;
  z-index: 10010;
  opacity: 0;
  transition: opacity 0.2s;
  pointer-events: none;
  max-width: 80vw;
  max-height: 60vh;
  text-align: center;
  /* break-word (not anywhere) so normal words like "submitted" stay whole;
     only genuinely unbreakable tokens (long paths in error toasts) wrap. */
  overflow-wrap: break-word;
  word-break: normal;
  box-sizing: border-box;
}
#toast .toast-sub {
  display: block;
  font-size: 0.7em;
  opacity: 0.72;
  margin-top: 0.5em;
  font-weight: 400;
  line-height: 1.35;
  color: #d0d0d0;
}
#toast.show {
  opacity: 1;
  pointer-events: auto;
  cursor: pointer;
}
#toast.error {
  background: #4a0f0f;
  border-color: #ff4747;
  color: #ffd9d9;
  padding: 13px 24px;
  font-size: 16px;
  font-weight: 600;
  box-shadow: 0 0 0 0 rgba(255, 71, 71, 0.6);
}
#toast.error.show {
  animation: toastErrorPulse 1.1s ease-out infinite;
}
@keyframes toastErrorPulse {
  0% {
    box-shadow: 0 0 0 0 rgba(255, 71, 71, 0.65);
    border-color: #ff4747;
  }
  70% {
    box-shadow: 0 0 0 14px rgba(255, 71, 71, 0);
    border-color: #ff8585;
  }
  100% {
    box-shadow: 0 0 0 0 rgba(255, 71, 71, 0);
    border-color: #ff4747;
  }
}

/* ── MOBILE ── all mobile styling lives in css/mobile.css, loaded via a
   media-gated <link media="(max-width:600px)"> in index.php (2026-06,
   Phase 1 — see docs/mobile.md). Only the default-hidden state of
   mobile-only markup lives here so desktop renders without it. */
#sb-mobile-footer {
  display: none;
}
::-webkit-scrollbar {
  width: 6px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: #444;
  border-radius: 3px;
}

/* Admin Starred view: filename + folder hover overlay at the BOTTOM —
   same layout as the Enhanced panel's cv-hover (star pill sits top-left). */
.cell-hoverinfo {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.88) 0%,
    rgba(0, 0, 0, 0.7) 60%,
    rgba(0, 0, 0, 0) 100%
  );
  color: #fff;
  padding: 18px 10px 8px;
  opacity: 0;
  transition: opacity 0.12s;
  pointer-events: none;
}
.cell:hover .cell-hoverinfo {
  opacity: 1;
}
.chi-name {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.chi-path {
  font-size: 10px;
  opacity: 0.75;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-top: 2px;
}

/* Inline button spinner — used by the Enhance button while a run is in
   flight (gallery-enhance.js adds/removes .btn-spinning). Small rotating
   ring before the label so long runs (OpenAI high: 90-180s) look alive. */
.btn-spinning::before {
  content: '';
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 7px;
  vertical-align: -1px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: btn-spin 0.8s linear infinite;
}
@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

/* ── Stats vFolder (superadmin) — docs/stats.md ─────────────────────────────
   #stats-bar: sort select + totals strip between #pagination-top and #grid.
   display:none default; gallery-stats-admin.js statsBarSync() flips it to
   flex only in the __stats__ view. */
#stats-bar {
  display: none;
  align-items: center;
  gap: 10px;
  padding: 6px 12px;
  font-size: 12px;
  color: var(--text, #ddd);
  flex-wrap: wrap;
  /* Pinned below the sticky #pagination-top (z-index just under its 5).
     `top` is re-measured to the pagination row's height by statsBarSync();
     42px is only the pre-measure fallback. */
  position: sticky;
  top: 42px;
  z-index: 4;
  background: var(--bg);
}
#stats-bar label {
  opacity: 0.7;
}
/* Match the site's input styling (#grid-search). The dropdown LIST is
   native UI: `option` background/color is honored by Chromium/Firefox on
   desktop, and `color-scheme: dark` tells the browser to render the popup
   in its dark palette where option styling isn't honored (e.g. mobile). */
#stats-bar select {
  background: var(--surface2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 6px;
  font-size: 12px;
  color-scheme: dark;
}
#stats-bar select option {
  background: var(--surface2);
  color: var(--text);
}
#stats-totals {
  opacity: 0.7;
}
/* Grid/List toggle in the bar. */
#stats-view-toggle {
  display: inline-flex;
  gap: 4px;
}
#stats-view-toggle button {
  background: var(--surface2);
  color: var(--text-dim, #999);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 8px;
  font-size: 12px;
  cursor: pointer;
}
#stats-view-toggle button.active {
  color: var(--text);
  border-color: var(--accent, #4a90d9);
}
/* Plain-English roll-up paragraph under the bar — sticky, pinned below the
   bar (top re-measured by statsBarSync; 87px is the fallback). Full-width so
   the background covers scrolling rows; .ss-inner keeps the text measure. */
#stats-summary {
  display: none;
  padding: 2px 12px 10px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim, #aaa);
  position: sticky;
  top: 87px;
  z-index: 4;
  background: var(--bg);
}
#stats-summary .ss-inner {
  max-width: 900px;
}
#stats-summary strong {
  color: var(--text);
  font-weight: 600;
}
/* List view — #grid.stats-list swaps the tile grid for rows. Columns:
   thumb | name+folder | views | opens | zooms | min | last viewed. */
#grid.stats-list {
  display: block;
}
#grid.stats-list .sl-head,
#grid.stats-list .sl-row {
  display: grid;
  /* thumb | name | ★ | 👤 | 👁 | 🖱 | 🔎 | ⏱ min | last viewed */
  grid-template-columns: 52px minmax(140px, 1fr) 56px 48px 52px 52px 52px 64px 110px;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
}
#grid.stats-list .sl-sort {
  cursor: pointer;
  user-select: none;
}
#grid.stats-list .sl-sort:hover {
  color: var(--text);
}
#grid.stats-list .sl-sort.active {
  color: var(--text);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
}
#grid.stats-list .sl-head {
  font-size: 11px;
  color: var(--text-dim, #888);
  border-bottom: 1px solid var(--border);
  /* Pinned below #pagination-top + #stats-bar + #stats-summary (z just under
     the bar/summary's 4). `top` is re-measured to their combined height by
     statsBarSync(); 145px is only the pre-measure fallback. */
  position: sticky;
  top: 145px;
  z-index: 3;
  background: var(--bg);
}
#grid.stats-list .sl-row {
  font-size: 12px;
  color: var(--text);
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
  cursor: pointer;
}
#grid.stats-list .sl-row:hover {
  background: rgba(255, 255, 255, 0.05);
}
#grid.stats-list .sl-row img {
  width: 48px;
  height: 36px;
  object-fit: cover;
  border-radius: 4px;
  display: block;
}
#grid.stats-list .sl-name {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
#grid.stats-list .sl-fname {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#grid.stats-list .sl-folder {
  font-size: 11px;
  color: var(--text-dim, #888);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#grid.stats-list .sl-ago {
  font-size: 11px;
  color: var(--text-dim, #999);
}
.stats-list-empty {
  padding: 30px 12px;
  color: var(--text-dim, #999);
  font-size: 13px;
}
/* Tail rollup rows — server-grouped low-activity signatures, rendered after
   the individual rows. Same grid columns as .sl-row (the count chip sits in
   the thumb column); dimmed + non-clickable. */
#grid.stats-list .sl-rollup {
  display: grid;
  grid-template-columns: 52px minmax(140px, 1fr) 56px 48px 52px 52px 52px 64px 110px;
  align-items: center;
  gap: 8px;
  padding: 4px 10px;
  font-size: 12px;
  color: var(--text-dim, #999);
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
#grid.stats-list .sl-roll-n {
  width: 48px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  background: var(--surface2);
  border: 1px dashed var(--border);
  font-size: 11px;
  font-weight: 600;
}
/* Per-tile metric badge — bottom-RIGHT (bottom-left = like badge, top-left =
   story badge, top-right = selection check). Shows the current sort metric;
   full breakdown in the tooltip. Non-interactive. */
.cell .stats-badge {
  position: absolute;
  bottom: 3px;
  right: 4px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1;
  color: #fff;
  padding: 3px 6px;
  background: rgba(0, 0, 0, 0.55);
  border-radius: 8px;
  pointer-events: none;
  z-index: 2;
}
