/* =========================================================
   Pawlery shared styles
   Used by every template via base.html so the look stays
   consistent and CSS doesn't get copy-pasted 8 times.

   Theming: :root holds the Classic theme's values -- Classic is the
   default look and renders when no [data-theme] attribute is set on
   <html>. [data-theme="glass"] ("Liquid Glass") and
   [data-theme="macchiato"] further down override tokens AND, for the
   surfaces whose recipe genuinely differs (not just a color swap --
   gradients, blur amounts, highlight borders), the component rule
   itself. Classic and Glass are transcribed to match two reference
   stylesheets pixel-for-pixel (css-classic.css / css-liquid.css); do
   not "simplify" their blur/gradient values back to a shared token
   without re-checking against those files -- that's what caused
   Classic to lose its header/toolbar blur(8px) last time. Layout,
   spacing and sizing are NOT themed -- only color/background/border/
   blur/shadow vary. See base.html for the theme picker (in the
   Settings popup) + the FOUC-avoiding blocking init script, and
   gallery.js for the experimental-gallery toggle.
   ========================================================= */

:root {
  color-scheme: dark;

  /* ---- Classic (default) ---- */
  --bg: #0b0b0c;
  --bg-raised: #1a1a1d;
  --bg-input: #2a2a2d;
  --bg-hover: #2a2a2d;
  --text: #eaeaea;
  --text-dim: rgba(234,234,234,.7);
  --text-dimmer: rgba(234,234,234,.5);
  --accent: #00ff76;
  --accent-hover: #00cd60;
  --accent-text: #06210f;
  --danger: #ff4b4b;
  --danger-hover: #ff6363;
  --info: #1726f7;
  --info-hover: #3441ff;
  --btn-colored-text: #fff;
  --radius: 12px;
  --radius-sm: 8px;
  --header-h: 56px;

  --nsfw-ambient: rgba(255, 44, 157, 0.4);
  --nsfw-surface-bg: rgba(255, 20, 147, 0.35);
}

/* ---------- Liquid Glass theme (matches css-liquid.css) ---------- */
[data-theme="glass"] {
  --bg: #0a0a11;
  --bg-raised: rgba(255,255,255,0.06);
  --bg-input: rgba(255,255,255,0.07);
  --bg-hover: rgba(255,255,255,0.13);
  --text: #f3f3f6;
  --text-dim: rgba(243,243,246,.72);
  --text-dimmer: rgba(243,243,246,.5);
  --accent: #00ff9d;
  --accent-hover: #33ffb3;
  --accent-text: #06210f;
  --danger: #ff5470;
  --danger-hover: #ff7086;
  --info: #4f6bff;
  --info-hover: #6f88ff;
  --btn-colored-text: #fff;
  --radius: 22px;
  --radius-sm: 14px;
  --header-h: 60px;

  /* Glass recipe, reused across every raised surface below. */
  --glass-bg: linear-gradient(150deg, rgba(255,255,255,0.16), rgba(255,255,255,0.03) 60%);
  --glass-bg-strong: linear-gradient(150deg, rgba(255,255,255,0.22), rgba(255,255,255,0.05) 60%);
  --glass-border: rgba(255,255,255,0.18);
  --glass-shadow: 0 8px 30px rgba(0,0,0,0.38);
  --glass-highlight: inset 0 1px 0 rgba(255,255,255,0.28);
  --glass-blur: blur(22px) saturate(165%);

  --nsfw-ambient:
    radial-gradient(circle at 15% 20%, rgba(255,44,157,0.28), transparent 45%),
    radial-gradient(circle at 85% 20%, rgba(255,90,180,0.22), transparent 45%),
    radial-gradient(circle at 50% 90%, rgba(255,20,147,0.20), transparent 50%),
    #150a10;
  --nsfw-surface-bg: linear-gradient(150deg, rgba(255,60,180,0.28), rgba(255,20,147,0.10) 60%);
  --nsfw-surface-border: rgba(255,150,210,0.3);
}

/* ---------- Macchiato theme (Catppuccin Macchiato palette) ----------
   Flat colors, zero backdrop-filter anywhere -- deliberately the
   cheapest theme to render (no blur compositing), with a slightly
   boxier corner radius for an old-Macintosh-ish feel. No reference
   file for this one -- it's an original addition, not a transcription. */
[data-theme="macchiato"] {
  --bg: #24273a;
  --bg-raised: #363a4f;
  --bg-input: #363a4f;
  --bg-hover: #494d64;
  --text: #cad3f5;
  --text-dim: #b8c0e0;
  --text-dimmer: #a5adcb;
  --accent: #a6da95;
  --accent-hover: #8bc47d;
  --accent-text: #181926;
  --danger: #ed8796;
  --danger-hover: #f2a6b1;
  --info: #8aadf4;
  --info-hover: #a8c4f7;
  --btn-colored-text: #181926;
  --radius: 8px;
  --radius-sm: 6px;
  --header-h: 56px;

  --surface-bg: #1e2030;
  --nav-dropdown-bg: #1e2030;
  --nav-dropdown-border: #363a4f;
  --toolbar-border: #363a4f;
  --btn-bg: #494d64;
  --btn-hover-bg: #5b6078;
  --input-border: #494d64;
  --chip-border: #494d64;
  --chip-hover-border: #5b6078;
  --table-border: #363a4f;

  --nsfw-ambient: #2e2438;
  --nsfw-surface-bg: #4a2e42;
}

/* ---------- Pastel theme ----------
   A light theme with warm, coffee-shop-inspired colors (cream/latte
   surfaces, espresso-brown text, caramel accent) -- same flat, zero-
   backdrop-filter recipe as Macchiato (see the `[data-theme="macchiato"],
   [data-theme="pastel"]` combined selectors throughout this file), just
   with light-theme token values instead of dark ones. */
[data-theme="pastel"] {
  color-scheme: light;

  --bg: #f6ecdf;
  --bg-raised: #fffaf2;
  --bg-input: #fffdf9;
  --bg-hover: #ecdcc4;
  --text: #4a3324;
  --text-dim: rgba(74,51,36,.72);
  --text-dimmer: rgba(74,51,36,.5);
  --accent: #b6805a;
  --accent-hover: #a06c46;
  --accent-text: #2b1b10;
  --danger: #c1584a;
  --danger-hover: #a8493c;
  --info: #5f7d8c;
  --info-hover: #4d6875;
  --btn-colored-text: #fff;
  --radius: 14px;
  --radius-sm: 10px;
  --header-h: 56px;

  --surface-bg: #efe0c9;
  --nav-dropdown-bg: #efe0c9;
  --nav-dropdown-border: #ddc9a8;
  --toolbar-border: #ddc9a8;
  --btn-bg: #e8d8bd;
  --btn-hover-bg: #ddc9a8;
  --input-border: #d6c1a0;
  --chip-border: #d6c1a0;
  --chip-hover-border: #c2a97e;
  --table-border: #e2d3ba;

  --nsfw-ambient: #f5dcd2;
  --nsfw-surface-bg: #f0c2b0;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  min-height: 100%;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  color: var(--text);
}
/* Classic paints a flat body background directly (matching css-classic.css
   exactly). Glass needs a fixed, non-scrolling wash of blurred color blobs
   behind everything instead (see [data-theme="glass"] below) -- rather
   than branch the mechanism per theme, body::before always exists (cheap/
   invisible when transparent) and Classic/Macchiato simply never set
   --ambient, so body's own flat background shows through untouched. */
body {
  position: relative;
  background: var(--bg);
}
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: var(--ambient, transparent);
  pointer-events: none;
}
[data-theme="glass"] body {
  background: var(--bg);
}
[data-theme="glass"] body::before {
  --ambient:
    radial-gradient(circle at 12% 18%, rgba(0,255,157,0.20), transparent 42%),
    radial-gradient(circle at 88% 12%, rgba(79,107,255,0.20), transparent 46%),
    radial-gradient(circle at 78% 82%, rgba(255,44,157,0.17), transparent 46%),
    radial-gradient(circle at 18% 88%, rgba(255,200,80,0.12), transparent 42%);
  background: var(--ambient);
}

/* Applied to <body> while the lightbox is open (see lockBodyScroll() in
   gallery.js). overflow:hidden alone doesn't fully stop touch-scroll
   "rubber-banding" on iOS Safari, so gallery.js also pins the body with
   position:fixed -- this class covers the rest (desktop, and browsers
   where the fixed-position trick isn't needed). */
body.lightbox-open {
  overflow: hidden;
  width: 100%;
}

a { color: inherit; }

/* NSFW dim mode. Classic tints <body> itself directly (flat hot pink,
   exactly matching css-classic.css); Glass tints the ::before ambient
   layer instead with its own pink gradient wash (matching css-liquid.css)
   -- see the [data-theme="glass"] override below. */
body.nsfw-mode { background-color: var(--nsfw-ambient); }
body.nsfw-mode .site-header,
body.nsfw-mode .toolbar,
body.nsfw-mode .card,
body.nsfw-mode .panel {
  /* No bare `form` selector here on purpose -- gallery.js's Edit/Delete
     buttons (and a couple of admin Delete buttons) are each their own
     unclassed <form>, and matching them here gave them an unintended pink
     halo in NSFW mode. Every real form-panel already carries class="panel". */
  background-color: var(--nsfw-surface-bg) !important;
}
[data-theme="glass"] body.nsfw-mode {
  background-color: transparent;
}
[data-theme="glass"] body.nsfw-mode::before {
  background: var(--nsfw-ambient);
}
[data-theme="glass"] body.nsfw-mode .site-header,
[data-theme="glass"] body.nsfw-mode .toolbar,
[data-theme="glass"] body.nsfw-mode .card,
[data-theme="glass"] body.nsfw-mode .panel {
  background: var(--nsfw-surface-bg) !important;
  border-color: var(--nsfw-surface-border) !important;
}

/* ---------- Header / nav ---------- */
.site-header {
  position: sticky;
  top: 0;
  height: var(--header-h);
  background: rgba(11,11,12,0.85);
  backdrop-filter: blur(8px);
  padding: 0 16px;
  z-index: 20;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
}
[data-theme="glass"] .site-header {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
  box-shadow: var(--glass-highlight);
}
[data-theme="macchiato"] .site-header,
[data-theme="pastel"] .site-header {
  background: var(--surface-bg);
  backdrop-filter: none;
}
.site-title {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}
.site-header h1 {
  margin: 0;
  font-size: 17px;
  letter-spacing: .3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.site-header h1 a { text-decoration: none; }
.brand-icon {
  width: 16px;
  height: 16px;
  vertical-align: -2px;
  margin-right: 2px;
  filter: invert(1);
}
[data-theme="pastel"] .brand-icon { filter: none; }

/* Small inline status icon (e.g. "✓ Yes" / "✓ Active" table cells in
   admin_tag_managers.html and admin-tags.js), replacing an emoji suffix. */
.inline-status-icon {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  filter: invert(1);
}
[data-theme="pastel"] .inline-status-icon { filter: none; }

/* Random-image button, next to the title -- deliberately outside
   .site-nav so it's reachable on mobile without opening the hamburger
   menu (see base.html). */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--text);
  cursor: pointer;
}
.icon-btn:hover { background: var(--bg-hover); }
.icon-btn .icon {
  width: 20px;
  height: 20px;
  display: block;
  /* star.svg is a plain black source file -- invert to white so it's
     clearly visible against the dark header, matching the existing
     img+filter icon convention used throughout (see .btn-icon .icon). */
  filter: invert(1);
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  justify-content: flex-end;
}
.site-nav a, .site-nav button.link-btn {
  margin-left: 4px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--text);
  font-size: 13px;
  background: transparent;
  border: none;
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
}
.site-nav a:hover, .site-nav button.link-btn:hover { background: var(--bg-hover); }

/* ---------- Settings popup ----------
   A single "⚙️ Settings" nav link opens this small anchored panel (theme
   picker + Experimental toggle) instead of those controls living directly
   in the fixed-height header, which was overflowing/breaking its layout. */
.settings-wrap {
  position: relative;
}
.settings-popup {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 25;
  min-width: 220px;
  padding: 14px;
  border-radius: var(--radius-sm);
  background: var(--bg-raised);
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
/* [hidden] and .settings-popup are equal specificity (0,1,0 each), and an
   author-stylesheet class rule beats a same-specificity UA-stylesheet
   attribute rule -- so the plain `display: flex` above was silently
   winning over the browser's built-in `[hidden] { display: none }` and
   the popup rendered permanently open regardless of the `hidden`
   attribute gallery.js/base.html toggle. This selector is one class more
   specific, so it correctly wins instead. */
.settings-popup[hidden] {
  display: none;
}
[data-theme="glass"] .settings-popup {
  /* A popup floats over arbitrary, often colorful gallery content --
     needs more opacity than a card/panel to stay reliably readable, so
     this layers the usual glass sheen over a mostly-opaque dark base
     instead of relying on --glass-bg-strong (~22% white) alone. */
  background-image: var(--glass-bg-strong);
  background-color: rgba(10,10,17,0.82);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), var(--glass-highlight);
}
[data-theme="macchiato"] .settings-popup,
[data-theme="pastel"] .settings-popup {
  background: var(--surface-bg);
  border: 1px solid var(--nav-dropdown-border);
}
.settings-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.settings-row label {
  margin: 0;
  font-size: 12px;
  color: var(--text-dim);
}
.theme-select {
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid #333;
  background: var(--bg-input);
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
  cursor: pointer;
}
[data-theme="glass"] .theme-select { border-color: var(--glass-border); }
[data-theme="macchiato"] .theme-select,
[data-theme="pastel"] .theme-select { border-color: var(--input-border); }
.settings-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-dim);
  cursor: pointer;
}
.settings-check input[type="checkbox"] {
  width: auto;
  margin: 0;
  cursor: pointer;
}

/* Hamburger for small screens */
.nav-toggle {
  display: none;
  background: transparent;
  border: none;
  color: var(--text);
  font-size: 22px;
  cursor: pointer;
  padding: 4px 8px;
}

@media (max-width: 720px) {
  .nav-toggle { display: block; }
  .toolbar { flex-direction: column; align-items: stretch; }
  .site-nav {
    display: none;
    position: absolute;
    top: var(--header-h);
    right: 0;
    left: 0;
    background: rgba(19,19,22,0.9);
    backdrop-filter: blur(8px);
    flex-direction: column;
    align-items: stretch;
    padding: 8px;
    border-bottom: 1px solid #2a2a2d;
  }
  [data-theme="glass"] .site-nav {
    /* Same fix as .settings-popup below: --glass-bg-strong alone is only
       ~22% white, which reads as "no blur" against the gallery grid
       showing straight through it -- layer that sheen over a mostly-
       opaque dark base instead so the nav links are actually legible. */
    background-image: var(--glass-bg-strong);
    background-color: rgba(10,10,17,0.82);
    backdrop-filter: var(--glass-blur);
    -webkit-backdrop-filter: var(--glass-blur);
    border-bottom: 1px solid var(--glass-border);
    border-radius: 0 0 var(--radius-sm) var(--radius-sm);
  }
  [data-theme="macchiato"] .site-nav,
  [data-theme="pastel"] .site-nav {
    background: var(--nav-dropdown-bg);
    backdrop-filter: none;
    border-bottom: 1px solid var(--nav-dropdown-border);
  }
  .site-nav.open { display: flex; }
  .site-nav a, .site-nav button.link-btn {
    margin: 2px 0;
    padding: 12px 10px;
    text-align: left;
    font-size: 15px;
  }
  .settings-wrap { width: 100%; }
  /* The plain nav <a> links are direct children of the column-flex
     .site-nav above, so align-items:stretch (also set there) makes them
     fill its width automatically. #settings-btn is nested one level
     deeper (inside .settings-wrap, which itself stretches), so it never
     got that same free stretch and stayed sized to its own text instead
     of matching its siblings -- set it explicitly here instead. */
  .settings-wrap #settings-btn {
    display: block;
    width: 100%;
  }
  .settings-popup {
    position: static;
    margin-top: 4px;
    width: 100%;
    box-shadow: none;
  }
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-block;
  padding: 10px 16px;
  border: none;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-weight: 600;
  font-family: inherit;
  font-size: 14px;
  text-decoration: none;
  text-align: center;
  color: #fff;
  background: #343434;
}
.btn:hover { background: #4a4a4a; }
.btn-primary { background: var(--accent); color: var(--accent-text); }
.btn-primary:hover { background: var(--accent-hover); }
.btn-info { background: var(--info); color: var(--btn-colored-text); }
.btn-info:hover { background: var(--info-hover); }
.btn-danger { background: var(--danger); color: var(--btn-colored-text); }
.btn-danger:hover { background: var(--danger-hover); }
.btn-block { display: block; width: 100%; }
.btn:disabled { opacity: .5; cursor: not-allowed; }

[data-theme="glass"] .btn {
  border: 1px solid var(--glass-border);
  border-radius: 999px;
  background: var(--glass-bg);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  box-shadow: var(--glass-highlight);
  transition: background .15s ease, transform .1s ease, box-shadow .15s ease;
}
[data-theme="glass"] .btn:hover { background: var(--glass-bg-strong); }
[data-theme="glass"] .btn:active { transform: scale(.97); }
[data-theme="glass"] .btn-primary {
  background: linear-gradient(150deg, rgba(0,255,157,0.85), rgba(0,205,96,0.75));
  border-color: rgba(0,255,157,0.5);
}
[data-theme="glass"] .btn-primary:hover { background: linear-gradient(150deg, rgba(51,255,179,0.9), rgba(0,255,157,0.8)); }
[data-theme="glass"] .btn-info {
  background: linear-gradient(150deg, rgba(79,107,255,0.85), rgba(23,38,247,0.75));
  border-color: rgba(111,136,255,0.5);
}
[data-theme="glass"] .btn-info:hover { background: linear-gradient(150deg, rgba(111,136,255,0.9), rgba(79,107,255,0.8)); }
[data-theme="glass"] .btn-danger {
  background: linear-gradient(150deg, rgba(255,84,112,0.85), rgba(255,75,75,0.75));
  border-color: rgba(255,112,134,0.5);
}
[data-theme="glass"] .btn-danger:hover { background: linear-gradient(150deg, rgba(255,112,134,0.9), rgba(255,84,112,0.8)); }

[data-theme="macchiato"] .btn,
[data-theme="pastel"] .btn { background: var(--btn-bg); color: var(--text); }
[data-theme="macchiato"] .btn:hover,
[data-theme="pastel"] .btn:hover { background: var(--btn-hover-bg); }
/* The plain `.btn` override just above is 2 selectors specific
   ([data-theme=...] + .btn), which beats .btn-primary/.btn-info/.btn-danger
   (1 class each) at equal specificity regardless of source order -- so
   without these, EVERY colored button (Download, Save, Edit, Delete, ...)
   silently fell back to the flat neutral --btn-bg color on Macchiato,
   losing its accent/info/danger color entirely. These need to be at least
   as specific and come after it to win. */
[data-theme="macchiato"] .btn-primary,
[data-theme="pastel"] .btn-primary { background: var(--accent); color: var(--accent-text); }
[data-theme="macchiato"] .btn-primary:hover,
[data-theme="pastel"] .btn-primary:hover { background: var(--accent-hover); }
[data-theme="macchiato"] .btn-info,
[data-theme="pastel"] .btn-info { background: var(--info); color: var(--btn-colored-text); }
[data-theme="macchiato"] .btn-info:hover,
[data-theme="pastel"] .btn-info:hover { background: var(--info-hover); }
[data-theme="macchiato"] .btn-danger,
[data-theme="pastel"] .btn-danger { background: var(--danger); color: var(--btn-colored-text); }
[data-theme="macchiato"] .btn-danger:hover,
[data-theme="pastel"] .btn-danger:hover { background: var(--danger-hover); }

/* ---------- Forms ---------- */
.panel {
  background: var(--bg-raised);
  padding: 16px;
  border-radius: var(--radius);
  margin: 16px;
}
[data-theme="glass"] .panel {
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow), var(--glass-highlight);
  padding: 18px;
}
label { display: block; margin-top: 10px; font-size: 13px; color: var(--text-dim); }
input, textarea, select {
  width: 100%;
  margin: 6px 0 0;
  padding: 10px;
  border-radius: var(--radius-sm);
  border: 1px solid #333;
  background: var(--bg-input);
  color: #fff;
  font-family: inherit;
  /* 16px prevents iOS Safari from zooming the whole viewport in when an
     input is focused (it does this for any font-size under 16px) -- that
     zoom is what made the mobile search bar look like a giant rectangle. */
  font-size: 16px;
}
[data-theme="glass"] input, [data-theme="glass"] textarea, [data-theme="glass"] select {
  padding: 10px 14px;
  border-color: var(--glass-border);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transition: border-color .15s ease, background .15s ease;
}
[data-theme="glass"] input:focus, [data-theme="glass"] textarea:focus, [data-theme="glass"] select:focus {
  outline: none;
  border-color: rgba(0,255,157,0.6);
  background: rgba(255,255,255,0.1);
}
[data-theme="macchiato"] input, [data-theme="macchiato"] textarea, [data-theme="macchiato"] select,
[data-theme="pastel"] input, [data-theme="pastel"] textarea, [data-theme="pastel"] select {
  border-color: var(--input-border);
  color: var(--text);
}
textarea { resize: vertical; min-height: 70px; }
.field { margin-bottom: 4px; }
.help-text { font-size: 12px; color: var(--text-dimmer); margin-top: 4px; }

.flash { padding: 10px 14px; border-radius: var(--radius-sm); margin: 12px 16px; font-size: 14px; }
.flash-error { background: rgba(255,75,75,0.15); border: 1px solid var(--danger); }
.flash-success { background: rgba(0,255,118,0.12); border: 1px solid var(--accent); }
/* Non-blocking heads-up (e.g. "did you mean an existing tag?" -- see
   find_similar_tag_names() in helpers.py) -- distinct from error (red,
   something failed) and success (green, it worked): amber, it worked but
   here's something worth double-checking. */
.flash-warning { background: rgba(255,180,0,0.14); border: 1px solid #d99a00; }
[data-theme="glass"] .flash {
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
}
[data-theme="glass"] .flash-error { background: rgba(255,84,112,0.16); border-color: rgba(255,84,112,0.45); }
[data-theme="glass"] .flash-success { background: rgba(0,255,157,0.14); border-color: rgba(0,255,157,0.45); }
[data-theme="glass"] .flash-warning { background: rgba(255,190,40,0.16); border-color: rgba(255,190,40,0.45); }

/* ---------- Gallery toolbar (rating filters + search) ---------- */
/* Sticky just below the header, and slid out of view on scroll-down /
   back into view on scroll-up (see the toolbar-visibility logic in
   gallery.js) so filters/search stay reachable without scrolling back to
   the top, while still getting out of the way while browsing. Using
   position:sticky + transform (rather than toggling display) means it
   never changes the document's layout/height, so it can't disturb the
   infinite-scroll sentinel's position. */
.toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  /* 2px extra top padding compensates for pulling the box 2px further up
     below (top: header-h - 2px) -- the box grows by exactly the amount it
     shifts, so its bottom edge (and the grid's start position) lands in
     the same place as a plain 12px box would. */
  padding: 14px 12px 12px;
  position: sticky;
  /* Same translucent-blur treatment as .site-header, so the two read as
     one continuous frosted surface rather than a blurred bar sitting on
     a flat one. Sticking 2px higher than the header's own bottom edge
     (rather than exactly flush with it) makes the two boxes overlap by
     those 2px instead of merely touching -- two independently-blurred
     backdrop-filter elements that only *touch* can render a faint seam
     line along the shared edge (each blurs its own box in isolation, so
     the boundary pixel row doesn't blend). Overlapping means the header
     (the higher z-index of the two) simply paints over that shared strip
     with its own blur, so there's nothing left to seam against. */
  top: calc(var(--header-h) - 2px);
  z-index: 15;
  background: rgba(11,11,12,0.85);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid #1f1f22;
  transform: translateY(0);
  opacity: 1;
  transition: transform .22s ease, opacity .22s ease;
  will-change: transform, opacity;
}
[data-theme="glass"] .toolbar {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-bottom: 1px solid var(--glass-border);
}
[data-theme="macchiato"] .toolbar,
[data-theme="pastel"] .toolbar {
  background: var(--surface-bg);
  backdrop-filter: none;
  border-bottom: 1px solid var(--toolbar-border);
}
.toolbar.toolbar-hidden {
  transform: translateY(-100%);
  /* Slid up, the toolbar tucks behind the header -- without also fading
     it out, its now-translucent/blurred content would still show through
     the header's own blur right at that boundary. Fully transparent by
     the time it's out of view avoids that bleed-through. */
  opacity: 0;
}
.rating-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid #333;
  background: var(--bg-raised);
  color: var(--text-dim);
  padding: 7px 14px;
  border-radius: 999px;
  font-size: 13px;
  cursor: pointer;
  text-decoration: none;
  font-family: inherit;
  white-space: nowrap;
}
.chip:hover { border-color: #555; color: var(--text); }
.chip.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); font-weight: 600; }
[data-theme="glass"] .chip {
  border-color: var(--glass-border);
  background: var(--glass-bg);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  padding: 8px 15px;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
[data-theme="glass"] .chip:hover { border-color: rgba(255,255,255,0.35); }
[data-theme="glass"] .chip.active {
  background: linear-gradient(150deg, rgba(0,255,157,0.9), rgba(0,205,96,0.75));
  border-color: rgba(0,255,157,0.6);
}
[data-theme="macchiato"] .chip,
[data-theme="pastel"] .chip { border-color: var(--chip-border); }
[data-theme="macchiato"] .chip:hover,
[data-theme="pastel"] .chip:hover { border-color: var(--chip-hover-border); }
.chip-locked { opacity: .6; cursor: pointer; }
.chip-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  display: block;
}
/* The rating icons (checkmark/pepper/fire/lock.svg, via <img>) are plain black
   source files -- invert so they read against the dark, inactive chip
   background, matching the existing img+filter icon convention used
   elsewhere (see .btn-icon .icon). On the *active* chip the background
   flips to bright accent green with near-black text (see .chip.active
   above), so the icon should stay its native black there instead. */
img.chip-icon {
  filter: invert(1);
}
.chip.active img.chip-icon {
  filter: none;
}
/* Positions search.svg inside the input itself -- .search-wrap is the
   actual flex/sizing participant in each parent layout (.toolbar, or a
   plain .panel flex row), taking over exactly the sizing .search-input
   used to carry alone, so wrapping it here doesn't change any existing
   layout. */
.search-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
  width: auto;
  min-width: 200px;
  max-width: 320px;
  margin: 0;
  flex: 1 1 200px;
  height: 40px;
}
[data-theme="glass"] .search-wrap { border-radius: 999px; }
.search-input {
  width: 100%;
  height: 100%;
  margin: 0;
  padding-left: 34px;
  box-sizing: border-box;
  -webkit-appearance: none;
  appearance: none;
}
[data-theme="glass"] .search-input { border-radius: 999px; }
/* Remove the native iOS "clear" (x) button so it doesn't add extra
   built-in padding/sizing on top of our own. */
.search-input::-webkit-search-cancel-button { -webkit-appearance: none; }
.search-icon {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  width: 15px;
  height: 15px;
  flex: 0 0 auto;
  pointer-events: none;
  opacity: .55;
  /* search.svg is a plain black source file -- invert to white so it
     reads against the dark input background, matching the existing
     img+filter icon convention used elsewhere (see .icon-btn .icon). */
  filter: invert(1);
}
[data-theme="pastel"] .search-icon { filter: none; }

/* Icon next to the <h1> on the login-user/login-code pages (see
   templates/login-user.html, login-code.html), replacing the emoji that
   used to prefix "Login"/"Code Login". Sized close to the h1's own text
   height so it reads as part of the heading, not a small decoration
   floating next to it. */
.heading-icon {
  width: 30px;
  height: 30px;
  vertical-align: -7px;
  margin-right: 4px;
  filter: invert(1);
}
[data-theme="pastel"] .heading-icon { filter: none; }

/* This must come AFTER the base .search-wrap rule above -- both have the
   same specificity, and with equal specificity the rule that appears later
   in the file wins regardless of media query nesting. Putting a mobile
   override earlier in the file (as it was before) meant the unconditional
   base rule below it silently overwrote it on every screen size, including
   mobile, which is why the search bar kept rendering oversized on phones. */
@media (max-width: 720px) {
  .search-wrap {
    max-width: none;
    width: 100%;
    flex: 0 0 auto;
    height: 44px;
  }
}

/* ---------- Grid / cards ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px,1fr));
  gap: 10px;
  padding: 12px;
}
[data-theme="glass"] .grid { gap: 12px; padding: 14px; }
.card {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--bg-raised);
  cursor: pointer;
  transition: transform .15s ease;
}
.card:hover { transform: translateY(-2px); }
[data-theme="glass"] .card {
  border-radius: var(--radius-sm);
  backdrop-filter: blur(18px) saturate(150%);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-highlight);
  transition: transform .18s ease, box-shadow .18s ease;
}
[data-theme="glass"] .card:hover {
  transform: translateY(-3px);
  box-shadow: var(--glass-highlight), 0 10px 26px rgba(0,0,0,0.35);
}
.card img {
  width: 100%;
  height: 210px;
  object-fit: cover;
  display: block;
  background: #151518;
}
.card .card-title {
  padding: 8px 10px;
  font-size: 13px;
  opacity: .85;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.variant-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(0,0,0,0.65);
  color: #fff;
  font-size: 11px;
  padding: 3px 7px;
  border-radius: 999px;
}
.rating-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  background: rgba(0,0,0,0.65);
  color: #fff;
  padding: 4px;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
}
[data-theme="glass"] .variant-badge,
[data-theme="glass"] .rating-badge,
[data-theme="glass"] .linked-badge {
  background: rgba(10,10,17,0.55);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid rgba(255,255,255,0.18);
}
.rating-badge .badge-icon {
  width: 14px;
  height: 14px;
  display: block;
  /* Inline SVG painted with fill="#fff" directly (see makeRatingIcon() in
     gallery.js) -- no filter needed. Rendering it through filter:invert(1)
     on a plain black <img> made this specific icon noticeably fuzzier at
     this small size than painting the white fill directly. */
}
.linked-badge {
  position: absolute;
  bottom: 34px;
  left: 8px;
  background: rgba(0,0,0,0.65);
  color: #fff;
  font-size: 10px;
  padding: 3px 7px;
  border-radius: 999px;
  white-space: nowrap;
}
.collection-card { padding: 16px; border-radius: var(--radius); }
/* No text-transform here on purpose -- a tag's canonical name is always
   lowercase (see find_or_create_tag()/normalize_tag_casing() in
   helpers.py), and capitalizing it for display only would make a
   correctly-lowercase tag LOOK capitalized on this one page, which is
   exactly what looked like a data bug before this was removed. */
.collection-card .title { font-size: 16px; margin: 0 0 6px; }
.collection-card .count { font-size: 13px; opacity: .65; }

.end { text-align: center; opacity: .6; padding: 20px; font-size: 14px; }

/* ---------- Toast (e.g. "no images match" for the random button) ---------- */
.pawlery-toast {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  background: var(--bg-raised, #1c1c1f);
  color: var(--text, #fff);
  border: 1px solid rgba(255,255,255,0.12);
  padding: 10px 18px;
  border-radius: 999px;
  font-size: 13px;
  box-shadow: 0 6px 20px rgba(0,0,0,0.4);
  z-index: 200;
  opacity: 0;
  transition: opacity .2s ease;
  pointer-events: none;
}
.pawlery-toast.show { opacity: 1; }
[data-theme="glass"] .pawlery-toast {
  background: var(--glass-bg-strong);
  backdrop-filter: blur(20px) saturate(160%);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
  border-color: var(--glass-border);
  padding: 11px 20px;
  box-shadow: var(--glass-highlight), 0 6px 24px rgba(0,0,0,0.45);
}

/* ---------- Access-gate modal (suggestive unlock / explicit login prompt) ---------- */
.pawlery-modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 300;
  padding: 16px;
}
.pawlery-modal {
  background: var(--bg-raised);
  border-radius: var(--radius);
  padding: 20px;
  max-width: 360px;
  width: 100%;
  box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.pawlery-modal h3 { margin: 0 0 10px; font-size: 17px; }
.pawlery-modal p { margin: 0; font-size: 14px; color: var(--text-dim); line-height: 1.5; }
.pawlery-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 18px;
}
[data-theme="glass"] .pawlery-modal-backdrop {
  background: rgba(0,0,0,0.55);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
[data-theme="glass"] .pawlery-modal {
  background: var(--glass-bg-strong);
  backdrop-filter: blur(26px) saturate(170%);
  -webkit-backdrop-filter: blur(26px) saturate(170%);
  border: 1px solid var(--glass-border);
  padding: 22px;
  box-shadow: var(--glass-highlight), 0 16px 44px rgba(0,0,0,0.5);
}

/* ---------- Lightbox ---------- */
.lightbox {
  position: fixed;
  inset: 0;
  display: none;
  grid-template-columns: 340px 1fr;
  grid-template-rows: 100%;
  background: rgba(0,0,0,0.95);
  z-index: 100;
}
.lightbox.show { display: grid; }
.lightbox .panel-col {
  background: var(--bg-raised);
  padding: 16px;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}
[data-theme="glass"] .lightbox { background: rgba(3,3,6,0.96); }
[data-theme="glass"] .lightbox .panel-col {
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-right: 1px solid var(--glass-border);
  padding: 18px;
}
.panel-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}
.lightbox h2 { margin: 0 0 6px; font-size: 18px; }
.lightbox .desc {
  opacity: .8;
  font-size: 14px;
  overflow-wrap: break-word;
  word-break: break-word;
  white-space: pre-line;
}
.lightbox .parent-link { font-size: 13px; margin-top: 10px; }
.lightbox .parent-link a { color: var(--accent); text-decoration: none; }

/* Tags: a wrapping row by default (desktop). On mobile this becomes a
   single-line horizontally-scrolling strip instead (see media query
   below) so a long tag list can't grow the fixed-height info panel. */
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}
.tag-chip {
  flex: 0 0 auto;
  background: var(--bg-input);
  color: var(--text-dim);
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 12px;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s ease, color .12s ease;
}
.tag-chip:hover,
.tag-chip:focus-visible {
  background: var(--accent);
  color: var(--accent-text);
}
[data-theme="glass"] .tag-chip {
  border: 1px solid var(--glass-border);
  padding: 4px 11px;
}

/* ---------- Artist checklist (lightbox Artists edit) ----------
   A checkbox per row instead of a native <select multiple> -- lets each
   row show a checkmark rather than relying on the browser's own (barely
   stylable) option-highlight look. Height grows with the row count up to
   ~3 rows, then scrolls, so a 1-2 option list isn't padded out with empty
   space but a long one doesn't take over the panel. */
.artist-option-list {
  display: flex;
  flex-direction: column;
  max-height: 138px; /* ~3 rows at this row's ~46px */
  overflow-y: auto;
  border: 1px solid #333;
  border-radius: var(--radius-sm);
  background: var(--bg-input);
}
[data-theme="glass"] .artist-option-list { border-color: var(--glass-border); }
[data-theme="macchiato"] .artist-option-list,
[data-theme="pastel"] .artist-option-list { border-color: var(--input-border); }
.artist-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 10px;
  font-size: 14px;
  cursor: pointer;
  user-select: none;
}
.artist-option:hover { background: var(--bg-hover); }
.artist-option:not(:last-child) { border-bottom: 1px solid rgba(128,128,128,0.15); }
/* Visually hidden but still a real, focusable, keyboard-toggleable
   checkbox -- screen readers announce it normally; only its own default
   box is hidden, not the row. */
.artist-option input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.artist-option-check {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  border-radius: 5px;
  border: 1px solid var(--text-dimmer);
  display: flex;
  align-items: center;
  justify-content: center;
}
.artist-option-check .icon {
  width: 12px;
  height: 12px;
  display: none;
  /* checkmark.svg is a plain black source file; painted white here so it
     reads against the accent-filled box below (same brightness(0)invert(1)
     "force white" treatment used for icons on other bright/colored
     buttons -- see .btn-primary .icon). */
  filter: brightness(0) invert(1);
}
.artist-option input:checked ~ .artist-option-check {
  background: var(--accent);
  border-color: var(--accent);
}
.artist-option input:checked ~ .artist-option-check .icon {
  display: block;
}
.artist-option input:focus-visible ~ .artist-option-check {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ---------- Icon-choice radio groups (Rating / Visibility on the
   upload/edit forms) ----------
   Native <select><option> can't contain an <img> -- there's no way to
   show an icon per option in a real dropdown -- so these render as a row
   of pill-shaped radio buttons with an icon instead. Same visually-
   hidden-but-focusable input technique as .artist-option above (just
   radio instead of checkbox, and the visible pill is a sibling span
   rather than a small fixed checkbox), so it's still a real, keyboard-
   operable native form control; only its own default appearance is
   hidden. */
.icon-choice-group {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}
.icon-choice {
  cursor: pointer;
  user-select: none;
}
.icon-choice input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.icon-choice-box {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 14px;
  border-radius: 999px;
  border: 1px solid #333;
  background: var(--bg-input);
  color: var(--text-dim);
  font-size: 14px;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
[data-theme="glass"] .icon-choice-box { border-color: var(--glass-border); }
[data-theme="macchiato"] .icon-choice-box,
[data-theme="pastel"] .icon-choice-box { border-color: var(--input-border); }
.icon-choice:hover .icon-choice-box { background: var(--bg-hover); }
.icon-choice input:checked ~ .icon-choice-box {
  background: var(--accent);
  color: var(--accent-text);
  border-color: var(--accent);
}
.icon-choice input:focus-visible ~ .icon-choice-box {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.icon-choice-box .icon {
  width: 16px;
  height: 16px;
  filter: invert(1);
}
/* Selected pill's background flips to bright accent green with
   near-black text (same as .chip.active) -- the icon should switch to
   native black there too instead of staying white-inverted. */
.icon-choice input:checked ~ .icon-choice-box .icon {
  filter: none;
}
[data-theme="pastel"] .icon-choice-box .icon {
  filter: none;
}

.variant-strip {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  margin-top: 12px;
  padding-bottom: 4px;
}
.variant-thumb-wrap {
  position: relative;
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  cursor: pointer;
}
.variant-strip .variant-thumb {
  width: 64px;
  height: 64px;
  border-radius: 8px;
  object-fit: cover;
  border: 2px solid transparent;
  opacity: .75;
  display: block;
}
.variant-strip .variant-thumb.active { border-color: var(--accent); opacity: 1; }
.variant-thumb-wrap:hover .variant-thumb { opacity: 1; }
[data-theme="glass"] .variant-strip .variant-thumb { border-radius: 12px; }

/* ---------- Play icon (video cards) ---------- */
.play-icon {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(0,0,0,0.55);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  pointer-events: none;
}
.play-icon-mini {
  width: 22px;
  height: 22px;
  font-size: 9px;
}
[data-theme="glass"] .play-icon {
  background: rgba(10,10,17,0.5);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,0.25);
}

/* ---------- Lightbox action row ----------
   Two stacked rows: action-row-main (Download/Share/Close) always stays
   on one line, and action-row-admin (Edit/Delete, admin-only) sits
   underneath it -- rather than one flex-wrap row where Close (and Edit/
   Delete) could wrap onto their own line unpredictably depending on
   panel width. */
.action-row {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: auto;
  padding-top: 12px;
}
.action-row-main {
  display: flex;
  flex-wrap: nowrap;
  gap: 8px;
}
.action-row-main .btn {
  flex: 0 1 auto;
  min-width: 0;
}
.action-row-main .btn span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.action-row-admin {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.action-row-admin:empty { display: none; }
.action-row form { display: inline; }

/* Share's label swaps to "Copied!"/"Copy failed" while its feedback state
   is showing (see flashShareButton() in gallery.js). No fixed min-width
   here -- Download/Share/Close's actual measured widths (108px/81-115px/
   38px) already fit comfortably inside the 300px+ action row at every one
   of Share's states, so flooring it (previously 168px, well past its
   ~115px real maximum) only made it balloon visibly larger than its
   neighbors for no reason. Left to size to its own content, it grows by
   at most ~35px during the flash -- Close shifts a little, but nowhere
   near as jarring as the button itself swelling past double width. */

.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.btn-icon .icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  display: block;
  /* Most downloadable icon packs ship black/dark-on-transparent SVGs --
     invert so they read clearly against our dark button backgrounds. */
  filter: invert(1);
}
.btn-primary .icon,
.btn-danger .icon {
  /* These buttons are already bright (green/red); a plain white icon
     has better contrast on them than the inverted treatment above. */
  filter: brightness(0) invert(1);
}
/* Both Download buttons (the lightbox's #download-btn and admin's "Download
   all Files") are .btn-primary, whose label text renders near-black (see
   .btn-primary above) -- a white icon next to near-black text read as
   mismatched. download.svg is already a plain black source file, so no
   filter at all (overriding the white conversion above) already matches
   the text color exactly. ID selectors win over the two classes above
   regardless of source order. */
#download-btn .icon,
#download-all-btn .icon {
  filter: none;
}

/* Pastel is the only *light* theme -- its neutral surfaces (header,
   plain buttons, inactive filter chips) are light backgrounds, unlike
   every other theme, so the shared invert(1)-to-white treatment above
   (tuned for icons sitting on dark surfaces) would render them nearly
   invisible here. Un-invert them back to their native black instead.
   Excludes .btn-primary/.btn-danger, which stay their own bright accent/
   danger color even in Pastel (see those themed overrides above) and so
   still want a white icon, same as every other theme. */
[data-theme="pastel"] .icon-btn .icon,
[data-theme="pastel"] img.chip-icon,
[data-theme="pastel"] .btn-icon:not(.btn-primary):not(.btn-danger) .icon {
  filter: none;
}

/* Desktop: Download/Share (which keep their text label -- see the
   #download-btn/#share-btn override further down) and the icon-only
   Edit/Delete/Close comfortably share one row for a signed-out visitor
   (3 buttons). For an admin viewing their own image, Edit and Delete sit
   on their own row underneath (see .action-row-admin above) so they never
   compete with Download/Share/Close for space. Left at the base
   flex-wrap:wrap on .action-row-admin (not forced nowrap), so it stays one
   row when it fits and wraps rather than overflowing when it doesn't. */
@media (min-width: 769px) {
  .action-row-main,
  .action-row-admin {
    gap: 6px;
  }
  .action-row .btn {
    padding: 8px 10px;
    font-size: 13px;
    white-space: nowrap;
    flex: 0 1 auto;
  }

  /* Collapse to icon-only on desktop so Close (and Edit/Delete) fit
     comfortably in the fairly narrow 340px info panel. Guarded behind
     :has() support -- on browsers without it (a shrinking minority),
     buttons just keep their icon+label layout instead of ending up empty,
     so this is a safe progressive enhancement either way. */
  @supports selector(:has(a)) {
    .btn-icon span { display: none; }
    .btn-icon:not(:has(.icon)) span { display: inline; }
    .btn-icon {
      padding: 8px;
      gap: 0;
    }
    .btn-icon .icon { width: 18px; height: 18px; }

    /* Download and Share stay icon+label even on desktop -- every visitor
       (not just admins, unlike Edit/Delete) relies on these two, so
       collapsing them to icon-only would hurt discoverability for the
       whole audience. ID selectors always outrank the plain-class rules
       above regardless of source order. #download-all-btn (admin.html's
       "Download all Files") isn't part of that narrow lightbox panel at
       all, but shares the .btn-icon class for its icon+label layout, so
       it needs the same opt-out or the bare .btn-icon span rule above
       would hide its label here too. */
    #download-btn span,
    #share-btn span,
    #download-all-btn span {
      display: inline;
    }
    #download-btn,
    #share-btn,
    #download-all-btn {
      padding: 8px 12px;
      gap: 6px;
    }
  }
}

.image-container {
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  overflow: hidden;
  min-width: 0;
  min-height: 0;
}
.lightbox img.full-img,
.lightbox video.full-img {
  /* Fixed width/height (not max-width/max-height) so the element's own
     box is always the same footprint regardless of which image is
     currently loaded into it -- object-fit just changes how the pixels
     are scaled *within* that box. Using max-width/max-height instead let
     the box shrink to the intrinsic size of whatever's loaded, so the
     low-res thumbnail buffer (smaller pixel dimensions than the full
     image) rendered visibly smaller and then "popped" larger once the
     full-res image swapped in. */
  width: 95%;
  height: 95%;
  object-fit: contain;
  transition: filter .35s ease;
}
[data-theme="glass"] .lightbox img.full-img,
[data-theme="glass"] .lightbox video.full-img {
  border-radius: var(--radius-sm);
}

/* Buffer state: the thumbnail is shown immediately (blurred/slightly
   scaled up to hide its low-res edges) while the full-resolution image or
   video loads in the background; see loadMedia() in gallery.js. */
.full-img.is-loading {
  filter: blur(18px) saturate(1.05);
  transform: scale(1.04);
}

.media-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34px;
  height: 34px;
  margin: -17px 0 0 -17px;
  border: 3px solid rgba(255,255,255,0.25);
  border-top-color: #fff;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transition: opacity .15s ease;
  z-index: 2;
  animation: pawlery-spin 0.8s linear infinite;
}
.image-container.media-loading .media-spinner { opacity: 1; }

@keyframes pawlery-spin {
  to { transform: rotate(360deg); }
}

.nav-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  color: #fff;
  background: rgba(0,0,0,0.4);
  border: none;
  border-radius: 50%;
  /* 56px comfortably clears the ~44px minimum touch-target guideline
     while staying small enough not to obstruct the media next to it. */
  width: 56px;
  height: 56px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.nav-btn:hover { background: rgba(0,0,0,0.6); }
.nav-btn .nav-icon {
  width: 26px;
  height: 26px;
  display: block;
  /* chevron-left.svg/chevron-right.svg are plain black source files --
     invert to white so they read against the dark circular button,
     matching the existing img+filter icon convention. */
  filter: invert(1);
}
.nav-left { left: 12px; }
.nav-right { right: 12px; }
[data-theme="glass"] .nav-btn {
  background: rgba(10,10,17,0.4);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  border: 1px solid rgba(255,255,255,0.2);
  transition: background .15s ease;
}
[data-theme="glass"] .nav-btn:hover { background: rgba(10,10,17,0.6); }

@media (max-width: 768px) {
  .lightbox {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto;
  }
  .lightbox.show { display: flex; flex-direction: column; }

  /* Fixed-height info panel: this must NOT change size based on
     description length, tag count, or variant info, or it pushes the
     media area (and the nav arrows within it) up/down on every
     navigation. ~40% of viewport height, adjustable via the values below
     (kept in sync between height/max-height/flex-basis). Declared as
     vh first with a dvh override so it still works on browsers that
     don't support dvh, while getting more accurate sizing (accounting
     for mobile browser toolbars) on ones that do. */
  .lightbox .panel-col {
    order: 1;
    padding: 12px;
    box-sizing: border-box;
    flex: 0 0 auto;
    height: 40vh;
    height: 40dvh;
    max-height: 40vh;
    max-height: 40dvh;
  }
  [data-theme="glass"] .lightbox .panel-col {
    border-right: none;
    border-bottom: 1px solid var(--glass-border);
  }

  .image-container {
    order: 2;
    padding: 8px;
    flex: 1 1 auto;
    min-height: 0;
  }
  .lightbox img.full-img,
  .lightbox video.full-img {
    width: 100%;
    height: 100%;
  }

  /* Tags become a single-line horizontally-scrolling strip on mobile
     (like the variant strip already is) instead of wrapping across
     multiple lines, which would eat into the fixed-height panel's
     limited vertical space. Scrollbar hidden (still fully scrollable via
     touch/trackpad) -- the visible track/thumb otherwise sits right on
     top of the tag chips instead of in space of its own. */
  .tags {
    flex-wrap: nowrap;
    overflow-x: auto;
    padding-bottom: 2px;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* legacy Edge */
  }
  .tags::-webkit-scrollbar {
    display: none; /* Chrome/Safari/newer Edge */
  }

  .action-row-main .btn { flex: 1 1 0; min-width: 0; }
  .action-row-admin .btn { flex: 1 1 auto; }
}

/* Phones in landscape are short, so a 40% info panel would leave very
   little room for the media itself -- shrink it there. Still scoped to
   narrow/mobile widths, so this never applies to desktop. */
@media (max-width: 768px) and (orientation: landscape) {
  .lightbox .panel-col {
    height: 28vh;
    height: 28dvh;
    max-height: 28vh;
    max-height: 28dvh;
  }
}

/* ---------- Tables (admin nsfw codes / tags) ---------- */
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #2a2a2d; font-size: 14px; }
.input-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
[data-theme="glass"] th, [data-theme="glass"] td { padding: 11px 12px; border-bottom-color: rgba(255,255,255,0.1); }
[data-theme="glass"] tbody tr { transition: background .12s ease; }
[data-theme="glass"] tbody tr:hover { background: rgba(255,255,255,0.04); }
[data-theme="macchiato"] th, [data-theme="macchiato"] td,
[data-theme="pastel"] th, [data-theme="pastel"] td { border-bottom-color: var(--table-border); }

/* Lets a wide table (a very long NSFW code, a long tag name) scroll
   *within* this box instead of stretching the whole page horizontally --
   the overflow is contained here, not on <body>, so it can't affect page
   layout on desktop or mobile. */
.table-scroll {
  overflow-x: auto;
}

/* NSFW code cell: the code + its Copy button. The code itself wraps
   (long codes don't force this box, and therefore the table/page, wider)
   while staying fully readable and manually selectable. */
.code-cell {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
}
.code-text {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  overflow-wrap: anywhere;
  word-break: break-all;
  min-width: 0;
  flex: 1 1 160px;
}
.copy-code-btn {
  flex: 0 0 auto;
  padding: 6px 12px;
  font-size: 13px;
  min-width: 64px;
}

/* Artist-tag rows on Manage Tags: a subtle warm tint + left accent bar so
   artist tags are recognizable at a glance while scanning a long list,
   without being a harsh/loud color. Never the *only* signal though -- the
   "Artist tag?" column's "Yes ✅" text (already present) still says the
   same thing in words, so this reads fine even without color. */
#tags-table tr.tag-row-artist td {
  background: rgba(255, 178, 76, 0.08);
}
#tags-table tr.tag-row-artist td:first-child {
  box-shadow: inset 3px 0 0 rgba(255, 178, 76, 0.55);
}

/* Tighter row padding than the site-wide th/td default (10px 12px) -- this
   table can get long, and the default padding wasted vertical space that
   mattered most on narrow/mobile screens, where every extra row of
   scrolling is more costly. Scoped to this table specifically so the
   NSFW-codes table elsewhere keeps its normal spacing. */
#tags-table th, #tags-table td {
  padding: 6px 10px;
}
/* The Edit and Mark/unmark actions are icon-only here (see admin-tags.js)
   -- shrink them from the base .btn's roomier 10px/16px padding so a dense
   list of rows doesn't get padded out by two oversized buttons, and so
   both actions are a consistent, compact square regardless of state. */
#tags-table .btn-icon {
  padding: 6px 8px;
}

/* ---------- Misc pages ---------- */
.centered-page {
  position: relative; /* containing block for .flash-overlay below */
  min-height: calc(100vh - var(--header-h));
  display: grid;
  place-items: center;
  padding: 24px;
}
.centered-page form { max-width: 340px; width: 100%; }

/* An error/success message inside .centered-page (e.g. login-code.html's
   invalid/expired-code flash): positioned absolutely against the page
   container instead of participating in normal flow, so showing it can't
   push the centered form down or shift its position. Sized/centered to
   match the form's own max-width so it lines up with the panel below it
   rather than spanning the full page width. */
.flash-overlay {
  position: absolute;
  /* Overrides .flash's normal-flow margin, which would otherwise offset
     this off-center since left/width are set explicitly here. */
  margin: 0;
  top: 24px;
  left: 50%;
  transform: translateX(-50%);
  width: calc(100% - 48px);
  max-width: 340px;
  box-sizing: border-box;
  z-index: 1;
}

/* =========================================================
   Experimental: Discord-style justified gallery
   =========================================================
   Opt-in via the "Experimental features" toggle in the Settings popup
   (see base.html / gallery.js). gallery.js adds/removes the
   .experimental-masonry class on #grid at startup and whenever the
   toggle changes; everything below is scoped under that class so it can
   never affect the default grid above (.grid's own display:grid rule is
   untouched).

   Row-packing: gallery.js sizes each .masonry-row's cards so their
   widths sum to exactly the container's content-box width (padding
   already subtracted -- see getMasonryContainerWidth() in gallery.js).
   overflow-x:hidden below is a defensive backstop only; the width math
   itself is exact (remainder-distributed across the row, never
   rounded up past the container), so it should never actually need to
   clip anything. */
.grid.experimental-masonry {
  display: block;
  overflow-x: hidden;
}
.masonry-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 10px;
}
.masonry-row .card {
  flex: 0 0 auto;
}
.grid.experimental-masonry .card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
