:root {
  --bg: #e2eaf5;
  --panel: #ffffff;
  --panel-border: #c3d3e8;
  --text: #16202e;
  --muted: #5b6b85;
  --accent: #0f7abe;
  --accent-hover: #0c6299;
  --accent-orange: #e5491a;
  --sidebar-bg: #0f7abe;
  --sidebar-text: #cfe9fb;
  --sidebar-hover-bg: rgba(255,255,255,0.1);
  --sidebar-active-bg: rgba(255,255,255,0.18);
  --good: #16a34a;
  --bad: #dc2626;
  --warn: #d97706;
  --good-bg: #dcfce7;
  --bad-bg: #fee2e2;
  --warn-bg: #fef3c7;
  --accent-bg: #e0f2fe;
  --console-bg: #101826;
  --console-border: #1e293b;
  --console-text: #cbd5e1;
  --console-muted: #7c8aa6;
  --console-good: #4ade80;
  --console-bad: #f87171;
  --console-info: #38bdf8;
}

* { box-sizing: border-box; }
/* Several element classes here (.btn among them) set their own `display`
   at the same specificity as the browser's built-in `[hidden]` rule, and
   being an author rule beats it on the tie - toggling `el.hidden = true`
   on a `.btn` silently did nothing before this. Found via the floorplan
   zone editor's delete button staying visible despite being hidden. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  /* Column flex + min-height so .app-shell (below) always fills whatever
     vertical space is left under the topbar/mock-mode banner, regardless
     of their height - .sidebar's own `align-self: stretch` then makes
     the blue sidebar reach the bottom of the viewport even on a page
     whose main content is shorter than the screen (e.g. a floorplan with
     a small image), not just on pages with enough stacked panels to
     naturally exceed viewport height. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  background: var(--panel);
  border-bottom: 2px solid var(--accent);
}

.brand { font-size: 20px; font-weight: 700; }
.topbar .brand { display: flex; align-items: center; }
.brand span { font-weight: 400; color: var(--muted); margin-left: 8px; font-size: 15px; }
.brand-logo { height: 32px; max-width: 180px; object-fit: contain; }

.topbar a, .auth-footer-link {
  color: var(--muted);
  font-size: 13px;
  text-decoration: none;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  padding: 6px 14px;
}
.topbar a:hover, .auth-footer-link:hover { border-color: var(--accent); color: var(--accent); }

.lang-toggle {
  display: inline-flex;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  overflow: hidden;
}
.lang-toggle a {
  padding: 6px 12px;
  font-size: 12px;
  font-weight: 600;
  text-decoration: none;
  color: var(--muted);
  /* Overrides .topbar a's own border/radius/padding, which would
     otherwise also apply here (same specificity, .topbar a just comes
     first) and give each link its own pill nested inside this one -
     only visible on pages where .lang-toggle sits inside .topbar, not
     the standalone auth pages, which is why login looked fine already. */
  border: none;
  border-radius: 0;
}
.lang-toggle a.active { background: var(--accent); color: #fff; }
.lang-toggle a:hover:not(.active) { color: var(--accent); }
.auth-page .lang-toggle { position: absolute; top: 20px; right: 20px; }

.btn {
  display: inline-block;
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 8px;
  padding: 9px 16px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
}
.btn:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg); border: 1px solid var(--panel-border); color: var(--text); }
.btn-danger { background: transparent; border: 1px solid var(--bad); color: var(--bad); }
.btn-small { padding: 4px 10px; font-size: 12px; }

.status {
  font-size: 13px;
  color: var(--muted);
  padding: 4px 10px;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
}
.status.ok { color: var(--good); border-color: var(--good); background: var(--good-bg); }
.status.error { color: var(--bad); border-color: var(--bad); background: var(--bad-bg); }
.status.warning { color: var(--warn); border-color: var(--warn); background: var(--warn-bg); }

.banner {
  background: var(--warn-bg);
  color: #92400e;
  padding: 8px 24px;
  font-size: 13px;
  border-bottom: 1px solid #fde68a;
}
.banner code { background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 4px; }

.auth-page {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 14px;
  padding: 32px;
  box-shadow: 0 4px 24px rgba(22,32,46,0.06);
}
.auth-card .brand { text-align: center; margin-bottom: 24px; }
.auth-card .brand span { display: block; margin: 12px 0 0 0; }
.brand-logo-plate {
  /* Used to be a boxed "plate" (background/border/padding) to make the
     logo stand out against the app's old dark theme - the current light
     theme's card background already does that job, so this is now just
     a full-width wrapper, no visible box. Block + width:100% (not the
     old inline-flex, which sizes to its own content) so the img below
     has an actual width to scale against - it needs to span the same
     width as the input fields further down the card. */
  display: block;
  width: 100%;
}
.brand-logo-plate img {
  display: block;
  width: 71%;
  height: auto;
  margin: 0 auto;
  position: relative;
  left: 6px;
}
.auth-field { margin-bottom: 16px; }
.auth-field label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.auth-field input[type="color"] {
  /* type="color"'s swatch fills its whole content box - the padding
     other inputs get would squash it down to a thin sliver instead of
     showing the actual selected color. */
  width: 60px;
  height: 40px;
  padding: 2px;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  cursor: pointer;
}
.auth-field input:not([type="color"]), .auth-field select {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 10px 12px;
  color: var(--text);
  font-size: 14px;
}
/* Linked-sensors picker (admin_floorplan_edit.html) - plain clickable
   rows instead of a native <select multiple>, which only toggles a row
   with ctrl/cmd held (a real usability complaint: the user had to hold
   ctrl to pick more than one sensor) and truncates long names with no
   wrap. Selection state lives in JS (floorplan_admin.js's
   `selectedSensors` Set), reflected here only via the *-selected class. */
.sensor-picker {
  width: 100%;
  max-height: 240px;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 4px;
}
.sensor-picker-item {
  padding: 8px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text);
  white-space: normal;
  overflow-wrap: anywhere;
}
.sensor-picker-item:hover { background: rgba(15, 122, 190, 0.1); }
.sensor-picker-item-selected, .sensor-picker-item-selected:hover { background: var(--accent); color: #fff; }
.auth-submit {
  width: 100%;
  margin-top: 8px;
  padding: 11px;
  border: none;
  border-radius: 8px;
  background: var(--accent);
  color: white;
  font-weight: 600;
  font-size: 14px;
  cursor: pointer;
}
.auth-submit:hover { background: var(--accent-hover); }
.auth-error {
  background: var(--bad-bg);
  color: #b91c1c;
  border: 1px solid #fecaca;
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 13px;
  margin-bottom: 16px;
}
.auth-success {
  background: var(--good-bg);
  color: #15803d;
  border: 1px solid #bbf7d0;
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 13px;
  margin-bottom: 16px;
}
.auth-footer { text-align: center; margin-top: 18px; font-size: 12px; color: var(--muted); }
.auth-footer a { color: var(--accent); text-decoration: none; }

.app-shell { display: flex; align-items: flex-start; flex: 1; }
.sidebar {
  width: 220px;
  flex-shrink: 0;
  padding: 24px 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: sticky;
  top: 0;
  background: var(--sidebar-bg);
  align-self: stretch;
}
.sidebar-link {
  display: block;
  padding: 10px 14px;
  border-radius: 8px;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  border: 1px solid transparent;
}
.sidebar-link:hover { background: var(--sidebar-hover-bg); color: #fff; }
.sidebar-link.active { background: var(--sidebar-active-bg); color: #fff; font-weight: 600; }

main { flex: 1; min-width: 0; padding: 24px; max-width: 1100px; margin: 0 auto; }
main.main-full-width { max-width: none; }

@media (max-width: 720px) {
  .app-shell { flex-direction: column; }
  .sidebar { width: 100%; flex-direction: row; position: static; padding: 12px 24px; flex-wrap: wrap; }
  /* margin:0 auto (for desktop centering below max-width:1100px) has an
     auto cross-axis margin, which suppresses flexbox's default stretch
     behavior once .app-shell becomes a column - without this override,
     main shrink-wraps to its widest child instead of filling the screen
     width, which is what was forcing the whole page wider than the
     viewport on mobile. */
  main { margin: 0; padding: 16px; max-width: 100%; }
  /* main.main-full-width's unconditional `max-width: none` (higher
     specificity than the plain `main` rule above) wins at any width,
     including mobile, unless matched here - without this, the
     Maintenance page (the only page using that class) overflows the
     viewport horizontally on narrow screens instead of respecting the
     100% cap every other page gets. */
  main.main-full-width { max-width: 100%; }
  .topbar { flex-wrap: wrap; gap: 10px; padding: 12px 16px; }
  .topbar > div { flex-wrap: wrap; row-gap: 8px; }
  .status, .topbar a { white-space: nowrap; }
  .panel-header { flex-wrap: wrap; gap: 10px; }
  .panel-header > div { flex-wrap: wrap; }
  .panel-header input { width: 100%; }
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.card {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: 18px;
}
.card-label { color: var(--muted); font-size: 13px; margin-bottom: 6px; }
.card-value { font-size: 28px; font-weight: 700; }
.card-unit { color: var(--muted); font-size: 12px; margin-top: 4px; }

/* Same grid as .cards, but without its margin-bottom - for a card row
   that's already inside a .panel (which has its own padding), e.g. the
   Expected Energy Usage panel. */
.cards-in-panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
}

.mega-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  margin-bottom: 24px;
}

.mega-btn {
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: 22px 18px;
  font-size: 17px;
  font-weight: 700;
  cursor: pointer;
  color: var(--text);
  background: var(--panel);
  transition: transform 0.1s, opacity 0.15s;
}
.mega-btn:active { transform: scale(0.98); }
.mega-btn:disabled { opacity: 0.6; cursor: wait; }

.mega-on { background: linear-gradient(135deg, #2fa968, #4ec48a); border-color: transparent; color: #fff; }
.mega-off { background: linear-gradient(135deg, #e2635c, #ee8177); border-color: transparent; color: #fff; }
.mega-auto { background: linear-gradient(135deg, #1f8fc4, #43abdb); border-color: transparent; color: #fff; }

.panel {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  padding: 20px;
  margin-bottom: 24px;
}
/* Only table panels need horizontal scroll (wide tables on narrow
   screens) - chart panels never do (chart-wrap/canvas are width:100%).
   Scoping overflow-x to just those matters because setting overflow-x
   to a non-visible value forces the UA to also compute overflow-y as
   auto (CSS Overflow spec), which was clipping chart panels with a
   spurious vertical scrollbar whenever the chart's rendered height came
   out even 1px taller than chart-wrap's fixed height. */
.panel:has(table) {
  overflow-x: auto;
}
.panel h2 { margin: 0 0 16px 0; font-size: 16px; color: var(--text); }
.chart-wrap {
  /* Chart.js needs an explicitly-sized parent when maintainAspectRatio
     is false (set in app.js) - without a fixed height here, the canvas
     would collapse to near-zero height instead of filling the panel. A
     flat height (not width-relative) keeps bars and rotated x-axis
     labels readable at any screen width, mobile included. */
  position: relative;
  width: 100%;
  height: 260px;
}
.panel canvas {
  max-width: 100%;
}
.group-energy-block { padding-bottom: 16px; margin-bottom: 16px; border-bottom: 1px solid var(--panel-border); }
.group-energy-block:last-child { padding-bottom: 0; margin-bottom: 0; border-bottom: none; }
.group-energy-block h3 {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 8px;
}
.group-energy-header {
  margin-bottom: 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.group-energy-block.collapsed .group-energy-header { margin-bottom: 0; }
.group-energy-summary { font-size: 12px; color: var(--muted); white-space: nowrap; }
.group-energy-block.collapsed .collapsible-body { display: none; }
.group-energy-block.collapsed .collapsible-chevron { transform: rotate(-90deg); }
.panel-header { display: flex; align-items: center; justify-content: space-between; }
.panel-header input {
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  padding: 6px 10px;
  color: var(--text);
  font-size: 13px;
}
.btn-export {
  color: var(--muted);
  font-size: 13px;
  text-decoration: none;
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  padding: 6px 14px;
  white-space: nowrap;
}
.btn-export:hover { border-color: var(--accent); color: var(--accent); }

.btn-order {
  display: inline-block;
  color: #fff;
  background: var(--accent-orange);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  border-radius: 999px;
  padding: 6px 14px;
  white-space: nowrap;
}
.btn-order:hover { opacity: 0.85; }

.card-action {
  display: flex;
  flex-direction: column;
}

.card-cta {
  display: block;
  text-align: center;
  margin-top: auto;
  padding: 8px 14px;
  white-space: normal;
}

.hourly-nav {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 13px;
  color: var(--muted);
}
.hourly-nav #hourly-date-label { min-width: 80px; text-align: center; }
.btn-nav {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 999px;
  color: var(--text);
  width: 28px;
  height: 28px;
  line-height: 1;
  font-size: 14px;
  cursor: pointer;
}
.btn-nav:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.btn-nav:disabled { opacity: 0.4; cursor: not-allowed; }

.collapsible-header { cursor: pointer; user-select: none; }
.collapsible-header h2 { margin: 0; display: flex; align-items: center; gap: 8px; }
.collapsible-chevron { display: inline-block; font-size: 11px; color: var(--muted); transition: transform 0.15s; }
.collapsible-body { margin-top: 16px; }
.panel.collapsed .collapsible-body { display: none; }
.panel.collapsed .collapsible-chevron { transform: rotate(-90deg); }

.wsmon-controls {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 13px;
  color: var(--muted);
}
.wsmon-controls label { display: flex; align-items: center; gap: 5px; cursor: pointer; }
.wsmon-controls button {
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  color: var(--text);
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}
.wsmon-controls button:hover { border-color: var(--accent); color: var(--accent); }

.wsmon-log {
  margin-top: 14px;
  height: 420px;
  overflow-y: auto;
  background: var(--console-bg);
  border: 1px solid var(--console-border);
  border-radius: 8px;
  padding: 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  color: var(--console-text);
}
.wsmon-entry { padding: 3px 4px; border-radius: 4px; white-space: pre-wrap; word-break: break-all; }
.wsmon-entry + .wsmon-entry { border-top: 1px solid rgba(255,255,255,0.06); }
.wsmon-ts { color: var(--console-muted); margin-right: 8px; }
.wsmon-entry.details { background: rgba(74,222,128,0.12); outline: 1px solid var(--console-good); }
.wsmon-entry.error { background: rgba(248,113,113,0.14); color: var(--console-bad); }
.wsmon-entry.info { color: var(--console-info); }

table.lum-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.lum-table th {
  text-align: left;
  color: var(--muted);
  font-weight: 500;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 8px 10px;
  border-bottom: 1px solid var(--panel-border);
}
.lum-table td {
  padding: 10px;
  border-bottom: 1px solid var(--panel-border);
  /* A device name with no spaces (a raw model number, e.g.
     "HNB358LBDLPIR(BC/Dim)_Black") can't line-wrap normally, which was
     forcing the whole Naam column - and the table's total width along
     with it - open to fit that one unbroken string on a single line. */
  overflow-wrap: anywhere;
  /* Without a floor, a now-wrappable column is free to shrink almost to
     nothing before wrapping kicks in - browsers only treat unbreakable
     content as a hard minimum, and overflow-wrap just removed that
     signal for this column. Result was viewport-sensitive: enough space
     and it wraps sensibly, too little and text gets chopped into
     unreadable 3-4 character fragments. A min-width makes the table
     properly wider than its panel at narrow viewports instead, which
     correctly triggers .panel's own horizontal scroll (see below)
     rather than squeezing text into illegibility. */
  min-width: 90px;
}
.lum-table th:first-child,
.lum-table td:first-child {
  min-width: 180px;
}
.lum-table tr:hover td { background: var(--accent-bg); }

.badge {
  display: inline-block;
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12px;
  line-height: 1.4;
  white-space: nowrap;
}
.badge.online { background: var(--good-bg); color: #15803d; }
.badge.offline { background: var(--bad-bg); color: #b91c1c; }
.badge.lingering { background: var(--warn-bg); color: #92400e; }
.badge.neutral { background: #eef1f6; color: var(--muted); }
.badge.measured {
  background: var(--accent-bg);
  color: var(--accent);
  padding: 1px 5px;
  font-size: 10px;
  font-weight: 700;
  margin-left: 4px;
  cursor: help;
}
.badge.estimated {
  background: #eef1f6;
  color: var(--muted);
  padding: 1px 5px;
  font-size: 10px;
  font-weight: 700;
  margin-left: 4px;
  cursor: help;
}

.scene-activate {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.scene-activate:hover { background: var(--accent-hover); }
.scene-activate:disabled { opacity: 0.6; cursor: wait; }

.watts-input {
  width: 64px;
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  color: var(--text);
  padding: 4px 6px;
  font-size: 13px;
}

.switch {
  position: relative;
  display: inline-block;
  width: 42px;
  height: 24px;
}
.switch input { opacity: 0; width: 0; height: 0; }
.slider {
  position: absolute; cursor: pointer; inset: 0;
  background-color: #cbd5e1;
  transition: .15s;
  border-radius: 999px;
}
.slider:before {
  position: absolute;
  content: "";
  height: 18px; width: 18px;
  left: 3px; bottom: 3px;
  background-color: white;
  transition: .15s;
  border-radius: 50%;
}
input:checked + .slider { background-color: var(--accent); }
input:checked + .slider:before { transform: translateX(18px); }
input:disabled + .slider { opacity: 0.5; cursor: wait; }

/* Floorplan zone overlays - shared by the admin editor
   (floorplan_admin.js) and the client-facing viewer (floorplan.js).
   Positioned in percentages (left/top/width/height set inline per zone,
   as fractions of #floorplan-canvas's own size) rather than pixels, so
   geometry stays correct at any image scale. */
/* display:block + width:100% on both, not inline-block/max-width - an
   inline-block canvas sizes to its image's own intrinsic width first,
   so a percentage width on the image inside it is a no-op (resolves
   against that same shrink-to-fit size) and the floorplan never grows
   past its uploaded image's natural pixel size even on a much wider
   panel. Forcing the canvas to claim the panel's full width first lets
   the image's own width:100% (height:auto keeps it proportional)
   actually stretch to fill it. */
#floorplan-canvas { position: relative; display: block; width: 100%; cursor: crosshair; touch-action: none; }
#floorplan-image { display: block; width: 100%; height: auto; pointer-events: none; user-select: none; }
.zone-box {
  position: absolute;
  box-sizing: border-box;
  border: 2px solid var(--accent);
  background: rgba(15, 122, 190, 0.15);
  border-radius: 4px;
  cursor: move;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  min-width: 12px;
  min-height: 12px;
}
.zone-box-drawing { border-style: dashed; background: rgba(15, 122, 190, 0.08); cursor: crosshair; }
.zone-box-active { border-color: var(--accent-orange); background: rgba(229, 73, 26, 0.15); }
.zone-box-label {
  background: var(--panel);
  color: var(--text);
  font-size: 11px;
  padding: 1px 6px;
  border-radius: 4px;
  margin-top: -10px;
  white-space: nowrap;
  pointer-events: none;
  box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.zone-box-handle {
  position: absolute;
  right: -6px;
  bottom: -6px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent-orange);
  border: 2px solid var(--panel);
  cursor: nwse-resize;
}

/* Client-facing viewer only (floorplan.js) - the live-state summary and
   On/Off buttons shown inside a zone box. */
.zone-box-view { cursor: default; align-items: center; justify-content: center; flex-direction: column; gap: 4px; }
.zone-box-summary { font-size: 11px; color: var(--text); background: var(--panel); padding: 2px 6px; border-radius: 4px; }
/* Two small separate buttons rather than one on/off toggle (by request) -
   each is just an action, not a reflection of current state, so neither
   needs an active/pressed look; the zone box's own green/orange tint
   already shows occupancy, and light_power_pct already shows brightness. */
.zone-onoff-row { display: flex; gap: 4px; }
.zone-onoff-btn {
  border: none;
  border-radius: 4px;
  padding: 3px 10px;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  color: #fff;
}
.zone-onoff-btn:disabled { opacity: 0.6; cursor: wait; }
.zone-onoff-on { background: var(--good); }
.zone-onoff-off { background: var(--bad); }
/* Presence-based box color, in addition to the Present/Absent text line
   - added by request so occupancy reads at a glance without having to
   read the text. Only applied when a zone actually has a known presence
   state (zonePresenceStatus() in floorplan.js) - a zone with no linked
   sensor keeps the plain default blue, not a claim about occupancy it
   can't actually make. */
.zone-box-view.zone-present { border-color: var(--good); background: rgba(22, 163, 74, 0.15); }
.zone-box-view.zone-absent { border-color: var(--warn); background: rgba(217, 119, 6, 0.15); }
/* Zone name label, reusing the admin editor's .zone-box-label look - but
   .zone-box-view's column flex (align-items/justify-content: center)
   would otherwise fold a plain flex child into the centered toggle/
   summary stack instead of sitting on the top border the way the admin
   editor's row-flex + margin-top:-10px does. Taking it out of flow with
   its own absolute position replicates that, but centered exactly ON
   the border (top:0 + translateY(-50%), not a fixed -10px offset) so it
   straddles the border the same way regardless of the label's own
   rendered height, matching the admin editor's look precisely instead
   of floating fully above it. */
.zone-box-view .zone-box-label { position: absolute; top: 0; left: 50%; margin-top: 0; transform: translate(-50%, -50%); }

/* Problem-status dot - reuses .zone-box-handle's corner-dot look (the
   admin editor's resize handle) but repurposed as a clickable health
   indicator: green when every luminaire in the zone's group(s) is fine,
   red when any one of them has a problem (same fault definition as the
   Maintenance page's "needs attention" count - see
   routers/luminaires.py's unit_has_problem, shared by both). Clicking it
   opens #zone-problem-popup. */
.zone-problem-dot {
  position: absolute;
  right: -8px;
  bottom: -8px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 2px solid var(--panel);
  cursor: pointer;
}
.zone-problem-dot-ok { background: var(--good); }
.zone-problem-dot-bad { background: var(--bad); }

.zone-problem-popup {
  position: fixed;
  z-index: 50;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
  padding: 16px;
  width: 260px;
  max-height: 320px;
  overflow-y: auto;
}
.zone-problem-popup-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.zone-problem-popup h3 { margin: 0; font-size: 14px; }
.zone-problem-popup-close { background: none; border: none; cursor: pointer; font-size: 18px; line-height: 1; color: var(--muted); padding: 0; }
.zone-problem-popup ul { list-style: none; margin: 12px 0; padding: 0; }
.zone-problem-popup li { padding: 4px 0; font-size: 13px; color: var(--bad); }
.zone-problem-popup-ok-text { font-size: 13px; color: var(--good); margin: 12px 0; }
.zone-problem-popup-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 8px; border-top: 1px solid var(--panel-border); }
.zone-problem-popup-toggle-row span { font-size: 12px; color: var(--muted); }
/* Off/On text flanking the switch, whichever is currently active bolded
   - the switch alone starts wherever the OK luminaires already are
   (checked = on), and clicking an already-checked switch turns it off,
   which read as "inverted" to the user before they realized it started
   checked. These labels make the switch's current meaning unambiguous
   without changing the underlying (already correct) control logic. */
.zone-toggle-labeled { display: flex; align-items: center; gap: 6px; }
.zone-toggle-text-label { font-size: 11px; color: var(--muted); }
.zone-toggle-text-label-active { color: var(--text); font-weight: 600; }
.zone-toggle-explainer { margin: 6px 0 0 0; font-size: 11px; color: var(--muted); line-height: 1.4; }

/* Admin editor (admin_floorplan_edit.html) - floorplan on the left, a
   settings sidebar to the right that's always in view (not just while
   editing) rather than stacked below: it shows the full zone list while
   idle and swaps to the selected/being-drawn zone's form, so the admin
   never loses track of what's there or which one they're editing. */
/* main's global max-width:1100px (line ~260) is fine for the single
   floorplan panel, but a *fixed* budget shared between both columns
   means the 320px settings panel eats directly into the floorplan's own
   width the moment it appears - the image visibly shrinks mid-drag,
   right when a stable reference matters most (reported by the user).
   Removing the cap here and giving the canvas panel its own bounded
   width (flex-shrink allowed, but not flex-grow into the sidebar's
   space) means the settings panel is additional width, not stolen
   width - the row simply grows wider on a large viewport instead. */
body.floorplan-editor-page main { max-width: none; }
.floorplan-editor-row { display: flex; align-items: flex-start; gap: 16px; }
.floorplan-editor-canvas-panel { flex: 0 1 1200px; min-width: 0; }
/* Wider than a typical form sidebar - a plain 320px cut off longer
   sensor names even with wrapping enabled (see .sensor-picker-item). */
.floorplan-editor-form-panel { flex: 0 0 420px; }
@media (max-width: 900px) {
  .floorplan-editor-row { flex-direction: column; }
  .floorplan-editor-form-panel { flex-basis: auto; width: 100%; }
}
.zone-list { list-style: none; margin: 0; padding: 0; }
.zone-list-item {
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  margin-bottom: 8px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text);
}
.zone-list-item:hover { border-color: var(--accent); color: var(--accent); }
.zone-list-item-active { border-color: var(--accent-orange); color: var(--accent-orange); }

/* Occupancy heat map - admin marker editor (floorplan_sensors_admin.js)
   and client viewer (occupancy_heatmap.js) share this same #floorplan-
   canvas/#floorplan-image wrapper as the light-control feature (see
   above), but with point markers instead of rectangular zone boxes. */
.sensor-marker {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  cursor: pointer;
  z-index: 5;
}
/* Color means online/offline (green/red) on both the admin marker editor
   and the client heat map - see .sensor-marker-online/-offline and
   .occupancy-marker-hit-online/-offline below. The "currently being
   edited" state (admin only) is shown as a ring instead of a color
   override, since color is reserved for online/offline here. */
.sensor-marker-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--muted); /* fallback for a just-placed marker with no sensor chosen yet */
  border: 2px solid var(--panel);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}
.sensor-marker-online .sensor-marker-dot { background: var(--good); }
.sensor-marker-offline .sensor-marker-dot { background: var(--bad); }
.sensor-marker-active .sensor-marker-dot { box-shadow: 0 0 0 3px var(--accent-orange); }
.sensor-marker-label {
  font-size: 11px;
  background: var(--panel);
  padding: 1px 6px;
  border-radius: 4px;
  white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}

/* The gradient itself (heatmap.js's own canvas gets appended inside this
   div) - pointer-events:none so clicks pass through to the small
   .occupancy-marker-hit circles above it, letting the gradient sit
   underneath without blocking marker interaction. */
#heatmap-container { position: absolute; inset: 0; pointer-events: none; }

.occupancy-marker-hit {
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  cursor: pointer;
  z-index: 6;
  border: 2px solid var(--panel);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.occupancy-marker-hit-online { background: var(--good); }
.occupancy-marker-hit-offline { background: var(--bad); }

.occupancy-range-picker { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.occupancy-range-btn-active { background: var(--accent); color: #fff; border-color: transparent; }
.occupancy-custom-range { display: flex; align-items: center; gap: 6px; }
.occupancy-custom-range input[type="date"] {
  background: var(--bg);
  border: 1px solid var(--panel-border);
  border-radius: 6px;
  padding: 4px 8px;
  color: var(--text);
  font-size: 13px;
}

.occupancy-day-nav { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.occupancy-day-label { font-size: 14px; font-weight: 600; min-width: 110px; text-align: center; }

/* Below the floorplan image, not above it - the slider scrubs through
   what's already on screen, so the image should stay put while the
   label above the handle is what changes as you drag. */
.occupancy-hour-slider-row { margin-top: 14px; }
.occupancy-hour-label {
  display: block;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
}
.occupancy-hour-slider-row input[type="range"] { width: 100%; }
.occupancy-hour-slider-ends {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--muted);
  margin-top: 2px;
}

/* Placed at the very end of the file, after .cards and
   .group-energy-summary's own base rules further up - a media-scoped
   override with equal specificity to its unconditional base rule only
   wins if it comes LATER in source order, regardless of which one is
   "inside a media query". Putting mobile overrides earlier in the file
   than the rule they're meant to override is a real trap - they get
   silently beaten by the later unconditional rule at every viewport
   width, media query or not. */
@media (max-width: 720px) {
  .cards { grid-template-columns: repeat(2, 1fr); }
  .group-energy-summary { white-space: normal; }
}
