/* ph stylesheet.
   Base layout + the "classic" gallery style (black-framed photos in a single
   centered column with a monospace EXIF caption). The `assist` body modifier
   enlarges tap targets and text for users who need it. Further styles (grid,
   artistic) will hook in via additional `.style-*` selectors. */

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

:root {
  color-scheme: light;
  --bg: #fff;
  --surface: #f3f3f3;
  --text: #222;
  --text-dim: #888;
  --accent: #000;
  --frame: #000;
  --line: #d6d6d6;
  --border: #8c8c8c;
  --badge-text: #555;
  --danger: #c0392b;
  --danger-bg: #fdecea;
  --maxw: 1100px;
}

/* Dark palette. Applied when the OS prefers dark (unless the user forced light
   via the corner toggle), and whenever the user explicitly selects dark - the
   toggle sets data-theme on <html>; see static/theme.js. Photos are never
   tinted, only the surrounding chrome. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --bg: #16181c;
    --surface: #23262b;
    --text: #e6e6e6;
    --text-dim: #9aa0a6;
    --accent: #d8dadf;
    --frame: #2c2f34;
    --line: #3a3e45;
    --border: #5b6068;
    --badge-text: #b8bcc2;
    --danger: #ef6f63;
    --danger-bg: #3a2422;
  }
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #16181c;
  --surface: #23262b;
  --text: #e6e6e6;
  --text-dim: #9aa0a6;
  --accent: #d8dadf;
  --frame: #2c2f34;
  --line: #3a3e45;
  --border: #5b6068;
  --badge-text: #b8bcc2;
  --danger: #ef6f63;
  --danger-bg: #3a2422;
}

html, body { height: 100%; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: 'Trebuchet MS', system-ui, sans-serif;
  line-height: 1.5;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* -- Top bar: breadcrumbs + user ------------------------------- */

.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  padding: 0.8rem 1rem;
  border-bottom: 2px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--bg);
  z-index: 10;
}

.crumbs {
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.95rem;
}
.crumbs .sep { color: var(--text-dim); padding: 0 0.35em; }

/* -- Portal tabs (Photos / Videos) ------------------------------ */
/* Two pills in the top bar that swap /p/ <-> /v/ at the current path. */
.portal-tabs {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.15rem;
  background: var(--surface, rgba(127, 127, 127, 0.08));
  border-radius: 999px;
  border: 1px solid var(--line);
}
.portal-tab {
  display: inline-block;
  padding: 0.2rem 0.85rem;
  border-radius: 999px;
  font-size: 0.85rem;
  color: var(--text-dim);
  text-decoration: none;
  line-height: 1.4;
}
.portal-tab:hover { text-decoration: none; color: var(--text); }
.portal-tab.active {
  background: var(--bg);
  color: var(--text);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.assist .portal-tab { font-size: 1rem; padding: 0.4rem 1.1rem; }

.userbox { display: flex; align-items: center; gap: 0.75rem; }
.userbox .user { color: var(--text-dim); font-size: 0.85rem; }

/* Per-tile cache state for video items: a small label rendered BELOW the
 * <video> element (not overlaid on the picture) so it never obscures the
 * frame. The .video-tile wrapper keeps the video and the badge grouped so
 * the runtime indicator (the shim flips .is-transcoding on the wrapper)
 * still finds them as a pair. */
.video-tile { display: block; }
.video-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35em;
  margin-top: 0.4rem;
  font-size: 0.7rem;
  font-family: ui-monospace, 'Courier New', monospace;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  background: transparent;
  color: var(--text-dim);
  text-transform: lowercase;
  line-height: 1.4;
  letter-spacing: 0.02em;
  border: 1px solid var(--line);
}
.video-badge .flag-icon {
  width: 0.95em;
  height: 0.95em;
  flex-shrink: 0;
}
/* The "ready to watch" flag stays positive (calm green tint) so the
 * operator can scan the index for cached videos at a glance. The
 * "first play transcodes" state is intentionally muted -- useful info,
 * not an alarm. The page is for admins first; viewers can still read it
 * but it does not compete with the photo content. */
.video-badge-cached {
  background: color-mix(in oklab, #1e8e3e 14%, transparent);
  color: #1e8e3e;
  border-color: color-mix(in oklab, #1e8e3e 30%, transparent);
}
/* Per-subdir aggregate cache flag. Mirrors the per-tile badge colours
 * but on the dir-listing item / menu-card label. */
.dir-video-state, .menu-video-state {
  display: inline-block;
  margin-left: 0.4em;
  padding: 0 0.4em;
  font-size: 0.7em;
  border-radius: 3px;
  border: 1px solid var(--line);
  color: var(--text-dim);
  letter-spacing: 0.02em;
  text-transform: lowercase;
  line-height: 1.5;
}
.dir-video-state-full, .menu-video-state-full {
  background: color-mix(in oklab, #1e8e3e 14%, transparent);
  color: #1e8e3e;
  border-color: color-mix(in oklab, #1e8e3e 30%, transparent);
}
.dir-video-state-partial, .menu-video-state-partial {
  background: color-mix(in oklab, #1565c0 12%, transparent);
  color: #1565c0;
  border-color: color-mix(in oklab, #1565c0 30%, transparent);
}

.video-badge-partial {
  /* Pre-warmed but not fully transcoded -- a soft blue so it does not
   * fight the green "ready" tint for attention but is still
   * distinguishable from the muted "none" baseline. */
  background: color-mix(in oklab, #1565c0 12%, transparent);
  color: #1565c0;
  border-color: color-mix(in oklab, #1565c0 30%, transparent);
}
.video-badge-uncached {
  /* Default muted look already; no overrides needed. */
}
/* Row beneath the video that holds the cached/uncached badge AND, for
 * admins, the per-video pin toggle. Wraps onto a second line on narrow
 * viewports rather than overflowing the tile. */
.video-meta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.inline-pin { display: inline; margin: 0; padding: 0; }
.pin-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  margin-top: 0.4rem;
  padding: 0.15rem 0.5rem;
  font-size: 0.7rem;
  font-family: ui-monospace, 'Courier New', monospace;
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
  text-transform: lowercase;
  line-height: 1.4;
  letter-spacing: 0.02em;
}
.pin-btn svg { width: 0.95em; height: 0.95em; }
.pin-btn:hover { color: var(--text); border-color: var(--text-dim); }
.pin-btn.pinned {
  background: color-mix(in oklab, var(--accent, #c80) 14%, transparent);
  color: var(--accent, #c80);
  border-color: color-mix(in oklab, var(--accent, #c80) 35%, transparent);
}

/* Active transcode for this tile: shim toggles .is-transcoding on the
 * wrapper while a slow fragment is in flight. Lift above the muted base
 * so the live-transcoding state is unambiguous during playback. */
.video-tile.is-transcoding .video-badge {
  background: color-mix(in oklab, #c97014 18%, transparent);
  color: #c97014;
  border-color: color-mix(in oklab, #c97014 40%, transparent);
}

/* Admin "what is transcoding right now" indicator. Hidden by default
 * (the JS unhides it when /admin/transcoding returns a non-empty list). */
.ph-transcoding-status {
  font-size: 0.75rem;
  font-family: ui-monospace, 'Courier New', monospace;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  background: color-mix(in oklab, var(--accent, #c80) 22%, transparent);
  color: var(--text);
  max-width: 32em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

button {
  font: inherit;
  cursor: pointer;
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.35rem 0.7rem;
  color: var(--text);
}
button:hover { border-color: var(--accent); }

/* -- Gallery container ----------------------------------------- */

.gallery {
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 1.5rem 1rem 5.5rem;
}

.up { margin-bottom: 1rem; font-family: ui-monospace, 'Courier New', monospace; }
/* Hierarchy-nav links (up a level, back to gallery) align their line-art
   arrow with the text, matching the folder icon in the directory list. */
.up a, .crumbs a.back { display: inline-flex; align-items: center; gap: 0.3em; }

.dirlist {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-bottom: 2rem;
}
.dirlist a {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  padding: 0.3rem 0;
  font-size: 1.1rem;
}
/* Subtle line-art folder, dimmer than the link text it precedes. */
.dirlist .icon {
  width: 1.2em;
  height: 1.2em;
  vertical-align: baseline;
  color: var(--text-dim);
}
/* A gallery's assigned title shown after its folder name. */
.dir-title { color: var(--text-dim); font-style: italic; }
.dir-title::before { content: "- "; }

/* "Already viewed" tick - a small green checkbox glyph next to folders the
   current user has opened (or anything under them). Inline at the end of a
   dirlist entry; tucked into the top-right corner of a big menu card. The
   tick is interactive: clicking it (or focusing and pressing Enter / Space)
   POSTs /unmark-viewed to clear that folder's viewed state. */
.viewed-mark {
  width: 1.1em;
  height: 1.1em;
  flex-shrink: 0;
  color: #2faa56; /* a calm "done" green, readable in both themes */
  cursor: pointer;
  transition: transform 0.1s ease, opacity 0.1s ease;
}
.viewed-mark:hover,
.viewed-mark:focus-visible { transform: scale(1.12); opacity: 1; outline: none; }
.viewed-mark:focus-visible { filter: drop-shadow(0 0 0.15rem #2faa56); }
.dirlist .viewed-mark { margin-left: 0.4rem; }
.menu-card .viewed-mark {
  position: absolute;
  top: 0.6rem;
  right: 0.6rem;
  width: 1.25rem;
  height: 1.25rem;
}

/* Direct image count next to a folder name, e.g. "(200)". */
.dir-count { color: var(--text-dim); font-size: 0.85em; }
.dir-count::before { content: "("; }
.dir-count::after { content: ")"; }

/* Album title + image count under a top-level menu card. */
.menu-subtitle { color: var(--text-dim); font-size: 0.95rem; }
.menu-count { color: var(--text-dim); font-size: 0.85rem; }
.menu-count::before { content: "("; }
.menu-count::after { content: ")"; }
.assist .menu-subtitle { font-size: 1.2rem; }
.assist .menu-count { font-size: 1.05rem; }

.empty { color: var(--text-dim); }

/* -- Pinned-album feed (main page): collage + title, newest first --------- */

.pinned-feed {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1.25rem;
  margin: 1rem 0 2.5rem;
}
.pin-card { display: block; color: var(--text); }
.pin-card:hover { text-decoration: none; }
.pin-card:hover .pin-title { color: var(--accent); }
.pin-collage {
  gap: 2px;
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  overflow: hidden;
  background: var(--surface);
}
.pin-collage img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* No portrait among the picks: an even landscape grid (2x2, or fewer). */
.pin-collage:not(.has-hero) {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-auto-rows: 1fr;
}
/* A lone photo fills the whole square; a 3rd spans the bottom row (no gap). */
.pin-collage.grid-1 img { grid-column: 1 / -1; grid-row: 1 / -1; }
.pin-collage.grid-3 img:last-child { grid-column: 1 / -1; }

/* A portrait is present: a full-height hero on the left (where a portrait crops
   gracefully), with the remaining tiles stacked in a column beside it. */
.pin-collage.has-hero { display: flex; }
.pin-collage.has-hero .pin-hero { flex: 1 1 0; min-width: 0; }
.pin-collage.has-hero .pin-rest {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.pin-collage.has-hero .pin-rest img { flex: 1 1 0; min-height: 0; }
.pin-title {
  display: block;
  margin-top: 0.5rem;
  font-weight: 600;
  text-align: center;
  word-break: break-word;
}
.assist .pin-title { font-size: 1.3rem; }

/* -- Landing menu: the archive root's top-level folders as big cards ------- */

.home-menu {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1.25rem;
  margin: 1rem 0 2.5rem;
}
.menu-card {
  position: relative; /* anchors the viewed tick in the corner */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding: 2.6rem 1.5rem;
  border: 2px solid var(--border);
  border-radius: 14px;
  color: var(--text);
  text-align: center;
  transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.menu-card:hover {
  text-decoration: none;
  border-color: var(--accent);
  background: var(--surface);
  transform: translateY(-2px);
}
.menu-icon { width: 3rem; height: 3rem; color: var(--text-dim); stroke: currentColor; flex-shrink: 0; }
.menu-name { font-size: 1.45rem; font-weight: 600; word-break: break-word; }

/* Assist: bigger cards, icons, and labels for easy tapping. */
.assist .home-menu { gap: 1.75rem; }
.assist .menu-card { padding: 3.6rem 1.5rem; gap: 1.2rem; }
.assist .menu-icon { width: 4rem; height: 4rem; }
.assist .menu-name { font-size: 1.95rem; }

/* On phone-sized viewports the top-level cards otherwise eat the whole screen
   (about 1.5 fit at a time). Tighten them so several fit at once - still big
   enough to tap easily, and a touch more generous in assist mode. */
@media (max-width: 600px) {
  .home-menu { gap: 0.75rem; }
  .menu-card { padding: 1.25rem 1rem; gap: 0.5rem; }
  .menu-icon { width: 2.2rem; height: 2.2rem; }
  .menu-name { font-size: 1.2rem; }
  .assist .home-menu { gap: 1rem; }
  .assist .menu-card { padding: 1.75rem 1rem; gap: 0.8rem; }
  .assist .menu-icon { width: 2.8rem; height: 2.8rem; }
  .assist .menu-name { font-size: 1.5rem; }
}

/* -- Likes (hearts) over photos ------------------------------------------- */

.heart {
  background: none;
  border: none;
  padding: 0.25rem;
  cursor: pointer;
  color: #fff;
  opacity: 0.85;
  line-height: 0;
  transition: transform 0.1s, opacity 0.15s, color 0.15s;
}
.heart svg {
  width: 1.6rem;
  height: 1.6rem;
  fill: none;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}
.heart:hover { transform: scale(1.12); opacity: 1; }
.heart.liked { color: #ff4d6d; opacity: 1; }
.heart.liked svg { fill: currentColor; }

/* The heart anchors to .photo-media (which is sized to the image), so it sits
   on the photo even when the figure is wider than a portrait image (e.g. a long
   EXIF caption in classic style). */
.photo-media { position: relative; display: block; }
.photo-media > a { display: block; }
.photo-media .heart { position: absolute; top: 0.4rem; right: 0.4rem; z-index: 2; }

/* Who has favourited this image, shown to everyone (logged-in users only -
   public share views skip it). The count badge sits below the heart; the
   names trail beneath in smaller text and wrap so longer lists don't push
   the layout. `.empty` hides the whole element when no one has liked yet. */
.fav-meta {
  position: absolute;
  top: 2.6rem;
  right: 0.4rem;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.1rem;
  pointer-events: none;
  text-align: right;
  line-height: 1.15;
}
.fav-meta.empty { display: none; }
.fav-count {
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
}
.fav-names {
  font-size: 0.7rem;
  color: #fff;
  max-width: min(10rem, 60vw);
  padding: 0.05rem 0.4rem;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.4);
  word-break: break-word;
}
/* Lightbox + artistic viewer: bottom-right corner of the image area. */
.viewer-fav-meta,
.lb-fav-meta { top: auto; bottom: 3rem; right: 0.6rem; }
.hide-hearts .fav-meta { display: none; }

/* Reveal a tile heart only while its photo is hovered, so empty hearts do not
   clutter the gallery. Liked hearts stay visible (your likes show at a glance),
   and keyboard focus reveals the heart too (so it stays usable without a mouse).
   Only on hover-capable devices - touch has no hover, so there the hearts stay
   shown. The artistic-viewer and lightbox hearts are deliberate single controls
   and keep their own styling. */
@media (hover: hover) {
  .photo-media .heart:not(.liked) { opacity: 0; pointer-events: none; }
  .photo-media:hover .heart:not(.liked),
  .photo-media:focus-within .heart:not(.liked) { opacity: 0.85; pointer-events: auto; }
  .photo-media:hover .heart:not(.liked):hover { opacity: 1; }
}

/* Thumbnail loading state. Cold galleries generate thumbnails on demand, so a
   tile can take a moment to appear; rather than a blank or broken-image tile,
   static/app.js marks each pending tile `.thumb-pending` and we show a soft
   shimmer until the image's load event fires, then fade the image in. A tile
   that genuinely fails (corrupt/unsupported) settles into a muted box via
   `.thumb-error`. Without JS the classes are never added, so tiles render
   normally - pure progressive enhancement. The filmstrip is excluded (its
   thumbs are small and already choreograph their own opacity). */
@keyframes thumb-shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
.photo-media.thumb-pending::after,
.photo-media.thumb-error::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  z-index: 1;
  pointer-events: none;
}
.photo-media.thumb-pending::after {
  background: linear-gradient(100deg, var(--surface) 30%, var(--border) 50%, var(--surface) 70%);
  background-size: 200% 100%;
  animation: thumb-shimmer 1.4s ease-in-out infinite;
}
.photo-media.thumb-error::after { background: var(--surface); }
.photo-media.thumb-pending img,
.photo-media.thumb-error img { opacity: 0; }
.photo-media img.frame { transition: opacity 0.35s ease; }
/* Classic tiles reserve no height before the image arrives; size the shimmer.
   The width must NOT be a percentage: a classic `.photo` is a content-sized
   flex item (the column centres rather than stretches it), so `100%` here has
   no definite basis and collapses to the heart's width whenever the EXIF
   caption that would otherwise give the column width is absent - which is
   always the case in assist mode (captions hidden) and for captionless photos.
   A viewport-relative cap stays definite in every case. */
.style-classic .photo-media.thumb-pending,
.style-classic .photo-media.thumb-error {
  width: min(540px, 90vw);
  min-height: 340px;
}
/* Pinned-collage images fade in over the card's surface background. */
.pin-collage img { transition: opacity 0.35s ease; }
.pin-collage img.thumb-pending { opacity: 0; }

/* Heart over the artistic viewer's big image. */
.style-artistic .viewer-img-wrap { position: relative; }
.viewer-heart { position: absolute; top: 0.5rem; right: 0.5rem; }

/* Heart in the lightbox (top-left; close is top-right). */
.lb-heart { position: absolute; top: 0.8rem; left: 1rem; z-index: 1; }
.lb-heart svg { width: 1.9rem; height: 1.9rem; }

.assist .heart svg { width: 2.2rem; height: 2.2rem; }

/* The artificial "Favourite" menu card: a filled heart to set it apart. */
.menu-card-fav .menu-icon { color: #ff4d6d; }

/* Favourites page: keep photos at their original aspect ratio (masonry columns,
   not square-cropped), with smaller, subtler hearts than the big-image views.
   The `.style-grid.is-favourites` prefix outweighs the square `.style-grid`
   rules regardless of source order. */
.style-grid.is-favourites .photos {
  display: block;
  column-width: 220px;
  column-gap: 8px;
}
.style-grid.is-favourites .photo {
  width: auto;
  break-inside: avoid;
  margin-bottom: 8px;
}
.style-grid.is-favourites .frame {
  aspect-ratio: auto;
  height: auto;
  object-fit: initial;
}
.style-grid.is-favourites .heart { opacity: 0.75; }
.style-grid.is-favourites .heart:hover { opacity: 1; }
.style-grid.is-favourites .heart svg { width: 1.2rem; height: 1.2rem; }
.style-grid.is-favourites.assist .heart svg { width: 1.7rem; height: 1.7rem; }

/* Gallery title / description / date range, shown at the top of a gallery. */
.gallery-meta { text-align: center; margin-bottom: 1.75rem; }
.gallery-title { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.3rem; }
.gallery-desc {
  color: var(--text-dim);
  max-width: 40rem;
  margin: 0 auto 0.4rem;
  white-space: pre-line;
}
.gallery-dates {
  color: var(--text-dim);
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}

/* -- Classic photo column -------------------------------------- */

.style-classic .photos {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2.5rem;
}

.style-classic .photo {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 100%;
}

.style-classic .frame {
  display: block;
  max-width: 100%;
  /* The cached thumbnail is sized generously (for the artistic viewer); cap the
     classic column's display height so photos are a touch larger than before
     without ballooning - the extra source pixels just sharpen on hi-DPI. */
  max-height: 720px;
  height: auto;
  border: 5px solid var(--frame);
}

.style-classic .exif {
  margin-top: 0.5rem;
  color: var(--text-dim);
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.72rem;
  text-align: center;
  letter-spacing: 0.02em;
}

/* Placeholder for media we can't render yet (HEIC/RAW). */
.placeholder {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-width: 240px;
  min-height: 180px;
  padding: 1.5rem;
  background: var(--surface);
  color: var(--text-dim);
  text-align: center;
  word-break: break-all;
}
.placeholder small { margin-top: 0.4rem; }

video.frame { width: 100%; }

/* -- Grid style: responsive thumbnail grid (good for phone dumps) - */

.style-grid .photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 6px;
}
.style-grid .photo { width: 100%; }
.style-grid .frame {
  border: none;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  display: block;
  border-radius: 2px;
}
/* Captions stay out of the way in grid view. */
.style-grid .exif { display: none; }
.style-grid .placeholder { min-width: 0; min-height: 0; aspect-ratio: 1 / 1; padding: 0.5rem; }
/* Grid and classic thumbnails act as lightbox triggers. */
.style-grid .photo a,
.style-classic .photo a { cursor: zoom-in; }

/* -- Grid lightbox: view a photo over the grid, no page navigation -------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: none;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.92);
}
.lightbox.open { display: flex; }

.lb-stage {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.6rem;
  max-width: 92vw;
  max-height: 92vh;
}
.lb-img {
  max-width: 92vw;
  max-height: 85vh;
  object-fit: contain;
  display: block;
  cursor: default;
}
.lb-caption {
  display: block;
  min-height: 1em;
  color: #ccc;
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.72rem;
  text-align: center;
  letter-spacing: 0.02em;
}

/* Close + nav controls: white line-art on the dark backdrop. */
.lb-close, .lb-nav {
  position: absolute;
  background: none;
  border: none;
  padding: 0.4rem;
  color: #fff;
  opacity: 0.55;
  transition: opacity 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}
.lb-close:hover, .lb-nav:hover { opacity: 1; }
.lb-close { top: 0.8rem; right: 1rem; }
.lb-close svg { width: 1.8rem; height: 1.8rem; }
.lb-nav { top: 50%; transform: translateY(-50%); }
.lb-nav svg { width: 2.6rem; height: 2.6rem; }
.lb-prev { left: 0.3rem; }
.lb-next { right: 0.3rem; }

/* Assist mode: larger lightbox controls. */
.assist .lb-close svg { width: 2.4rem; height: 2.4rem; }
.assist .lb-nav svg { width: 3.4rem; height: 3.4rem; }

/* -- Artistic style: one large central image + filmstrip ------- */
/* Ported from p-folio's viewer: ~65vh image (object-fit: contain), faint
   prev/next arrows, a dimmed horizontal thumbnail strip that auto-centres the
   active thumb. Navigation (arrows/keyboard/swipe) lives in static/app.js. */

.style-artistic .viewer {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.style-artistic .viewer-wrap {
  display: flex;
  align-items: center;
  height: 65vh;
}

.style-artistic .viewer-img-wrap {
  flex: 1;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.style-artistic .viewer-img {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  display: block;
}

.style-artistic .nav-btn {
  border: none;
  color: var(--accent);
  font-size: 2rem;
  width: 2.5rem;
  opacity: 0.12;
  transition: opacity 0.2s;
  user-select: none;
  line-height: 1;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.style-artistic .nav-btn:hover { opacity: 0.6; }

.style-artistic .viewer-caption {
  margin: 0;
  min-height: 1.2em;
  color: var(--text-dim);
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.72rem;
  text-align: center;
  letter-spacing: 0.02em;
}

.style-artistic .thumb-strip-wrap {
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.style-artistic .thumb-strip-wrap::-webkit-scrollbar { display: none; }

.style-artistic .thumb-strip {
  display: flex;
  gap: 5px;
  padding: 2px 0;
}

.style-artistic .thumb-btn {
  border: 2px solid transparent;
  padding: 0;
  flex-shrink: 0;
  transition: border-color 0.15s;
}
.style-artistic .thumb-btn img {
  display: block;
  height: 72px;
  width: auto;
  opacity: 0.4;
  transition: opacity 0.15s;
}
.style-artistic .thumb-btn:hover img { opacity: 0.7; }
.style-artistic .thumb-btn.active { border-color: var(--accent); }
.style-artistic .thumb-btn.active img { opacity: 1; }

/* Non-image media (video / unsupported) shown beneath the viewer. */
.style-artistic .viewer-extras {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
  margin-top: 1.5rem;
}
.style-artistic .viewer-extras .frame { width: 100%; border: none; }

/* On phone-sized viewports the artistic viewer is cramped (big image + a small
   filmstrip + arrow buttons), so the server also emits a classic-style column
   inside `.mobile-classic-fallback` (the wrapper carries the `style-classic`
   class so the existing classic rules apply to its descendants). Default:
   hidden everywhere; on small screens, show it and hide the artistic viewer +
   viewer-extras. Desktop artistic is unchanged. */
.mobile-classic-fallback { display: none; }
@media (max-width: 600px) {
  .style-artistic .viewer { display: none; }
  .style-artistic .viewer-extras { display: none; }
  .style-artistic .mobile-classic-fallback { display: block; }
}

/* -- Admin gallery-settings panel (on the browse page) --------- */

.gallery-settings {
  background: var(--surface);
  border-radius: 6px;
  padding: 0.6rem 1rem;
  margin-bottom: 1.5rem;
  font-size: 0.9rem;
}
.gallery-settings summary { cursor: pointer; color: var(--text-dim); }
/* Clear separation between the panel's sections (style / share / thumbnails /
   title+description). */
.gallery-settings hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 1.4rem 0 1rem;
}

/* Inline SVG icons inherit the surrounding text colour and size. */
.icon {
  width: 1.05em;
  height: 1.05em;
  vertical-align: -0.18em;
  stroke: currentColor;
  flex-shrink: 0;
}
.gallery-settings form { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: center; margin-top: 0.7rem; }
.gallery-settings select { font: inherit; padding: 0.35rem; border: 1px solid var(--border); border-radius: 4px; }
.gallery-settings .hint { color: var(--text-dim); font-size: 0.8rem; margin-top: 0.5rem; }

/* Per-gallery title/description editor (admin panel). Scoped under
   .gallery-settings so it outweighs the generic ".gallery-settings form" row
   layout above; the fields fill the panel width for roomy titles/descriptions. */
.gallery-settings .meta-form { display: flex; flex-direction: column; gap: 0.6rem; align-items: stretch; margin-top: 0.7rem; }
.gallery-settings .meta-form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.8rem; color: var(--text-dim); }
.gallery-settings .meta-form input,
.gallery-settings .meta-form textarea {
  width: 100%;
  font: inherit;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg);
  color: var(--text);
}
.gallery-settings .meta-form textarea { resize: vertical; min-height: 4rem; }
.gallery-settings .meta-form button { align-self: flex-start; }

/* -- Login page ------------------------------------------------ */

.login-page {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}
.login {
  width: 100%;
  max-width: 320px;
  padding: 2rem;
  text-align: center;
}
.login h1 { font-family: ui-monospace, 'Courier New', monospace; margin-bottom: 1.5rem; }
.login form { display: flex; flex-direction: column; gap: 1rem; text-align: left; }
.login label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.85rem; color: var(--text-dim); }
.login input {
  font: inherit;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 4px;
}
.login button { padding: 0.6rem; border-color: var(--accent); }
.login .error {
  background: var(--danger-bg);
  color: var(--danger);
  padding: 0.6rem;
  border-radius: 4px;
  margin-bottom: 1rem;
  font-size: 0.9rem;
}

/* One-time login-link landing: a big, friendly, tap-anywhere button. Aimed at
   assist users, so it stays generous regardless of the assist body class. */
.login-link-page .welcome { font-size: 1.4rem; font-weight: 600; margin-bottom: 0.4rem; }
.login-link-page .prompt { color: var(--text-dim); margin-bottom: 1.5rem; }
.login-link-page .big-login {
  width: 100%;
  padding: 1.1rem;
  font-size: 1.4rem;
  border-color: var(--accent);
}

/* Login PIN keypad: a 3-column numeric pad with big, tappable keys. */
.pin-page .pin-label { display: block; color: var(--text-dim); margin-bottom: 0.5rem; }
.pin-page .pin-input {
  width: 100%;
  text-align: center;
  font-size: 1.6rem;
  letter-spacing: 0.5rem;
  margin-bottom: 1rem;
}
.keypad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.6rem;
}
.keypad .key {
  font-size: 1.5rem;
  padding: 1rem 0;
  border: 1px solid var(--border);
  border-radius: 10px;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}
.keypad .key:hover { border-color: var(--accent); }
.keypad .key:active { background: var(--surface); }
.keypad .key-ok { color: var(--accent); border-color: var(--accent); }
.keypad .key svg { width: 1.5rem; height: 1.5rem; }
/* Assist mode: even larger keys for shaky fingers. */
.assist .keypad .key { font-size: 1.9rem; padding: 1.4rem 0; }
.assist .keypad .key svg { width: 1.9rem; height: 1.9rem; }

/* -- Click-to-copy link fields (share + login links) ---------------------- */

.copy-url {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  max-width: 100%;
}
.copy-url .copy-text {
  font-family: ui-monospace, 'Courier New', monospace;
  font-size: 0.85rem;
  word-break: break-all;
}
.copy-btn {
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.4rem;
  line-height: 0;
  color: var(--text-dim);
}
.copy-btn:hover { color: var(--accent); border-color: var(--accent); }
.copy-btn svg { width: 1.05rem; height: 1.05rem; }
.copy-btn .ic-check { display: none; }
/* Briefly swap the clipboard icon for a check after a successful copy. */
.copy-btn.copied { color: var(--accent); border-color: var(--accent); }
.copy-btn.copied .ic-copy { display: none; }
.copy-btn.copied .ic-check { display: inline; }

.userbox .navlink { font-size: 0.85rem; }

/* -- Admin pages ----------------------------------------------- */

.admin {
  max-width: 760px;
  margin: 0 auto;
  padding: 1.5rem 1rem 5.5rem;
}
.admin h1 { margin-bottom: 1.25rem; }
.admin-nav { margin: -0.75rem 0 1.25rem; }
.admin-nav a { margin-right: 0.75rem; }

/* Plain, readable admin tables (activity log, share links). */
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.admin-table th,
.admin-table td {
  text-align: left;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}
.admin-table th { color: var(--text-dim); font-weight: 600; }
.admin-table th.num,
.admin-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
.admin-table .nowrap { white-space: nowrap; }
.admin-table .dim { color: var(--text-dim); }
.admin-table td a { word-break: break-all; }

/* Activity bar chart: one stacked column per day; each user is a coloured
   segment. The plot area is a fixed-height row of equal-width columns; segment
   height is set as a percent of that area where 100% maps to the chart's max
   single-day total (passed in via the --max custom property if needed for
   tooltips). The y-axis sits to the left with the max value at the top and 0
   at the bottom; the x-axis below shows sparse date labels. The legend lists
   each user with their swatch and total. */
.activity-graph .bar-chart {
  display: grid;
  grid-template-columns: 2.2rem 1fr;
  gap: 0.5rem;
  align-items: stretch;
  margin: 0.6rem 0 0.3rem;
}
.activity-graph .bc-yaxis {
  position: relative;
  font-size: 0.7rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  border-right: 1px solid var(--line);
}
.activity-graph .bc-tick { position: absolute; right: 0.4rem; line-height: 1; }
.activity-graph .bc-tick.top { top: 0; }
.activity-graph .bc-tick.bottom { bottom: 0; }
.activity-graph .bc-plot {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 160px;
  border-bottom: 1px solid var(--line);
}
.activity-graph .bc-col {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
}
/* The stack fills its column so each segment's `height: X%` (set inline) is
   a percent of a defined parent - leaving the part above the topmost segment
   empty. Without an explicit height here the percentages would resolve to 0
   and the bars would disappear. */
.activity-graph .bc-stack {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column-reverse; /* stack from the bottom up */
}
.activity-graph .bc-seg {
  width: 100%;
  min-height: 1px;
  flex-shrink: 0; /* keep each segment at its inline height (no flex-squish) */
}
/* Round only the visible top corner of each column - the bottom sits on the
   chart's baseline. column-reverse means the LAST child is the topmost segment. */
.activity-graph .bc-seg:last-child {
  border-top-left-radius: 2px;
  border-top-right-radius: 2px;
}
.activity-graph .bc-axis {
  display: flex;
  gap: 2px;
  padding-left: 2.7rem; /* match the y-axis column + grid gap above */
  margin-bottom: 0.6rem;
  font-size: 0.65rem;
  color: var(--text-dim);
}
.activity-graph .bc-axis > span {
  flex: 1 1 0;
  min-width: 0;
  text-align: center;
  white-space: nowrap;
}
.activity-graph .bc-legend {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  margin: 0.4rem 0 0;
  padding: 0;
  font-size: 0.85rem;
}
.activity-graph .bc-legend li { display: flex; align-items: center; gap: 0.35rem; }
.activity-graph .bc-swatch {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 2px;
  display: inline-block;
}

.card {
  border: 2px solid var(--border);
  border-radius: 8px;
  padding: 1rem 1.2rem;
  margin-bottom: 1.25rem;
}
.card h2 { font-size: 1.1rem; margin-bottom: 0.75rem; }

.userform {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  align-items: center;
}
.userform input[type="text"],
.userform input[name="username"],
.userform input[name="password"],
.userform input[name="preferred_style"] {
  font: inherit;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 4px;
}
.check { display: inline-flex; align-items: center; gap: 0.3rem; color: var(--text-dim); }

.badge {
  display: inline-block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  background: var(--surface);
  color: var(--badge-text);
  border-radius: 999px;
  padding: 0.1rem 0.55rem;
  margin-left: 0.4rem;
  vertical-align: middle;
}

.created {
  font-size: 0.75rem;
  font-weight: normal;
  color: var(--text-dim);
  margin-left: 0.4rem;
}

.grants { margin: 0.5rem 0 1rem; }
.grants ul { list-style: none; margin: 0.4rem 0; }
.grants li { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0; }
.grants code { background: var(--surface); padding: 0.1rem 0.4rem; border-radius: 4px; }

.inline { display: inline-flex; gap: 0.4rem; align-items: center; }
.inline input {
  font: inherit;
  padding: 0.4rem 0.55rem;
  border: 1px solid var(--border);
  border-radius: 4px;
}
.useractions { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
button.danger { border-color: var(--danger); color: var(--danger); }
button.danger:hover { background: var(--danger-bg); }
.self-note { color: var(--text-dim); font-size: 0.85rem; }

/* -- Breadcrumb home icon ----------------------------------------------- */
.crumb-home { display: inline-flex; align-items: center; }
.crumb-home .icon { width: 1.2em; height: 1.2em; vertical-align: baseline; }

/* Hidden hearts layer (toggled from the bottom bar). */
.hide-hearts .heart { display: none; }

/* -- Bottom action bar -------------------------------------------------- */
/* Built and wired by static/theme.js: home / top / hearts / dark mode. Floats
   at bottom centre, below the lightbox so it never overlaps an open photo. */

.bottombar {
  position: fixed;
  left: 50%;
  bottom: 0.8rem;
  transform: translateX(-50%);
  z-index: 50;
  display: flex;
  gap: 0.25rem;
  padding: 0.3rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 999px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
.bb-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}
.bb-btn:hover { color: var(--accent); background: var(--surface); }
.bb-btn svg { width: 1.4rem; height: 1.4rem; display: block; flex-shrink: 0; }
/* Hearts button when the layer is hidden: dimmed/struck to show the off state. */
.bb-btn.off { opacity: 0.4; }

.assist .bb-btn { width: 3.2rem; height: 3.2rem; }
.assist .bb-btn svg { width: 1.8rem; height: 1.8rem; }

/* -- Assist mode: larger text and tap targets ------------------ */

.assist { font-size: 1.25rem; }
.assist .dirlist a { padding: 0.7rem 0; font-size: 1.4rem; }
.assist button { padding: 0.7rem 1.2rem; font-size: 1.1rem; }
.assist .crumbs { font-size: 1.1rem; }
.assist .login input { padding: 0.9rem; }
/* Assist mode drops the per-photo EXIF captions entirely (the gallery date
   range at the top stays). */
.assist .exif, .assist .viewer-caption { display: none; }
/* Artistic viewer: bigger arrows and filmstrip thumbnails for easier tapping. */
.assist.style-artistic .nav-btn { font-size: 2.8rem; width: 3.5rem; opacity: 0.3; }
.assist.style-artistic .thumb-btn img { height: 96px; }
