/* === Theme tokens ============================================
   Tokens are layered so a single ``[data-theme="..."]`` flip on
   ``<html>`` reskins the whole app.  Defaults below are the
   "forest" palette — desaturated greens, low contrast, easier on
   the eye than the original matrix-terminal look.  Alternate
   palettes live further down (search for ``[data-theme=``).

   Naming conventions:
     --bg               page background
     --surface          card/panel background (one step above bg)
     --surface-2        elevated surface (hover, header, sticky)
     --surface-3        deepest elevation (modal, dropdown)
     --text             primary readable text
     --text-muted       secondary text (labels, captions)
     --text-subtle      faintest text (helper, placeholder)
     --on-accent        text colour to use ON --accent backgrounds
     --accent           primary brand colour
     --accent-hover     hover/active state of accent
     --accent-strong    saturated emphasis (links, highlights)
     --accent-dim       translucent tint of accent (chips, fills)
     --accent-ring      focus ring colour (translucent)
     --border           default border
     --border-strong    emphasised border (focus, selected)
     --success / --warning / --danger / --info  status colours
       (each has a ``-dim`` translucent variant for fill chips)
     --shadow-sm/md/lg  layered drop shadows
     --radius / --radius-sm / --radius-lg  corner radii
     --safe-bottom      iOS safe-area inset
   The historical names ``--panel``, ``--panel-2``, and ``--muted``
   are kept as aliases so older selectors continue to resolve. */
:root,
:root[data-theme="forest"],
[data-theme="forest"] {
  /* Surfaces — warm desaturated greens, no pure black */
  --bg:           #1b231d;
  --surface:      #232c25;
  --surface-2:    #2b362e;
  --surface-3:    #344037;
  /* Text — warm off-white, never neon */
  --text:         #dee5db;
  --text-muted:   #a0aea2;
  --text-subtle:  #788a7c;
  --on-accent:    #15201a;
  /* Accent — sage / leaf, friendlier than Tailwind green-400 */
  --accent:       #88b884;
  --accent-hover: #9fcd9b;
  --accent-strong:#b1d8ad;
  --accent-dim:   rgba(136, 184, 132, 0.16);
  --accent-ring:  rgba(136, 184, 132, 0.38);
  /* Structural */
  --border:       #3a4a3e;
  --border-strong:#52685a;
  /* Status — desaturated so they read as informational, not alarming */
  --success:      #8ec089;
  --success-dim:  rgba(142, 192, 137, 0.18);
  --warning:      #d4b86a;
  --warning-dim:  rgba(212, 184, 106, 0.18);
  --danger:       #d28080;
  --danger-dim:   rgba(210, 128, 128, 0.18);
  --info:         #8aa9c4;
  --info-dim:     rgba(138, 169, 196, 0.18);
  /* Hover tints — subtle "press a layer" on interactive surfaces.
     White-on-dark for dark themes, black-on-light for parchment. */
  --hover:        rgba(255, 255, 255, 0.05);
  --hover-strong: rgba(255, 255, 255, 0.09);
  /* Depth */
  --shadow-sm:    0 1px 2px rgba(0, 0, 0, 0.18);
  --shadow-md:    0 4px 12px rgba(0, 0, 0, 0.28);
  --shadow-lg:    0 12px 32px rgba(0, 0, 0, 0.42);
  /* Geometry */
  --radius:       12px;
  --radius-sm:    8px;
  --radius-lg:    18px;
  --safe-bottom:  env(safe-area-inset-bottom, 0px);
  /* Legacy aliases — keep existing rules working */
  --panel:        var(--surface);
  --panel-2:      var(--surface-2);
  --muted:        var(--text-muted);
  --text-muted-legacy: var(--text-muted);
  color-scheme:   dark;
}

/* ── Matrix terminal — the original look, kept for those who like
   neon-green-on-near-black.  Same token names, different values.
   The non-:root selector lets the theme picker's preview swatches
   render in their target palette regardless of the page theme. */
:root[data-theme="matrix"],
[data-theme="matrix"] {
  --bg:           #0f1f14;
  --surface:      #16331f;
  --surface-2:    #1d4029;
  --surface-3:    #245034;
  --text:         #e6f4ea;
  --text-muted:   #9bbfa6;
  --text-subtle:  #6a8a76;
  --on-accent:    #0a1810;
  --accent:       #4ade80;
  --accent-hover: #22c55e;
  --accent-strong:#86efac;
  --accent-dim:   rgba(74, 222, 128, 0.15);
  --accent-ring:  rgba(74, 222, 128, 0.40);
  --border:       #2a5a3a;
  --border-strong:#3d7c54;
  --success:      #4ade80;
  --success-dim:  rgba(74, 222, 128, 0.18);
  --warning:      #facc15;
  --warning-dim:  rgba(250, 204, 21, 0.18);
  --danger:       #ef4444;
  --danger-dim:   rgba(239, 68, 68, 0.18);
  --info:         #93c5fd;
  --info-dim:     rgba(147, 197, 253, 0.18);
  --hover:        rgba(255, 255, 255, 0.06);
  --hover-strong: rgba(255, 255, 255, 0.10);
  --shadow-sm:    0 1px 2px rgba(0, 0, 0, 0.40);
  --shadow-md:    0 4px 12px rgba(0, 0, 0, 0.55);
  --shadow-lg:    0 12px 32px rgba(0, 0, 0, 0.70);
  --panel:        var(--surface);
  --panel-2:      var(--surface-2);
  --muted:        var(--text-muted);
  color-scheme:   dark;
}

/* ── Slate — neutral dark theme, no green.  For users who want
   "just a dark UI" without a colour personality. */
:root[data-theme="slate"],
[data-theme="slate"] {
  --bg:           #15181c;
  --surface:      #1d2128;
  --surface-2:    #262b34;
  --surface-3:    #303744;
  --text:         #e2e6ec;
  --text-muted:   #98a1ad;
  --text-subtle:  #6b7280;
  --on-accent:    #0f1115;
  --accent:       #7aa2d6;
  --accent-hover: #93b6e3;
  --accent-strong:#b0caec;
  --accent-dim:   rgba(122, 162, 214, 0.16);
  --accent-ring:  rgba(122, 162, 214, 0.40);
  --border:       #343b47;
  --border-strong:#4a5566;
  --success:      #7fbf9c;
  --success-dim:  rgba(127, 191, 156, 0.18);
  --warning:      #d4b86a;
  --warning-dim:  rgba(212, 184, 106, 0.18);
  --danger:       #d28080;
  --danger-dim:   rgba(210, 128, 128, 0.18);
  --info:         #8aa9c4;
  --info-dim:     rgba(138, 169, 196, 0.18);
  --hover:        rgba(255, 255, 255, 0.05);
  --hover-strong: rgba(255, 255, 255, 0.09);
  --shadow-sm:    0 1px 2px rgba(0, 0, 0, 0.30);
  --shadow-md:    0 4px 12px rgba(0, 0, 0, 0.45);
  --shadow-lg:    0 12px 32px rgba(0, 0, 0, 0.60);
  --panel:        var(--surface);
  --panel-2:      var(--surface-2);
  --muted:        var(--text-muted);
  color-scheme:   dark;
}

/* ── Parchment — light theme on a cream page.  Same forest accent
   family, inverted for daytime reading. */
:root[data-theme="parchment"],
[data-theme="parchment"] {
  --bg:           #f4efe2;
  --surface:      #ece5d2;
  --surface-2:    #e0d8c0;
  --surface-3:    #d4caaa;
  --text:         #2a3327;
  --text-muted:   #5c6a58;
  --text-subtle:  #8a9683;
  --on-accent:    #f7f3e8;
  --accent:       #3d6c3a;
  --accent-hover: #2c5429;
  --accent-strong:#1f3f1c;
  --accent-dim:   rgba(61, 108, 58, 0.14);
  --accent-ring:  rgba(61, 108, 58, 0.32);
  --border:       #c4baa0;
  --border-strong:#a5997c;
  --success:      #3d8a4a;
  --success-dim:  rgba(61, 138, 74, 0.16);
  --warning:      #a07c1e;
  --warning-dim:  rgba(160, 124, 30, 0.16);
  --danger:       #a84444;
  --danger-dim:   rgba(168, 68, 68, 0.16);
  --info:         #2c5b86;
  --info-dim:     rgba(44, 91, 134, 0.14);
  --hover:        rgba(40, 30, 10, 0.05);
  --hover-strong: rgba(40, 30, 10, 0.09);
  --shadow-sm:    0 1px 2px rgba(40, 30, 10, 0.10);
  --shadow-md:    0 4px 12px rgba(40, 30, 10, 0.16);
  --shadow-lg:    0 12px 32px rgba(40, 30, 10, 0.22);
  --panel:        var(--surface);
  --panel-2:      var(--surface-2);
  --muted:        var(--text-muted);
  color-scheme:   light;
}

/* === Reset ================================================== */

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

/* Default link color — explicit so browsers don't fall back to blue/purple
   on visited state, which is unreadable on the dark green theme.
   Wrapped in ``:where()`` so the rule has zero specificity: any
   class on an anchor (``.landing-cta-primary``, ``.about-signin``,
   ``.btn-primary`` used as ``<a>``, etc.) wins without needing its
   own ``:visited`` override.  Without this, a bare ``a:visited``
   selector outranks single-class colour rules (1 type + 1 pseudo-
   class = 11, vs 1 class = 10) and after first visit a green-on-
   green CTA button has its text disappear.  Specific surfaces
   (.box-card, .header-back, room-chip, etc.) still override their
   color independently. */
:where(a) { color: var(--accent); }
:where(a:visited) { color: var(--accent); }
:where(a:hover) { color: var(--accent-hover); }

/* All <dialog> elements opened via showModal() — explicitly center.
   Browser defaults vary (Safari and some Chromium configs render them
   anchored to the top-left when there are fixed-position ancestors or
   custom max-height rules), so set position + inset + margin:auto for
   reliable centering across the app. */
dialog {
  position: fixed;
  inset: 0;
  margin: auto;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
  -webkit-text-size-adjust: 100%;
  padding-bottom: calc(64px + var(--safe-bottom));
}

/* Make text feel slightly lighter than the old hard-contrast dark
   theme.  ``font-weight: 400`` is the default but some browsers
   bump it via user-agent on dark backgrounds; lock it. */
body, input, textarea, select, button {
  font-weight: 400;
}

/* === Top Header === */
header {
  background: var(--panel);
  padding: 0.75rem 1rem;
  /* Was ``2px solid var(--accent)`` — the heavy neon strip under
     the header was the loudest single element in the matrix look.
     Drop to 1px of border + a subtle shadow so the header still
     reads as a sticky surface without yelling. */
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  position: sticky;
  top: 0;
  z-index: 100;
}
header h1 { font-size: 1.2rem; line-height: 1; }
header h1 a { color: var(--accent); text-decoration: none; }

/* ── Tenant switcher (header dropdown) ──────────────────────────
   CSS-only ``<details>`` dropdown so it works without JS.  Active
   tenant's avatar + name sit in the trigger; clicking opens a
   menu listing all memberships with role badges.  Scoped tight
   to the header so it never bleeds into in-page <details>. */
/* === Account menu (top-right) ================================
   Always-visible dropdown for signed-in users.  Wraps the
   per-tenant switcher (when applicable) + a Sign-out link.  The
   inner ``.tenant-switcher-item`` styles below are still in use
   inside the panel — selectors kept stable for the existing
   tenant-switcher tests. */
.account-menu {
  margin-left: auto;
  position: relative;
}
.account-menu > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.6rem;
  border-radius: 0.4rem;
  background: var(--hover);
  border: 1px solid var(--border);
  font-size: 0.85rem;
  user-select: none;
}
.account-menu > summary::-webkit-details-marker { display: none; }
.account-menu > summary:hover {
  background: var(--hover-strong);
}
.account-menu-avatar {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--accent);
  color: var(--panel);
  font-weight: 700;
  font-size: 0.8rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.account-menu-name {
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.account-menu-chevron { opacity: 0.6; font-size: 0.7rem; }
.account-menu-panel {
  position: absolute;
  right: 0;
  top: calc(100% + 0.3rem);
  min-width: 15rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 0.4rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
  padding: 0.25rem;
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.account-menu-header {
  padding: 0.5rem 0.6rem 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.2rem;
}
.account-menu-header-email {
  font-size: 0.85rem;
  word-break: break-all;
  overflow-wrap: anywhere;
  min-width: 0;
}
.account-menu-section {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding-bottom: 0.2rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 0.2rem;
}
.account-menu-section-label {
  padding: 0.2rem 0.55rem 0.1rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 0.7rem;
}
.account-menu-signout {
  display: block;
  padding: 0.5rem 0.6rem;
  color: var(--text);
  text-decoration: none;
  font-size: 0.9rem;
  border-radius: 0.3rem;
}
.account-menu-signout:hover {
  background: var(--hover);
  color: var(--danger, #d97a7a);
}

/* === Global drag-drop ingest overlay + toast =================
   Triggered from the file-drag handlers in base.html.  Visible
   on dragenter (anywhere on the page), hides on drop / leave.
   Mobile users don't see it because touch doesn't drag files —
   the regular /ingest page stays the canonical mobile entry. */
.global-drop-overlay {
  position: fixed;
  inset: 0;
  z-index: 9000;
  background: rgba(0, 0, 0, 0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  /* ``pointer-events: none`` is load-bearing.  If the overlay
     ever gets stuck on screen (browser drag-state quirk), the
     user can still click through to the app underneath.  The
     dragover-timeout in base.html should hide it within 200 ms
     anyway, but this is the belt-and-suspenders guarantee that
     a stuck overlay never blocks the whole app. */
  pointer-events: none;
  backdrop-filter: blur(2px);
}
/* CRITICAL — without this rule the ``display: flex`` above
   overrides the UA's ``[hidden] { display: none }`` and the
   overlay renders on every page load (the emergency-fix
   regression: users couldn't use the app because the dim + blur
   was always on screen).  Higher specificity (class + attribute)
   beats the class-only rule above without needing !important. */
.global-drop-overlay[hidden] {
  display: none;
}
.global-drop-hint {
  /* The hint card IS clickable — clicking it dismisses the
     overlay (escape hatch).  Everything else stays
     click-through to the underlying app. */
  pointer-events: auto;
  cursor: pointer;
  text-align: center;
  padding: 2rem 2.5rem;
  border: 3px dashed var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
  max-width: min(28rem, calc(100vw - 2rem));
}
.global-drop-glyph {
  font-size: 3.5rem;
  margin-bottom: 0.5rem;
}
.global-drop-title {
  margin: 0 0 0.4rem;
  font-size: 1.25rem;
  color: var(--accent);
}
.global-drop-sub { margin: 0; }

.global-drop-toast {
  position: fixed;
  bottom: calc(1rem + var(--safe-bottom, 0px));
  right: 1rem;
  z-index: 9001;
  padding: 0.7rem 1rem;
  border-radius: var(--radius-sm);
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  font-size: 0.9rem;
  max-width: min(26rem, calc(100vw - 2rem));
}
.global-drop-toast a {
  color: var(--accent);
  font-weight: 600;
  text-decoration: none;
  margin-left: 0.2rem;
}
.global-drop-toast a:hover { text-decoration: underline; }
.global-drop-toast[data-kind="success"] {
  border-left-color: var(--success, #5ec47a);
}
.global-drop-toast[data-kind="error"] {
  border-left-color: var(--danger, #d97a7a);
}
.global-drop-toast[data-kind="progress"]::before {
  content: '⏳ ';
  margin-right: 0.25rem;
}

/* === Version-update toast (auto-refresh on backend rollover) ===
   Sits on /maintenance + /admin/maintenance.  When the page is
   open during a release rollover, version-watch.js polls the
   /maintenance/version endpoint, detects when the running version
   shifts (watchtower pulled a new image + recreated the container),
   and shows this toast for a beat before reloading.

   Slide-down + fade-in transition so it doesn't pop in abruptly —
   the operator gets a moment to see "Updated to X.Y.Z" before the
   page actually refreshes.
*/
.version-update-toast {
  position: fixed;
  top: calc(1rem + var(--safe-top, 0px));
  left: 50%;
  transform: translate(-50%, -150%);
  z-index: 9001;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.65rem 1.1rem;
  border-radius: var(--radius-sm);
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 4px solid var(--success, #5ec47a);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
  font-size: 0.9rem;
  max-width: min(26rem, calc(100vw - 2rem));
  opacity: 0;
  transition:
    transform 320ms cubic-bezier(0.22, 1, 0.36, 1),
    opacity 240ms ease-out;
}
.version-update-toast.visible {
  transform: translate(-50%, 0);
  opacity: 1;
}
.version-update-toast-spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: version-update-spin 0.6s linear infinite;
}
@keyframes version-update-spin {
  to { transform: rotate(360deg); }
}

/* === Marketing funnel widget (admin /admin#marketing) =======
   Shows the 30-day funnel as a row of stat cards (one per
   step), followed by a per-page interest table and a recent-
   sessions list.  Data is first-party, consent-gated; see
   dao/marketing.py + static/marketing-track.js.
*/
.admin-funnel-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.7rem;
  margin: 0.75rem 0 1.25rem;
}
.admin-funnel-cell {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.7rem 0.85rem 0.85rem;
}
.admin-funnel-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}
.admin-funnel-value {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--text);
  line-height: 1.1;
}
.admin-funnel-sub {
  margin-top: 0.2rem;
}
.admin-funnel-cell-good {
  border-left: 4px solid var(--success, #5ec47a);
}
.admin-funnel-cell-warn {
  border-left: 4px solid var(--warning, #d99c4d);
}
.admin-funnel-cell-attention {
  background: color-mix(in srgb, var(--warning, #d99c4d) 10%, var(--surface));
}
.admin-funnel-tag {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 0.78rem;
  color: var(--text-muted);
}
.admin-funnel-tag-good {
  background: color-mix(in srgb, var(--success, #5ec47a) 18%, var(--surface));
  border-color: var(--success, #5ec47a);
  color: var(--text);
}
.admin-funnel-tag-warn {
  background: color-mix(in srgb, var(--warning, #d99c4d) 18%, var(--surface));
  border-color: var(--warning, #d99c4d);
  color: var(--text);
}
.admin-subsection-title {
  margin: 1.25rem 0 0.5rem;
  font-size: 1rem;
  color: var(--accent);
}

/* === First-run "Get started" card ===========================
   Three-step checklist on /home for new tenants — see
   dao/onboarding.py for the state derivation.  Each step has a
   number badge that flips to a ✓ when done; the *next* unfinished
   step gets an active accent border + a primary CTA button.
   Card disappears entirely once all three are done.
*/
.getting-started {
  border-left: 4px solid var(--accent);
  background: linear-gradient(
    140deg,
    var(--surface) 0%,
    color-mix(in srgb, var(--accent) 8%, var(--surface)) 100%
  );
}
.getting-started-head {
  margin-bottom: 0.8rem;
}
.getting-started-title {
  margin: 0 0 0.25rem;
  font-size: 1.15rem;
  color: var(--accent);
}
.getting-started-steps {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.getting-started-step {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.65rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  transition: border-color 0.15s, background 0.15s;
}
.getting-started-step-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
.getting-started-step-done {
  opacity: 0.7;
}
.getting-started-step-done .getting-started-step-title {
  text-decoration: line-through;
  color: var(--text-muted);
}
.getting-started-check {
  flex: 0 0 28px;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--text-muted);
}
.getting-started-step-done .getting-started-check {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.getting-started-step-active .getting-started-check {
  border-color: var(--accent);
  color: var(--accent);
}
.getting-started-step-body {
  flex: 1 1 auto;
  min-width: 0;
}
.getting-started-step-title {
  font-weight: 600;
  margin-bottom: 0.1rem;
}
.getting-started-step-hint {
  margin-bottom: 0.4rem;
}
.getting-started-cta {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.2rem;
}
.getting-started-done {
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  border-left: 4px solid var(--accent);
  font-size: 0.95rem;
  padding: 0.7rem 0.95rem;
}
.getting-started-done a {
  color: var(--accent);
  font-weight: 600;
}

/* === Two-step room picker (feedback #71) ====================
   On /boxes/{id}'s edit form.  Click the current-room chip to
   open a dropdown panel; pick a location, then a room.  If
   there's only one location, the location step is skipped and
   the panel shows rooms directly.  Hidden input carries the
   selected room_id into the surrounding form's Save action.
*/
.form-field-label {
  display: block;
  font-size: 0.85rem;
  margin: 0.5rem 0 0.3rem;
  color: var(--text-muted);
}
.room-picker {
  position: relative;
  margin-bottom: 0.6rem;
}
.room-picker-current {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text);
  text-align: left;
  font-family: inherit;
  font-size: 0.95rem;
  cursor: pointer;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.room-picker-current:hover {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
.room-picker-current[aria-expanded="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
.room-picker-current-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.15rem 0.55rem;
  border-radius: 999px;
  background: color-mix(in srgb, var(--room-color, #4ade80) 22%, transparent);
  border: 1px solid color-mix(in srgb, var(--room-color, #4ade80) 50%, var(--border));
  color: var(--text);
  font-weight: 600;
}
.room-picker-chevron {
  margin-left: auto;
  color: var(--text-muted);
  transition: transform 0.15s ease;
}
.room-picker-current[aria-expanded="true"] .room-picker-chevron {
  transform: rotate(180deg);
}
.room-picker-panel {
  position: absolute;
  top: calc(100% + 0.3rem);
  left: 0;
  right: 0;
  z-index: 50;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.65rem 0.7rem 0.75rem;
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
  max-height: 60vh;
  overflow-y: auto;
}
.room-picker-step {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.room-picker-step-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.room-picker-back {
  align-self: flex-start;
  background: transparent;
  border: none;
  color: var(--accent);
  font-size: 0.85rem;
  font-family: inherit;
  cursor: pointer;
  padding: 0.2rem 0;
}
.room-picker-back:hover { text-decoration: underline; }
.room-picker-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}
.room-picker-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font-family: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  transition: border-color 0.12s, transform 0.08s, background 0.12s;
}
.room-picker-chip:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}
.room-picker-chip:active { transform: translateY(0); }
.room-picker-chip-room {
  border-left: 4px solid var(--room-color, var(--accent));
}
.room-picker-chip-current {
  background: color-mix(in srgb, var(--accent) 15%, var(--surface));
  border-color: var(--accent);
  font-weight: 600;
}
.room-picker-chip-clear {
  color: var(--text-muted);
  font-style: italic;
}

/* === Loose-tray sidebar (Nondre's first-user thread) ==========
   Right-edge floating tray of items currently parked in a
   ``is_loose=1`` box ("in a room but no specific box yet").
   Persistent across pages so the workflow survives navigation;
   drag a thumbnail onto any box card / box tile on the page to
   put it there.

   **Hidden on phones.**  Cross-page drag from a touch device is
   clunky at best, and the sidebar would just steal screen real
   estate from the actual content.  Surfaces the same items via
   /queue + per-box views as before.  Re-enable at 720 px (the
   project's mobile breakpoint). */
.loose-tray {
  display: none;
}
@media (min-width: 720px) {
  .loose-tray {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    position: fixed;
    right: 1rem;
    bottom: calc(1rem + var(--safe-bottom, 0px));
    z-index: 150;
    max-height: calc(100vh - 6rem);
    pointer-events: none;
  }
  .loose-tray > * { pointer-events: auto; }
}
.loose-tray-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.85rem;
  background: var(--accent);
  color: var(--on-accent);
  border: none;
  border-radius: 2rem;
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
}
.loose-tray-toggle:hover { filter: brightness(1.08); }
.loose-tray-toggle-glyph { font-size: 1.05rem; }
.loose-tray-toggle-count {
  background: rgba(0, 0, 0, 0.25);
  padding: 0.05rem 0.5rem;
  border-radius: 1rem;
  font-variant-numeric: tabular-nums;
  min-width: 1.75rem;
  text-align: center;
}
.loose-tray-open .loose-tray-toggle {
  border-radius: 2rem 2rem 0 0;
  box-shadow: none;
}
.loose-tray-panel {
  width: 18rem;
  max-height: calc(100vh - 8rem);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius) 0 var(--radius) var(--radius);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  overflow: hidden;
  margin-bottom: -1px;  /* meet the toggle's bottom border */
}
/* Same gotcha as ``.global-drop-overlay[hidden]`` — the ``display:
   flex`` above would otherwise win against the UA's hidden rule,
   leaving the panel visible whether collapsed or expanded. */
.loose-tray-panel[hidden] {
  display: none;
}
.loose-tray-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
}
.loose-tray-close {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.4rem;
}
.loose-tray-close:hover { color: var(--text); }
.loose-tray-hint { margin: 0; }
.loose-tray-list {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  overflow-y: auto;
  min-height: 0;
  padding-right: 0.2rem;
}
.loose-tray-more { margin: 0; text-align: center; }
.loose-tray-card {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  padding: 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--surface);
  border: 1px solid var(--border);
  cursor: grab;
  user-select: none;
}
.loose-tray-card:hover { border-color: var(--accent); }
.loose-tray-card:active { cursor: grabbing; }
.loose-tray-card-dragging {
  opacity: 0.4;
  cursor: grabbing;
}
.loose-tray-card-thumb {
  width: 2.6rem;
  height: 2.6rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}
.loose-tray-card-thumb-empty {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--surface-2);
  font-size: 1.5rem;
}
.loose-tray-card-text {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.loose-tray-card-name {
  font-size: 0.9rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 11rem;
}
/* Drop target highlight (applied to ``[data-box-id]`` elements
   when a loose-tray drag is hovering them). */
.loose-drop-target {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
  background: var(--accent-dim);
}
.loose-drop-success {
  outline: 2px solid var(--success, #5ec47a);
  outline-offset: 2px;
}
/* When a drag is active, give every existing box-id surface a
   subtle scaffold tint so the user can find drop targets at a
   glance — even ones currently scrolled off the visible page. */
body.loose-drag-active [data-box-id] {
  box-shadow: 0 0 0 1px var(--accent-dim);
}
.tenant-switcher-item { margin: 0; }
.tenant-switcher-item button {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: transparent;
  border: none;
  color: var(--text);
  padding: 0.45rem 0.55rem;
  border-radius: 0.3rem;
  cursor: pointer;
  text-align: left;
  font: inherit;
}
.tenant-switcher-item button:hover:not(:disabled) {
  background: var(--hover);
}
.tenant-switcher-item button:disabled {
  cursor: default;
  opacity: 0.95;
}
.tenant-switcher-item[data-active="1"] button {
  background: var(--hover);
}
.tenant-switcher-item-avatar {
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--border);
  color: var(--text-muted, #aaa);
  font-size: 0.7rem;
  font-weight: 700;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.tenant-switcher-item[data-active="1"] .tenant-switcher-item-avatar {
  background: var(--accent);
  color: var(--panel);
}
.tenant-switcher-item-text {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  min-width: 0;
}
.tenant-switcher-item-text strong {
  font-size: 0.9rem;
  font-weight: 600;
}
.tenant-switcher-active-mark {
  color: var(--accent);
  font-size: 0.9rem;
}
.tenant-switcher-shared {
  display: block;
  padding: 0.45rem 0.55rem;
  border-top: 1px solid var(--border);
  margin-top: 0.15rem;
  color: var(--text);
  text-decoration: none;
  font-size: 0.85rem;
}
.tenant-switcher-shared:hover {
  background: var(--hover);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--accent);
  text-decoration: none;
}
.brand-mark {
  width: 28px;
  height: 22px;
  display: block;
  flex-shrink: 0;
  transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Cute reaction when you mouse over the brand — the turtle pokes its head
   out a little. Pure CSS, no JS. */
.brand:hover .brand-mark { transform: translateX(2px); }
.header-nav { display: none; }
.header-back { color: var(--muted); text-decoration: none; font-size: 0.9rem; }
.header-back:hover { color: var(--accent); }
.header-actions { margin-left: auto; display: flex; gap: 0.5rem; }

/* === Bottom Tab Bar === */
.tab-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: var(--panel);
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: space-around;
  padding: 0.4rem 0 calc(0.4rem + var(--safe-bottom));
  z-index: 100;
}
.tab-bar a,
.tab-bar button.tab-bar-more {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  text-decoration: none;
  color: var(--muted);
  font-size: 0.65rem;
  padding: 0.35rem 0.5rem 0.25rem;
  min-width: 48px;
  min-height: 44px;
  justify-content: center;
  /* Reset button-specific browser defaults so the More tab
     visually matches the <a> tabs around it. */
  background: none;
  border: 0;
  cursor: pointer;
  font-family: inherit;
  /* For the ``::before`` accent-stripe positioning when active. */
  position: relative;
}
/* Active-page indication.  The server stamps ``active`` on
   whichever tab matches the current URL prefix (and on the More
   button when the current page lives inside the sheet) — see
   base.html.  We give the active item the accent text colour AND
   a small accent stripe pinned to the top edge so it reads as a
   tab in the iOS sense rather than just "this label looks
   slightly different". */
.tab-bar a.active,
.tab-bar button.tab-bar-more.active { color: var(--accent); }
.tab-bar a.active::before,
.tab-bar button.tab-bar-more.active::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  width: 28px;
  height: 3px;
  background: var(--accent);
  border-radius: 0 0 3px 3px;
}
.tab-bar a:hover, .tab-bar button.tab-bar-more:hover { color: var(--accent); }
.tab-bar button.tab-bar-more[aria-expanded="true"] { color: var(--accent); }
.tab-icon { font-size: 1.3rem; line-height: 1; }

/* === "More" bottom-sheet drawer (mobile secondary nav) ===
   Hidden by default.  Slides up from the bottom when the More
   tab is tapped; backdrop dims the rest of the screen and is
   itself a close affordance.  Auto-hidden on >=640px since the
   header-nav exposes the full set there. */
.more-sheet {
  position: fixed;
  inset: 0;
  z-index: 200;
  pointer-events: none;
  visibility: hidden;
}
.more-sheet.is-open {
  pointer-events: auto;
  visibility: visible;
}
.more-sheet-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  opacity: 0;
  transition: opacity 0.2s;
}
.more-sheet.is-open .more-sheet-backdrop { opacity: 1; }
.more-sheet-panel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  background: var(--panel);
  border-top: 1px solid var(--border);
  border-top-left-radius: 14px;
  border-top-right-radius: 14px;
  display: flex;
  flex-direction: column;
  /* Push content above the tab-bar + safe-area inset so a
     tapped link is never under the fingerprint of the iPhone
     home-indicator strip. */
  padding: 0.5rem 0.75rem calc(0.75rem + var(--safe-bottom));
  transform: translateY(100%);
  transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1);
  max-height: 80vh;
  overflow-y: auto;
}
.more-sheet.is-open .more-sheet-panel { transform: translateY(0); }
.more-sheet-handle {
  width: 40px;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  margin: 0.25rem auto 0.5rem;
}
.more-sheet-panel a,
.more-sheet-panel button.more-sheet-close {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 0.85rem 0.5rem;
  text-decoration: none;
  color: var(--text);
  font-size: 1rem;
  border: 0;
  background: none;
  text-align: left;
  font-family: inherit;
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.more-sheet-panel a:last-of-type { border-bottom: 1px solid var(--border); }
.more-sheet-panel a:hover { color: var(--accent); }
.more-sheet-panel a.active {
  color: var(--accent);
  background: var(--accent-dim);
}
.more-sheet-panel a .tab-icon { font-size: 1.25rem; }
.more-sheet-panel button.more-sheet-close {
  justify-content: center;
  color: var(--muted);
  margin-top: 0.5rem;
  border-bottom: 0;
}
@media (min-width: 640px) {
  .more-sheet, .tab-bar button.tab-bar-more { display: none !important; }
}

/* === Main Content === */
main {
  max-width: 600px;
  margin: 0 auto;
  padding: 0.75rem;
}

/* Wider canvas on bigger screens — the old 720px cap left a lot of empty
   gutters on desktop while box lists stayed in a narrow stripe. */
@media (min-width: 720px) {
  main { max-width: 760px; }
}
@media (min-width: 1100px) {
  main { max-width: 1080px; }
}

/* Pages that want to spread across the full screen (the floorplan, in
   particular — the bigger the canvas, the easier it is to read tiles
   and zoom into items) opt out of the global cap with .main-wide. */
main.main-wide {
  max-width: 100%;
  /* Side padding so cards aren't kissing the screen edge on huge monitors. */
  padding-left: max(0.75rem, env(safe-area-inset-left));
  padding-right: max(0.75rem, env(safe-area-inset-right));
}
@media (min-width: 1100px) {
  main.main-wide { padding-left: 1.5rem; padding-right: 1.5rem; }
}

/* === Cards === */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
  margin-bottom: 0.75rem;
  /* Soft entrance — every card eases up when its details open or content
     swaps. Cheap and gives the surface "movement" without heavy animation. */
  transition: border-color 0.2s, background 0.2s;
}
/* The whole document gets a tasteful fade-in on first paint. */
@keyframes app-fade-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: no-preference) {
  main { animation: app-fade-in 0.25s ease both; }
}
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.card-title {
  font-size: 1rem;
  font-weight: 600;
  color: var(--accent);
  margin-bottom: 0.5rem;
}
/* When used inside .card-header (which already arranges title + actions on
   one line), don't add an extra margin below. */
.card-header .card-title { margin-bottom: 0; }
/* "Tag all" disclosure on box detail: closed = just a button; open =
   inline input + apply button.  Keeps the contents card header tidy
   while leaving the bulk action one tap away. */
.bulk-tag-disclosure summary { list-style: none; cursor: pointer; }
.bulk-tag-disclosure summary::-webkit-details-marker { display: none; }
.bulk-tag-disclosure[open] summary {
  color: var(--muted);
  border-color: var(--border);
}
.bulk-tag-form {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}
.bulk-tag-form input[type="text"] {
  flex: 1 1 12rem;
  min-width: 0;
}

/* AI tag-suggest: a single trigger button followed by a row of
   click-to-apply pills.  Lives on the item-detail dialog and inside
   the bulk-tag disclosure on the contents card. */
.tag-suggest {
  margin-top: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.tag-suggest-trigger {
  align-self: flex-start;
}
.tag-suggest-icon {
  display: inline-block;
  margin-right: 0.25rem;
}
.tag-suggest-results {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}
.tag-suggest-pill {
  background: var(--accent-dim);
  color: var(--accent);
  border: 1px solid transparent;
  border-radius: 999px;
  padding: 0.25rem 0.7rem;
  font-size: 0.85rem;
  cursor: pointer;
  font-family: inherit;
}
.tag-suggest-pill:hover {
  background: var(--accent);
  color: var(--bg);
}
.tag-suggest-empty {
  font-size: 0.85rem;
  color: var(--muted);
}

/* === Feedback widget ===
   Floating launcher (bottom-right on desktop, above the bottom nav
   on mobile) + modal dialog.  The launcher is always visible to
   authenticated tenant members; the dialog opens with the textarea
   focused so a user can start typing without a second tap. */
.feedback-launcher {
  position: fixed;
  right: 1rem;
  bottom: calc(1rem + var(--safe-bottom));
  z-index: 200;
  background: var(--accent);
  color: var(--bg);
  border: none;
  border-radius: 999px;
  padding: 0.6rem 1rem;
  font-family: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.feedback-launcher:hover { transform: translateY(-1px); }
.feedback-launcher-icon { font-size: 1.05rem; }
/* On phones where the bottom nav already lives, scoot the
   launcher above it so it doesn't sit on top of the nav
   buttons.  Also collapse to a circular icon-only button — the
   text label is redundant for the second-most-common phone
   user-flow, and the icon-only shape (with a ring of contrast
   shadow) reads more clearly as a "this is a button" than the
   pill-with-tiny-emoji we shipped first. */
@media (max-width: 639px) {
  .feedback-launcher {
    bottom: calc(72px + var(--safe-bottom));
    right: 0.85rem;
    padding: 0;
    width: 48px;
    height: 48px;
    border-radius: 50%;
    justify-content: center;
    /* Stronger shadow so it reads as floating above content,
       even when scrolled over a busy card. */
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45),
                0 0 0 1px var(--accent-ring);
  }
  .feedback-launcher-icon { font-size: 1.4rem; }
  .feedback-launcher-label { display: none; }
}
.feedback-dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  padding: 0;
  width: min(420px, 100vw - 1.5rem);
  max-height: 85vh;
  overflow: hidden;
}
.feedback-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}
.feedback-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  padding: 1rem 1rem 0.85rem;
  max-height: 85vh;
  overflow: auto;
}
.feedback-dialog-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
}
.feedback-dialog-head h2 {
  font-size: 1rem;
  margin: 0;
  color: var(--accent);
}
.feedback-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.3rem;
}
.feedback-close:hover { color: var(--accent); }
.feedback-field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.feedback-label {
  font-size: 0.85rem;
  color: var(--muted);
}
.feedback-form textarea {
  width: 100%;
  min-height: 100px;
  resize: vertical;
  font-family: inherit;
  padding: 0.5rem;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.feedback-screenshot-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.feedback-screenshot-status {
  font-size: 0.8rem;
  color: var(--muted);
}
.feedback-screenshot-hint {
  margin: -0.25rem 0 0;
  font-style: italic;
}
.feedback-dialog-foot {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border);
}
.feedback-result {
  font-size: 0.85rem;
  margin: 0;
  padding: 0.45rem 0.6rem;
  border-radius: var(--radius-sm);
}
.feedback-result-ok {
  color: var(--accent);
  background: var(--accent-dim);
}
.feedback-result-err {
  color: var(--danger);
  background: var(--danger-dim);
}

/* === /usage facelift ===
   Spec § phase 13: per-tenant home reorganised into named sections.
   The TOC is a horizontal anchor strip at the top; each section
   gets an h2 heading + id so the anchor links scroll cleanly. */
.usage-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0 0 1rem;
  padding: 0.5rem 0.6rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.85rem;
}
.usage-toc a {
  color: var(--accent);
  text-decoration: none;
  padding: 0.15rem 0.5rem;
  border-radius: var(--radius-sm);
}
.usage-toc a:hover { background: var(--accent-dim); }
.usage-section {
  font-size: 0.95rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin: 1.2rem 0 0.4rem;
  padding-top: 0.2rem;
  /* Scroll-margin keeps anchored sections from sitting under the
     top nav when the user jumps via TOC. */
  scroll-margin-top: 1rem;
}
.usage-cost-block .cost-block {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.5rem 1rem;
  margin: 0;
}
.usage-cost-block .cost-block dt {
  font-size: 0.75rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.usage-cost-block .cost-block dd {
  margin: 0;
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--accent);
}

/* "Download my data" vs "Encrypted backup" — two adjacent cards
   that need clear differentiation so users pick the right one.
   The portability card is the primary CTA (most users want this);
   the encrypted backup is recessed visually as the self-host / DR
   path. */
.data-choice-card {
  position: relative;
}
.data-choice-card .data-choice-list {
  list-style: none;
  margin: 0.5rem 0 0.85rem;
  padding: 0;
  font-size: 0.85rem;
  color: var(--muted);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.data-choice-card .data-choice-list li {
  padding-left: 1rem;
  position: relative;
}
.data-choice-card .data-choice-list li::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--accent);
  opacity: 0.6;
}
.data-choice-card-advanced {
  background: var(--panel-2);
  opacity: 0.92;
}
.data-choice-card-advanced .card-title { color: var(--muted); }
.data-choice-badge {
  display: inline-block;
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 0.1rem 0.45rem;
  margin-left: 0.4rem;
  border-radius: 999px;
  background: var(--accent-dim);
  color: var(--accent);
  vertical-align: middle;
}

/* Billing cards on /usage: upgrade CTA (gradient highlight when
   the tenant is free) and the manage-subscription card (recessed
   when they're already paying). */
.billing-card-upgrade {
  background: linear-gradient(
    140deg, var(--panel-2), var(--accent-dim) 200%
  );
  border-color: var(--accent);
}
.billing-card-upgrade .card-title { color: var(--accent); }
.billing-card-pro { background: var(--panel-2); }

/* Admin feedback queue */
.feedback-queue-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.5rem;
  padding: 0.55rem 0;
  border-top: 1px solid var(--border);
  align-items: start;
}
.feedback-queue-row:first-of-type { border-top: 0; }
.feedback-queue-status {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.72rem;
  text-transform: uppercase;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--muted);
}
.feedback-queue-status-open { background: var(--accent-dim); color: var(--accent); }
.feedback-queue-status-accepted { background: var(--info-dim); color: var(--info); }
.feedback-queue-status-rejected { background: var(--danger-dim); color: var(--danger); }
.feedback-queue-status-done { background: var(--success-dim); color: var(--success); }
.feedback-queue-body {
  white-space: pre-wrap;
  font-size: 0.88rem;
}
.feedback-queue-meta {
  font-size: 0.75rem;
  color: var(--muted);
  margin-top: 0.2rem;
}
.feedback-queue-actions {
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
}
.feedback-export-row {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.75rem;
}

/* === Floorplan editor (Fabric.js) ===
   Reached from /floors/{id}/edit-image.  Toolbar sits at the top,
   canvas fills the rest.  Touch-friendly action sizes; tool group
   stays on a single row on phones via horizontal scroll. */
.floor-edit-main {
  /* The editor wants the full window width — opt out of the
     content max-width the rest of the app uses so the floorplan
     canvas can stretch on a desktop. */
  max-width: none !important;
  padding: 0 !important;
}
.floor-edit-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  position: sticky;
  top: 0;
  z-index: 40;
}
.floor-edit-tool-group {
  display: inline-flex;
  gap: 0.25rem;
  padding-right: 0.4rem;
  border-right: 1px solid var(--border);
  flex-shrink: 0;
}
.floor-edit-tool-group:last-child { border-right: 0; padding-right: 0; }
.floor-edit-tool-group-trailing { margin-left: auto; }
.floor-edit-tool {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.7rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
  color: var(--text);
  cursor: pointer;
  font-family: inherit;
  text-decoration: none;
  white-space: nowrap;
}
.floor-edit-tool:hover { border-color: var(--accent); }
.floor-edit-tool.is-active {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--accent);
}
.floor-edit-tool-primary {
  background: var(--accent);
  color: var(--bg);
  border-color: var(--accent);
  font-weight: 700;
}
.floor-edit-tool-primary:hover { filter: brightness(1.05); }
.floor-edit-tool-primary:disabled { opacity: 0.6; cursor: not-allowed; }
.floor-edit-tool-danger { color: var(--danger); }

.floor-edit-swatch {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--swatch, #888);
  cursor: pointer;
  padding: 0;
  flex-shrink: 0;
}
.floor-edit-swatch.is-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-dim);
}

.floor-edit-status {
  padding: 0.3rem 0.8rem;
  font-size: 0.82rem;
  color: var(--muted);
  min-height: 1.4em;
}
.floor-edit-canvas-wrap {
  display: flex;
  justify-content: center;
  padding: 0.6rem;
  background: var(--bg);
  min-height: 50vh;
}
#floor-edit-canvas {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: #fff;
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.3);
  max-width: 100%;
  height: auto;
}

@media (max-width: 720px) {
  .floor-edit-tool span:not([aria-hidden]) {
    /* Drop the label on phones, keep the icon — saves horizontal
       space and the title attr still describes each button. */
    display: none;
  }
  .floor-edit-tool { padding: 0.5rem 0.6rem; }
}

/* === Onboarding tour overlay ===
   First-run walkthrough rendered by a single overlay div in
   base.html.  Spotlight is an absolutely-positioned hole that lets
   the highlighted element shine through; the tooltip floats next
   to it.  When no target element is present (welcome step on the
   home page, or a target that doesn't exist on this tenant's
   data), the tooltip centres on the viewport with the spotlight
   hidden. */
.tour-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: auto;
}
/* No separate ::before backdrop anymore — the spotlight's outer
   ``box-shadow`` is the dimmer.  When a step has no target the
   spotlight gets positioned off-screen so the shadow covers the
   whole viewport with no visible cutout.  Two layers was causing
   the spotlight to flash on/off as one was hidden during step
   transitions; one layer is always visible.

   Feedback #22: the previous treatment used a 3 px accent border
   + 18 px glow — too subtle to read as "look here" on the new
   forest palette, where the sage-green accent sits much closer
   to the page surface than the original neon green.  Boosted
   the ring (4 px) and the glow (40 px), and gave it a slow pulse
   so the eye actually catches the highlight even when the user
   is reading the tooltip text first. */
.tour-spotlight {
  position: absolute;
  border-radius: 8px;
  pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.72),
              0 0 0 4px var(--accent),
              0 0 40px var(--accent),
              0 0 0 2px var(--bg) inset;
  /* Smooth slide between targets so consecutive steps feel
     continuous rather than blinking from one rect to the next. */
  transition: left 0.22s ease, top 0.22s ease,
              width 0.22s ease, height 0.22s ease;
  animation: tour-spotlight-pulse 1.6s ease-in-out infinite;
}
@keyframes tour-spotlight-pulse {
  0%, 100% {
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.72),
                0 0 0 4px var(--accent),
                0 0 32px var(--accent),
                0 0 0 2px var(--bg) inset;
  }
  50% {
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.72),
                0 0 0 5px var(--accent-strong),
                0 0 60px var(--accent),
                0 0 0 2px var(--bg) inset;
  }
}
@media (prefers-reduced-motion: reduce) {
  .tour-spotlight { animation: none; }
}
.tour-tooltip {
  position: absolute;
  width: min(320px, calc(100vw - 16px));
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 0.95rem 0.7rem;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
  pointer-events: auto;
  transition: opacity 0.18s ease;
}
.tour-tooltip-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}
.tour-tooltip-step {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  font-family: ui-monospace, Menlo, monospace;
}
.tour-tooltip-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.3rem;
}
.tour-tooltip-close:hover { color: var(--accent); }
.tour-tooltip-title {
  margin: 0 0 0.35rem;
  font-size: 1rem;
  color: var(--accent);
}
.tour-tooltip-body {
  margin: 0 0 0.7rem;
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--text);
}
.tour-tooltip-foot {
  display: flex;
  justify-content: space-between;
  gap: 0.4rem;
}
/* Earlier passes locked body overflow while a tour was active; that
   caused the page to snap-scroll to top when the tour finished
   (browser restoring layout after overflow:hidden ↔ auto toggle).
   Letting the page scroll keeps user-initiated position stable
   and lets the user scroll past the spotlight if they want to. */

/* Tour management list on /usage */
.tour-catalogue {
  margin: 0.6rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.tour-catalogue-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.tour-catalogue-meta { min-width: 0; }
.tour-catalogue-meta code {
  font-size: 0.78rem;
  background: var(--panel);
  padding: 0.05rem 0.3rem;
  border-radius: var(--radius-sm);
}

/* === /audit Tinder-style swipe ===
   One card at a time, photo-first, three always-visible action
   buttons so accept/reject is never below the fold.  Cards stack
   visually (next card peeks behind the active one) for the "deck"
   feel.  Drag offsets are set inline by the swipe JS; the CSS
   transitions return cards to centre on release. */
.audit-hero {
  margin-bottom: 0.85rem;
  padding: 0.9rem 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.audit-hero-title {
  margin: 0 0 0.3rem;
  font-size: 1.2rem;
  color: var(--accent);
}
.audit-hero-sub {
  margin: 0 0 0.6rem;
  font-size: 0.88rem;
  color: var(--muted);
  max-width: 60ch;
}
.audit-progress { margin-top: 0.4rem; }
.audit-progress-bar {
  height: 6px;
  background: var(--panel-2);
  border-radius: 999px;
  overflow: hidden;
  margin-bottom: 0.25rem;
}
.audit-progress-fill {
  height: 100%;
  background: var(--accent);
  transition: width 0.25s ease-out;
}
.audit-progress-label {
  font-size: 0.82rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.audit-start-card, .audit-complete-card {
  text-align: center;
  padding: 1.6rem 1.2rem;
}
.audit-start-card h2, .audit-complete-card h2 {
  margin: 0 0 0.6rem;
  font-size: 1.3rem;
  color: var(--accent);
}

/* The card deck: position relative so each card sits on top of the
   next.  Min-height ensures the swipe area is generous on phones
   even when the photo is small. */
.audit-deck {
  position: relative;
  margin: 0 auto 1rem;
  width: 100%;
  max-width: 460px;
  min-height: 460px;
  touch-action: pan-y;  /* allow vertical page scroll but let JS
                          handle horizontal drag */
}
.audit-card {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  user-select: none;
  cursor: grab;
  transform: translateY(8px) scale(0.97);
  opacity: 0;
  pointer-events: none;
  transition: transform 0.2s ease-out, opacity 0.2s ease-out;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}
.audit-card.is-active {
  transform: none;
  opacity: 1;
  pointer-events: auto;
  z-index: 2;
}
.audit-card.is-stacked {
  transform: translateY(8px) scale(0.97);
  opacity: 0.6;
  z-index: 1;
}
.audit-card.is-buried {
  opacity: 0;
  z-index: 0;
}
.audit-card.is-gone { display: none; }

.audit-card-photo {
  flex: 1 1 auto;
  min-height: 0;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.audit-card-photo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.audit-card-photo-empty {
  font-size: 4rem;
  color: var(--muted);
  background: var(--panel-2);
}
.audit-card-body {
  padding: 0.85rem 1rem 0.9rem;
  background: var(--panel);
}
.audit-card-name {
  margin: 0;
  font-size: 1.15rem;
  color: var(--text);
}
.audit-card-notes {
  margin: 0.3rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}
.audit-card-meta { margin: 0.4rem 0 0; }

/* Direction-intent feedback during a swipe.  Feedback #34 —
   "swiping was not intuitive to me … I accidentally deleted my
   box while thinking I was accepting everything."  Mid-swipe
   the user needs to know WHICH side they're committing to
   before they let go.  Two changes:

   1. Persistent edge guides on the active card — small labels
      that ALWAYS show "← MISSING" on the left and "FOUND →"
      on the right.  Faint by default; brighten + color-shift
      as the swipe crosses each side's threshold.

   2. The big stamps stay (FOUND / MISSING rotated text), but
      now they fade in earlier (intent > 0.25 instead of 0.4)
      and lock in colour earlier so the user feels the commit
      before release. */
.audit-card-stamps {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.audit-stamp {
  position: absolute;
  top: 1.2rem;
  padding: 0.45rem 0.85rem;
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  border-radius: var(--radius-sm);
  border: 4px solid currentColor;
  opacity: 0;
  transform: rotate(-12deg);
  transition: opacity 0.12s;
  text-transform: uppercase;
}
.audit-stamp-found {
  right: 1rem;
  color: var(--success);
  transform: rotate(-15deg);
}
.audit-stamp-missing {
  left: 1rem;
  color: var(--danger);
  transform: rotate(15deg);
}
.audit-card.intent-right .audit-stamp-found { opacity: 1; }
.audit-card.intent-left .audit-stamp-missing { opacity: 1; }

/* Persistent edge guides — visible at all times on the active
   card so the user knows which direction means what BEFORE
   they start swiping.  Faded by default so they don't fight
   the card content; brighten on intent crossing. */
.audit-edge-guide {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  padding: 0.5rem 0.7rem;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  border-radius: var(--radius-sm);
  background: var(--bg);
  opacity: 0.55;
  pointer-events: none;
  transition: opacity 0.15s, background 0.15s, color 0.15s;
  user-select: none;
  text-transform: uppercase;
}
.audit-edge-guide-missing {
  left: 0.75rem;
  color: var(--danger);
  border: 2px solid var(--danger);
}
.audit-edge-guide-found {
  right: 0.75rem;
  color: var(--success);
  border: 2px solid var(--success);
}
.audit-card.intent-left .audit-edge-guide-missing {
  opacity: 1;
  background: var(--danger);
  color: var(--bg);
}
.audit-card.intent-right .audit-edge-guide-found {
  opacity: 1;
  background: var(--success);
  color: var(--bg);
}
.audit-card-finished {
  position: static;
  text-align: center;
  padding: 2rem 1.2rem;
  transform: none;
  opacity: 1;
}

/* Always-visible action bar — never below the fold.
   ``bottom`` clears the mobile tab-bar (fixed bottom: 0, ~64 px
   tall) AND the iOS safe-area inset.  Without this, the
   action row sat directly underneath the tab-bar's fixed
   strip, hiding Missing / Skip / Found from anyone trying to
   audit on a phone (feedback #33).  The 72 px figure matches
   the value used by the feedback-launcher's mobile media
   query — both clear the tab-bar by ~8 px breathing room. */
.audit-actions {
  position: sticky;
  bottom: calc(72px + var(--safe-bottom, 0px) + 0.5rem);
  display: grid;
  grid-template-columns: 1fr 0.6fr 1fr;
  gap: 0.5rem;
  padding: 0.6rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  z-index: 30;
  box-shadow: 0 -4px 14px rgba(0, 0, 0, 0.25);
}
@media (min-width: 640px) {
  /* Desktop has no bottom tab-bar — restore the smaller offset
     so the action bar sits closer to the bottom of the
     viewport.  Body's padding-bottom is reset to 0 above 640 px
     (see the ``@media (min-width: 640px)`` block near the
     ``main`` section), so we don't need the 72 px clearance. */
  .audit-actions {
    bottom: calc(var(--safe-bottom, 0px) + 0.5rem);
  }
}
.audit-actions.is-busy { opacity: 0.6; pointer-events: none; }
.audit-action {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  padding: 0.7rem 0.5rem;
}
.audit-action-icon { font-size: 1.4rem; line-height: 1; }
.audit-action-label { font-size: 0.85rem; font-weight: 600; }
.audit-action-hint {
  font-size: 0.7rem;
  opacity: 0.5;
  font-family: ui-monospace, Menlo, monospace;
}

@media (max-width: 640px) {
  .audit-deck { min-height: 60vh; }
  .audit-action-hint { display: none; }
}

/* === Public /about pages ===
   Stripe + similar KYC partners need a public-facing surface
   describing the business, pricing, contact, refund policy, etc.
   These pages render WITHOUT the actor-aware base.html so they
   work for unauthenticated visitors.  Layout is intentionally
   plain — readable typography, clear nav, no in-app chrome. */
body.about-public {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               Helvetica, Arial, sans-serif;
  background: var(--bg, #0f1f14);
  color: var(--text, #e6f4ea);
  line-height: 1.5;
}
.about-header {
  /* Desktop: one row — brand on the left, nav in the middle,
     sign-in on the right.  Mobile (≤ 720 px): brand + ☰ + sign-in
     in one tight row, with the nav promoted to a full-width
     drawer beneath when the ☰ checkbox is toggled.
     Padding clamp keeps the inner content aligned with
     ``.about-main`` on wide viewports (the 1080 px inner column
     + 1.5 rem floor side padding align the bar with the body's
     left and right edges on wide desktops) and tightens to
     0.85 rem on phones so the bar doesn't waste horizontal
     space. */
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.7rem max(0.85rem, calc(50% - 540px));
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  background: var(--panel);
  position: sticky;
  top: 0;
  z-index: 10;
}
.about-brand {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--accent, #4ade80);
  text-decoration: none;
  white-space: nowrap;
  flex-shrink: 0;
  order: 1;
}
.about-nav {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
  flex: 1;
  order: 2;
  min-width: 0;
}
.about-nav a {
  color: var(--text, #e6f4ea);
  text-decoration: none;
  font-size: 0.9rem;
  padding: 0.4rem 0.65rem;
  border-radius: 6px;
  transition: background 0.15s, color 0.15s;
}
.about-nav a:hover,
.about-nav a:focus-visible {
  color: var(--accent);
  background: var(--hover);
}
.about-signin {
  padding: 0.55rem 1rem;
  background: var(--accent, #4ade80);
  color: var(--on-accent, var(--bg, #0a1810));
  border-radius: 8px;
  text-decoration: none;
  font-weight: 600;
  white-space: nowrap;
  flex-shrink: 0;
  order: 3;
  transition: background 0.15s, transform 0.1s;
}
.about-signin:hover {
  background: var(--accent-hover, var(--accent));
  transform: translateY(-1px);
}
.about-signin:active { transform: translateY(0); }

/* Mobile menu toggle — checkbox + label hack so the drawer
   works pure-CSS.  Hidden on desktop. */
.about-nav-toggle-input {
  /* Visually hidden but keyboard-focusable. */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.about-nav-toggle {
  display: none;  /* shown only at mobile breakpoint */
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  border-radius: 8px;
  cursor: pointer;
  background: transparent;
  border: 1px solid var(--border);
  font-size: 1.25rem;
  line-height: 1;
  color: var(--text);
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition: background 0.15s, border-color 0.15s;
  order: 2;
  flex-shrink: 0;
}
.about-nav-toggle:hover,
.about-nav-toggle-input:focus-visible + .about-nav-toggle {
  background: var(--hover);
  border-color: var(--border-strong, var(--accent));
}
.about-nav-toggle-icon-close { display: none; }
.about-nav-toggle-input:checked + .about-nav-toggle {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--accent);
}
.about-nav-toggle-input:checked + .about-nav-toggle .about-nav-toggle-icon-open {
  display: none;
}
.about-nav-toggle-input:checked + .about-nav-toggle .about-nav-toggle-icon-close {
  display: inline;
}

@media (max-width: 720px) {
  .about-header {
    flex-wrap: wrap;
    padding: 0.6rem 0.85rem;
    gap: 0.55rem;
  }
  .about-brand {
    font-size: 1.05rem;
    flex: 1;
    min-width: 0;
  }
  .about-nav-toggle { display: inline-flex; order: 2; }
  .about-signin {
    padding: 0.5rem 0.85rem;
    font-size: 0.92rem;
    order: 3;
  }
  /* Nav lives in source order at the END so it can wrap below
     the top row as a full-width drawer.  ``order: 4`` puts it
     last in flex flow regardless of source. */
  .about-header > .about-nav {
    order: 4;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0.1rem;
    padding-top: 0.55rem;
    margin-top: 0.55rem;
    border-top: 1px solid var(--border);
    /* Closed by default; the checkbox toggle reveals it. */
    display: none;
  }
  .about-nav-toggle-input:checked ~ .about-nav {
    display: flex;
  }
  .about-header > .about-nav a {
    padding: 0.75rem 0.5rem;  /* 44 px tap target */
    font-size: 1rem;
    border-radius: 8px;
  }
}
.about-main {
  /* Reading column.  820 → 1080 at wide-desktop breakpoint
     matches the authenticated ``main`` element's 760 → 1080
     progression so the public surface doesn't read as
     dramatically narrower than the rest of the app on the
     same viewport.  Sticky header above uses the same 1080
     center column for its content alignment so the page
     reads as a single centred axis. */
  max-width: 820px;
  margin: 0 auto;
  padding: 2rem 1.5rem 3rem;
}
@media (min-width: 1100px) {
  .about-main { max-width: 1080px; }
}
.about-hero h1 {
  font-size: 2rem;
  color: var(--accent, #4ade80);
  margin: 0 0 0.6rem;
  line-height: 1.2;
}
.about-lede {
  font-size: 1.05rem;
  color: var(--muted, #9bbfa6);
  margin: 0 0 1.5rem;
  max-width: 65ch;
}
.about-section { margin: 2rem 0; }
.about-section h2 {
  font-size: 1.25rem;
  color: var(--accent, #4ade80);
  margin: 0 0 0.5rem;
  border-bottom: 1px solid var(--border, #2a5a3a);
  padding-bottom: 0.3rem;
}
.about-section p { margin: 0.7rem 0; }
.about-section ul {
  padding-left: 1.4rem;
  margin: 0.6rem 0;
}
.about-section ul li { margin: 0.35rem 0; }
.about-section a {
  color: var(--accent, #4ade80);
}
.about-features ul {
  padding-left: 1.4rem;
}
.about-features li { margin: 0.55rem 0; }

.about-plans-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem;
  margin: 1.5rem 0;
}
.about-plan {
  background: var(--panel, #16331f);
  border: 1px solid var(--border, #2a5a3a);
  border-radius: 12px;
  padding: 1.25rem 1.35rem 1.4rem;
}
.about-plan h2 {
  margin: 0 0 0.4rem;
  font-size: 1.4rem;
  color: var(--accent, #4ade80);
  border: 0;
  padding: 0;
}
.about-plan-price {
  font-size: 1.8rem;
  font-weight: 700;
  margin: 0 0 1rem;
  color: var(--text, #e6f4ea);
}
.about-plan ul {
  padding-left: 1.2rem;
  margin: 0.5rem 0 0.8rem;
}
.about-plan-pro {
  border-color: var(--accent);
  background: linear-gradient(140deg,
              var(--panel),
              var(--accent-dim));
}
.about-plan-capacity,
.about-capacity-line {
  margin: 0 0 1rem;
  padding: 0.55rem 0.75rem;
  border-left: 3px solid var(--accent, #4ade80);
  background: var(--panel-2, rgba(74, 222, 128, 0.06));
  border-radius: 0 4px 4px 0;
}

.about-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.5rem;
  font-size: 0.9rem;
}
.about-table th, .about-table td {
  border-bottom: 1px solid var(--border, #2a5a3a);
  padding: 0.55rem 0.6rem;
  text-align: left;
  vertical-align: top;
}
.about-table th { color: var(--accent, #4ade80); font-weight: 600; }
/* Cost-ledger table on /about/transparency: tinted rows make the
   labor + tax + remainder lines easy to spot among the vendor
   bills.  Subtle enough that the table reads as one ledger, not
   a heatmap. */
.about-cost-table td:nth-child(2) {
  font-family: ui-monospace, Menlo, monospace;
  white-space: nowrap;
}
.about-cost-row-tax    td { background: var(--warning-dim); }
.about-cost-row-labor  td { background: var(--info-dim); }
.about-cost-row-remainder td { background: var(--accent-dim); }

/* Mobile (#57) — the 4-column ledger tables on /about/transparency
   crushed the "What it covers" description column into a 60-px
   sliver on 384-px phones, rendering one word per line.  Two
   stacked treatments depending on cell type:

   * The numeric cells (Today / At scale / Stripe / etc.) sit in
     ``.about-cost-table td:nth-child(2)`` and have
     ``white-space: nowrap`` set above, so they can't be shrunk.
     Wrap the whole table in horizontal scroll so the user can
     pan to the description column instead of reading one-word
     lines.
   * Tighten cell padding so the unscrolled viewport shows as
     much as possible up-front. */
@media (max-width: 720px) {
  .about-table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* min-width prevents the description column from collapsing
       below readable width.  Lets the user scroll to read it
       instead of squinting at one-word lines. */
    min-width: 0;
  }
  .about-table thead, .about-table tbody, .about-table tr {
    /* ``display: block`` on .about-table broke the implicit
       table layout — re-establish via inline-table on the rows
       so the columns still align. */
    display: table-row-group;
  }
  .about-table tr { display: table-row; }
  .about-table th, .about-table td {
    padding: 0.45rem 0.5rem;
    font-size: 0.85rem;
  }
  /* Pin a sane minimum width for description columns ("Where it
     goes" / "What it covers") so the column doesn't get squeezed
     by the other three; the horizontal scroll above lets the user
     pan to read them. */
  .about-table td:last-child,
  .about-table th:last-child {
    min-width: 14rem;
  }
}

.about-contact-card {
  background: var(--panel, #16331f);
  border: 1px solid var(--border, #2a5a3a);
  border-radius: 12px;
  padding: 1.2rem 1.4rem;
}
.about-contact-email {
  font-size: 1.3rem;
  font-weight: 600;
  margin: 0.5rem 0;
}
.about-contact-email a {
  color: var(--accent, #4ade80);
  text-decoration: none;
}

.about-footer {
  padding: 1.75rem 1rem 2.5rem;
  color: var(--muted, #9bbfa6);
  font-size: 0.85rem;
  border-top: 1px solid var(--border, #2a5a3a);
  margin-top: 2rem;
}
.about-footer-row {
  text-align: center;
  margin: 0 0 0.5rem;
}
.about-footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.1rem;
  justify-content: center;
}
.about-footer-links a {
  color: var(--accent);
  text-decoration: none;
  padding: 0.35rem 0;
  /* Phone-friendly tap targets without making desktop links feel
     gappy — the gap above spaces siblings; the padding gives each
     link its own non-overlapping click box. */
}
.about-footer-links a:hover,
.about-footer-links a:focus-visible {
  text-decoration: underline;
}

/* === Public landing (/) =====================================
   The marketing-side home page that unauthenticated visitors
   land on.  Extends about/_layout.html so it inherits the
   public header + footer; these classes style the inner content
   (hero, feature grid, transparency callout, final CTA). */
.landing-hero {
  text-align: center;
  /* Clamp shrinks the vertical padding on phones so the hero
     doesn't eat half the viewport before the headline lands;
     stretches comfortably on desktop. */
  padding: clamp(1.5rem, 5vw, 2.75rem) 0 clamp(1.25rem, 4vw, 2.25rem);
  max-width: 720px;
  margin: 0 auto;
}
.landing-eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.12em;
  font-size: clamp(0.72rem, 1.6vw, 0.82rem);
  color: var(--accent);
  margin: 0 0 0.55rem;
  font-weight: 600;
}
.landing-headline {
  font-size: clamp(2rem, 6vw, 2.85rem);
  line-height: 1.12;
  letter-spacing: -0.01em;
  margin: 0 0 0.85rem;
  color: var(--text);
  border: 0;
  /* Long headlines on the public landing can break awkwardly on
     phones if a single word lands alone on the last line.  ``balance``
     evens the line lengths so the headline reads as a deliberate
     unit on every viewport. */
  text-wrap: balance;
}
.landing-sub {
  font-size: clamp(0.98rem, 2.2vw, 1.1rem);
  line-height: 1.55;
  color: var(--text-muted);
  max-width: 56ch;
  margin: 0 auto 1.4rem;
  text-wrap: pretty;
}
.landing-cta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  justify-content: center;
  margin-bottom: 0.75rem;
}
.landing-cta-primary,
.landing-cta-secondary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.8rem 1.5rem;
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-weight: 600;
  text-decoration: none;
  border: 1px solid transparent;
  transition: background 0.15s, transform 0.1s, border-color 0.15s;
}
.landing-cta-primary {
  background: var(--accent);
  color: var(--on-accent);
}
.landing-cta-primary:hover {
  background: var(--accent-hover);
  transform: translateY(-1px);
}
.landing-cta-secondary {
  background: transparent;
  color: var(--accent);
  border-color: var(--border-strong);
}
.landing-cta-secondary:hover {
  background: var(--hover);
  border-color: var(--accent);
}
.landing-cta-note {
  margin: 0;
}
/* Auth-options footnote.  Plain muted small text — earlier
   passes had a coloured callout box that competed visually with
   the primary CTA and looked like a second clickable element
   wedged next to the button.  This is a footnote, not a CTA.
   Same shape on landing + pricing. */
.landing-cta-auth,
.about-lede-auth {
  margin: 0.5rem 0 0;
  font-size: 0.82rem;
  color: var(--text-muted);
  line-height: 1.5;
}
/* Plan-card CTAs on /about/pricing.  Sit right under the price
   so a visitor's eye lands on the button before scanning the
   feature list. */
.about-plan-cta {
  margin: 0.5rem 0 1rem;
  font-weight: 600;
}
.landing-features {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
  margin: 1.75rem 0 2.25rem;
}
@media (min-width: 720px) {
  .landing-features {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1024px) {
  .landing-features {
    grid-template-columns: repeat(3, 1fr);
  }
}
.landing-feature {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem 1.15rem 1.25rem;
  transition: border-color 0.15s, transform 0.12s;
}
.landing-feature:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
}
.landing-feature h2 {
  margin: 0 0 0.4rem;
  font-size: 1.05rem;
  color: var(--accent);
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.landing-feature p {
  margin: 0;
  color: var(--text-muted);
  font-size: 0.95rem;
}
.landing-transparency {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem 1.4rem 1.4rem;
  margin: 2rem 0 2.25rem;
}
.landing-transparency h2 {
  margin: 0 0 0.7rem;
  font-size: 1.2rem;
  color: var(--accent);
  border: 0;
  padding: 0;
}
.landing-transparency ul {
  margin: 0;
  padding-left: 1.2rem;
}
.landing-transparency li {
  margin: 0.5rem 0;
  color: var(--text);
}
.landing-transparency li strong { color: var(--accent); }
.landing-final-cta {
  text-align: center;
  padding: 2rem 1rem;
  margin-top: 1.5rem;
  background: linear-gradient(140deg,
              var(--surface),
              var(--accent-dim));
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.landing-final-cta h2 {
  margin: 0 0 0.6rem;
  font-size: 1.5rem;
  color: var(--text);
  border: 0;
  padding: 0;
}
.landing-final-cta p {
  margin: 0 auto 1.1rem;
  color: var(--text-muted);
  max-width: 50ch;
}

/* === /ingest facelift ===
   Lead with the primary action (Take photo / From gallery as big
   icon-and-label cards), keep detection scope as visible secondary
   control, hide packing-into behind a disclosure since it's
   optional and used in <10% of sessions.  No more wide-but-
   left-justified column. */
.ingest-hero {
  background: linear-gradient(140deg, var(--panel) 0%, var(--panel-2) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.2rem 1.25rem 0.9rem;
  margin-bottom: 1rem;
}
.ingest-hero-head { margin-bottom: 1rem; }
.ingest-hero-title {
  font-size: 1.4rem;
  margin: 0 0 0.3rem;
  color: var(--accent);
}
.ingest-hero-sub {
  margin: 0;
  font-size: 0.92rem;
  color: var(--muted);
  max-width: 60ch;
}
.ingest-cta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 0.6rem;
  margin-bottom: 1rem;
}
.ingest-upload { display: contents; }
.ingest-cta-button {
  display: flex !important;
  align-items: center;
  gap: 0.7rem;
  padding: 0.85rem 1rem;
  text-align: left;
  cursor: pointer;
  min-height: 70px;
}
.ingest-cta-icon {
  font-size: 1.6rem;
  flex: 0 0 auto;
}
.ingest-cta-label {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  line-height: 1.15;
}
.ingest-cta-label strong { font-size: 1rem; }
.ingest-cta-label small { font-size: 0.78rem; opacity: 0.75; }

.ingest-scope {
  border: 0;
  padding: 0;
  margin: 0 0 0.85rem;
}
.ingest-scope-legend {
  font-size: 0.78rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 0.35rem;
  padding: 0;
}
.ingest-scope-options {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 0.4rem;
}
.ingest-scope-option {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  cursor: pointer;
  user-select: none;
  transition: border-color 0.12s, background 0.12s;
}
.ingest-scope-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
.ingest-scope-option:hover { border-color: var(--accent); }
.ingest-scope-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-dim);
}
.ingest-scope-option-title {
  font-weight: 600;
  color: var(--text);
  font-size: 0.9rem;
}
.ingest-scope-option-hint {
  font-size: 0.78rem;
  color: var(--muted);
}
.ingest-scope-option:has(input:checked) .ingest-scope-option-title {
  color: var(--accent);
}

.ingest-packing {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.ingest-packing-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.55rem 0.75rem;
  cursor: pointer;
  font-size: 0.9rem;
  list-style: none;
}
.ingest-packing-summary::-webkit-details-marker { display: none; }
.ingest-packing-summary::after {
  content: "▾";
  color: var(--muted);
  font-size: 0.75rem;
  margin-left: 0.5rem;
}
.ingest-packing[open] .ingest-packing-summary::after { content: "▴"; }
.ingest-packing-summary-label small { color: var(--muted); font-weight: normal; }
.ingest-packing-current {
  font-size: 0.82rem;
  color: var(--accent);
}
.ingest-packing-body {
  padding: 0 0.75rem 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.ingest-packing-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.85rem;
  color: var(--muted);
}
.ingest-packing-field select { width: 100%; font-size: 0.9rem; }
.ingest-packing-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
}
.ingest-packing-banner[hidden] { display: none; }

.ingest-jobs-section { margin-top: 0.5rem; }

/* === /admin dashboard facelift ===
   Modern dashboard pattern: hero KPI strip, sticky anchor TOC,
   section cards in priority order (feedback first because it's
   the most actionable), per-tenant cards instead of a wide table.
   Operators are heavy users of this page — density on the things
   they act on (feedback, tokens) and decompression on the things
   they read (tenant roster) is the trade we're making. */
/* Hero is sticky below the page header (top: ~3.25rem accounts
   for the global <header>'s height + safe spacing).  When the
   user scrolls past the sentinel placed before the hero, body
   gets ``.admin-hero-compact`` and the hero collapses to a thin
   floating bar with KPI tiles only — title + description + tile
   footers fade so the dashboard becomes a peripheral indicator
   rather than a wall.

   Z-index 30 stays below the page-level <header> (z 100) so the
   header always sits on top.  The hero's background is opaque so
   content scrolling underneath doesn't bleed through. */

/* Disable Chrome's scroll-anchoring on the admin page.  Reason:
   the sticky hero collapses while in flow, so its flow-box height
   drops ~7rem when compact mode kicks in.  With the default
   ``overflow-anchor: auto`` Chrome reads that as "content above
   the viewport just changed height" and tugs ``scrollY`` back
   toward zero to "preserve" the visible anchor.  That pulls the
   user past the sentinel boundary in the OPPOSITE direction —
   IntersectionObserver fires the inverse edge, hero re-expands,
   anchoring tugs the other way, hero re-collapses.  Visible
   jitter loop, scroll feels "stuck at the top."  Feedback #12.

   With anchoring off, the layout shift is visible (content rises
   as the hero shrinks) but the toggle happens exactly once per
   scroll past the sentinel and the user can scroll freely. */
html:has(.admin-hero),
body:has(.admin-hero) {
  overflow-anchor: none;
}

.admin-hero-sentinel {
  height: 1px;
  margin: 0 0 -1px;
  pointer-events: none;
}
.admin-hero {
  position: sticky;
  top: 3.25rem;
  /* z-index history: 30 → 50 (quick-action disclosure at z-30
     was painting over the hero) → 95 (feedback #16: in-flow
     section cards that opened their own stacking context via
     ``transform``/``filter`` on hover were still rendering on
     top of the compact KPI bar during scroll).  95 sits one
     below the page-level ``<header>`` (z 100) and well below
     any modal-class overlay (z 200+), giving the hero exactly
     the "on top of every page surface, but under modals"
     layer the operator asked for.
     ``isolation: isolate`` opens a stacking context on the
     hero itself so children's z-indexes stay scoped here and
     can't escape upward into something that out-paints us. */
  z-index: 95;
  isolation: isolate;
  margin-bottom: 1rem;
  padding: 1rem 1.15rem 0.9rem;
  background: var(--panel);
  background-image: linear-gradient(140deg, var(--panel) 0%, var(--panel-2) 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: padding 0.18s ease, max-width 0.2s ease,
              margin 0.2s ease;
}
.admin-hero-head {
  margin-bottom: 0.75rem;
  transition: max-height 0.2s ease, opacity 0.18s ease, margin 0.2s ease;
  /* Was ``6rem`` — too tight for the description text, which
     wraps to 4-6 visual lines on a narrow phone viewport.  The
     dashboard preamble ("Signed in as ... Spec § Operator
     surface — names, counts, costs only.  Reading tenant data
     takes a maintainer invite.") was getting clipped mid-line
     on most phones.  14rem fits the full text on every viewport
     we ship to and still transitions cleanly to 0 in compact
     mode (the animation duration is independent of the source
     value). */
  max-height: 14rem;
  opacity: 1;
  overflow: hidden;
}
.admin-hero-title {
  font-size: 1.2rem;
  margin: 0 0 0.25rem;
  color: var(--accent);
}
.admin-hero-sub {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
  /* Long emails in inline ``<code>`` (eg ``some.long.address+
     tag@trustyninjas.com``) blew past the hero on narrow phone
     viewports — added wrap so the cell breaks at any character
     instead of forcing a horizontal scroll. */
  overflow-wrap: anywhere;
}
.admin-hero-sub code {
  word-break: break-all;
  overflow-wrap: anywhere;
}
.admin-kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 0.5rem;
}
.admin-kpi {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.7rem 0.6rem;
  text-decoration: none;
  color: inherit;
  display: block;
  transition: transform 0.12s, border-color 0.12s, padding 0.18s ease;
}
.admin-kpi:hover {
  transform: translateY(-1px);
  border-color: var(--accent);
}
.admin-kpi-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  transition: font-size 0.18s ease;
}
.admin-kpi-value {
  font-size: 1.6rem;
  font-weight: 700;
  color: var(--accent);
  line-height: 1.1;
  margin: 0.1rem 0;
  font-variant-numeric: tabular-nums;
  transition: font-size 0.18s ease;
}
.admin-kpi-foot {
  font-size: 0.75rem;
  color: var(--muted);
  transition: max-height 0.2s ease, opacity 0.18s ease;
  max-height: 2rem;
  opacity: 1;
  overflow: hidden;
}
.admin-kpi-attention {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-dim) inset;
}

/* Compact mode — body.admin-hero-compact is toggled by the
   IntersectionObserver in admin.html when the sentinel scrolls
   off the top of the viewport.  Hero collapses copy + shrinks
   the KPI tiles AND moves to a narrow right-side rail so the
   page content has full width to itself.  On phones the hero
   disappears entirely (a dashboard pinned at top eats half the
   tiny viewport — the operator can scroll back up to see it). */
body.admin-hero-compact .admin-hero {
  padding: 0.4rem 0.65rem 0.45rem;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
  /* Slide to the right rail: max-width caps the tile cluster,
     margin-left auto pushes it to the right.  The page content
     below flows full-width as if the hero weren't there. */
  max-width: 32rem;
  margin-left: auto;
}
body.admin-hero-compact .admin-hero-head {
  max-height: 0;
  opacity: 0;
  margin-bottom: 0;
}
body.admin-hero-compact .admin-kpis {
  /* Single row when compact — auto-fit pulls every tile inline. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(110px, 1fr);
  grid-template-columns: none;
}
body.admin-hero-compact .admin-kpi {
  padding: 0.25rem 0.5rem 0.3rem;
}
body.admin-hero-compact .admin-kpi-label { font-size: 0.62rem; }
body.admin-hero-compact .admin-kpi-value { font-size: 1rem; margin: 0; }
body.admin-hero-compact .admin-kpi-foot {
  max-height: 0;
  opacity: 0;
}
@media (max-width: 720px) {
  /* Compact-on-mobile = vanish.  The hero is too big to be useful
     as a pinned bar on a phone viewport, and we don't have the
     horizontal room to push it to the side either.  Scroll back
     up to see it again. */
  body.admin-hero-compact .admin-hero { display: none; }
}
/* In-page anchor strip — NOT sticky.  Earlier passes had
   ``position: sticky; top: 0`` here, which collided with the
   page-level ``<header>`` (also sticky, top 0, z-index 100) —
   the TOC slid under the header on scroll, the section cards
   rode over it via their own z-index, and the result was the
   "cut off at the top, elements on top of it" failure mode the
   operator saw at /admin.  The TOC is a launch pad to jump to
   a section once, not a navbar that needs to follow the user
   down the page.  Keep it in the document flow. */
.admin-toc {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  padding: 0.4rem 0.6rem;
  margin: 0 0 1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.85rem;
}
.admin-toc a {
  color: var(--accent);
  text-decoration: none;
  padding: 0.15rem 0.55rem;
  border-radius: var(--radius-sm);
}
.admin-toc a:hover { background: var(--accent-dim); }

.admin-section {
  margin: 0 0 1.4rem;
  padding: 1rem 1.1rem 0.85rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  scroll-margin-top: 4rem;
}
.admin-section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
.admin-section-head h2 {
  font-size: 1rem;
  margin: 0;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.admin-section-actions {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  flex-wrap: wrap;
}
.admin-empty {
  padding: 1rem 0;
  text-align: center;
  color: var(--muted);
  font-size: 0.9rem;
}
.admin-disclosure { margin-top: 0.7rem; }
.admin-disclosure > summary { cursor: pointer; }

/* Tenant cards — replaces the 11-column table.  Each card is a
   self-contained roster entry with key counts, member roll,
   and lifecycle actions. */
.admin-tenant-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 0.7rem;
}
.admin-tenant-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem 0.85rem;
  background: var(--panel-2);
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.admin-tenant-card-soft {
  opacity: 0.7;
  background: var(--panel);
}
.admin-tenant-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
}
.admin-tenant-card-head h3 {
  margin: 0;
  font-size: 1rem;
  color: var(--text);
}
.admin-tenant-card-badges {
  display: inline-flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}
.admin-tenant-card-stats {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.4rem 0.8rem;
  margin: 0;
}
.admin-tenant-card-stats div { min-width: 0; }
.admin-tenant-card-stats dt {
  font-size: 0.7rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.admin-tenant-card-stats dd {
  margin: 0;
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.admin-tenant-card-invites {
  background: var(--accent-dim);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.6rem;
}
.admin-tenant-card-invites summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--accent);
  list-style: none;
}
.admin-tenant-card-invites summary::-webkit-details-marker { display: none; }
.admin-tenant-invite-list {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.admin-tenant-invite-row {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.admin-tenant-invite-url {
  width: 100%;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.78rem;
  padding: 0.35rem 0.55rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
}
.admin-tenant-card-members ul {
  margin: 0.4rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
}
.admin-tenant-card-actions {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding-top: 0.4rem;
  border-top: 1px solid var(--border);
}
.admin-hard-delete-form {
  width: 100%;
  display: flex;
  gap: 0.3rem;
  margin-top: 0.4rem;
}
.admin-hard-delete-form input { flex: 1; min-width: 0; }

/* Operator plan-override disclosure (comp-Pro flow).  Inline
   collapsible form inside the tenant card so the operator can
   flip plan + leave an audit-log reason in one place without
   navigating away. */
.admin-tenant-plan-action {
  display: inline-block;
  position: relative;
  isolation: isolate;
}
.admin-tenant-plan-action summary {
  list-style: none;
  cursor: pointer;
}
.admin-tenant-plan-action summary::-webkit-details-marker { display: none; }
.admin-tenant-plan-form {
  width: 100%;
  margin-top: 0.4rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.6rem 0.7rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}
.admin-tenant-plan-form p { margin: 0; }
.admin-tenant-plan-form label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  color: var(--muted);
}
.admin-tenant-plan-form input[type="text"] {
  font-size: 0.9rem;
}

/* Plan pills + status pills */
.pill-plan-free {
  background: var(--panel);
  color: var(--muted);
  border: 1px solid var(--border);
}
.pill-plan-pro {
  background: var(--accent-dim);
  color: var(--accent);
}
/* Feedback source pills.  ``user_widget`` rows don't render a pill
   at all (template skips when source == 'user_widget') so we only
   style the alternates here. */
.pill-source-mcp {
  background: rgba(96, 165, 250, 0.15);
  color: #93c5fd;
  border-color: rgba(96, 165, 250, 0.4);
}

/* Free-tier capacity card on /admin — at-a-glance number tiles
   ("used / available / total slots") plus the bump-the-pool
   form.  Operator scales storage on AWS, then comes here to
   tell the platform there's more room. */
.admin-subsection {
  margin-top: 1rem;
  padding: 0.85rem 1rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: rgba(255, 255, 255, 0.015);
}
.admin-subsection h3 {
  margin: 0 0 0.55rem;
  font-size: 0.95rem;
  color: var(--accent);
}
.admin-capacity-stats {
  display: flex;
  gap: 1.25rem;
  margin: 0.4rem 0 0.5rem;
  flex-wrap: wrap;
}
.admin-capacity-stat-num {
  font-size: 1.7rem;
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
.admin-capacity-stat-label {
  font-size: 0.75rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.admin-capacity-form {
  margin-top: 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* Feedback queue rows — replaces the old simple-grid layout with
   a "card per row" look so each entry has visible structure
   (status + meta + body + actions). */
.admin-feedback-list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.6rem;
}
.admin-feedback-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.6rem 0.8rem;
  background: var(--panel-2);
}
.admin-feedback-open { border-left: 3px solid var(--accent); }
.admin-feedback-accepted { border-left: 3px solid var(--info); }
.admin-feedback-rejected { border-left: 3px solid var(--danger); }
.admin-feedback-done { border-left: 3px solid var(--success); }
.admin-feedback-row-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}
.admin-feedback-row-meta {
  font-size: 0.78rem;
  color: var(--muted);
}
.admin-feedback-row-body {
  white-space: pre-wrap;
  font-size: 0.92rem;
  margin-bottom: 0.45rem;
}
.admin-feedback-row-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.admin-feedback-row-context {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  align-items: center;
}
.admin-feedback-row-actions {
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
}

/* Kanban feedback queue — four columns (Open / Accepted / Rejected
   / Done) with scrollable bodies + a shared filter bar above.
   Caps the section's vertical footprint so it doesn't dominate
   the dashboard.  Filter narrows visible cards across all columns
   simultaneously. */
.kanban-filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: flex-end;
  margin: 0.6rem 0 0.55rem;
  padding: 0.5rem 0.6rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
}
.kanban-filter-bar label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.75rem;
  color: var(--muted);
}
.kanban-filter-bar select,
.kanban-filter-bar input {
  font-size: 0.85rem;
  padding: 0.3rem 0.45rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
}
.kanban-filter-search { flex: 1; min-width: 14rem; }
.kanban-filter-count { margin-left: auto; }

.kanban-board {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.55rem;
  align-items: stretch;
}
@media (max-width: 980px) {
  .kanban-board { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 540px) {
  .kanban-board { grid-template-columns: 1fr; }
}
.kanban-column {
  display: flex;
  flex-direction: column;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-height: 12rem;
  max-height: 32rem;
}
.kanban-column-open                { border-top: 3px solid var(--accent); }
.kanban-column-accepted            { border-top: 3px solid var(--info); }
.kanban-column-needs_verification  { border-top: 3px solid var(--warning); }
.kanban-column-rejected            { border-top: 3px solid var(--danger); }
.kanban-column-done                { border-top: 3px solid var(--success); }

/* Fix-commit block on cards that have a recorded fix.  Sits
   between the body and the meta row so the operator's eye lands
   on "what was the fix" before "who reported it". */
.kanban-card-fix {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
  padding: 0.4rem 0.55rem;
  margin: 0.35rem 0;
  background: var(--panel-2);
  border-left: 3px solid var(--warning);
  border-radius: var(--radius-sm);
  font-size: 0.82rem;
}
.kanban-card-fix-sha {
  font-family: ui-monospace, Menlo, monospace;
  background: var(--bg);
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius-sm);
}
.kanban-card-fix-summary {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-muted, var(--muted));
}
.pill-verified {
  background: var(--success-dim, var(--accent-dim));
  color: var(--success, var(--accent));
  font-size: 0.7rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
}
.pill-released {
  background: var(--accent-dim);
  color: var(--accent);
  font-size: 0.7rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  font-family: ui-monospace, Menlo, monospace;
}
.kanban-card-action-pending {
  display: inline-flex;
  align-items: center;
  padding: 0.3rem 0.6rem;
  font-style: italic;
}
.kanban-column-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}
.kanban-column-title {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent);
  font-weight: 700;
}
.kanban-column-count {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--muted);
  background: var(--panel-2);
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
}
.kanban-column-body {
  flex: 1;
  overflow-y: auto;
  padding: 0.45rem;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}
.kanban-column-empty {
  font-size: 0.85rem;
  color: var(--muted);
  text-align: center;
  padding: 1rem 0;
}

.kanban-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.65rem;
  font-size: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.kanban-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  font-size: 0.75rem;
}
.kanban-card-id {
  font-family: ui-monospace, Menlo, monospace;
  color: var(--accent);
  font-weight: 600;
}
.kanban-card-when {
  color: var(--muted);
  font-family: ui-monospace, Menlo, monospace;
}
.kanban-card-body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.88rem;
  line-height: 1.35;
}
.kanban-card-meta {
  font-size: 0.78rem;
  color: var(--muted);
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
  align-items: center;
}
.kanban-card-meta a { color: var(--accent); text-decoration: none; }
.kanban-card-resolved { margin-top: 0.15rem; }
.kanban-card-actions {
  display: flex;
  gap: 0.25rem;
  flex-wrap: wrap;
  margin-top: 0.2rem;
}
.kanban-card-action {
  padding: 0.2rem 0.55rem;
  font-size: 0.75rem;
}
.kanban-card-action-done { color: var(--success); }
.kanban-card-action-rejected { color: var(--danger); }
.kanban-card-action-accepted { color: var(--info); }
/* Urgent flag — feedback #45.  Cards flagged urgent sort to the
   top of each kanban column (server-side ORDER BY) and get a red
   left border + corner pill so they're impossible to miss on a
   crowded queue. */
.kanban-card-urgent {
  border-left: 4px solid var(--danger);
  background: linear-gradient(90deg,
              rgba(248, 113, 113, 0.08),
              var(--panel) 18%);
}
.pill-urgent {
  background: rgba(248, 113, 113, 0.18);
  color: #fca5a5;
  border-color: rgba(248, 113, 113, 0.5);
  font-weight: 700;
}
.kanban-card-action-urgent-off { color: var(--danger); }
.kanban-card-action-urgent-on {
  background: var(--danger);
  color: var(--bg);
  border-color: var(--danger);
}

/* Activity feed — compact rows aligned to a grid. */
.admin-activity {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  font-size: 0.85rem;
}
.admin-activity-row {
  display: grid;
  grid-template-columns: 9.5rem 1fr 1fr auto;
  gap: 0.5rem;
  padding: 0.25rem 0.4rem;
  border-radius: var(--radius-sm);
  font-variant-numeric: tabular-nums;
}
.admin-activity-row:hover { background: var(--panel-2); }
.admin-activity-when { color: var(--muted); font-family: ui-monospace, Menlo, monospace; font-size: 0.78rem; }
.admin-activity-action { color: var(--accent); font-size: 0.82rem; }
.admin-activity-actor { color: var(--text); }
@media (max-width: 720px) {
  .admin-activity-row {
    grid-template-columns: 1fr;
    gap: 0.1rem;
    border-bottom: 1px solid var(--border);
    padding: 0.4rem 0;
  }
}

/* Quick action: inline disclosure that opens a compact create-tenant
   form right inside the section header so it's never more than one
   tap away. */
.admin-quick-action summary { list-style: none; cursor: pointer; }
.admin-quick-action summary::-webkit-details-marker { display: none; }
/* Previously ``[open] summary { color: var(--muted) }`` —
   meant to dim the "+ Create tenant" header when the form is
   showing, but on the ``.btn-primary`` accent-green background
   that muted colour rendered grey-on-green which read as
   "this button just stopped working" after first click.  The
   button stays at its normal primary styling regardless of
   disclosure state; the form being expanded below is the
   open-indicator. */
.admin-quick-action {
  position: relative;
  /* Establish a stacking context with ``isolation: isolate``
     so the absolute-positioned form below isn't out-ranked by
     sibling stacking contexts created elsewhere on the page
     (tenant cards with ``opacity < 1`` for soft-delete state,
     hover transforms with their own contexts, etc.).  Without
     this, the form's ``z-index: 30`` was painting beneath
     in-flow tenant cards on the user's screen. */
  isolation: isolate;
  z-index: 60;
}
.admin-quick-action-form {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 0.4rem;
  width: min(360px, 95vw);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.85rem;
  box-shadow: var(--shadow-lg);
  /* z-index is now scoped to the .admin-quick-action stacking
     context above; 1 is enough — and the parent's z-60 puts
     the entire disclosure above sibling cards / sections. */
  z-index: 1;
}

/* Quotas: per-tenant card grid (one disclosure per tenant). */
.admin-quotas-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.4rem;
}
.admin-quota-card {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.75rem;
  background: var(--panel-2);
}
.admin-quota-summary { cursor: pointer; }

/* Vendor cost: prominent total + breakdown row. */
.admin-cost-summary {
  display: grid;
  grid-template-columns: minmax(180px, 220px) 1fr;
  gap: 0.8rem;
  margin: 0.55rem 0;
  align-items: stretch;
}
@media (max-width: 720px) {
  .admin-cost-summary { grid-template-columns: 1fr; }
}
.admin-cost-total {
  background: linear-gradient(140deg, var(--accent-dim), var(--panel-2));
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 0.7rem 0.85rem;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.admin-cost-by-kind {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.55rem 0.7rem;
}
.admin-cost-kind {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.85rem;
}
.admin-cost-kind-value { color: var(--muted); }

/* OAuth client groups on /admin's API tokens card.  Each group is
   one card showing the (client + tenant) pair, status pill counts,
   and a "Revoke all active" action so the operator can collapse
   a flood of claude.ai-issued access tokens in one click. */
.oauth-client-groups {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.5rem 0 0.85rem;
}
.oauth-client-groups-title {
  font-size: 0.82rem;
  color: var(--accent);
  font-weight: 600;
  margin-bottom: 0.1rem;
}
.oauth-client-group {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.7rem;
  background: var(--panel-2);
}
.oauth-client-group-quiet { opacity: 0.65; }
.oauth-client-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.oauth-client-group-counts {
  display: inline-flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}
.oauth-client-group-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.35rem;
  flex-wrap: wrap;
}
.pill {
  display: inline-block;
  font-size: 0.72rem;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-family: ui-monospace, Menlo, monospace;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.pill-active { background: var(--accent-dim); color: var(--accent); }

/* === /admin handles table (feedback #65) =====================
   Replaces the per-row revoke form with a sortable compact
   table + bulk-action toolbar.  Previous layout had a full
   reason input + Revoke button stacked beneath each row's meta
   — fine with three users, untenable at thirty.  Now: tick the
   rows you want, type one shared reason, hit Revoke selected. */
.admin-handles-toolbar {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
  padding: 0.55rem 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  margin: 0.6rem 0;
  opacity: 0.6;
  transition: opacity 0.15s ease;
}
.admin-handles-toolbar[data-selected]:not([data-selected="0"]) {
  opacity: 1;
  border-color: var(--accent);
  background: var(--accent-dim);
}
.admin-handles-toolbar-count {
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  min-width: 5.5rem;
}
.admin-handles-toolbar-reason {
  flex: 1 1 18rem;
  min-width: 0;
}

.admin-handles-table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.admin-handles-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9rem;
}
.admin-handles-table th,
.admin-handles-table td {
  padding: 0.45rem 0.6rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
}
.admin-handles-table thead th {
  background: var(--panel);
  position: sticky;
  top: 0;
  z-index: 1;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.admin-handles-table tbody tr:nth-child(even) {
  background: var(--panel-2);
}
.admin-handles-table tbody tr:hover {
  background: var(--hover, var(--accent-dim));
}
.admin-handles-row-revoked td {
  opacity: 0.65;
}
.admin-handles-th-select,
.admin-handles-td-select {
  width: 1.6rem;
  text-align: center;
}
.admin-handles-td-handle {
  font-weight: 600;
}
.admin-handles-td-email code {
  font-size: 0.82rem;
  word-break: break-all;
}
.admin-handles-td-created {
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Sortable column header buttons — click to sort, click again
   to flip direction.  Arrow indicator is filled in by the
   inline script.  Plain ``<button>`` rendering (no border, no
   bg) so the header reads as text but is still keyboard-
   focusable + screen-reader-announced as interactive. */
.admin-th-sort {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  background: transparent;
  border: none;
  padding: 0;
  font: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  color: inherit;
  cursor: pointer;
}
.admin-th-sort:hover { color: var(--accent); }
.admin-th-sort-arrow {
  font-size: 0.7rem;
  width: 0.8rem;
  display: inline-block;
}
.pill-suspended { background: var(--warning-dim); color: var(--warning); }
.pill-revoked { background: var(--danger-dim); color: var(--danger); }
.oauth-client-groups-details {
  margin-top: 0.5rem;
}
.oauth-client-groups-details summary {
  cursor: pointer;
}
/* Same when the title is followed by a short sub-headline / description —
   keep them visually paired with a tight gap. */
.card-title-tight { margin-bottom: 0.25rem; }
/* Bigger title for "this is the page subject" headers like a box detail. */
.card-title-lg { font-size: 1.1rem; }
/* Danger variant for "scary" sections (delete confirmations). */
.card-title-danger { color: var(--danger); }
/* Make <summary class="card-title"> behave like a normal heading by hiding
   the default disclosure marker and giving it the right cursor — saves an
   inline style="cursor:pointer;list-style:none;" on every collapsible card. */
summary.card-title {
  cursor: pointer;
  list-style: none;
}
summary.card-title::-webkit-details-marker { display: none; }
/* Forms inside a <details class="card"> need a small top margin so they
   don't crowd the summary line that sits above them. */
details.card > form,
details.card > .card-form { margin-top: 0.75rem; }
.card-form { margin-top: 0.75rem; }
.flex-1 { flex: 1; min-width: 0; }

/* Soft variant of summary.card-title — used when the summary itself is a
   structured block (e.g. box detail header) rather than a single text line. */
summary.card-summary-soft {
  cursor: pointer;
  list-style: none;
}
summary.card-summary-soft::-webkit-details-marker { display: none; }
.card-meta-row { margin-top: 0.2rem; }

/* Box-detail header: meta on the left, visible "Edit box" CTA on
   the right.  Without a CTA the disclosure looks like a static
   header — users were filing bugs about not being able to edit
   the box at all (feedback #4).  The chevron rotates on open so
   the affordance also doubles as the state indicator. */
.box-edit-summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
}
.box-edit-summary-meta {
  min-width: 0;
  flex: 1;
}
.box-edit-cta {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  /* The CTA is purely visual — the parent <summary> handles the
     click — so pointer-events on this child are off to avoid
     swallowing the toggle target. */
  pointer-events: none;
}
.box-edit-chevron {
  display: inline-block;
  transition: transform 0.15s ease;
}
.box-edit-card[open] .box-edit-chevron { transform: rotate(180deg); }
@media (max-width: 480px) {
  /* Hide the label on phones so the CTA shrinks to ``✎ ▾`` and
     doesn't push the box name into a tiny column. */
  .box-edit-cta-label { display: none; }
}

/* Floor actions disclosure on /locations/{id} — the rename / replace
   floorplan / delete-floor block.  Was rendered as muted small text
   that looked like a paragraph, not an action.  Promoted to a
   button-styled disclosure so it reads as "click me to manage this
   floor" without the user needing to find it via the tour. */
.floor-actions-summary {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 0.85rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--accent);
  list-style: none;
  user-select: none;
}
.floor-actions-summary::-webkit-details-marker { display: none; }
.floor-actions-summary:hover { border-color: var(--accent); }
.floor-actions-chevron {
  font-size: 0.75rem;
  transition: transform 0.15s ease;
}
.floor-actions-disclosure[open] .floor-actions-chevron {
  transform: rotate(180deg);
}
.danger-zone-blurb { margin: 0.75rem 0 0.5rem; }

/* === /locations/{id} top action bar (feedback #48) ============
   Old layout buried the rename/delete forms inside a collapsed
   <details> at the top + the floor-rename/replace/delete forms in
   another <details> below the floorplan, only in edit mode.
   New layout: one prominent "Edit rooms" primary button + one
   "⚙️ Settings" button that opens a single dialog containing
   every location + floor edit form.  An edit-mode help banner
   sits ABOVE the floorplan with the "Done editing" exit visible. */
.location-action-bar {
  display: flex;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}
.location-action-bar-title {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.location-action-bar-heading {
  margin: 0;
  font-size: 1.4rem;
  line-height: 1.2;
}
.location-action-bar-meta { margin: 0; }
.location-action-bar-buttons {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.location-edit-help {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  background: var(--accent-dim);
  border-left: 4px solid var(--accent);
}
.location-edit-help-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.location-settings-dialog {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: var(--radius);
  box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
  padding: 0;
  max-width: min(36rem, calc(100vw - 2rem));
  width: 100%;
  max-height: calc(100vh - 4rem);
  overflow: hidden;
  flex-direction: column;
}
/* Same trap as ``#floorplan-box-dialog[open]`` above (and the
   ``.global-drop-overlay[hidden]`` fix from earlier today):
   ``display: flex`` on the un-scoped selector beats the UA
   ``dialog:not([open]) { display: none }`` rule (author rules
   trump UA rules in the cascade, regardless of specificity), so
   the dialog renders inline as a flex container EVEN WHEN
   CLOSED.  Result: the Replace-floorplan + Delete-this-floor
   form fields sit permanently on the page below the floorplan
   card — feedback #63: "I'm seeing visual bugs where replace
   floorplan and delete this floor are showing up behind the
   floorplan image at all times".  Scope ``display: flex`` to
   ``[open]`` so the UA gate works. */
.location-settings-dialog[open] {
  display: flex;
}
.location-settings-dialog::backdrop {
  background: rgba(0, 0, 0, 0.55);
}
.location-settings-close-form {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: flex-end;
  padding: 0.5rem 0.5rem 0;
  background: linear-gradient(to bottom, var(--panel) 75%, transparent);
  margin: 0;
}
.location-settings-close {
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.5rem;
}
.location-settings-close:hover { color: var(--text); }
.location-settings-body {
  overflow-y: auto;
  padding: 0.5rem 1.25rem 1.25rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.location-settings-heading {
  margin: 0;
  font-size: 1.1rem;
}
.location-settings-section {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--border);
}
.location-settings-section:last-child {
  border-bottom: none;
  padding-bottom: 0;
}
.location-settings-section-title {
  margin: 0;
  font-size: 0.95rem;
  color: var(--accent);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 600;
}
.location-settings-form { margin: 0; }
.location-settings-danger {
  padding-top: 0.7rem;
  margin-top: 0.3rem;
  border-top: 1px dashed var(--border);
}

/* === Add-box disclosure on /rooms/{id}/boxes (#51) ============
   ``+ Add box`` summary doubles as the primary CTA; the inline
   form expands on click.  Keeps the page clean when collapsed
   while making the affordance obvious. */
.room-add-box {
  margin: 0;
}
.room-add-box > summary {
  list-style: none;
  display: inline-flex;
  cursor: pointer;
}
.room-add-box > summary::-webkit-details-marker { display: none; }
.room-add-box-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.7rem;
  padding-top: 0.7rem;
  border-top: 1px dashed var(--border);
  max-width: 28rem;
}

/* #50 — inline "create this AI-suggested box" button on
   /queue cards.  Sits in the recommendation line where the
   plain "(create it first)" hint used to live. */
.sort-recommend-create {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  margin-left: 0.3rem;
  vertical-align: baseline;
}
.sort-recommend-create strong {
  font-weight: 700;
}

/* #53 — "Misc" pill on loose boxes in the room boxes list.
   Distinguishes them from regular boxes at a glance so the user
   knows the catch-all is there. */
.box-misc-pill {
  display: inline-block;
  font-size: 0.7rem;
  padding: 0.05rem 0.4rem;
  border-radius: 0.6rem;
  background: var(--accent-dim);
  color: var(--accent-strong, var(--accent));
  font-weight: 600;
  margin-right: 0.3rem;
  vertical-align: 1px;
}
.box-card-loose {
  /* Subtle border treatment so the loose box reads as "different
     kind of box" without being visually competitive with regular
     ones. */
  border-style: dashed;
}

/* #53 — "Fresh" / "Needs review" badge on item tiles created in
   the last 7 days.  Top-right corner of the photo so the badge
   sits over an empty area without obscuring the item's identity. */
.item-tile-fresh-badge {
  position: absolute;
  top: 0.2rem;
  right: 0.2rem;
  background: var(--accent);
  color: var(--on-accent);
  border-radius: 50%;
  width: 1.4rem;
  height: 1.4rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.85rem;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}
.item-tile-fresh {
  /* The badge sits inside .item-tile-photo (relative positioning
     comes from that container's existing rule); double-up here in
     case a future refactor flattens the structure. */
  position: relative;
}
.item-tile-fresh .item-tile-photo {
  position: relative;
}

.location-card-preview {
  width: 64px;
  height: 64px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  flex-shrink: 0;
}
.room-chip-link { text-decoration: none; }

/* === Forms === */
form { display: flex; flex-direction: column; gap: 0.6rem; }

label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.8rem;
  color: var(--muted);
}
label.inline {
  flex-direction: row;
  align-items: center;
  gap: 0.6rem;
  cursor: pointer;
}

input[type="text"],
input[type="file"],
textarea,
select {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius-sm);
  font-size: 1rem;
  font-family: inherit;
  width: 100%;
  min-height: 44px;
}
input[type="text"]:focus,
input[type="file"]:focus,
textarea:focus,
select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
input[type="text"]:hover,
select:hover,
textarea:hover {
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}

/* Native <option> popups inherit OS rendering and on a dark page often
   end up with near-illegible contrast — especially on the search filter
   pills which sit on the dark green panel. Force a sensible color pair
   so the popup always reads as part of the app. */
select option {
  background: var(--panel);
  color: var(--text);
}
select optgroup {
  background: var(--panel);
  color: var(--accent);
  font-style: normal;
  font-weight: 600;
}
select option:checked {
  background: var(--accent-dim);
  color: var(--accent);
}
textarea { min-height: 60px; resize: vertical; }
input[type="checkbox"] { width: 22px; height: 22px; accent-color: var(--accent); flex-shrink: 0; }

.help-text { font-size: 0.75rem; color: var(--muted); }

/* === Buttons === */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  padding: 0.65rem 1rem;
  border: none;
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  text-decoration: none;
  min-height: 44px;
  transition: background 0.15s, transform 0.1s;
}
.btn:active { transform: scale(0.97); }
.btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}

.btn-primary { background: var(--accent); color: var(--on-accent); }
.btn-primary:hover { background: var(--accent-hover); }

.btn-secondary { background: var(--panel-2); color: var(--accent); border: 1px solid var(--border); }
.btn-secondary:hover { background: var(--accent-dim); }

.btn-danger { background: var(--danger-dim); color: var(--danger); border: 1px solid transparent; }
.btn-danger:hover { background: var(--danger); color: var(--text); }

.btn-ghost { background: transparent; color: var(--muted); padding: 0.5rem; }
.btn-ghost:hover { color: var(--accent); }

.btn-sm { padding: 0.4rem 0.75rem; font-size: 0.85rem; min-height: 36px; }
.btn-block { width: 100%; }
.btn-icon { padding: 0.5rem; min-width: 44px; }

/* Button row — always wraps well on mobile */
.btn-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.btn-row > * { flex: 1; min-width: 0; }
.btn-row-end { justify-content: flex-end; }
.btn-row-end > * { flex: none; }

/* === Grouped boxes (index page) ===
   The boxes index buckets every card into a (location, room) section so
   a stash with 80 boxes doesn't read as one undifferentiated grid.  The
   header is intentionally subtle — a room chip + small location text +
   a count — because it sits *above* the visual structure (the actual
   tiles); the tiles themselves still carry the per-box colour stripe. */
.box-group {
  margin-bottom: 1.4rem;
}
.box-group:last-child {
  margin-bottom: 0;
}
.box-group-header {
  /* Was: ``border-bottom: 1px solid var(--border)`` over a flex
     row with 0.1rem horizontal padding.  The hairline underline
     visually stopped wherever the row's content ended (chip +
     count) and gave the impression the separator was "cut off"
     past the count badge — feedback #14, "to the right of the
     # of boxes there's just nothing, it cuts off with the
     background with no border or anything".

     New treatment: full filled strip that reads as a deliberate
     section divider end-to-end.  Background sits one elevation
     above the page bg so the strip is unmistakable on every
     theme without depending on the eye to follow a 1px line. */
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.45rem 0.85rem;
  margin: 0 0 0.65rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.box-group-loc {
  font-size: 0.85rem;
  color: var(--muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.box-group-count {
  margin-left: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  color: var(--muted);
  flex-shrink: 0;
}

/* === Box List (index + locations) ===
   Single column on phones, packs into a responsive grid on bigger screens
   so the desktop view actually uses the new wider main.  `grid-auto-rows:
   1fr` makes every implicit row the same height as the tallest one in the
   page — combined with a card structure that always renders the same set
   of sections (head + meta + thumbs, with placeholders for empties), the
   grid lines up cleanly instead of leaving stub-cards on rows with fewer
   thumbs than the row above. */
.box-list {
  list-style: none;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  grid-auto-rows: 1fr;
  gap: 0.5rem;
}
.box-list > li { display: flex; }
.box-list > li > .box-card { flex: 1; }
@media (min-width: 720px) {
  .box-list { grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }
}
@media (min-width: 1100px) {
  .box-list { grid-template-columns: repeat(3, 1fr); gap: 0.7rem; }
}
.box-card {
  display: grid;
  grid-template-columns: 1fr auto;
  /* Top-align so the body sits at the top of the card and any extra
     stretch space (forced by grid-auto-rows: 1fr) accrues at the bottom
     instead of pushing the title out of the head row. */
  align-items: start;
  gap: 0.5rem;
  padding: 0.85rem 0.6rem 0.85rem 1.1rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--text);
  min-height: 56px;
  position: relative;
  overflow: hidden;
  transition: background 0.15s, border-color 0.15s, transform 0.12s,
              box-shadow 0.15s;
}
/* Left-edge stripe in this card's accent colour. `--tile-color` is the
   per-box override (set inline when the user picked a colour on the box edit
   form); it falls back to the room's colour, then global accent. The
   .room-chip inside keeps using --room-color so it always reflects the
   actual room, even when the box overrides the visual accent. */
.box-card::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: var(--tile-color, var(--room-color, var(--accent)));
  opacity: 0.85;
  transition: width 0.15s, opacity 0.15s;
}
.box-card:hover {
  background: var(--panel-2);
  border-color: color-mix(in srgb, var(--tile-color, var(--room-color, var(--accent))) 50%, var(--border));
  transform: translateY(-1px);
  box-shadow: var(--shadow-md),
              0 0 0 1px color-mix(in srgb, var(--tile-color, var(--room-color, var(--accent))) 25%, transparent);
}
.box-card:hover::before { width: 6px; opacity: 1; }
.box-card:active { transform: translateY(0); }

.box-card-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.box-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-width: 0;
}
.box-name {
  font-weight: 600;
  color: var(--accent);
  font-size: 1.05rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.box-card-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.75rem;
  font-weight: 700;
  color: var(--tile-color, var(--room-color, var(--accent)));
  background: color-mix(in srgb, var(--tile-color, var(--room-color, var(--accent))) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--tile-color, var(--room-color, var(--accent))) 35%, transparent);
  padding: 0.1rem 0.55rem;
  border-radius: 100px;
  flex-shrink: 0;
}
.box-meta {
  font-size: 0.82rem;
  color: var(--muted);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  min-width: 0;
}
.box-meta .room-chip { --room-color: var(--room-color); }

.box-card-chevron {
  font-size: 1.6rem;
  font-weight: 300;
  color: var(--muted);
  opacity: 0.55;
  padding: 0 0.25rem;
  transition: color 0.15s, opacity 0.15s, transform 0.15s;
  flex-shrink: 0;
  /* Re-centre the chevron in the card body even though .box-card itself is
     top-aligned (so multi-row stretch space goes to the bottom, not next to
     the chevron). */
  align-self: center;
}
.box-card:hover .box-card-chevron {
  color: var(--tile-color, var(--room-color, var(--accent)));
  opacity: 1;
  transform: translateX(3px);
}

/* === Item thumbs (shared with search / tags / audit) === */
.item-thumb {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}
.item-info { flex: 1; min-width: 0; }
.item-name { font-weight: 600; font-size: 0.95rem; }
.item-desc { color: var(--muted); font-size: 0.85rem; margin-top: 0.15rem; }

/* === Box-list thumbnail strip === */
.box-thumbs {
  display: flex;
  gap: 0.4rem;
  margin-top: 0.15rem;
  flex-wrap: nowrap;
  overflow: hidden;
  /* Stretch thumbs across the available width — each one auto-fits up to
     a max so the strip doesn't leave a gaping void on the right.
     Reserve at least one thumb's worth of vertical space so a box with
     no items still occupies the same row height as a populated one — the
     index/room grid stays uniform. */
  min-height: 48px;
  align-items: center;
}
.box-thumbs.box-thumbs-empty {
  justify-content: center;
  border: 1px dashed var(--border);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--panel-2) 50%, transparent);
}
.box-thumbs-empty-label {
  font-size: 0.78rem;
  font-style: italic;
  color: var(--muted);
  opacity: 0.65;
  letter-spacing: 0.02em;
}
.box-thumbs img {
  flex: 1 1 0;
  width: 0;            /* let flex distribute; min-width handles minimum */
  min-width: 48px;
  max-width: 64px;
  aspect-ratio: 1 / 1;
  height: auto;
  border-radius: var(--radius-sm);
  object-fit: cover;
  border: 1px solid var(--border);
}
.box-thumbs-more {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 48px;
  aspect-ratio: 1 / 1;
  padding: 0 0.5rem;
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--muted);
  font-size: 0.78rem;
  font-weight: 600;
  border: 1px solid var(--border);
}

/* === Item grid (box contents) === */
.item-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.item-tile {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  overflow: hidden;
  transition: transform 0.1s, border-color 0.15s;
}
.item-tile:hover, .item-tile:focus-visible {
  border-color: var(--accent);
  outline: none;
}
.item-tile:active { transform: scale(0.97); }
.item-tile-photo {
  width: 100%;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  background: var(--panel);
  display: flex;
  align-items: center;
  justify-content: center;
}
.item-tile-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.item-tile-placeholder {
  font-size: 2rem;
  color: var(--muted);
}
.item-tile-name {
  padding: 0.35rem 0.5rem 0.5rem;
  font-size: 0.8rem;
  font-weight: 500;
  line-height: 1.2;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* === Item detail dialog === */
.item-dialog {
  width: min(560px, 95vw);
  /* ``dvh`` (dynamic viewport height) follows the *visible* viewport
     on mobile — when iOS Safari's URL bar is showing, ``vh`` reports
     the bigger no-chrome viewport, which made the dialog overflow
     past the URL bar and put the close × under the chrome where it
     couldn't be tapped.  Feedback #69 (Nash, Android Mobile Chrome):
     "Pops up with last room, won't let me close pop-up." */
  max-height: 90dvh;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  overflow: hidden;
}
.item-dialog::backdrop {
  background: rgba(0, 0, 0, 0.65);
  backdrop-filter: blur(2px);
}
.item-dialog-close-form { margin: 0; }
.item-dialog-close {
  position: absolute;
  /* Add safe-area-inset-top so the × doesn't slip under iOS notches
     or Android system bars.  On desktop ``env(safe-area-inset-top)``
     resolves to 0, so this is a no-op there. */
  top: calc(0.5rem + env(safe-area-inset-top, 0px));
  right: 0.5rem;
  z-index: 10;
  /* 44×44 on mobile per Apple HIG / Android Material — the previous
     36×36 was just under the recommended hit target and got missed
     on a finger-tap. */
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 1.5rem;
  cursor: pointer;
  font-family: inherit;
}
.item-dialog-close:hover { background: rgba(0, 0, 0, 0.8); }
@media (min-width: 640px) {
  /* Desktop has precise pointers — give some real estate back. */
  .item-dialog-close {
    width: 36px;
    height: 36px;
    font-size: 1.3rem;
  }
}

/* Search item modal — wider on desktop than the default item-dialog
   cap so the hero photo + actions don't feel cramped. */
#search-item-dialog {
  width: min(820px, 95vw);
  max-height: 92vh;
}

/* Header strip inside the search-result modal — gives users a clear way
   to break out into the actual box page when they want to. */
.search-item-modal-header {
  padding: 0.75rem 0.9rem 0.4rem;
  border-bottom: 1px solid var(--border);
  background: var(--panel-2);
}

/* Scrollable content region inside the dialog */
.item-detail {
  display: flex;
  flex-direction: column;
  max-height: 90vh;
  overflow-y: auto;
}
.item-hero {
  width: 100%;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  max-height: 55vh;
  overflow: hidden;
}
.item-hero img {
  width: 100%;
  height: auto;
  max-height: 55vh;
  object-fit: contain;
  display: block;
}
.item-detail-body {
  padding: 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.item-detail-name {
  margin: 0;
  font-size: 1.15rem;
  color: var(--accent);
}
.item-detail-notes {
  margin: 0;
  color: var(--muted);
  font-size: 0.9rem;
}
.item-detail-section {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.section-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.action-row {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}
.action-row > .btn,
.action-row > form { flex: 1; min-width: 0; }
.action-row .btn { width: 100%; }

/* === Tags === */
.tag-list { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.35rem; }
.tag {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  background: var(--accent-dim);
  color: var(--accent);
  padding: 0.2rem 0.55rem;
  border-radius: 100px;
  font-size: 0.75rem;
  white-space: nowrap;
}
.tag-remove {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  padding: 0;
  font-size: 0.9rem;
  line-height: 1;
  min-width: 20px;
  min-height: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tag-remove:hover { color: var(--danger); }

/* === Badges === */
.badge {
  display: inline-block;
  padding: 0.2rem 0.5rem;
  border-radius: 100px;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}
.badge-pending { background: var(--warning-dim); color: var(--warning); }
.badge-processing { background: var(--info-dim); color: var(--info); }
.badge-done { background: var(--success-dim); color: var(--success); }
.badge-failed { background: var(--danger-dim); color: var(--danger); }

/* === Error pages ===
   404, 401, 403, 413, 429, 500 — every error route renders templates/
   error.html with a Siberian Forest cat or a wise tortoise mascot
   plus a sassy headline + quip.  Centred column on desktop, full-
   width on mobile; mascot is SVG so it scales sharply at any size. */
.error-page {
  max-width: 36rem;
  margin: 3rem auto;
  padding: 2rem 1.5rem;
  text-align: center;
}
.error-mascot {
  display: block;
  margin: 0 auto 1.25rem;
  max-width: 14rem;
  filter: drop-shadow(0 4px 14px rgba(0, 0, 0, 0.45));
}
.error-mascot svg { width: 100%; height: auto; }
.error-status {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.95rem;
  color: var(--accent);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin-bottom: 0.5rem;
}
.error-headline {
  font-size: 1.6rem;
  font-weight: 700;
  margin: 0 0 0.75rem;
  line-height: 1.2;
}
.error-quip {
  font-size: 1.05rem;
  color: var(--text-muted, #aaa);
  margin: 0 0 1rem;
  line-height: 1.5;
}
.error-detail {
  font-size: 0.85rem;
  color: var(--text-muted, #888);
  margin: 0 0 1.5rem;
  word-break: break-word;
}
.error-detail code {
  background: var(--hover);
  padding: 0.15rem 0.45rem;
  border-radius: 0.25rem;
}
.error-actions {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}
.error-signature {
  font-style: italic;
  color: var(--text-muted, #888);
  font-size: 0.85rem;
  margin: 0;
}

/* === Sort Queue === */
.sort-card { padding: 0; overflow: hidden; }
.sort-photo {
  width: 100%;
  max-height: 250px;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}
.sort-photo img {
  display: block;
  max-width: 100%;
  max-height: 250px;
  object-fit: contain;
}

/* The overlay is positioned relative to the rendered image, not the
   letterboxed .sort-photo container — wrap them both in an inline-block
   frame that hugs the image's actual size. overflow:hidden clips the
   bbox overlay's huge box-shadow so it darkens only the photo, not
   the form fields underneath the card. */
.sort-photo-frame {
  position: relative;
  display: inline-block;
  line-height: 0;
  max-width: 100%;
  overflow: hidden;
}
.sort-photo { overflow: hidden; }
.sort-photo-frame img {
  display: block;
  max-width: 100%;
  max-height: 250px;
}

/* Bounding-box overlay drawn on top of the sort-queue photo. The AI returns
   bbox in 0-1000 normalized coords stashed on data-* attrs; the JS in
   queue.html (``placeBboxOverlay``) computes pixel position from the IMG
   element's bounding rect on load + resize and writes inline left/top/
   width/height so the box sticks to the actual image regardless of how
   the surrounding frame is sized.  The earlier CSS-percentages approach
   broke when the inline-block frame ended up wider than the rendered
   image (flex-basis: auto → image natural width capped at 100%, while
   max-height shrunk the image but not the frame). */
.sort-bbox-overlay {
  position: absolute;
  border: 3px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: 4px;
  pointer-events: none;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.45);
  /* The shadow darkens the rest of the photo so the focus is unmistakable. */
}
.sort-bbox-label {
  position: absolute;
  top: -1.5rem;
  left: 0;
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--accent);
  background: rgba(0, 0, 0, 0.75);
  padding: 0.15rem 0.45rem;
  border-radius: 4px;
  white-space: nowrap;
}
.sort-body { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.6rem; }

.suggestion {
  padding: 0.6rem;
  background: var(--accent-dim);
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  border-left: 3px solid var(--accent);
}
.suggestion strong { color: var(--accent); }

/* ── /queue facelift ──────────────────────────────────────────
   Photo at the top, AI recommendation prominent, edit form
   collapsed by default, accept/reject sticky at the bottom of
   the card.  The user can decide accept/reject without scrolling. */
.sort-card { display: flex; flex-direction: column; }
.sort-summary {
  padding: 0.85rem 0.95rem 0.55rem;
  border-bottom: 1px solid var(--border);
}
.sort-summary-name {
  margin: 0;
  font-size: 1.05rem;
  color: var(--text);
}
.sort-summary-desc {
  margin: 0.3rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
}
.sort-summary-provenance {
  margin: 0.4rem 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}
.sort-summary-provenance strong { color: var(--text); }

.sort-recommend {
  margin: 0;
  padding: 0.7rem 0.95rem;
  background: var(--accent-dim);
  border-left: 4px solid var(--accent);
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sort-recommend-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  font-weight: 700;
}
.sort-recommend-target {
  font-size: 1.05rem;
  color: var(--text);
}
.sort-recommend-target strong { color: var(--accent); }
.sort-recommend-reason {
  font-size: 0.82rem;
  color: var(--muted);
}

.sort-box-picker {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.7rem 0.95rem 0;
  font-size: 0.82rem;
  color: var(--muted);
}
.sort-box-picker-label small { color: var(--muted); font-weight: normal; }
.sort-box-picker select {
  font-size: 0.95rem;
  padding: 0.45rem 0.55rem;
  background: var(--panel-2);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

.sort-customize {
  margin: 0.55rem 0.95rem 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.sort-customize-summary {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.7rem;
  cursor: pointer;
  font-size: 0.9rem;
  list-style: none;
}
.sort-customize-summary::-webkit-details-marker { display: none; }
.sort-customize-summary::after {
  content: "▾";
  color: var(--muted);
  font-size: 0.75rem;
}
.sort-customize[open] .sort-customize-summary::after { content: "▴"; }
.sort-customize-body {
  padding: 0 0.7rem 0.7rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.sort-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.82rem;
  color: var(--muted);
}
.sort-field input, .sort-field textarea, .sort-field select {
  font-size: 0.92rem;
  padding: 0.45rem 0.55rem;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.sort-customize-crop {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
}
/* Photo-adjacent crop toolbar — feedback #42/#43.  Sits directly
   beneath the photo so the cropper UI's visual target and its
   buttons stay paired at every viewport width.  Pre-existing
   ``.sort-customize-crop`` rule is kept for backwards compat
   in case any deployed fragment still references the old class. */
.sort-photo-controls {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding: 0.45rem 0.85rem 0;
  margin-top: 0.35rem;
}

/* Sticky action bar at the bottom of the card — Accept / Reject
   always visible, no matter how tall the card gets. */
.sort-action-bar {
  position: sticky;
  bottom: 0;
  display: flex;
  gap: 0.4rem;
  padding: 0.7rem 0.95rem;
  margin-top: 0.6rem;
  background: var(--panel);
  border-top: 1px solid var(--border);
  z-index: 5;
}
.sort-action {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.05rem;
  padding: 0.55rem 0.4rem;
}
.sort-action-icon { font-size: 1.15rem; line-height: 1; }
.sort-action-label { font-size: 0.8rem; font-weight: 600; }

.crop-toolbar {
  display: flex;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  background: var(--bg);
  border-top: 1px solid var(--border);
}

/* === Audit === */
.audit-item {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  cursor: pointer;
  min-height: 56px;
  transition: background 0.15s;
}
.audit-item:active { background: var(--panel-2); }
.audit-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

/* === Ingest Queue === */
.ingest-item {
  display: flex;
  gap: 0.75rem;
  align-items: center;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
}
.ingest-thumb {
  width: 48px;
  height: 48px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  flex-shrink: 0;
}

/* === Search Results === */
.result-card {
  display: flex;
  gap: 0.75rem;
  padding: 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  text-decoration: none;
  color: var(--text);
}
.result-card:hover { background: var(--panel-2); }

/* === Search redesign — query bar, facet filters, grouped results === */
.search-shell {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-bottom: 0.75rem;
}

/* Query bar — looks like a real search box, not a card. */
.search-bar {
  position: relative;
  display: flex;
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.4rem 0.5rem 0.4rem 2.4rem;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.search-bar:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-dim);
}
.search-bar-icon {
  position: absolute;
  left: 0.85rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 1rem;
  opacity: 0.7;
}
.search-bar input[type="search"] {
  border: none;
  background: transparent;
  color: var(--text);
  font-size: 1rem;
  font-family: inherit;
  padding: 0.45rem 0;
  flex: 1;
  min-width: 0;
  outline: none;
}
.search-bar input[type="search"]::placeholder { color: var(--muted); }
.search-bar-clear {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.2rem 0.4rem;
}
.search-bar-clear:hover { color: var(--accent); }

/* Filter row — wraps gracefully on narrow screens. Each select is a
   compact "pill" so the row stays scannable. */
.search-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.5rem;
  align-items: center;
}
.search-filter {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 100px;
  padding: 0.25rem 0.6rem 0.25rem 0.85rem;
  font-size: 0.8rem;
  color: var(--muted);
  transition: border-color 0.15s;
}
.search-filter:focus-within { border-color: var(--accent); }
.search-filter-label { font-weight: 500; white-space: nowrap; }
.search-filter select {
  background: transparent;
  border: none;
  color: var(--text);
  padding: 0.2rem 0.1rem;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  min-height: auto;
}
.search-filter select:focus { box-shadow: none; }

/* Boolean toggle — a checkbox dressed up as a pill. */
.search-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 100px;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.search-toggle input { display: none; }
.search-toggle:has(input:checked) {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: var(--accent);
}

/* Active-filter chip strip. */
.search-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  align-items: center;
}
.search-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  background: var(--accent-dim);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
  border-radius: 100px;
  font-size: 0.78rem;
  padding: 0.2rem 0.6rem;
  cursor: pointer;
  font-family: inherit;
}
.search-chip:hover { background: var(--accent); color: var(--on-accent); }
.search-chip-clear-all {
  margin-left: 0.3rem;
  font-size: 0.78rem;
  color: var(--muted);
  text-decoration: underline;
}

.search-summary {
  font-size: 0.85rem;
  color: var(--muted);
  padding: 0.25rem 0.25rem 0.5rem;
}
.search-summary strong { color: var(--accent); }

/* Result groups — one card per box, with item rows packed in. */
.search-group {
  padding: 0.65rem 0.85rem 0.65rem 1.1rem;
  position: relative;
  overflow: hidden;
}
/* Same coloured stripe as the box-card on the index — gives each result
   group its box's identity at a glance, even when the room/location read
   identical for adjacent groups. */
.search-group::before {
  content: "";
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 4px;
  background: var(--tile-color, var(--room-color, var(--accent)));
  opacity: 0.85;
}
.search-group-header {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.5rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}
.search-group-box {
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  font-size: 0.95rem;
}
.search-group-where {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.78rem;
  min-width: 0;
  flex-wrap: wrap;
}
.search-group-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 100px;
  padding: 0.05rem 0.55rem;
  color: var(--muted);
}
.search-group-items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.search-result {
  display: flex;
  gap: 0.75rem;
  width: 100%;
  padding: 0.5rem;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  align-items: flex-start;
  transition: background 0.12s, border-color 0.12s;
}
.search-result:hover {
  background: var(--panel-2);
  border-color: var(--border);
}
.search-result .item-thumb {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-sm);
  object-fit: cover;
  flex-shrink: 0;
}
.item-thumb-empty {
  background: var(--panel-2);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  color: var(--muted);
  border: 1px solid var(--border);
}
.search-result-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.search-result-name {
  font-weight: 500;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.95rem;
}
.search-result-notes {
  font-size: 0.8rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.search-loadmore {
  margin-top: 0.5rem;
  margin-bottom: 1rem;
}

/* === Re-crop === */
.recrop-layout { display: flex; flex-direction: column; gap: 0.75rem; }
.recrop-source {
  width: 100%;
  max-height: 350px;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}
.recrop-source img {
  display: block;
  max-width: 100%;
  max-height: 350px;
  object-fit: contain;
}
.recrop-current {
  display: flex;
  gap: 0.5rem;
  align-items: center;
}
.recrop-current img {
  width: 80px;
  height: 80px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 2px solid var(--border);
}

/* === Labels === */
.label-grid-card {
  padding: 0.5rem;
}
/* One section per (location, room) bucket — mirrors the index page's
   .box-group structure so the visual rhythm matches.  The header
   carries the location + count + a per-group select-all so users
   can flip a whole room of boxes in one tap (huge for stash setups
   where the user just wants to print labels for one room). */
.label-group {
  margin-bottom: 0.9rem;
}
.label-group:last-of-type { margin-bottom: 0.5rem; }
.label-group-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  padding: 0.15rem 0.25rem 0.55rem;
  margin-bottom: 0.55rem;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.label-group-title {
  display: flex;
  align-items: baseline;
  gap: 0.55rem;
  flex-wrap: wrap;
  min-width: 0;
}
.label-group-loc {
  font-size: 0.85rem;
  color: var(--muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.label-group-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  color: var(--muted);
  flex-shrink: 0;
}
.label-group-toggle { flex-shrink: 0; }
.label-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.5rem;
}
@media (min-width: 640px) {
  /* Tile width bumped 320 → 380 so the thumbnail + name + actions
     have room to breathe — earlier cards felt cramped on a desktop
     viewport.  Auto-fill keeps the responsive packing behaviour. */
  .label-grid { grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); }
}

/* Card: a relative container holding (1) selection checkbox in the corner,
   (2) a button-trigger that occupies the thumb + meta and opens the modal,
   (3) inline action buttons. Each region has its own click target — no
   overlapping click handlers. */
.label-card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel-2);
  overflow: hidden;
  transition: border-color 0.15s, box-shadow 0.15s;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
}
.label-card[data-has-art="1"] { background: linear-gradient(180deg, var(--panel-2), var(--panel)); }
.label-card:has(.label-card-select:checked) {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent-dim) inset;
}
.label-card:hover { border-color: var(--accent); }

/* Selection checkbox: top-LEFT of the card, overlaying the thumb's
   corner (Instagram-style select).  Was top-right, which collided
   with the regen-art button in .label-card-actions — the button is
   ~34px wide at ~7px from the right edge, the checkbox was at
   8px right, so they sat on top of each other.  The thumb's
   white background gives the checkbox enough contrast to read,
   and the checkbox's own white halo (the surrounding card-edge
   padding) keeps it tappable. */
.label-card-select {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  width: 22px;
  height: 22px;
  margin: 0;
  cursor: pointer;
  accent-color: var(--accent);
  /* Light backdrop so the native checkbox reads cleanly against
     either a photo thumbnail or the plain panel-2 background. */
  background: var(--panel);
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--border);
}
.label-card-checkbox-affordance { display: none; } /* the native checkbox is fine */

/* The trigger button — fills the left + center, normalized so it doesn't
   look like a default <button>. */
.label-card-trigger {
  display: grid;
  grid-template-columns: 88px 1fr;
  gap: 0.7rem;
  align-items: center;
  padding: 0.6rem 0.7rem;
  background: none;
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}
.label-card-trigger:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius);
}

.label-card-thumb {
  position: relative;
  border-radius: var(--radius-sm);
  background: white;
  overflow: hidden;
  border: 1px solid var(--border);
  display: block;
}
.label-card-thumb-landscape {
  width: 96px;
  aspect-ratio: 4 / 2;
}
.label-card-thumb-portrait {
  width: 48px;
  aspect-ratio: 2 / 4;
}
.label-card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* Avery sheet-format picker — sits above the grid card.
   ``.label-format-row`` is the side-by-side wrapper around the
   picker form and the printer-compat notice (feedback #32 —
   "the sheet format is off on the right, and the advice /
   warning is on the left, there's a ton of empty space").  At
   desktop widths the dropdown sits on the left and the notice
   to its right; at narrow widths the wrapper wraps to a
   stack.  ``align-items: stretch`` lines the notice's height
   with the form so neither column looks ragged. */
.label-format-row {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0.85rem;
  margin-top: 0.5rem;
}
.label-format-form {
  display: flex;
  align-items: flex-end;
  gap: 0.6rem;
  /* Was ``margin-top: 0.5rem`` — moved to the wrapper above so
     the form + notice gap is the only vertical spacing. */
}
.label-format-label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.85rem;
  color: var(--muted);
  flex: 1;
  min-width: 0;
  max-width: 380px;
}
.label-format-label select {
  font-size: 0.92rem;
}

/* Printer-compat callout under the Sheet-format picker.  Loud
   enough to read at a glance, calm enough not to feel like an
   error — this is a "before you print" hint, not a "you broke
   something" toast.  Two variants, laser + inkjet, share the
   base layout and only differ in the accent stripe colour.
   Feedback #23. */
.label-printer-notice {
  /* Sits next to the format-form inside ``.label-format-row``
     on desktop, wraps below on mobile.  ``margin-top: 0`` since
     the wrapper's gap handles spacing; flex-grow lets the
     notice occupy whatever horizontal space is left over after
     the dropdown. */
  margin: 0;
  padding: 0.55rem 0.75rem 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-left-width: 4px;
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.88rem;
  line-height: 1.4;
  color: var(--text);
  flex: 1 1 22rem;
  max-width: 60ch;
}
.label-printer-notice code {
  font-size: 0.85rem;
  background: var(--surface-2);
  padding: 0.05rem 0.35rem;
  border-radius: 4px;
}
.label-printer-notice-laser {
  border-left-color: var(--warning);
}
.label-printer-notice-inkjet {
  border-left-color: var(--info);
}

/* Per-card landscape/portrait pill toggle. */
.label-orient-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--panel);
}
.label-orient-segment {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.05rem;
  border: 0;
  background: none;
  cursor: pointer;
  padding: 0.25rem 0.45rem;
  min-width: 30px;
  font-family: inherit;
  font-size: 0.7rem;
  color: var(--muted);
  line-height: 1;
}
.label-orient-segment:not(:last-child) {
  border-right: 1px solid var(--border);
}
.label-orient-segment.is-active {
  background: var(--accent);
  color: var(--bg);
}
.label-orient-segment:hover:not(.is-active) {
  color: var(--accent);
}
.label-orient-label {
  font-weight: 600;
  font-size: 0.7rem;
}
.label-card-meta { min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
/* Name wraps to up to 2 lines — single-line + ellipsis chopped many
   real-world box names mid-word ("Holiday Decorat…").  Two-line clamp
   gives ~30 chars of headroom in the same vertical footprint as the
   thumb so the row height stays tidy. */
.label-card-name {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
  overflow: hidden;
  overflow-wrap: anywhere;
  /* Checkbox moved to the top-LEFT, so no need to reserve space
     on the right anymore — full name width available. */
}
.label-card-notes {
  font-size: 0.78rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.label-card-actions {
  display: flex;
  gap: 0.2rem;
  align-items: center;
  flex-shrink: 0;
  padding-right: 0.45rem;
  padding-left: 0.1rem;
}
.label-card-actions .label-action {
  min-height: 34px;
  min-width: 34px;
  padding: 0.35rem;
  font-size: 0.95rem;
}
.art-icon { font-size: 1.05rem; line-height: 1; }

/* Art-state-aware buttons. The "generate" state is a soft accent dot to
   pull the eye when no art exists yet; the "regenerate" state stays muted
   but uses the rotating-arrow glyph so it reads at a glance as "redo this". */
.label-action-generate {
  background: var(--accent-dim);
  color: var(--accent);
  border: 1px solid transparent;
}
.label-action-generate:hover { background: var(--accent); color: var(--on-accent); }
.label-action-regen {
  background: var(--panel);
  color: var(--accent);
  border: 1px solid var(--border);
}
.label-action-regen:hover { background: var(--accent-dim); border-color: var(--accent); }

.btn.art-busy {
  pointer-events: none;
  opacity: 0.7;
}
.btn.art-busy .art-icon { animation: spin 1.2s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Sticky footer toolbar with selected count + actions */
.label-toolbar {
  position: sticky;
  bottom: calc(64px + var(--safe-bottom));
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.6rem 0.85rem;
  margin-top: 0.75rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
  box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.25);
  z-index: 50;
}
@media (min-width: 640px) {
  .label-toolbar { bottom: 1rem; }
}
.label-toolbar-summary {
  font-size: 0.85rem;
  color: var(--muted);
}
.label-toolbar-summary strong { color: var(--accent); font-size: 1rem; }
.label-toolbar-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  align-items: center;
}
.label-copies-control {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  color: var(--text-muted, #aaa);
  padding: 0.25rem 0.5rem;
  border-radius: 0.3rem;
  user-select: none;
}
.label-copies-control select {
  font-size: 0.85rem;
  padding: 0.1rem 0.35rem;
}
.label-tint-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  color: var(--text-muted, #aaa);
  cursor: pointer;
  padding: 0.25rem 0.5rem;
  border-radius: 0.3rem;
  user-select: none;
}
.label-tint-toggle:has(input:checked) {
  color: var(--text, #fff);
}
.label-tint-toggle input {
  margin: 0;
}

/* === Label preview modal === */
.label-preview-modal {
  width: min(720px, 95vw);
  max-height: 92vh;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  overflow: hidden;
  font-family: inherit;
}
.label-preview-modal::backdrop {
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(3px);
}
.label-preview-modal[open] {
  display: flex;
  flex-direction: column;
}
.label-preview-image-wrap {
  background: var(--surface-2);
  padding: 1.25rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.label-preview-image {
  width: 100%;
  max-width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-sm);
  background: white;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.label-preview-meta {
  padding: 0.85rem 1rem 0.5rem;
}
.label-preview-name {
  margin: 0 0 0.25rem;
  font-size: 1.1rem;
  color: var(--accent);
}
.label-preview-notes {
  margin: 0;
  font-size: 0.85rem;
  color: var(--muted);
}
.label-preview-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.5rem 1rem 1rem;
}
.label-preview-actions > .btn { flex: 1; min-width: 140px; }
.label-preview-close {
  position: absolute;
  top: 0.65rem;
  right: 0.65rem;
  z-index: 10;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 1.4rem;
  cursor: pointer;
  font-family: inherit;
  line-height: 1;
}
.label-preview-close:hover { background: rgba(0, 0, 0, 0.85); }

/* === Locations + floorplan === */

/* Inline chip used wherever a room name shows up — index list, box detail,
   floorplan room list, etc. The --room-color custom prop gates accent color
   per room so the same chip pattern colors itself based on the assigned hue. */
.room-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.1rem 0.55rem;
  border-radius: 100px;
  background: color-mix(in srgb, var(--room-color, var(--accent)) 18%, transparent);
  color: var(--room-color, var(--accent));
  border: 1px solid color-mix(in srgb, var(--room-color, var(--accent)) 35%, transparent);
  font-size: 0.78rem;
  font-weight: 500;
  white-space: nowrap;
}

.floor-bar { padding: 0.5rem; }
.floor-tabs {
  display: flex;
  gap: 0.3rem;
  flex-wrap: wrap;
  align-items: center;
}
.floor-tab {
  display: inline-flex;
  align-items: center;
  padding: 0.45rem 0.85rem;
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--muted);
  border: 1px solid var(--border);
  text-decoration: none;
  font-size: 0.85rem;
  font-weight: 500;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
  cursor: pointer;
  user-select: none;
}
.floor-tab:hover { color: var(--accent); border-color: var(--accent); }
.floor-tab.active {
  background: var(--accent-dim);
  color: var(--accent);
  border-color: var(--accent);
}
.floor-tab-add { color: var(--accent); list-style: none; }
.floor-tab-add::-webkit-details-marker { display: none; }
.floor-add { position: relative; }
.floor-add[open] > .floor-tab-add { background: var(--accent-dim); }
.floor-add-form {
  position: absolute;
  top: calc(100% + 0.4rem);
  left: 0;
  z-index: 5;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem;
  display: flex;
  gap: 0.4rem;
  align-items: center;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
  white-space: nowrap;
}
.floor-add-form input {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0.4rem 0.55rem;
  border-radius: var(--radius-sm);
  font-size: 0.85rem;
  font-family: inherit;
  min-width: 160px;
}

.floorplan-card {
  padding: 0.5rem;
}

/* The viewport is a fixed-size scroll window. Pan = native scroll, zoom =
   --zoom which scales the stage's width. On mobile this means pinch-to-
   zoom + touch-drag-to-pan come "for free" via standard browser scrolling.

   Flex + `safe center` keeps the stage centred when it's smaller than the
   viewport (low zoom on a wide monitor) — without this the stage left-aligns
   and zooming in/out feels off-balance because the content snaps to the left
   gutter. The `safe` keyword falls back to start-alignment once the stage is
   larger than the viewport, so normal scroll-to-pan still works at zoom > 1. */
.floorplan-viewport {
  position: relative;
  width: 100%;
  max-height: 75vh;
  overflow: auto;
  border-radius: var(--radius);
  background: var(--bg);
  display: flex;
  justify-content: safe center;
  align-items: flex-start;
  /* Pan via scroll, but allow pinch via custom Pointer Events handlers. */
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
}
/* Horizontal scrollbar lives at the bottom of the viewport, vertical at
   the right. Style them subtly for the dark theme. */
.floorplan-viewport::-webkit-scrollbar { width: 10px; height: 10px; }
.floorplan-viewport::-webkit-scrollbar-thumb {
  background: var(--panel-2);
  border-radius: 100px;
  border: 2px solid var(--bg);
}
.floorplan-viewport::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }

.floorplan-stage {
  position: relative;
  width: calc(100% * var(--zoom, 1));
  /* Don't let flex squish the stage smaller than its CSS-driven width — the
     room overlays are positioned in % of the stage, so a flex-shrink would
     misalign them against the underlying floorplan image. */
  flex-shrink: 0;
  user-select: none;
  -webkit-user-select: none;
  /* In edit mode the stage owns pointer events for drawing/moving rooms;
     touch-action: none so taps don't end up scrolling the viewport. */
}
.floorplan-stage[data-edit="1"] { cursor: crosshair; touch-action: none; }
.floorplan-stage[data-edit="1"] .room-rect { cursor: move; }
.floorplan-stage[data-edit="0"] .room-rect { cursor: default; }

/* Zoom toolbar — sits below the viewport, easy thumb-reach on mobile. */
.floorplan-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
}
.floorplan-control {
  background: var(--panel-2);
  border: 1px solid var(--border);
  color: var(--text);
  width: 38px;
  height: 38px;
  border-radius: 100px;
  font-size: 1.1rem;
  font-family: inherit;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.floorplan-control:hover {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: var(--accent);
}
.floorplan-zoom-label {
  min-width: 4ch;
  text-align: center;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: var(--muted);
}
.floorplan-stage img {
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none;
}
.floorplan-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.room-rect {
  position: absolute;
  border: 2px solid var(--room-color, var(--accent));
  background: color-mix(in srgb, var(--room-color, var(--accent)) 15%, transparent);
  border-radius: 6px;
  pointer-events: auto;
  cursor: pointer;
  transition: background 0.12s, transform 0.08s;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  padding: 0.25rem 0.4rem;
  min-width: 24px;
  min-height: 24px;
  overflow: hidden;
}
.floorplan-stage[data-edit="0"] .room-rect:hover {
  background: color-mix(in srgb, var(--room-color, var(--accent)) 28%, transparent);
}
.floorplan-stage[data-edit="1"] .room-rect:hover {
  background: color-mix(in srgb, var(--room-color, var(--accent)) 30%, transparent);
}
.room-rect-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: #fff;
  text-shadow: 0 1px 3px rgba(0,0,0,0.7);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  flex: 1;
  min-width: 0;
}
.room-rect-count {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--on-accent);
  background: var(--room-color, var(--accent));
  padding: 0.05rem 0.4rem;
  border-radius: 100px;
  margin-left: 0.4rem;
  pointer-events: none;
  flex-shrink: 0;
}
.room-rect-draft {
  border-style: dashed;
  background: var(--accent-dim);
  pointer-events: none;
}

/* Boxes inside a room — fill the room area instead of clustering
   top-left. The column count comes from the server (--box-cols =
   ⌈√N⌉) so a single big box truly fills its room and a 12-box
   room packs into a roughly square grid. Rows stretch with 1fr
   + align-content: stretch so tiles claim the full vertical space. */
.room-rect-boxes {
  position: absolute;
  inset: 1.6rem 0.35rem 0.35rem 0.35rem;  /* leave room for label + count */
  display: grid;
  grid-template-columns: repeat(var(--box-cols, 3), 1fr);
  grid-auto-rows: 1fr;
  gap: 0.3rem;
  align-content: stretch;
  pointer-events: auto;
  overflow: hidden;
}
.room-box-tile {
  /* Tile color falls back through: explicit box override → room color
     → global accent. The CSS variables stack so any level can win. */
  background: var(--tile-color, var(--room-color, var(--accent)));
  color: var(--on-accent);
  border: none;
  border-radius: 4px;
  padding: 0.25rem 0.45rem;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 600;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0.15rem;
  text-align: left;
  overflow: hidden;
  min-width: 0;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45);
  transition: transform 0.12s, box-shadow 0.12s, filter 0.12s;
}
.room-box-tile:hover {
  transform: translateY(-1px);
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.55);
  filter: brightness(1.08);
}
.room-box-tile:active { transform: translateY(0); }
.room-box-tile.drag-source {
  opacity: 0.35;
  pointer-events: none;  /* don't fight the ghost for hover */
}

/* Floating clone that follows the pointer during drag. */
.drag-ghost {
  position: fixed;
  z-index: 1000;
  pointer-events: none;
  opacity: 0.85;
  transform: rotate(-2deg) scale(1.05);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.55);
  transition: none;
}

/* Room rect lighting up as a drop target during drag. */
.room-rect.drop-target {
  background: color-mix(in srgb, var(--room-color, var(--accent)) 35%, transparent);
  box-shadow: inset 0 0 0 3px var(--room-color, var(--accent)),
              0 0 0 9999px rgba(0, 0, 0, 0.25);
}

/* Item drag — mosaic image is the source. While dragging, the box tile
   under the cursor highlights as a drop target. */
.room-box-tile-mosaic-img.drag-source {
  opacity: 0.3;
  filter: grayscale(0.6);
  pointer-events: none;
}
.room-box-tile.item-drop-target {
  filter: brightness(1.25);
  outline: 2px dashed #fff;
  outline-offset: -2px;
}
.item-drag-ghost {
  position: fixed;
  z-index: 1000;
  width: 96px;
  height: 96px;
  object-fit: cover;
  border-radius: 6px;
  pointer-events: none;
  box-shadow: var(--shadow-lg),
              0 0 0 2px var(--accent);
  opacity: 0.9;
  transform: rotate(-2deg);
}

/* Item DnD source styling shares with the floorplan drag-source rule
   above; a fade-in/zoom for the bottom drawer that appears during drag. */
.item-tile.drag-source {
  opacity: 0.35;
  pointer-events: none;
}

/* Drop drawer — slides up from the bottom while an item is being dragged. */
.item-drop-drawer {
  position: fixed;
  bottom: calc(64px + var(--safe-bottom));  /* clear of the mobile tab bar */
  left: 0.75rem;
  right: 0.75rem;
  z-index: 90;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 0.5rem 0.6rem 0.6rem;
  box-shadow: 0 -8px 22px rgba(0, 0, 0, 0.45);
  animation: drop-drawer-in 0.18s ease-out;
}
@media (min-width: 640px) {
  /* On desktop there's no bottom tab bar, so sit closer to the edge. */
  .item-drop-drawer { bottom: 0.75rem; }
}
@keyframes drop-drawer-in {
  from { transform: translateY(20px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}
.item-drop-drawer-header {
  font-size: 0.78rem;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
.item-drop-drawer-targets {
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  padding-bottom: 0.2rem;
}
.item-drop-target {
  flex-shrink: 0;
  background: var(--panel-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.7rem;
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.1rem;
  text-align: left;
  min-width: 110px;
  transition: background 0.12s, border-color 0.12s, transform 0.1s;
}
.item-drop-target-name { font-weight: 600; }
.item-drop-target-loc {
  font-size: 0.7rem;
  color: var(--muted);
}
.item-drop-target.drop-active {
  background: var(--accent-dim);
  border-color: var(--accent);
  transform: scale(1.05);
}
.room-box-tile-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.3rem;
  min-width: 0;
}
.room-box-tile-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.room-box-tile-count {
  flex-shrink: 0;
  background: rgba(0, 0, 0, 0.18);
  border-radius: 100px;
  padding: 0.02rem 0.4rem;
  font-size: 0.65rem;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}
/* Detail rows — hidden at the lowest zoom tier, revealed as the user
   zooms in. Tier 0 ≈ 1x, tier 1 ≈ 1.5x, tier 2 ≈ 2.5x, tier 3 ≈ 3.5x+. */
.room-box-tile-detail {
  display: none;
  flex-direction: column;
  font-size: 0.6rem;
  font-weight: 500;
  opacity: 0.85;
}
.room-box-tile-meta {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
[data-zoom-tier="1"] .room-box-tile-name,
[data-zoom-tier="2"] .room-box-tile-name,
[data-zoom-tier="3"] .room-box-tile-name {
  /* Name can wrap to fully fit at higher zooms */
  white-space: normal;
  text-overflow: clip;
  overflow: visible;
}
[data-zoom-tier="2"] .room-box-tile-detail,
[data-zoom-tier="3"] .room-box-tile-detail {
  display: flex;
}
[data-zoom-tier="3"] .room-box-tile {
  /* Tier 3 is "I really want to see everything" — bigger type */
  font-size: 0.78rem;
  padding: 0.4rem 0.55rem;
}

/* Photo mosaic inside a tile — fills the tile both horizontally (server-
   picked --mosaic-cols of equal-width columns) AND vertically (auto-rows:
   1fr with align-content: stretch). Imgs use object-fit: cover so they
   crop instead of warp when cells aren't square. flex: 1 on the mosaic
   makes it claim the rest of the tile after the head/detail rows. */
.room-box-tile-mosaic {
  display: none;
  position: relative;
  margin-top: 0.3rem;
  gap: 2px;
  border-radius: 3px;
  overflow: visible;  /* let hover-zoomed thumbs spill the tile */
  grid-template-columns: repeat(var(--mosaic-cols, 3), 1fr);
  grid-auto-rows: 1fr;
  flex: 1 1 auto;
  min-height: 0;       /* allow the flex child to actually shrink/stretch */
  align-content: stretch;
  /* Inset shadow gives the mosaic a "tile inside a tile" feel. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.room-box-tile-mosaic-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: rgba(0, 0, 0, 0.15);
  cursor: grab;
  transition: transform 0.15s, box-shadow 0.15s, z-index 0s 0.15s;
  position: relative;
  z-index: 1;
  min-height: 0;
  pointer-events: auto;  /* explicit so no parent rule blocks the drag */
  touch-action: none;     /* let the pointer handler claim touch + mouse */
  -webkit-user-drag: none;
  user-select: none;
}
.room-box-tile-mosaic-img:active { cursor: grabbing; }
/* Hover preview — scale up the thumbnail and pop it above the others.
   The mosaic itself is display:none below tier 3 so the rule only kicks
   in when the user can actually see it; no zoom-tier gate needed
   on the :hover selector itself. */
.room-box-tile-mosaic-img:hover {
  transform: scale(2.2);
  z-index: 30;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.6),
              0 0 0 2px var(--accent);
  transition: transform 0.15s, box-shadow 0.15s, z-index 0s;
}
[data-zoom-tier="3"] .room-box-tile-mosaic { display: grid; }

/* Pan cursor — view-mode stage is grabbable when not over a tile. */
.floorplan-stage[data-edit="0"] { cursor: grab; }
.floorplan-stage[data-edit="0"].panning { cursor: grabbing; }
.floorplan-stage[data-edit="0"] .room-box-tile { cursor: pointer; }

/* Box preview modal — wide on desktop so 60+ items fit in a generous
   grid, near-full-screen on mobile so the thumbnails are still useful.
   max-height respects iOS safe-area bottom inset so the bottom buttons
   don't get clipped by the home indicator. */
#floorplan-box-dialog {
  width: min(1100px, 96vw);
  max-width: 96vw;
  /* dvh + safe-bottom — see ``.item-dialog`` for the dvh rationale
     (feedback #69). */
  max-height: calc(94dvh - var(--safe-bottom));
  /* Drop the browser default <dialog> padding so the inner
     preview can fill the full dialog rect and host its own
     scroll.  Without this, "max-height: inherit" on the preview
     inherits the dialog's outer max-height but the dialog's
     content area is smaller (by the default padding), so the
     thumbs grid for a 60-item box overflows the dialog and
     looks "cut off" — feedback #18. */
  padding: 0;
  overflow: hidden;
  flex-direction: column;
}
/* THE bug from feedback #37/#41/#46: the rule above used to set
   ``display: flex`` unconditionally on this dialog, which beats
   the UA stylesheet's ``dialog:not([open]) { display: none }``
   on specificity and made the dialog visible AT ALL TIMES,
   regardless of the ``[open]`` attribute.  The initial
   ``<div class="empty">Loading…</div>`` placeholder content
   then sat permanently on the page.  Clicking the X did nothing
   because ``<form method="dialog">`` only closes a dialog that
   was actually opened — without ``[open]``, submitting the form
   just posts to the current URL and the page reload produces
   the same broken state.  Hoisting ``display: flex`` into an
   ``[open]`` selector restores the UA gate.  ``display: none``
   when closed wins; ``display: flex`` when open wins.  X works
   again because the dialog is now actually open when the user
   sees it. */
#floorplan-box-dialog[open] {
  display: flex;
}
.floorplan-box-preview {
  padding: 0.8rem 0.85rem 0.95rem;
  border-left: 4px solid var(--tile-color, var(--room-color, var(--accent)));
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  /* ``flex: 1 1 auto`` + ``min-height: 0`` lets the preview
     occupy the dialog's full vertical area AND scroll inside
     itself when the thumbs grid is taller than the viewport.
     The dialog already caps the outer height; we don't need a
     redundant max-height here. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
@media (min-width: 640px) {
  .floorplan-box-preview { padding: 1rem 1.1rem 1.1rem; }
}
.floorplan-box-preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding-right: 2.5rem;  /* clear of the close × button */
}
.floorplan-box-preview-name {
  margin: 0;
  font-size: 1.25rem;
  color: var(--accent);
}
.floorplan-box-preview-meta { font-size: 0.9rem; }

/* Item thumbnails — auto-fit so the grid grows column count with the
   modal width. 100 px on phones, 140 px on bigger screens — both let
   you read the item name and see the photo clearly. */
.floorplan-box-preview-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 0.5rem;
  padding: 0.25rem 0;
}
@media (min-width: 640px) {
  .floorplan-box-preview-thumbs {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: 0.6rem;
  }
}
.floorplan-box-preview-thumb {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.12s, transform 0.12s;
}
.floorplan-box-preview-thumb:hover {
  border-color: var(--accent);
  transform: translateY(-1px);
}
.floorplan-box-preview-thumb-img {
  display: block;
  aspect-ratio: 1 / 1;
  background: var(--panel);
  overflow: hidden;
}
.floorplan-box-preview-thumb-img img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.floorplan-box-preview-thumb-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 1.6rem;
  color: var(--muted);
}
.floorplan-box-preview-thumb-name {
  display: block;
  font-size: 0.78rem;
  font-weight: 500;
  padding: 0 0.45rem 0.45rem;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.floorplan-box-preview-more .floorplan-box-preview-thumb-fallback {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 1rem;
  font-weight: 700;
  color: var(--accent);
}

/* Corner resize handles. Only rendered in edit mode (the template only emits
   them then), but cursor: col/row-resize signals their function. */
.room-handle {
  position: absolute;
  width: 12px;
  height: 12px;
  background: var(--room-color, var(--accent));
  border: 2px solid var(--bg);
  border-radius: 50%;
  pointer-events: auto;
  z-index: 2;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.handle-nw { top: -6px; left: -6px; cursor: nwse-resize; }
.handle-ne { top: -6px; right: -6px; cursor: nesw-resize; }
.handle-sw { bottom: -6px; left: -6px; cursor: nesw-resize; }
.handle-se { bottom: -6px; right: -6px; cursor: nwse-resize; }
.room-handle:hover { transform: scale(1.25); }

/* Room edit dialog — reuses some of the label-preview styles for consistency */
.room-edit-dialog {
  width: min(420px, 92vw);
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
}
.room-edit-dialog::backdrop {
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(2px);
}
.room-edit-body {
  padding: 1.25rem 1rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.room-edit-title {
  margin: 0;
  font-size: 1.05rem;
  color: var(--accent);
}

.room-color-swatches {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-top: 0.3rem;
}
.room-swatch {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--swatch-color);
  cursor: pointer;
  padding: 0;
  transition: transform 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.room-swatch:hover { transform: scale(1.12); border-color: var(--text); }
.room-swatch.active {
  border-color: var(--text);
  box-shadow: 0 0 0 2px var(--swatch-color), 0 0 0 4px var(--bg);
}
.room-swatch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* "Inherit" pseudo-swatch — for boxes that fall back to the room color. */
.room-swatch-inherit {
  background: var(--panel-2);
  color: var(--muted);
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.room-swatch-inherit:hover { color: var(--text); }

/* === Empty State === */
.empty {
  color: var(--muted);
  text-align: center;
  padding: 2rem 1rem;
  font-style: italic;
}
.empty a { color: var(--accent); }

/* Mascot empty states — a friendly turtle or cat above the message. The
   actual SVG is rendered via the data-mascot attribute through a CSS
   ::before so templates only need a single attribute. */
.empty-mascot {
  font-style: normal;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  padding: 2.5rem 1rem 2rem;
}
.empty-mascot p { margin: 0; max-width: 32ch; line-height: 1.5; }
.empty-mascot::before {
  content: "";
  display: block;
  width: 88px;
  height: 88px;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  opacity: 0.85;
  /* Mascot animations — the turtle "swims" with a gentle bob, the cat
     gives a subtle tail-flick. Reduced-motion users get static art. */
  animation: mascot-bob 4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .empty-mascot::before { animation: none; }
}
@keyframes mascot-bob {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-3px) rotate(1deg); }
}
/* Inline-encoded SVG mascots so they don't add HTTP requests. The fill uses
   the accent green so they pick up theme tweaks for free. The colors below
   are the literal accent hex with `%23` URL-encoding for `#`. */
.empty-mascot[data-mascot="turtle"]::before {
  background-image: url('data:image/svg+xml;utf8,\
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 60">\
      <ellipse cx="36" cy="36" rx="22" ry="16" fill="none" stroke="%234ade80" stroke-width="2.4"/>\
      <path d="M36 22 L36 50 M16 30 L56 30 M16 42 L56 42 M22 24 L22 48 M50 24 L50 48"\
            stroke="%234ade80" stroke-width="1.5" opacity="0.6" fill="none"/>\
      <circle cx="62" cy="28" r="7" fill="none" stroke="%234ade80" stroke-width="2.4"/>\
      <circle cx="65" cy="26" r="1.5" fill="%234ade80"/>\
      <path d="M64 31 q2 1 4 0" stroke="%234ade80" stroke-width="1.5" fill="none" stroke-linecap="round"/>\
      <line x1="14" y1="50" x2="11" y2="56" stroke="%234ade80" stroke-width="2" stroke-linecap="round"/>\
      <line x1="26" y1="52" x2="25" y2="58" stroke="%234ade80" stroke-width="2" stroke-linecap="round"/>\
      <line x1="46" y1="52" x2="47" y2="58" stroke="%234ade80" stroke-width="2" stroke-linecap="round"/>\
      <line x1="58" y1="50" x2="61" y2="56" stroke="%234ade80" stroke-width="2" stroke-linecap="round"/>\
    </svg>');
}
.empty-mascot[data-mascot="cat"]::before {
  background-image: url('data:image/svg+xml;utf8,\
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">\
      <path d="M22 30 L18 18 L28 24 M58 30 L62 18 L52 24"\
            fill="none" stroke="%234ade80" stroke-width="2.4" stroke-linejoin="round"/>\
      <ellipse cx="40" cy="38" rx="22" ry="20" fill="none" stroke="%234ade80" stroke-width="2.4"/>\
      <circle cx="32" cy="36" r="2" fill="%234ade80"/>\
      <circle cx="48" cy="36" r="2" fill="%234ade80"/>\
      <path d="M38 44 q2 2 4 0" stroke="%234ade80" stroke-width="2" fill="none" stroke-linecap="round"/>\
      <path d="M28 42 L20 42 M28 44 L20 45 M52 42 L60 42 M52 44 L60 45"\
            stroke="%234ade80" stroke-width="1" opacity="0.6" stroke-linecap="round"/>\
      <path d="M62 56 q12 4 8 16" stroke="%234ade80" stroke-width="2.4" fill="none" stroke-linecap="round"/>\
    </svg>');
}
.empty-mascot[data-mascot="boxes"]::before {
  background-image: url('data:image/svg+xml;utf8,\
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80">\
      <rect x="14" y="38" width="22" height="22" fill="none" stroke="%234ade80" stroke-width="2.2"/>\
      <rect x="44" y="44" width="20" height="20" fill="none" stroke="%234ade80" stroke-width="2.2" opacity="0.85"/>\
      <rect x="28" y="14" width="22" height="22" fill="none" stroke="%234ade80" stroke-width="2.2" opacity="0.7"/>\
      <line x1="14" y1="49" x2="36" y2="49" stroke="%234ade80" stroke-width="1.2" opacity="0.5"/>\
      <line x1="44" y1="54" x2="64" y2="54" stroke="%234ade80" stroke-width="1.2" opacity="0.5"/>\
      <line x1="28" y1="25" x2="50" y2="25" stroke="%234ade80" stroke-width="1.2" opacity="0.5"/>\
    </svg>');
}

/* === Utility === */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  border: 0;
}
.text-muted { color: var(--muted); }
.text-sm { font-size: 0.8rem; }
.text-danger { color: var(--danger); }
.mt-1 { margin-top: 0.5rem; }
.mt-2 { margin-top: 0.75rem; }
.mt-3 { margin-top: 1rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 0.75rem; }
.mb-3 { margin-bottom: 1rem; }
.gap-row { display: flex; gap: 0.5rem; align-items: center; }
.row-gap-tight { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
/* Soft disclosure for nested "rarely used" actions inside a card. The
   triangle marker is implicit — text turns into a clickable line. */
.disclosure-soft {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.disclosure-soft::-webkit-details-marker { display: none; }
.disclosure-soft::before {
  content: "▸ ";
  display: inline-block;
  transition: transform 0.15s;
  font-size: 0.7em;
  color: var(--muted);
  width: 0.8em;
}
details[open] > .disclosure-soft::before { transform: rotate(90deg); }

/* === Cropper.js Overrides === */
.cropper-view-box { outline: 2px solid var(--accent); }
.cropper-line, .cropper-point { background-color: var(--accent); }
.cropper-modal { background: rgba(0,0,0,0.6); }
.cropper-point { width: 12px !important; height: 12px !important; }

/* === Desktop === */
@media (min-width: 640px) {
  body { padding-bottom: 0; }
  .tab-bar { display: none; }
  .header-nav {
    display: flex;
    gap: 0.25rem;
    margin-left: 1rem;
  }
  .header-nav a {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.85rem;
    padding: 0.35rem 0.6rem;
    border-radius: var(--radius-sm);
  }
  .header-nav a:hover, .header-nav a.active {
    color: var(--accent);
    background: var(--accent-dim);
  }
  main { padding: 1rem; }
  .item-thumb { width: 80px; height: 80px; }
}

.stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.stat-row > div {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.75rem;
}
.stat-row dt { font-size: 0.75rem; color: var(--muted); }
.stat-row dd { font-size: 1.2rem; font-weight: 600; margin: 0; }
.trend-row > div { padding-bottom: 0.4rem; }
.trend-cell {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  /* Critical for the sparkline-shrink path on narrow phones:
     without min-width:0 on this flex container, the inner
     ``.trend-spark`` won't honour ``max-width: 100%`` and the
     100-px-intrinsic SVG overflows the card.  Feedback #25
     from nashmankas — line graphs going outside the label
     boxes on a 399 px Android viewport. */
  min-width: 0;
}
.trend-value {
  white-space: nowrap;
  flex-shrink: 0;
}
.trend-spark {
  color: var(--accent);
  display: inline-flex;
  /* Allow the inline SVG to scale down on narrow screens
     instead of forcing the trend cell wider than its card. */
  flex: 1 1 auto;
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
}
.sparkline {
  display: block;
  /* The server emits ``width="100" height="24"`` as intrinsic
     attributes; on narrow viewports we'd rather have the SVG
     shrink to fit and stretch back out on wider columns.
     viewBox makes that visually correct; this CSS just tells
     the browser to use container width instead of the px
     attribute. */
  width: 100%;
  max-width: 100px;
  height: auto;
}

/* === Maintenance page polish === */
.maintenance-card .card-title { margin-bottom: 0.5rem; }
.maintenance-card p { margin-bottom: 0.75rem; }
.maintenance-card code {
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 0.05rem 0.35rem;
  border-radius: 4px;
  font-size: 0.78rem;
}
.maintenance-card input[type="file"] { margin-bottom: 0.5rem; }
.allowed-emails {
  list-style: none;
  padding: 0;
  margin: 0.25rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.allowed-emails li {
  font-size: 0.85rem;
  display: flex;
  align-items: baseline;
  gap: 0.4rem;
  /* Long emails / token names + a Revoke button would overflow
     the viewport on mobile.  flex-wrap lets the row break onto
     the next line; word-break on inline <code> lets the email
     itself wrap on hyphens / dots when there's no space. */
  flex-wrap: wrap;
}
.allowed-emails li > code {
  word-break: break-all;
  overflow-wrap: anywhere;
  min-width: 0;
}
.notice {
  font-size: 0.8rem;
  color: var(--muted);
  background: var(--panel-2);
  border-left: 2px solid var(--border);
  padding: 0.5rem 0.7rem;
  border-radius: var(--radius-sm);
  margin-bottom: 0.75rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.notice::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: currentColor;
  flex-shrink: 0;
  animation: notice-pulse 1.4s ease-in-out infinite;
}
.notice-success, .notice-danger { animation: none; }
.notice-success::before, .notice-danger::before { animation: none; }
.notice-success {
  color: var(--accent);
  background: var(--accent-dim);
  border-left-color: var(--accent);
}
.notice-danger {
  color: var(--danger);
  background: var(--danger-dim);
  border-left-color: var(--danger);
}
@keyframes notice-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

/* === Disclosure toggle (used by changelog summary) === */
.disclosure {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: var(--muted);
  font-size: 0.85rem;
  font-weight: 500;
  user-select: none;
  padding: 0.25rem 0;
}
.disclosure::-webkit-details-marker { display: none; }
.disclosure::before {
  content: "";
  display: inline-block;
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform 0.15s ease;
}
details[open] > .disclosure::before { transform: rotate(90deg); }
.disclosure:hover { color: var(--accent); }

/* === Changelog (release notes) === */
.changelog {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
  line-height: 1.55;
}
/* The card title already says "Changelog"; suppress the duplicate h1 from the
   rendered markdown source. */
.changelog > h1 { display: none; }

.changelog h2 {
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--muted);
  margin: 1.25rem 0 0.5rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}
.changelog h2:first-of-type { margin-top: 0; }
.changelog h2 a {
  color: var(--accent);
  text-decoration: none;
  background: var(--accent-dim);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.78rem;
  font-weight: 600;
  padding: 0.1rem 0.5rem;
  border-radius: 100px;
  letter-spacing: 0.02em;
}
.changelog h2 a:hover { background: var(--accent); color: var(--on-accent); }

.changelog h3 {
  font-size: 0.65rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0.85rem 0 0.4rem;
}

.changelog ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.changelog li {
  position: relative;
  padding-left: 1rem;
  color: var(--text);
}
.changelog li::before {
  content: "";
  position: absolute;
  left: 0.25rem;
  top: 0.6em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
  opacity: 0.7;
}
.changelog strong { color: var(--accent); font-weight: 600; }

/* All inline links inside list items are commit hashes. Render them as a
   subtle monospace pill so they don't compete with the message text. */
.changelog li a {
  color: var(--muted);
  text-decoration: none;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.72rem;
  background: var(--bg);
  border: 1px solid var(--border);
  padding: 0.02rem 0.4rem;
  border-radius: 4px;
  margin-left: 0.15rem;
  vertical-align: 1px;
  transition: color 0.15s, border-color 0.15s;
}
.changelog li a:hover { color: var(--accent); border-color: var(--accent); }
.changelog p { color: var(--muted); margin-top: 0.5rem; font-size: 0.8rem; }

/* === Version display === */
.version-display {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
  padding: 0.6rem 0.8rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.version-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
}
.version-value {
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--accent);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
.version-sha {
  font-size: 0.75rem;
  color: var(--muted);
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  margin-left: auto;
}

/* === /tags landing redesign ==================================
   Feedback #20: the original /tags template was a one-line-per-
   tag flat list — "this page is so sad, can it have fun
   pictures, better UI, a reason for existing?"  Replaced with a
   grid of substantive tag-cards each showing the tag's
   distribution across boxes / rooms / locations.  Each card is
   a self-contained answer to "where does the 'fragile' tag
   actually live in my stash?". */
.tags-hero {
  margin-bottom: 1rem;
}
.tags-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.7rem;
}
@media (min-width: 720px) {
  .tags-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1100px) {
  .tags-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
.tag-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.85rem 0.95rem 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
  transition: border-color 0.15s, box-shadow 0.15s;
}
.tag-card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
}
.tag-card-empty {
  opacity: 0.7;
}
.tag-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.tag-card-chip {
  background: var(--accent-dim);
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 999px;
  padding: 0.2rem 0.7rem;
  font-size: 0.95rem;
  font-weight: 600;
  text-decoration: none;
}
.tag-card-chip:hover {
  background: var(--accent);
  color: var(--on-accent);
}
.tag-card-count {
  font-size: 0.82rem;
  color: var(--text-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.tag-card-section {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.tag-card-section-label {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}
.tag-card-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}
.tag-card-room-chip {
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.78rem;
}
.tag-card-room-count {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.72rem;
  background: var(--bg);
  color: var(--text-muted);
  padding: 0.02rem 0.35rem;
  border-radius: 999px;
  border: 1px solid var(--border);
}
.tag-card-locations {
  font-size: 0.85rem;
  line-height: 1.5;
}
.tag-card-location-link {
  color: var(--text);
  text-decoration: none;
  border-bottom: 1px dashed var(--border-strong);
}
.tag-card-location-link:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.tag-card-boxes {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
}
.tag-card-box {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: var(--text);
  padding: 0.2rem 0.4rem;
  border-radius: var(--radius-sm);
}
.tag-card-box:hover {
  background: var(--hover);
  color: var(--accent);
}
.tag-card-box-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tag-card-box-count {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.78rem;
  flex-shrink: 0;
}
.tag-card-foot {
  margin-top: auto;
  padding-top: 0.25rem;
}
.tag-card-empty-note {
  font-style: italic;
  margin: 0;
}

/* === Broken-image placeholder ================================
   Swapped in by the global ``error``-event handler in base.html
   when an ``<img>`` fails to load.  Inherits the original
   img's class list so size / shape rules from
   .item-thumb, .audit-thumb, .floorplan-box-preview-thumb-img,
   etc. carry over and the placeholder lays out exactly where
   the image did.  Feedback #21: "if an item picture cannot
   load, I want to show a sad face, not the default error
   jpeg thing." */
.img-broken {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  min-height: 48px;
  background: var(--surface-2);
  color: var(--text-muted);
  border-radius: var(--radius-sm);
  font-size: 1.8rem;
  user-select: none;
  text-align: center;
  /* Subtle dashed border so the placeholder reads as "this
     should have been an image" rather than just empty space. */
  border: 1px dashed var(--border);
  box-sizing: border-box;
}

/* === /leaderboard ============================================
   Celebratory ranking page for feedback contributors.  Goal is
   "make you feel super special" — bigger type, prominent
   star strings, podium emojis 🥇🥈🥉.  No sortable table. */
.leaderboard-hero {
  text-align: center;
  margin: 0.75rem 0 1.75rem;
}
.leaderboard-hero-title {
  font-size: clamp(1.5rem, 4vw, 2.1rem);
  color: var(--accent);
  margin: 0 0 0.5rem;
  line-height: 1.2;
}
.leaderboard-hero-sub {
  max-width: 56ch;
  margin: 0 auto;
  color: var(--text-muted);
}
.leaderboard-you {
  background: linear-gradient(140deg,
              var(--surface) 0%,
              var(--accent-dim) 100%);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 1.1rem 1.2rem 1.15rem;
  margin-bottom: 1.5rem;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1rem;
  align-items: center;
}
@media (max-width: 540px) {
  .leaderboard-you {
    grid-template-columns: 1fr;
    text-align: center;
  }
}
.leaderboard-you-stars {
  font-size: 1.5rem;
  letter-spacing: 0.05em;
  line-height: 1.1;
  text-shadow: 0 0 12px var(--accent-dim);
}
.leaderboard-you-stars-empty {
  filter: grayscale(1);
  opacity: 0.45;
}
.leaderboard-you-text h2 {
  margin: 0 0 0.35rem;
  font-size: 1.15rem;
  color: var(--accent);
}
.leaderboard-you-text p {
  margin: 0;
  color: var(--text);
}
.leaderboard-you-excluded {
  margin-top: 0.45rem !important;
  font-style: italic;
}
.leaderboard-podium {
  margin: 1.5rem 0 2rem;
}
.leaderboard-podium-title {
  font-size: 1rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin: 0 0 0.6rem;
}
.leaderboard-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}
.leaderboard-row {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.7rem;
  align-items: center;
  padding: 0.7rem 0.95rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: transform 0.15s, box-shadow 0.15s;
}
.leaderboard-row-1 {
  background: linear-gradient(140deg,
              color-mix(in srgb, gold 22%, var(--surface)),
              var(--surface));
  border-color: gold;
  box-shadow: 0 0 18px color-mix(in srgb, gold 25%, transparent);
}
.leaderboard-row-2 {
  background: linear-gradient(140deg,
              color-mix(in srgb, silver 22%, var(--surface)),
              var(--surface));
  border-color: silver;
}
.leaderboard-row-3 {
  background: linear-gradient(140deg,
              color-mix(in srgb, #cd7f32 22%, var(--surface)),
              var(--surface));
  border-color: #cd7f32;
}
.leaderboard-rank {
  font-size: 1.6rem;
  line-height: 1;
}
.leaderboard-who {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.leaderboard-who strong {
  font-size: 1.05rem;
  color: var(--text);
}
.leaderboard-stars {
  font-size: 1.05rem;
  white-space: nowrap;
  letter-spacing: 0.04em;
  text-align: right;
}
.leaderboard-fineprint {
  margin-top: 1.5rem;
  padding: 0.75rem 0.95rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  border-left: 3px solid var(--accent);
}
.leaderboard-fineprint p { margin: 0; }

/* === Star rain on /leaderboard first load (feedback #40) ======
   Each shipped contribution drops one star from above the
   viewport, carrying a small date label.  Cleaned up via
   ``animationend`` in the inline spawner.  ``pointer-events: none``
   so the rain never blocks clicks on the real page underneath. */
#leaderboard-rain {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 60;
}
.leaderboard-rain-star {
  position: absolute;
  top: -3rem;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  transform: translateZ(0) scale(var(--rain-scale, 1));
  will-change: transform, opacity;
  animation-name: leaderboard-rain-fall;
  /* Linear timing — feedback #55 round 2: the previous cubic-
     bezier(0.35, 0.1, 0.55, 1) has its second control point at
     y=1, x=0.55, which makes the curve plateau near the top of
     its range for the last ~45% of the animation.  Visually this
     reads as "star hovers around the same y-position for ages
     then suddenly disappears" — the user reported it as stars
     getting cut off half way down.  Linear timing makes the
     descent steady and the off-screen exit clean. */
  animation-timing-function: linear;
  animation-fill-mode: forwards;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
  user-select: none;
}
.leaderboard-rain-glyph {
  font-size: 2rem;
  line-height: 1;
}
.leaderboard-rain-date {
  font-size: 0.7rem;
  padding: 0.1rem 0.4rem;
  border-radius: 0.5rem;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  white-space: nowrap;
}
@keyframes leaderboard-rain-fall {
  /* Star starts ``top: -3rem`` (above the viewport) and translates
     a generous 130vh down so it exits well below the visible
     viewport before the animation completes.  With linear timing
     the descent is uniform — the user sees the star at every
     y-position before it leaves the screen.

     Fade in at the top (0% → 8%) so the entry isn't a hard pop;
     no fade out — the translateY overshoot carries the star
     completely off the bottom while still at full opacity. */
  0%   { transform: translateY(0)     scale(var(--rain-scale, 1)); opacity: 0; }
  8%   { opacity: 1; }
  100% { transform: translateY(130vh) scale(var(--rain-scale, 1)); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  /* Respect the accessibility setting.  Skip the shower entirely
     rather than running it slowly — the user's intent is "no
     decorative motion", which this animation definitively is. */
  .leaderboard-rain-star { display: none; }
}

/* "Signed in as <email>" stat-row cell — long Gmail-style
   addresses overflow the chip on narrow phones if the cell
   doesn't allow per-character wrap.  Feedback #29 from
   nashmankas: "my email is cut off and overhanging the
   display box". */
.signed-in-email {
  word-break: break-all;
  overflow-wrap: anywhere;
  min-width: 0;
}

/* === Handle prompt on /leaderboard ============================
   Surfaced when the viewer has stars but no handle.  Visual
   nudge for them to claim a public-facing name. */
.leaderboard-handle-prompt {
  margin: 1.25rem 0 0;
  padding: 1rem 1.15rem 1.1rem;
  background: linear-gradient(140deg,
              var(--surface) 0%,
              var(--accent-dim) 100%);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
}
.leaderboard-handle-prompt h3 {
  margin: 0 0 0.4rem;
  font-size: 1.05rem;
  color: var(--accent);
}
.leaderboard-handle-prompt p { margin: 0 0 0.7rem; }
.leaderboard-handle-form {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: flex-end;
}
.leaderboard-handle-form label {
  flex: 0 0 18rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 8rem;
  max-width: 100%;
}
.leaderboard-handle-form input[type="text"] {
  /* Explicit sizing — was rendering as the global ``width: 100%``
     and stretching to fill the parent label, which combined with
     the label's own width left the input ``way off on the right``
     of the prompt card (feedback #66). */
  width: 100%;
  height: 44px;
  box-sizing: border-box;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.95rem;
}
.leaderboard-handle-form .btn {
  flex: 0 0 auto;
  min-height: 44px;
}
.leaderboard-handle-form .handle-availability {
  flex: 1 1 100%;
  font-size: 0.82rem;
  min-height: 1.2em;
  color: var(--text-muted);
}
.leaderboard-handle-form .handle-availability[data-state="taken"] {
  color: var(--danger, #d97a7a);
}
.leaderboard-handle-form .handle-availability[data-state="ok"] {
  color: var(--success, #5ec47a);
}
.leaderboard-handle-current {
  margin: 1rem 0 0;
  padding: 0.55rem 0.85rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
}
.leaderboard-handle-current p { margin: 0; }
.leaderboard-handle-revoked {
  border-color: var(--warning);
  background: linear-gradient(140deg,
              var(--surface) 0%,
              var(--warning-dim) 100%);
}
.leaderboard-handle-revoked h3 { color: var(--warning); }
.leaderboard-anon {
  font-style: italic;
  color: var(--text-muted);
}

/* === Handle disclosure on /usage ============================== */
.contrib-handle-disclosure {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.4rem 0.7rem;
  margin: 0.4rem 0;
}
.contrib-handle-disclosure[open] {
  border-color: var(--accent);
  padding-bottom: 0.6rem;
}
.contrib-handle-disclosure summary {
  cursor: pointer;
  list-style: none;
  font-size: 0.9rem;
  padding: 0.1rem 0;
  /* Make the summary itself feel like a button so users
     understand it's clickable. */
  user-select: none;
}
.contrib-handle-hint {
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
}
.contrib-handle-disclosure summary::-webkit-details-marker { display: none; }
.contrib-handle-disclosure summary::marker { content: ''; }
.contrib-handle-form {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
  align-items: center;
}
.contrib-handle-form input[type="text"] {
  /* Explicit width + height so nothing in the cascade can stretch
     the input.  The previous ``flex: 0 1 18rem; width: auto``
     combination was getting overridden somewhere — the input
     rendered as a textarea-tall block taking the full card width
     and forcing the button onto its own row (feedback #66 — "the
     input box is massive on the usage page").
     ``width`` + ``flex: 0 0 auto`` together pin the size against
     any flex-grow / cross-axis-stretch that may be inherited from
     parent containers. */
  flex: 0 0 18rem;
  width: 18rem;
  max-width: 100%;
  min-width: 8rem;
  height: 44px;
  box-sizing: border-box;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.95rem;
}
.contrib-handle-form .btn {
  flex: 0 0 auto;
  /* Match the input height so the row reads as one tidy line. */
  min-height: 44px;
}
/* Availability hint (#67) — sits next to the input on the same
   row; on mobile it wraps under.  Single-line, badge-ish. */
.contrib-handle-form .handle-availability {
  flex: 1 1 100%;
  font-size: 0.82rem;
  min-height: 1.2em;
  margin-top: -0.25rem;
  color: var(--text-muted);
}
.contrib-handle-form .handle-availability[data-state="taken"] {
  color: var(--danger, #d97a7a);
}
.contrib-handle-form .handle-availability[data-state="ok"] {
  color: var(--success, #5ec47a);
}
.contrib-history-disclosure {
  margin-top: 0.25rem;
}
.contrib-history-disclosure summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--accent);
  padding: 0.35rem 0;
  list-style: none;
}
.contrib-history-disclosure summary::-webkit-details-marker { display: none; }
.contrib-leaderboard-link { margin-left: auto; }

/* Status notices used by handle-save / handle-error flashes. */
.notice-error {
  background: var(--danger-dim);
  border-color: var(--danger);
  color: var(--danger);
}
.notice-warning {
  background: var(--warning-dim);
  border-color: var(--warning);
  color: var(--warning);
}

/* === /usage "Your contributions" =============================
   Mirrors the leaderboard star treatment on the user's own
   /usage page so they don't have to click through to see their
   star count + the status of each piece of feedback they've
   sent in. */
.contrib-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.contrib-stars {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  font-size: 1.1rem;
  letter-spacing: 0.04em;
}
.contrib-stars-count {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 0.85rem;
  color: var(--text-muted);
}
.contrib-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.contrib-row {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.55rem;
  padding: 0.45rem 0.6rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  border-left-width: 3px;
}
.contrib-row-open      { border-left-color: var(--accent); }
.contrib-row-accepted  { border-left-color: var(--info); }
.contrib-row-rejected  { border-left-color: var(--danger); opacity: 0.7; }
.contrib-row-done      { border-left-color: var(--success); }
.contrib-row-status {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  align-self: center;
  font-family: ui-monospace, Menlo, monospace;
}
.contrib-row-body {
  font-size: 0.9rem;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

/* === Theme picker (on /usage) ================================
   Four-up row of swatches.  Each swatch's preview block has its
   own ``data-theme="..."`` attribute so its inner bars render in
   THAT palette regardless of the page's active theme — the user
   sees what they're switching to before they click. */
.theme-picker-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
.theme-picker {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.55rem;
  margin: 0.25rem 0 0.25rem;
}
@media (min-width: 720px) {
  .theme-picker {
    grid-template-columns: repeat(2, 1fr);
  }
}
@media (min-width: 1100px) {
  .theme-picker {
    grid-template-columns: repeat(4, 1fr);
  }
}
.theme-swatch {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  text-align: left;
  font: inherit;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.1s;
}
.theme-swatch:hover {
  border-color: var(--border-strong);
  background: var(--surface-2);
}
.theme-swatch:active { transform: scale(0.99); }
.theme-swatch:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.theme-swatch-active {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-ring);
}
.theme-swatch-preview {
  position: relative;
  width: 64px;
  height: 44px;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--border);
  overflow: hidden;
  display: block;
  box-shadow: var(--shadow-sm);
}
.theme-swatch-bar {
  position: absolute;
  left: 0;
  right: 0;
  display: block;
}
.theme-swatch-bg {
  top: 0;
  height: 100%;
  background: var(--bg);
}
.theme-swatch-surface {
  top: 8px;
  bottom: 8px;
  left: 8px;
  right: 22px;
  background: var(--surface);
  border-radius: 4px;
  border: 1px solid var(--border);
}
.theme-swatch-dot {
  position: absolute;
  top: 50%;
  right: 8px;
  transform: translateY(-50%);
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--surface);
}
.theme-swatch-text {
  position: absolute;
  top: 50%;
  left: 12px;
  transform: translateY(-50%);
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: 0.04em;
  z-index: 1;
}
.theme-swatch-meta {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}
.theme-swatch-meta strong {
  font-size: 0.95rem;
  font-weight: 600;
}
.theme-swatch-check {
  font-size: 1.1rem;
  color: var(--accent);
  opacity: 0;
  transition: opacity 0.15s;
}
.theme-swatch-active .theme-swatch-check {
  opacity: 1;
}
.theme-picker-note {
  margin-top: 0.25rem;
  color: var(--success);
}
