:root { color-scheme: dark; }
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: radial-gradient(circle at 20% 20%, #1b2735, #090a0f 70%);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: #e8ecf1;
}

/* -- Sign-in card (also used for small info states) ------------------------- */
/* flex: 1 (not min-height: 100vh) so the sign-in card still centers in
   whatever space is left above the footer, instead of always claiming a full
   viewport height and pushing the footer below the fold. */
.centered { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; }
#app-view { flex: 1; display: flex; flex-direction: column; }
.spinner {
  width: 32px; height: 32px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,0.15); border-top-color: #3fa9f5;
  animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.site-footer { flex-shrink: 0; text-align: center; padding: 10px; font-size: 0.68rem; color: #5a6472; }
.site-footer a { color: #5a6472; }
.footer-ios-link { display: inline-flex; align-items: center; gap: 4px; vertical-align: middle; }
.card {
  width: 100%; max-width: 380px;
  background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08);
  border-radius: 16px; padding: 32px;
}
.card h1 { font-size: 1.5rem; margin: 0 0 4px; letter-spacing: -0.01em; }
.card p.tagline { color: #aab4c0; margin: 0 0 24px; font-size: 0.95rem; }
label { display: block; font-size: 0.85rem; color: #aab4c0; margin-bottom: 6px; }
input, select {
  width: 100%; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.15);
  border-radius: 8px; padding: 10px 12px; color: #e8ecf1; font-size: 1rem; margin-bottom: 16px;
}
input:focus, select:focus { outline: 2px solid #3fa9f5; border-color: transparent; }
input[type=color] { padding: 3px; height: 42px; cursor: pointer; }
button, a.secondary {
  padding: 11px 16px; border-radius: 8px; border: none; font-size: 0.95rem; font-weight: 600;
  cursor: pointer; margin-bottom: 10px; font-family: inherit;
}
button.primary { background: #3fa9f5; color: #06131f; }
button.secondary, a.secondary { background: rgba(255,255,255,0.08); color: #e8ecf1; border: 1px solid rgba(255,255,255,0.15); }
button.danger { background: rgba(240,90,90,0.15); color: #f5a3a3; border: 1px solid rgba(240,90,90,0.3); }
button.link { background: none; color: #8fbfe8; font-weight: 400; text-decoration: underline; padding: 4px; }
button:disabled { opacity: 0.5; cursor: default; }
.divider { display: flex; align-items: center; gap: 10px; color: #5a6472; font-size: 0.8rem; margin: 6px 0 16px; }
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: rgba(255,255,255,0.1); }
.msg { font-size: 0.85rem; padding: 10px 12px; border-radius: 8px; margin-bottom: 16px; }
.msg.error { background: rgba(240,90,90,0.12); color: #f5a3a3; border: 1px solid rgba(240,90,90,0.3); }
.msg.ok { background: rgba(143,211,163,0.1); color: #8fd3a3; border: 1px solid rgba(143,211,163,0.25); }
.hidden { display: none !important; }
/* auto, not hidden -- the layout math in layoutCamerasGrid aims to need no
   scroll, but overflow must never be blocked outright: that's what silently
   hid cameras off-screen with no way to reach them when the computed size
   didn't fit correctly. Scrolling is the safety net, not the layout goal. */
body.cameras-open { overflow-y: auto; }

/* -- Dashboard shell ---------------------------------------------------------- */
#dashboard { width: 100%; padding: 20px; }
.topbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; gap: 12px; flex-wrap: wrap; }
.topbar h1 { font-size: 1.25rem; margin: 0; }
.topbar .spacer { flex: 1; }
.topbar button { margin-bottom: 0; }
#board-switcher { width: auto; margin-bottom: 0; font-weight: 600; }
#tile-filter { width: auto; margin-bottom: 0; font-size: 0.85rem; }
.conn-status { font-size: 0.75rem; color: #5a6472; display: flex; align-items: center; gap: 6px; }
.conn-dot { width: 7px; height: 7px; border-radius: 50%; background: #5a6472; }
.conn-status.live .conn-dot { background: #8fd3a3; }
.conn-status.polling .conn-dot { background: #e0b24a; }

/* -- Hamburger menu (replaces the old inline row of topbar buttons) --------- */
.menu-wrap { position: relative; }
.menu-toggle {
  width: 40px; height: 40px; padding: 0; margin: 0; border-radius: 8px;
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: #e8ecf1;
  font-size: 1.3rem; line-height: 1; display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.menu-toggle:hover { background: rgba(255,255,255,0.14); }
.menu-toggle[aria-expanded="true"] { background: rgba(63,169,245,0.15); border-color: #3fa9f5; color: #3fa9f5; }
/* Floating card, right-aligned under the toggle so it stays on-screen at
   narrow widths -- position: absolute keeps it out of the topbar's own flex
   flow entirely, so it can never itself cause the topbar to overflow
   horizontally (the concern the topbar's flex-wrap already guards against). */
.menu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 55;
  display: flex; flex-direction: column; gap: 8px;
  width: max-content; min-width: 200px; max-width: calc(100vw - 24px);
  background: #161b24; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5); padding: 10px;
}
.menu-panel button { width: 100%; margin: 0; text-align: left; }
.menu-fit-row {
  display: flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 0.9rem; padding: 2px 4px 10px; margin-bottom: 2px;
  border-bottom: 1px solid rgba(255,255,255,0.1);
}
.menu-fit-row input { width: auto; margin: 0; }

.board-sections { width: 100%; }
.board-section { width: 100%; margin-bottom: 5px; }
.board-section:last-child { margin-bottom: 0; }
.section-heading {
  font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  opacity: 0.6; margin: -5px 0 2px 2px;
}

.grid {
  display: grid;
  --gap: 6px;
  /* Same per-column width as N fixed columns whenever that's >= 100px; below that,
     auto-fill wraps to fewer (wider) columns instead of shrinking tiles further. */
  grid-template-columns: repeat(auto-fill, minmax(max(70px, calc((100% - (var(--cols, 4) - 1) * var(--gap)) / var(--cols, 4))), 1fr));
  gap: var(--gap);
}

/* Fit to screen: rooms flow left-to-right and pack as whole blocks onto shared
   lines ("shelves"), with a tile-width spacer between rooms sharing a shelf,
   instead of one room per row. Shelf assignment is computed in JS (renderGrid,
   best-fit-decreasing) rather than left to native flex-wrap, since wrap only
   does first-fit in DOM order and won't backfill an earlier gap with a later,
   smaller room -- so each .fit-shelf here is a fixed, pre-decided line, not a
   wrapping container. --cols/--tile-px are set in JS since the shared tile
   size has to be computed once from the full board width, then reused across
   each room's independently-sized block -- plain CSS auto-fill/fr can't do that. */
.board-sections.fit-to-screen {
  display: flex;
  flex-direction: column;
  row-gap: 5px;
}
.board-sections.fit-to-screen .fit-shelf {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  /* A "1 tile" spacer needs a normal gap on BOTH sides of that empty column
     (one between the previous room and the empty column, one between the
     empty column and the next room) -- column-gap is a single value for the
     whole space, so it has to include both, or every room after the first
     spacer on a shelf lands 6px short of where the same column lands on a
     shelf with no spacer, and rows stop lining up. */
  column-gap: calc(var(--tile-px, 100px) + 12px);
}
.board-sections.fit-to-screen .board-section {
  width: auto;
  max-width: calc(var(--cols, 4) * var(--tile-px, 100px) + (var(--cols, 4) - 1) * 6px);
  margin-bottom: 0;
}
.board-sections.fit-to-screen .board-section .grid {
  /* Explicit count, not auto-fill: auto-fill needs a definite container size to
     count repetitions, and this grid's own container (the room block) is
     intentionally indefinite (shrink-to-fit) here -- auto-fill collapses to a
     single column in that case. --cols is set per-room in JS to this room's
     own tile count (capped at the board's column count), not the board-wide one. */
  grid-template-columns: repeat(var(--cols, 4), var(--tile-px, 100px));
}

.tile {
  position: relative;
  aspect-ratio: 1;
  min-width: 0;
  min-height: 0;
  border-radius: 14px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  /* Scale with the tile's own rendered size (fit-to-screen sets --tile-px;
     138px is the size these values were originally tuned at, so anything
     without --tile-px set falls back to exactly the old fixed values) --
     otherwise a high column count shrinks the tile but not its padding/gap,
     which eats an increasingly large share of a small tile. */
  gap: calc(var(--tile-px, 138px) * 0.058); padding: calc(var(--tile-px, 138px) * 0.0725); text-align: center;
  cursor: pointer; user-select: none;
  transition: transform 0.1s ease;
  border: 1px solid rgba(255,255,255,0.08);
}
.tile:active { transform: scale(0.96); }
.tile .icon { opacity: 0.95; }
.tile .icon svg {
  /* Overrides the icon's inline width/height (set in JS, meant for contexts
     that aren't size-responsive, like the icon picker) -- CSS wins over SVG
     presentation attributes, so this only takes effect where --tile-px is
     actually in play. */
  width: clamp(14px, calc(var(--tile-px, 138px) * 0.19), 32px);
  height: clamp(14px, calc(var(--tile-px, 138px) * 0.19), 32px);
}
.tile .label { font-size: clamp(0.55rem, calc(var(--tile-px, 138px) * 0.00565), 0.78rem); font-weight: 600; line-height: 1.2; overflow-wrap: anywhere; max-width: 100%; }
.tile .sublabel { font-size: clamp(0.48rem, calc(var(--tile-px, 138px) * 0.00493), 0.68rem); opacity: 0.75; overflow-wrap: anywhere; max-width: 100%; }
.tile.add-tile { background: rgba(255,255,255,0.03); border: 1px dashed rgba(255,255,255,0.25); color: #aab4c0; }

/* healthCheck.DeviceWatch-DeviceStatus === "offline" -- a diagonal hatch over
   the tile's own color, not a replacement for it, so the tile stays
   identifiable while still reading clearly as "not currently reachable." */
.tile.offline::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  --offline-stripe: clamp(4px, calc(var(--tile-px, 138px) * 0.045), 10px);
  background: repeating-linear-gradient(
    45deg,
    transparent,
    transparent var(--offline-stripe),
    rgba(0, 0, 0, 0.35) var(--offline-stripe),
    rgba(0, 0, 0, 0.35) calc(var(--offline-stripe) * 1.7)
  );
  pointer-events: none;
}

.tile.edit-mode { cursor: grab; }
.tile.edit-mode.dragging { opacity: 0.4; }
.tile-edit-btn {
  position: absolute; top: 6px; right: 6px; width: 24px; height: 24px; border-radius: 50%;
  background: rgba(0,0,0,0.35); border: none; color: #fff; font-size: 13px; display: flex;
  align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 0;
}
.tile-battery, .tile-volume {
  position: absolute; top: 6px; left: 6px;
  font-size: 0.65rem; font-weight: 700; line-height: 1.3;
  padding: 2px 6px; border-radius: 10px;
  background: rgba(120,130,140,0.4); color: #dfe3e8;
}
/* Opaque, not the translucent 0.4 the neutral badge above uses -- confirmed
   live: a 40%-opacity yellow/red tint reads fine over a plain dark tile, but
   blends into an indistinct muddy brown on top of a saturated custom tile
   color (e.g. a lock whose "locked" state is mapped to the Stable blue),
   defeating the whole point of a low-battery warning. Solid color guarantees
   it reads as yellow/red regardless of what's under it. */
.tile-battery.low { background: #e0b24a; color: #1a1400; }
.tile-battery.critical { background: #f05a5a; color: #fff; }
.tile-more-btn {
  position: absolute; bottom: 6px; right: 6px; width: 24px; height: 24px; border-radius: 50%;
  background: rgba(0,0,0,0.35); border: none; color: #fff; font-size: 15px; line-height: 1; display: flex;
  align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 0;
}
.device-control { margin-bottom: 18px; }
.device-control-label { font-size: 0.8rem; color: #aab4c0; margin-bottom: 8px; display: flex; justify-content: space-between; }
.lock-buttons { display: flex; gap: 10px; }
.lock-buttons button { flex: 1; margin-bottom: 0; }
.media-transport { display: flex; gap: 10px; margin-bottom: 18px; flex-wrap: wrap; }
.media-transport button { flex: 1; margin-bottom: 0; font-size: 1.1rem; }
/* Playback-mode/home-theater toggles reuse .media-transport's row layout but
   at text-button scale, not the big transport-icon scale -- and need a
   visually distinct "on" state secondary buttons don't otherwise have. */
.media-transport button.active { background: rgba(63,169,245,0.25); border-color: #3fa9f5; color: #bfe0fb; font-size: 0.85rem; }
#media-playmodes button, #media-hometheater button { font-size: 0.85rem; }

/* -- Modal ------------------------------------------------------------------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center;
  justify-content: center; padding: 20px; z-index: 50;
}
.modal {
  width: 100%; max-width: 420px; max-height: 85vh; overflow-y: auto;
  background: #161b24; border: 1px solid rgba(255,255,255,0.12); box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.modal.modal-wide { max-width: 560px; }
.modal h2 { font-size: 1.1rem; margin: 0 0 16px; }
.bulk-room:first-child .bulk-room-heading { margin-top: 0; }
.bulk-room-heading {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
  opacity: 0.6; margin: 14px 0 4px;
}
.bulk-room-heading button { font-size: 0.75rem; text-transform: none; letter-spacing: normal; }
.bulk-row {
  display: flex; align-items: center; gap: 8px; padding: 6px 4px; font-size: 0.88rem;
  border-radius: 6px; cursor: pointer;
}
.bulk-row:hover { background: rgba(255,255,255,0.04); }
.bulk-row input { margin: 0; width: auto; }
.rule-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.rule-row input { margin: 0; padding: 6px 8px; font-size: 0.85rem; }
.rule-row input.rule-word-input { flex: 1; min-width: 0; }
.rule-row input.rule-emoji-input { width: 3.2em; text-align: center; flex: none; }
.rule-row button { margin: 0; white-space: nowrap; }
.icon-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px; margin-bottom: 16px; }
.icon-choice {
  aspect-ratio: 1; border-radius: 8px; display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.05); border: 1px solid transparent; cursor: pointer; color: #e8ecf1;
}
.icon-choice.selected { border-color: #3fa9f5; background: rgba(63,169,245,0.15); }
.color-row { display: flex; gap: 12px; }
.color-row > div { flex: 1; }
.attr-list { list-style: none; padding: 0; margin: 0 0 16px; font-size: 0.85rem; }
.attr-list li { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.attr-list li span:first-child { color: #aab4c0; }

/* -- Idle screensaver --------------------------------------------------- */
#screensaver {
  position: fixed; inset: 0; background: #000; color: #fff; z-index: 100;
  font-family: inherit; cursor: pointer; overflow-y: auto;
  display: flex; flex-direction: column; align-items: center; padding: 32px 16px 48px;
}
/* width/max-width matches .ss-weather below so both share the same center
   and left/right edges on wide screens -- #screensaver's align-items:center
   otherwise centers each child around its own box, and a shrink-wrapped
   .ss-clock (no width) centers on a much narrower box than the 1100px-wide
   .ss-weather, so their edges land nowhere near each other above ~1100px
   viewports even though each looks internally fine on its own. */
.ss-clock { width: 100%; max-width: 1100px; text-align: center; font-size: 3.2rem; font-weight: 700; line-height: 1; }
.ss-date { width: 100%; max-width: 1100px; text-align: center; opacity: 0.6; margin-top: 6px; font-size: 1.1rem; }
.ss-current { width: 100%; max-width: 1100px; margin-top: 18px; display: flex; justify-content: center; align-items: center; gap: 12px; font-size: 1.3rem; }
.ss-current img { width: 48px; height: 48px; }
.ss-current .ss-loc { opacity: 0.6; font-size: 1rem; }
.ss-weather { width: 100%; max-width: 1100px; margin-top: 28px; }
.ss-alert { border-radius: 10px; padding: 12px 16px; margin-bottom: 10px; text-align: left; color: #1a1a1a; font-size: 0.9rem; }
.ss-alert h4 { margin: 0 0 4px; font-size: 1rem; }
.ss-alert p { margin: 0; opacity: 0.85; }
/* Centered, not left -- .ss-weather's own box already shares the clock's
   center (see the comment above .ss-clock), but that only centers the BOX;
   left-aligning content within a box that's centered in a much wider
   viewport still reads as "bunched on the left" with a lot of empty space
   on the right. Title and strip must move together, same reasoning as
   before when a centered/left mismatch between them was the bug. */
.ss-section-title { text-align: center; font-size: 0.85rem; opacity: 0.5; text-transform: uppercase; letter-spacing: 0.05em; margin: 22px 0 8px; }
.ss-hourly-strip, .ss-daily-strip { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.ss-hour-card, .ss-day-card {
  flex: 0 0 auto; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
  border-radius: 10px; padding: 10px 12px; text-align: center;
}
.ss-hour-card { width: 76px; }
.ss-day-card { width: 150px; text-align: left; }
.ss-hour-card img, .ss-day-card img { width: 32px; height: 32px; }
.ss-hour-time { font-size: 0.75rem; opacity: 0.6; }
.ss-hour-temp { font-size: 1.1rem; font-weight: 600; margin: 2px 0; }
.ss-hour-pop { font-size: 0.7rem; color: #6cc2ff; }
.ss-day-name { font-size: 0.85rem; font-weight: 600; }
.ss-day-temp { font-size: 1rem; margin: 4px 0 2px; }
.ss-day-short { font-size: 0.78rem; opacity: 0.7; }
.ss-day-detail { font-size: 0.7rem; opacity: 0.55; margin-top: 6px; line-height: 1.35; }
.ss-radar-wrap { margin-top: 22px; text-align: center; }

/* -- Tiles/Cameras topbar switch + camera dashboard ------------------------- */
.view-toggle {
  width: 40px; height: 40px; padding: 0; margin: 0; border-radius: 8px;
  background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: #e8ecf1;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
}
.view-toggle:hover { background: rgba(255,255,255,0.14); }
.view-toggle.active { background: rgba(63,169,245,0.15); border-color: #3fa9f5; color: #3fa9f5; }

/* width:100% is load-bearing, not decorative -- #app-view is a column flex
   container (see its rule above), and without an explicit width a flex item
   with a max-width can shrink to content size instead of stretching to fill
   the cross axis, which is what actually caused the single-narrow-column
   layout bug (not the grid-template-columns rule below, which was correct
   all along). */
/* flex:1 + min-height:0 down this whole chain is load-bearing: .cameras-grid's
   JS-computed grid-template-rows (layoutCamerasGrid) are 1fr fractions of
   whatever height the grid actually has -- without a real, non-auto height
   flowing all the way down from #app-view, the grid just shrinks to its
   content's own size instead of filling the viewport, which is what actually
   caused the "doesn't fill the screen" bug (not the column math, which was
   already correct). min-height:0 overrides flex's default min-height:auto,
   which otherwise refuses to let a flex child shrink below its content size. */
/* position:fixed, not flex:1 -- this is the third attempt at this box's
   sizing, and every flex/height-percentage approach broke somewhere in the
   ancestor chain (body's min-height:100vh allows growth rather than capping
   it, so nothing forced a hard viewport size back down through #app-view).
   Fixed positioning has zero dependency on any of that: top is set from JS
   (the topbar's own height, the one genuinely dynamic value) and
   left/right/bottom anchor directly to the viewport, so this box's size is
   fully determined by the viewport alone, not by its parents or its own
   content. */
#cameras-view { position: fixed; left: 0; right: 0; bottom: 0; padding: 20px; overflow-y: auto; }
.cameras-grid { width: 100%; height: 100%; display: grid; gap: 10px; }
.camera-card {
  position: relative; border-radius: 14px; overflow: hidden;
  background: #05070a; border: 1px solid rgba(255,255,255,0.1);
}
.camera-card video, .camera-card .camera-snapshot-fallback { width: 100%; height: 100%; object-fit: cover; display: block; background: #05070a; }
.camera-card .label {
  position: absolute; left: 8px; bottom: 8px; background: rgba(0,0,0,.55); padding: 3px 9px;
  border-radius: 6px; font-size: 0.78rem; font-weight: 600; z-index: 1;
}
.camera-card .camera-status-dot {
  position: absolute; top: 8px; right: 8px; width: 8px; height: 8px; border-radius: 50%;
  background: #f5a3a3; box-shadow: 0 0 0 2px rgba(0,0,0,.4); z-index: 1;
  animation: live-pulse 1.4s ease-in-out infinite;
}
.camera-card .camera-status-dot.offline { background: #5a6472; animation: none; }
@keyframes live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.camera-connecting {
  position: absolute; inset: 0; z-index: 1; display: flex; align-items: center; justify-content: center;
  font-size: 0.78rem; opacity: 0.6; margin: 0; text-align: center; padding: 0 12px;
}
.camera-connecting.permanent { opacity: 0.9; font-weight: 600; }
/* UNSUPPORTED_DEVICE_FAMILY is permanent for this device -- Ring's own WHEP
   endpoint refuses it (confirmed live, not a bug in our request). Same
   diagonal-hatch language as .tile.offline elsewhere in this app, so this
   reads as "settled, not stuck" rather than a spinner that never finished. */
.camera-card.unsupported::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: repeating-linear-gradient(
    45deg,
    transparent,
    transparent 8px,
    rgba(0, 0, 0, 0.35) 8px,
    rgba(0, 0, 0, 0.35) 14px
  );
  pointer-events: none;
}
.camera-expand-btn {
  position: absolute; top: 6px; left: 6px; width: 26px; height: 26px; border-radius: 50%;
  background: rgba(0,0,0,0.45); border: none; color: #fff; font-size: 13px; display: flex;
  align-items: center; justify-content: center; cursor: pointer; margin: 0; padding: 0; z-index: 1;
}
.camera-card.expanded {
  position: fixed; inset: 5vh 5vw; z-index: 60; aspect-ratio: unset;
  box-shadow: 0 20px 60px rgba(0,0,0,0.6);
}
.camera-card.dimmed { opacity: 0.25; }
.cameras-grid.has-expanded::before {
  content: ""; position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 59;
}
/* width:100% (actively fills its box), not max-width:100% (only ever caps,
   never grows) -- confirmed live: NWS's station radar loop images have a
   fixed, modest native size, well under this panel's own 1100px width on
   anything bigger than a phone, so max-width alone left it rendering at
   native size with large empty margins on either side instead of using the
   space it already had. height:auto preserves aspect ratio, same as the
   plain img tag always did -- only the width behavior changes. */
.ss-radar-wrap img { width: 100%; height: auto; border-radius: 12px; border: 1px solid rgba(255,255,255,0.1); }
