/*
  app.css — component and layout styles. Tokens live in tokens.css; this
  file should reference var(--...) and rarely hardcode a colour, spacing,
  or radius value directly.

  Contents:
    1. Base (reset, typography, links, selection)
    2. Layout shell (skip-link, app-bar, main, tab-bar)
    3. Menu sheet (dialog)
    4. Focus visibility
    5. Reduced motion
    6. Forms (login / upload / tag-add)
    7. Messages (flash messages) + errorlist (form field errors)
    8. Tag chips
    9. Month header
    10. Gallery grid
    11. Pagination
    12. Detail page
    13. Tag picker (pick-don't-type)
    14. Gallery multi-select (batch tagging)
    15. Admin date-edit
    16. Error pages (404)
    17. Lightbox
    18. Favourites
    19. Filter sheet
    20. Load more
    21. Empty state
    22. Upload drop zone
    23. Activity feed
    24. Help page
    25. Setup card (first-run install / notifications)
*/

/* ---------- 1. Base ---------- */

* {
  box-sizing: border-box;
}

html {
  font-size: var(--font-size-base);
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial,
    sans-serif;
  line-height: var(--leading-normal);
}

h1,
h2,
h3 {
  line-height: var(--leading-tight);
  color: var(--text);
}

h1 {
  font-size: var(--text-2xl);
  margin: 0;
}

h2 {
  font-size: var(--text-xl);
}

h3 {
  font-size: var(--text-lg);
}

p {
  color: var(--text);
}

a {
  color: var(--accent);
}

a:visited {
  color: var(--accent);
}

::selection {
  background: var(--accent-soft);
  color: var(--text);
}

/* ---------- 2. Layout shell ---------- */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  padding: var(--space-2) var(--space-3);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius);
}

.skip-link:focus-visible {
  left: var(--space-3);
  top: var(--space-3);
}

.app-bar {
  position: sticky;
  top: 0;
  z-index: 8;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.app-bar-inner {
  max-width: var(--content-max);
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
}

.app-bar-brand {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 700;
  font-size: var(--text-lg);
  color: var(--text);
  text-decoration: none;
}

.app-bar-logo {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: var(--radius-sm);
}

/* Below 480px, drop the wordmark so the page title has room; keep it in
   the accessibility tree (visually hidden, not display:none) so the brand
   link's accessible name doesn't disappear on small screens. */
.app-bar-brand-text {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (min-width: 480px) {
  .app-bar-brand-text {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    white-space: normal;
  }
}

.app-bar-title {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: var(--leading-tight);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.icon-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  font-size: var(--text-lg);
  color: var(--text-muted);
  background: transparent;
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background-color var(--dur) var(--ease);
}

.icon-btn:not(:disabled):hover {
  background: var(--accent-soft);
  color: var(--text);
}

.icon-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* De-emphasised, not hidden: the default gallery experience (scroll, tap a
   year/month chip) never needs this, but it must stay easy to find for
   whoever wants a text/tag/date search. */
#filter-sheet-open {
  opacity: 0.55;
}

#filter-sheet-open:hover {
  opacity: 1;
}

#main {
  display: block;
  max-width: var(--content-max);
  margin-inline: auto;
  padding: var(--space-4) var(--space-4) calc(var(--tap-min) + var(--space-6));
}

@media (min-width: 900px) {
  #main {
    padding-bottom: var(--space-4);
  }
}

.tab-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 9;
  display: flex;
  background: var(--surface);
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
  padding-bottom: env(safe-area-inset-bottom);
}

.tab-item {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-xs);
  font-family: inherit;
  color: var(--text-muted);
  text-decoration: none;
  background: none;
  border: none;
  cursor: pointer;
}

.tab-icon {
  position: relative;
  font-size: var(--text-lg);
  line-height: 1;
}

.tab-badge {
  position: absolute;
  top: -6px;
  right: -10px;
  min-width: 16px;
  padding: 0 4px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}

.tab-item[aria-current="page"] {
  color: var(--accent);
}

.tab-item-disabled {
  color: var(--text-faint);
  cursor: default;
}

/* Both .tab-bar and #batch-bar are position:fixed;bottom:0. gallery-select.js
   already toggles [hidden] on #batch-bar directly, so :has() hides the tab
   bar underneath it with zero JS changes. */
body:has(#batch-bar:not([hidden])) .tab-bar {
  display: none;
}

@media (min-width: 900px) {
  .tab-bar {
    position: static;
    z-index: auto;
    box-shadow: none;
    border-top: none;
    border-bottom: 1px solid var(--border);
    padding-bottom: 0;
    justify-content: flex-start;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-4);
  }

  .tab-item {
    flex: 0 0 auto;
    flex-direction: row;
    gap: var(--space-2);
    min-width: var(--tap-min);
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-base);
    border-radius: var(--radius);
  }

  .tab-item:not(.tab-item-disabled):hover {
    background: var(--accent-soft);
  }
}

/* ---------- 3. Menu sheet ---------- */

.menu-sheet {
  position: fixed;
  inset: auto 0 0 0;
  margin: 0;
  width: 100%;
  max-width: none;
  max-height: 80vh;
  overflow-y: auto;
  border: none;
  border-top-left-radius: var(--radius-lg);
  border-top-right-radius: var(--radius-lg);
  padding: var(--space-4);
  padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom));
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
}

.menu-sheet::backdrop {
  background: rgba(0, 0, 0, 0.4);
}

@media (min-width: 900px) {
  .menu-sheet {
    inset: 64px var(--space-4) auto auto;
    width: 280px;
    max-height: none;
    border-radius: var(--radius-lg);
  }
}

.menu-sheet-user {
  margin: 0 0 var(--space-3);
  font-weight: 700;
}

.menu-sheet-link {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2) 0;
  font-size: var(--text-base);
  font-family: inherit;
  text-align: left;
  text-decoration: none;
  color: var(--text);
  background: none;
  border: none;
  border-top: 1px solid var(--border);
  cursor: pointer;
}

.menu-sheet-logout {
  margin: 0;
  border-top: 1px solid var(--border);
}

.menu-sheet-logout button {
  width: 100%;
  margin-top: var(--space-2);
}

.menu-sheet-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  font-size: var(--text-xl);
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
}

/* ---------- 4. Focus visibility ---------- */

a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

/* ---------- 5. Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  * {
    transition-duration: 0.001ms !important;
    animation-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ---------- 6. Forms (shared: login, upload, tag-add) ---------- */

form {
  display: block;
}

/* `display: block` above would otherwise beat the UA [hidden] rule -- upload.js
   hides the picker form this way when it swaps in the progress screen. */
form[hidden] {
  display: none;
}

label {
  display: block;
  font-size: var(--text-base);
  font-weight: 600;
  margin: var(--space-4) 0 var(--space-2);
  color: var(--text);
}

input[type="text"],
input[type="password"],
input[type="email"],
input[type="search"] {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  font-size: var(--text-base);
  font-family: inherit;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="search"]:focus {
  border-color: var(--accent);
}

/* The only two <select> elements in the app both live on manage-members
   (per-row role, and the Add-member form's role field). Styled directly on
   the bare element rather than a class, since the Add-member field is
   Django-auto-rendered and has no hook to add one. */
select {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  font-size: var(--text-base);
  font-family: inherit;
  padding: var(--space-2) var(--space-6) var(--space-2) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  cursor: pointer;
}

select:focus {
  border-color: var(--accent);
}

select::-ms-expand {
  display: none;
}

/* Draws a chevron over a wrapped <select> with a border-triangle instead of
   an inline-SVG data-URI -- a data-URI's fill can't reference
   var(--text-muted), so it would need a hand-duplicated dark-mode copy. */
.select-wrap {
  position: relative;
  display: block;
}

.select-wrap::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--space-3);
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid var(--text-muted);
  transform: translateY(-50%);
  pointer-events: none;
}

input[type="file"] {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  font-size: var(--text-base);
  padding: var(--space-2) var(--space-2);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

/* The native "choose file(s)" button part of the file input */
input[type="file"]::file-selector-button {
  min-height: var(--tap-min);
  margin-right: var(--space-3);
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-base);
  font-family: inherit;
  border: none;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-contrast);
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  filter: brightness(1.08);
}

button,
input[type="submit"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  font-size: var(--text-base);
  font-family: inherit;
  font-weight: 600;
  color: var(--accent-contrast);
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  transition: filter var(--dur-fast) var(--ease);
}

button:hover,
input[type="submit"]:hover {
  filter: brightness(1.08);
}

button:active,
input[type="submit"]:active {
  filter: brightness(0.95);
}

button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* The blanket `button { display: inline-flex; }` above beats the browser's
   UA [hidden] { display: none } rule, so these two toggle buttons stay
   visible together unless overridden here. push.js's updateButtons() only
   ever unhides one at a time. */
#push-enable-btn[hidden],
#push-disable-btn[hidden] {
  display: none;
}

/* Destructive actions (delete). Resting state, not just :hover, unlike
   .tag-remove-form's hover-only use of the same tokens -- deleting media
   is higher-stakes than removing a tag. */
.button-danger {
  color: var(--error-text);
  background: var(--error-bg);
  border: 1px solid var(--error-border);
}

/* Shrinks a button's footprint for a secondary inline action sitting next
   to its own field (e.g. "Save" beside a nickname/role control), without
   dropping the min-height tap target. */
.button-compact {
  min-width: auto;
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
}

/* A low-emphasis danger trigger for an inline list row (manage-members'
   per-row "Remove") -- distinct from .button-danger's filled treatment,
   which stays reserved for higher-stakes, page-level delete confirmations. */
.button-danger-outline {
  min-width: auto;
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--error-text);
  background: transparent;
  border: 1px solid var(--error-border);
}

.button-danger-outline:hover {
  background: var(--error-bg);
}

#upload-status {
  margin-left: var(--space-3);
  color: var(--text-muted);
  font-size: var(--text-base);
}

.field-help {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ---------- 7. Messages + errorlist ---------- */

.messages {
  margin: var(--space-4) 0;
}

.message {
  margin: 0 0 var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-left: 4px solid;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
}

.message-success {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success-text);
}

.message-info {
  background: var(--info-bg);
  border-color: var(--info-border);
  color: var(--info-text);
}

.message-warning {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning-text);
}

.message-error {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--error-text);
}

.message-debug {
  background: var(--surface-sunken);
  border-color: var(--border-strong);
  color: var(--text-muted);
}

.errorlist {
  list-style: none;
  margin: var(--space-4) 0;
  padding: 0;
}

.errorlist li {
  background: var(--error-bg);
  color: var(--error-text);
  border-left: 4px solid var(--error-border);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-2);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
}

/* ---------- 8. Tag chips (gallery filter + detail tag list) ---------- */

.tag-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0;
  padding: 0;
  list-style: none;
}

.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
  color: var(--accent);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

/* Select toggles select-mode, it isn't a filter -- keep it visually neutral. */
#gallery-select-toggle {
  color: var(--text);
}

.tag-chip:hover {
  background: var(--accent-soft);
}

.tag-chip-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

/* .tag-chip:hover's background would otherwise win here on specificity
   (two class-selectors vs. .tag-chip-active's one) and paint the pale hover
   background under .tag-chip-active's light text -- unreadable. Found via
   manual testing of the new year/month chips on a mouse-driven desktop. */
.tag-chip-active:hover {
  background: var(--accent);
}

/* Every chip except the (button-only) year chips this was first noticed
   on is an <a href> -- and the global `a:visited { color: var(--accent) }`
   rule above (element + pseudo-class, (0,1,1)) otherwise beats
   `.tag-chip-active`'s plain class (0,1,0), repainting the label blue the
   moment a chip's own URL has been visited -- unreadable on the same
   solid-blue active background. A `<button>`-based chip (e.g. the old
   year picker) never hit this, which is exactly why it looked fine while
   every link-based chip didn't. */
.tag-chip-active:visited {
  color: var(--accent-contrast);
}

.tag-count {
  font-size: var(--text-xs);
  opacity: 0.75;
}

/* People get their own row above the topic chips, visually distinguished by
   an initial avatar rather than a different colour -- the palette stays
   quiet so photos remain the only saturated thing on screen. */
.tag-chips-people {
  margin-bottom: 0;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.tag-chip-person {
  padding-left: var(--space-1);
}

.tag-chip-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: 0 0 auto;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent-contrast);
  background: var(--accent);
  border-radius: var(--radius-pill);
}

.tag-chip-active .tag-chip-avatar {
  color: var(--accent);
  background: var(--accent-contrast);
}

/* Month-jump navigator: one horizontally-scrolling row (reuses .tag-chip
   styling as-is) rather than wrapping -- the full history can run to many
   chips, so scrolling keeps every one on a single aligned line instead of
   wrapping into ragged, unequal-width rows. `margin`/`padding`/`border`
   live on the `.chip-scroll-row` wrapper, not on `.month-chips` itself, so
   there's no competing `display` rule to fight the browser's built-in
   `[hidden] { display: none }` the way `.tag-chips`'s own `display: flex`
   would if this row were ever conditionally hidden. The border below
   separates the whole navigator from the topic/people chips beneath it,
   same idiom as .tag-chips-people. */
.chip-scroll-row {
  position: relative;
  margin: var(--space-4) 0;
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border);
}

.month-chips {
  margin: 0;
  flex-wrap: nowrap;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scrollbar-width: none;
}

.month-chips::-webkit-scrollbar {
  display: none;
}

.month-chip {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

/* Click-to-scroll, for desktop/mouse users who have no swipe gesture --
   touch devices already scroll this row natively, so these would just be
   redundant clutter there. month-jump.js toggles each button's `hidden`
   based on the row's actual scroll position, so one only ever appears on
   an end that's actually reachable -- including not at all, for a row
   short enough to never overflow in the first place. */
@media (hover: hover) and (pointer: fine) {
  .chip-scroll-btn:not([hidden]) {
    display: flex;
  }

  /* Room for the arrow overlays so they don't sit on top of the first/last
     chip; touch devices never render the buttons, so they don't need it. */
  .month-chips {
    padding-inline: 2rem;
    scroll-padding-inline: 2rem;
  }
}

.chip-scroll-btn {
  display: none;
  position: absolute;
  top: 0;
  bottom: var(--space-2);
  align-items: center;
  justify-content: center;
  width: 2rem;
  border: none;
  background: none;
  color: var(--text-muted);
  font-size: var(--text-lg);
  cursor: pointer;
  z-index: 1;
}

.chip-scroll-left {
  left: 0;
  background: linear-gradient(to right, var(--bg) 50%, transparent);
}

.chip-scroll-right {
  right: 0;
  background: linear-gradient(to left, var(--bg) 50%, transparent);
}

/* ---------- 9. Month header ---------- */

.month-header {
  position: sticky;
  top: 0;
  z-index: 5;
  margin: var(--space-5) 0 var(--space-3);
  padding: var(--space-2) 0;
  background: var(--bg);
  font-size: var(--text-lg);
  font-weight: 700;
  border-bottom: 1px solid var(--border);
}

/* ---------- 10. Gallery grid ---------- */

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

@media (min-width: 480px) {
  .gallery-grid {
    grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  }
}

@media (min-width: 900px) {
  .gallery-grid {
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: var(--space-3);
  }
}

.gallery-tile {
  display: block;
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--tile-bg);
}

.gallery-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.gallery-tile:focus-visible {
  outline-offset: -3px;
}

.video-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 2rem;
  color: var(--video-glyph);
  background: var(--tile-bg);
}

.tile-day-badge {
  position: absolute;
  top: var(--space-1);
  left: var(--space-1);
  z-index: 1;
  padding: 1px var(--space-1);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: 1.4;
  color: #fff;
  background: rgba(0, 0, 0, 0.45);
  border-radius: var(--radius-sm);
  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
}

.tile-duration {
  position: absolute;
  bottom: var(--space-1);
  right: var(--space-1);
  z-index: 1;
  padding: 1px var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  color: #fff;
  background: rgba(0, 0, 0, 0.55);
  border-radius: var(--radius-sm);
}

/* ---------- 11. Pagination ---------- */

.pagination {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin: var(--space-5) 0;
  padding: var(--space-2) 0;
  border-top: 1px solid var(--border);
}

.pagination a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  text-decoration: none;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
}

.pagination a:hover {
  background: var(--accent-soft);
}

.pagination span {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* ---------- 12. Detail page ---------- */

.detail-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0;
}

.detail-nav a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  text-decoration: none;
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.detail-nav a:hover {
  background: var(--accent-soft);
}

.detail-media {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #000;
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: var(--space-4);
}

.detail-media img,
.detail-media video {
  display: block;
  max-width: 100%;
  max-height: 80vh;
  width: auto;
  height: auto;
}

.detail-meta {
  margin: var(--space-4) 0;
}

.detail-meta dt {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: var(--space-3);
}

.detail-meta dd {
  margin: 0.15rem 0 0;
  font-size: var(--text-base);
  color: var(--text);
}

/* Shown next to a date guessed from the filename/save date (library/dates.py),
   so nobody mistakes it for a recorded capture time. */
.date-estimated {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.detail-actions {
  margin: var(--space-4) 0;
}

.confirm-delete-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-4) 0;
}

.tag-remove-form {
  display: inline-flex;
  margin: 0;
}

.tag-remove-form .tag-chip {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text);
  font-family: inherit;
}

.tag-remove-form .tag-chip:hover {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--error-text);
}

/* ---------- 13. Tag picker (pick-don't-type) ---------- */

/*
  Replaces the old <input list="existing-tags"> + tag-input.js chip widget.
  Everything here is a <label> wrapping a real checkbox, so the pick path
  needs no JavaScript and behaves the same on iOS Safari (where <datalist>
  is unreliable) as on Android. The native checkbox stays visible on
  purpose: it is the state indicator that works in every browser, and it
  reads correctly to a screen reader. :has() adds the filled-chip look on
  top where supported -- if it isn't, the checkbox alone still shows state.
*/

.upload-tags-heading,
.detail-tags-heading {
  margin: var(--space-5) 0 0;
  font-size: var(--text-lg);
}

.tag-set-form {
  margin: 0;
}

.tag-picker {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin: var(--space-4) 0 var(--space-5);
}

.tag-picker-group {
  margin: 0;
  padding: 0;
  border: 0;
}

.tag-picker-legend {
  padding: 0;
  margin-bottom: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-muted);
}

.tag-picker-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.picker-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  margin: 0;
  padding: var(--space-2) var(--space-4) var(--space-2) var(--space-3);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease);
}

.picker-chip:hover {
  background: var(--accent-soft);
}

.picker-chip input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  flex: 0 0 auto;
  accent-color: var(--accent);
  cursor: pointer;
}

.picker-chip:has(input:checked) {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.picker-chip-label {
  white-space: nowrap;
}

.picker-chip-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex: 0 0 auto;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent-contrast);
  background: var(--accent);
  border-radius: var(--radius-pill);
}

.tag-picker-empty {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.tag-picker-new {
  margin-top: var(--space-3);
}

.tag-picker-new summary {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-pill);
  cursor: pointer;
  list-style: none;
}

.tag-picker-new summary::-webkit-details-marker {
  display: none;
}

.tag-picker-new summary:hover {
  background: var(--accent-soft);
}

.tag-picker-new[open] summary {
  margin-bottom: var(--space-2);
}

.tag-picker-new input[type="text"] {
  display: block;
  width: 100%;
  max-width: 24rem;
}

.tag-picker-hint {
  display: block;
  max-width: 34rem;
  margin-top: var(--space-1);
  font-size: var(--text-xs);
  color: var(--text-faint);
}

/*
  Lightbox variant: the review-pass row of person chips under the photo.
  Buttons rather than checkboxes -- the lightbox has no <form>, so each tap
  POSTs to the tag-toggle endpoint (static/js/tag-toggle.js). Kept in this
  section because it is the same component, not a lightbox concern.
*/

.lightbox-people {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  width: 100%;
  padding: 0 var(--space-3);
}

.lightbox-person {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.25rem;
  padding: var(--space-1) var(--space-3) var(--space-1) var(--space-1);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.lightbox-person:hover {
  background: rgba(255, 255, 255, 0.26);
  filter: none;
}

.lightbox-person[aria-pressed="true"] {
  color: #10222f;
  background: #fff;
  border-color: #fff;
}

.lightbox-person .picker-chip-avatar {
  width: 1.6rem;
  height: 1.6rem;
  font-size: var(--text-xs);
  color: #10222f;
  background: rgba(255, 255, 255, 0.85);
}

.lightbox-person[aria-pressed="true"] .picker-chip-avatar {
  color: #fff;
  background: var(--accent);
}

.lightbox-person[data-busy="1"] {
  opacity: 0.5;
  pointer-events: none;
}


/* ---------- 14. Gallery multi-select (batch tagging) ---------- */

.tile-checkbox {
  position: absolute;
  top: var(--space-1);
  left: var(--space-1);
  z-index: 2;
  display: none;
}

#batch-tag-form.selecting .tile-checkbox {
  display: block;
}

#batch-tag-form.selecting .tile-day-badge {
  display: none;
}

#batch-tag-form.selecting .gallery-tile {
  cursor: pointer;
}

.tile-checkbox input[type="checkbox"] {
  width: 1.5rem;
  height: 1.5rem;
  accent-color: var(--accent);
}

.gallery-tile.tile-selected {
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}

.batch-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
  background: var(--surface);
  border-top: 1px solid var(--border);
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
}

.batch-bar[hidden] {
  display: none;
}

.batch-bar-count {
  flex: 0 0 auto;
  font-weight: 600;
  color: var(--text);
}

.batch-bar {
  flex-direction: column;
  align-items: stretch;
}

.batch-bar-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}

/* Caps how much of the grid the bar can cover: with many tags the picker
   scrolls inside the bar instead of pushing the photos you are selecting
   off screen. */
.batch-bar-picker {
  max-height: 38vh;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.batch-bar-picker .tag-picker {
  margin: var(--space-2) 0 0;
  gap: var(--space-3);
}

/* ---------- 15. Detail page: date/time correction (admin only) ---------- */

.link-button {
  display: inline;
  min-height: auto;
  min-width: auto;
  padding: 0;
  margin-left: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--accent);
  background: transparent;
  border: none;
  text-decoration: underline;
  cursor: pointer;
}

.date-edit-form {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.date-edit-form[hidden] {
  display: none;
}

.date-edit-form label {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 500;
  flex-basis: 100%;
}

.date-edit-form input[type="datetime-local"] {
  min-height: var(--tap-min);
  font-size: var(--text-base);
  font-family: inherit;
  padding: var(--space-2) var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

/* ---------- 16. Error pages ---------- */

.error-page {
  max-width: 32rem;
  margin: var(--space-6) auto;
  text-align: center;
}

.error-page-code {
  font-size: var(--text-2xl);
  font-weight: 700;
  margin: 0 0 var(--space-2);
  color: var(--text-muted);
}

/* ---------- 17. Lightbox ---------- */

.lightbox-overlay {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.92);
}

.lightbox-overlay[hidden] {
  display: none;
}

.lightbox-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  touch-action: pan-y pinch-zoom;
}

.lightbox-stage img,
.lightbox-stage video {
  max-width: 100%;
  max-height: 85vh;
  width: auto;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}

.lightbox-close {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  /* Reset the base button rule's text-oriented padding (section 6) --
     without this, an icon-only button grows well past min-height/width
     instead of being sized by it, and the glyph sits off-centre. */
  padding: 0;
  font-size: var(--text-2xl);
  line-height: 1;
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: calc(var(--tap-min) * 1.2);
  min-width: calc(var(--tap-min) * 1.2);
  padding: 0;
  font-size: var(--text-2xl);
  line-height: 1;
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
  border: none;
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.lightbox-nav[hidden] {
  display: none;
}

.lightbox-prev {
  left: var(--space-3);
}

.lightbox-next {
  right: var(--space-3);
}

.lightbox-footer {
  position: absolute;
  bottom: var(--space-3);
  left: 0;
  right: 0;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  row-gap: var(--space-2);
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  padding: 0 var(--space-5);
}

.lightbox-caption {
  /* Own full-width line so it can't be squeezed sideways by the icon row
     or the person-tag chips appended after it (tag-toggle.js). */
  flex: 1 1 100%;
  min-width: 0;
  margin: 0;
  text-align: center;
  font-size: var(--text-sm);
  color: rgba(255, 255, 255, 0.85);
}

.lightbox-footer .favorite-toggle,
.lightbox-edit,
.lightbox-delete {
  flex-shrink: 0;
  /* .favorite-toggle's own rule (section 18) sets a larger padding sized
     for an icon+label pair; the lightbox only ever renders the icon, so
     reset to 0 here for the same reason as .lightbox-close/.lightbox-nav
     above -- an icon-only button should be sized by min-height/width, not
     grown by padding meant for text. */
  padding: 0;
  color: #fff;
  background: rgba(255, 255, 255, 0.12);
}

.lightbox-edit,
.lightbox-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  font-size: var(--text-lg);
  line-height: 1;
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  text-decoration: none;
  cursor: pointer;
}

.lightbox-delete[hidden] {
  display: none;
}

@media (min-width: 900px) {
  .lightbox-close,
  .lightbox-nav {
    color: var(--text);
    background: var(--surface);
  }

  .lightbox-footer .favorite-toggle,
  .lightbox-edit,
  .lightbox-delete {
    color: var(--text);
    background: var(--surface);
  }
}

/* Delete confirm dialog, shared by the lightbox's delete button. A small
   centered modal (unlike the bottom-sheet .menu-sheet/.filter-sheet), so
   its own rules rather than reusing those. */
.confirm-dialog {
  max-width: 22rem;
  margin: auto;
  padding: var(--space-5);
  border: none;
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
}

.confirm-dialog::backdrop {
  background: rgba(0, 0, 0, 0.4);
}

.confirm-dialog p {
  margin: 0 0 var(--space-4);
}

.confirm-dialog-actions {
  display: flex;
  gap: var(--space-3);
}

/* ---------- 18. Favourites ---------- */

.favorite-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  min-width: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-lg);
  line-height: 1;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  cursor: pointer;
}

.favorite-toggle-label {
  font-size: var(--text-sm);
  font-weight: 500;
}

.favorite-toggle-active {
  color: #c0392b;
  border-color: #c0392b;
}

.detail-favorite-form {
  position: absolute;
  bottom: var(--space-3);
  right: var(--space-3);
  margin: 0;
}

.detail-media {
  position: relative;
}

.tile-fav-heart {
  position: absolute;
  top: var(--space-1);
  right: var(--space-1);
  z-index: 1;
  font-size: var(--text-base);
  color: #ff5c4d;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* ---------- 19. Filter sheet ---------- */

.filter-sheet-backdrop {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0.4);
}

.filter-sheet-backdrop[hidden] {
  display: none;
}

.filter-sheet {
  width: 100%;
  max-height: 85vh;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-4) var(--space-4) var(--space-6);
  background: var(--surface);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  box-shadow: var(--shadow-lg);
}

/* Same zero-JS trick as .tab-bar/#batch-bar above: lock background scroll
   while the sheet is open instead of letting a drag on the backdrop
   scroll-chain into the gallery behind it. */
body:has(.filter-sheet-backdrop:not([hidden])) {
  overflow: hidden;
}

.filter-sheet-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-3);
}

.filter-sheet-header h2 {
  margin: 0;
  font-size: var(--text-lg);
}

.filter-sheet label {
  display: block;
  margin-top: var(--space-3);
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  font-weight: 500;
}

.filter-sheet input[type="search"] {
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

.filter-sheet-checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  margin-top: var(--space-3);
}

.filter-sheet-checkbox input {
  width: 20px;
  height: 20px;
}

.filter-sheet-months {
  margin: var(--space-4) 0 0;
  padding: 0;
  border: none;
}

.filter-sheet-months legend {
  padding: 0;
  font-size: var(--text-sm);
  font-weight: 500;
}

.filter-sheet-month-list {
  max-height: 40vh;
  overflow-y: auto;
  margin-top: var(--space-2);
}

.filter-sheet-month-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap-min);
  padding: var(--space-1) 0;
  font-size: var(--text-base);
  font-weight: 400;
}

.filter-sheet-month-option input {
  width: 20px;
  height: 20px;
}

.filter-sheet-date-range {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.filter-sheet-date-field {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-base);
}

.filter-sheet-date-range input[type="date"] {
  min-height: var(--tap-min);
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-base);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
}

.filter-sheet-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: var(--space-5);
}

.filter-sheet-actions button {
  min-height: var(--tap-min);
  padding: var(--space-2) var(--space-5);
}

@media (min-width: 900px) {
  .filter-sheet-backdrop {
    align-items: flex-start;
    justify-content: flex-end;
    background: transparent;
  }

  .filter-sheet {
    width: 360px;
    max-height: calc(100vh - var(--space-7) - var(--space-4) * 2);
    margin: calc(var(--space-7) + var(--space-4)) var(--space-4) 0 0;
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }
}

/* ---------- 20. Infinite scroll ---------- */

/* Zero visual footprint, but must keep real layout (never display:none) so
   IntersectionObserver can compute its position and fire. */
#gallery-sentinel {
  height: 1px;
}

.gallery-end {
  margin: var(--space-5) 0;
  text-align: center;
  color: var(--text-faint);
  font-size: var(--text-sm);
}

.month-header-count {
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--text-faint);
}

/* ---------- 21. Empty state ---------- */

.empty-state {
  padding: var(--space-6) var(--space-4);
  text-align: center;
  color: var(--text-muted);
}

.empty-state a {
  color: var(--accent);
}

/* ---------- 22. Upload drop zone (progressive enhancement over <input type=file>) ---------- */

.upload-dropzone {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 9rem;
  padding: var(--space-4);
  text-align: center;
  border: 2px dashed var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition: border-color var(--dur-fast) var(--ease), background var(--dur-fast) var(--ease);
}

/* Once files are picked: a slim "add more" strip, so the tags and the
   Upload button stay right under it instead of a screen further down. */
.upload-dropzone-compact {
  min-height: 3.5rem;
  padding: var(--space-2) var(--space-4);
}

.upload-dropzone-active {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.upload-dropzone-text {
  margin: 0;
  font-size: var(--text-base);
  color: var(--text);
  pointer-events: none;
}

.upload-dropzone-hint {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* The real <input> sits on top, stretched to fill the zone and fully
   transparent, so both a tap and a native drag-and-drop still hit it. */
.upload-dropzone-input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  min-height: 0;
  opacity: 0;
  cursor: pointer;
  border: none;
}

.upload-file-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.upload-file-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.upload-file-preview {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.upload-file-preview img,
.upload-file-preview video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.upload-file-meta {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-1) var(--space-2);
  min-width: 0;
}

.upload-file-name {
  flex: 1 1 100%;
  overflow: hidden;
  font-size: var(--text-sm);
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}

.upload-file-size {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* No bars on the pick screen: an empty bar there reads as "uploading
   already". They appear once the list moves to the progress screen. */
#upload-form .upload-file-progress {
  display: none;
}

.upload-selection-heading {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-base);
  color: var(--text-muted);
}

.upload-selection-heading[hidden] {
  display: none;
}

.upload-file-progress {
  flex: 1 1 100%;
  height: 6px;
  border: none;
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.upload-file-progress::-webkit-progress-bar {
  background: var(--surface-sunken);
  border-radius: var(--radius-pill);
}

.upload-file-progress::-webkit-progress-value {
  background: var(--accent);
  border-radius: var(--radius-pill);
}

.upload-file-progress::-moz-progress-bar {
  background: var(--accent);
  border-radius: var(--radius-pill);
}

/* Server is still processing (hashing/thumbnailing/probing) after the
   browser has finished sending bytes -- pulse the bar so it doesn't read as
   "stopped" or "done" while nothing is left to animate off upload progress. */
.upload-file-item.upload-file-processing .upload-file-progress::-webkit-progress-value,
.upload-file-item.upload-file-processing .upload-file-progress::-moz-progress-bar {
  animation: upload-file-processing-pulse 1.2s ease-in-out infinite;
}

@keyframes upload-file-processing-pulse {
  0%, 100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

.upload-file-result {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.upload-file-item.upload-file-ok .upload-file-result {
  color: var(--success-text);
}

.upload-file-item.upload-file-error .upload-file-result {
  color: var(--error-text);
}

/* A transport abort or 5xx got an automatic retry queued -- distinct from
   the permanent .upload-file-error state above, which never retries. */
.upload-file-item.upload-file-retrying .upload-file-result {
  color: var(--warning-text);
}

.upload-file-note {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--info-border);
  border-radius: var(--radius);
  background: var(--info-bg);
  color: var(--info-text);
  font-size: var(--text-sm);
}

.upload-file-remove {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2rem;
  min-width: 2rem;
  padding: 0;
  font-size: var(--text-base);
  line-height: 1;
  color: var(--text-muted);
  background: transparent;
  border-radius: var(--radius-pill);
}

.upload-file-remove:hover {
  background: var(--border);
  filter: none;
}

/* Shown after a batch settles, only when one or more files exhausted all
   automatic retries (upload.js's showSummary) -- re-queues just those rows.
   [hidden] needs restating: the blanket `button { display: inline-flex }`
   would otherwise beat the UA rule (same as #push-enable-btn above). */
.upload-retry-failed[hidden] {
  display: none;
}

.upload-file-item.upload-file-waiting {
  opacity: 0.7;
}

/* The dedicated progress screen (upload.js swaps the picker form for it the
   moment Upload is tapped). */
.upload-progress-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-xl);
}

.upload-progress-bar {
  display: block;
  width: 100%;
  height: 10px;
  border: none;
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.upload-progress-bar::-webkit-progress-bar {
  background: var(--surface-sunken);
}

.upload-progress-bar::-webkit-progress-value {
  background: var(--accent);
}

.upload-progress-bar::-moz-progress-bar {
  background: var(--accent);
}

.upload-progress-hint {
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--info-border);
  border-radius: var(--radius);
  background: var(--info-bg);
  color: var(--info-text);
  font-size: var(--text-sm);
}

.upload-progress-hint[hidden] {
  display: none;
}

.upload-progress-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

.upload-progress-actions[hidden] {
  display: none;
}

/* A link that looks like the primary button -- for navigation actions that
   belong next to real buttons (the upload summary's "View & tag these"). */
.button-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-min);
  padding: var(--space-3) var(--space-5);
  font-size: var(--text-base);
  font-weight: 600;
  text-decoration: none;
  color: var(--accent-contrast);
  background: var(--accent);
  border-radius: var(--radius);
}

.button-link[hidden] {
  display: none;
}

.button-link-secondary {
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
}

/* ---------- 23. Activity feed ---------- */

.activity-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.activity-row {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3);
  border-radius: var(--radius);
  background: var(--surface);
}

.activity-row-new {
  box-shadow: inset 3px 0 0 var(--accent);
}

.activity-avatar {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-lg);
  font-weight: 600;
}

.activity-body {
  flex: 1 1 auto;
  min-width: 0;
}

.activity-summary {
  margin: 0;
  font-size: var(--text-base);
}

.activity-new-badge {
  display: inline-block;
  margin-right: var(--space-2);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 600;
  vertical-align: middle;
}

.activity-time {
  margin: var(--space-1) 0 var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.activity-preview-strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 4rem;
  gap: var(--space-2);
  overflow-x: auto;
}

.activity-preview-strip img,
.activity-preview-placeholder {
  width: 4rem;
  height: 4rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
}

.activity-preview-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-sunken);
  color: var(--text-muted);
}

/* --- Notifications page (UX-7) --- */

.notifications-page {
  max-width: 32rem;
  margin: 0 auto;
  padding: var(--space-4) 0;
}

.notifications-ios-prereq {
  background: var(--surface-sunken);
  border-radius: var(--radius);
  padding: var(--space-4);
  line-height: var(--leading-normal);
}

.notifications-controls {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
}

/* --- Help page --- */

.help-page {
  max-width: 32rem;
  margin: 0 auto;
  padding: var(--space-4) 0;
}

.help-page section {
  margin-bottom: var(--space-6);
}

.help-page section:last-child {
  margin-bottom: 0;
}

/* --- Manage members page --- */

.manage-members-page {
  max-width: 32rem;
  margin: 0 auto;
  padding: var(--space-4) 0;
}

.member-list {
  list-style: none;
  margin: 0 0 var(--space-6);
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Adopts the app's card pattern (see .setup-card) instead of a flat,
   borderless surface. */
.member-row {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.member-avatar {
  flex: none;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-lg);
  font-weight: 600;
}

.member-body {
  flex: 1 1 auto;
  min-width: 0;
}

.member-name {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.member-status-badge {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 600;
  vertical-align: middle;
}

.member-status-badge--active {
  background: var(--success-bg);
  color: var(--success-text);
}

.member-status-badge--invited {
  background: var(--warning-bg);
  color: var(--warning-text);
}

.member-status-badge--removed {
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.member-email {
  margin: var(--space-1) 0 var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.member-nickname-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* Reset from the global label rhythm is intentional: this label sits
   inline next to its input/select in a single flex row, not stacked above
   it like the rest of the app's forms. */
.member-nickname-form label {
  margin: 0;
}

.member-nickname-form input[type="text"] {
  min-width: 0;
  flex: 1 1 10rem;
}

/* Role + Remove live in one block, visually separated from the nickname
   editor above by a divider (same border-top technique as
   .setup-card-step). Stacked (not side-by-side): Remove sits underneath
   Role rather than competing with it in the same row, which is what let
   Role's own contents wrap apart from each other at narrow widths. */
.member-row-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border);
  width: 100%;
}

.member-row-actions .field-help {
  margin: 0;
}

/* --- Highlight reels page --- */

.highlight-reels-page {
  max-width: 32rem;
  margin: 0 auto;
  padding: var(--space-4) 0;
}

.highlight-reel-request-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.highlight-reel-request-field label {
  display: block;
  margin-bottom: var(--space-1);
  font-weight: 500;
}

.highlight-reel-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.highlight-reel-row {
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.highlight-reel-title {
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
}

.highlight-reel-status-badge {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 600;
  vertical-align: middle;
}

.highlight-reel-status-badge--pending,
.highlight-reel-status-badge--rendering {
  background: var(--warning-bg);
  color: var(--warning-text);
}

.highlight-reel-status-badge--ready,
.highlight-reel-status-badge--added {
  background: var(--success-bg);
  color: var(--success-text);
}

.highlight-reel-status-badge--failed {
  background: var(--error-bg);
  color: var(--error-text);
}

.highlight-reel-status-badge--discarded {
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.highlight-reel-preview {
  display: block;
  width: 100%;
  max-width: 20rem;
  margin: var(--space-3) 0;
  border-radius: var(--radius-sm);
}

.highlight-reel-error {
  color: var(--error-text);
  font-size: var(--text-sm);
}

.highlight-reel-row-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.member-role-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  width: 100%;
}

.member-role-form label {
  margin: 0;
}

/* Select + Save are grouped in their own row (nowrap) so Save can never
   wrap away from the select it belongs to -- the label is free to sit on
   its own line above instead. */
.member-role-control {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--space-2);
}

.member-role-control .select-wrap {
  flex: 1 1 auto;
  min-width: 0;
}

.member-remove-form {
  margin: 0;
}

.member-resend-form {
  margin-top: var(--space-2);
}

/* --- Add member form --- */

.member-add-form {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 var(--space-4);
  margin: var(--space-5) 0;
}

.member-add-field {
  margin: 0;
}

.member-add-form button[type="submit"] {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: var(--space-2);
}

@media (min-width: 480px) {
  .member-add-form {
    grid-template-columns: 1fr 1fr;
  }

  .member-add-field--full {
    grid-column: 1 / -1;
  }
}

/* ---------- 25. Setup card (first-run install / notifications) ---------- */

/*
  Quiet on purpose. This sits above the family's photos on the page they open
  most, so it reads as a card on --surface rather than an alert: no accent
  fill, no coloured left border like .message, and it removes itself step by
  step as each one is done (setup-card.js owns every [hidden] here).
*/

.setup-card {
  margin: var(--space-4) 0;
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.setup-card[hidden] {
  display: none;
}

.setup-card-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  line-height: var(--leading-tight);
}

.setup-card-step {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-top: 1px solid var(--border);
}

.setup-card-step[hidden] {
  display: none;
}

.setup-card-step-text {
  flex: 1 1 14rem;
  margin: 0;
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
}

.setup-card-step-text strong {
  display: inline-block;
  font-size: var(--text-base);
  color: var(--text);
}

/*
  The iOS branch: instructions, not a button, because Safari has no
  programmatic install. Full-width under the step text rather than beside it
  — it's a sentence, and squeezing it into a flex row next to the label made
  it unreadable at 360px.
*/
.setup-card-hint {
  flex: 1 1 100%;
  margin: 0;
  padding: var(--space-3);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-muted);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
}

.setup-card-hint[hidden] {
  display: none;
}

/* Empty until setup-card.js writes to it; must not reserve space when blank. */
.setup-card-status:empty {
  display: none;
}

.setup-card-status {
  margin: var(--space-3) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.setup-card-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-2);
}
