/* IAC Portal — shared stylesheet.
 *
 * Tokens, sidebar shape, and button/table conventions are intentionally
 * IDENTICAL to infra-monitoring's public/css/style.css (same iaconnects
 * "Enterprise slate" theme, same Inter font, same 8px/12px radius scale) —
 * this app runs as a sibling Docker container and should feel like the same
 * product family, not a different tool. Structure/content is new (built
 * from the Retool app's screenshots), so this file uses generic reusable
 * classes (.btn, .stat-tile, .data-table, .modal) rather than
 * infra-monitoring's per-page-id styling — that app hand-styles every
 * button/table by its own element id because it never needed more than one
 * table per page; this app has several (Assets, Customers, Sites) sharing
 * the exact same look, so one shared component vocabulary avoids
 * duplicating the same block three times.
 */

:root {
  --bg: #f2f4f7;
  --panel: #ffffff;
  --panel-alt: #f2f4f7;
  --text: #20202d;
  --text-secondary: #4a4a58;
  --muted: #6b6b6b;
  --border: #dddddd;
  --border-strong: #c3c4ae;
  --row-hover: rgba(32, 32, 45, 0.035);

  --sidebar-bg: #20202d;
  --sidebar-bg-hover: #323348;
  --sidebar-border: #323348;
  --sidebar-text: #a3a3ad;
  --sidebar-text-active: #ffffff;

  /* Brand accent for interactive/informational elements in the MAIN content
     area (buttons, focus rings, active states, badges) — deliberately a
     distinct color, not a shade of --text or of --status-fresh's green: it
     reads as a genuine third color rather than "text-black" (that's what
     --accent literally was before — identical hex to --text) or a
     near-duplicate of the success-green already used for status badges.
     "Deep Teal" — one of four directions mocked up and reviewed with Adrian
     before this was picked; calm/technical rather than a shade of the
     dashboard's chart-2 blue this replaced. --accent-on-dark (the brand
     lime) stays sidebar-only, unchanged — it has poor contrast against
     white (~1.6:1) so it was never usable as a button fill; it keeps doing
     the one job it's good at. */
  --accent: #0f6d66;
  --accent-hover: #2b7e78;
  --accent-soft: #d9efed;
  --accent-on-dark: #bed730;

  --status-fresh: #13612e;
  --status-fresh-soft: #dfeee4;
  --status-warning: #c25200;
  --status-warning-soft: #fbe7d4;
  --status-error: #7a1600;
  --status-error-soft: #fae3db;
  --status-unknown: #64748b;

  /* Categorical palette for the Dashboard's hand-rolled SVG charts
     (public/js/dashboard.js) — cycled in order, not tied to any specific
     category (state/type/customer all draw from the same sequence) so a
     chart's colors stay stable across re-renders regardless of which
     categories happen to be present. Muted/dusty tones rather than the
     saturated "rainbow" set this replaced (Adrian: dashboard colors were
     "very generic... go for more subtle tones") — same idea as --accent's
     own Deep Teal pick, kept out of pure grey/black/white so it still reads
     as color, just not a primary-colors palette. --chart-1 is --accent
     itself (rather than a separate hardcoded value) so the largest/first
     slice always reads as "the brand color" and stays in sync if --accent
     ever changes again; --chart-8 is the most desaturated of the eight,
     since it's also what a folded "Other (n)" bucket ends up colored
     (see dashboard.js's colorFor — always the last one picked). */
  --chart-1: var(--accent);
  --chart-2: #5b7ca8;
  --chart-3: #b9764f;
  --chart-4: #8a6aa8;
  --chart-5: #5f8a5a;
  --chart-6: #b06a7a;
  --chart-7: #96775c;
  --chart-8: #8f959c;

  --shadow-sm: 0 1px 3px rgba(32, 32, 45, 0.08);
  --shadow-md: 0 4px 14px rgba(32, 32, 45, 0.10);
  --shadow-lg: 0 12px 32px rgba(32, 32, 45, 0.14);

  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;

  --font-sans: 'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; }
button { font: inherit; }
button, input, select, textarea {
  transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease, transform 0.08s ease;
}
button:not(:disabled) { cursor: pointer; }
button:not(:disabled):active { transform: scale(0.98); }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.hidden { display: none !important; }
table { font-variant-numeric: tabular-nums; border-collapse: collapse; width: 100%; }

/* ---------------------------------------------------------------------- */
/* App shell / sidebar                                                     */
/* ---------------------------------------------------------------------- */

#app-shell { display: flex; min-height: 100vh; gap: 20px; background: var(--bg); }

#app-sidebar {
  width: 208px;
  flex-shrink: 0;
  background: var(--sidebar-bg);
  padding: 16px 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  box-shadow: var(--shadow-md);
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}
#app-sidebar-brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 0 20px 16px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--sidebar-border);
}
.app-sidebar-brand-logo { display: block; width: 140px; height: auto; }
.app-sidebar-brand-subtitle {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
}

/* Hamburger toggle — desktop never shows it (the sidebar's always fully
   visible there); see the mobile breakpoint below for what it looks like
   and does once it's shown. Wrapping the nav links/user-widget/version in
   #app-sidebar-links (rather than leaving them direct children of
   #app-sidebar) is what lets the mobile breakpoint hide/show that one
   element instead of every link individually. */
#app-nav-toggle { display: none; }
#app-sidebar-links {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-height: 0;
}
.app-nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 12px;
  padding: 9px 12px;
  border-radius: 8px;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 13.5px;
}
.app-nav-link svg { flex-shrink: 0; opacity: 0.85; }
.app-nav-link:hover { color: var(--sidebar-text-active); background: var(--sidebar-bg-hover); }
.app-nav-link.active {
  color: var(--sidebar-text-active);
  background: var(--sidebar-bg-hover);
  font-weight: 600;
  box-shadow: inset 3px 0 0 var(--accent-on-dark);
}
.app-nav-link.active svg { opacity: 1; color: var(--accent-on-dark); }
.app-nav-link.disabled { opacity: 0.4; pointer-events: none; }
.app-nav-soon {
  margin-left: auto;
  font-size: 9px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--sidebar-text);
  border: 1px solid var(--sidebar-border);
  border-radius: 4px;
  padding: 1px 4px;
}
#app-version { padding: 4px 20px 0; font-size: 11px; color: var(--sidebar-text); }
#app-content { flex: 1; min-width: 0; }

#app-sidebar #user-widget {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: 12px 20px;
  border-top: 1px solid var(--sidebar-border);
  font-size: 12px;
  color: var(--sidebar-text);
}
#app-sidebar #user-widget .role-badge {
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.04em;
  border: 1px solid var(--sidebar-border);
  border-radius: 4px;
  padding: 2px 6px;
  color: var(--sidebar-text);
}
#app-sidebar #sign-out-btn {
  width: 100%;
  padding: 5px 10px;
  font-size: 12px;
  background: transparent;
  color: var(--sidebar-text);
  border: 1px solid var(--sidebar-border);
  border-radius: 8px;
}
#app-sidebar #sign-out-btn:hover { background: var(--sidebar-bg-hover); color: var(--sidebar-text-active); }

header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  /* Some pages' header only ever has an <h1> (Dashboard, Reports, Audit
     Logs, Lookup Lists); others also have a right-hand action-button <div>
     (Assets' "Export", Maintenance Tools' "+ New report", etc.). Without an
     explicit min-height, the header's own height was just whatever its
     tallest child needed — shorter on the button-less pages, since a .btn
     (padding 7px 14px, border) is taller than the bare <h1> line-height —
     so switching between those pages and the rest visibly shifted the
     whole layout down/up by a few pixels. This pins every page's header to
     the same height regardless of which pages happen to have a button. */
  min-height: 64px;
}
header h1 { font-size: 19px; font-weight: 700; margin: 0; letter-spacing: -0.01em; }
.section-subtitle { font-size: 13.5px; color: var(--muted); margin: 0 0 12px; }
main, .page-main { padding: 20px 24px 40px; }

/* ---------------------------------------------------------------------- */
/* Buttons                                                                  */
/* ---------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border-radius: 8px;
  padding: 7px 14px;
  font-size: 13px;
  font-weight: 500;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
}
.btn:hover { border-color: var(--border-strong); background: var(--row-hover); }
.btn:disabled { opacity: 0.5; cursor: default; }
.btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); box-shadow: var(--shadow-md); }
.btn-danger { background: var(--panel); color: var(--status-error); border-color: var(--status-error); }
.btn-danger:hover { background: var(--status-error-soft); }
.btn-sm { padding: 4px 10px; font-size: 12px; }

/* ---------------------------------------------------------------------- */
/* Stat tiles                                                               */
/* ---------------------------------------------------------------------- */

.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px; }
.stat-tile {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  box-shadow: var(--shadow-sm);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.stat-tile-label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 4px; }
.stat-tile-value { font-size: 24px; font-weight: 700; }
/* Small "X new in the last 30 days" line under a stat-tile-value — folds a
   period-over-time count into its running-total tile (e.g. Total assets)
   instead of giving it a whole separate tile of its own. Up/down mirror
   .contract-yes/.audit-log-old's existing --status-fresh/--status-error
   convention rather than inventing new colors; "up" is the only case any
   current tile actually produces (a "new in period" count can't go
   negative), .stat-tile-trend-down exists for symmetry/future reuse. */
.stat-tile-trend { font-size: 12px; margin-top: 4px; color: var(--muted); }
.stat-tile-trend-up { color: var(--status-fresh); }
.stat-tile-trend-down { color: var(--status-error); }
.stat-tile-filter {
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.stat-tile-filter:hover { border-color: var(--border-strong); background: var(--row-hover); }
.stat-tile-filter.active {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent);
}
.stat-tile-filter.active .stat-tile-label { color: var(--accent); }
.stat-tile-icon {
  width: 34px;
  height: 34px;
  border-radius: 8px;
  background: var(--panel-alt);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

/* ---------------------------------------------------------------------- */
/* Filter bars                                                              */
/* ---------------------------------------------------------------------- */

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 16px;
  margin-bottom: 20px;
  box-shadow: var(--shadow-sm);
}

.filter-notice {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  border-radius: var(--radius-md);
  padding: 10px 16px;
  margin-bottom: 16px;
  font-size: 13.5px;
  color: var(--accent);
}
/* A filter bar that lives inside a panel's own .panel-body (see
   customers.html's Customers/Sites/Contacts sections) reads as part of
   that panel rather than a second floating box sitting on top of it —
   strips the standalone card look (background/border/shadow/margin) the
   base .filter-bar has and adds a bottom divider instead, so it's visually
   continuous with the table directly beneath it while still being clearly
   grouped with that one table and not any other panel on the page. */
.panel-body .filter-bar-inline {
  background: none;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  box-shadow: none;
  padding: 16px;
  margin-bottom: 0;
}
.filter-field { display: flex; flex-direction: column; gap: 4px; min-width: 160px; flex: 1; }
.filter-field label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }

/* Filters button + popover — mobile-only (see the 720px breakpoint below).
   On desktop #filters-btn stays hidden, and both wrapper elements collapse
   with display:contents so their contents — the button (hidden, so nothing)
   and every filter-field + Clear inside the panel — become direct children
   of .filter-bar itself, right alongside the Search field. That's what
   makes Search and every dropdown share the exact same .filter-field
   flex:1/min-width:160px sizing and end up equal width: there's no nested
   flex row splitting the space unevenly any more, just one flat row.
   Below 720px the wrappers become real boxes again and the panel turns
   into the popover (a stacked column, one field per row) behind the
   button, since a row of 4+ selects next to the search box is the first
   thing to break on a phone-width screen. */
.filters-picker { display: contents; }
.filters-btn { display: none; position: relative; }
.filters-btn.has-active-filters::after {
  content: '';
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
/* Higher specificity than .hidden's class-only selector (and its own
   !important), so the "closed" state that class represents (used to drive
   the mobile popover) has no effect here — the filters stay unwrapped and
   visible on desktop regardless. */
#filters-panel {
  display: contents !important;
}
.filters-panel .filter-field { min-width: 160px; flex: 1; }

input[type="text"], input[type="search"], input[type="email"], input[type="tel"],
input[type="number"], input[type="date"], input[type="password"], select, textarea {
  font: inherit;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  color: var(--text);
  width: 100%;
}
/* Marks the specific field a submit error was actually about (e.g. a
   duplicate Dev EUI) — see highlightFieldErrors in assets.js. Border +
   focus ring only, no background fill, so it reads as "this one" without
   competing with .import-row-invalid's much louder full-row treatment
   (that one flags many rows in a big table; this is a single field next to
   its own error text already visible in the modal). */
.field-invalid { border-color: var(--status-error) !important; }
.field-invalid:focus-visible { outline-color: var(--status-error); }
label { font-size: 13px; color: var(--text-secondary); display: flex; flex-direction: column; gap: 4px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; align-items: start; }
/* Pairs a field with its "Clear X" checkbox as a single .form-grid cell, so
   the pair always stays together (never wraps into a different column than
   its own field, or lands beside an unrelated field) regardless of how many
   columns the surrounding auto-fit grid renders at a given width. */
.field-with-clear { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field-with-clear .checkbox-field { white-space: nowrap; }
/* Pairs a date input with a small shortcut button on one row (e.g. Warranty
   expiry's "+1 yr" button, which fills it from Install date + 12 months) —
   label text stays above per the plain `label` rule below, this just keeps
   the input and button side by side underneath it instead of stacking. */
.input-with-shortcut { display: flex; gap: 6px; align-items: center; }
.input-with-shortcut input { flex: 1; min-width: 0; }
.input-with-shortcut button { flex-shrink: 0; white-space: nowrap; }

/* Camera-scan fields — search box + Serial number + Dev EUI on the Assets
   page (see attachCameraScanButton in public/js/barcode-scan.js). Reuses
   .input-with-shortcut for the input+button row itself; this wraps that row
   with room underneath for a transient "Scanned: …" / "No barcode found…"
   status line, which the row alone has no room for. */
.camera-scan-field { display: flex; flex-direction: column; gap: 4px; }
/* Camera-scan buttons (search box, Serial number, Dev EUI — see
   attachCameraScanButton in public/js/barcode-scan.js) are desktop-hidden by
   default and only restored inside the @media (max-width: 720px) block
   below, same breakpoint public/js/assets.js's own isMobileViewport() uses
   for every other mobile/desktop layout decision on this page. The button
   was originally meant to work on desktop too via barcode-scan.js's file-
   picker fallback (see that file's own top comment), but Adrian asked for
   it to only ever appear on mobile — hiding it here (rather than touching
   every call site in assets.js) keeps that behavior in one place and still
   lets it reappear automatically if the window is resized/rotated across
   the breakpoint, no JS re-wiring needed. */
.camera-scan-btn { display: none; padding: 6px 9px; }
.camera-scan-status { font-size: 12px; color: var(--muted); }
.camera-scan-status.camera-scan-status-error { color: var(--status-error); }
.form-section { margin-bottom: 20px; }
.form-section h3 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin: 0 0 10px; }
.checkbox-field { display: flex; flex-direction: row; align-items: center; gap: 8px; }
.checkbox-field input { width: auto; }

/* On/off toggle switch — used for the asset edit form's "Under contract"
   field in place of a plain tickbox. Same true/false meaning as a checkbox
   (still a real <input type="checkbox">, just visually hidden), so no other
   code needs to change: reads/writes the same .checked property under the
   same field name. Green when on, red when off — reuses the existing
   --status-fresh/--status-error tokens rather than inventing new colors. */
.toggle-field { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.toggle-switch { position: relative; display: inline-block; width: 42px; height: 24px; flex-shrink: 0; }
.toggle-switch input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.toggle-switch-track {
  position: absolute;
  inset: 0;
  background: var(--status-error);
  border-radius: 999px;
  transition: background-color 0.15s ease;
}
.toggle-switch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 20px;
  height: 20px;
  background: #ffffff;
  border-radius: 50%;
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease;
}
.toggle-switch input:checked ~ .toggle-switch-track { background: var(--status-fresh); }
.toggle-switch input:checked ~ .toggle-switch-track .toggle-switch-thumb { transform: translateX(18px); }
.toggle-switch input:focus-visible ~ .toggle-switch-track { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------------------------------------------------------------------- */
/* Panels / data tables                                                     */
/* ---------------------------------------------------------------------- */

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  margin-bottom: 20px;
  overflow: hidden;
}
.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
}
.panel-header h2 { font-size: 15px; margin: 0; }
.panel-body { padding: 0; }
.panel-body.padded { padding: 16px; }

/* Dashboard (public/dashboard.html + public/js/dashboard.js) — two charts
   side by side above a full-width one on wide screens, stacking to a single
   column once they'd otherwise get too narrow to read. */
/* Lookup Lists admin page (public/lookups-admin.html) — one tab per
   manageable list (Manufacturers/Models/Battery types/Tags/Device types/
   Asset states), switched client-side (see public/js/lookups-admin.js)
   rather than six stacked panels, since only one list is ever being worked
   on at a time. */
.lookup-tabs { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
.lookup-tab { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--border); background: var(--panel); color: var(--text-secondary); font-size: 13px; font-weight: 500; cursor: pointer; }
.lookup-tab:hover { border-color: var(--border-strong); }
.lookup-tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.lookup-row-name-input { width: 100%; max-width: 260px; }
.lookup-usage-count { font-variant-numeric: tabular-nums; color: var(--text-secondary); }
.lookup-usage-count.lookup-usage-zero { color: var(--muted); }
/* Manufacturers/models rows with at least one asset link their count
   straight to that filtered assets list (see assetLinkParam in
   lookups-admin.js) — styled as an actual link rather than inheriting the
   plain muted text color the global `a { color: inherit }` reset would
   otherwise give it, so it reads as clickable rather than as a stray
   number. */
.lookup-usage-count a { color: var(--accent); text-decoration: underline; }
.lookup-usage-count a:hover { color: var(--accent-hover); }
/* Assets page header's "Device sync: 3m ago" link (public/js/assets.js's
   loadIntegrationSyncStatus(), left of the Export button) — same "style as
   an actual link, don't just inherit the muted color" reasoning as
   .lookup-usage-count a above. */
#asset-sync-status a { color: var(--accent); text-decoration: underline; }
#asset-sync-status a:hover { color: var(--accent-hover); }
#asset-sync-status.contract-no a { color: var(--status-error); }
.lookup-add-row { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; padding: 14px 16px; border-bottom: 1px solid var(--border); background: var(--panel-alt); }
.lookup-add-row label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-secondary); }
/* .lookup-add-row label (a class + a type selector) outranks the plain
   .checkbox-field class it's also carrying on the "Installation height
   applies" field (Models tab), so without this override its flex-direction:
   column would win and stack the checkbox above its own label text — same
   bug already fixed once for .columns-list-label and .pagination-size. */
.lookup-add-row .checkbox-field { flex-direction: row; align-self: center; }
.lookup-import-export { display: flex; gap: 8px; align-items: center; }

/* Each dashboard module (Assets/Maintenance/Customers) as its own bordered,
   shadowed card rather than a plain uppercase label over a run of tiles —
   Adrian's own flag: "the word Assets on the left hand side is not clear
   enough" for what's meant to be a page divider between modules. Fill is
   --panel-alt, which is literally the same value as the page's own --bg
   (see :root above) — so this adds a visible boundary/shadow/padding
   without changing the white stat-tile/panel cards' existing contrast
   against their surroundings; only the grouping became more visible, not
   the individual components inside it. */
.dashboard-section {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: 20px 20px 24px;
  margin-bottom: 24px;
}
.dashboard-section-header {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding-bottom: 16px;
  margin-bottom: 18px;
  border-bottom: 1px solid var(--border);
}
.dashboard-section-icon {
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  background: var(--accent-soft);
  color: var(--accent);
}
/* One identity color per module, reusing the same icon each module's own
   sidebar nav link already uses (see #app-sidebar-links in dashboard.html)
   so the section reads as "the Maintenance page's own icon, just bigger"
   rather than a new, unrelated symbol. */
.dashboard-section-icon--maintenance { background: var(--status-warning-soft); color: var(--status-warning); }
.dashboard-section-icon--customers { background: #dce6f0; color: var(--chart-2); }
.dashboard-section-title { font-size: 17px; font-weight: 700; color: var(--text); margin: 0; line-height: 1.3; }
.dashboard-section-description { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.dashboard-section .dashboard-grid:last-child { margin-top: 20px; }

.dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); gap: 20px; align-items: start; }
.dashboard-grid .panel { margin-bottom: 0; }
.dashboard-chart-body { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
.dashboard-chart-body svg { flex-shrink: 0; }
.chart-legend { list-style: none; margin: 0; padding: 0; flex: 1; min-width: 160px; }
.chart-legend-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 13px; }
.chart-legend-swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
.chart-legend-name { flex: 1; color: var(--text-secondary); }
.chart-legend-count { font-weight: 600; font-variant-numeric: tabular-nums; }
.bar-chart { display: flex; flex-direction: column; gap: 10px; width: 100%; }
.bar-chart-row { display: grid; grid-template-columns: 140px 1fr 44px; align-items: center; gap: 10px; font-size: 13px; }
.bar-chart-row-label { color: var(--text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-chart-track { background: var(--panel-alt); border-radius: 4px; height: 14px; overflow: hidden; }
.bar-chart-fill { height: 100%; border-radius: 4px; }
.bar-chart-row-count { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
/* Assets by customer — online/offline (public/js/dashboard.js's
   renderStackedBarChart) — two segments side by side in one track rather
   than the single fill the plain bar chart above uses. border-radius: 0 on
   the segments themselves (not the track, which keeps its own) avoids a
   rounded notch appearing at the seam between them. */
.bar-chart-track--stacked { display: flex; }
.bar-chart-track--stacked .bar-chart-fill { border-radius: 0; }
.bar-chart-fill--online { background: var(--status-fresh); }
.bar-chart-fill--offline { background: var(--status-error); }

/* Dashboard month-by-month heatmaps (public/js/dashboard.js's
   renderHeatmap) — "New assets by month" and "Maintenance activity by
   month", each a plain HTML table with one COLUMN per trailing month
   (the x-axis) and one colored cell per metric ROW; each cell's
   background/text color is set inline per-value (see heatmapCellStyle),
   not by a class, since the color is data-driven rather than one of a
   fixed set of states. Border-collapse: separate + border-spacing is what
   gives the cells their gap — letting each <td> keep its own
   border-radius, which collapsed borders would prevent. Wrapped in
   .table-scroll by renderHeatmap itself: 12 month columns run wider than
   a phone screen. */
.heatmap-table { border-collapse: separate; border-spacing: 6px; width: 100%; }
.heatmap-table th { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--muted); padding: 0 8px 6px; text-align: center; }
.heatmap-table th:first-child { text-align: left; padding-left: 2px; }
.heatmap-table tbody th { font-size: 13px; font-weight: 600; text-transform: none; letter-spacing: normal; color: var(--text-secondary); text-align: left; white-space: nowrap; padding: 0 8px 0 2px; }
.heatmap-cell { border-radius: var(--radius-sm); padding: 10px 8px; text-align: center; font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; min-width: 60px; }

.table-scroll { overflow-x: auto; }
/* Caps a table's own height with an internal vertical scrollbar, rather
   than letting the whole page grow taller as more rows get added — used on
   customers.html, which stacks three of these tables on one page and would
   otherwise keep growing indefinitely as customers/sites/contacts pile up.
   thead is sticky within that scroll area so the column headers stay
   visible while scrolling a long list. */
.table-scroll-capped { max-height: 380px; overflow-y: auto; }
.table-scroll-capped thead th { position: sticky; top: 0; background: var(--panel-alt); z-index: 1; }

.data-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
  background: var(--panel-alt);
}
.data-table th.sortable-col { cursor: pointer; user-select: none; }
.data-table th.sortable-col:hover { color: var(--text-secondary); }
.data-table th .sort-indicator { display: inline-block; width: 10px; margin-left: 2px; }
.data-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: top; font-size: 13px; }
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover { background: var(--row-hover); cursor: pointer; }
.data-table .muted { color: var(--muted); }
.data-table .row-actions { white-space: nowrap; text-align: right; }
.empty-row td { text-align: center; color: var(--muted); padding: 24px; }

/* Resizable columns — assets.html's table only (#assets-table; see
   attachColumnResizeHandle() in assets.js). table-layout:fixed makes each
   <th>'s own width authoritative for its whole column, so dragging the
   handle at a th's right edge to change that one <th>'s width is enough to
   resize the column — on both desktop's full column set and the smaller
   mobile one alike, since it's the same head row either way. */
#assets-table { table-layout: fixed; }
#assets-table th { position: relative; overflow: hidden; text-overflow: ellipsis; }
.col-resize-handle {
  position: absolute;
  top: 0;
  right: -4px;
  bottom: 0;
  width: 8px;
  cursor: col-resize;
  z-index: 3;
  touch-action: none;
}
.col-resize-handle:hover, .col-resize-handle.resizing { background: var(--accent); opacity: 0.5; }

.badge {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--muted);
  background: var(--panel-alt);
  white-space: nowrap;
}
.badge-accent { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.badge-success { color: var(--status-fresh); border-color: var(--status-fresh); background: var(--status-fresh-soft); }
.badge-warning { color: var(--status-warning); border-color: var(--status-warning); background: var(--status-warning-soft); }
.badge-error { color: var(--status-error); border-color: var(--status-error); background: var(--status-error-soft); }
.tag-chip {
  display: inline-block;
  font-size: 10.5px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 999px;
  /* Soft accent tint rather than flat grey — tag chips show up in the
     Assets table, the asset detail view, and the Lookup Lists Tags tab, so
     this is one of the more frequently-seen "grey pill" spots in the app. */
  background: var(--accent-soft);
  color: var(--accent);
  margin: 1px 2px 1px 0;
}
.contract-yes { color: var(--status-fresh); }
.contract-no { color: var(--muted); }

/* Integrations page's status readout (public/integrations.html) — a plain
   label/value grid, two columns on desktop collapsing to one on mobile same
   breakpoint as everywhere else in this app (720px). */
.integrations-status-grid {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 8px 16px;
  margin: 0;
}
.integrations-status-grid dt { color: var(--muted); font-weight: 500; }
.integrations-status-grid dd { margin: 0; }
@media (max-width: 720px) {
  .integrations-status-grid { grid-template-columns: 1fr; }
  .integrations-status-grid dt { margin-top: 8px; }
}

/* ---------------------------------------------------------------------- */
/* Modals                                                                   */
/* ---------------------------------------------------------------------- */

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 16px;
  overflow-y: auto;
  background: rgba(15, 23, 42, 0.45);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.modal-panel {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-lg);
  padding: 24px;
  width: 560px;
  max-width: calc(100vw - 32px);
}
.modal-panel.modal-wide { width: 760px; }
.modal-panel h3 { margin: 0 0 4px; font-size: 16px; }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }
.modal-error { color: var(--status-error); font-size: 13px; margin: 8px 0 0; }
/* Assets table Dev EUI / Serial # columns (see ASSET_COLUMNS in
   public/js/assets.js) — a monospace, tabular-figure font so every
   character in a hex identifier occupies the same width, instead of
   Inter's proportional digit/letter glyphs making a digit-heavy value read
   as visibly bigger than a letter-heavy one of the same length. */
.mono-cell { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; font-variant-numeric: tabular-nums; }
/* Bulk-edit device-type change warning (see public/assets.html) — same
   --status-error/--status-error-soft tokens as every other error/danger
   surface in this file, not a one-off color. */
.modal-warning-box { border: 1px solid var(--status-error); background: var(--status-error-soft); border-radius: 8px; padding: 12px 14px; }
.modal-warning-title { color: var(--status-error); font-weight: 600; font-size: 13.5px; margin: 0 0 6px; }
.modal-warning-text { color: var(--status-error); font-size: 12.5px; margin: 0 0 10px; line-height: 1.5; }
.modal-warning-box input[type="text"] { width: 100%; box-sizing: border-box; }
.modal-header-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
/* Groups multiple header-row buttons (e.g. the asset modal's "View change
   log" + "Maintenance Log") so space-between still splits title-vs-actions
   two ways, not unevenly across three flex children. */
.modal-header-actions { display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap; flex-shrink: 0; }

/* Asset form: the date fields sit on their own row below the other
   Details, with a gap above to separate them. */
.form-grid-dates { margin-top: 20px; }
/* auto-fill (not auto-fit) keeps the empty tracks, so Under contract and
   Renewal date keep a normal field width side by side instead of each
   stretching across half the modal. Centered so the toggle lines up with
   the date input rather than sitting at the top of the row. */
.contract-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); align-items: center; }

/* Asset modal "Field rules" help view (see #asset-help-view). */
.field-rules { margin: 12px 0 0; }
.field-rules dt { font-weight: 600; font-size: 13.5px; color: var(--text); margin-top: 14px; }
.field-rules dt:first-child { margin-top: 0; }
.field-rules dd { margin: 4px 0 0; font-size: 13px; line-height: 1.5; color: var(--text-secondary); padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.field-rules dd:last-child { border-bottom: none; }

/* Close "X" injected into every modal by auth-widget.js's
   addModalCloseButtons() — .modal-panel-closable's extra top padding keeps
   it clear of whatever a modal already has at the top (a title, or a
   .modal-header-row with its own button(s) on the right, e.g. the asset
   modal's "View change log" or the maintenance report modal's "View
   changes"/"Edit"/"Download PDF" row) rather than the two overlapping. */
.modal-panel.modal-panel-closable { padding-top: 54px; }
.modal-close-x {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
}
.modal-close-x:hover { background: var(--row-hover); border-color: var(--border-strong); color: var(--text); }

/* The maintenance report wizard and its read-only view both got flagged as
   cramped, with text on the small side and a couple of nested blocks whose
   background happened to be the exact same grey as the page behind the
   modal (--panel-alt === --bg — see the report-asset-row/
   report-preview-customer/report-bulk-notes-bar rules below), which read as
   a hole in the modal rather than a card sitting on it. A first pass at this
   only bumped a few font sizes and left everything sitting flush against
   the modal's own edges — still cramped, per Adrian's follow-up — so this
   second pass adds real padding around the whole modal and turns the
   summary grid into an actual bordered card (matching .info-subpanel's own
   look elsewhere) instead of bare text, on top of larger type throughout.
   Scoped to these two modals specifically (by id) rather than changing the
   shared classes everywhere they're used elsewhere (assets.html's own
   modals, in particular) — worth keeping in mind as the baseline for new
   screens going forward, without retrofitting every existing one right
   now. */
#report-modal .modal-panel.modal-wide, #view-report-modal .modal-panel.modal-wide { width: 860px; padding: 32px; }
#report-modal .modal-panel.modal-wide.modal-panel-closable, #view-report-modal .modal-panel.modal-wide.modal-panel-closable { padding-top: 58px; }
/* The report wizard (public/reports.html/reports.js) shares one modal
   across every report type, including "Asset key dates" — Asset/Customer/
   Site plus up to 4 date columns, 7 in total — which the base 760px
   .modal-wide squeezed hard enough to force horizontal scrolling on the
   results table. Widened just for this modal (max-width still guards
   narrow screens, same as every other .modal-panel) rather than the shared
   .modal-wide class every other modal uses. */
#report-wizard-modal .modal-panel.modal-wide { width: 1100px; }
/* Add asset wizard (public/assets.html's #wizard-modal, public/js/assets.js)
   — step 2's one-row-per-asset grid can run to well over a dozen columns
   once the chosen device type's own detail fields (Dev EUI, IP address,
   MAC address, and so on) join the common ones every asset has, so this
   needs more room than any other modal in the app; .table-scroll still
   catches whatever doesn't fit even at this width. */
#wizard-modal .modal-panel.modal-wide { width: min(96vw, 1400px); max-width: 96vw; }
#wizard-modal .wizard-table th, #wizard-modal .wizard-table td { white-space: nowrap; }
#wizard-modal .wizard-table input, #wizard-modal .wizard-table select { width: 100%; min-width: 110px; }
#wizard-modal .wizard-table input[type="checkbox"] { width: auto; min-width: 0; }
#wizard-modal .wizard-table input[type="number"] { min-width: 90px; }
.wizard-row-number { color: var(--muted); font-variant-numeric: tabular-nums; text-align: center; }
.wizard-checkbox-cell { text-align: center; }
/* The 32px panel padding above only buys space around the OUTSIDE edge of
   the modal — it does nothing about the gap between the header button row
   (View changes/Edit/Download PDF) and whatever's rendered directly below
   it, which had no margin of its own and so sat right up against those
   buttons regardless of how much outer padding the panel had. Also bumps
   the buttons themselves up a size — the header row in particular was
   using the small .btn-sm variant throughout, which read as undersized
   next to the larger type the rest of this pass introduced. */
#report-modal .modal-header-row, #view-report-modal .modal-header-row { margin-bottom: 20px; }
#report-modal .btn, #view-report-modal .btn { padding: 9px 18px; font-size: 14.5px; }
#report-modal label, #view-report-modal label { font-size: 14.5px; }
#report-modal .form-grid { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px 20px; }
#report-modal .section-subtitle, #view-report-modal .section-subtitle { font-size: 15px; line-height: 1.6; }
#report-modal .info-subpanel-grid, #view-report-modal .info-subpanel-grid {
  gap: 18px 28px;
  /* Turns the summary fields into a real card — border, tinted background,
     its own internal padding — rather than bare text sitting flush against
     the modal's edges (the literal "no margins" complaint). */
  background: #f7f8fb;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 20px 24px;
}
#report-modal .info-subpanel-item-label, #view-report-modal .info-subpanel-item-label { font-size: 13px; margin-bottom: 4px; }
#report-modal .info-subpanel-item-value, #view-report-modal .info-subpanel-item-value { font-size: 16px; line-height: 1.4; font-weight: 500; }

/* Asset change log — the in-modal "View change log" view (see
   public/js/assets.js) and the standalone Audit Logs page both render a
   list of these grouped entries. */
.audit-log-list { max-height: 60vh; overflow-y: auto; margin-top: 14px; }
.audit-log-empty { color: var(--muted); font-size: 13.5px; }
.audit-log-group { padding: 12px 0; border-bottom: 1px solid var(--border); }
.audit-log-group:first-child { padding-top: 0; }
.audit-log-group:last-child { border-bottom: none; }
.audit-log-group-meta { font-size: 12.5px; color: var(--muted); margin-bottom: 8px; }
.audit-log-group-meta .audit-log-asset-link { font-weight: 600; color: var(--text); }
.audit-log-field { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 13.5px; margin: 4px 0; }
.audit-log-field-name { font-weight: 600; min-width: 150px; flex-shrink: 0; }
.audit-log-old { color: var(--status-error); }
.audit-log-new { color: var(--status-fresh); }
.audit-log-arrow { color: var(--muted); }
/* Deleted/restored are synthetic "Asset status" rows (see entry_type in
   db/schema.sql) rather than a plain field edit — bolding the new value
   makes them easier to spot scanning down a mixed list of ordinary edits. */
.audit-log-field-deleted .audit-log-new { font-weight: 700; }
.audit-log-field-restored .audit-log-new { font-weight: 700; }
/* A row logged by "Revert this change" (entry_type='field_revert' — see
   db/schema.sql) rather than a direct edit — pale orange to flag at a
   glance that this entry is itself the result of a revert. Reuses the
   existing warning token rather than inventing a new color; extended out
   to the row's full width with a negative margin so the tint reads as a
   highlighted row, not just tinted text. No Revert button is rendered for
   this entry_type (see renderAuditGroups) — instead .audit-log-reverted-label
   sits in that same slot, below, spelling out what the highlight means. */
.audit-log-field-field_revert {
  background: var(--status-warning-soft);
  padding: 4px 8px;
  margin: 4px -8px;
  border-radius: var(--radius-sm);
}
.audit-log-revert-btn { margin-left: auto; }
/* Maintenance Log view (see public/js/assets.js) reuses the same
   .audit-log-group/-meta shell as the change log above, but each group
   holds free-text work notes rather than field-diff rows, so they get
   their own class: normal block flow with wrapping, not the diff layout's
   flex row. */
.audit-log-note-text { font-size: 13.5px; line-height: 1.5; white-space: pre-wrap; }
/* Takes the Revert button's own place (same margin-left: auto, so it lands
   in the same right-hand slot) on a field_revert row — text rather than a
   button since there's nothing left to click, just the orange highlight's
   meaning spelled out. */
.audit-log-reverted-label { margin-left: auto; color: var(--status-warning); font-weight: 600; font-size: 12.5px; }
/* "Recently deleted" panel rows (public/js/audit-log.js) reuse
   .audit-log-field's layout for the same reason every other list on this
   page does — one flex row, wraps at narrow widths, no new component. */
.deleted-asset-meta { color: var(--muted); }
.deleted-asset-days { font-weight: 600; white-space: nowrap; }

/* Audit Logs page (public/audit-log.html) — the "Changes" and "Platform
   changes" panels render as flat, columnised .data-table bodies (one row
   per field change) rather than the grouped blocks above, which stay as-is
   for the in-modal "View change log" (see public/js/assets.js) where the
   narrower modal still suits that layout. .audit-log-old/.audit-log-new
   (above) are reused here as-is for the Old/New value cells — both are
   plain color rules with nothing table-unsafe in them. */
.audit-table td { vertical-align: middle; }
.audit-table .badge { white-space: nowrap; }

/* Shared pagination control (public/js/audit-log.js's createPaginator) —
   one instance per table, both styled identically so "25/50/100 rows" reads
   as the same control in both places. */
.pagination-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Was flex-wrap: wrap — at anything narrower than the info text + all
     three controls combined, that dropped "Rows per page [25]" onto its
     own line under Prev/Next instead of keeping the whole bar on one row.
     nowrap + overflow-x here (same escape hatch .table-scroll already uses
     for wide tables) keeps Prev/Next/Rows-per-page together on one line at
     any width, scrolling horizontally in the rare case it doesn't fit
     rather than ever stacking. */
  flex-wrap: nowrap;
  overflow-x: auto;
  gap: 10px;
  padding: 10px 16px 14px;
  font-size: 12.5px;
  color: var(--text-secondary);
}
.pagination-info { font-variant-numeric: tabular-nums; white-space: nowrap; }
.pagination-controls { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; white-space: nowrap; flex-shrink: 0; }
/* flex-direction: row is required here, not just display:flex — the global
   `label` rule above sets flex-direction: column for ordinary above-input
   labels, and .pagination-size is itself a <label>, so without this the
   dropdown stacked under "Rows per page" instead of sitting beside it (same
   bug already fixed once for .columns-list-label). */
.pagination-size { display: flex; flex-direction: row; align-items: center; gap: 6px; white-space: nowrap; }
.pagination-size select { padding: 3px 6px; font-size: 12.5px; }

/* ---------------------------------------------------------------------- */
/* Login page                                                               */
/* ---------------------------------------------------------------------- */

#login-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
#login-page .login-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  padding: 36px 32px;
  width: 360px;
  max-width: calc(100vw - 32px);
  text-align: center;
}
#login-page h1 { font-size: 20px; margin: 0 0 4px; }
#login-page .login-brand { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 20px; }
#login-error {
  background: var(--status-error-soft);
  color: var(--status-error);
  border-radius: 8px;
  padding: 8px 12px;
  font-size: 13px;
  margin-bottom: 14px;
  text-align: left;
}
#microsoft-sign-in-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 10px 14px;
  border-radius: 8px;
  background: var(--accent);
  color: #fff;
  border: 1px solid var(--accent);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
}
#microsoft-sign-in-btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
#login-password-form { display: flex; flex-direction: column; gap: 12px; text-align: left; }

@media (max-width: 720px) {
  #app-shell { flex-direction: column; gap: 0; }
  /* The sidebar itself becomes a slim top bar (brand + hamburger) instead
     of the full-height column it is on desktop — #app-sidebar-links (the
     actual links/user-widget/version) is a dropdown panel positioned
     below it, hidden until #app-nav-toggle is clicked (see
     initMobileNav() in public/js/auth-widget.js). Five links
     (Import Tool/Reports/User Management/Audit Logs/Lookup Lists) are
     desktop-only tools Adrian doesn't want cluttering the phone nav at
     all — .app-nav-desktop-only just hides them outright here rather than
     folding them into the dropdown, same reasoning as everything else in
     this breakpoint: less on the phone screen, not less capability (the
     real access boundary is still server-side regardless of what the nav
     shows, same as every other data-requires-* gate in this file). */
  #app-sidebar {
    position: sticky;
    top: 0;
    z-index: 50;
    width: 100%;
    height: auto;
    /* The desktop rule's overflow-y: auto (for a full-height, internally
       scrolling column) has to be cancelled here — left in place, it clips
       #app-sidebar-links' dropdown panel below, since that panel is
       absolutely positioned outside this now-56px-tall bar's own box and
       overflow anything-but-visible treats that as content to clip/scroll
       rather than paint. */
    overflow: visible;
    border-radius: 0;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    padding: 8px 12px;
    gap: 0;
  }
  #app-sidebar-brand {
    flex-direction: row;
    align-items: center;
    gap: 8px;
    border-bottom: none;
    padding: 0;
    margin-bottom: 0;
  }
  .app-sidebar-brand-logo { width: 108px; }
  #app-nav-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    background: transparent;
    border: none;
    color: var(--sidebar-text-active);
    flex-shrink: 0;
  }
  #app-nav-toggle:hover { background: var(--sidebar-bg-hover); border-radius: 8px; }
  .app-nav-desktop-only { display: none !important; }
  /* Restores the camera-scan buttons this breakpoint's default hides above
     (see .camera-scan-btn's own comment) — .btn's own default display. */
  .camera-scan-btn { display: inline-flex; }
  #app-sidebar-links {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--sidebar-bg);
    padding: 8px 0;
    box-shadow: var(--shadow-md);
    max-height: calc(100vh - 56px);
    overflow-y: auto;
  }
  #app-sidebar.nav-open #app-sidebar-links { display: flex; }
  #app-sidebar #user-widget { margin-top: 0; border-top: 1px solid var(--sidebar-border); }
  /* Bigger touch targets in the dropdown than the desktop sidebar needs —
     9px vertical padding is comfortable for a mouse pointer but under the
     ~44px minimum a finger wants; this alone gets a nav link there. */
  #app-sidebar-links .app-nav-link { padding: 13px 20px; margin: 0; border-radius: 0; }
  .filter-bar { flex-direction: column; align-items: stretch; }

  /* Dashboard module cards: full desktop padding (20px sides) plus the 16px
     page gutter either side reads as excessive on a phone — trims to match
     the tighter mobile rhythm the rest of this page's cards already use. */
  .dashboard-section { padding: 14px 14px 16px; }
  .dashboard-section-icon { width: 32px; height: 32px; }

  /* Compact row variant — assets.html's tiles (no icon, just a label/value
     pair) are three big cards on desktop but read better as a single slim
     list on a phone screen, so this swaps the grid-of-cards layout for one
     bordered panel with each tile as a thin label-left/value-right row and
     a divider between them, e.g. "Total assets   1,284" / "Under contract
     987" stacked line by line instead of three separate boxes. */
  .stat-tiles-row {
    display: flex;
    flex-direction: column;
    gap: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--panel);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
  }
  .stat-tiles-row .stat-tile {
    border: none;
    border-radius: 0;
    border-bottom: 1px solid var(--border);
    box-shadow: none;
    padding: 10px 16px;
  }
  .stat-tiles-row .stat-tile:last-child { border-bottom: none; }
  .stat-tiles-row .stat-tile > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    width: 100%;
  }
  .stat-tiles-row .stat-tile-label { margin-bottom: 0; }
  .stat-tiles-row .stat-tile-value { font-size: 15px; }
  .stat-tiles-row .stat-tile-filter.active { box-shadow: inset 3px 0 0 var(--accent); }

  /* Filters popover — see the default (desktop) rules above for why this
     only takes effect here. Below 720px the button shows, the wrappers
     stop being display:contents (a real box is needed for the popover to
     position against and to actually hide/show), and the panel becomes an
     absolutely-positioned, mobile-friendly stacked column instead of an
     inline row. align-items: stretch is set explicitly (rather than left
     as the flex default of "stretch" and trusted to just work) so every
     field is flush against the panel's left edge and full width — without
     it, each field only gets a box as wide as its own content and default
     cross-axis alignment leaves that box floating, which is what made
     Customer/Asset type/State/Contract status drift rightward earlier. */
  .filters-picker { position: relative; display: block; }
  .filters-btn { display: inline-flex; }
  #filters-panel.hidden { display: none !important; }
  #filters-panel {
    display: flex !important;
    position: absolute;
    z-index: 20;
    top: 100%;
    left: 0;
    margin-top: 6px;
    width: min(300px, calc(100vw - 32px));
    flex-direction: column;
    align-items: stretch;
    flex-wrap: nowrap;
    gap: 12px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: var(--shadow-md);
    padding: 14px;
  }
  .filters-panel .filter-field { min-width: 0; flex: none; }
  .filters-panel #clear-filters-btn { align-self: flex-start; }

  /* Every page's <header> (h1 + action buttons, e.g. Maintenance Tools'
     "+ New report", Assets' "Export"/"+ Add asset") — the buttons render at
     their normal desktop size here otherwise, oversized next to the
     smaller nav/type scale the rest of this breakpoint already uses.
     flex-wrap so a header with several buttons (Assets has two) can drop
     to a second line on a narrow phone instead of squeezing them or
     forcing the page to scroll sideways. */
  header { padding: 12px 16px; gap: 10px 12px; flex-wrap: wrap; }
  header h1 { font-size: 17px; }
  header .btn { padding: 6px 10px; font-size: 12.5px; }

  /* The drag-to-reorder Columns picker doesn't translate to a touch
     screen, and isn't needed there anyway — the Assets table shows a fixed,
     already-short column set on mobile regardless of what it's set to (see
     visibleAssetColumns() in assets.js), so there's nothing left for this
     button to control. */
  #columns-btn { display: none; }
}

/* ---------------------------------------------------------------------- */
/* Read-only info sub-panel (asset modal's Customer section)                */
/* ---------------------------------------------------------------------- */

.info-subpanel {
  background: var(--panel-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 12px 14px;
  font-size: 13px;
}
.info-subpanel-empty { color: var(--muted); }
.info-subpanel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px 20px; }
.info-subpanel-item-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin-bottom: 2px; }
.info-subpanel-item-value { color: var(--text); }

/* ---------------------------------------------------------------------- */
/* Bulk actions bar (Assets)                                                */
/* ---------------------------------------------------------------------- */

/* Lives inline in the assets table's panel-header now (see assets.html),
   not as a standalone block above the panel — so nothing here reserves a
   fixed width or full-bar background any more; ticking a row's checkbox
   just recolors this one header cluster in place instead of inserting a
   new page section that pushes the table (and the user's mouse) down. */
.bulk-actions-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  /* No vertical padding: this sits beside the Columns button in the header
     (see assets.html) and must match its height exactly (its tallest child,
     the buttons, sets it), or it grows the whole header row taller and
     shifts the table down the moment a row is ticked — the very "jump"
     behaviour this change is meant to remove. */
  padding: 0 10px;
  border-radius: var(--radius-md);
  background: var(--accent-soft);
}
.bulk-actions-bar .bulk-actions-count { font-size: 13px; font-weight: 600; color: var(--accent); }
.data-table .row-select-col { width: 34px; text-align: center; }
.data-table td.row-select-col { cursor: default; }
.data-table td.row-select-col input, .data-table th.row-select-col input { width: auto; margin: 0; }

/* ---------------------------------------------------------------------- */
/* Tag picker (chip UI — Assets add/edit + bulk edit modals)                */
/* ---------------------------------------------------------------------- */

.tag-picker {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel);
  padding: 6px 8px;
  position: relative;
}
.tag-picker.disabled { opacity: 0.5; pointer-events: none; }
/* Viewer role (no editAssets permission) — plain chip list, no border/
   background box and no input, since there's nothing to click/type into
   (see applyAssetFormEditability()/createTagPicker's setReadOnly in
   public/js/assets.js). Distinct from .disabled above, which still needs to
   look like a real (if greyed-out) control for the bulk-edit modal's own
   temporary mutual-exclusion toggling. */
.tag-picker.read-only { border: none; background: transparent; padding: 0; }
.tag-picker-empty { font-size: 13px; color: var(--muted); }
.tag-picker-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 4px; }
.tag-picker-chips:empty { margin-bottom: 0; }
.tag-picker-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  font-weight: 600;
  padding: 3px 6px 3px 9px;
  border-radius: 999px;
  background: var(--panel-alt);
  color: var(--text-secondary);
}
.tag-picker-chip button {
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 13px;
  line-height: 1;
  padding: 0 2px;
  border-radius: 999px;
}
.tag-picker-chip button:hover { color: var(--status-error); background: var(--status-error-soft); }
.tag-picker-input {
  border: none;
  padding: 4px 2px;
  width: 100%;
}
.tag-picker-input:focus-visible { outline: none; }
.tag-picker-suggestions {
  position: absolute;
  z-index: 10;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  max-height: 200px;
  overflow-y: auto;
}
.tag-picker-suggestion { padding: 7px 10px; font-size: 13px; cursor: pointer; }
.tag-picker-suggestion:hover, .tag-picker-suggestion.active { background: var(--row-hover); }
.tag-picker-suggestion.tag-picker-create { color: var(--accent); font-weight: 600; }

/* Assets table "Columns" picker (public/js/assets.js) — show/hide + a pair
   of up/down arrow buttons to reorder every column beyond the two fixed
   ones (row-select checkbox, Asset name). Same floating-panel visual
   language as .tag-picker-suggestions (absolute, bordered, shadow) since
   it's the same kind of "small popover anchored to a control" shape.
   Previously drag-and-drop (a single drag handle per row) — Adrian asked
   for explicit reorder buttons instead, matching a picker style he'd seen
   elsewhere: more discoverable, and doesn't rely on a pointer-drag gesture
   that's fiddly on trackpads/touch. */
.columns-picker { position: relative; }
.columns-panel {
  position: absolute;
  z-index: 20;
  top: 100%;
  right: 0;
  margin-top: 6px;
  width: 280px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-md);
  padding: 10px;
}
.columns-panel-hint { font-size: 12px; color: var(--muted); margin: 0 0 8px; }
.columns-list { list-style: none; margin: 0; padding: 0; max-height: 360px; overflow-y: auto; }
.columns-list-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 4px;
  border-radius: 6px;
}
.columns-list-item:hover { background: var(--row-hover); }
.columns-list-label {
  /* The global `label` rule (see above) sets flex-direction: column for
     ordinary above-input labels elsewhere in the app — without overriding it
     here too, the checkbox stacked above the column title instead of
     sitting beside it, so each option took two rows in the list. */
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 7px;
  font-size: 13px;
  cursor: pointer;
  flex: 1;
  user-select: none;
}
/* Drag grip at the row's left edge — the whole <li> is draggable (see
   assets.js), this just gives the pointer a "grab" affordance and something
   to visually hang the reorder gesture off. */
.columns-drag-handle {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
  color: var(--muted);
  cursor: grab;
}
.columns-drag-handle svg { width: 14px; height: 14px; }
/* Drop-position feedback while dragging — an inset border on whichever edge
   of the hovered row the dragged column would land against, plus fading the
   dragged row itself so its original slot doesn't look like a second target. */
.columns-list-item.dragging { opacity: 0.4; }
.columns-list-item.drag-over-before { box-shadow: inset 0 2px 0 0 var(--accent); }
.columns-list-item.drag-over-after { box-shadow: inset 0 -2px 0 0 var(--accent); }
.columns-panel-actions { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 8px; text-align: right; }

/* ---------------------------------------------------------------------- */
/* Import Tool                                                             */
/* ---------------------------------------------------------------------- */
.dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius-md);
  padding: 24px;
  text-align: center;
  color: var(--muted);
  font-size: 13.5px;
  background: var(--panel-alt);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); background: var(--status-fresh-soft); }
.dropzone strong { color: var(--text); }
.import-row-invalid { background: var(--status-error-soft); }
.import-row-invalid:hover { background: var(--status-error-soft); }
.import-reason { color: var(--status-error); font-size: 12.5px; }
.import-summary-banner {
  border: 1px solid var(--status-fresh); background: var(--status-fresh-soft); color: var(--status-fresh);
  border-radius: var(--radius-md); padding: 10px 14px; font-size: 13.5px; margin-bottom: 14px;
}
.import-reference-list { max-height: 260px; overflow-y: auto; font-size: 12.5px; }
.import-reference-list table { width: 100%; border-collapse: collapse; }
.import-reference-list th, .import-reference-list td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--border); }

/* ---------------------------------------------------------------------- */
/* Maintenance Tools                                                       */
/* ---------------------------------------------------------------------- */
/* Generic muted-text utility — .data-table already scopes its own .muted
   (see above); this covers every other use (report rows, view-report modal)
   without a table ancestor. */
.muted { color: var(--muted); }

.wizard-steps {
  display: flex;
  gap: 18px;
  margin: 14px 0 18px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
.wizard-step { font-size: 13px; color: var(--muted); font-weight: 500; }
.wizard-step.active { color: var(--text); font-weight: 700; }

.report-step { max-height: 60vh; overflow-y: auto; padding-right: 4px; }

.report-asset-results {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.report-asset-result-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  font-size: 14px;
  border-bottom: 1px solid var(--border);
}
.report-asset-result-row:last-child { border-bottom: none; }
/* The name/serial/site text is the only flexible part of the row — the
   optional bulk-select checkbox (or its alignment spacer on an already-added
   row) and the Add/Added button stay a fixed width either side of it. */
.report-asset-result-row > span:not(.report-result-select-spacer) { flex: 1; }
.report-result-select-spacer { display: inline-block; width: 14px; flex-shrink: 0; }
.report-asset-result-row .report-result-select { flex-shrink: 0; }

/* Bar above the search results offering to add several matches at once —
   only shown when there's more than one addable asset in the current
   results (see searchReportAssets in maintenance.js). */
.report-asset-results-bulk-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  /* See .report-asset-row's comment — panel-alt matches the page bg exactly. */
  background: #f7f8fb;
}
.report-asset-results-bulk-bar .checkbox-field { white-space: nowrap; }

.report-asset-selected-list { display: flex; flex-direction: column; gap: 16px; }
.report-asset-row {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* Was var(--panel-alt), which is the exact same colour as the page
     background behind this (white) modal — a nested "card" filled with
     that colour reads as a hole punched through to the page, not a card
     sitting on it. A plain white card with its existing border reads as a
     card instead. */
  background: var(--panel);
}
.report-asset-row-header { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 15.5px; }
.report-asset-row-header button { margin-left: auto; }
.report-asset-row input[type="text"], .report-asset-row textarea { width: 100%; font-size: 14.5px; }
/* The read-only view's secondary lines (identifier scanned, installation
   height, work notes — see openViewReport in maintenance.js) are plain
   divs with no class of their own, so they'd otherwise inherit the page's
   14px body size, unusually small next to the 15.5px header right above
   them in the same row. These are DESCENDANT selectors (no ">"), not just
   direct-child ones: openViewReport builds this markup as one innerHTML
   string wrapped in an el('html', {...}) node (see the comment there), so
   the actual header/muted/work-note divs sit one level deeper than a
   direct child of .report-asset-row — a ">" selector silently never
   matched them, which is why this fix didn't visibly apply the first time. */
.report-asset-row div.muted, .report-asset-row div:not(.report-asset-row-header) { font-size: 14.5px; line-height: 1.5; }

.report-preview-block { font-size: 15px; line-height: 1.6; }
.report-preview-divider {
  margin: 20px 0 16px;
  padding-top: 14px;
  border-top: 2px dashed var(--border-strong);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}
.report-preview-customer {
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 20px;
  margin-top: 4px;
  /* Same fix as .report-asset-row above — a distinct light tint instead of
     the page's own background colour, so this reads as "the customer-facing
     card" rather than a gap in the modal. */
  background: #f7f8fb;
}

.report-bulk-notes-bar {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 14px 0;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
}
.report-bulk-notes-bar textarea { width: 100%; font-size: 14px; }
.report-bulk-notes-bar-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.report-bulk-notes-bar-actions button { flex-shrink: 0; white-space: nowrap; }
.report-asset-row-select { margin-right: 4px; }

.report-note-presets { display: flex; flex-wrap: wrap; gap: 6px; }
.report-note-preset-btn {
  font-size: 12.5px;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
}
.report-note-preset-btn:hover { border-color: var(--border-strong); color: var(--text); }

.report-asset-row-height { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.report-asset-row-height label { display: flex; align-items: center; gap: 6px; font-size: 14px; white-space: nowrap; }
/* No separate "Installation height (mm)" title any more (see
   maintenance.js) — the field's placeholder text is the only description
   of what it's for, so it needs to be wide enough not to clip that text
   (110px was only ever wide enough for a typed value, not the full
   placeholder). Only some device types have an installation height at all
   (e.g. people-counters, not air-quality sensors) — once device-type
   parameters exist as their own lookup, this field's visibility should key
   off that rather than showing unconditionally for every asset type. */
.report-asset-row-height input[type="number"] { width: 210px; font-size: 14px; }

/* Viewer-only form fields (originally just openEditModal in
   public/js/assets.js, now also public/js/customers.js's three modals) —
   every field is genuinely disabled (not just visually dimmed), but a plain
   grey "disabled" look reads as broken rather than intentionally read-only.
   This strips the input chrome so a disabled field reads as plain text
   instead — applied only when the signed-in user lacks editor access to
   that module; an editor/full/admin never sees this class on the form at
   all. Class-scoped (not id-scoped to #asset-form) so any form on the site
   can opt in the same way. */
.read-only-form input:disabled,
.read-only-form select:disabled,
.read-only-form textarea:disabled {
  background: none;
  border: none;
  padding: 4px 0;
  color: var(--text);
  opacity: 1;
  -webkit-text-fill-color: var(--text);
  appearance: none;
}
.read-only-form .tag-picker,
.read-only-form input[type="checkbox"]:disabled {
  pointer-events: none;
}
