/* Modern Design System for Key Recognition App */

:root {
  /* Light theme colors. The page bg sits a step below white and borders a
     step darker than the classic slate values, so cards visibly separate
     from the page — light mode used to wash together (white on #f8fafc). */
  --bg-primary: #ffffff;
  --bg-secondary: #eef2f7;
  --bg-tertiary: #e3e9f1;
  --text-primary: #1e293b;
  --text-secondary: #55657c;
  --text-muted: #8494ab;
  --border-light: #d7dfe9;
  --border-medium: #b9c5d4;
  --accent-primary: #3b82f6;
  --accent-hover: #2563eb;
  --success: #10b981;
  --success-hover: #059669;
  --warning: #f59e0b;
  --danger: #ef4444;
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
  /* Single source of truth for navbar height — used by the sidebar, its
     overlay, and the body wrapper so they can never drift out of sync
     (previously each hardcoded its own "73px" guess). */
  --navbar-h: 64px;
  /* Query vs matched-key colour coding, shared by the recognition result
     image borders AND the blade-profile overlay plot itself
     (key_script.py plot_overlay_profiles uses these same two hex values) —
     so a user can tell which line is which key at a glance. */
  --query-color: #3b82f6;
  --match-color: #eab308;
}

/* Dark theme colors */
.dark-mode {
  --bg-primary: #0f172a;
  --bg-secondary: #1e293b;
  --bg-tertiary: #334155;
  --text-primary: #f1f5f9;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border-light: #334155;
  --border-medium: #475569;
  --accent-primary: #60a5fa;
  --accent-hover: #3b82f6;
  --success: #34d399;
  --success-hover: #10b981;
  --warning: #fbbf24;
  --danger: #f87171;
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.2);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.2), 0 2px 4px -2px rgb(0 0 0 / 0.2);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.2), 0 4px 6px -4px rgb(0 0 0 / 0.2);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.2), 0 8px 10px -6px rgb(0 0 0 / 0.2);
}

/* Image optimization */
.image-container img {
  max-width: 100%;
  height: auto;
  object-fit: contain;
  border-radius: 0.5rem;
}

.image-container img[data-bs-toggle="modal"] {
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.image-container img[data-bs-toggle="modal"]:hover {
  transform: scale(1.02);
  box-shadow: var(--shadow-lg);
}

/* Responsive image sizing */
@media (max-width: 768px) {
  .image-container img {
    max-height: 250px;
  }
}

@media (max-width: 576px) {
  .image-container img {
    max-height: 200px;
  }
}

/* Modal image styling */
#modalImage {
  transition: transform 0.1s ease;
}

/* Base styles */
* {
  box-sizing: border-box;
}

body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background-color: var(--bg-secondary);
  color: var(--text-primary);
  line-height: 1.6;
  margin: 0;
  padding: 0;
  transition: none; /* Remove transition to prevent flash */
  font-size: 1rem; /* Increased from default 14px */
}

/* Typography */
h1, h2, h3, h4, h5, h6 {
  color: var(--text-primary);
  font-weight: 600;
  line-height: 1.2;
  margin-bottom: 1rem;
}

h1 { font-size: 2.5rem; }
h2 { font-size: 2rem; }
h3 { font-size: 1.625rem; }
h4 { font-size: 1.375rem; }
h5 { font-size: 1.25rem; }
h6 { font-size: 1.125rem; }

/* Navigation. Fixed height (not just padding) + nowrap so it can never grow
   taller than --navbar-h and silently desync from the sidebar/overlay offset
   that assumes this exact height. */
.navbar {
  background-color: var(--bg-primary) !important;
  border-bottom: 1px solid var(--border-light);
  box-shadow: var(--shadow-sm);
  height: var(--navbar-h);
  padding: 0;
  transition: all 0.3s ease;
}

.navbar .container-fluid {
  height: 100%;
  flex-wrap: nowrap;
}

/* The header has three slots in the markup — hamburger, brand, actions — but
   only ever shows two of them side by side on a desktop. Hiding the start slot
   here leaves the container with exactly two flex children, which is what
   Bootstrap's justify-content:space-between needs to put the brand hard left
   and the actions hard right. The phone layout below turns all three on. */
.navbar-side--start {
  display: none;
}

.navbar-side--end {
  min-width: 0;
}

.navbar-brand {
  font-weight: 600;
  font-size: 1.25rem;
  color: var(--text-primary) !important;
  white-space: nowrap;
}

/* The witch-on-a-key mark. Two PNGs are in the DOM and one is hidden, so the
   right colour is painted on first frame — the theme class is already on <html>
   by then (inline script in base.html's head), which a JS src swap couldn't
   match without flashing.

   The art is black-on-transparent with white detail lines cut out of it, so it
   cannot be recoloured with a filter or a mask; the inverse file is a real
   second asset. */
.brand-mark {
  display: inline-block;
  /* As large as the header can carry: the navbar is a fixed 64px (--navbar-h)
     with no padding of its own, and Bootstrap's .navbar-brand takes 5px above
     and below, so 46px leaves a comfortable margin and nothing can push the bar
     taller — which would silently desync the sidebar offset that assumes this
     exact height. One size at every width now: the mark carries the brand alone
     on a phone, and 30px was a size picked for sitting beside a word. */
  width: 46px;
  height: 46px;
  flex: 0 0 auto;
  line-height: 0;
}

.brand-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.brand-mark-inverse,
.dark-mode .brand-mark-ink {
  display: none;
}

.dark-mode .brand-mark-inverse {
  display: block;
}

/* Icon-only brand on phones — full lettering plus the hamburger and theme
   toggle was crowding narrow screens. The mark is what stays, which is the
   whole reason it has to read on its own at any width. */
@media (max-width: 767px) {
  .navbar-brand-text {
    display: none;
  }

  /* Brand dead centre, menu and actions pinned to the edges. A grid rather
     than flex because centring the middle child of a flex row only works when
     the two outside it happen to be the same width, and they never are — a
     lone hamburger on one side, "Sign Out" on the other.

     minmax(0, 1fr) rather than 1fr: an fr column won't shrink below its
     content by default, so a wide enough action group would quietly push the
     brand off centre — the exact failure this replaces. */
  .navbar .container-fluid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
  }

  .navbar-side--start {
    display: flex;
    align-items: center;
    justify-self: start;
  }

  .navbar-brand {
    justify-self: center;
    margin-right: 0;
  }


  .navbar-side--end {
    justify-self: end;
  }
}

/* ── The signed-out header ──
   Brand on the left, one button on the right. Not a variant for its own sake:
   the landing, sign-in and sign-up pages each carry a large centred mark as
   the first thing on the page, and a second one centred in the bar directly
   above it read as a duplicate rather than as a header. On the left it is a
   masthead again.

   The word comes back with it at every width. The mark is hidden on a phone in
   the app because the header is crowded there — a hamburger, a name, a sign-out
   button — but this header holds two things, and a stranger who has never seen
   the mark needs to be told the site's name in words.

   After the media query, not inside it: the flex declaration has to beat
   `.navbar .container-fluid`'s grid at equal specificity, which only source
   order decides. */
.navbar--public .navbar-brand-text {
  display: inline;
}

.navbar--public .container-fluid {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.navbar--public .navbar-brand {
  justify-self: start;
  margin-right: 0;
}

.dark-mode-toggle {
  background: var(--bg-tertiary);
  border: 1px solid var(--border-light);
  color: var(--text-primary);
  border-radius: 0.5rem;
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
  font-weight: 500;
  transition: all 0.2s ease;
  cursor: pointer;
}

.dark-mode-toggle:hover {
  background: var(--border-light);
  transform: translateY(-1px);
}

/* Sidebar (widths live here, not inline in base.html: 300px desktop,
   240px tablet, off-canvas drawer under 768px) */
.sidebar-custom {
  background-color: var(--bg-primary) !important;
  border-right: 1px solid var(--border-light) !important;
  box-shadow: var(--shadow-md);
  transition: all 0.3s ease;
  width: 300px;
  min-width: 250px;
  max-width: 350px;
  flex-shrink: 0;
}

.sidebar-custom {
  padding: 1.5rem 1rem;
}

@media (min-width: 769px) and (max-width: 991px) {
  .sidebar-custom {
    width: 240px;
    min-width: 240px;
  }
}

/* Tap-outside-to-close backdrop for the mobile drawer */
.sidebar-overlay {
  display: none;
}

@media (max-width: 768px) {
  .sidebar-overlay.show {
    display: block;
    position: fixed;
    inset: 0;
    top: var(--navbar-h);
    background: rgb(0 0 0 / 0.4);
    z-index: 999;
  }
}

.sidebar-section {
  margin-bottom: 2rem;
}

.sidebar-title {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
}

/* Folder tree */
.explorer-folder {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  border-radius: 0.375rem;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 0.875rem;
  color: var(--text-primary);
}

.explorer-folder:hover {
  background-color: var(--bg-tertiary);
}

.explorer-arrow {
  width: 1rem;
  height: 1rem;
  color: var(--text-muted);
  transition: transform 0.2s ease;
}

.folder-icon {
  width: 1rem;
  height: 1rem;
  color: var(--accent-primary);
}

.folder-add {
  background: none;
  border: none;
  color: var(--text-muted);
  padding: 0.25rem;
  border-radius: 0.25rem;
  cursor: pointer;
  transition: all 0.2s ease;
  opacity: 0;
  font-size: 0.875rem;
}

.explorer-folder:hover .folder-add {
  opacity: 1;
}

.folder-add:hover {
  color: var(--accent-primary);
  background-color: var(--bg-tertiary);
}

/* Key items */
.key-item {
  display: block;
  padding: 0.5rem 0.75rem;
  border-radius: 0.375rem;
  color: var(--text-primary);
  text-decoration: none;
  transition: all 0.2s ease;
  font-size: 0.875rem;
  border-left: 2px solid transparent;
}

.key-item:hover {
  background-color: var(--bg-tertiary);
  border-left-color: var(--accent-primary);
  color: var(--text-primary);
  text-decoration: none;
  transform: translateX(2px);
}

/* Row below the navbar (sidebar + main content) */
.app-body {
  min-height: calc(100vh - var(--navbar-h));
}

/* Main content — the ONLY place a page's outer gutter is set.
   Pages must not add their own wrapper padding or top margin on top of this
   (a .container-fluid.mt-4 on the folder explorer is what made that page sit
   lower and narrower than every other one). Side padding grows with the
   viewport; the top stays put, because the gap above the first heading should
   look the same on a laptop as on a wide monitor.

   Top is deliberately smaller than the bottom: above the heading it's dead
   space before the page starts, below the last card it's just room to breathe
   at the end of a scroll. */
.main-content {
  padding: 1.25rem 1.5rem 2rem;
  max-width: 1400px;
  margin: 0 auto;
}

@media (min-width: 1200px) {
  .main-content {
    padding: 1.25rem 2.5rem 2rem;
  }
}

@media (min-width: 1400px) {
  .main-content {
    padding: 1.25rem 3rem 2rem;
  }
}

/* Cards */
.modern-card {
  background-color: var(--bg-primary);
  border-radius: 0.75rem;
  border: 1px solid var(--border-light);
  box-shadow: var(--shadow-md);
  padding: 1.5rem;
  margin-bottom: 1.5rem;
  transition: all 0.3s ease;
}

.modern-card:hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(-2px);
}

.card-header {
  border-bottom: 1px solid var(--border-light);
  padding-bottom: 1rem;
  margin-bottom: 1.5rem;
}

.card-title {
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

/* Forms */
.form-group {
  margin-bottom: 1.5rem;
}

.form-label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: 500;
  color: var(--text-primary);
  font-size: 0.875rem;
}

.form-control {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid var(--border-medium);
  border-radius: 0.5rem;
  background-color: var(--bg-primary);
  color: var(--text-primary);
  font-size: 1rem;
  transition: all 0.2s ease;
}

.form-control:focus {
  outline: none;
  border-color: var(--accent-primary);
  box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
}

.form-control:disabled {
  background-color: var(--bg-tertiary);
  color: var(--text-muted);
}

/* The actual <input type=file> is visually hidden; it's triggered by the
   native <label for=...> (bulletproof cross-browser/touch trigger) or set
   programmatically by the camera capture below. Never use display:none on a
   file input some browsers stop firing events on a fully undisplayed input;
   the standard "clipped to nothing" technique keeps it functional. */
.visually-hidden-file-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Photo picker: two big obvious buttons (Take Photo / Choose Photo) side by
   side. A real-user test showed the previous single dashed-box-with-overlay
   pattern wasn't reliably tappable on mobile. */
.photo-picker {
  display: flex;
  gap: 0.75rem;
}

.photo-picker-btn {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 1.1rem 0.75rem;
  border: 2px solid var(--border-medium);
  border-radius: 0.75rem;
  background-color: var(--bg-primary);
  color: var(--text-primary);
  font-weight: 600;
  font-size: 0.9rem;
  text-align: center;
  cursor: pointer;
  transition: all 0.2s ease;
}

.photo-picker-btn:hover {
  border-color: var(--accent-primary);
  background-color: rgb(59 130 246 / 0.06);
}

.photo-picker-btn:active {
  transform: scale(0.98);
}

.photo-picker-btn--primary {
  border-color: var(--accent-primary);
  background-color: rgb(59 130 246 / 0.08);
  color: var(--accent-primary);
}

@media (max-width: 480px) {
  .photo-picker {
    flex-direction: column;
  }
}

/* Live camera capture panel */
.camera-panel {
  margin-top: 0.5rem;
}

/* Height is capped in viewport units, not just by the 3:4 ratio: on a phone a
   ratio-sized preview pushed the zoom row and shutter below the fold, and the
   whole point of a live preview is seeing it and the shutter at the same time.
   The video is object-fit:cover, so a shorter box just crops the view. */
.camera-video-wrap {
  position: relative;
  width: 100%;
  max-width: 420px;
  margin: 0 auto;
  aspect-ratio: 3 / 4;
  max-height: 50vh;
  overflow: hidden;
  border-radius: 0.75rem;
  background: #000;
}

.camera-video-wrap video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Dashed framing guide. This IS the crop — the shutter captures exactly the
   region inside it (key_upload.html reads this element's box), so what you
   framed is what the CV pipeline sees. It sits flush with the top of the
   preview on purpose: a key photographed head-up has its ring above the head,
   and anything above the guide's top edge is now discarded rather than being
   silently included in the "whole photo" the pipeline used to get. */
.camera-guide {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 62%;
  height: 82%;
  border: 3px dashed rgb(255 255 255 / 0.9);
  border-radius: 0 0 0.75rem 0.75rem;
  box-shadow: 0 0 0 999px rgb(0 0 0 / 0.45);
  pointer-events: none;
}

.camera-guide-caption {
  position: absolute;
  bottom: 0.75rem;
  left: 0;
  right: 0;
  text-align: center;
  color: #fff;
  font-size: 0.8rem;
  font-weight: 500;
  text-shadow: 0 1px 3px rgb(0 0 0 / 0.8);
  pointer-events: none;
}

/* Stepped zoom (−/+). Uses the camera's optical zoom where the browser exposes
   it, otherwise digital zoom (crop into the full-res capture) — see
   key_upload.html. A slider was too hard to place precisely with a thumb. */
.camera-zoom {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  max-width: 420px;
  margin: 0.6rem auto 0;
}

.camera-zoom-label {
  font-size: 0.8rem;
  color: var(--text-secondary);
  flex-shrink: 0;
  margin-right: 0.25rem;
}

.camera-zoom-btn {
  width: 2.5rem;
  height: 2.5rem;
  flex-shrink: 0;
  border-radius: 50%;
  border: 1px solid var(--border-medium);
  background: var(--bg-primary);
  color: var(--text-primary);
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1;
  cursor: pointer;
  transition: all 0.15s ease;
}

.camera-zoom-btn:active {
  transform: scale(0.94);
  background: var(--bg-tertiary);
}

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

.camera-zoom-value {
  font-size: 0.95rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  width: 3.2rem;
  text-align: center;
  flex-shrink: 0;
}

.camera-hint {
  max-width: 420px;
  margin: 0.5rem auto 0;
  font-size: 0.78rem;
  color: var(--warning);
  text-align: center;
}

/* Brief flash to acknowledge a tap-to-focus */
.camera-video-wrap.focus-ping .camera-guide {
  border-color: var(--accent-primary);
}

/* Capture confirmation: a bright white flash (mimics a physical camera
   shutter) plus a checkmark badge — fired together on every shutter press so
   it's unmistakable a photo was taken, even when firing several shots in a
   row. Both are inert until camera_capture.js adds the trigger class. */
.camera-flash {
  position: absolute;
  inset: 0;
  background: #fff;
  opacity: 0;
  pointer-events: none;
  z-index: 3;
}

.camera-flash.flash {
  animation: cameraFlashPulse 320ms ease-out;
}

@keyframes cameraFlashPulse {
  0%   { opacity: 0.85; }
  100% { opacity: 0; }
}

.camera-capture-badge {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  transform: translate(-50%, -50%) scale(0.7);
  background: rgba(16, 185, 129, 0.95);
  color: #fff;
  font-weight: 700;
  font-size: 0.95rem;
  padding: 0.5rem 1rem;
  border-radius: 2rem;
  opacity: 0;
  pointer-events: none;
  z-index: 4;
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.35);
}

.camera-capture-badge.show {
  animation: cameraBadgePop 900ms cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes cameraBadgePop {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  15%  { opacity: 1; transform: translate(-50%, -50%) scale(1.08); }
  25%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  75%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1); }
}

.camera-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1.5rem;
  margin-top: 0.6rem;
}

.camera-controls-spacer {
  width: 4.25rem; /* balances the Cancel button so the shutter stays centered */
}

.camera-shutter {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  border: 4px solid var(--border-medium);
  background: #fff;
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}

.camera-shutter:active {
  transform: scale(0.94);
}

/* Briefly dead after a shot — see SHUTTER_COOLDOWN_MS in camera_capture.js.
   A button that ignores a press while still looking live reads as a missed
   press, and the natural response to that is to press it again, which is the
   double-shot this exists to prevent. The ring drains back over the cooldown
   so the wait is visibly finite. */
.camera-shutter--cooling {
  cursor: default;
  opacity: 0.55;
  transform: scale(0.94);
  border-color: var(--accent-primary);
  animation: shutter-cool 0.9s linear;
}

.camera-shutter--cooling:active {
  transform: scale(0.94);
}

@keyframes shutter-cool {
  from { opacity: 0.35; }
  to   { opacity: 0.75; }
}

@media (prefers-reduced-motion: reduce) {
  .camera-shutter--cooling { animation: none; }
}

/* Preview + "use a different photo" once a file is present */
.photo-status {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6rem;
}

.upload-preview {
  display: block;
  max-width: 100%;
  max-height: 220px;
  border-radius: 0.5rem;
  border: 1px solid var(--border-light);
  box-shadow: var(--shadow-sm);
}

/* The photo rules above the camera button (mainkey/partials/photo_tips.html).
   It was one line of prose, on the theory that the camera guide teaches framing
   visually; the guide does teach framing, but not "one key", "off the blade" or
   "matte background", and a sentence of grey text under a big button reads as a
   disclaimer and goes unread.

   A bordered panel, not loose text. On a phone it sits between the ways of
   getting a photo in and the batch options, and unframed it read as debris
   between two controls rather than as one block of advice.

   It is still redundant while the live preview is up, and hiding it there buys
   the room the preview needs — but the pages hide it themselves (the bulk
   page's CameraCapture onOpen/onClose), because the camera panel isn't a
   sibling of this block on either of them. */
.photo-tips {
  margin-top: 0.9rem;
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border-light);
  border-radius: 0.7rem;
  background-color: var(--bg-secondary);
  font-size: 0.8rem;
  color: var(--text-secondary);
}

.photo-tips-heading {
  margin: 0 0 0.5rem;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

/* One per line on a phone. Two columns looked worse than it sounds: a grid row
   is as tall as its taller item, so a one-line point beside a two-line one left
   a visible hole, and these six can't all be cut to one line without losing the
   half that says what to do. Columns come back once there's width for every
   point to fit on its own line. */
.photo-tips-list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.3rem 1.2rem;
  list-style: none;
  padding: 0;
  margin: 0;
}

@media (min-width: 576px) {
  .photo-tips-list {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 992px) {
  .photo-tips-list {
    grid-template-columns: repeat(3, 1fr);
  }
}

.photo-tips-list li {
  position: relative;
  padding-left: 0.85rem;
  line-height: 1.45;
}

.photo-tips-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.5rem;
  width: 0.3rem;
  height: 0.3rem;
  border-radius: 50%;
  background-color: var(--accent-primary);
}

.photo-tips-list strong {
  color: var(--text-primary);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Folded away: this is for the first visit and the failed one, not the
   hundredth. */
.photo-examples {
  margin-top: 0.7rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border-light);
}

/* Not underlined. An underline in this app means "this navigates somewhere";
   this opens a drawer on the page it is already on, so it gets the shape of a
   control instead.

   A full-width centred strip rather than a pill: the panel it sits in already
   draws a border, so a second small outlined shape inside it was one box too
   many, and centred it reads as the foot of the panel — which is what it is —
   instead of as a stray tag pinned to the left. The caret follows the words and
   turns when it opens, the same language as the batch-options group. */
.photo-examples > summary {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  cursor: pointer;
  padding: 0.15rem 0;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-secondary);
  list-style: none;
  transition: color 0.15s ease;
}

.photo-examples > summary::-webkit-details-marker {
  display: none;
}

/* Down when shut, up when open — a caret drawn from two borders, the same one
   the batch-options group uses, rather than a second kind of arrow. */
.photo-examples > summary::after {
  content: "";
  width: 0.4rem;
  height: 0.4rem;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg);
  margin-top: -0.2rem;
  transition: transform 0.15s ease;
}

.photo-examples[open] > summary::after {
  transform: rotate(-135deg);
  margin-top: 0.15rem;
}

/* Only where a pointer can actually hover: on a phone :hover sticks after the
   tap and clears only when something else is touched, so a control that just
   collapsed sits there still lit. */
@media (hover: hover) and (pointer: fine) {
  .photo-examples > summary:hover {
    color: var(--accent-primary);
  }
  .photo-examples > summary:hover::after {
    border-color: var(--accent-primary);
  }
}

@media (prefers-reduced-motion: reduce) {
  .photo-examples > summary::after { transition: none; }
}

.photo-examples-defs {
  position: absolute;
  width: 0;
  height: 0;
}

.photo-examples-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.7rem;
  margin-top: 0.7rem;
}

@media (min-width: 576px) {
  .photo-examples-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 992px) {
  .photo-examples-grid {
    grid-template-columns: repeat(6, 1fr);
  }
}

.photo-example {
  margin: 0;
}

.photo-example svg {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 0.5rem;
  background-color: var(--bg-primary);
}

/* The frame is the photo's edge, so "head cropped off" has something to be
   cropped by. */
.pe-frame {
  fill: none;
  stroke: var(--border-medium);
  stroke-width: 2;
}

.pe-key {
  color: var(--text-secondary);
}

.photo-example--good .pe-key {
  color: var(--accent-primary);
}

/* Everything that shouldn't be lying across the blade, drawn back so the key
   still reads as the subject. */
.pe-clutter {
  stroke: var(--text-muted);
  fill: none;
  opacity: 0.75;
}

.pe-shadow {
  fill: var(--text-primary);
  opacity: 0.28;
}

.photo-example figcaption {
  margin-top: 0.3rem;
  font-size: 0.72rem;
  line-height: 1.35;
  color: var(--text-secondary);
}

.pe-mark {
  font-weight: 700;
  color: var(--danger);
}

.pe-mark--good {
  color: var(--success);
}

.photo-examples-note {
  margin: 0.7rem 0 0;
  font-size: 0.75rem;
  color: var(--text-muted);
}

/* Plain-language verdict banner on the recognition result pages.
   The border carries the verdict on its own, before any of the words are read:
   GREEN whenever the app is telling you it found the key among your saved ones
   (a recognition match, or a duplicate on enroll), RED whenever it isn't. */
.verdict-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.9rem 1.1rem;
  border-radius: 0.6rem;
  margin-bottom: 1.25rem;
}

.verdict-match,
.verdict-duplicate {
  background-color: rgb(16 185 129 / 0.1);
  border: 2px solid var(--success);
  color: var(--success);
}

.verdict-notfound {
  background-color: rgb(239 68 68 / 0.08);
  border: 2px solid var(--danger);
  color: var(--danger);
}

.verdict-banner .verdict-title {
  font-weight: 600;
  font-size: 1.05rem;
  color: var(--text-primary);
}

/* The thing the verdict is ABOUT — a key's name — on its own line, bigger than
   the sentence introducing it. */
.verdict-banner .verdict-title-subject {
  display: block;
  font-size: 1.2rem;
  margin-top: 0.15rem;
}

.verdict-banner .verdict-sub {
  font-size: 0.875rem;
  color: var(--text-secondary);
  margin-top: 0.1rem;
}

/* Query vs matched-key photo comparison (recognition result page). Side by
   side at EVERY width including mobile (nowrap) — the ask was "not stacked,
   but not cramped either" — both photos the same size (the matched key used
   to be bigger, but equal weight reads better). */
.key-compare-row {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.75rem;
  justify-content: center;
  align-items: flex-start;
}

.key-compare-card {
  min-width: 0; /* let a flex child shrink below its image's natural size */
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}

.key-compare-card--query,
.key-compare-card--match {
  flex: 1;
}

.key-compare-label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  font-weight: 600;
  font-size: 0.95rem;
  margin-bottom: 0.6rem;
}

.key-compare-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.key-compare-dot--query { background: var(--query-color); }
.key-compare-dot--match { background: var(--match-color); }

/* Two bugs were making these borders look wrong, neither fixed by the
   img-level rules alone:
   1. A later `.image-container img { width: 100% }` rule (further down this
      file) doesn't compete on specificity — it wins per-property, since
      nothing here declared `width`. Forcing the img to full container width
      then letting object-fit:contain shrink the actual photo inside it left
      the border tracing an oversized invisible box, with the photo (and the
      illusion of the border) floating off-centre inside it.
   2. The `.image-container` parent has its own border-radius + overflow:
      hidden (for the plain, non-bordered images elsewhere), which clips
      this element's square corners into rounded ones no matter what
      border-radius is set on the <img> itself. The parent needs the
      `image-container--flush` modifier (below) to opt out.
   `width: auto` here restores intrinsic sizing so the border hugs the
   actual visible photo, with true square corners once the parent clip is
   also disabled. */
.image-container img.key-compare-image {
  width: auto;
  max-width: 100%;
  object-fit: contain;
  border-radius: 0;
  cursor: pointer;
  border: 3px solid transparent;
}

.image-container.image-container--flush {
  border-radius: 0;
  overflow: visible;
  box-shadow: none;
}

.image-container img.key-compare-image--query {
  border-color: var(--query-color);
  max-height: 260px;
}

.image-container img.key-compare-image--match {
  border-color: var(--match-color);
  max-height: 260px;
}

/* No-match page: the closest key is a candidate, not a result, so its border
   is dashed. Same high specificity as the base rule above, or the shorthand's
   implicit `border-style: solid` wins and the dashes never show (this is why
   it looked solid on the deployed build). */
.image-container img.key-compare-image--closest {
  border-style: dashed;
}

.key-compare-view-btn {
  margin-top: 0.75rem;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--match-color);
  color: #1e1300;
  font-weight: 700;
  font-size: 0.9rem;
  border: none;
  border-radius: 0.5rem;
  padding: 0.6rem 1.1rem;
  text-decoration: none;
  box-shadow: var(--shadow-sm);
  transition: all 0.15s ease;
}

.key-compare-view-btn:hover {
  background: #ca9a04;
  color: #1e1300;
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

/* Blade profile overlay on the match result page. Deliberately compact on
   desktop so the About card below pokes above the fold — at full size the
   overlay filled the viewport and nothing hinted there was more to scroll to. */
.image-container img.key-overlay-image {
  max-height: 350px;
  border-radius: 0.5rem;
  cursor: pointer;
}

@media (min-width: 768px) {
  .image-container img.key-overlay-image {
    max-height: 230px;
  }
}

/* Emphasised block: the thing on the page worth looking at first. Strength
   comes from a much darker/lighter (theme-adaptive) border colour rather than
   extra thickness — var(--text-primary) gives a strong outline in both light
   and dark mode without pulling in an accent colour that would clash with the
   query/match colour coding used elsewhere.

   It wraps the Key Information list on the key detail page, not that page's
   card. Around the whole card it outlined everything at once, which singles
   out nothing; around the list it points at the details themselves. */
.card-emphasis {
  border: 2px solid var(--text-primary);
  box-shadow: var(--shadow-lg);
}

/* Bottom-of-card CTA to the matched key's detail page. The label is a full
   sentence, so let it wrap centred instead of shrinking, and make it a
   full-width tap target on phones. */
.key-compare-view-btn--cta {
  justify-content: center;
  text-align: center;
}

@media (max-width: 576px) {
  .key-compare-view-btn--cta {
    width: 100%;
  }
}

/* No-match page: the closest key gets a dashed, unfilled View button to match
   its dashed photo border — solid yellow is reserved for an actual match. */
.key-compare-view-btn--ghost {
  background: transparent;
  border: 2px dashed var(--match-color);
  color: var(--text-primary);
  font-weight: 600;
  box-shadow: none;
}

.key-compare-view-btn--ghost:hover {
  background: rgb(234 179 8 / 0.12);
  color: var(--text-primary);
  transform: none;
  box-shadow: none;
}

/* Inline name + enroll button on the "Enroll New Key" card; wraps on mobile */
.enroll-new-row {
  display: flex;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.enroll-new-row .form-control {
  flex: 1 1 220px;
}

.enroll-new-row .btn {
  flex-shrink: 0;
}

@media (max-width: 576px) {
  /* When the row wraps on phones, a full-width button beats a stubby
     left-aligned one. */
  .enroll-new-row .btn {
    flex: 1 1 100%;
  }
}

@media (max-width: 576px) {
  .key-compare-row { gap: 0.5rem; }
  .image-container img.key-compare-image--query,
  .image-container img.key-compare-image--match { max-height: 170px; }
  .key-compare-label { font-size: 0.8rem; }
  .key-compare-view-btn { font-size: 0.8rem; padding: 0.5rem 0.8rem; }
}

@media (min-width: 577px) and (max-width: 767px) {
  .image-container img.key-compare-image--query,
  .image-container img.key-compare-image--match { max-height: 220px; }
}

/* Radio buttons */
.radio-group {
  display: flex;
  gap: 1rem;
  margin-top: 0.5rem;
}

.radio-option {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 1rem;
  border: 1px solid var(--border-medium);
  border-radius: 0.5rem;
  cursor: pointer;
  transition: all 0.2s ease;
  background-color: var(--bg-primary);
  flex: 1;
}

.radio-option input[type="radio"] {
  margin: 0;
  accent-color: var(--accent-primary);
  flex-shrink: 0;
}

.radio-option-body {
  font-size: 0.9rem;
  color: var(--text-secondary);
}

.radio-option-body strong {
  color: var(--text-primary);
}

@media (max-width: 576px) {
  .radio-group {
    flex-direction: column;
    gap: 0.5rem;
  }
}

.radio-option:hover {
  border-color: var(--accent-primary);
  background-color: var(--bg-tertiary);
}

.radio-option.selected {
  border-color: var(--accent-primary);
  background-color: rgb(59 130 246 / 0.05);
}

/* Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  font-weight: 500;
  font-size: 1rem;
  text-decoration: none;
  cursor: pointer;
  transition: all 0.2s ease;
  border: 1px solid transparent;
}

/* Give the outline variants their outline back. Bootstrap's own `.btn` draws
   it with `border: … solid var(--bs-btn-border-color)`, and the rule above —
   same specificity, later in the cascade — was replacing that with
   `transparent` for every button on the site. The outline variants set only
   the custom property, so they had no visible border at all and read as bare
   coloured text; .key-manage-btn's shadow exists because someone noticed
   "Manage Tags" looking like a label rather than something you press. */
[class*="btn-outline-"] {
  border-color: var(--bs-btn-border-color);
}

/* Center content only for specific form buttons, not navigation buttons */
.btn[type="submit"],
input[type="submit"].btn,
.modal .btn,
form .btn {
  justify-content: center;
  text-align: center;
}

/* Mobile menu button. Filled in the accent colour, because on a phone it's the
   only route to Search, Folders, Tags and the previous batch — an outlined icon
   is easy to miss entirely if you aren't already hunting for a hamburger. It
   used to carry the word "Menu" too; the fill does that job alone now, and the
   room went to centring the brand.

   Sized by padding rather than a fixed width/height: the button sits inside a
   flex slot inside a grid column, and a fixed box in that chain came out as a
   dot on a real phone. Padding around an explicitly-sized icon can't collapse
   the same way. line-height:0 because there is no text left to leave room for,
   and the radius is stated so nothing upstream can round it into a circle. */
.sidebar-toggle {
  background-color: var(--accent-primary);
  border: 1px solid var(--accent-primary);
  color: #ffffff;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  padding: 0.45rem 0.55rem;
  border-radius: 0.5rem;
  line-height: 0;
}

/* In CSS, not just the element's width/height attributes, so no flex or grid
   ancestor can shrink the three lines away to nothing. */
.sidebar-toggle svg {
  display: block;
  width: 22px;
  height: 22px;
  flex: 0 0 auto;
}
.sidebar-toggle:hover,
.sidebar-toggle:focus {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #ffffff;
}

/* ── Help tips ─────────────────────────────────────────────────────────────
   A small "?" beside a label that explains what the thing does. Hover on a
   pointer device, tap on a touch one — it's a real <button>, so a tap focuses
   it and Bootstrap's tooltip fires on focus as well as hover. Never the only
   place an explanation lives; it's for the detail that would clutter the page
   if it were always on screen. */
.help-tip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  padding: 0;
  margin-left: 0.35rem;
  border: 1px solid var(--text-muted);
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
  cursor: help;
  vertical-align: middle;
}
.help-tip:hover,
.help-tip:focus-visible {
  border-color: var(--accent-primary);
  color: var(--accent-primary);
  outline: none;
}

/* Sidebar nav buttons: one accent (Upload, the primary action) + neutrals.
   Replaces four different outline colours (yellow/blue/cyan/green) that read
   as scattered on a light background. */
.sidebar-custom .sidebar-nav-btn {
  font-size: 1rem;
  padding: 0.6rem 1rem;
  color: var(--text-primary);
  background-color: var(--bg-primary);
  border: 1px solid var(--border-medium);
}

.sidebar-custom .sidebar-nav-btn:hover {
  background-color: var(--bg-tertiary);
  border-color: var(--accent-primary);
  color: var(--text-primary);
}

.sidebar-custom .sidebar-nav-btn--primary {
  background-color: var(--accent-primary);
  border-color: var(--accent-primary);
  color: #ffffff;
}

.sidebar-custom .sidebar-nav-btn--primary:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #ffffff;
}

/* The page you're already on. On a phone the sidebar is a drawer you opened
   deliberately, so the first question on opening it is "where am I?" — and
   until now nothing in it answered. A left accent bar plus a filled row, so
   it survives being read in greyscale and doesn't rely on colour alone;
   aria-current="page" in sidebar.html says the same thing to a screen reader. */
.sidebar-custom .sidebar-nav-btn--active {
  background-color: var(--bg-tertiary);
  border-color: var(--accent-primary);
  font-weight: 600;
}

/* Upload is already accent-filled, so the outline can't do the work there.
   Deepen the fill instead. */
.sidebar-custom .sidebar-nav-btn--primary.sidebar-nav-btn--active {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  color: #ffffff;
}

/* Ensure sidebar navigation buttons stay left-aligned */
.sidebar-custom .btn {
  justify-content: flex-start;
  text-align: left;
}

.btn-primary {
  background-color: var(--accent-primary);
  color: white;
  border-color: var(--accent-primary);
}

.btn-primary:hover {
  background-color: var(--accent-hover);
  border-color: var(--accent-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow-md);
}

.btn-secondary {
  background-color: var(--bg-tertiary);
  color: var(--text-primary);
  border-color: var(--border-medium);
}

.btn-secondary:hover {
  background-color: var(--border-light);
  transform: translateY(-1px);
}

.btn-success {
  background-color: var(--success);
  color: white;
  border-color: var(--success);
}

.btn-success:hover {
  background-color: var(--success-hover);
  border-color: var(--success-hover);
}

.btn-sm {
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
}

.btn-lg {
  padding: 1rem 2rem;
  font-size: 1.125rem;
}

/* Images */
.image-container {
  position: relative;
  border-radius: 0.75rem;
  overflow: hidden;
  box-shadow: var(--shadow-md);
  transition: all 0.3s ease;
}

.image-container:hover {
  box-shadow: var(--shadow-lg);
  transform: scale(1.02);
}

.image-container img {
  width: 100%;
  height: auto;
  display: block;
}

/* List groups */
.list-group {
  border-radius: 0.5rem;
  overflow: hidden;
  border: 1px solid var(--border-light);
}

.list-group-item {
  background-color: var(--bg-primary);
  border-color: var(--border-light);
  color: var(--text-primary);
  padding: 1rem;
  border-bottom: 1px solid var(--border-light);
}

.list-group-item:last-child {
  border-bottom: none;
}

.list-group-item strong {
  color: var(--text-primary);
  font-weight: 600;
}

/* Alerts */
.alert {
  padding: 1rem 1.25rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  margin-bottom: 1.5rem;
}

.alert-success {
  background-color: rgb(16 185 129 / 0.1);
  border-color: var(--success);
  color: var(--success);
}

.alert-warning {
  background-color: rgb(245 158 11 / 0.1);
  border-color: var(--warning);
  color: var(--warning);
}

.alert-danger {
  background-color: rgb(239 68 68 / 0.1);
  border-color: var(--danger);
  color: var(--danger);
}

/* Accordion */
.accordion {
  border-radius: 0.5rem;
  overflow: hidden;
  border: 1px solid var(--border-light);
}

.accordion-item {
  border: none;
  background-color: var(--bg-primary);
}

.accordion-header {
  margin: 0;
}

.accordion-button {
  background-color: var(--bg-primary);
  color: var(--text-primary);
  border: none;
  padding: 1rem 1.25rem;
  font-weight: 500;
  border-bottom: 1px solid var(--border-light);
}

.accordion-button:not(.collapsed) {
  background-color: var(--bg-tertiary);
  color: var(--accent-primary);
}

.accordion-button:focus {
  box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
}

.accordion-body {
  padding: 1.25rem;
  background-color: var(--bg-primary);
}

/* Utilities */
.text-center { text-align: center; }
.text-muted { color: var(--text-muted); }
.text-muted-dark {
  color: var(--text-muted);
  opacity: 0.85;
}
.dark-mode .text-muted-dark {
  color: #cbd5e1;
  opacity: 1;
}
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.d-flex { display: flex; }
.align-items-center { align-items: center; }
.justify-content-between { justify-content: space-between; }
.gap-2 { gap: 0.5rem; }
.gap-3 { gap: 0.75rem; }

/* Responsive design */
@media (max-width: 768px) {
  /* 1rem read as edge-to-edge/cramped on real phones ("looks amateur") —
     a bit more breathing room on the sides the content sits in. This is the
     only mobile rule for it: a second `.main-content { padding: 1rem }` in the
     block further down used to override this one silently, so the 1.25rem
     sides written here never actually reached a phone. */
  .main-content {
    padding: 1rem 1.25rem 1.5rem;
  }

  .modern-card {
    padding: 1rem;
  }

  .radio-group {
    flex-direction: column;
  }

  /* Full-width only for full-size buttons (the page's primary actions).
     A blanket rule here stretched every small/inline button — tag filter
     pills, "Manage Tags", Delete Key — edge to edge on phones. */
  .btn:not(.btn-sm) {
    width: 100%;
    justify-content: center;
  }

  /* ...but not the sidebar's, which are nav items and read as a list. The
     rule above matches them at the same specificity as `.sidebar-custom .btn`
     and sits later in the file, so it was quietly centring every item in the
     hamburger drawer while the desktop sidebar stayed left-aligned. */
  .sidebar-custom .btn {
    justify-content: flex-start;
    text-align: left;
  }
}

/* Additional utilities and fixes */
.bg-tertiary {
  background-color: var(--bg-tertiary) !important;
}

/* Sidebar styling improvements */
.sidebar-custom {
  padding: 1.5rem 1rem;
}

/* Form control improvements */
.form-control {
  background-color: var(--bg-primary);
  border: 1px solid var(--border-medium);
  color: var(--text-primary);
}

.form-control:focus {
  background-color: var(--bg-primary);
  border-color: var(--accent-primary);
  color: var(--text-primary);
  box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}

/* Pre/code styling */
pre {
  background-color: var(--bg-tertiary);
  color: var(--text-primary);
  border: 1px solid var(--border-light);
  font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
  font-size: 0.875rem;
  line-height: 1.4;
}

/* Dark mode improvements for Bootstrap overrides */
.dark-mode .list-group-item {
  background-color: var(--bg-primary);
  border-color: var(--border-light);
  color: var(--text-primary);
}

.dark-mode .accordion-button {
  background-color: var(--bg-primary);
  color: var(--text-primary);
  border-color: var(--border-light);
}

.dark-mode .accordion-button:not(.collapsed) {
  background-color: var(--bg-tertiary);
  color: var(--accent-primary);
  border-color: var(--border-light);
  box-shadow: none;
}

.dark-mode .accordion-button:focus {
  border-color: var(--accent-primary);
  box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}

.dark-mode .accordion-body {
  background-color: var(--bg-primary);
  border-color: var(--border-light);
}

/* Custom scrollbar for dark mode */
.dark-mode ::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}

.dark-mode ::-webkit-scrollbar-track {
  background: var(--bg-secondary);
}

.dark-mode ::-webkit-scrollbar-thumb {
  background: var(--border-medium);
  border-radius: 4px;
}

.dark-mode ::-webkit-scrollbar-thumb:hover {
  background: var(--text-muted);
}

/* Loading states */
.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none !important;
}

/* A disabled button has to look the same however it got that way.
   Bootstrap's `.btn-secondary:hover` and `.btn:disabled` have equal
   specificity and hover is declared later, so it won — and a button disabled
   by the very tap that used it keeps the browser's sticky :hover afterwards.
   The result was a third appearance: faded like a disabled button, but still
   carrying the pressed background, sitting next to a button that had been
   disabled all along. Tapping elsewhere cleared it, which is the tell.
   The extra pseudo-class lifts these over Bootstrap's variant rules; the
   values are Bootstrap's own disabled tokens, so every variant stays correct. */
.btn:disabled,
.btn:disabled:hover,
.btn:disabled:focus,
.btn:disabled:focus-visible,
.btn:disabled:active {
  color: var(--bs-btn-disabled-color, var(--bs-btn-color));
  background-color: var(--bs-btn-disabled-bg, var(--bs-btn-bg));
  border-color: var(--bs-btn-disabled-border-color, var(--bs-btn-border-color));
  box-shadow: none;
}

/* Focus states. :focus-visible, not :focus — a tapped button on a phone keeps
   :focus afterwards, so the ring stayed on and the button read as "still
   applied" long after the tap. */
.btn:focus-visible,
.form-control:focus,
.radio-option:focus-within {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

/* Same problem for :hover — a touch device leaves the last-tapped element in
   the hover state indefinitely. Outline buttons fill with colour on hover, so
   e.g. "Clear Filters" looked permanently active after being pressed. Repaint
   them back to their resting skin where there's no real pointer. */
@media (hover: none) {
  [class*="btn-outline-"]:hover {
    color: var(--bs-btn-color);
    background-color: transparent;
    border-color: var(--bs-btn-border-color);
  }
}

/* Animation improvements */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

.modern-card {
  animation: fadeIn 0.3s ease-out;
}

/* Mobile improvements */
@media (max-width: 768px) {
  .sidebar-custom {
    position: fixed;
    top: var(--navbar-h);
    left: -320px;
    width: 300px;
    height: calc(100vh - var(--navbar-h));
    overflow-y: auto;
    transition: left 0.3s ease;
    z-index: 1000;
    box-shadow: var(--shadow-xl);
  }

  .navbar-text {
    display: none;
  }
  
  .sidebar-custom.show {
    left: 0;
  }

  /* (.main-content padding is set once, in the mobile block above.) */

  .row > .col-md-6,
  .row > .col-md-7,
  .row > .col-md-5 {
    margin-bottom: 1.5rem;
  }
  
  .d-flex.justify-content-between {
    flex-direction: column;
    gap: 1rem;
    align-items: stretch;
  }
  
  .card-title {
    font-size: 1.125rem;
  }
}

/* Print styles */
@media print {
  .sidebar-custom,
  .btn,
  .accordion-button {
    display: none !important;
  }
  
  .main-content {
    padding: 0;
  }
  
  .modern-card {
    box-shadow: none;
    border: 1px solid #000;
  }
}

/* Dark mode improvements and folder hierarchy fixes */
.folder-add-root {
  background: var(--bg-primary) !important;
  border: 1px solid var(--border-medium) !important;
  color: var(--text-secondary) !important;
  padding: 0.375rem;
  border-radius: 0.375rem;
  cursor: pointer;
  transition: all 0.2s ease;
  opacity: 1;
}

.folder-add-root:hover,
.folder-add-root:focus {
  color: var(--accent-primary) !important;
  background-color: var(--bg-tertiary) !important;
  border-color: var(--accent-primary) !important;
  transform: scale(1.05);
}

.folder-add {
  background: var(--bg-primary) !important;
  border: 1px solid var(--border-light) !important;
  color: var(--text-secondary) !important;
}

.folder-add:hover,
.folder-add:focus {
  color: var(--accent-primary) !important;
  background-color: var(--bg-tertiary) !important;
  border-color: var(--accent-primary) !important;
}

/* Improved folder alignment */
.explorer-folder {
  display: flex;
  align-items: center;
  min-height: 2rem;
}

.explorer-arrow {
  width: 16px;
  height: 16px;
  margin-right: 0.5rem;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.folder-spacer {
  width: 16px;
  height: 16px;
  margin-right: 0.5rem;
  flex-shrink: 0;
}

.folder-icon {
  width: 16px !important;
  height: 16px !important;
  margin-right: 0.5rem;
  flex-shrink: 0;
}

.folder-label {
  flex-grow: 1;
  color: var(--text-primary);
  font-size: 0.875rem;
}

/* Ensure dark mode visibility for all interactive elements */
.dark-mode .btn-close {
  filter: invert(1);
}

.dark-mode .modal-content {
  background-color: var(--bg-primary);
  border-color: var(--border-medium);
}

.dark-mode .modal-header {
  border-bottom-color: var(--border-medium);
}

/* Image modal improvements - proper initial sizing */
.modal-body {
  background: var(--bg-primary);
}

#zoomContainer {
  background: var(--bg-primary) !important;
  display: flex;
  align-items: center;
  justify-content: center;
}

#modalImage {
  background: var(--bg-primary);
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

.dark-mode .sidebar-custom {
  background-color: var(--bg-secondary) !important;
  border-right: 1px solid var(--border-medium) !important;
}
.dark-mode .key-item {
  background-color: var(--bg-secondary);
  color: var(--text-primary);
}
.dark-mode .key-item:hover {
  background-color: var(--bg-tertiary);
}
.dark-mode .folder-header {
  background-color: var(--bg-secondary) !important;
  color: var(--text-primary) !important;
}
.dark-mode .folder-item {
  background-color: transparent !important;
  color: var(--text-primary) !important;
}
.dark-mode .folder-header:hover {
  background-color: rgba(255, 255, 255, 0.12) !important;
  border-color: rgba(108, 117, 125, 0.5) !important;
}
.dark-mode .modern-card,
.dark-mode .card {
  background-color: var(--bg-primary) !important;
  color: var(--text-primary) !important;
}
.dark-mode .breadcrumb-item.active {
  color: var(--accent-primary) !important;
  opacity: 1;
}

/* Removed custom #deleteKeyModal styles for stability */

#keyList {
  background: var(--bg-secondary);
  border-radius: 0.5rem;
  padding: 0.5rem 0;
}
.dark-mode #keyList {
  background: var(--bg-secondary);
}

/* ══════════════════════════════════════════════════════════════════
   Shared tag-picker + folder-picker modals
   (partials/tag_picker_modal.html, partials/folder_picker_modal.html —
   used by the key detail page and the bulk upload page)
   ══════════════════════════════════════════════════════════════════ */

/* A picked tag, shown as a filled chip in the colour of the tag itself. Lives
   here rather than in one page's <style> block because three pages render it:
   the upload page's batch options, the duplicate modal, and the previous-batch
   list. It used to be defined only inside bulk_upload.html, so the batch list
   rendered its tags as bare text. */
.bulk-tag-chip {
    display: inline-flex; align-items: center; gap: 0.3rem;
    border: 1px solid var(--tag-color);
    background: var(--tag-color);
    color: #fff;
    border-radius: 1rem;
    font-size: 0.8rem;
    padding: 0.15rem 0.6rem;
}
.bulk-tag-chip .btn-close {
    font-size: 0.55em;
    filter: invert(1) grayscale(100%) brightness(200%);
}

/* ── Tag picker modal ───────────────────────────────────────────────────────
   The dialog and the search field are both back to what they were: modal-lg,
   field the full width of the body. Two goes at making the search "not so
   long" — first by shrinking the whole dialog, then by capping the field — and
   both times what actually arrived was a modal with a short thing on the left
   and a close button stranded far away on the right. It was fine as it was;
   the icon is all that's left of the idea. */
.tag-picker-search { position: relative; }
.tag-picker-search svg {
    position: absolute;
    left: 0.6rem; top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted);
    pointer-events: none;
}
.tag-picker-search .form-control { padding-left: 2rem; }

/* The field of pickable tags. Capped and scrolling, so a hundred tags can't
   push the create form off the bottom of a phone. */
.tag-picker-list {
    display: flex; flex-wrap: wrap; gap: 0.35rem;
    max-height: 11rem;
    overflow-y: auto;
}

/* One chip, used by every list in the modal.

   Tinted rather than filled: a filled chip puts text on the tag's own colour,
   and the palette runs from #1B3B6F to #FFF6B7 — white is unreadable on half
   of it and black on the other half. A tint behind the page's normal text
   colour is legible whatever was picked, and the solid dot still carries the
   colour at full strength. */
.tag-chip {
    display: inline-flex; align-items: center; gap: 0.4rem;
    border: 1px solid color-mix(in srgb, var(--tag-color) 55%, transparent);
    background: color-mix(in srgb, var(--tag-color) 14%, var(--bg-primary));
    color: var(--text-primary);
    border-radius: 1rem;
    font-size: 0.82rem;
    line-height: 1.35;
    padding: 0.2rem 0.7rem;
    cursor: pointer;
    transition: border-color 0.12s ease, background-color 0.12s ease;
}
.tag-chip:hover {
    border-color: var(--tag-color);
    background: color-mix(in srgb, var(--tag-color) 26%, var(--bg-primary));
}
.tag-chip-dot {
    width: 0.5rem; height: 0.5rem;
    border-radius: 50%;
    background: var(--tag-color);
    flex: 0 0 auto;
}
.tag-chip-x {
    font-size: 1.05em;
    line-height: 1;
    color: var(--text-muted);
    margin-left: 0.05rem;
}
.tag-chip--picked:hover .tag-chip-x { color: var(--danger, #dc3545); }

/* Creating a tag is occasional; the form is a name, thirty-five swatches and a
   button. Folded away it stops being the biggest thing in a modal that exists
   to pick an existing tag. TagPicker opens it for a user who owns none. */
.tag-create { border-top: 1px solid var(--border-light); padding-top: 0.6rem; }
.tag-create > summary {
    cursor: pointer;
    list-style: none;
    font-size: 0.85rem;
    font-weight: 600;
    display: inline-flex; align-items: center; gap: 0.35rem;
}
.tag-create > summary::-webkit-details-marker { display: none; }
.tag-create > summary::before {
    content: '+';
    font-size: 1.05em;
    line-height: 1;
}
.tag-create[open] > summary::before { content: '\2212'; }   /* minus */
.tag-create > summary:hover { color: var(--accent-primary); }

/* Colour swatch grid picker. A fixed seven across, so the palette is always
   the same shape and a colour stays where the user last found it — auto-fill
   re-flowed it at every width, which is exactly what a palette must not do.
   Seven columns and five rows for the thirty-five colours. */
.color-picker-grid {
    display: grid;
    grid-template-columns: repeat(7, 22px);
    gap: 5px;
}

.hex-color-small {
    width: 22px;
    height: 22px;
    border-radius: 3px;
    cursor: pointer;
    position: relative;
    transition: transform 0.1s ease;
}

.hex-color-small:hover {
    transform: scale(1.2);
    z-index: 10;
}

.hex-color-small.active {
    box-shadow: 0 0 0 2px var(--bs-body-bg), 0 0 0 4px var(--bs-body-color);
    z-index: 20;
}

/* ── Folder-picker modal tree ── */
.fm-tree-item { user-select: none; }
.fm-tree-children { padding-left: 1.1rem; }
.fm-tree-row {
    display: flex; align-items: center; gap: 0.35rem;
    padding: 0.28rem 0.6rem; border-radius: 0.375rem;
    cursor: pointer; position: relative;
    font-size: 0.875rem; white-space: nowrap; overflow: hidden;
}
.fm-tree-row:hover { background: var(--bs-tertiary-bg); }
.fm-tree-row.selected { background: rgba(var(--bs-primary-rgb), 0.12); font-weight: 500; }
.fm-tree-row.selected .fm-tree-folder-icon { color: var(--bs-primary); }
.fm-tree-chevron {
    width: 1rem; flex-shrink: 0; font-size: 0.6rem;
    color: var(--bs-secondary-color);
    display: flex; align-items: center; justify-content: center;
}
.fm-tree-chevron svg { transition: transform 0.15s ease; }
.fm-tree-chevron.open svg { transform: rotate(90deg); }
.fm-tree-folder-icon { color: var(--accent-primary); flex-shrink: 0; }
.fm-inline-icon { color: var(--accent-primary); vertical-align: -2px; }
.fm-tree-name { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; }
.fm-section-label {
    font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.06em; color: var(--bs-secondary-color);
    margin-bottom: 0.6rem; padding-bottom: 0.3rem;
    border-bottom: 1px solid var(--bs-border-color);
}
/* A responsive grid, not a wrapped flex row of fixed 88px boxes: at 88px the
   cards left a ragged margin down the right of the pane at most widths, and a
   folder name longer than about ten characters was ellipsised into nothing
   useful. Auto-fill gives even columns at every width and lets the name have
   two lines before it gives up. */
.fm-folder-card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
    gap: 0.5rem;
    margin-bottom: 1rem;
}
/* No box. A folder is already a recognisable shape, and drawing a bordered
   card around every one of them made a grid of boxes with a small icon rattling
   inside each — the border was doing the work the icon should do. The icon gets
   the space instead and the tap target stays exactly the same size, just
   invisible until you touch it. */
.fm-folder-card {
    display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
    gap: 0.3rem;
    padding: 0.7rem 0.4rem;
    border: 1px solid transparent;
    border-radius: 0.6rem; cursor: pointer;
    text-align: center; background: transparent;
    transition: background-color 0.12s ease;
}
.fm-folder-card:hover,
.fm-folder-card:focus-visible {
    background: var(--bs-tertiary-bg);
}
.fm-folder-card svg { color: var(--accent-primary); flex: 0 0 auto; }
.fm-folder-card-name {
    font-size: 0.76rem; line-height: 1.25;
    width: 100%;
    overflow-wrap: anywhere;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    color: var(--bs-body-color);
}
.fm-key-list-item {
    display: flex; align-items: center; gap: 0.7rem;
    padding: 0.45rem 0.7rem;
    border: 1px solid var(--bs-border-color);
    border-radius: 0.375rem; background: var(--bs-body-bg);
    margin-bottom: 0.4rem;
    transition: border-color 0.12s ease;
}
.fm-key-list-item:hover { border-color: var(--bs-primary); }
.fm-key-thumb {
    width: 42px; height: 42px;
    object-fit: cover; border-radius: 4px; flex-shrink: 0;
    border: 1px solid var(--bs-border-color);
}
.fm-key-thumb-placeholder {
    width: 42px; height: 42px; flex-shrink: 0;
    background: var(--bs-tertiary-bg); border-radius: 4px;
    border: 1px solid var(--bs-border-color);
}
.fm-key-list-name {
    flex-grow: 1; font-size: 0.875rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.fm-modal-content {
    height: 560px;
    display: flex;
    flex-direction: column;
}

/* Sits under the cards now, so the rule that separates it from them goes on
   top. No fill either: a grey band is how you mark a toolbar, and this is a
   path plus one button — it reads better as part of the pane than as a bar
   bolted to it. */
.fm-content-header {
    display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--bs-border-color);
    background: transparent;
    flex-shrink: 0;
}
.fm-breadcrumb-nav { min-width: 0; overflow: hidden; }
/* The path is how you know where you are, and it was set two steps smaller
   than the folder names it describes. */
#fmBreadcrumb { font-size: 0.9rem; }
#fmSelectionLabel { font-size: 0.9rem; }

.fm-new-folder-btn { flex-shrink: 0; white-space: nowrap; }
.fm-new-folder-row {
    display: flex; gap: 0.4rem; align-items: center;
    padding: 0 1rem 0.5rem;
    flex-shrink: 0;
}
.fm-new-folder-row .btn { flex-shrink: 0; }
/* White, not accent-blue. It sits on the tinted content header next to the
   breadcrumb, and a filled blue button there read as the main thing to do in
   the modal — which it isn't; picking a folder is. */
.fm-new-folder-btn,
.fm-new-folder-btn:hover {
    background: var(--bg-primary);
    color: var(--text-primary);
    border-color: var(--border-medium);
}
.fm-new-folder-btn:hover { border-color: var(--accent-primary); }

/* Bootstrap's .btn-sm is inert in this app: modern.css's own .btn sets an
   explicit padding and font-size, is loaded after Bootstrap and wins the tie,
   so every "small" button here is full size. Rather than change that
   everywhere at once, the buttons in this modal are sized here — its footer
   held three of them at full size across the bottom of a dialog. */
#folderManagementModal .btn-sm {
    padding: 0.4rem 0.8rem;
    font-size: 0.85rem;
}

/* The magnifier sits beside the close button; both stay on the title's line. */
.fm-header-actions { display: flex; align-items: center; gap: 0.5rem; }
.fm-header-actions .btn-close { margin: 0; }

/* Search, shown only when the magnifier is pressed. A field permanently across
   the top of a modal is a row of a phone screen spent on something used
   occasionally. */
.fm-search-bar {
    padding: 0.5rem 1rem;
    border-bottom: 1px solid var(--bs-border-color);
    background: var(--bs-tertiary-bg);
    flex-shrink: 0;
}
.fm-search-results {
    max-height: 11rem;
    overflow-y: auto;
    margin-top: 0.4rem;
}

/* ── Folder-picker modal on mobile ──
   The desktop layout is a 260px tree beside a content pane. Inside a 95vw
   dialog on a phone that leaves two unusable ~150px columns, so stack them —
   and put the folder CARDS first, the tree under them, matching the explorer
   page. Expanding a tree with a fingertip is far more work than tapping an
   icon, and the tree is the thing you can do without. */
@media (max-width: 768px) {
    #folderManagementModal .modal-dialog { margin: 0.5rem auto; width: 96vw; }
    .fm-modal-content { height: 88vh; }
    #folderManagementModal .modal-body { flex-direction: column; }

    #fmContentPanel { order: 1; }

    #fmTreePanel {
        order: 2;
        width: 100% !important;
        max-height: 30%;
        border-right: none !important;
        border-top: 1px solid var(--bs-border-color);
    }

    #folderManagementModal .modal-footer {
        flex-direction: column;
        align-items: stretch !important;
        gap: 0.5rem;
    }
    #folderManagementModal .modal-footer .d-flex { justify-content: space-between; }
    #folderManagementModal .modal-footer .btn { flex: 1; }

    /* Finger-sized rows and chevrons (the chevron is the only way to expand —
       there's no double-click on touch). */
    .fm-tree-row { padding: 0.55rem 0.6rem; font-size: 0.95rem; }
    .fm-tree-chevron { width: 1.75rem; height: 1.75rem; font-size: 0.8rem; margin: -0.3rem 0; }
    .fm-tree-folder-icon { font-size: 1rem; }
    .fm-folder-card { padding: 0.9rem 0.4rem; }
    .fm-folder-card-name { font-size: 0.8rem; }
    .fm-key-list-item { padding: 0.55rem; }
}

/* ── Shoulder-line overlay (see static/mainkey/shoulder_overlay.js) ──────────
   Marks where the app judged the blade to stop and the shoulder to start. Grey
   and thin on purpose: it's a reference mark the customer can sanity-check, not
   a result, and it must not compete with the photo underneath. The wrapper
   shrink-wraps the <img> so the absolutely-positioned SVG covers exactly the
   displayed photo and nothing else. */
.shoulder-wrap {
  position: relative;
  display: inline-block;
  line-height: 0;
  max-width: 100%;
}

.shoulder-wrap > svg.shoulder-line {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Thin on purpose. This line marks where the blade was judged to start — it
   has to be readable over the photo without becoming the subject of it. At
   2.5px with round caps each dash bulged into a lozenge and the whole thing
   read as a heavy grey stripe laid across the key. */
.shoulder-wrap > svg.shoulder-line line {
  stroke: rgb(148 163 184 / 0.95);
  stroke-width: 1.25;
  stroke-dasharray: 6 4;
  stroke-linecap: butt;
}

.shoulder-note {
  font-size: 0.8rem;
  color: var(--text-secondary);
  margin: 0.4rem 0 0;
}

/* The explanation of that line is a question people ask once and then never
   again, so it hides behind the same "?" every other one-time explanation on
   this app uses. It was a <details> the reader had to expand; that made it a
   third thing on the page to work out, and the answer it hid is one sentence
   long. Four screens carry this note, so consistency with the "?" pattern is
   worth more here than a bespoke disclosure. */

/* UNDERLINES MEAN NAVIGATION. A line of text that opens a drawer, a picker or
   a modal on the page you are already on gets the shape of a control instead —
   a caret, a centred strip, a bordered row (see .photo-examples > summary and
   .batch-options-disclosure in bulk_upload.html). Underlining those made them
   read as links to somewhere else, and they aren't.

   This one is the deliberate exception. It is the secondary route to the file
   picker: an existing photo has no framing guide and fails far more often, so
   it has to stay available without inviting itself, and a quiet underlined
   line is the least inviting thing that is still obviously pressable.
   .photo-fallback--only promotes it back to a real button on devices with no
   camera, where it is the ONLY way in. */
.photo-fallback {
  text-align: center;
  margin-top: 0.6rem;
}

.photo-fallback label {
  font-size: 0.85rem;
  color: var(--text-secondary);
  text-decoration: underline;
  cursor: pointer;
}

.photo-fallback label:hover {
  color: var(--accent-primary);
}

.photo-fallback--only {
  margin-top: 0;
}

.photo-fallback--only label {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.1rem 0.75rem;
  border: 2px solid var(--accent-primary);
  border-radius: 0.75rem;
  background-color: rgb(59 130 246 / 0.08);
  color: var(--accent-primary);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
}

/* ==========================================================================
   Public pages: landing, privacy, terms, and the footer on every page.
   Uses the same custom properties as the app so light/dark come for free.
   ========================================================================== */

.site-footer {
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem 1.5rem;
  margin-top: 3rem;
  border-top: 1px solid var(--border-light);
  font-size: 0.85rem;
  color: var(--text-muted);
}

.site-footer a {
  color: var(--text-secondary);
  text-decoration: none;
}

.site-footer a:hover {
  color: var(--accent-primary);
  text-decoration: underline;
}

/* A page title with an icon in front of it. Folder Explorer has had one for a
   while; this is the shared version so the next page that wants one doesn't
   invent its own alignment. flex + baseline-ish centring rather than a margin
   on the <svg>, which drifts with the heading's line-height. */
.page-title-with-icon {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* No colour of its own: the icon takes the heading's, which is what Folder
   Explorer's has always done. Tinting it accent was a change to a page that was
   already right. */
.page-title-with-icon svg,
.page-title-with-icon i {
  flex: 0 0 auto;
}

/* Sized in em, not pixels, so an inline-SVG icon tracks the heading exactly as
   a Font Awesome glyph does. A fixed 20px matched Folder Explorer's icon at
   neither size: too small beside a 2rem heading on a desktop, too big beside a
   1.5rem one on a phone. 0.95em rather than the 0.8em first guessed — a solid
   FA glyph fills more of its em box than that. */
.page-title-with-icon svg {
  width: 0.95em;
  height: 0.95em;
}

/* The size lives here, not on either page. Folder Explorer set its own 1.5rem
   on a phone while Manage Tags kept the global h2's 2rem, so the two sat at
   different sizes on exactly the screen where they look most alike. */
@media (max-width: 768px) {
  .page-title-with-icon { font-size: 1.5rem; }
}

/* --- Bulk apply: one folder-and-tags group, used in two places ------------
   The upload page offers it before a batch runs; the previous-batch page offers
   it afterwards, over the keys that batch saved. It lived in bulk_upload.html's
   own <style> while previous_batch.html kept a hand-made lookalike, and the two
   drifted — .bulk-chip-row was among the classes /batch/ used without ever
   loading the rule for it. One definition, here, so they can't. */

/* ── Batch options ────────────────────────────────────────────────────────
   Two full-width rows that look and behave like the settings rows they are:
   icon, what it does, what's currently set, chevron. Previously a label and a
   small outline button per line, which read as a form and gave no hint that
   the choice covered the whole batch. */
.bulk-options { display: flex; flex-direction: column; gap: 0.5rem; }

/* Under the options, so it reads as a footnote to a control someone has
   already found rather than the paragraph standing between them and it. */
.bulk-options-intro {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin: 0;
    padding: 0.5rem 0.85rem 0.6rem;
    border-top: 1px solid var(--border-light);
}

.bulk-option {
    display: flex; align-items: center; gap: 0.75rem;
    width: 100%;
    text-align: left;
    border: 1px solid var(--border-medium);
    border-radius: 0.6rem;
    background: var(--bg-primary);
    padding: 0.65rem 0.85rem;
    cursor: pointer;
    transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}
@media (hover: hover) and (pointer: fine) {
    .bulk-option:hover {
        border-color: var(--accent-primary);
        background: var(--bg-tertiary);
        box-shadow: var(--shadow-sm);
    }
}

.bulk-option-icon {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    width: 2.1rem; height: 2.1rem;
    border-radius: 0.5rem;
    background: var(--bg-tertiary);
    color: var(--accent-primary);
}
@media (hover: hover) and (pointer: fine) {
    .bulk-option:hover .bulk-option-icon { background: var(--bg-primary); }
}

.bulk-option-text { flex: 1 1 auto; min-width: 0; }
.bulk-option-label {
    display: block;
    font-size: 0.92rem; font-weight: 600; color: var(--text-primary);
}
.bulk-option-value {
    display: block;
    font-size: 0.82rem;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A set value stops being muted, so "chosen" is visible at a glance. */
.bulk-option-value:not(.text-muted) { color: var(--accent-primary); font-weight: 600; }

.bulk-option-chevron {
    flex: 0 0 auto;
    width: 0.5rem; height: 0.5rem;
    border-top: 2px solid var(--text-muted);
    border-right: 2px solid var(--text-muted);
    transform: rotate(45deg);
    margin-right: 0.2rem;
}

.bulk-chip-row {
    display: flex; flex-wrap: wrap; gap: 0.35rem;
    padding: 0.15rem 0.1rem 0.1rem;
}

/* (.bulk-tag-chip — the chip for a picked tag — is shared; see modern.css.) */

/* -- Batch options -------------------------------------------------------
   A <details>. The page's job is to get someone to the camera button, so
   anything that isn't that starts closed and takes one line.

   The whole thing is ONE bordered group now: a header row shaped like the
   option rows it opens onto, with those rows flattened inside it so they read
   as its contents rather than as boxes in a box. It used to be a coloured line
   of text with a chevron in front of it, which is the shape of a link -- and
   what it actually does is open two pickers. */
.batch-options-disclosure {
    margin-top: 1rem;
    border: 1px solid var(--border-medium);
    border-radius: 0.6rem;
    background: var(--bg-primary);
    overflow: hidden;
}

/* A tinted bar, not another row. The indent that used to carry this job made
   the group read as a folder tree — three things of one kind at two depths —
   because the header wore the same icon badge and the same shape as the rows
   under it. Ranking them by depth was the wrong axis: they are different KINDS
   of thing, so the header is coloured like a heading and the rows sit flush and
   white beneath it, which is a difference you can see without measuring. */
.batch-options-disclosure > summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.65rem 0.85rem;
    cursor: pointer;
    list-style: none;                 /* Firefox's default triangle */
    background: var(--bg-secondary);
    transition: background-color 0.15s ease;
}
/* Plain, small and muted — deliberately NOT the .bulk-option-icon badge the two
   rows wear, which was the loudest thing saying "I am one of these too". */
.batch-options-icon {
    flex: 0 0 auto;
    display: flex;
    color: var(--text-muted);
}
.batch-options-disclosure > summary::-webkit-details-marker { display: none; }
/* Only where a pointer can actually hover. On a phone there is no such thing as
   leaving an element, so a plain :hover rule stayed lit after the tap that shut
   the group and only cleared when something else was touched — which reads as
   "still open" on the one control whose whole job is to say whether it is. */
@media (hover: hover) and (pointer: fine) {
    .batch-options-disclosure > summary:hover { background: var(--bg-tertiary); }
}

.batch-options-heading {
    flex: 1 1 auto;
    min-width: 0;
}
.batch-options-title {
    display: block;
    font-size: 0.92rem; font-weight: 600;
    color: var(--text-primary);
}
/* What's currently chosen, under the title the way an option row carries its
   value, so a set folder or tag stays visible while the group is shut. On a
   phone it used to fight the title for one line and win, ellipsising away the
   half that says this is a batch-wide action. */
.batch-options-summary {
    display: block;
    font-size: 0.82rem;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.batch-options-summary.is-set { color: var(--accent-primary); font-weight: 600; }

/* Down when shut, up when open, on the right where this page's other rows put
   their chevron. */
.batch-options-caret {
    flex: 0 0 auto;
    width: 0.5rem; height: 0.5rem;
    border-right: 2px solid var(--text-muted);
    border-bottom: 2px solid var(--text-muted);
    transform: rotate(45deg);
    margin: -0.2rem 0.2rem 0 0;
    transition: transform 0.15s ease;
}
.batch-options-disclosure[open] .batch-options-caret {
    transform: rotate(-135deg);
    margin: 0.15rem 0.2rem 0 0;
}

/* Inside the group the rows lose their own borders: one hairline between them
   is enough, and nested boxes read as clutter on a phone. The line under the
   header is the full-weight one — it separates two kinds of thing, where the
   line between the rows only separates two of a kind. */
.batch-options-disclosure .bulk-options {
    gap: 0;
    border-top: 1px solid var(--border-medium);
    background: var(--bg-primary);
}
.batch-options-disclosure .bulk-option {
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    padding: 0.6rem 0.85rem;
}
.batch-options-disclosure .bulk-option + .bulk-option {
    border-top: 1px solid var(--border-light);
}
@media (hover: hover) and (pointer: fine) {
    .batch-options-disclosure .bulk-option:hover {
        background: var(--bg-tertiary);
        box-shadow: none;
    }
}
.batch-options-disclosure .bulk-chip-row { padding: 0.15rem 0.85rem 0.35rem 3.6rem; }

@media (prefers-reduced-motion: reduce) {
    .batch-options-caret { transition: none; }
}

/* --- Landing ------------------------------------------------------------ */

.landing {
  max-width: 62rem;
  margin: 0 auto;
}

.landing section {
  margin-bottom: 3.5rem;
}

.landing-hero {
  text-align: center;
  padding: 2.5rem 0 1rem;
}

/* Bigger than the navbar copy of the same mark, and sized off the viewport so
   it doesn't eat a phone screen above the headline. */
.landing-mark {
  width: clamp(88px, 22vw, 132px);
  height: clamp(88px, 22vw, 132px);
  margin-bottom: 1.25rem;
}

/* Sign-in and register: big enough to register as branding, small enough that
   the form stays the point of the page. */
.auth-mark {
  width: 88px;
  height: 88px;
  margin-bottom: 1rem;
}

.landing-hero h1 {
  font-size: clamp(1.9rem, 5vw, 3rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

.landing-lead {
  font-size: clamp(1rem, 2.4vw, 1.2rem);
  color: var(--text-secondary);
  max-width: 40rem;
  margin: 0 auto 2rem;
  line-height: 1.65;
}

.landing-cta,
.landing-cta-end {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  justify-content: center;
}

/* The three capability cards, side by side on a desktop and stacked on a
   phone — where most of these photos get taken, so the stacked order is the
   real order. auto-fit rather than a fixed three columns: at a middling width
   two-and-a-bit narrow columns are worse than two wide ones. */
.landing-features {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.5rem;
}

/* Where a plan name and a price will go. Small and quiet: it labels the card,
   it isn't the heading. */
.landing-card-eyebrow {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-primary);
  margin: 0 0 0.4rem;
}

.landing-card {
  padding: 1.75rem;
  border: 1px solid var(--border-light);
  border-radius: 0.9rem;
  background-color: var(--bg-primary);
  box-shadow: var(--shadow-sm);
}

.landing-card h2 {
  font-size: 1.2rem;
  font-weight: 650;
  color: var(--text-primary);
  margin-bottom: 0.75rem;
}

.landing-card p,
.landing-card li {
  color: var(--text-secondary);
  line-height: 1.6;
}

.landing-card ul {
  margin: 0;
  padding-left: 1.15rem;
}

.landing-card li {
  margin-bottom: 0.4rem;
  font-size: 0.94rem;
}

.landing-how h2,
.landing-privacy h2 {
  font-size: 1.35rem;
  font-weight: 650;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

.landing-how ol {
  padding-left: 1.25rem;
  color: var(--text-secondary);
  line-height: 1.7;
}

.landing-how li {
  margin-bottom: 0.85rem;
}

.landing-how strong {
  color: var(--text-primary);
}

/* Not styled as a warning: it's part of the pitch, not a disclaimer bolted to
   the bottom of it. */
.landing-privacy {
  padding: 1.5rem 1.75rem;
  border-left: 3px solid var(--border-medium);
  background-color: var(--bg-secondary);
  border-radius: 0 0.75rem 0.75rem 0;
}

.landing-privacy p {
  color: var(--text-secondary);
  line-height: 1.7;
  margin-bottom: 0;
}

.landing-cta-end {
  padding-bottom: 1rem;
}

/* --- Long-form prose pages (legal, help) --------------------------------
   Same shape for both: a single readable column of headed sections. The help
   page joins these selectors rather than restating them, so the two can't
   drift into looking like pages from different apps. */

.legal-page,
.help-page {
  max-width: 44rem;
  margin: 0 auto;
  padding: 1rem 0 2rem;
  color: var(--text-secondary);
  line-height: 1.7;
}

.legal-page h1,
.help-page h1 {
  font-size: 1.9rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 0.35rem;
}

.legal-page h2,
.help-page h2 {
  font-size: 1.2rem;
  font-weight: 650;
  color: var(--text-primary);
  margin: 2.25rem 0 0.75rem;
}

.legal-page h3,
.help-page h3 {
  font-size: 1rem;
  font-weight: 650;
  color: var(--text-primary);
  margin: 1.5rem 0 0.5rem;
}

.legal-meta {
  font-size: 0.85rem;
  color: var(--text-muted);
  margin-bottom: 1.5rem;
}

.legal-page ul,
.legal-page ol,
.help-page ul,
.help-page ol {
  padding-left: 1.25rem;
}

.legal-page li,
.help-page li {
  margin-bottom: 0.5rem;
}

.legal-page strong,
.help-page strong {
  color: var(--text-primary);
}

.legal-table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0 1.5rem;
  font-size: 0.92rem;
}

.legal-table th,
.legal-table td {
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border-light);
  text-align: left;
  vertical-align: top;
}

.legal-table th {
  background-color: var(--bg-secondary);
  color: var(--text-primary);
  font-weight: 600;
}

/* --- Member home -------------------------------------------------------- */

.home {
  /* Wider than the old 52rem: the page is sections now, and the three
     how-it-works steps want to read as a row on a desktop rather than as a
     cramped column beside empty space. */
  max-width: 60rem;
  margin: 0 auto;
}

.home-head {
  padding: 0.5rem 0 1.25rem;
}

.home-head h1 {
  font-size: clamp(1.5rem, 4vw, 2rem);
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text-primary);
  margin-bottom: 0.6rem;
}

.home-lead {
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 0;
  max-width: 38rem;
}

/* Two tiles now, not eight: the only two things on the page that DO something.
   One column on a phone, so each is a full-width tap target rather than a pair
   of small ones — this page exists to be obvious. */
.home-actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
}

@media (min-width: 576px) {
  .home-actions {
    grid-template-columns: repeat(2, 1fr);
  }
}

.home-tile {
  display: flex;
  align-items: flex-start;
  gap: 0.9rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--border-light);
  border-radius: 0.9rem;
  background-color: var(--bg-primary);
  box-shadow: var(--shadow-sm);
  text-decoration: none;
  transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
}

.home-tile:hover,
.home-tile:focus-visible {
  border-color: var(--accent-primary);
  background-color: var(--bg-tertiary);
  box-shadow: var(--shadow-md);
  text-decoration: none;
}

/* The one thing a new account has to do first. */
.home-tile--primary {
  border-color: var(--accent-primary);
}

.home-tile-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 0.65rem;
  background-color: var(--bg-tertiary);
  color: var(--accent-primary);
}

.home-tile--primary .home-tile-icon {
  background-color: var(--accent-primary);
  color: #fff;
}

.home-tile-text {
  min-width: 0;
}

.home-tile-title {
  display: block;
  font-size: 1.05rem;
  font-weight: 650;
  color: var(--text-primary);
  margin-bottom: 0.15rem;
}

.home-tile-desc {
  display: block;
  font-size: 0.88rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

/* --- The sections below the two actions --------------------------------- */

.home-section {
  padding-top: 1.75rem;
}

/* Each group gets a real heading and a sentence, not a small-caps label: the
   sidebar already names these places, so what this page adds is saying what
   they are for. */
.home-section-h {
  font-size: 1.05rem;
  font-weight: 650;
  color: var(--text-primary);
  margin-bottom: 0.35rem;
}

.home-section-note {
  font-size: 0.88rem;
  line-height: 1.6;
  color: var(--text-secondary);
  margin-bottom: 0.85rem;
  max-width: 42rem;
}

.home-section-note:last-child {
  margin-bottom: 0;
}

.home-section-note--after {
  margin: 0.85rem 0 0;
  font-size: 0.85rem;
}

/* The last batch, as one tappable row rather than a paragraph with a link
   hidden in it. Same language as the option rows elsewhere: icon, what it says,
   chevron. */
.home-strip {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.75rem;
  padding: 0.7rem 0.85rem;
  border: 1px solid var(--border-light);
  border-radius: 0.7rem;
  background-color: var(--bg-primary);
  text-decoration: none;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .home-strip:hover {
    border-color: var(--accent-primary);
    background-color: var(--bg-tertiary);
    text-decoration: none;
  }
}

.home-strip-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 0.5rem;
  background-color: var(--bg-tertiary);
  color: var(--accent-primary);
}

.home-strip-text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.88rem;
  line-height: 1.4;
  color: var(--text-primary);
}

.home-strip-caret {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border-top: 2px solid var(--text-muted);
  border-right: 2px solid var(--text-muted);
  transform: rotate(45deg);
  margin-right: 0.2rem;
}

/* The rest of the app: compact buttons under the sentence that introduces
   them. Two across on a phone, so a group of three or four is one glance
   rather than a column to scroll past. */
.home-nav {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.6rem;
}

@media (min-width: 576px) {
  .home-nav {
    grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  }
}

.home-nav-tile {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  min-width: 0;
  padding: 0.7rem 0.8rem;
  border: 1px solid var(--border-light);
  border-radius: 0.7rem;
  background-color: var(--bg-primary);
  text-decoration: none;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .home-nav-tile:hover {
    border-color: var(--accent-primary);
    background-color: var(--bg-tertiary);
    text-decoration: none;
  }
}

.home-nav-icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border-radius: 0.5rem;
  background-color: var(--bg-tertiary);
  color: var(--accent-primary);
}

.home-nav-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--text-primary);
}

/* Discard: a de-emphasized dashed treatment in red, because it throws the
   photo (or the key) away. Never the expected path anywhere it appears, so it
   never competes with the button above it. Shared by the batch page's modals
   and the one-photo page. */
.modal-discard-btn {
  background: transparent;
  border: 2px dashed var(--danger);
  color: var(--text-muted);
}

.modal-discard-btn:hover {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}

/* Armed — one tap in, so now it means it. */
.modal-discard-btn--armed {
  border-style: solid;
  color: var(--danger);
  font-weight: 600;
}

/* --- "Verify your email" banner ----------------------------------------
   Shown above every page until the address is verified, so its height is a
   permanent tax on every other page — worst on a phone, where it was costing
   three lines: two of sentence plus a full-width button. Flex with wrapping
   keeps the button beside the text while it fits, and the phone rules below
   shrink everything that made it tall. */

/* "Verify email" chip in the navbar (base.html). Amber like the alert it
   replaced, sized like the theme toggle beside it, and never taller than the
   fixed --navbar-h row. */
.verify-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  background: rgb(245 158 11 / 0.12);
  border: 1px solid var(--warning);
  color: var(--warning);
  border-radius: 0.5rem;
  padding: 0.375rem 0.7rem;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.verify-chip:hover {
  background: rgb(245 158 11 / 0.22);
}

/* Icon-only on phones, same as the theme toggle next to it — the navbar can't
   wrap, and the accessible name still carries the whole sentence. */
@media (max-width: 768px) {
  .verify-chip-text {
    display: none;
  }

  .verify-chip {
    padding: 0.4rem 0.5rem;
  }
}

/* --- Image lightbox -----------------------------------------------------
   Tap-to-enlarge for one image (see static/mainkey/image_lightbox.js). Sits
   above Bootstrap's modal layer (1055) because everything it's used on so far
   is inside a modal. */

.image-lightbox {
  position: fixed;
  inset: 0;
  /* A rotated image's layout box is wider than the window (see below); this
     keeps that from producing a scrollbar or nudging the centring. */
  overflow: hidden;
  z-index: 1080;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  /* Top clears the close button; the rest is a thin margin, since the image
     is now the only thing in here and every spare pixel goes to it. */
  padding: 3.25rem 0.75rem 0.75rem;
  background: rgb(2 6 23 / 0.92);
}

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

/* The picture gets the whole overlay. object-fit:contain rather than a max
   height in px: a blade profile is a wide, short image and a key photo is a
   tall one, and both have to fill what they can of the same box. */
.image-lightbox img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 0.5rem;
  background: #fff;
}

/* Sideways, for a wide image in a tall window — see applyRotation() in
   image_lightbox.js. Absolutely positioned rather than left in the flex flow:
   a rotated element keeps its ORIGINAL box for layout, so in flow it would
   reserve the wrong dimensions and be centred against them. The two max-
   values are swapped on purpose — they cap the image before it turns, so
   max-width limits its eventual height and max-height its eventual width. */
.image-lightbox--rotated img {
  position: absolute;
  top: 50%;
  left: 50%;
  /* 6.5rem is 3.25rem at each end, because the image is centred and so grows
     both ways at once. That's what it takes to clear the close button, which
     is the only other thing in the overlay now. */
  max-width: calc(100vh - 6.5rem);
  max-height: calc(100vw - 1.5rem);
  transform: translate(-50%, -50%) rotate(90deg);
}

.image-lightbox-close {
  position: absolute;
  top: 0.5rem;
  right: 0.75rem;
  width: 2.5rem;
  height: 2.5rem;
  border: none;
  border-radius: 50%;
  background: rgb(148 163 184 / 0.25);
  color: #f8fafc;
  font-size: 1.6rem;
  line-height: 1;
  cursor: pointer;
}

.image-lightbox-close:hover { background: rgb(148 163 184 / 0.4); }

/* An image that opens the lightbox. Says so before it's tapped — an image
   that silently happens to be a button gets tapped by nobody. */
.image-expandable {
  cursor: zoom-in;
}

.image-expandable:focus-visible {
  outline: 2px solid var(--accent-primary);
  outline-offset: 2px;
}

/* ────────────────────────────────────────────────────────────────────────────
   Welcome tour  (partials/welcome_modal.html + welcome_modal.js)

   Four full-width panes on a flex track, moved with translateX. The panes are
   never `hidden` — that collapses them and the track's geometry with them — so
   everything here assumes all four are laid out and only one is in view.

   SIZE IS THE POINT. The first version was 30rem wide with a 16:10 image on
   every slide, which on a phone was the whole screen — and something occupying
   the whole screen reads as the app rather than as a message from it, so Skip
   starts to look like leaving. It is now a narrow card with the page visible
   around it. The width below is deliberately below a phone's, so there is
   always a margin of app on both sides.

   The track is one height for all four slides (a flex row is as tall as its
   tallest child), so the dialog must not resize between them — hence the fixed
   ratio on every media slot, and copy short enough that no slide is much taller
   than the others. `.welcome-viewport` sets a min-height so the short title
   card doesn't leave the buttons floating a long way up on slide 1.
   ──────────────────────────────────────────────────────────────────────────── */

.welcome-dialog { max-width: 21.5rem; }

.welcome-content {
    /* Its own padding throughout — no Bootstrap modal-header/body/footer, so
       none of their borders and none of their spacing to override. */
    overflow: hidden;
    background: var(--bg-primary);
}

/* ── Dots and Skip ── */
.welcome-top {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.7rem 1rem 0.1rem;
}

.welcome-dots { display: flex; gap: 0.35rem; }

/* Tap target is the padding, not the dot: a 6px circle is not something to ask
   a fingertip to hit, but a 6px circle is what should be on screen. */
.welcome-dot {
    appearance: none;
    border: none;
    background: none;
    padding: 0.4rem 0.22rem;
    cursor: pointer;
    line-height: 0;
}
.welcome-dot::before {
    content: "";
    display: block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--border-medium);
    transition: background 0.15s ease, transform 0.15s ease;
}
.welcome-dot.is-active::before {
    background: var(--accent-primary);
    transform: scale(1.3);
}

/* Quiet, but present from the first slide. Someone who doesn't want a tour
   should be able to find the way out without reading the tour to find it. */
.welcome-skip {
    appearance: none;
    border: none;
    background: none;
    padding: 0.2rem 0.1rem;
    font-size: 0.8rem;
    color: var(--text-muted);
    cursor: pointer;
}
.welcome-skip:hover { color: var(--text-secondary); }

/* ── The track ── */
.welcome-viewport {
    overflow: hidden;
    min-height: 16.5rem;
}

.welcome-track {
    display: flex;
    align-items: stretch;
    transition: transform 0.28s ease;
}

/* Flex needs all three parts: `0 0 100%` alone still lets a long slide push
   the pane wider than the viewport in some engines, so the width is pinned. */
.welcome-slide {
    flex: 0 0 100%;
    width: 100%;
    padding: 0.5rem 1.15rem 0.2rem;
}

@media (prefers-reduced-motion: reduce) {
    .welcome-track { transition: none; }
    .welcome-dot::before { transition: none; }
}

/* ── Copy ──
   Sized to be read at arm's length on a phone without making the card tall.
   If a slide ever needs more words than this comfortably holds, the slide is
   wrong — help.html is where the long version belongs. */
.welcome-title {
    font-size: 1.06rem;
    font-weight: 650;
    color: var(--text-primary);
    margin: 0 0 0.4rem;
}

.welcome-lead {
    font-size: 0.875rem;
    line-height: 1.45;
    color: var(--text-secondary);
    margin: 0;
}

.welcome-sub {
    font-size: 0.78rem;
    color: var(--text-muted);
    margin: 0.6rem 0 0;
}

/* The "strong suggestion, not a guarantee" line. Set apart by a rule and a
   size, not by a warning colour or an icon — it is a fact about how to read a
   result, and dressing it as an alert would make the slide end on a worry. */
.welcome-note {
    font-size: 0.76rem;
    line-height: 1.4;
    color: var(--text-muted);
    margin: 0.65rem 0 0;
    padding-top: 0.55rem;
    border-top: 1px solid var(--border-light);
}

/* ── Media slots ──
   One base, four shapes. Each has a fixed aspect ratio so the dialog is the
   same height before and after a real screenshot replaces the placeholder. */
.welcome-media {
    border-radius: 8px;
    overflow: hidden;
    background: var(--bg-secondary);
}

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

/* Dashed and captioned, so an unfilled slot reads as unfinished rather than
   as broken. */
.welcome-media-empty {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    border: 2px dashed var(--border-medium);
    border-radius: 8px;
    color: var(--text-muted);
    font-size: 0.72rem;
    text-align: center;
    padding: 0 0.5rem;
}

/* Slide 2. A blade is a long thin thing and a 16:10 box around one is mostly
   background, so the frame is the shape of the subject. */
.welcome-media--strip {
    aspect-ratio: 5 / 2;
    margin-bottom: 0.85rem;
}

/* Slide 3. Square, because the three of them are being compared and anything
   else makes one look more important than the others. */
.welcome-media--thumb { aspect-ratio: 1 / 1; }

/* Slide 4. The size a tile actually is on the batch page, so it is recognised
   when it turns up there. */
.welcome-media--tile {
    flex: 0 0 5.5rem;
    aspect-ratio: 1 / 1;
}

/* ── Slide 1: a title card ──
   No media frame at all. The mark is the picture, and a dashed box around it
   would make the app's own logo look like a missing image. */
.welcome-slide--title {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 0.1rem;
    padding-bottom: 0.6rem;
}
.welcome-mark { width: 54px; height: 54px; margin-bottom: 0.7rem; }
.welcome-slide--title .welcome-lead { max-width: 17rem; }

/* ── Slide 3: the examples row ── */
.welcome-examples {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.45rem;
    margin-top: 0.8rem;
}
.welcome-example { margin: 0; }
.welcome-example figcaption {
    margin-top: 0.35rem;
    font-size: 0.68rem;
    line-height: 1.25;
    color: var(--text-muted);
}
/* Green tick, red cross — the only colour on any slide, and it is carrying
   meaning rather than decorating. */
.welcome-mark-yes { color: var(--success); font-weight: 700; }
.welcome-mark-no { color: var(--danger); font-weight: 700; }

/* ── Slide 4: picture beside the words ── */
.welcome-slide--split {
    display: flex;
    align-items: center;
    gap: 0.85rem;
}
.welcome-split-copy { min-width: 0; }

/* ── Footer ── */
.welcome-nav {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.7rem 1.15rem 0.9rem;
}

/* An auto margin rather than a spacer element, so the row still separates
   correctly when it wraps on a phone. Both carry it: whichever is the first
   one visible absorbs the gap, which keeps Back on the left whether the row
   ends in Next or in the pair on the last slide. */
.welcome-later,
.welcome-next { margin-left: auto; }

.welcome-back,
.welcome-later {
    color: var(--text-secondary);
    text-decoration: none;
    padding-left: 0;
    padding-right: 0;
    font-size: 0.85rem;
}
.welcome-back:hover,
.welcome-later:hover { color: var(--text-primary); }

/* modern.css sets an explicit .btn padding that beats Bootstrap's .btn-sm, so
   the tour's buttons are sized here or they arrive full size in a card this
   narrow. */
.welcome-nav .btn-primary {
    padding: 0.42rem 0.95rem;
    font-size: 0.85rem;
}

/* The card is already narrow enough to sit inside a phone with room either
   side, so the phone layout differs only where the last slide needs it: the
   action full width, the way out under it. */
@media (max-width: 575.98px) {
    /* Keep the width — it is the whole fix. Bootstrap's phone rule is a flat
       `margin: .5rem`, which with a max-width would pin the card to the left
       of a wider phone, so the auto is restored here. */
    .welcome-dialog { margin: 0.75rem auto; }
    .welcome-nav { flex-wrap: wrap; }
    .welcome-finish { flex: 1 1 100%; order: 1; }
    .welcome-later { flex: 1 1 100%; order: 2; margin-left: 0; text-align: center; }
    .welcome-back { order: 3; }
}
