/* === 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>. */
.tenant-switcher {
  margin-left: auto;
  position: relative;
}
.tenant-switcher > 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;
}
.tenant-switcher > summary::-webkit-details-marker { display: none; }
.tenant-switcher > summary:hover {
  background: var(--hover-strong);
}
.tenant-switcher-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;
}
.tenant-switcher-name {
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tenant-switcher-chevron { opacity: 0.6; font-size: 0.7rem; }
.tenant-switcher-menu {
  position: absolute;
  right: 0;
  top: calc(100% + 0.3rem);
  min-width: 14rem;
  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;
}
.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-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); }

.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;
}
.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);
}

/* 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-rejected{ border-top: 3px solid var(--danger); }
.kanban-column-done    { border-top: 3px solid var(--success); }
.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); }
.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; }
.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);
  max-height: 90vh;
  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;
  top: 0.5rem;
  right: 0.5rem;
  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.3rem;
  cursor: pointer;
  font-family: inherit;
}
.item-dialog-close:hover { background: rgba(0, 0, 0, 0.8); }

/* 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;
  max-height: calc(94vh - 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; }

/* "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-wrap: wrap;
  gap: 0.5rem;
  align-items: flex-end;
}
.leaderboard-handle-form label {
  flex: 0 1 18rem;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  min-width: 8rem;
}
.leaderboard-handle-form input[type="text"] {
  /* Same width override as ``.contrib-handle-form`` — the global
     ``width: 100%`` on inputs would otherwise push the Claim
     button onto its own row. */
  width: auto;
  max-width: 100%;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.95rem;
}
.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-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.5rem;
  align-items: center;
}
.contrib-handle-form input[type="text"] {
  /* Override the global ``input[type="text"] { width: 100% }``
     rule that ships from the form-control reset — without this
     the input stretched to fill the flex row and pushed the
     Submit button onto its own line, which made the whole
     disclosure read as "multiple paragraphs of vertical space"
     (feedback #31).  Cap the width to something reasonable so
     the input + button sit on one line on a desktop column. */
  flex: 0 1 18rem;
  width: auto;
  max-width: 100%;
  min-width: 8rem;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}
.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);
}
