/* frontend/style.css */

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

body {
  font-family: var(--sys-font-body);
  background: var(--color-surface);
  color: var(--color-text-primary);
  min-height: 100vh;
}

[hidden] { display: none !important; }

:root {
  /* Compatibility shim: existing style.css uses --color-(name) vars.
     Map them onto Cornerstone --sys / --cmp tokens here so the rest of
     the file needs no per-rule edits. Raw hex literals elsewhere are still
     converted in a later task. */
  --color-primary:          var(--sys-primary);
  --color-primary-hover:    var(--sys-primary-hover);
  --color-primary-active:   var(--sys-primary-pressed);
  --color-primary-muted:    var(--sys-primary-container);
  --color-primary-ring:     var(--sys-outline-focus);
  --color-accent-warm:      var(--sys-secondary);
  --color-surface:          var(--sys-surface);
  --color-surface-elevated: var(--sys-surface-variant);
  --color-surface-inset:    var(--sys-surface-container);
  --color-surface-modal:    var(--cmp-modal-surface);
  --color-surface-item:     var(--sys-surface-container);
  --color-border:           var(--sys-outline);
  --color-border-input:     var(--cmp-input-outline);
  --color-text-primary:     var(--sys-text-primary);
  --color-text-secondary:   var(--sys-text-secondary);
  --color-text-muted:       var(--sys-text-secondary);
  --color-text-dim:         var(--sys-text-disabled);
  /* These were referenced in style.css but NEVER defined in the old :root
     (latent bugs — resolved to nothing). Define them on the shim. */
  --color-text:             var(--sys-text-primary);
  --color-bg:               var(--sys-background);
  --color-input-bg:         var(--cmp-input-bg);
}

/* ── Login ───────────────────────────────────────────────── */
#login-view {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
}

.login-card {
  background: var(--color-surface-elevated);
  border-radius: 8px;
  padding: 2rem;
  width: 100%;
  max-width: 360px;
}

.login-card h1 {
  margin-bottom: 1.5rem;
  font-size: 1.5rem;
  text-align: center;
  letter-spacing: 0.05em;
}

.login-card label {
  display: block;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
  color: var(--color-text-secondary);
}

.login-card input {
  display: block;
  width: 100%;
  padding: 0.5rem 0.75rem;
  margin-bottom: 1rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 1rem;
}

.login-card input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-ring);
}

/* Three roles, so a control's weight says what it does. Selecting on
   [type="submit"] alone is what left every type="button" control in this card
   rendering as a raw browser default with no padding. */
.login-card button[type="submit"],
.login-card .btn-primary {
  width: 100%;
  padding: 0.6rem;
  background: var(--color-primary);
  border: none;
  border-radius: 4px;
  color: var(--sys-on-primary);
  font-size: 1rem;
  cursor: pointer;
}

.login-card button[type="submit"]:hover,
.login-card .btn-primary:hover { background: var(--color-primary-hover); }

/* Secondary: reveals a form rather than committing anything. Same size and
   shape as primary so the two read as siblings, outlined rather than filled so
   only one action per block looks like the one to press. */
.login-card .btn-secondary {
  width: 100%;
  padding: 0.6rem;
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 1rem;
  cursor: pointer;
}
.login-card .btn-secondary:hover { border-color: var(--color-primary); }

/* .linklike is the third role, defined below: inline, no weight, for Remove and
   for leaving without doing anything. */

.error { color: var(--sys-danger-text); font-size: 0.85rem; margin-top: 0.5rem; }

/* TOTP enrollment */
/* .modal-box brings `display:flex; gap:1rem`, which would stack on top of the
   margin .mfa-method already carries and space the two options twice. The card
   keeps its own spacing; the modal contributes only the centring, the width
   cap and the scroll. */
#enroll-view .login-card.modal-box {
  max-width: 420px;
  gap: 0;
  padding: 2rem;
}
.enroll-qr { display: flex; justify-content: center; margin: 1rem 0; }
.enroll-qr img { background: var(--sys-on-light); padding: 0.5rem; border-radius: 4px; display: block; }
.enroll-secret { font-size: 0.85rem; color: var(--color-text-secondary); word-break: break-all; }
.enroll-secret code { color: var(--color-text-primary); }
.enroll-recovery-codes {
  list-style: none;
  margin: 1rem 0;
  padding: 0.75rem;
  background: var(--color-surface-inset);
  border-radius: 4px;
  font-family: monospace;
  columns: 2;
}
.enroll-recovery-codes li { padding: 0.15rem 0; }

/* The two MFA methods on the enrollment card, passkey first. Each is a boxed
   block rather than a run of headings, because when they shared one flow the
   second heading read as a step of the first. */
.mfa-method {
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 1rem;
  margin-bottom: 1rem;
}
.mfa-method h3 {
  margin: 0 0 0.25rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
/* The preferred method earns the accent border; the alternative stays quiet. */
.mfa-method-preferred { border-color: var(--color-primary); }
.mfa-badge {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0.15rem 0.45rem;
  border-radius: 999px;
  background: var(--color-primary);
  color: var(--sys-on-primary);
  white-space: nowrap;
}
/* The MFA policy applies to the card, not to either method, so it sits above
   both. Informational rather than an error: nothing has gone wrong. */
.mfa-policy-note {
  font-size: 0.85rem;
  color: var(--color-text-secondary);
  background: var(--color-surface-inset);
  border-radius: 4px;
  padding: 0.6rem 0.75rem;
  margin-bottom: 1rem;
}

/* A status, not a recommendation, so it does not compete with the accent badge
   on the preferred method. */
.mfa-badge-on {
  background: none;
  border: 1px solid var(--color-border);
  color: var(--color-text-secondary);
}
.mfa-method-hint {
  font-size: 0.85rem;
  color: var(--color-text-secondary);
  margin: 0 0 0.75rem;
}
/* While the passkey form is open, no other credential field on the card is
   reachable. Set as a class, not the hidden attribute, so removing it restores
   each field's own state -- a forced user's disable form stays hidden either
   way. */
#enroll-view.mfa-focus-passkey #enroll-disable-form,
#enroll-view.mfa-focus-passkey #enroll-disable-recovery-details,
#enroll-view.mfa-focus-passkey #enroll-totp-reveal-btn { display: none; }
.passkey-list { list-style: none; margin: 0 0 0.75rem; padding: 0; }
/* The name and the Remove control were adjacent inline elements, so they
   rendered with no gap at all -- "1PasswordRemove" read as one word. */
.passkey-list li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--color-surface-inset);
  border-radius: 4px;
}
.passkey-list li + li { margin-top: 0.35rem; }
.passkey-list li.passkey-empty {
  background: none;
  padding: 0;
  color: var(--color-text-secondary);
  font-size: 0.85rem;
}
.passkey-name { font-weight: 500; word-break: break-word; }
.passkey-meta {
  display: block;
  font-size: 0.75rem;
  color: var(--color-text-secondary);
  font-weight: 400;
}
.linklike {
  background: none;
  border: none;
  color: var(--color-text-secondary);
  text-decoration: underline;
  cursor: pointer;
  font-size: 0.85rem;
}

/* ── Gallery header ──────────────────────────────────────── */
#back-btn, #admin-back-btn {
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
  border-radius: 4px;
  transition: color 0.15s;
}
#back-btn:hover, #admin-back-btn:hover { color: var(--color-text); }

.gallery-header {
  display: flex;
  flex-direction: column;
  padding: 0.5rem 1rem;
  background: var(--color-surface-elevated);
  border-bottom: 1px solid var(--color-border);
  position: sticky;
  top: 0;
  z-index: 10;
  gap: 0.4rem;
}

/* Row 1: brand + action icons */
.header-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.site-name {
  font-weight: 600;
  font-size: 1.1rem;
  margin-right: auto;
  white-space: nowrap;
}

/* Action buttons (Upload/Admin/Sign out — replaced by ☰ in Task 2) */
.header-actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-shrink: 0;
}

/* Small icon-only buttons */
.icon-btn {
  padding: 0.3rem 0.5rem;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  color: var(--color-text-secondary);
  font-size: 1rem;
  cursor: pointer;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Google's stylesheet used to supply this base rule alongside the font
   itself; self-hosting the font means the class needs its own declaration.
   The sans-serif fallback is deliberate: if the woff2 ever fails to load,
   the span's textContent (e.g. "delete_forever") reads as plain words
   instead of tofu. */
.material-symbols-outlined {
  font-family: 'Material Symbols Outlined', sans-serif;
  font-weight: normal;
  font-style: normal;
  font-size: 24px;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  display: inline-block;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
}

.icon-btn .material-symbols-outlined {
  font-size: 20px;
  font-variation-settings: 'opsz' 20, 'wght' 400, 'FILL' 0, 'GRAD' 0;
  line-height: 1;
  user-select: none;
}
.icon-btn:hover {
  background: var(--color-surface-inset);
  border-color: var(--color-border-input);
  color: var(--color-text-primary);
}
.icon-btn[aria-expanded="true"] {
  border-color: var(--color-primary);
  box-shadow: 0 0 0 2px var(--color-primary-ring);
}

/* ── Hamburger nav menu ───────────────────────────────────── */
.nav-menu-wrap {
  position: relative;
}

.nav-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  background: var(--color-surface-elevated);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  box-shadow: var(--cmp-elevation-2);
  min-width: 150px;
  z-index: 50;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

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

.nav-menu-item {
  width: 100%;
  padding: 0.6rem 1rem;
  background: transparent;
  border: none;
  color: var(--color-text-primary);
  font-size: 0.9rem;
  text-align: left;
  cursor: pointer;
}

.nav-menu-item:hover { background: var(--color-surface-inset); }

.nav-menu-item--danger { color: var(--sys-danger-text); }
.nav-menu-item--danger:hover { background: color-mix(in srgb, var(--sys-danger) 12%, transparent); }

/* Row 2: filter controls — always visible */
.controls-filters {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: nowrap;
}

.controls-filters select {
  flex: 1 1 0;
  min-width: 0;
}

/* Row 3: search — hidden until toggled */
.controls-search {
  display: none;
  align-items: center;
  gap: 0.4rem;
}
.gallery-header.search-open .controls-search { display: flex; }

.controls-search input[type="search"] {
  flex: 1;
  min-width: 0;
  padding: 0.4rem 0.75rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 0.9rem;
}
.controls-search input[type="search"]:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-ring);
}

/* Shared styles for filter selects and buttons */
.controls-filters select,
.controls-filters button,
.controls-search button:not(.icon-btn) {
  padding: 0.4rem 0.75rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 0.9rem;
  cursor: pointer;
  white-space: nowrap;
}
.controls-filters select { cursor: pointer; min-width: 0; }

@media (max-width: 600px) {
  .controls-filters select,
  .controls-filters button:not(.icon-btn) {
    padding: 0.35rem 0.3rem;
    font-size: 0.8rem;
  }
}

.controls-filters button:hover,
.controls-search button:not(.icon-btn):hover {
  background: var(--color-border);
}
.controls-filters select:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-ring);
  background: var(--color-surface-inset);
}

/* ── Photo grid ──────────────────────────────────────────── */
main { padding: 1rem; }

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 6px;
}

.photo-card {
  cursor: pointer;
  border-radius: var(--cmp-photo-radius);
  overflow: hidden;
  background: var(--color-surface-elevated);
  transition: transform 0.15s, box-shadow 0.15s;
  aspect-ratio: 1;
}

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

.photo-card:focus-visible {
  outline: 2px solid var(--color-primary-ring);
  outline-offset: 2px;
}

.photo-card:hover {
  opacity: 0.85;
  transform: translateY(-2px);
  box-shadow: var(--cmp-elevation-3);
}

.photo-section-header {
  grid-column: 1 / -1;
  padding: 0.5rem 0 0.25rem;
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--color-text-muted);
  letter-spacing: -0.02em;
  border-bottom: 1px solid var(--color-border);
  margin-bottom: 0.25rem;
}

.video-thumb-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  height: 100%;
  background: var(--color-surface-modal);
  color: var(--color-text-secondary);
  font-size: 0.72rem;
  text-align: center;
  padding: 0.5rem;
  overflow: hidden;
}
.video-thumb-placeholder::before {
  content: '▶';
  font-size: 1.8rem;
  opacity: 0.6;
}

#load-more-container { text-align: center; padding: 1.5rem; }

#scroll-sentinel { height: 1px; }

#end-of-gallery { color: var(--color-text-dim); font-size: 0.9rem; }

#no-results { color: var(--color-text-dim); font-size: 0.95rem; padding: 2rem 0; text-align: center; }

/* ── Year pill (jump-to-year) ─────────────────────────────── */
/* Floating pill + picker, all viewports. Replaced the desktop date scrubber. */
.year-pill {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  background: var(--color-surface-elevated);
  border: 1px solid var(--color-border);
  border-radius: 20px;
  padding: 0.35rem 0.75rem;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--color-text-primary);
  cursor: pointer;
  z-index: 20;
  box-shadow: var(--cmp-elevation-1);
  user-select: none;
}
.year-pill:hover { background: var(--color-surface-inset); }
/* Hide while searching or filtered: the jump pages the unfiltered gallery and
   would silently drop the filter (setSearchMode toggles .filtering). */
.gallery-header.search-open ~ .year-pill,
.gallery-header.filtering ~ .year-pill { display: none; }

.year-picker {
  position: fixed;
  bottom: 3.5rem;
  right: 1rem;
  background: var(--color-surface-elevated);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  box-shadow: var(--cmp-elevation-2);
  max-height: 50vh;
  overflow-y: auto;
  z-index: 21;
  min-width: 80px;
  display: flex;
  flex-direction: column;
}
.year-picker[hidden] { display: none; }
.year-picker-item {
  padding: 0.5rem 1rem;
  font-size: 0.9rem;
  color: var(--color-text-primary);
  cursor: pointer;
  text-align: center;
  border: none;
  background: transparent;
}
.year-picker-item:hover { background: var(--color-surface-inset); }
.year-picker-item.active { font-weight: 700; color: var(--color-primary); }

/* ── Detail view ─────────────────────────────────────────── */
.detail-main { display: flex; flex-direction: column; min-height: calc(100vh - 56px); }

.detail-image-wrap {
  position: relative;
  width: 100%;
  background: var(--sys-photo-backdrop);
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
}

.detail-image-wrap img,
.detail-image-wrap video {
  max-width: 100%;
  max-height: 90vh;
  object-fit: contain;
}

/* Manual rotation (photos.rotation). Clockwise, on top of whatever EXIF
   orientation the browser already applied. */
#detail-img[data-rotation="90"]  { transform: rotate(90deg); }
#detail-img[data-rotation="180"] { transform: rotate(180deg); }
#detail-img[data-rotation="270"] { transform: rotate(270deg); }

/* A quarter turn swaps the axes, so the pre-rotation width is what the height
   budget has to cover. Paired with a JS-set wrap height: transforms don't
   reflow, so the container would otherwise keep its un-rotated height and the
   turned photo would spill over the metadata panel below. */
#detail-img[data-rotation="90"],
#detail-img[data-rotation="270"] {
  max-width: 90vh;
  max-height: 100vw;
}

.detail-nav-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 5;
  background: var(--sys-scrim);
  border: 1px solid var(--sys-divider);
  border-radius: 50%;
  width: 48px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  color: var(--color-text-primary);
  opacity: 0;
  transition: opacity 0.2s ease, background 0.2s ease;
  backdrop-filter: blur(4px);
}

.detail-nav-prev { left: 1.5rem; }
.detail-nav-next { right: 1.5rem; }
.detail-image-wrap:hover .detail-nav-btn:not(:disabled) { opacity: 1; }
.detail-nav-btn:hover { background: color-mix(in srgb, var(--sys-info) 50%, transparent); border-color: var(--color-primary); }
.detail-nav-btn:disabled { opacity: 0 !important; pointer-events: none; }
/* Keyboard users can't hover; reveal + ring the prev/next buttons on focus. */
.detail-nav-btn:focus-visible {
  opacity: 1;
  outline: 3px solid var(--sys-outline-focus);
  outline-offset: 2px;
}

/* Labels-first layout: a single vertical column under the photo —
   header row, Activities, Labels, then the EXIF "Photo details" footnote. */
.detail-meta {
  background: var(--color-surface-elevated);
  padding: 1.5rem 2rem;
  border-top: 1px solid var(--color-border);
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

/* (a) Header row: filename + date left, Share/Delete right. */
.detail-meta-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.detail-meta-title { min-width: 0; flex: 1; }
/* Space the Share/Delete icons apart so they're not easy to misclick. */
.detail-actions { display: flex; align-items: center; gap: 1.25rem; flex-shrink: 0; }
.detail-share-panel { font-size: 0.875rem; }
.detail-delete-confirm { font-size: 0.875rem; }

.detail-meta h2 {
  font-size: 1rem;
  font-weight: 600;
  word-break: break-all;
  margin-bottom: 0.25rem;
}

.detail-subtitle { font-size: 0.85rem; color: var(--color-text-muted); }

.detail-section { min-width: 140px; }

.detail-meta h3 {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-text-dim);
  margin-bottom: 0.5rem;
}

/* Count next to the Labels heading. */
.detail-count {
  font-weight: 600;
  color: var(--color-text-muted);
  letter-spacing: 0;
}

.detail-meta ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.detail-meta li {
  font-size: 0.9rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}

/* (b) Activities: folder-derived events — distinct from labels, slightly more
   prominent. Wrapping row of chips with a folder cue, primary-tinted. */
.activity-chips {
  flex-direction: row !important;
  flex-wrap: wrap;
  gap: 0.4rem !important;
}
.activity-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--color-primary-muted);
  color: var(--color-text-primary);
  border: 1px solid var(--color-primary);
  border-radius: 14px;
  padding: 0.3rem 0.7rem;
  font-size: 0.9rem;
  font-weight: 600;
}
.activity-chip-icon {
  font-size: 18px;
  line-height: 1;
  font-variation-settings: 'opsz' 18, 'wght' 400, 'FILL' 0, 'GRAD' 0;
  user-select: none;
}
/* Name chips that filter the gallery. Only the padding belongs to the <li>;
   the button fills it so the whole chip is the hit target, not just the text. */
.activity-chip--link { padding: 0; }
/* Solid primary is yellow and the chip text is white, so the hover must swap
   the text to on-primary or the name disappears exactly when it is pointed at. */
.activity-chip--link:hover { background: var(--color-primary); color: var(--sys-on-primary); }
.activity-link {
  background: none;
  border: 0;
  padding: 0.3rem 0.7rem;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.activity-link:hover { text-decoration: underline; }
/* Resetting the chrome above takes the browser's default focus ring with it. */
.activity-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 14px;
}
/* Activity names inside the folder path. The hit target is the name alone,
   so the year, month and slashes around it never light the chip up; the chip
   itself highlights only while one of its names is pointed at. */
/* A <button> lays out inline-block and centred; a name that wraps on a phone
   should flow like the path text around it. */
.activity-link--segment { padding: 0; text-align: start; }
/* Keyboard focus gets what hover gets. The primary ring would sit on the
   primary fill at under 3:1, so it takes the on-primary colour instead. */
.activity-link--segment:focus-visible {
  border-radius: 2px;
  outline-color: var(--sys-on-primary);
  text-decoration: underline;
}
.activity-chip--path:has(.activity-link:hover),
.activity-chip--path:has(.activity-link:focus-visible) {
  background: var(--color-primary);
  color: var(--sys-on-primary);
}

/* (d) Photo details (EXIF) footnote. */
.detail-exif {
  border-top: 1px solid var(--color-border);
  padding-top: 1rem;
  font-size: 0.85rem;
}
.detail-exif > summary {
  cursor: pointer;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--color-text-dim);
  margin-bottom: 0.5rem;
}
.exif-curated {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 1rem;
  margin: 0.5rem 0;
}
.exif-curated dt { color: var(--color-text-muted); }
.exif-curated dd { margin: 0; color: var(--color-text-primary); }
.exif-raw { margin-top: 0.75rem; }
.exif-raw > summary {
  cursor: pointer;
  color: var(--color-primary);
  font-size: 0.8rem;
}
.exif-raw pre {
  margin-top: 0.5rem;
  max-height: 16rem;
  overflow: auto;
  background: var(--color-surface-inset);
  border-radius: 6px;
  padding: 0.6rem 0.8rem;
  /* Cornerstone has no mono token; CSS generic monospace family is the
     compliant choice for a raw EXIF dump (not a raw color literal). */
  font-family: monospace;
  font-size: 0.78rem;
  line-height: 1.4;
  white-space: pre;
  color: var(--color-text-secondary);
}

/* Editable label chips */
#detail-labels, .label-hidden-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0;
  margin: 0;
}
.label-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--color-surface-inset);
  border-radius: 12px;
  padding: 0.15rem 0.5rem;
  font-size: 0.85rem;
}
.label-chip--hidden { opacity: 0.55; }
.label-chip:has(.label-link):hover { background: var(--color-primary-muted); }
/* Label names that search for themselves. Hidden labels stay plain <span>. */
.label-link {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.label-link:hover { text-decoration: underline; }
/* Resetting the chrome above takes the browser's default focus ring with it. */
.label-link:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 4px;
}
.label-action {
  background: none;
  border: none;
  color: var(--color-text-muted);
  cursor: pointer;
  font-size: 0.8rem;
  line-height: 1;
  padding: 0 0.1rem;
}
.label-action:hover { color: var(--color-text); }
.label-add { display: inline-flex; gap: 0.25rem; align-items: center; }
.label-add input {
  font-size: 0.85rem;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--color-border-input);
  border-radius: 12px;
  background: var(--color-surface-inset);
  color: var(--color-text-primary);
  width: 9rem;
}
.label-add button {
  background: none;
  border: 1px solid var(--color-border-input);
  border-radius: 50%;
  width: 1.5rem;
  height: 1.5rem;
  cursor: pointer;
  color: var(--color-text-muted);
}
.label-hidden-toggle { list-style: none; width: 100%; margin-top: 0.3rem; }
.label-hidden-list { margin-top: 0.3rem; }

/* ── Admin view ──────────────────────────────────────────── */
.admin-main {
  padding: 1.5rem;
  max-width: 900px;
  margin: 0 auto;
}

.admin-section {
  margin-bottom: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--color-border);
}

.admin-section:first-child { border-top: none; }

.admin-section h2 {
  font-size: 1rem;
  font-weight: 600;
  margin-bottom: 1rem;
}

.admin-section-header {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}
.admin-section-header h2 { margin-bottom: 0; }
.admin-section-header button {
  padding: 0.25rem 0.7rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 0.85rem;
  cursor: pointer;
}
.admin-section-header button:hover { background: var(--color-border); }
.admin-section-header button:disabled { opacity: 0.5; cursor: default; }
.admin-section-desc { font-size: 0.85rem; color: var(--color-text-muted); margin-bottom: 0.75rem; }

.admin-accordion-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: none;
  border: none;
  color: var(--color-text-primary);
  font-size: 1rem;
  font-weight: 600;
  padding: 0;
  margin-bottom: 0;
  cursor: pointer;
  text-align: left;
}
.admin-accordion-btn:hover { color: var(--color-primary); }
.admin-accordion-chevron { font-size: 0.7rem; color: var(--color-text-muted); }
#admin-activities-body { margin-top: 1rem; }
.admin-toggle-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--color-text-secondary);
  cursor: pointer;
  user-select: none;
}

table.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
  margin-bottom: 1rem;
}

table.admin-table th {
  text-align: left;
  padding: 0.4rem 0.75rem;
  color: var(--color-text-dim);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  border-bottom: 1px solid var(--color-border);
}

table.admin-table td {
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--color-surface-item);
  word-break: break-all;
}

table.admin-table tr:last-child td { border-bottom: none; }

table.admin-table button {
  padding: 0.25rem 0.6rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 3px;
  color: var(--color-text-primary);
  font-size: 0.8rem;
  cursor: pointer;
}

table.admin-table button:hover { background: var(--color-border); }
table.admin-table button:disabled { opacity: 0.5; cursor: default; }

.admin-form {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.admin-form input, .admin-form select {
  padding: 0.4rem 0.75rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 4px;
  color: var(--color-text-primary);
  font-size: 0.9rem;
}

.admin-form input:focus, .admin-form select:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-ring);
}

.admin-form button {
  padding: 0.4rem 0.9rem;
  background: var(--color-primary);
  border: none;
  border-radius: 4px;
  color: var(--sys-on-primary);
  font-size: 0.9rem;
  cursor: pointer;
}

.admin-form button:hover { background: var(--color-primary-hover); }

/* ── Upload modal ────────────────────────────────────────── */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--sys-scrim);
  z-index: 100;
}

.modal-box {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 101;
  background: var(--color-surface-modal);
  border: 1px solid var(--color-border);
  border-radius: var(--cmp-photo-radius);
  padding: 1.5rem;
  width: min(560px, 95vw);
  max-height: 80vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.modal-box h2 {
  font-size: 1.1rem;
  font-weight: 600;
}

/* The wizard opens this input programmatically — from the "My device" source
   card and from the photo step's "Choose photos" — so it never needs a visible
   label of its own. */
#upload-file-input { display: none; }

.activity-combo {
  position: relative;
}

.activity-combo input[type="text"] {
  padding: 0.2rem 0.4rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 3px;
  color: var(--color-text-primary);
  font-size: 0.8rem;
  width: 100%;
  box-sizing: border-box;
}

.activity-dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--color-surface-modal, var(--color-surface-item));
  border: 1px solid var(--color-border-input);
  border-top: none;
  border-radius: 0 0 4px 4px;
  max-height: 10rem;
  overflow-y: auto;
  z-index: 200;
  display: none;
}

.activity-dropdown.open {
  display: block;
}

.activity-dropdown-item {
  padding: 0.4rem 0.5rem;
  font-size: 0.85rem;
  cursor: pointer;
  color: var(--color-text-primary);
}

.activity-dropdown-item:hover,
.activity-dropdown-item.active {
  background: var(--color-primary);
  color: var(--sys-on-primary);
}

.upload-item-status {
  font-size: 0.8rem;
  min-width: 5rem;
  text-align: right;
}

/* The status set is closed: upload_flow.js emits ready/duplicate/uploading/
   skipped/done/error and app.js adds processing. 'ready' deliberately has no
   rule of its own — the base above is what an untouched row should look like. */
.upload-item-status.uploading { color: var(--color-primary); }
.upload-item-status.processing { color: var(--color-accent-warm); }
.upload-item-status.done { color: var(--sys-success); }
/* --sys-danger-text, not --sys-danger: the latter is a FILL weight and fails
   contrast badly as text on the dark surfaces — measured 1.54:1 on
   --sys-surface-variant (what the modal uses), where AA needs 4.5:1.
   --sys-danger-text is the text-safe pair at 5.88:1, and is what `.error`
   above already uses. */
.upload-item-status.error { color: var(--sys-danger-text); }
.upload-item-status.duplicate { color: var(--color-accent-warm); font-weight: 600; }
/* Only the Google Photos route produces this status (a row the server
   skipped as an existing duplicate once the import ran). Without a rule it
   inherits plain text colour and reads identically to an untouched 'ready'
   row — italic plus the muted tone (matching .gp-badge.skipped) makes a
   finished-but-skipped row visually distinct from one that never started. */
.upload-item-status.skipped { color: var(--color-text-muted); font-style: italic; }

@media (max-width: 600px) {
  .modal-box {
    width: 95vw;
    padding: 1rem;
    gap: 0.75rem;
    max-height: 88vh;
  }
  .activity-combo { grid-column: 1 / -1; }
}

.modal-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
}

.modal-actions button {
  padding: 0.4rem 0.9rem;
  border-radius: 4px;
  font-size: 0.9rem;
  cursor: pointer;
  border: none;
}

/* The wizard's primary action, whichever step it is on. This rule used to
   target #upload-start-btn, the two-tab modal's single "Start upload"; that id
   is gone, and with the rule orphaned Next/Upload rendered identically to
   Cancel — nothing on the step told a user which button moved them forward.
   Same declarations, re-pointed at the two buttons that inherited the role. */
#upload-next-btn,
#upload-upload-btn {
  background: var(--color-primary);
  color: var(--sys-on-primary);
}

#upload-next-btn:hover:not(:disabled),
#upload-upload-btn:hover:not(:disabled) { background: var(--color-primary-hover); }

#upload-next-btn:disabled,
#upload-upload-btn:disabled { opacity: 0.4; cursor: default; }

#upload-cancel-btn {
  background: var(--color-surface-inset);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-input);
}

#upload-cancel-btn:hover { background: var(--color-border); }

/* ── Duplicate review ────────────────────────────────────── */
.dup-group {
  margin-bottom: 1.5rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  overflow: hidden;
}
.dup-group-header {
  padding: 0.5rem 1rem;
  background: var(--color-surface-inset);
  font-size: 0.85rem;
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
.dup-match-badge {
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  text-transform: uppercase;
}
.dup-match-badge.confirmed { background: var(--color-primary-muted); color: var(--color-primary); }
.dup-match-badge.high { background: color-mix(in srgb, var(--sys-info) 20%, transparent); color: var(--sys-info); }
.dup-match-badge.uncertain { background: color-mix(in srgb, var(--sys-warning) 20%, transparent); color: var(--color-accent-warm); }
.dup-match-badge.possible { background: color-mix(in srgb, var(--sys-warning) 20%, transparent); color: var(--color-accent-warm); }
/* legacy — keep for any cached responses */
.dup-match-badge.hash { background: var(--color-primary-muted); color: var(--color-primary); }
.dup-match-badge.filename { background: color-mix(in srgb, var(--sys-warning) 20%, transparent); color: var(--color-accent-warm); }
.dup-thumbs { display: flex; flex-wrap: wrap; gap: 1rem; padding: 1rem; }
.dup-thumb {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 150px;
}
.dup-thumb img {
  width: 150px;
  height: 110px;
  object-fit: cover;
  border-radius: var(--cmp-photo-radius);
  background: var(--color-surface-item);
  border: 2px solid transparent;
}
.dup-thumb.is-keep img { border-color: var(--color-primary); }
.dup-thumb.is-dupe img { border-color: var(--color-accent-warm); }
/* Unreviewed: same hue, DASHED. A solid border is a decision the admin made;
   dashed is the server's guess, which "Use suggestion" or a click confirms. */
.dup-thumb.is-suggested-keep img { border-color: var(--color-primary); border-style: dashed; }
.dup-thumb.is-suggested-dupe img { border-color: var(--color-accent-warm); border-style: dashed; }
.dup-thumb-video {
  width: 150px;
  height: 110px;
  border-radius: var(--cmp-photo-radius);
  background: var(--color-surface-item);
  border: 2px solid transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-muted);
}
.dup-thumb-video svg { width: 40px; height: 40px; }
.dup-thumb.is-keep .dup-thumb-video { border-color: var(--color-primary); }
.dup-thumb.is-dupe .dup-thumb-video { border-color: var(--color-accent-warm); }
.dup-thumb.is-suggested-keep .dup-thumb-video {
  border-color: var(--color-primary);
  border-style: dashed;
}
.dup-thumb.is-suggested-dupe .dup-thumb-video {
  border-color: var(--color-accent-warm);
  border-style: dashed;
}
.dup-thumb-path {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  word-break: break-all;
}
.dup-badge {
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  text-transform: uppercase;
  align-self: flex-start;
}
.dup-badge.keep { background: var(--color-primary-muted); color: var(--color-primary); }
.dup-badge.dupe { background: color-mix(in srgb, var(--sys-warning) 20%, transparent); color: var(--color-accent-warm); }
.dup-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.25rem; }
.dup-delete-btn {
  padding: 0.2rem 0.5rem;
  background: color-mix(in srgb, var(--sys-danger) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--sys-danger) 35%, transparent);
  border-radius: 3px;
  /* --sys-danger-text, not --sys-danger: the fill weight measures 1.76:1 as text
     on this button's own 12% wash (1.66:1 hovered) where 12px needs 4.5:1. The
     text-safe pair measures 6.71:1 and 6.32:1. Same fix as the wizard's. */
  color: var(--sys-danger-text);
  font-size: 0.75rem;
  cursor: pointer;
}
.dup-delete-btn:hover { background: color-mix(in srgb, var(--sys-danger) 25%, transparent); }
.dup-delete-btn:disabled { opacity: 0.4; cursor: default; }
.dup-mark-btn {
  padding: 0.2rem 0.5rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 3px;
  color: var(--color-text-secondary);
  font-size: 0.75rem;
  cursor: pointer;
}
.dup-mark-btn:hover { background: var(--color-border); }
.dup-mark-btn:disabled { opacity: 0.4; cursor: default; }
/* The chosen button is filled. Colour follows the thumb's state class so a
   filled Keep and a filled Hide are not the same swatch; both wash/text pairs
   are the ones .dup-badge.keep and .dup-badge.dupe already ship. */
.dup-mark-btn.active { font-weight: 600; }
.dup-thumb.is-keep .dup-mark-btn.active {
  background: var(--color-primary-muted);
  border-color: var(--color-primary);
  color: var(--color-primary);
}
.dup-thumb.is-dupe .dup-mark-btn.active {
  background: color-mix(in srgb, var(--sys-warning) 20%, transparent);
  border-color: var(--color-accent-warm);
  color: var(--color-accent-warm);
}
.dup-use-suggestion-btn {
  margin-left: auto;
  padding: 0.2rem 0.6rem;
  background: color-mix(in srgb, var(--sys-info) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--sys-info) 40%, transparent);
  border-radius: 3px;
  /* --sys-accent, not --sys-info: the fill weight measures 2.05:1 as text on
     this button's own 15% wash (1.89:1 hovered) where 12px needs 4.5:1. The
     lighter cyan step measures 5.17:1 and 4.77:1. There is no --sys-info-text
     to pair with, unlike .dup-delete-btn above. */
  color: var(--sys-accent);
  font-size: 0.75rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.dup-use-suggestion-btn:hover { background: color-mix(in srgb, var(--sys-info) 25%, transparent); }
.dup-use-suggestion-btn:disabled { opacity: 0.4; cursor: default; }

/* ── Hidden Duplicates panel ── */
/* Reason badges: exact=green, phash=yellow, manual=cyan, unknown=grey. Reuse
   the .dup-match-badge sizing; only the color tokens differ. */
.dup-match-badge.reason-exact {
  background: color-mix(in srgb, var(--sys-success) 22%, transparent);
  color: var(--sys-success);
}
.dup-match-badge.reason-phash {
  background: var(--color-primary-muted);
  color: var(--color-primary);
}
.dup-match-badge.reason-manual {
  background: color-mix(in srgb, var(--sys-accent) 20%, transparent);
  color: var(--sys-accent);
}
.dup-match-badge.reason-unknown {
  background: color-mix(in srgb, var(--sys-text-disabled) 30%, transparent);
  color: var(--color-text-muted);
}
.hidden-dupes-group-sel-label { margin-left: auto; font-size: 0.78rem; }
.hidden-dupes-rows { display: flex; flex-direction: column; gap: 0.75rem; padding: 1rem; }
.hidden-dupes-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
.hidden-dupes-row .dup-thumb { width: 130px; }
.hidden-dupes-row .dup-thumb img,
.hidden-dupes-row .dup-thumb-video { width: 130px; height: 95px; }
/* Keeper thumb gets a green (success) border to read as "this is the one we keep". */
.hidden-dupes-row .dup-thumb.is-keep img,
.hidden-dupes-row .dup-thumb.is-keep .dup-thumb-video { border-color: var(--sys-success); }
.hidden-dupes-arrow { font-size: 0.8rem; color: var(--color-text-muted); font-weight: 600; }
.hidden-dupes-no-keeper { font-size: 0.8rem; color: var(--color-text-muted); font-style: italic; }
/* Sticky danger bar pinned to the bottom of the viewport while the panel is open. */
.hidden-dupes-bar {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 1rem;
  padding: 0.75rem 1rem;
  margin-top: 0.5rem;
  background: var(--sys-surface-variant);
  border-top: 1px solid color-mix(in srgb, var(--sys-danger) 35%, transparent);
}
.hidden-dupes-bar #admin-hidden-dupes-count { font-size: 0.85rem; color: var(--color-text-secondary); }
.hidden-dupes-delete-btn {
  padding: 0.4rem 0.9rem;
  background: color-mix(in srgb, var(--sys-danger) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--sys-danger) 45%, transparent);
  border-radius: 4px;
  color: var(--sys-danger-text);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
}
.hidden-dupes-delete-btn:hover:not(:disabled) { background: color-mix(in srgb, var(--sys-danger) 30%, transparent); }
.hidden-dupes-delete-btn:disabled { opacity: 0.4; cursor: default; }

#detail-delete-btn {
  background: none;
  border: none;
  cursor: pointer;
  color: var(--sys-danger-text);
  padding: 4px;
  border-radius: 4px;
  transition: background-color 0.15s;
}
/* Hover as a wash, like .nav-menu-item--danger: the fill weights are not AA as text. */
#detail-delete-btn:hover { background: color-mix(in srgb, var(--sys-danger) 12%, transparent); }
#detail-delete-confirm button { margin-left: 0.5rem; font-size: 0.8rem; }

/* ── Upload wizard ──────────────────────────────────────── */

.upload-step {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.upload-field-label {
  font-size: 0.78rem;
  color: var(--color-text-muted);
}

.upload-hint {
  font-size: 0.78rem;
  color: var(--color-text-muted);
  margin: 0;
}

.upload-chip {
  font-size: 0.82rem;
  color: var(--color-text-muted);
  margin: 0 0 0.4rem;
}

.upload-source-card {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  width: 100%;
  padding: 0.7rem 0.8rem;
  text-align: left;
  border: 1px solid var(--color-border);
  border-radius: 8px;
  background: var(--color-surface);
  color: inherit;
  cursor: pointer;
}

/* These are div[role="button"], so they get none of a real button's focus
   styling for free. Every other focusable control in this file has a visible
   focus treatment; without this one, keyboard users cannot see which card
   they are on. */
.upload-source-card:focus-visible {
  outline: 2px solid var(--color-primary-ring);
  outline-offset: 2px;
}

.upload-source-card.is-selected {
  border-color: var(--color-primary);
}

/* Selection must not be carried by colour alone (WCAG 1.4.1). The border
   colour change is the visual cue; this adds a checkmark that survives
   greyscale, low vision, and every form of colour blindness. */
.upload-source-card.is-selected .upload-source-name::after {
  content: " ✓";
  color: var(--color-primary);
}

.upload-source-name {
  font-weight: 600;
}

.upload-source-detail,
.upload-waiting {
  font-size: 0.8rem;
  color: var(--color-text-muted);
}

/* .upload-waiting sits outside the step containers (it reports on reads two
   different steps can trigger), so it is a direct child of the flex .modal-box
   and its default paragraph margins stack on top of that 1rem gap. */
.upload-waiting { margin: 0; }

.upload-review {
  max-height: 22rem;
  overflow-y: auto;
}

/* The name track needs a floor. Grid items default to `min-width: auto`, and
   for a number input that resolves to its intrinsic width — roughly 20
   characters plus the spinner — so the 4rem/3rem date tracks blew past their
   declared size. `.upload-review-name` sets `overflow: hidden`, which makes
   ITS auto minimum resolve to 0, so the name absorbed the entire overflow and
   collapsed to a single character. `min-width: 0` on the inputs (below) makes
   them honour their tracks; the minmax floor keeps the filename readable even
   when a row is tight. */
.upload-review-row {
  display: grid;
  grid-template-columns: 1.5rem minmax(7rem, 1fr) 4rem 3rem 8rem;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0.2rem;
  border-bottom: 1px solid var(--color-border);
  font-size: 0.82rem;
}

.upload-review-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* WCAG 2.5.8 wants 24x24 CSS px minimum for a pointer target. The grid track
   is 1.5rem, but `line-height: 1` with no padding made the button's own box
   smaller than the track — and this is a per-row destructive action on a
   phone-first uploader, so it is the target most worth getting right. */
.upload-review-remove {
  border: none;
  background: none;
  color: var(--color-text-muted);
  cursor: pointer;
  line-height: 1;
  min-width: 24px;
  min-height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.upload-review-remove:focus-visible {
  outline: 2px solid var(--color-primary-ring);
  outline-offset: 1px;
}

/* Number inputs are unstyled by default and their spinners overflow a 3rem
   column; box-sizing + width:100% keeps them inside whichever track they land
   in, on both layouts. */
.upload-review-year,
.upload-review-month {
  width: 100%;
  /* Without this a number input refuses to shrink below its intrinsic width
     as a grid item, overflowing its track and squeezing the filename out. */
  min-width: 0;
  box-sizing: border-box;
  padding: 0.2rem 0.4rem;
  background: var(--color-surface-inset);
  border: 1px solid var(--color-border-input);
  border-radius: 3px;
  color: var(--color-text-primary);
  font-size: 0.8rem;
}

/* Error text in this cell can be a whole S3 XML fault ("S3 403: <?xml…"), far
   more than the 8rem track holds. Let it wrap inside the row instead of
   spilling past it. */
.upload-review-row .upload-item-status {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* Phone reflow. The desktop row's fixed tracks total ~18.5rem with gaps, and a
   95vw modal on a 360px screen leaves about 19rem of content box — so the
   filename gets a sliver and the 8rem status column has to hold strings like
   "S3 403: <?xml…". Three lines instead: × + filename, then the two dates,
   then the status across the full width where it is free to wrap. This is a
   phone-first uploader; the small-screen layout is the important one. */
@media (max-width: 600px) {
  .upload-review-row {
    grid-template-columns: 1.5rem 1fr 1fr;
    row-gap: 0.35rem;
    padding: 0.5rem 0.2rem;
  }
  .upload-review-remove { grid-area: 1 / 1; }
  .upload-review-name { grid-area: 1 / 2 / 2 / 4; }
  .upload-review-year { grid-area: 2 / 2; }
  .upload-review-month { grid-area: 2 / 3; }
  .upload-review-row .upload-item-status {
    grid-area: 3 / 1 / 4 / 4;
    text-align: left;
    min-width: 0;
    white-space: normal;
    overflow-wrap: anywhere;
  }
}

/* ── Google Photos import ── */
.gp-progress-badges { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.5rem 0; }
.gp-badge { border-radius: 10px; padding: 0.15rem 0.6rem; font-size: 0.75rem; font-weight: 600; }
.gp-badge.imported { background: var(--color-primary-muted); color: var(--color-primary); }
/* Fill token stays on the background; the label needs the text-safe pair. */
.gp-badge.failed { background: color-mix(in srgb, var(--sys-danger) 15%, transparent); color: var(--sys-danger-text); }
.gp-badge.skipped { background: color-mix(in srgb, var(--sys-text-secondary) 15%, transparent); color: var(--color-text-muted); }

/* ── Public single-photo share page ──────────────────────── */
.share-page { min-height: 100vh; display: flex; flex-direction: column; }
.share-header { padding: 1rem 1.5rem; border-bottom: 1px solid var(--color-border); }
.share-main {
  flex: 1; display: flex; align-items: center; justify-content: center;
  flex-direction: column; padding: 1.5rem; gap: 1rem; text-align: center;
}
.share-main img { max-width: 100%; max-height: 80vh; border-radius: var(--cmp-photo-radius); }
.share-main figcaption { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.25rem; }
#share-error { color: var(--color-text-dim); font-size: 1.05rem; }

/* Respect a user's reduced-motion preference (WCAG 2.3.3): collapse the
   hover/transition animations on photo cards, menus, modals, etc. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
