.sw-widget {
  height: 100%;
}

/* Small "connected to the shared Filter widget" indicator - shown next
   to the settings gear (once moved into the header slot, see
   moveIntoHeaderSlot()) for any multi-instance widget instance
   currently set to FOLLOW_FILTER_DEVICE rather than pinned to its own
   device (Chart/Log/Status/Single Value/Gauge/Switches - see each
   widget's own updateFollowIndicator() in sensor-widgets.js). Shared/
   ancestor-independent, same "one class reused verbatim by every widget
   type" idiom as user-dashboard's own .dashboard-size (see that class's
   own comment in dashboard.css) - the icon/behavior is identical
   everywhere this appears, so it isn't worth a per-widget BEM prefix.
   Purely informational, not a button - no cursor/hover state of its
   own, but the same font-size/opacity/color/padding as the icon buttons
   next to it (.dashboard-widget__controls button in dashboard.css) so
   it reads as part of that same row rather than a different kind of
   thing. Hidden by default in markup; each widget's own JS unhides it
   whenever `device === FOLLOW_FILTER_DEVICE`. */
.sw-widget__follow-indicator {
  display: flex;
  align-items: center;
  font-size: 1.1rem;
  line-height: 1;
  opacity: .7;
  padding: .2rem .3rem;
  color: var(--dash-text, currentColor);
}

/* A plain flex row, not Bootstrap's row/col-md-4 grid - deliberately, see
   filter.html.twig's own docblock for why a *viewport* breakpoint is the
   wrong signal for a dashboard widget's own (independently resizable)
   width. flex-wrap is what actually delivers "one row when there's room,
   stacked once there isn't" - continuously, as the widget is resized,
   not at a fixed pixel breakpoint. */
.sw-filter__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* flex-basis (not just min-width) - gives every control an equal-ish
   starting share of the row's width when there's room for all three, the
   same visual balance the old three-equal-columns grid had, while
   min-width is what actually forces a wrap once the row can no longer
   fit them all at a still-readable width instead of squeezing them
   arbitrarily thin first. flex-grow:1 lets whichever ones DO fit expand
   to fill any leftover width rather than leaving a gap. */
.sw-filter__controls > .input-group {
  flex: 1 1 12rem;
  min-width: 12rem;
}

/* Its own full-width line rather than competing for a ~12rem share like
   the other controls - two datetime-local inputs (each needs real room
   to show its date, not just squeeze down to "00:00" with the date
   segment clipped) plus an Apply button don't fit that budget. */
.sw-filter__controls > .sw-filter__custom-range {
  flex: 1 1 100%;
}

/* Device combobox (filter.html.twig) - a plain text input standing in
   for the old native <select>, with a custom-rendered dropdown list
   underneath instead of the browser's own <option> rendering (which -
   confirmed elsewhere in this codebase, see SensorWidgets.
   markAdminOption()'s own comment - can't show an icon/color inside an
   <option> at all; this needs a colored online/offline dot per row). */
.sw-filter__device-combo {
  position: relative;
}
.sw-filter__device-toggle {
  cursor: pointer;
}
.sw-filter__device-list {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 1050;
  margin-top: 2px;
  max-height: 16rem;
  overflow-y: auto;
  background: var(--bs-body-bg);
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
  padding: 0.25rem 0;
}
.sw-filter__device-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
}
.sw-filter__device-item-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sw-filter__device-dot {
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--bs-secondary-color);
}
.sw-filter__device-dot--online {
  background: #22c55e;
}
/* Same reasoning as .sw-status__row .form-check-input:checked elsewhere
   in this file - a plain bg tint here rather than reaching for a
   hardcoded blue, so it still reads sensibly against a customized
   --bs-primary. */
.sw-filter__device-item--active,
.sw-filter__device-item--selected {
  background-color: var(--bs-tertiary-bg, rgba(13, 110, 253, 0.08));
}
.sw-filter__device-empty {
  padding: 0.375rem 1rem;
  font-size: 0.85rem;
}

/* Time-range picker (filter.html.twig) - a readonly text input + toggle
   icon, styled identically to .sw-filter__device-combo above (same
   pattern, different popover - Litepicker manages its own instead of a
   hand-rolled dropdown div). cursor:pointer since the input itself can't
   be typed into (open-the-picker is its only real interaction). */
.sw-filter__range-input {
  cursor: pointer;
}
.sw-filter__range-toggle {
  cursor: pointer;
}

/* The picker's own injected "From"/"To" time-of-day row (see the
   `footerHTML` option in the 'filter' registerType) - sits above the
   default preview-text/Cancel/Apply row it's prepended to. Plain flexbox,
   same reasoning as .sw-filter__controls above: this is a small popover,
   not something that needs a breakpoint-based grid. */
.sw-filter__range-time {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  padding: 0 5px 8px;
  font-size: 0.85rem;
}
.sw-filter__range-time label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--bs-secondary-color);
}
.sw-filter__range-time input[type="time"] {
  width: auto;
}

/* Litepicker (assets/vendor/litepicker*.js/css) re-themed via its own
   --litepicker-* custom properties (see assets/vendor/litepicker.css)
   rather than overriding its selectors directly - keeps this plugin's
   part of the override to values only, and tracks this site's actual
   Bootstrap theme (light/dark, and any customized --bs-primary) instead
   of Litepicker's own hardcoded light-mode defaults. `.litepicker` itself
   is appended straight to <body> (not scoped under .sw-widget), so this
   has to be a page-level rule, same as :root would be. */
.litepicker {
  --litepicker-container-months-color-bg: var(--bs-body-bg);
  --litepicker-container-months-box-shadow-color: var(--bs-border-color);
  --litepicker-footer-color-bg: var(--bs-tertiary-bg, var(--bs-body-bg));
  --litepicker-footer-box-shadow-color: var(--bs-border-color);
  --litepicker-tooltip-color-bg: var(--bs-body-bg);
  --litepicker-month-header-color: var(--bs-body-color);
  --litepicker-button-prev-month-color: var(--bs-secondary-color);
  --litepicker-button-next-month-color: var(--bs-secondary-color);
  --litepicker-button-prev-month-color-hover: var(--bs-primary);
  --litepicker-button-next-month-color-hover: var(--bs-primary);
  --litepicker-month-weekday-color: var(--bs-secondary-color);
  --litepicker-month-week-number-color: var(--bs-secondary-color);
  --litepicker-day-color: var(--bs-body-color);
  --litepicker-day-color-hover: var(--bs-primary);
  --litepicker-is-today-color: var(--bs-danger);
  --litepicker-is-in-range-color: var(--bs-tertiary-bg, rgba(13, 110, 253, 0.08));
  --litepicker-is-locked-color: var(--bs-secondary-color);
  --litepicker-is-start-color: #fff;
  --litepicker-is-start-color-bg: var(--bs-primary);
  --litepicker-is-end-color: #fff;
  --litepicker-is-end-color-bg: var(--bs-primary);
  --litepicker-button-cancel-color: var(--bs-body-color);
  --litepicker-button-cancel-color-bg: var(--bs-secondary-bg-subtle);
  --litepicker-button-apply-color: #fff;
  --litepicker-button-apply-color-bg: var(--bs-primary);
  --litepicker-button-reset-color: var(--bs-secondary-color);
  --litepicker-button-reset-color-hover: var(--bs-primary);
  --litepicker-highlighted-day-color: var(--bs-body-color);
  --litepicker-highlighted-day-color-bg: var(--bs-warning-bg-subtle, #ffeb3b);
  z-index: 1060;
  border-radius: var(--bs-border-radius);
  overflow: hidden;
}
.litepicker .container__predefined-ranges {
  color: var(--bs-body-color);
}

/* Manual refresh button (filter.html.twig) - a brief spin on click as
   click-feedback, same idea as a native pull-to-refresh spinner; the
   auto-refresh interval already covers the ambient case, this is purely
   "I know I just clicked something, confirm it did something". */
.sw-filter__refresh-now-spin {
  display: inline-block;
  animation: sw-filter-spin 0.5s linear;
}
@keyframes sw-filter-spin {
  to { transform: rotate(360deg); }
}

/* "Save as Sensor default"/"Save as <Device> default" (filter.html.twig) -
   see refreshSaveButtons()/collectContent() in sensor-widgets.js. Sits in
   .sw-filter__top-row now (below), not its own row any more - margin-left:
   auto is what actually does the right-alignment there (pushes this whole
   group, and only this group, to the far end of the row), so this rule
   itself stays a plain flex row for its own two buttons. flex-wrap in case
   a genuinely narrow widget can't fit both icon buttons plus the show-all
   switch on one line - matches the same "wrap rather than squeeze" idiom
   .sw-filter__controls uses. */
.sw-filter__save-defaults {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-left: auto;
}

/* "Updated Xs ago" (filter.html.twig) - see renderLastUpdated() in
   sensor-widgets.js. Right-aligned so it reads as belonging to the
   refresh-interval control (the last, rightmost one in the row when
   there's room for all of them on one line) rather than the device
   picker it would otherwise line up under on the left.

   Bumped up from Bootstrap's old "small text-secondary" (0.875rem,
   regular weight) to its own explicit size/weight - with auto-refresh
   now defaulting to Off (see the _refresh <select> in filter.html.twig),
   this is the only cue that the numbers on screen might be old, so it
   needs to actually catch the eye rather than read as fine print. */
.sw-filter__last-updated {
  margin-top: 0.35rem;
  text-align: right;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--bs-secondary-color);
  /* Doubles as a "refresh now" click target, same action as the
     refresh-interval control's own toggle button - see its click handler
     in sensor-widgets.js. */
  cursor: pointer;
}

.sw-filter__last-updated:hover {
  text-decoration: underline;
  color: var(--bs-body-color);
}

/* Data is older than plugins.sensor-widgets.stale_after_seconds (default
   60, admin-configurable - see staleAfterMs in sensor-widgets.js) -
   toggled by renderLastUpdated(), not a CSS-only :contains() trick (plain
   text, no data attribute to match on). Red here is a plain staleness
   warning on a label every viewer sees the same way, not the CLAUDE.md
   "admin-only control" red - unrelated use of the same danger color. */
.sw-filter__last-updated--stale {
  color: var(--bs-danger);
  font-weight: 700;
}

/* Admin-only "show all devices" checkbox, left-hand side of
   .sw-filter__top-row (below) - unchecked by default (see
   filter.html.twig), so it reads as an opt-in rather than something
   already on.

   Danger/red rather than the plain body text every other label in this
   widget uses - the same "this is an admin function" marker as the
   Device Inspector widget's la-search icon and the Log widget's "All"
   lines option (SensorWidgets.markAdminOption()), applied here to a
   checkbox instead of a select option (the label's own la-search icon,
   see filter.html.twig, inherits this color for free). Colored
   regardless of checked state (not just once turned on) - unchecked-by-
   default is already the "safe" state, the label itself is what needs
   to read as staff-only at a glance. The switch's own "on" color follows suit instead of the
   theme's primary blue every other toggle in this plugin uses (e.g.
   .sw-status__row .form-check-input:checked), so flipping it on visibly
   reads as the admin action it is, not an ordinary settings toggle. */
.sw-filter__show-all {
  font-size: 0.85rem;
}

/* Wraps the "show all devices" switch (staff-only, may not render at all)
   and the "Save as ... default" icon buttons (below) into one line -
   see both classes' own comments above for what each side is. Always
   renders (unlike the switch inside it) so the buttons stay pinned to
   the right-hand end of this row - via .sw-filter__save-defaults' own
   margin-left:auto - whether or not the switch is present. */
.sw-filter__top-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}

.sw-filter__show-all .form-check-label {
  color: var(--bs-danger);
}

.sw-filter__show-all .form-check-input:checked {
  background-color: var(--bs-danger);
  border-color: var(--bs-danger);
}

/* No positioning/opacity rules of its own - see Chart's own
   settings-toggle comment for why (this becomes a plain header icon
   button once JS moves it there). Minimal modal (Widget field only, see
   this widget's own template docblock) - same shape as Chart's own
   settings-body/field, just no device/add-another. */
.sw-filter__settings-toggle {
  display: flex;
  align-items: center;
}
.sw-filter__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-filter__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-filter__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-filter__widget-controls {
  display: flex;
  gap: 0.4rem;
}

.sw-widget--chart {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  height: 100%;
}

/* Same truncate-on-narrow-widgets treatment as .sw-status__title-text -
   see that class's own comment for why min-width:0 is required here. The
   chart's title can run longer than Device Status's own (a device name
   PLUS " Chart", not just the bare name), so this matters even more
   at sm/md sizes. */
.sw-chart__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* No positioning/opacity rules of its own any more - see Single Value's
   own settings-toggle comment for why (this becomes a plain header icon
   button once JS moves it there). */
.sw-chart__settings-toggle {
  display: flex;
  align-items: center;
}

/* Device/Group/Group2 all live in the settings modal now (see this
   widget's own template) - .sw-chart__status is the only thing still
   inline in the card body, so it no longer needs the old
   .sw-chart__controls row's "space-between, status roughly centered
   between two selects" layout - just a small caption line above the
   chart. */
.sw-chart__status {
  font-size: 0.8rem;
  opacity: 0.7;
  text-align: left;
  min-height: 1.1em;
}

/* Same "now the outer .modal, no layout rules of its own" change as
   Single Value's own .sw-single-value__settings - see that class's
   comment. */
.sw-chart__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-chart__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-chart__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* Same reasoning as Single Value's own .sw-single-value__widget-controls
   - size/reorder controls moved here out of the shared header row. */
.sw-chart__widget-controls {
  display: flex;
  gap: 0.4rem;
}

.sw-chart__add {
  align-self: flex-start;
}

.sw-chart__container-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: 220px;
}

/* The ECharts instance is init()'d directly onto this div (see
   sensor-widgets.js) - it manages its own canvas(es) inside it, so this
   just needs to fill the available space; ECharts is resized to match via
   a ResizeObserver whenever the div itself changes size (widget resize,
   window resize, ...).

   Absolutely positioned against .sw-chart__container-wrap's `position:
   relative`, filling it via inset:0, rather than `width/height: 100%` -
   a percentage height doesn't reliably resolve against a flex item whose
   own height comes from `flex: 1 1 auto`/`min-height` rather than an
   explicit `height` (confirmed live: it rendered as 0px tall, so ECharts
   had nothing to draw into no matter what data it got). Absolute
   positioning sizes against the wrapper's actual laid-out box instead,
   sidestepping that percentage-resolution quirk entirely. */
.sw-chart__container {
  position: absolute;
  inset: 0;
}

.sw-widget--log {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  height: 100%;
}

/* Same truncate-on-narrow-widgets treatment as .sw-status__title-text -
   see that class's own comment. */
.sw-log__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sw-log__controls {
  display: flex;
  flex-wrap: wrap; /* loglevel + 2 filter selects + status can outgrow a narrow (sm/md) widget */
  align-items: center;
  gap: 0.5rem;
}

/* Loglevel / Filters / Lines are visually three separate groups within
   the controls row (see log.html.twig) - each is its own flex row so its
   label sits right next to its own select(s), and .sw-log__filters-group
   gets extra left margin on top of .sw-log__controls' own gap so it
   doesn't read as glued onto the Loglevel select next to it. */
.sw-log__loglevel-group,
.sw-log__filters-group,
.sw-log__lines-group {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.sw-log__filters-group {
  margin-left: 1rem;
}

.sw-log__loglevel-label,
.sw-log__filters-label,
.sw-log__lines-label {
  font-size: 0.85rem;
  opacity: 0.7;
  white-space: nowrap;
}

.sw-log__loglevel,
.sw-log__filter-level,
.sw-log__filter-component,
.sw-log__lines {
  max-width: 10rem;
}

/* Selects that reflect a live device setting rather than filter or
   control anything else on the page - the Loglevel select here, `mode`
   (and any other `selects`-category entry) in the Device Status widget -
   marked apart from ordinary filter/control selects with a dashed border
   and a tinted background so they read as "this reflects the device" at
   a glance, whether or not this particular one is currently editable.
   Editability itself is per-instance now (canWrite && item.command_ready,
   see the `status` registerType in sensor-widgets.js) - a disabled one
   still gets the native browser disabled look on top of this styling
   (dimmed, no pointer cursor), an enabled one additionally gets a real
   change handler that POSTs to /sensor-widgets/api/command. Loglevel
   itself has no change handler at all regardless (see the `log`
   registerType's own comment on why). Selector repeats .form-select
   alongside .sw-device-select (specificity 0,2,0) rather than just the
   latter alone (0,1,0) - Bootstrap's own .form-select rule is equal
   specificity and loads *after* this stylesheet (adminlte.css, further
   down <head>), so without the boost it silently wins on load order
   alone. Higher specificity still than this on a given select's own
   value-specific color (e.g. .sw-status__select--winter, see
   below - two of ITS OWN classes) lets that override this rule's
   background/border-color while keeping the dashed border-style. */
.form-select.sw-device-select {
  border-style: dashed;
  background-color: var(--bs-secondary-bg-subtle);
}

.sw-log__table-wrap .badge {
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0.3em 0.65em;
}

.sw-log__status {
  font-size: 0.8rem;
  opacity: 0.7;
}

/* min-width:0 is what actually makes overflow-x:auto do anything here:
   as a flex item of .sw-widget--log (flex-direction:column, so width is
   the cross axis), this item's default min-width:auto resolves to the
   table's own min-content width (nowrap cells - see below - have a WIDE
   one) and refuses to shrink below it, so the wrap's box grows to match
   the table instead of clipping it. That growth was also escaping this
   widget entirely, upstream through .dashboard-widget__body and
   .dashboard-widget (see user-dashboard's dashboard.css, same fix
   applied there) to stretch the whole dashboard grid on mobile - fixing
   it at every level of that chain is what actually contains a wide
   table to just this widget's own horizontal scrollbar. */
.sw-log__table-wrap {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}

/* Dynamic (content-fit) column widths rather than an even/fixed split -
   Time/Level/Component naturally stay compact, leaving Message (by far
   the longest content) the rest of the space. Every column is nowrap so
   a row always reads as one line - .sw-log__table-wrap already scrolls
   horizontally, which reads better for a log than wrapped, ragged rows. */
.sw-log__table-wrap table.sw-log__table {
  table-layout: auto;
}

.sw-log__table-wrap td,
.sw-log__table-wrap th {
  white-space: nowrap;
}

/* DataTables' own layout controls (search box, page-length select, paging)
   inherit Bootstrap's .form-control/.page-link colors already, so they
   pick up light/dark theming for free the same way the plain-table
   styling elsewhere in this theme does. The two rules below it are the
   exception - see their own comment. */
.sw-log__table-wrap .dataTables_wrapper {
  font-size: 0.85rem;
}

/* Two more hardcoded-#0d6efd cases (like .form-check-input:checked
   above): the compiled adminlte.css's --bs-pagination-active-bg/
   -border-color (the paginator's current-page button) don't track
   var(--bs-primary) either, and DataTables' own Bootstrap 5 integration
   CSS (dataTables.bootstrap5.min.css) bakes the same blue into
   --dt-row-selected (row-selection highlight, and the processing-spinner
   dots) as a literal "13, 110, 253" RGB triple rather than reusing
   Bootstrap's own --bs-primary-rgb. Shared between both DataTables
   widgets (Log and Device Inspector) since neither table styles its own
   pagination/DataTables chrome differently - one rule covers both.
   --dt-row-stripe similarly hardcodes black for table-striped's tint
   (rgba(var(--dt-row-stripe), 0.05)); pointed at --bs-secondary-rgb
   instead so alternating rows actually use the theme's secondary color,
   not an untinted grey. All three *-rgb variables are Bootstrap's own
   comma-separated triples, already in the exact format
   rgb(var(--dt-...)) expects. CSS custom properties inherit, so setting
   them here on the wrap element cascades down into the
   .dataTables_wrapper/table/pagination DataTables renders inside it,
   with no vendored file touched. */
.sw-log__table-wrap,
.sw-inspector__table-wrap {
  --bs-pagination-active-bg: var(--bs-primary);
  --bs-pagination-active-border-color: var(--bs-primary);
  --dt-row-selected: var(--bs-primary-rgb);
  --dt-row-stripe: var(--bs-secondary-rgb);
}

/* No positioning/opacity rules of its own - see Chart's own
   settings-toggle comment for why (this becomes a plain header icon
   button once JS moves it there). */
.sw-log__settings-toggle {
  display: flex;
  align-items: center;
}

/* Device now lives in the settings modal (see this widget's own
   template) alongside the "Widget" field - same shape as Chart's own
   .sw-chart__settings-body/.sw-chart__field. */
.sw-log__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-log__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-log__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-log__widget-controls {
  display: flex;
  gap: 0.4rem;
}
.sw-log__add {
  align-self: flex-start;
}

.sw-widget--status {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  height: 100%;
}

/* No positioning/opacity rules of its own - see Chart's own
   settings-toggle comment for why (this becomes a plain header icon
   button once JS moves it there). */
.sw-status__settings-toggle {
  display: flex;
  align-items: center;
}

/* Same shape as Chart/Log's own settings-body/field - see Chart's own
   comment. */
.sw-status__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-status__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-status__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-status__widget-controls {
  display: flex;
  gap: 0.4rem;
}
.sw-status__add {
  align-self: flex-start;
}

/* Truncates the card title's text with an ellipsis instead of wrapping
   or overflowing on narrow (sm/md) widgets - the icon stays full size
   since it's a sibling, not part of this span. min-width:0 is required
   for a flex child (.dashboard-widget__title is display:inline-flex) to
   be allowed to shrink below its content's natural width at all -
   without it text-overflow never gets the chance to trigger. */
.sw-status__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* No reserved min-height here any more (it used to hold a full text
   line's worth of space so rows below wouldn't jump once this emptied
   out after loading) - it sat empty far more often than not, reading as
   dead space right above `mode`. The brief jump while this text is
   actually populated ("Loading…", an error) is the accepted trade-off. */
.sw-status__status {
  display: block;
  font-size: 0.8rem;
  opacity: 0.7;
}

/* No section headings (there's no fixed set of sections any more - the
   number of status_position groups is entirely data-driven, see this
   widget's template docblock) - a visible gap between .sw-status__head
   and each .sw-status__group tells them apart instead, hence the larger
   margin here than .sw-widget--status's own small flex gap would give on
   its own (that gap is fine *within* a list's own rows, too tight between
   two full sections with nothing in between to say "new section" any
   more). .sw-status__head has no top margin of its own - it's the whole
   widget's lead section, not a "new section" of its own, and
   .sw-widget--status's own 0.35rem flex gap (see above) already separates
   it from the status line without adding to it. */
.sw-status__head {
  margin-top: 0;
}

.sw-status__group {
  margin-top: 0.3rem;
}

/* `status_position: head` entries are deliberately more prominent than an
   ordinary row - see this widget's template docblock for why they're
   promoted above every group. Explicit gap (rather than relying on
   justify-content:space-between's own auto-spacing) keeps a real,
   guaranteed minimum gap between the label and the value control even on
   a narrow (sm) widget, where space-between alone can pinch them together
   with barely a hair of room. */
.sw-status__row--head > .sw-status__row-main {
  font-size: 1rem;
  font-weight: 600;
  gap: 0.75rem;
}

.sw-status__select {
  max-width: 10rem;
}

/* Tinted per the currently selected option - "Winter"/"Summer" per the
   catalog (any `selects` entry's `options:` in a site's
   blueprints/sensors/*.yaml) - via Bootstrap 5.3's own theme-aware
   *-bg-subtle/-text-emphasis/-border-subtle CSS variables (the same ones
   text-bg-*-subtle utility classes use), so this stays correctly readable
   in both light and dark mode without any hand-picked colors. Two classes
   on the selector matches .form-select.sw-device-select's own specificity
   (0,2,0, see above) rather than falling short of it with just
   .sw-status__select--winter alone (0,1,0) - equal specificity with this
   rule declared later in the file is what lets the tint win here while
   still keeping that rule's dashed border-style. */
.sw-status__select.sw-status__select--winter {
  background-color: var(--bs-info-bg-subtle);
  color: var(--bs-info-text-emphasis);
  border-color: var(--bs-info-border-subtle);
}

.sw-status__select.sw-status__select--summer {
  background-color: var(--bs-warning-bg-subtle);
  color: var(--bs-warning-text-emphasis);
  border-color: var(--bs-warning-border-subtle);
}

/* list-group-flush's own borders/background already theme correctly via
   Bootstrap's --bs-list-group-* variables (data-bs-theme aware) - no
   color rules needed here. */
.sw-status__group {
  font-size: 0.85rem;
}

/* The actual vertical spacing control - not .sw-status__row-main below,
   which is otherwise the more obvious place to put it. Bootstrap's own
   .list-group-item rule puts a comparatively spacious 0.5rem 1rem
   padding directly on this <li>; overriding it *here* replaces that
   default outright, whereas padding added only to the inner
   .sw-status__row-main div stacks on TOP of it - every row in this
   widget was carrying both at once, which is what made the whole thing
   read as far less compact than intended (confirmed by measuring an
   actual row's box model live: the <li> itself was still contributing
   8px top+bottom no matter how far .sw-status__row-main's own padding
   was tightened). Horizontal padding is zeroed rather than kept - the
   card around this widget already has its own padding, a second inset
   here just wastes width a narrow (sm) widget doesn't have to spare. */
.sw-status__row {
  padding: 0.2rem 0;
}

.sw-status__row-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

/* The label can shrink and ellipsis (min-width:0 is what makes a flex
   child allowed to shrink below its content's natural width at all -
   without it text-overflow never gets the chance to trigger); the value
   side never wraps instead (see .sw-status__text-value/.sw-status__number
   below) - between the two, a narrow (sm) widget loses the label's tail
   to an ellipsis rather than breaking a value + its unit suffix onto a
   second line, which read far worse. The chevron/spacer stays a fixed
   width so every row's label starts at the same x position within a
   group, whether or not that particular row has children. */
.sw-status__row-main > .sw-status__label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Every value control this widget renders (plain text, a number's
   -/+/textbox/unit) claims its own natural width rather than shrinking -
   see the comment above .sw-status__label for why. */
.sw-status__row-main > .sw-status__text-value,
.sw-status__row-main > .sw-status__number {
  flex: 0 0 auto;
  white-space: nowrap;
}

.sw-status__toggle,
.sw-status__toggle-spacer {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
}

.sw-status__toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--bs-secondary-color);
  cursor: pointer;
}

.sw-status__toggle svg {
  transition: transform 0.15s ease;
}

/* Chevron points right collapsed, rotates to point down expanded - per
   this widget's own spec. */
.sw-status__row--expanded > .sw-status__row-main > .sw-status__toggle svg {
  transform: rotate(90deg);
}

.sw-status__label--toggle {
  cursor: pointer;
}

.sw-status__label--toggle:hover {
  text-decoration: underline;
}

/* wifi_ip_address's value cell (see buildWifiIpCell() in
   sensor-widgets.js) - the info icon shown instead of a clickable link
   when this browser can't reach the device's IP (most commonly: viewing
   the dashboard from outside its local network). Sits right after the
   text value, native `title` attribute for the tooltip - same "no JS
   tooltip library, just the browser's own" convention as this widget's
   card title truncation. */
.sw-status__wifi-ip-info {
  margin-left: 0.3rem;
  color: var(--bs-secondary-color);
}

/* Collapsed by default - only an expanded parent row reveals its own
   direct children. Nested one level deeper than its parent group for a
   visible "this belongs to the row above" indent, without needing to
   fight list-group's default flush padding.

   Selector repeats .list-group alongside .sw-status__children
   (specificity 0,2,0) rather than just the latter alone (0,1,0) -
   Bootstrap's own `.list-group { display: flex }` is equal specificity
   and loads *after* this stylesheet (adminlte.css, further down <head>),
   so without the boost it silently wins on load order alone and this
   rule's `display: none` never actually takes effect - confirmed live
   (a freshly built child list rendered permanently open, not collapsed,
   until this was fixed). Same precedent as .form-select.sw-device-select
   above.

   Unlike a top-level group, children read as one block attached to
   their parent row rather than a separate list: no border between (or
   around) individual child rows - list-group-flush's own border, still
   in effect for .sw-status__group above, would otherwise draw a full
   set of separator lines here too, at odds with "these belong to the
   row above" - and a thin left border instead runs the full height of
   the block as a single visual "stem" connecting it back up to the
   parent's chevron. Tighter vertical margins than before (this replaced
   a flatter, single-list layout that had no separate children concept
   at all - see this widget's own history) keep the block snug under its
   parent instead of reading as its own section. */
.sw-status__children.list-group {
  display: none;
  margin: 0 0 0.15rem 1.6rem;
  padding-left: 0.5rem;
  border-left: 2px solid var(--bs-border-color);
}

.sw-status__row--expanded > .sw-status__children.list-group {
  display: block;
}

/* Three classes' worth of specificity (0,3,0) - safely above Bootstrap's
   own .list-group-item border/padding rules (0,1,0) regardless of
   stylesheet load order, same reasoning as
   .form-select.sw-device-select/.sw-status__children above, no
   !important needed. Padding here (not .sw-status__row-main - see that
   rule's own comment for why the <li> itself, not its inner div, is
   where padding belongs) is tighter than a top-level row's: a child is
   already visually set apart by the indent/left border on
   .sw-status__children, so it doesn't need as much of its own breathing
   room to read as a distinct row. */
.sw-status__children.list-group > .sw-status__row {
  border: 0;
  padding: 0.05rem 0;
}

/* Smaller font-size rather than just less padding - Bootstrap's own
   controls here (the switch/select) are sized in `em`, so this shrinks
   them right along with the text instead of leaving an oversized control
   next to now-tiny text. Line-height tightened too - Bootstrap's default
   (1.5) reserves more vertical space than a single-line label/value row
   actually needs. */
.sw-status__children.list-group {
  font-size: 0.8em;
  line-height: 1.2;
}

/* The chevron/spacer and number controls are sized in `rem` (root em),
   not `em` - deliberately, everywhere else in this widget, so a size
   change to one row never cascades into its own children's controls by
   accident (there was no nesting to worry about before children
   existed). Children are the one place that same fixed-`rem` sizing
   works against the smaller font-size right above, so it's undone
   locally here instead of switching those rules to `em` globally. */
.sw-status__children.list-group .sw-status__toggle,
.sw-status__children.list-group .sw-status__toggle-spacer {
  width: 1rem;
  height: 1rem;
}

.sw-status__children.list-group .sw-status__number-btn {
  width: 1.1rem;
  height: 1.1rem;
  font-size: 0.7rem;
}

/* font-size here is its own explicit override, not just inherited -
   .sw-status__number-input sets its own fixed 0.85rem below, and `rem`
   is relative to the document root, not this row's own (`em`-scaled)
   font-size, so the parent .sw-status__children rule above never
   reaches it on its own. */
.sw-status__children.list-group .sw-status__number-input {
  padding: 0.05rem 0.3rem;
  font-size: 0.75rem;
  line-height: 1.1;
}

/* Bootstrap's .form-check reserves a min-height (1.5rem) regardless of
   font-size or this row's own padding - without this override a child
   switch row stays Bootstrap's minimum height no matter how far
   everything else here is tightened. */
.sw-status__children.list-group .form-check {
  min-height: 0;
}

/* A writable `numbers` setpoint: -/+ buttons flanking a plain textbox
   (see buildNumberCell() in sensor-widgets.js for why a slider isn't
   used). Buttons match the chevron's footprint-ish sizing so the whole
   row stays visually compact and dense, matching every other value
   control in this widget. */
.sw-status__number {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

.sw-status__number-btn {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius-sm);
  background-color: var(--bs-secondary-bg-subtle);
  color: var(--bs-body-color);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.sw-status__number-btn:hover {
  background-color: var(--bs-secondary-bg);
}

.sw-status__number-input {
  width: 4.5rem;
  padding: 0.15rem 0.4rem;
  border: 1px dashed var(--bs-border-color);
  border-radius: var(--bs-border-radius-sm);
  background-color: var(--bs-secondary-bg-subtle);
  color: var(--bs-body-color);
  font-size: 0.85rem;
  text-align: right;
}

.sw-status__number-unit {
  font-size: 0.8rem;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}

/* Write-back commit button for a writable numbers setpoint (see
   buildNumberCell() in sensor-widgets.js) - only rendered at all once
   this viewer may actually write back to the device; disabled until the
   field is dirty. Same footprint as .sw-status__number-btn above but
   tinted primary (rather than the -/+ buttons' neutral gray) so the one
   control that actually sends something reads as distinct from the two
   that only edit the local textbox. */
.sw-status__number-apply {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--bs-primary);
  border-radius: var(--bs-border-radius-sm);
  background-color: var(--bs-primary-bg-subtle);
  color: var(--bs-primary);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.sw-status__number-apply:disabled {
  opacity: 0.4;
  cursor: default;
}

.sw-status__number-apply:not(:disabled):hover {
  background-color: var(--bs-primary);
  color: var(--bs-white);
}

/* Transient "that write-back command failed" flash - applied by
   SensorWidgets.flashError() to whichever control just failed (a
   switch/select/number's own input), removed automatically after a
   couple of seconds. Outline rather than border/background so it
   doesn't fight the control's own existing border-color/background
   rules (.sw-device-select's dashed border, .sw-status__number-input's,
   ...) - it just draws on top of whatever's already there. */
.sw-command-error {
  outline: 2px solid var(--bs-danger);
  outline-offset: 1px;
}

/* The switch toggles' own "on" color is #0d6efd hardcoded into the
   compiled adminlte.css (.form-check-input:checked), not
   var(--bs-primary) - so it silently stops matching the theme's actual
   primary color the moment a site customizes it. Scoped override here
   makes it track --bs-primary for real, wherever in the widget a switch
   ends up (head, a group, or nested as a child). */
.sw-status__row .form-check-input:checked {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}

/* Single Value widget - meant to stay small (SM is its default_size, see
   the catalog entry in sensor-widgets.php) and glance-able, so this is
   centered/compact rather than following the left-aligned, multi-row
   layout every other widget here uses. */
.sw-widget--single-value {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 5rem;
  text-align: center;
}

/* Same truncate-on-narrow-widgets treatment as .sw-status__title-text -
   see that class's own comment for why min-width:0 is required here. */
.sw-single-value__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* No positioning/opacity rules of its own any more - on init, this
   widget's own JS physically moves this button out of the card body and
   into the shared dashboard-widget__controls header row (see
   user-dashboard's dashboard.html.twig "compact_controls" slot and this
   widget's own registerType() in sensor-widgets.js), where
   dashboard.css's own `.dashboard-widget__controls button` rule already
   styles it identically to the collapse/hide buttons next to it. */
.sw-single-value__settings-toggle {
  display: flex;
  align-items: center;
}

.sw-single-value__value {
  font-size: 2rem;
  font-weight: 600;
  line-height: 1.15;
}
.sw-single-value__unit {
  font-size: 1rem;
  font-weight: 400;
  margin-left: 0.3rem;
  color: var(--bs-secondary-color);
}

.sw-single-value__label {
  font-size: 0.8rem;
  margin-top: 0.15rem;
}

/* The metric's own translated catalog label, under the device name
   (.sw-single-value__label above) - see sensor-widgets.js's own comment
   on keyEl for why this is meta.label, not the raw key. A smaller,
   tertiary line rather than matching the label's own size, since it
   visually repeats the card title above and isn't the primary thing most
   viewers need to read at a glance. */
.sw-single-value__key {
  font-size: 0.65rem;
}

/* Deliberately short and unlabeled (no axis, no legend - see
   renderSparkline() in sensor-widgets.js) - a trend indicator, not a
   second Chart widget. Fixed height rather than flex-growing to fill the
   widget: an SM tile has little enough room already that letting the
   chart claim "whatever's left" would fight the value/label above it for
   space unpredictably as the card is resized. */
.sw-single-value__chart {
  height: 44px;
  margin-top: 0.5rem;
}

/* Display size (see the widget's own "Display size" settings field and
   registerType() in sensor-widgets.js) - independent of the dashboard
   card's own xs/sm/.../xxl size (that controls how much grid space the
   whole card takes; this controls how big the number itself reads
   within it, e.g. a small card with a large, dense number, or a wide
   card with a modest one). `--size-md` matches the original unmodified
   values above exactly, so a widget that's never touched this setting
   (no `display_size` ever saved) looks identical to before this existed
   - only reached via the JS default of 'md', never written into the DOM
   as its own class the way sm/lg are.
   Font sizes only - not the whole tile's proportions - so cards keep
   comparing sensibly against each other at a glance even when their
   number sizes differ. */
.sw-single-value--size-sm .sw-single-value__value { font-size: 1.3rem; }
.sw-single-value--size-sm .sw-single-value__unit { font-size: 0.8rem; }
.sw-single-value--size-sm .sw-single-value__label { font-size: 0.7rem; }
.sw-single-value--size-sm .sw-single-value__key { font-size: 0.6rem; }
.sw-single-value--size-sm .sw-single-value__chart { height: 32px; }

.sw-single-value--size-lg .sw-single-value__value { font-size: 2.75rem; }
.sw-single-value--size-lg .sw-single-value__unit { font-size: 1.2rem; }
.sw-single-value--size-lg .sw-single-value__label { font-size: 0.9rem; }
.sw-single-value--size-lg .sw-single-value__key { font-size: 0.75rem; }
.sw-single-value--size-lg .sw-single-value__chart { height: 56px; }

/* .sw-single-value__settings is now the outer Bootstrap `.modal` element
   itself (see this widget's own template) - no layout rules of its own
   any more, Bootstrap fully owns its display/position/backdrop toggling
   and this plugin's CSS must not fight that (the old margin/border-top/
   display:flex here made sense for an inline panel sitting below the
   value display in the same card; a modal has its own header for that
   visual separation instead). The flex-column/gap layout those old
   rules also carried moved to .sw-single-value__settings-body below,
   which is what actually wraps the fields now (the modal's own
   .modal-body). */
.sw-single-value__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-single-value__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-single-value__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-single-value__field--check {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
}
.sw-single-value__field--check label {
  font-size: 0.85rem;
  color: inherit;
}
/* Same reasoning as .sw-status__row .form-check-input:checked above -
   the compiled adminlte.css hardcodes the switch's "on" color rather
   than tracking a customized --bs-primary. */
.sw-single-value__show-chart:checked {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}

/* Size/reorder controls, moved here out of the shared header row (see
   this widget's own template docblock and dashboard.html.twig's
   "compact_controls" slot) - same buttons/data-action attributes the
   header used to render, just restyled as plain Bootstrap buttons to fit
   this panel instead of bare header icons. */
.sw-single-value__widget-controls {
  display: flex;
  gap: 0.4rem;
}

.sw-single-value__add {
  align-self: flex-start;
}

/* Gauge widget - same "stay small and glance-able" treatment as Single
   Value above (SM is its default_size too), centered rather than the
   left-aligned multi-row layout most other widgets use. */
.sw-widget--gauge {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 7rem;
  text-align: center;
}

.sw-gauge__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* No positioning/opacity rules of its own any more - see Single Value's
   own settings-toggle comment above, same mechanism. */
.sw-gauge__settings-toggle {
  display: flex;
  align-items: center;
}

.sw-gauge__display {
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* Square-ish and fixed-height rather than flex-growing to fill the card -
   same reasoning as .sw-single-value__chart: an SM tile has little enough
   room already that "whatever's left" would make the ring's proportions
   unpredictable as the card is resized. */
.sw-gauge__chart {
  width: 100%;
  max-width: 160px;
  height: 120px;
}

.sw-gauge__placeholder {
  font-size: 0.85rem;
  padding: 1.5rem 0;
}

.sw-gauge__label {
  font-size: 0.8rem;
  margin-top: 0.15rem;
}

/* Same "smaller, tertiary line under the device name" treatment as
   Single Value's own .sw-single-value__key - see that widget's own
   comment. No display-size (--size-sm/lg) overrides here, matching
   .sw-gauge__label's own lack of them - unlike Single Value, Gauge's
   own display size only scales the ring/its ECharts-drawn center text,
   never this label's CSS font-size. */
.sw-gauge__key {
  font-size: 0.65rem;
}

/* Display size (see the widget's own "Display size" settings field) -
   same independent-of-card-size idea as Single Value's own --size-sm/lg,
   see that widget's own comment. Only the ring's diameter is CSS here -
   its center text's own font size is drawn by ECharts onto canvas, not
   styleable from here, so that part is set directly in renderGauge()'s
   own `detail.fontSize` (see sensor-widgets.js, keyed off the same
   `displaySize` variable this class name comes from). `--size-md`
   matches the original unmodified .sw-gauge__chart values above exactly
   - see Single Value's own comment for why that one's never written into
   the DOM as its own class. */
.sw-gauge--size-sm .sw-gauge__chart { max-width: 110px; height: 85px; }
.sw-gauge--size-lg .sw-gauge__chart { max-width: 210px; height: 160px; }

/* Same "now the outer .modal, no layout rules of its own" change as
   Single Value's own .sw-single-value__settings - see that class's
   comment. */
.sw-gauge__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-gauge__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-gauge__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-gauge__minmax {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.sw-gauge__minmax input {
  min-width: 0;
  flex: 1 1 0;
}
.sw-gauge__zone {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.sw-gauge__zone input[type="number"] {
  min-width: 0;
  flex: 1 1 0;
}
.sw-gauge__zone input[type="color"] {
  flex: 0 0 auto;
  width: 2.1rem;
  height: 1.6rem;
  padding: 0.1rem;
}
.sw-gauge__zone-op {
  color: var(--bs-secondary-color);
  font-size: 0.8rem;
}
.sw-gauge__zone-label {
  font-size: 0.75rem;
}

/* Value/% mode select on each threshold row (see threshold1Mode/
   threshold2Mode in sensor-widgets.js) - fixed narrow width rather than
   sharing the number input's flex: 1 1 0, since its own two option
   labels are always short regardless of language (de "Wert"/fr "Valeur"
   still fit). Scoped through .sw-gauge__zone (matching the ancestor
   combinator .sw-gauge__zone input[type="color"] below already uses)
   rather than the bare class alone - needed here because Bootstrap's own
   .form-select sets `width: 100%` at the exact same specificity as a
   single class selector; adminlte.css's compiled Bootstrap loads AFTER
   this stylesheet, so a same-specificity rule here would lose the
   cascade tie and the select would balloon to the row's full width
   (flex: 0 0 auto means it never shrinks back down), shoving the color
   swatch after it outside the modal entirely. */
.sw-gauge__zone select.sw-gauge__threshold1-mode,
.sw-gauge__zone select.sw-gauge__threshold2-mode {
  flex: 0 0 auto;
  width: 4.5rem;
}

/* Holds just the swap-colors button, right-aligned so it lands directly
   under the 3rd zone's own color swatch above it (every .sw-gauge__zone
   row ends in a fixed-width color input at its own right edge, so their
   right edges - and this button - all line up in one column). */
.sw-gauge__swap-colors-row {
  display: flex;
  justify-content: flex-end;
}

/* Plain icon button, no border/background of its own - same reset as
   .sw-status__toggle above. Swaps base_color with threshold2Color on
   click (see this widget's own template docblock). Width matches the
   2.1rem color swatches above it exactly (same value
   .sw-gauge__zone input[type="color"] uses) so its icon centers directly
   under the 3rd zone's own swatch, not merely flush with the row's right
   edge. */
.sw-gauge__swap-colors {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--bs-secondary-color);
  font-size: 0.95rem;
  cursor: pointer;
}
.sw-gauge__swap-colors:hover {
  color: var(--bs-body-color);
}

/* Same reasoning as Single Value's own .sw-single-value__widget-controls
   above - size/reorder controls moved here out of the shared header row. */
.sw-gauge__widget-controls {
  display: flex;
  gap: 0.4rem;
}

.sw-gauge__add {
  align-self: flex-start;
}

/* Switches widget - unlike Single Value/Gauge (a single centered value),
   this shows a LIST of rows, so it stays left-aligned like Device
   Status's own rows rather than centered. Still keeps the small-and-
   glance-able sizing those two widgets use (SM is its default_size too). */
.sw-widget--switches {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 5rem;
}

.sw-switches__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sw-switches__list {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.sw-switches__row {
  padding: 0.15rem 0;
}
/* Same reasoning as .sw-status__row .form-check-input:checked - the
   compiled adminlte.css hardcodes the switch's "on" color rather than
   tracking a customized --bs-primary. */
.sw-switches__row .form-check-input:checked {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}

.sw-switches__placeholder {
  font-size: 0.85rem;
  text-align: center;
  padding: 1.5rem 0;
}

/* Same "now the outer .modal, no layout rules of its own" change as
   Single Value's own .sw-single-value__settings - see that class's
   comment. */
.sw-switches__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-switches__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-switches__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.sw-switches__picker {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  max-height: 10rem;
  overflow-y: auto;
}
.sw-switches__picker-empty {
  font-size: 0.8rem;
  margin: 0;
}
.sw-switches__picker .form-check-input:checked {
  background-color: var(--bs-primary);
  border-color: var(--bs-primary);
}

.sw-switches__widget-controls {
  display: flex;
  gap: 0.4rem;
}

.sw-switches__add {
  align-self: flex-start;
}

.sw-widget--inspector {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  height: 100%;
}

/* No positioning/opacity rules of its own - see Chart's own
   settings-toggle comment for why (this becomes a plain header icon
   button once JS moves it there). Minimal modal (Widget field only, see
   this widget's own template docblock) - same shape as Filter's own
   settings-body/field. */
.sw-inspector__settings-toggle {
  display: flex;
  align-items: center;
}
.sw-inspector__settings-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: left;
}
.sw-inspector__field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.sw-inspector__field label {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}
.sw-inspector__widget-controls {
  display: flex;
  gap: 0.4rem;
}

/* Same truncate-on-narrow-widgets treatment as .sw-status__title-text -
   see that class's own comment. */
.sw-inspector__title-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sw-inspector__controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.sw-inspector__status {
  font-size: 0.8rem;
  opacity: 0.7;
}

/* Same fix, same reason as .sw-log__table-wrap above (min-width:0 is
   what makes overflow-x:auto actually clip/scroll the table instead of
   the wrap growing to match its width) - this table has even more
   columns (Last Seen/First Seen/Status/Category/Label/Latest Value, all
   nowrap - see below), so it's the more visible case of the two on a
   narrow viewport. */
.sw-inspector__table-wrap {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
}

/* Same reasoning as .sw-log__table-wrap .dataTables_wrapper - DataTables'
   own controls already theme correctly via Bootstrap's own classes. */
.sw-inspector__table-wrap .dataTables_wrapper {
  font-size: 0.85rem;
}

/* Category/status badges (see the `inspector` registerType in
   sensor-widgets.js) - sized down slightly from Bootstrap's default
   badge padding so they sit comfortably in a dense table row instead of
   inflating the row height. Colors come entirely from Bootstrap's own
   text-bg-*, -subtle, -emphasis utility classes, which are already
   data-bs-theme aware - nothing theme-specific needed here. */
.sw-inspector__table-wrap .badge {
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0.3em 0.65em;
}

/* Latest Value cell for a `switches` key (see valueCell() in the
   `inspector` registerType) - the disabled toggle preview sits inline
   with the raw ON/OFF text next to it rather than stacked, so the row
   doesn't grow taller than a plain badge/text cell would. */
.sw-inspector__switch {
  display: inline-flex;
  align-items: center;
  margin-right: 0.35rem;
  vertical-align: middle;
}

/* Last Seen / First Seen (columns 1-2) / Latest Value (column 6) - short,
   fixed-ish content that reads worse wrapped across lines than it does
   just claiming its own natural width; the table already scrolls
   horizontally (.sw-inspector__table-wrap) so there's no downside.
   Column numbers match the current thead order in inspector.html.twig -
   Last Seen, First Seen, Status, Category, Label, Latest Value. */
.sw-inspector__table-wrap td:nth-child(1),
.sw-inspector__table-wrap td:nth-child(2),
.sw-inspector__table-wrap td:nth-child(6) {
  white-space: nowrap;
}

/* The Device Inspector widget requires the sensor-widgets.inspector
   permission (see this widget's template docblock and its
   'restricted_to_inspector_permission' catalog entry) - its header
   icon is tinted danger/red so it reads as internal-only tooling at a
   glance, distinct from every other (non-admin) widget's plain-colored
   icon. Targets the card by its data-id since the header itself
   (.dashboard-widget__title) is rendered by user-dashboard's own
   template, outside this widget's own .sw-widget markup. */
.dashboard-widget[data-id="sensor-widgets-inspector"] .dashboard-widget__title i {
  color: var(--bs-danger);
}
