:root {
  --bg: #0c0e14;
  --panel: #11131a;
  --panel-2: #161a24;
  /* Distinctly lighter surface for OPEN DRAWERS so they read clearly against the
     near-black main screen (the old --panel was almost the same as --bg). */
  --drawer: #222838;
  --drawer-head: #2a3142;
  --line: #232838;
  --text: #e8eaf0;
  --muted: #8b93a7;
  --accent: #5b8cff;
  --user: #2b3450;
  --howie: #161a24;
  --ok: #3fb96b;
  --warn: #e0a030;
  --bad: #e0524a;
  --radius: 16px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

/* iOS long-press popups on CONTROLS (Pete, 2026-08-17: "safari/apple popups are still
   activated"). Holding a button or its glyph — ✕, ⚙, ▲◀■▶▼ — is text-selection or a
   link callout to WebKit, so it pops the selection bubble / share sheet and then eats
   the tap that follows. Suppressed fleet-wide on interactive elements rather than the
   per-widget basis it was on before (rover d-pad + camera PTZ had it; every other
   button did not).

   Deliberately NOT on `*`: chat replies, statements and log text must stay selectable
   so you can copy what Howie said. Controls don't need selection; text does. */
/* touch-action:manipulation is the PER-ELEMENT version of what user-scalable=no did
   globally: it tells iOS "no double-tap-zoom on this element", so a tap fires without
   the ~300ms wait-and-see delay that was swallowing taps — while the PAGE stays
   pinch-zoomable for camera stills and recordings (Pete, 2026-08-18). This is the
   correct lever; v509's viewport hammer was not. */
button, .icon-btn, .chip, .jewel, .tile, .toggle, summary,
[role="button"], input[type="range"], input[type="checkbox"],
label, select, .set-row, .conv-body, .home-tappable, .wx-tab, .sc-tile, .cam-tile {
  touch-action: manipulation;
}

button, .icon-btn, .chip, .jewel, .tile, .toggle, summary,
[role="button"], input[type="range"], input[type="checkbox"],
/* Custom DRAG surfaces. These aren't buttons — an <svg> dial, a <div> scrubber, a
   <span> grab handle — so the control selectors above miss them, and a drag that
   lingers is exactly the gesture iOS reads as "select this". v509 fixed the buttons
   and left these (Pete, 2026-08-17: check live view, settings and recordings too).
   Every one already sets touch-action:none; selection suppression belongs with it. */
.rec-timeline, .tstat-dial, .cam-drag-handle {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
html, body { height: 100%; margin: 0; }
body {
  background: var(--bg);
  color: var(--text);
  font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overscroll-behavior: none;
}
.hidden { display: none !important; }

/* --- Name gate --- */
.gate {
  position: fixed; inset: 0;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
  background: radial-gradient(120% 80% at 50% 0%, #1a2030 0%, var(--bg) 60%);
}
.gate-card {
  width: 100%; max-width: 360px; text-align: center;
  background: var(--panel); border: 1px solid var(--line);
  border-radius: 20px; padding: 32px 24px;
}
.logo {
  font-size: 30px; font-weight: 700; letter-spacing: -0.5px;
  background: linear-gradient(90deg, #7aa2ff, #b388ff);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.gate-sub { color: var(--text); margin: 10px 0 22px; }
.gate-note { color: var(--muted); font-size: 13px; margin-top: 18px; }
#register-form { display: flex; gap: 8px; }
#name-input {
  flex: 1; padding: 13px 14px; font-size: 16px;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 12px; color: var(--text);
}
#register-form button, #send-btn {
  border: none; background: var(--accent); color: #fff; font-weight: 600;
  border-radius: 12px; padding: 0 18px; font-size: 16px; cursor: pointer;
}

/* --- App layout --- */
.app {
  display: flex; flex-direction: column; height: 100%;
  max-width: 1100px; margin: 0 auto; width: 100%;
}
/* Phone-first: stack the header into 3 rows (title+new / status / icons) so a
   narrow screen doesn't scrunch everything onto one line. */
.topbar {
  display: flex; flex-direction: column; gap: 8px;
  /* Top padding clears the iOS status bar / Dynamic Island in standalone PWA mode
     (black-translucent status bar = content renders UNDER the island). Use the
     safe-area inset, but floor it generously so the first row never tucks under
     the island on phones that report a small/zero inset. */
  padding: calc(env(safe-area-inset-top, 0px) + 16px) 16px 12px;
  background: var(--panel); border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 5;
}
.topbar-row { display: flex; align-items: center; gap: 8px; }
.topbar-title-row { justify-content: space-between; }
.topbar-icons { gap: 10px; }
.title { font-weight: 800; font-size: 28px; letter-spacing: -0.5px; }
/* The jewel strip hugs the title; auto margin takes the slack so ＋ stays far right. */
.jewel-strip { margin-left: 14px; margin-right: auto; }
.icon-btn {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  width: 60px; height: 60px; border-radius: 15px; font-size: 34px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex: none; padding: 0;
}
.icon-btn:active { background: var(--line); }
#new-btn { font-weight: 600; }
.status-strip { display: flex; gap: 10px; flex-wrap: wrap; transition: opacity .3s; }
/* Live status feed (SSE) dropped — dim the strip so it's clear updates are paused
   (not that the house is quiet). Cleared the instant the feed reconnects. */
.status-strip.feed-stale { opacity: .45; }

/* Desktop/tablet: keep the SAME stacked header as mobile (separate rows for
   title, status chips, icons) — it renders cleanly and avoids scrunching
   everything onto one line. The app just gets wider (see .app max-width). */

/* --- History drawer --- */
.drawer { position: fixed; inset: 0; z-index: 20; }
.drawer-scrim { position: absolute; inset: 0; background: rgba(0,0,0,.5); z-index: 1; }

/* Centered modal popup (e.g. the thermostat dial from a shortcut). Stays until
   dismissed (✕ / scrim tap / Esc). */
.modal-back {
  position: fixed; inset: 0; z-index: 40; background: rgba(0,0,0,.55);
  display: flex; align-items: center; justify-content: center; padding: 18px;
}
.modal-card {
  width: min(92%, 360px); background: var(--drawer); border: 1px solid var(--line);
  border-radius: 18px; padding: 14px 16px 18px;
  box-shadow: 0 18px 50px rgba(0,0,0,.5);
}
.modal-head { display: flex; align-items: center; justify-content: space-between; }
.modal-title { font-size: 17px; font-weight: 600; color: var(--text); }
.modal-body { margin-top: 4px; }
.drawer-panel {
  /* Near-full width by default (was min(86%, 340px), too narrow — statements/rows wrapped
     badly). On a phone this fills almost the whole screen (the scrim still peeks so it
     reads as a drawer); capped on tablet/desktop. Drawers that need MORE (home/cam/report)
     keep their own wider override; this base now covers memory, scenes, settings,
     batteries, doors & locks, and history in one place. */
  position: absolute; top: 0; left: 0; bottom: 0; width: min(94%, 540px);
  background: var(--drawer); border-right: 1px solid var(--line);
  box-shadow: 0 0 40px rgba(0,0,0,.6);   /* lift it off the main screen */
  display: flex; flex-direction: column; z-index: 2;   /* above the scrim — taps in the panel never fall through */
  /* Same island/notch clearance as the topbar: actual safe-area inset PLUS a
     floor, so the drawer head (title + gear/refresh) never tucks under the iOS
     Dynamic Island in standalone mode. */
  padding: calc(env(safe-area-inset-top, 0px) + 16px) 0 max(env(safe-area-inset-bottom), 14px);
  animation: slidein .18s ease;
}
@keyframes slidein { from { transform: translateX(-100%); } to { transform: none; } }
/* Two-row header (Pete, 2026-07-06): the TITLE gets its own full-width line above,
   and the jewel strip sits CENTERED on the line below it — so a long title no longer
   truncates fighting the jewels + ✕ for horizontal room (vertical space is cheap;
   the drawer scrolls). The ✕ is absolutely pinned top-right, out of the flow, so it's
   always reachable regardless of either row's width. */
.drawer-head {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px; padding: 0 14px 12px; position: relative;
  font-weight: 700; border-bottom: 1px solid var(--line);
}
/* Title — full width, its own line, centered, no truncation now that it owns the row.
   Right padding leaves room for the pinned ✕ so a long centered title doesn't run under it. */
.drawer-title { font-size: 22px; font-weight: 800; letter-spacing: -0.4px;
  flex: 1 1 100%; text-align: center; padding: 0 34px; box-sizing: border-box; }
/* (jewel-strip-drawer's full-width centering lives with the jewel styles below.) */
/* The close ✕ — pinned top-right, always reachable, never in the flow. */
.drawer-head > .icon-btn { position: absolute; top: 0; right: 10px; flex: 0 0 auto; }
/* Home Status sub-header: action icons (gear, refresh) on their own row beneath
   the title+close, with the freshness age trailing. */
.home-subhead {
  display: flex; align-items: center; gap: 10px; padding: 10px 14px 4px;
}
.home-subhead .home-age { margin-left: auto; }
.drawer-new {
  margin: 12px 14px; padding: 11px; border-radius: 12px; cursor: pointer;
  background: var(--accent); color: #fff; border: none; font-size: 15px; font-weight: 600;
}
.conv-list { list-style: none; margin: 0; padding: 0 8px; overflow-y: auto; flex: 1; }
.conv-item {
  display: flex; align-items: center; gap: 6px;
  padding: 11px 8px 11px 12px; border-radius: 11px; margin-bottom: 4px;
}
.conv-item:active, .conv-item:hover { background: var(--panel-2); }
.conv-item.active { background: var(--panel-2); border: 1px solid var(--line); }
.conv-body { flex: 1; min-width: 0; cursor: pointer; }
.conv-del {
  flex: none; background: none; border: none; color: var(--muted);
  font-size: 15px; line-height: 1; padding: 6px; border-radius: 8px; cursor: pointer;
}
.conv-del:active, .conv-del:hover { background: var(--line); color: #fff; }
.conv-title { font-size: 14px; line-height: 1.35; }
.conv-meta { font-size: 11px; color: var(--muted); margin-top: 3px; }
.conv-summary { font-size: 12px; color: var(--muted); margin-top: 5px; line-height: 1.35; font-style: italic; }
.conv-empty { color: var(--muted); font-size: 14px; padding: 16px 14px; }

/* --- Memory drawer (what Howie knows about you) --- */
.mem-note { color: var(--muted); font-size: 12px; line-height: 1.4; margin: 10px 14px 6px; }
.mem-list { list-style: none; margin: 0; padding: 0 8px 8px; overflow-y: auto; flex: 1; }
.mem-item {
  padding: 11px 12px; border-radius: 11px; margin-bottom: 6px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.mem-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.mem-key { font-size: 13px; font-weight: 600; }
.mem-tag {
  font-size: 10px; text-transform: uppercase; letter-spacing: .03em;
  padding: 1px 6px; border-radius: 999px; background: var(--line); color: var(--muted);
}
.mem-tag.noticed { background: #4a3a12; color: #e8c97a; }
.mem-val { font-size: 14px; line-height: 1.35; margin-top: 4px; }
.mem-actions { display: flex; gap: 8px; margin-top: 8px; }
/* "What Howie remembers" drawer: category filter chips + per-item meta line. */
.hmem-filters { display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 14px 8px; }
.hmem-filter {
  font-size: 12px; padding: 5px 11px; border-radius: 999px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
}
.hmem-filter.is-sel { background: #14414f; border-color: #2a7d94; color: #eaf6fa; }
.hmem-meta { font-size: 11px; color: var(--muted); margin-top: 4px; }
.mem-edit, .mem-del {
  font-size: 12px; padding: 4px 10px; border-radius: 8px; cursor: pointer;
  background: none; border: 1px solid var(--line); color: var(--muted);
}
.mem-edit:hover { color: #fff; border-color: var(--accent); }
.mem-del:hover { color: #fff; border-color: #b3433a; }
.mem-editor {
  width: 100%; margin-top: 6px; box-sizing: border-box; resize: vertical;
  background: var(--panel); color: inherit; border: 1px solid var(--accent);
  border-radius: 8px; padding: 8px; font: inherit;
}
.mem-editbar { display: flex; align-items: center; gap: 8px; margin-top: 6px; }
.mem-save { font-size: 12px; padding: 5px 12px; border-radius: 8px; cursor: pointer;
  background: var(--accent); color: #fff; border: none; }
.mem-cancel { font-size: 12px; padding: 5px 12px; border-radius: 8px; cursor: pointer;
  background: none; border: 1px solid var(--line); color: var(--muted); }
.mem-msg { font-size: 12px; color: #e8a07a; }
/* Icon chips match the icon-btn size so the whole top bar reads as one set of
   equal-sized controls (52px box, 30px glyph). Width grows for the count digit. */
.chip {
  height: 60px; min-width: 60px; box-sizing: border-box;
  padding: 0 14px; border-radius: 15px; font-size: 34px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  white-space: nowrap;
  display: inline-flex; align-items: center; justify-content: center; flex: none;
}
.chip.ok { color: var(--ok); border-color: #1e3a2a; }
.chip.warn { color: var(--warn); border-color: #3a2f18; }
.chip.bad { color: var(--bad); border-color: #3a1f1c; }

/* Shortcuts accessor chip (slot #1 of the status strip): same 60x60 box as the other
   chips, with the gold star over a small "Shortcuts" label so the row never overflows.
   Opens the Shortcuts drawer. */
.shortcuts-chip {
  flex-direction: column; gap: 1px; padding: 0 6px;
}
.shortcuts-chip .sc-chip-ic { line-height: 0; }
.shortcuts-chip .sc-chip-ic .star-ico { width: 26px; height: 26px; }
.shortcuts-chip .sc-chip-lbl {
  font-size: 9px; font-weight: 600; color: var(--muted);
  max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shortcuts-chip:active { transform: scale(0.96); }

/* Shortcuts drawer: WIDER panel than the standard drawer so a 3-per-row grid of tiles
   fits without bleeding/overflowing into a mini horizontal scroll. */
.sc-panel { width: min(94%, 540px); }
/* Shortcuts-only: pull the tile grid up close under the title. The shared .drawer-head
   reserves a generous 12px below the title (right for list drawers); here we want the
   first tile row to start almost immediately, so tighten the head's bottom padding for
   THIS drawer only. Combined with the small .sc-body top padding, this reclaims most of
   a row's worth of gap. */
.sc-panel .drawer-head { padding-bottom: 6px; }
/* A grid of pinned device/scene tiles (big icon over name). minmax(0,1fr) lets the
   columns shrink to the available width instead of overflowing; the body never scrolls
   sideways. */
.sc-body {
  /* Tight top gap so tiles start right under the title. NOTE: no safe-area-inset-top
     here — the .drawer-panel already adds that notch clearance ABOVE the title, so
     repeating it on the body (which sits BELOW the header) double-counted the inset and
     was the bulk of the "almost a full row" gap under the title on notched iPhones. */
  padding: 6px 14px 18px;
  overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
}
.sc-grid {
  /* 5 columns: square tiles small enough that many shortcuts fit in one view, but ~25%
     bigger than the 6-col pass (there's headroom on a standard iOS width) so they're
     easier to see and tap — icon/label/padding scaled up to match. The "Frequently Used"
     section at the top uses the same grid so freq-used and pinned tiles look equivalent. */
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px;
}
/* Room heading above each group of pinned shortcut tiles. Tight vertical margins so the
   per-room headers don't eat the space we're trying to reclaim for extra tile rows. */
.sc-roomlabel {
  font-size: 13px; font-weight: 700; color: var(--text);
  margin: 10px 2px 5px; padding-bottom: 3px; border-bottom: 1px solid var(--line);
}
.sc-roomlabel:first-child { margin-top: 0; }
.sc-tile {
  position: relative; box-sizing: border-box; aspect-ratio: 1 / 1; min-width: 0;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px;
  color: var(--text); cursor: pointer; padding: 4px 3px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; transition: transform .08s, border-color .2s;
}
.sc-tile:active { transform: scale(0.95); }
.sc-tile.off .sc-ic { opacity: .38; filter: grayscale(0.5); }
.sc-tile .sc-ic { font-size: 22px; line-height: 1; }
.sc-tile .sc-ic .fixture-ico, .sc-tile .sc-ic .lock-ico,
.sc-tile .sc-ic .shade-ico, .sc-tile .sc-ic .nest-ico { width: 25px; height: 25px; }
.sc-tile .sc-lbl {
  font-size: 11px; max-width: 100%; text-align: center; line-height: 1.1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted);
}
.sc-tile .sc-unpin {
  position: absolute; top: 0; right: 2px; font-size: 11px; line-height: 1;
  color: var(--muted); opacity: .55; padding: 2px; border-radius: 7px;
}
.sc-tile .sc-unpin:active { background: var(--line); opacity: 1; }
.sc-empty { color: var(--muted); font-size: 15px; text-align: center; padding: 32px 12px; }
.sc-empty-sub { font-size: 13px; opacity: .8; }
.sc-hint { color: var(--muted); font-size: 12px; text-align: center; opacity: .7;
  padding: 16px 12px 4px; }

/* ★ Pin toggle inside a Home Status device control panel. */
.home-btn.pin-btn.pinned { color: #e8c15a; border-color: #5a4d22; }

/* ★ pin beside a control-panel TITLE (Pete, 2026-07-30). Replaces the labelled
   "☆ Pin / ★ Pinned" button in the button row: the star IS the control, so there is no
   label. Always the FILLED glyph — only the COLOUR changes, so the shape never shifts as
   you toggle. Gold = pinned, blue = not, matching the labelled button's gold (#e8c15a)
   so both readings of "pinned" agree wherever they appear. */
.pin-star {
  flex: 0 0 auto; background: none; border: 0; padding: 0 10px 0 0; cursor: pointer;
  font-size: 20px; line-height: 1; color: var(--accent, #3a7afe);
  -webkit-tap-highlight-color: transparent;
}
.pin-star.pinned { color: #e8c15a; }
.pin-star:disabled { opacity: .5; cursor: default; }

/* --- Device-chip row (Sprint 28): a 4th header row for individual devices that open
   their own drawer, distinct from the section-icon row above. Each is an ICON-ONLY chip
   the same size as the section icons; the vacuum is position 1, the row grows over time. */
.device-chips { gap: 10px; flex-wrap: wrap; align-items: center; }
.vacuum-chip .vac-chip-ic { line-height: 0; display: inline-flex; }
/* Fill the 60×60 icon-btn like the section icons (which use a 50px glyph). */
.vacuum-chip .vac-chip-ic .vac-ico { width: 50px; height: 50px; }
/* State cues on the icon-button: a coloured ring while cleaning (warn) or on error (bad);
   plain otherwise; dim + greyed when offline. Keeps it the same footprint as the others. */
.vacuum-chip.warn { color: #e0a030; }
.vacuum-chip.bad  { color: #e0524a; }
.vacuum-chip.off  { opacity: .45; }
.vacuum-chip.off .vac-ico { filter: grayscale(0.7); }

/* Rover chip — same footprint/state cues as the vacuum. */
.rover-chip .rover-chip-ic { line-height: 0; display: inline-flex; }
.rover-chip .rover-chip-ic .rover-ico { width: 50px; height: 50px; }
.rover-chip.warn { color: #e0a030; }
.rover-chip.bad  { color: #e0524a; }
.rover-chip.off  { opacity: .45; }
.rover-chip.off .rover-ico { filter: grayscale(0.7); }

/* Sprinklers (Rachio) chip — same footprint/state cues. warn = actively watering (its
   the water drop gently pulses), bad = API key rejected, off = offline. */
.rachio-chip .rachio-chip-ic { line-height: 0; display: inline-flex; }
.rachio-chip .rachio-chip-ic .rachio-ico { width: 50px; height: 50px; }
.rachio-chip.warn { color: #2fa3c7; }
.rachio-chip.warn .rachio-drops { animation: rachio-pulse 1.6s ease-in-out infinite; }
.rachio-chip.bad  { color: #e0524a; }
.rachio-chip.off  { opacity: .45; }
.rachio-chip.off .rachio-ico { filter: grayscale(0.7); }
@keyframes rachio-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }

/* Dishwasher "Cookie" chip (Home Connect). Stainless body is fixed gray in the SVG; the
   fork + spoon use currentColor so they tint per state — idle steel-gray, running blue-green
   (pulsing), finished green. */
.dishwasher-chip .dishwasher-chip-ic { line-height: 0; display: inline-flex; }
.dishwasher-chip .dishwasher-chip-ic .dw-ico { width: 50px; height: 50px; }
.dishwasher-chip { color: #5f646b; }                    /* idle utensils = steel gray */
.dishwasher-chip.dw-running { color: #2fa3c7; }
.dishwasher-chip.dw-running .dw-utensil { animation: rachio-pulse 1.8s ease-in-out infinite; }
.dishwasher-chip.dw-done { color: #46b26a; }

/* --- Door + status light (v405). Three states, and they can't overlap: the appliance
   won't run with the door open, so RUNNING always implies CLOSED (Pete, 2026-07-26).
     open     -> door down, light off
     ready    -> door up, light STEADY ("left light on")
     running  -> door up, light BLINKING
   The door and light live in the SVG always; CSS shows/hides them, so a state change
   never re-renders the icon (it's painted once, see setDishwasherChip). --- */
.dishwasher-chip .dw-door { display: none; }
.dishwasher-chip.dw-open .dw-door { display: block; }
/* Door open = the tub is exposed; dim the utensils behind it so the door reads as in front. */
.dishwasher-chip.dw-open .dw-utensil { opacity: 0.45; }

/* Status light: off by default, steady when ready, blinking while running. A distinct
   blink (not the utensils' soft pulse) — it should read as an appliance indicator LED. */
.dishwasher-chip.dw-ready .dw-light { opacity: 1; }
.dishwasher-chip.dw-running .dw-light { animation: dw-blink 1.4s steps(1, end) infinite; }
@keyframes dw-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0.08; } }
@media (prefers-reduced-motion: reduce) {
  /* Respect reduced-motion: hold the light ON rather than flashing, and drop the pulse.
     Running is still distinguishable from ready by the chip's blue-green tint + tooltip. */
  .dishwasher-chip.dw-running .dw-light { animation: none; opacity: 1; }
  .dishwasher-chip.dw-running .dw-utensil { animation: none; }
}

/* Weather chip — same footprint/state cues as the vacuum + rover. warn = high wind,
   bad = lightning nearby; a subtle pulse on bad so a strike catches the eye. */
.weather-chip { position: relative; }          /* anchor for the temp overlay */
.weather-chip .wx-chip-ic { line-height: 0; display: inline-flex; }
.weather-chip .wx-chip-ic .wx-ico { width: 50px; height: 50px; }
/* Current temp overlaid on the icon so it reads at a glance without opening the drawer.
   Transparent — white text with a soft shadow for contrast on any glyph colour, so it
   never occludes the SVG behind it. Pinned over the lower part of the icon. */
.weather-chip .wx-chip-temp {
  position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%);
  font-size: 13px; font-weight: 800; line-height: 14px; text-align: center;
  color: #fff; pointer-events: none;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
  text-shadow: 0 1px 2px rgba(0,0,0,.75), 0 0 3px rgba(0,0,0,.6);   /* legible on any bg */
}
.weather-chip .wx-chip-temp:empty { display: none; }   /* hide until there's a reading */
.weather-chip.warn { color: #e0a030; }
.weather-chip.bad  { color: #e0524a; animation: wxpulse 1.6s ease-in-out infinite; }
.weather-chip.off  { opacity: .45; }
@keyframes wxpulse {
  0%, 100% { filter: drop-shadow(0 0 0 rgba(224,82,74,0)); }
  50%      { filter: drop-shadow(0 0 5px rgba(224,82,74,0.6)); }
}

/* Garage chip — PERMANENT (always shown). Icon swaps open/closed; amber tint while the
   door is open (a door left open matters); dim when state is unknown. */
.garage-chip .garage-chip-ic { line-height: 0; display: inline-flex; }
.garage-chip .garage-chip-ic .garage-ico { width: 50px; height: 50px; }
.garage-chip.warn { color: #e0a030; }        /* door OPEN */
.garage-chip.unknown { opacity: .5; }

/* Bed chip (moved into the device-chips row 2026-07-25 so it reflows per user). */
.bed-chip .bed-chip-ic { line-height: 0; display: inline-flex; }
.bed-chip .bed-chip-ic .bed-ico { width: 50px; height: 50px; }

/* Rover drawer */
.rover-panel { width: min(96%, 640px); }
.rover-live-stage {
  position: relative; background: #000; aspect-ratio: 4 / 3;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.rover-live-stage img, .rover-live-stage video {
  width: 100%; height: 100%; object-fit: contain; display: block;
}
.rover-live-stage img[hidden], .rover-live-stage video[hidden] { display: none; }
.rover-live-note {
  position: absolute; left: 8px; bottom: 8px; font-size: 12px; color: #fff;
  background: rgba(0,0,0,.5); padding: 2px 8px; border-radius: 10px; pointer-events: none;
}
.rover-actions { display: flex; gap: 8px; align-items: center; padding: 10px 14px 4px; }
.rover-btn {
  flex: 1 1 0; padding: 9px 10px; border-radius: 10px; font-size: 15px;
  border: 1px solid var(--line); background: var(--panel); color: var(--fg); cursor: pointer;
}
.rover-btn:active { transform: scale(0.96); border-color: var(--accent); }
.rover-btn-ic { display: inline-flex; vertical-align: middle; line-height: 0; }
.rover-btn-ic .rover-caps-ico { width: 18px; height: 18px; }
.rover-btn.recording { background: #c0392b; border-color: transparent; color: #fff; }
.rover-msg { font-size: 13px; color: var(--muted); min-width: 60px; }
.rover-msg.hidden { display: none; }
/* Telemetry readout bar: FIRST under the header (below the jewels), CENTERED. A row of
   compact icon tiles (battery/link/motion/heading/temps) that scrolls horizontally if it
   can't fit rather than wrapping raggedly. */
.rover-status {
  display: flex; gap: 8px; padding: 10px 14px; overflow-x: auto;
  justify-content: center;                 /* centered in the drawer */
  -webkit-overflow-scrolling: touch;
}
.rover-tile {
  flex: 0 0 auto; min-width: 60px; display: flex; flex-direction: column; align-items: center;
  gap: 2px; padding: 8px 6px; border-radius: 12px; background: var(--panel);
  border: 1px solid var(--line);
}
.rover-tile-ic  { line-height: 0; display: inline-flex; }
.rover-tile-svg { width: 26px; height: 26px; display: block; }
.rover-tile-val { font-size: 14px; font-weight: 600; color: var(--fg); }
.rover-tile-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.rover-tile.warn { border-color: #e0a030; }
.rover-tile.warn .rover-tile-val { color: #e0a030; }
.rover-tile.bad  { border-color: #e0524a; }
.rover-tile.bad  .rover-tile-val { color: #e0524a; }
.rover-offline { padding: 12px 16px; color: var(--muted); font-size: 14px; text-align: center; }

/* Video start/stop toggle — floats bottom-right over the live stage. */
.rover-video-toggle {
  position: absolute; right: 8px; top: 8px; z-index: 2;
  padding: 6px 12px; border-radius: 999px; font-size: 13px; cursor: pointer;
  border: 1px solid rgba(255,255,255,.35); background: rgba(0,0,0,.55); color: #fff;
}
.rover-video-toggle.on { background: rgba(192,57,43,.85); border-color: transparent; }
/* Captures grid (now the body of the captures drawer): a responsive tile grid. */
.rover-gallery {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px; padding: 14px 16px 20px;
}
.rover-cap {
  aspect-ratio: 4 / 3; border-radius: 10px; overflow: hidden; background: #222;
  display: flex; align-items: center; justify-content: center; text-decoration: none;
  border: none; padding: 0; cursor: pointer;
}
.rover-cap img { width: 100%; height: 100%; object-fit: cover; }
.rover-cap-vid { font-size: 30px; }
.rover-gallery-empty { padding: 16px; color: var(--muted); font-size: 14px; grid-column: 1 / -1; text-align: center; }

/* Rover drive pad: a 3×3 grid D-pad (▲ / ◀ ■ ▶ / ▼) + a speed slider. Buttons are
   HOLD-to-drive; .pressed cues the active one. touch-action:none so a hold doesn't
   scroll/select on mobile. */
/* Drive block: CENTERED under the video — the D-pad + a vertical speed slider beside it. */
.rover-drive { display: flex; align-items: center; justify-content: center; gap: 22px; padding: 14px 16px 8px; }
.rover-dpad {
  display: grid; grid-template-columns: repeat(3, 52px); grid-template-rows: repeat(3, 52px);
  gap: 6px; touch-action: none;
  /* iOS: a HOLD on these buttons must NOT trigger the text-selection highlight, the
     magnifier loupe, or the callout (copy/look up/translate) menu — they hijack the
     press-and-hold drive. Suppress all of it (needs the -webkit- prefixes on iOS). */
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.rover-speed-col { display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* Reuse the shared vertical-slider look; a touch shorter than the bed's. */
.rover-speed.bed-vslider { height: 120px; width: 40px; }
.rover-speed-lbl { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.rover-dpad-btn {
  font-size: 20px; border-radius: 10px; border: 1px solid var(--line);
  background: var(--panel); color: var(--fg); cursor: pointer;
  display: flex; align-items: center; justify-content: center; touch-action: none;
  /* Full iOS long-press suppression (callout menu + magnifier + selection). The button
     glyphs (▲◀■▶▼) are still selectable text without these, so a hold pops the menu. */
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.rover-dpad-btn.pressed { background: var(--accent); border-color: transparent; color: #fff; }
/* The 3x3 grid always had four empty corners; the diagonals fill them, so the pad
   gains finer steering without growing. Corner glyphs are smaller — the triangles read
   heavier than the arrows at the same size. */
.rover-dpad-fwdl  { grid-column: 1; grid-row: 1; font-size: 15px; }
.rover-dpad-fwd   { grid-column: 2; grid-row: 1; }
.rover-dpad-fwdr  { grid-column: 3; grid-row: 1; font-size: 15px; }
.rover-dpad-left  { grid-column: 1; grid-row: 2; }
.rover-dpad-stop  { grid-column: 2; grid-row: 2; background: #c0392b; border-color: transparent; color: #fff; }
.rover-dpad-right { grid-column: 3; grid-row: 2; }
.rover-dpad-backl { grid-column: 1; grid-row: 3; font-size: 15px; }
.rover-dpad-back  { grid-column: 2; grid-row: 3; }
.rover-dpad-backr { grid-column: 3; grid-row: 3; font-size: 15px; }

/* Follow mode: a STATE toggle, not a momentary action like the buttons beside it, so it
   has to look latched when it's on — the rover is moving on its own and that must be
   obvious at a glance. Pulses while engaged. */
.rover-follow[aria-pressed="true"] {
  background: var(--accent); border-color: transparent; color: #fff;
  animation: rover-follow-pulse 2s ease-in-out infinite;
}
.rover-follow:disabled { opacity: .55; cursor: default; }
@keyframes rover-follow-pulse { 0%,100% { opacity: 1; } 50% { opacity: .72; } }
@media (prefers-reduced-motion: reduce) { .rover-follow[aria-pressed="true"] { animation: none; } }

/* Rover captures drawer (the file grid) + the single-capture lightbox viewer. */
.rover-caps-panel { width: min(96%, 640px); }
.rover-cap-panel { width: min(96%, 720px); }
.rover-cap-stage {
  background: #000; display: flex; align-items: center; justify-content: center;
  max-height: 70vh; overflow: hidden;
}
.rover-cap-stage img, .rover-cap-stage video { max-width: 100%; max-height: 70vh; object-fit: contain; }
.rover-cap-actions { display: flex; gap: 10px; padding: 12px 16px; }
.rover-cap-actions .rover-btn { text-align: center; text-decoration: none; line-height: 1.6; }
.rover-cap-del { background: #7a2018; border-color: transparent; color: #fff; }

/* Vacuum drawer: the LIDAR floor map + a status line. */
/* ---- Weather drawer → Tempest DASHBOARD (Sprint 30, modeled on the console) ----
   The drawer is a fixed-height instrument dashboard: header (day/date/clock/
   lightning) → 2×2 ring gauges → stat chips → compact solar/baro → daylight arc +
   moon. Bands flex to FILL the drawer height with NO scroll. Two tabs: Now (live) ·
   Forecast (stub → Tempest Better Forecast later). Gauge hues are their own tokens
   (semantic to the reading), separate from the app --accent. */
:root {
  --wx-temp: #ff7a1a; --wx-temp-lo: #4f7dff; --wx-humid: #4f7dff;
  --wx-wind: #23d1c0; --wx-rain: #2fa8ff; --wx-sun: #ffcf3f;
  --wx-uv1: #3fb96b; --wx-uv2: #c9d13a; --wx-uv3: #e0a030; --wx-uv4: #e0524a;
}
/* Device Settings drawer (Sprint 31): a narrow read-only info panel. */
.dev-panel { width: min(96%, 460px); }
.dev-body { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 14px 16px; }
.dev-sec {
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); margin: 16px 0 6px;
}
.dev-sec:first-child { margin-top: 6px; }
.dev-row {
  display: flex; justify-content: space-between; gap: 14px;
  padding: 9px 2px; border-bottom: 1px solid var(--line);
}
.dev-k { color: var(--muted); font-size: 14px; flex: 0 0 auto; }
.dev-v { color: var(--text); font-size: 14px; font-weight: 600; text-align: right; word-break: break-word; }
.dev-v.dev-na { color: var(--muted); font-weight: 400; font-style: italic; }
.dev-err { color: var(--muted); font-size: 14px; padding: 30px 8px; text-align: center; }

/* Camera recording / detection controls (Sprint 42) — the interactive half of the ⚙
   panel. Rows match .dev-row's geometry so the section reads as part of the same list,
   but the value side is a switch instead of text. */
.cam-det-row {
  display: flex; justify-content: space-between; align-items: center; gap: 14px;
  padding: 9px 2px; border-bottom: 1px solid var(--line);
}
.cam-det-label { color: var(--text); font-size: 14px; }
.cam-det-row.busy { opacity: .55; }          /* in flight — a camera write takes a beat */
.cam-det-sw {
  position: relative; flex: 0 0 auto; width: 46px; height: 26px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
  -webkit-tap-highlight-color: transparent;
}
.cam-det-sw::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
  border-radius: 50%; background: var(--muted); transition: transform .15s ease, background .15s ease;
}
.cam-det-sw.on { background: var(--accent, #3a7afe); border-color: transparent; }
.cam-det-sw.on::after { transform: translateX(20px); background: #fff; }
.cam-det-sw:disabled { cursor: default; }
/* One detector = its switch plus (where the camera has one) its own sensitivity slider,
   grouped so they read as ONE decision rather than two lists to match up by label.
   The divider belongs at the GROUP's edge: leaving it on the inner rows would draw a line
   between a toggle and its own slider — exactly the split this grouping removes. */
.cam-det-group { border-bottom: 1px solid var(--line); }
.cam-det-group > .cam-det-row,
.cam-det-group > .cam-sens-row { border-bottom: 0; }
.cam-det-group > .cam-sens-row { padding-top: 0; padding-left: 12px; }
/* Sensitivity sliders. step=10 matches the camera's own granularity, so the control can't
   promise a precision the firmware will quietly round away. */
.cam-sens-row { padding: 9px 2px 12px; border-bottom: 1px solid var(--line); }
.cam-sens-row.busy { opacity: .55; }
.cam-sens-top { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.cam-sens-val { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.cam-sens {
  -webkit-appearance: none; appearance: none; width: 100%; margin-top: 9px;
  height: 4px; border-radius: 999px; background: var(--panel-2); outline: none;
}
.cam-sens::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
  border-radius: 50%; background: var(--accent, #3a7afe); cursor: pointer;
  border: 2px solid var(--panel, #12141a);
}
.cam-sens::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; background: var(--accent, #3a7afe);
  cursor: pointer; border: 2px solid var(--panel, #12141a);
}
.cam-det-retry {
  margin-top: 8px; padding: 7px 14px; border-radius: 8px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); font-size: 13px;
}
.dev-note { color: var(--muted); font-size: 12px; padding: 8px 2px 0; line-height: 1.45; }
/* The NVR's own words on a failed read — admin-only, and deliberately quieter than the
   plain-language line above it: it names internals and is for diagnosing, not deciding. */
.dev-detail {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; opacity: .75; padding-top: 4px; word-break: break-word;
}
.cam-det-err { padding: 10px 2px 0; text-align: left; color: #e0736f; }

/* max-width caps the panel on tablet/desktop; overflow-x:hidden is the hard backstop
   so NOTHING in the weather tabs can bleed past the panel edge (the iPhone "floating
   frame" + horizontal scroll). Vertical scroll still works (it's on #wx-tabbody). */
.wx-panel { width: min(96%, 640px); max-width: 100%; overflow-x: hidden; }
/* Body fills the panel; tabs stay pinned, the tab content SCROLLS so every band sits at
   its natural size without distortion (Pete: allow scroll, don't cram to one screen). */
.wx-body { flex: 1; min-height: 0; min-width: 0; display: flex; flex-direction: column;
  padding: 10px 12px 4px; overflow: hidden; }
.wx-empty { color: var(--muted); font-size: 15px; padding: 40px 8px; text-align: center; }

/* tabs */
.wx-tabs { display: flex; gap: 6px; padding: 0 0 8px; }
.wx-tab { flex: 1; text-align: center; padding: 7px 4px; border-radius: 9px;
  cursor: pointer; font-size: 12px; font-weight: 700; letter-spacing: .3px;
  color: var(--muted); background: var(--panel-2); border: 1px solid var(--line);
  user-select: none; }
.wx-tab.active { color: var(--text); background: var(--panel);
  border-color: var(--accent); box-shadow: inset 0 0 0 1px rgba(91,140,255,.25); }
.wx-tab .sub { display: block; font-size: 9px; font-weight: 600; color: var(--muted); margin-top: 1px; opacity: .8; }
#wx-tabbody { flex: 1; min-height: 0; min-width: 0; display: flex; overflow-y: auto;
  /* Vertical scroll is fine; NEVER let a wide child force horizontal scroll +
     the "floating frame" on a narrow iPhone. min-width:0 lets grid tracks shrink
     below their intrinsic content width; overflow-x:hidden is the backstop. */
  overflow-x: hidden; -webkit-overflow-scrolling: touch; }

/* forecast stub */
.wx-forecast-soon { flex: 1; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; gap: 8px; color: var(--muted); }
.wx-soon-ic { font-size: 44px; }
.wx-soon-t { font-size: 17px; font-weight: 800; color: var(--text); }
.wx-soon-s { font-size: 13px; max-width: 34ch; }

/* dashboard column — sizes to its content (bands at natural size); the tab body
   scrolls if the total exceeds the drawer height. */
.wx-dash { flex: 1 0 auto; min-width: 0; max-width: 100%; display: flex; flex-direction: column;
  gap: 8px; padding-bottom: 4px; }
/* Both tab roots must fit the tab body's width — never wider (min-width:0 so their
   grid children can shrink; max-width:100% so nothing bleeds past the panel edge). */
.wx-dash > *, #wx-tabbody > * { min-width: 0; max-width: 100%; }

/* header band */
/* Station status row: LIVE/OFFLINE pill + last-update time. Tells the truth when the
   local Tempest UDP feed drops (cached readings still render; this flags them held). */
.wx-statrow { display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 4px 12px; padding: 0 2px 2px; }
.wx-stat { font-size: 11px; font-weight: 700; letter-spacing: .3px; display: inline-flex;
  align-items: center; gap: 5px; white-space: nowrap; }
.wx-stat.on { color: var(--ok); } .wx-stat.off { color: var(--warn); }
.wx-updt { font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums;
  white-space: nowrap; }
.wx-hdr { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 6px; }
.wx-hdr .day { font-size: 12px; color: var(--muted); font-weight: 600; letter-spacing: .4px; }
.wx-hdr .date { font-size: 17px; font-weight: 800; line-height: 1.05; }
.wx-hdr .clock { text-align: center; }
.wx-hdr .time { font-size: 26px; font-weight: 800; letter-spacing: .5px; font-variant-numeric: tabular-nums; }
.wx-hdr .time .mer { font-size: 12px; color: var(--muted); font-weight: 700; margin-left: 3px; }
.wx-hdr .lit { text-align: right; font-size: 11px; color: var(--muted); line-height: 1.5; }
.wx-hdr .lit b { color: var(--wx-sun); font-weight: 700; }
.wx-hdr .wx-lit-none { opacity: .6; }
/* Station battery readout in the header slot: green when healthy, red when low. */
.wx-hdr .wx-batt-ok b { color: var(--wx-good, #30c66f); }
.wx-hdr .wx-batt-low b { color: var(--wx-bad, #e5484d); }

/* 2×2 gauge grid. The ring SVG sits in a FIXED-size .gface box (not free-scaling to the
   tile) so the centered readout is positioned relative to the RING and can never overlay
   the tile (the iOS overlap bug). Now that the drawer scrolls, tiles get a comfortable
   fixed height rather than being crammed to fit — no distortion. */
.wx-gauges { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.wx-g { background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 10px 4px 8px; position: relative; min-height: 150px; overflow: hidden; }
/* The gauge "face": a fixed square that holds the ring + its centered readout, so the
   readout is positioned relative to the RING, never the whole (variable) tile. */
.wx-g .gface { position: relative; width: clamp(104px, 34vw, 128px); aspect-ratio: 1; }
.wx-g .gface svg { width: 100%; height: 100%; display: block; }
.wx-g .gval { position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; pointer-events: none; padding: 0 6px; }
.wx-g .gnum { font-size: clamp(24px, 7vw, 30px); font-weight: 800; line-height: 1;
  font-variant-numeric: tabular-nums; }
.wx-g .gnum small { font-size: .55em; }
.wx-g .gsub { font-size: clamp(8px, 2.5vw, 10px); color: var(--muted); text-align: center;
  line-height: 1.15; max-width: 100%; }
.wx-g .hilo { font-size: clamp(8px, 2.5vw, 10px); margin-top: 1px; display: flex; gap: 7px; }
.wx-g .hilo .hi { color: var(--wx-temp); } .wx-g .hilo .lo { color: var(--wx-temp-lo); }
.wx-g .gcap { font-size: 10px; color: var(--muted); text-transform: uppercase;
  letter-spacing: .7px; margin-top: 5px; }

/* stat chips */
.wx-chips { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.wx-chip { background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 6px 4px; text-align: center; }
.wx-chip .k { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.wx-chip .v { font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; margin-top: 2px; }
.wx-chip .v small { font-size: 9px; color: var(--muted); font-weight: 600; }

/* compact solar/light + barometer */
.wx-mid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.wx-cell { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 6px 10px; display: flex; flex-direction: column; }
.wx-cell .ct { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; }
.wx-cell .cmain { display: flex; align-items: baseline; gap: 6px; }
.wx-cell .cbig { font-size: 22px; font-weight: 800; line-height: 1.1; font-variant-numeric: tabular-nums; }
.wx-cell .cu { font-size: 10px; color: var(--muted); font-weight: 700; }
.wx-cell .csub { font-size: 10px; color: var(--muted); }

/* Daylight footer: a full daylight arc (+ moon beside it), then a LARGE
   sunrise · UV · sunset readout. Now that the drawer scrolls, the arc is restored to a
   proper height — no cramming (Pete, 2026-07-13). */
.wx-footer { display: flex; flex-direction: column; gap: 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px;
  padding: 12px 14px 14px; flex: 0 0 auto; }
.wx-arcrow { display: grid; grid-template-columns: 1fr auto; gap: 14px; align-items: center; }
.wx-footer .uv-wrap { display: flex; align-items: center; }
.wx-footer .uv-arc { width: 100%; height: auto; max-height: 112px; }
.wx-footer .moon { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.wx-footer .moon svg { width: 52px; height: 52px; }
.wx-footer .moon .ph { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .3px; }
/* big sunrise · UV · sunset row */
.wx-sunbig { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;
  border-top: 1px solid var(--line); padding-top: 8px; }
.wx-sunbig .sb { text-align: center; }
.wx-sunbig .sk { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .5px; }
.wx-sunbig .sv { font-size: 20px; font-weight: 800; font-variant-numeric: tabular-nums; margin-top: 2px; }
.wx-sunbig .uv .sv { color: var(--wx-sun); }
/* Absorb any leftover vertical room ABOVE the footer instead of stretching the arc:
   a flexible spacer keeps bands compact and top-packed, arc stays small. */
.wx-dash { justify-content: flex-start; }
.wx-foot { color: var(--muted); font-size: 12px; padding: 8px 16px 14px; }

/* ---- Forecast tab (WeatherFlow Better Forecast) ---- */
.wx-fc { flex: 1 0 auto; min-width: 0; max-width: 100%; display: flex; flex-direction: column;
  gap: 10px; padding-bottom: 6px; }
/* Every forecast band fits the tab width — never wider. min-width:0 lets the grid
   bands' 1fr columns shrink below their content instead of forcing a horizontal
   scroll; the label columns wrap/ellipsis rather than push the row wide. */
.wx-fc > * { min-width: 0; max-width: 100%; }
.fc-today, .fc-row { min-width: 0; }
.fc-today > *, .fc-row > * { min-width: 0; }
.fc-today .ft-l, .fc-row .fr-l,
.fc-today .ft-cond, .fc-row .fr-cond { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* current-conditions hero: big temp + feels-like, icon + condition on the right */
.fc-hero { display: flex; align-items: center; justify-content: space-between;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 12px 16px; }
.fc-hero .fc-temp { font-size: 52px; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.fc-hero .fc-feels { font-size: 12px; color: var(--muted); margin-top: 4px; }
.fc-hero .fc-feels b { color: var(--text); font-weight: 700; }
.fc-hero-r { text-align: center; }
.fc-hero-ic { font-size: 46px; line-height: 1; }
.fc-hero-r .fc-cond { font-size: 12px; color: var(--muted); margin-top: 2px; }
/* current mini grid: humidity · wind · pressure · UV */
.fc-mini { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.fc-mini .fm { display: flex; align-items: center; justify-content: space-between; gap: 6px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; }
.fc-mini .fmk { font-size: 11px; color: var(--muted); }
.fc-mini .fmv { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
/* Today summary row + sun times */
.fc-today { display: grid; grid-template-columns: 1fr auto auto auto; align-items: center; gap: 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 10px 14px; }
.fc-today .ft-day { font-size: 15px; font-weight: 800; }
.fc-today .ft-cond { font-size: 11px; color: var(--muted); }
.fc-today .ft-ic { font-size: 26px; }
.fc-today .ft-precip { font-size: 12px; color: var(--wx-rain); font-weight: 700; }
.fc-today .ft-hilo { display: flex; gap: 8px; font-size: 16px; font-weight: 800; font-variant-numeric: tabular-nums; }
.fc-today .ft-hilo .hi { color: var(--wx-temp); } .fc-today .ft-hilo .lo { color: var(--muted); }
.fc-sun { display: flex; justify-content: space-around; font-size: 11px; color: var(--muted);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px; padding: 6px 10px; }
.fc-sun b { color: var(--text); font-weight: 700; }
/* section labels */
.fc-sec-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: .7px;
  margin: 2px 2px -2px; }
/* 5-day strip: horizontal columns */
.fc-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.fc-strip .fd { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
  padding: 8px 2px; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.fc-strip .fd-dn { font-size: 13px; font-weight: 800; }
.fc-strip .fd-ic { font-size: 22px; line-height: 1.1; }
.fc-strip .fd-hi { font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.fc-strip .fd-lo { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; margin-top: -2px; }
.fc-strip .fd-precip { font-size: 10px; color: var(--wx-rain); }
.fc-strip .fd-wind { font-size: 10px; color: var(--muted); }
/* named-day rows */
.fc-rows { display: flex; flex-direction: column; gap: 6px; }
.fc-row { display: grid; grid-template-columns: 1fr auto auto auto; align-items: center; gap: 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 9px 14px; }
.fc-row .fr-day { font-size: 14px; font-weight: 800; }
.fc-row .fr-cond { font-size: 11px; color: var(--muted); }
.fc-row .fr-ic { font-size: 22px; }
.fc-row .fr-precip { font-size: 12px; color: var(--wx-rain); font-weight: 600; }
.fc-row .fr-hilo { display: flex; gap: 8px; font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; }
.fc-row .fr-hilo .hi { color: var(--wx-temp); } .fc-row .fr-hilo .lo { color: var(--muted); }
.fc-src { font-size: 10px; color: var(--muted); text-align: center; padding-top: 4px; }

.vac-panel { width: min(96%, 640px); }
.vac-status {
  padding: 4px 16px 10px; color: var(--muted); font-size: 14px;
  border-bottom: 1px solid var(--line);
}
.vac-map-wrap {
  padding: 14px; display: flex; align-items: center; justify-content: center;
  overflow: auto; -webkit-overflow-scrolling: touch;
}
/* Stage wraps the map + the absolutely-positioned live robot marker (shrink-to-fit the
   image so % offsets are relative to the rendered map, not the drawer). */
.vac-map-stage { position: relative; display: inline-block; line-height: 0; }
.vac-map {
  max-width: 100%; height: auto; image-rendering: pixelated;   /* crisp room edges */
  border-radius: 10px; background: #181a24;
  box-shadow: 0 2px 10px rgba(0,0,0,0.35);
}
/* Live robot marker: a cyan disc with a heading notch, overlaid on the map at the robot's
   real position (updated from /api/vacuum/position). transform carries the centring +
   heading rotation; a short transition makes it glide between polls instead of jumping. */
.vac-robot {
  position: absolute; width: 16px; height: 16px; border-radius: 50%;
  background: #00c8ff; border: 2px solid #0a1e28; box-sizing: border-box;
  transition: left .6s linear, top .6s linear, transform .3s ease;
  pointer-events: none; z-index: 2;
}
.vac-robot::after {                       /* heading notch pointing "up" (rotated by transform) */
  content: ""; position: absolute; left: 50%; top: -5px; transform: translateX(-50%);
  border-left: 4px solid transparent; border-right: 4px solid transparent;
  border-bottom: 6px solid #00c8ff;
}
/* Active (moving): a pulsing glow so it's obvious the robot is working; steady otherwise. */
.vac-robot.active { animation: vacpulse 1.4s ease-in-out infinite; }
@keyframes vacpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(0,200,255,0.55); }
  50%      { box-shadow: 0 0 10px 5px rgba(0,200,255,0.35); }
}
.vac-map-msg { color: var(--muted); font-size: 15px; padding: 40px 16px; text-align: center; }
/* Vacuum remote: Clean/Pause toggle, Spot, Dock. */
.vac-controls { display: flex; gap: 8px; padding: 10px 14px 4px; }
.vac-btn {
  flex: 1 1 0; padding: 11px 8px; border-radius: 12px; font-size: 14px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; transition: transform .08s, border-color .2s;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
}
.vac-btn:active, .vac-btn.pressed { transform: scale(0.96); border-color: var(--accent); }
.vac-toggle { background: #1c3a44; border-color: #2a5a68; }   /* the primary action stands out */
.vac-toggle-ic { font-size: 16px; }
/* Room-selection bar under the controls. */
.vac-roombar {
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 8px 14px 2px; font-size: 13px; color: var(--muted);
}
.vac-roombar-lbl { flex: 1 1 100%; line-height: 1.4; }
.vac-btn.vac-go { flex: 2 1 0; background: #00778a; border-color: transparent; color: #fff; }
.vac-btn.vac-clear { flex: 1 1 0; }
/* Selection overlay canvas sits over the map; taps pass THROUGH to the map handler. */
.vac-sel { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }
/* Live cleaning-path breadcrumb trail: the robot's ACTUAL traversed path, sampled from
   its position during a clean (reflects real re-routes around obstacles, not a plan).
   Sits above the map, below the room-selection tint + the robot marker. */
.vac-trail { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 0; }
/* Big STOP bar under the map — halts a task in progress. Prominent + red; only shown while
   the robot is actively running. */
.vac-stop {
  display: block; width: calc(100% - 28px); margin: 4px 14px 14px;
  padding: 16px; border-radius: 14px; border: none; cursor: pointer;
  background: #c0392b; color: #fff; font-size: 18px; font-weight: 800; letter-spacing: .04em;
  transition: transform .08s, background .2s;
}
.vac-stop:active, .vac-stop.pressed { transform: scale(0.98); background: #a5311f; }

/* ===== Sprinklers (Rachio) drawer (Sprint 31) ===== */
/* Dishwasher "Cookie" drawer (Home Connect; v1 status-only). */
.dishwasher-panel { width: min(96%, 560px); }
.dishwasher-status { padding: 12px 16px 16px; }
.dw-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 2px; border-bottom: 1px solid var(--line); font-size: 15px;
}
.dw-k { color: var(--muted); }
.dw-v { font-weight: 600; }
.dw-note { margin-top: 14px; color: var(--muted); font-size: 13px; line-height: 1.4; }

.rachio-panel { width: min(96%, 640px); }
.rachio-status {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 16px 10px; color: var(--muted); font-size: 14px;
  border-bottom: 1px solid var(--line);
}
.rachio-status-txt { flex: 1 1 auto; min-width: 0; }
/* Master Watering On/Off pill switch. */
.rachio-power {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 10px 4px 5px; border-radius: 999px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  font-size: 12px; font-weight: 700; transition: background .2s, border-color .2s, color .2s;
}
.rachio-power .rachio-power-knob {
  width: 14px; height: 14px; border-radius: 50%; background: var(--muted);
  transition: background .2s, transform .2s;
}
.rachio-power.is-on {
  background: #14414f; border-color: #2a7d94; color: #eaf6fa;
}
.rachio-power.is-on .rachio-power-knob { background: #4fc3e8; }
.rachio-power[disabled] { opacity: .4; cursor: default; }
/* Programs (saved schedules) section. Compact to help the drawer fit iOS. */
.rachio-sec-lbl {
  font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--muted); padding: 8px 16px 2px;
}
.rachio-programs { padding-bottom: 2px; }
.rachio-prog {
  margin: 5px 16px; padding: 9px 11px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.rachio-prog.is-off { opacity: .62; }
.rachio-prog-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.rachio-prog-name { font-size: 15px; font-weight: 700; }
.rachio-prog-when { font-size: 12px; color: var(--muted); }
.rachio-prog-zones { display: flex; flex-wrap: wrap; gap: 5px; margin: 6px 0 2px; }
.rachio-prog-z {
  font-size: 11px; color: var(--muted);
  background: rgba(255,255,255,0.05); border-radius: 8px; padding: 2px 7px;
}
.rachio-prog-btns { display: flex; gap: 8px; margin-top: 7px; }
.rachio-prog-btn {
  flex: 1 1 0; padding: 8px 8px; border-radius: 10px; font-size: 13px; font-weight: 600;
  background: #14414f; border: 1px solid #2a7d94; color: #eaf6fa; cursor: pointer;
  transition: transform .08s, background .2s;
}
.rachio-prog-btn:active { transform: scale(0.97); }
.rachio-prog-btn.rachio-prog-skip { background: var(--panel-2); border-color: var(--line); color: var(--text); }

/* Run-duration selector. */
.rachio-durbar {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 16px 2px; font-size: 13px; color: var(--muted);
}
.rachio-dur { display: flex; gap: 6px; }
.rachio-dur-btn {
  padding: 7px 12px; border-radius: 10px; font-size: 13px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); cursor: pointer;
  transition: background .15s, border-color .15s;
}
.rachio-dur-btn.is-sel { background: #14414f; border-color: #2a7d94; color: #eaf6fa; }
/* Zone tiles — a responsive grid; enabled tiles tappable, disabled greyed, running tinted.
   COMPACT single-row layout (num • name • state) so the whole drawer stays short enough to
   fit an iOS viewport without vertical overflow. */
.rachio-zones {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 7px; padding: 8px 16px;
}
.rachio-zone {
  display: flex; flex-direction: row; align-items: center; gap: 8px;
  padding: 9px 11px; border-radius: 12px; text-align: left; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  transition: transform .08s, border-color .2s, background .2s;
}
.rachio-zone:active { transform: scale(0.97); border-color: var(--accent); }
.rachio-zone-num {
  flex: 0 0 auto;
  font-size: 12px; font-weight: 700; color: var(--muted);
  width: 20px; height: 20px; border-radius: 50%; background: rgba(255,255,255,0.06);
  display: inline-flex; align-items: center; justify-content: center;
}
.rachio-zone-name {
  flex: 1 1 auto; min-width: 0;
  font-size: 14px; font-weight: 600; line-height: 1.15;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rachio-zone-state { flex: 0 0 auto; font-size: 11px; color: var(--muted); }
.rachio-zone.is-running {
  background: #14414f; border-color: #2a7d94;
  animation: rachio-tile-pulse 1.8s ease-in-out infinite;
}
@keyframes rachio-tile-pulse { 0%,100% { border-color: #2a7d94; } 50% { border-color: #4fc3e8; } }
/* Rain-delay row. */
.rachio-raindelay {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px 16px 10px; font-size: 13px; color: var(--muted);
  border-top: 1px solid var(--line);
}
.rachio-rd-btns { display: flex; gap: 6px; margin-left: auto; }
.rachio-rd-btn {
  padding: 7px 11px; border-radius: 10px; font-size: 13px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); cursor: pointer;
}
.rachio-rd-btn.rachio-rd-clear { color: var(--muted); }
/* Big STOP bar — halts ALL watering; shown only while something runs. */
.rachio-stop {
  display: block; width: calc(100% - 32px); margin: 4px 16px 14px;
  padding: 14px; border-radius: 14px; border: none; cursor: pointer;
  background: #c0392b; color: #fff; font-size: 17px; font-weight: 800; letter-spacing: .03em;
  transition: transform .08s, background .2s;
}
.rachio-stop:active { transform: scale(0.98); background: #a5311f; }

/* Zone-run confirm modal (safety check before a zone actually waters). */
.rachio-confirm-card { max-width: 320px; text-align: center; }
.rachio-confirm-body { font-size: 16px; font-weight: 600; padding: 20px 18px 4px; }
.rachio-confirm-btns { display: flex; gap: 10px; padding: 14px 18px 18px; }
.rachio-confirm-btn {
  flex: 1 1 0; padding: 12px; border-radius: 12px; font-size: 15px; font-weight: 700;
  cursor: pointer; border: 1px solid var(--line);
}
.rachio-confirm-cancel { background: var(--panel-2); color: var(--text); }
.rachio-confirm-yes { background: #14414f; border-color: #2a7d94; color: #eaf6fa; }
.rachio-confirm-btn:active { transform: scale(0.97); }

/* Shortcut line (line 4): your most-used actions as one-tap chips. An action icon
   over a short label; tappable, with brief ⏳→✓/✗ feedback on run. Up to 5 chips
   share the row evenly (flex:1) so all 5 fit a phone width without scrolling. */
.shortcut-row { gap: 6px; flex-wrap: nowrap; }
.shortcut {
  flex: 1 1 0; min-width: 0; height: 58px; box-sizing: border-box;
  padding: 4px 4px; border-radius: 14px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  display: inline-flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 2px; cursor: pointer;
  transition: transform .08s, border-color .2s, color .2s;
}
.shortcut:active { transform: scale(0.94); }
/* Re-tap acknowledgement: a tap that arrives while the prior command is still in
   flight can't act, but this quick flash shows it registered (no dead-button feel). */
.shortcut.chip-pulse { animation: chippulse 0.3s ease; }
@keyframes chippulse {
  0%   { transform: scale(1); }
  40%  { transform: scale(0.9); filter: brightness(1.5); }
  100% { transform: scale(1); }
}
.shortcut .shortcut-ic { font-size: 24px; line-height: 1; }
.shortcut .shortcut-lbl {
  font-size: 10px; max-width: 100%; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; color: var(--muted);
}
/* OFF / closed: dim ONLY THE ICON (not the whole button — a dimmed button reads as
   disabled, which it isn't). The label + border stay full strength; the glyph
   brightness/grey conveys the device state at a glance. */
.shortcut.off .shortcut-ic { opacity: .38; filter: grayscale(0.5); }

/* --- Thread --- */
.thread {
  flex: 1; overflow-y: auto; padding: 18px 14px;
  display: flex; flex-direction: column; gap: 12px;
  -webkit-overflow-scrolling: touch;
}
.msg { max-width: min(84%, 680px); padding: 10px 14px; border-radius: var(--radius); white-space: pre-wrap; word-wrap: break-word; }
.msg.user { align-self: flex-end; background: var(--user); border-bottom-right-radius: 5px; }
.msg.howie { align-self: flex-start; background: var(--howie); border: 1px solid var(--line); border-bottom-left-radius: 5px; }
.msg.howie.error { border-color: #3a1f1c; }
/* Server notice (failure surface) — centered, full-width, warn-toned so it reads
   as an unprompted "heads up", distinct from a user message or a Howie reply. */
.msg.notice {
  align-self: stretch; max-width: 100%;
  background: rgba(224, 160, 48, 0.08);
  border: 1px solid var(--warn);
  border-radius: var(--radius);
  position: relative;
}
.notice-title { color: var(--warn); font-weight: 600; font-size: 14px; padding-right: 22px; }
.notice-body { color: var(--text); font-size: 14px; margin-top: 3px; }
/* Dismiss (top-right ✕, matching the app's drawer/attach-preview close glyph):
   these can pile up (e.g. a blind failing on every retry) with no other way
   to clear a seen one, so every notice gets one. */
.notice-dismiss {
  position: absolute; top: 10px; right: 10px;
  width: 22px; height: 22px; padding: 0; line-height: 1;
  background: transparent; border: none; color: var(--muted);
  font-size: 13px; cursor: pointer; border-radius: 6px;
}
.notice-dismiss:active { background: rgba(255, 255, 255, 0.08); color: var(--text); }
/* Floating "Clear all alerts (N)" pill — sits just above the composer, centered, shown
   only when 2+ notices are stacked up. One tap removes them all (they're ephemeral). */
.clear-notices {
  position: sticky; bottom: 8px; z-index: 6;
  align-self: center; margin: 4px auto 2px;
  padding: 8px 16px; border-radius: 999px; cursor: pointer;
  background: var(--warn); color: #1a1205; border: none;
  font-size: 13px; font-weight: 700; letter-spacing: .01em;
  box-shadow: 0 3px 10px rgba(0,0,0,0.35);
  transition: transform .08s, filter .2s;
}
.clear-notices:active { transform: scale(0.96); filter: brightness(0.92); }
/* Opt-in "ask Howie to look into it" button under an attention alert. */
.notice-analyze {
  margin-top: 8px; font-size: 13px; font-weight: 700; padding: 7px 12px;
  border-radius: 9px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--warn); color: var(--text);
}
.notice-analyze:active { background: var(--line); }
.notice-analyze:disabled { opacity: .55; }
/* Update-available notice: same in-stream notice style but accent-tinted + a clear
   tap-to-refresh button. No popup; just a line in the chat that the user can tap. */
.update-notice { background: rgba(91, 140, 255, 0.10); border-color: var(--accent); }
.update-notice .notice-title { color: var(--accent); }
.update-notice-btn {
  margin-top: 8px; padding: 8px 14px; border-radius: 10px; cursor: pointer;
  font-size: 14px; font-weight: 700; color: #0c0e14; background: var(--accent);
  border: none;
}
.update-notice-btn:disabled { opacity: .6; }
.tools {
  font-size: 11px; color: var(--muted); margin-top: 6px;
  display: flex; gap: 5px; flex-wrap: wrap;
}
.tool-tag { background: var(--panel-2); border: 1px solid var(--line); border-radius: 6px; padding: 1px 6px; }

/* typing / tool-activity indicator */
.thinking { align-self: flex-start; color: var(--muted); font-size: 14px;
  display: flex; align-items: center; gap: 8px; padding: 4px 6px; }
.dots { display: inline-flex; gap: 4px; }
.dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); animation: blink 1.2s infinite; }
.dots i:nth-child(2){ animation-delay: .2s } .dots i:nth-child(3){ animation-delay: .4s }
@keyframes blink { 0%,80%,100%{opacity:.25} 40%{opacity:1} }

.greeting { align-self: center; color: var(--muted); text-align: center; font-size: 14px; margin: 28px 12px; }

/* --- Composer --- */
.composer {
  padding: 8px 12px max(env(safe-area-inset-bottom), 10px);
  background: var(--panel); border-top: 1px solid var(--line);
}
#ask-form { display: flex; gap: 8px; align-items: flex-end; }
#ask-input {
  flex: 1; resize: none; max-height: 140px; padding: 11px 14px; font-size: 16px;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: 14px; color: var(--text); font-family: inherit; line-height: 1.4;
}
#send-btn { height: 60px; min-width: 60px; font-size: 24px; }
#send-btn:disabled { opacity: .4; }
/* Stop state: the send button BECOMES the stop button while Howie is replying, rather
   than a second control that's dead most of the time. Red-ish so it reads as "interrupt",
   and never disabled — being able to stop is the whole point. */
/* --- Pending confirmation bar (above the composer) ---
   Deliberately NOT a dismissible toast: a staged command that nobody answers just
   expires, so the affordance has to persist until it's actually decided. */
/* CONTAINER of one .confirm-bar-item per pending command. It used to be the single
   bar itself, which could only ever show the oldest request — with three staged at
   once the other two were invisible. Each request now gets its own row so what you
   approve is always what you are looking at. Capped in height: many pending requests
   scroll rather than pushing the composer off screen. */
.confirm-bar {
  display: flex; flex-direction: column; gap: 8px;
  margin: 0 0 8px; max-height: 40vh; overflow-y: auto;
}
.confirm-bar.hidden { display: none; }
.confirm-bar-item {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 10px 12px;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--danger, #c0392b); border-radius: 12px;
}
.confirm-bar-text { flex: 1 1 220px; min-width: 0; }
.confirm-bar-title { display: block; font-weight: 600; font-size: 13px; }
.confirm-bar-cmd {
  display: block; margin-top: 2px; font-size: 12px; opacity: .85;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;   /* never blow up the composer */
}
.confirm-bar-actions { display: flex; gap: 8px; flex: none; }
.confirm-bar-actions button {
  border: none; border-radius: 10px; padding: 8px 16px;
  font-size: 14px; font-weight: 600; cursor: pointer; min-height: 40px;
}
.confirm-bar-actions button:disabled { opacity: .5; cursor: default; }
.cb-run { background: var(--danger, #c0392b); color: #fff; }
.cb-decline { background: var(--panel); color: var(--text); border: 1px solid var(--line) !important; }

#send-btn.stopping { background: var(--danger, #c0392b); opacity: 1; }
#send-btn.stopping:disabled { opacity: 1; }
#attach-btn {
  height: 44px; min-width: 44px; font-size: 20px; flex: none;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  border-radius: 14px; cursor: pointer;
}
#attach-btn:active { background: var(--line); }

.composer-head { display: flex; justify-content: flex-end; margin-bottom: 6px; }
#voice-toggle {
  width: 40px; height: 40px; border-radius: 12px; border: 1px solid var(--line);
  background: var(--panel-2); color: var(--text); font-size: 18px; cursor: pointer;
  display: flex; align-items: center; justify-content: center; flex: none;
  padding: 0; overflow: hidden;
}
#voice-toggle.on { border-color: var(--accent); color: #bcd0ff; box-shadow: 0 0 0 1px var(--accent) inset; }
/* Idle state: a tiny live version of the communicator dial instead of a 🎤
   glyph — reuses the same moiré render (see the voice-mode IIFE in app.js) so
   the toggle previews what tapping it opens. At 40px the fine ring detail
   that reads clearly on the full 220px dial compresses into low-contrast
   noise, so the icon gets its own silver BEZEL (the same conic-gradient as
   .comm-ring, thinned down) to frame it and make it read as "the dial",
   not just a dark smudge (Pete, 2026-07-04).
   Circular crop via border-radius on both the bezel and the canvas (the
   button's own radius is a rounded SQUARE, 12px). */
.voice-toggle-bezel {
  width: 100%; height: 100%; border-radius: 50%;
  background: conic-gradient(from 0deg, #8a8f99, #e9edf4, #6d7480, #f4f7fb, #7c828d, #dfe4ec, #8a8f99);
  padding: 2.5px; display: block;
}
#voice-toggle-moire { width: 100%; height: 100%; border-radius: 50%; display: block; }
/* When voice mode is active the button shows a plain ✕ CLOSE (Pete, 2026-07-07):
   a touch larger + muted so it reads as a standard close control. */
#voice-toggle-glyph { font-size: 20px; font-weight: 400; color: var(--muted); line-height: 1; }
#voice-toggle.on #voice-toggle-glyph { color: var(--text); }
#voice-toggle-glyph.hidden { display: none; }

/* --- Voice mode: the "communicator" (Star Trek moiré dial) ---------------
   Design locked 2026-07-02 (howie/design/voice-mode/communicator-mockup.html).
   Two near-black concentric ring sets, offset centers, drawn 'multiply' on a
   silver dial -> a shimmering moiré. A center glow signals state: cool blue
   when the person is speaking (mic level), warm amber when Howie is speaking
   (TTS). Rotation is a steady slow drift, not speech-driven. */
.communicator {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 10px 0 4px;
}
.comm-wrap { position: relative; width: 220px; height: 220px; margin: 0 auto; }
.comm-ring {
  position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from 0deg, #8a8f99, #e9edf4, #6d7480, #f4f7fb, #7c828d, #dfe4ec, #8a8f99);
  padding: 10px; box-shadow: 0 6px 28px rgba(0,0,0,.55), inset 0 0 8px rgba(0,0,0,.5);
}
.comm-inner { width: 100%; height: 100%; border-radius: 50%; overflow: hidden; position: relative; background: #8a8f99; }
#comm-moire { display: block; width: 100%; height: 100%; }
.comm-status { font-size: 13px; font-weight: 600; letter-spacing: .03em; color: #8ea2c8; }
.communicator.listening .comm-status { color: var(--text); }   /* user speaking = white */
.communicator.speaking .comm-status { color: #ffb347; }        /* Howie speaking = amber */
.comm-hint { font-size: 12px; color: var(--muted); }
.comm-replay {
  font-size: 12px; color: var(--muted); background: none; border: 1px solid rgba(255,255,255,.14);
  border-radius: 999px; padding: 4px 12px; cursor: pointer;
}
.comm-replay:active { background: rgba(255,255,255,.08); }

.attach-preview {
  display: flex; align-items: center; gap: 10px; padding: 6px 4px 10px;
}
#attach-thumb {
  height: 64px; width: 64px; object-fit: cover; border-radius: 10px;
  border: 1px solid var(--line);
}
#attach-clear {
  background: var(--panel-2); border: 1px solid var(--line); color: var(--muted);
  border-radius: 8px; width: 28px; height: 28px; cursor: pointer; font-size: 13px;
}

.msg-photo {
  display: block; margin-top: 8px; max-width: 100%; max-height: 320px;
  border-radius: 10px; border: 1px solid var(--line); cursor: pointer;
}

/* --- controls drawer: scenes + kill switches --- */
.ctl-panel {
  /* Top uses the island/notch clearance (overriding .drawer-panel's `padding`
     shorthand would otherwise reset the safe-area top to a flat value). */
  padding: calc(env(safe-area-inset-top, 0px) + 16px) 14px 14px;
  display: flex; flex-direction: column; gap: 18px;
  /* Short phone viewports clip content (scenes + toggles + message) below the
     fold; make the panel scrollable so the helper text is always reachable. */
  overflow-y: auto; -webkit-overflow-scrolling: touch;
}
.ctl-section { display: flex; flex-direction: column; gap: 10px; }
.ctl-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted);
}
.ctl-loading { color: var(--muted); font-size: 14px; }
.scene-buttons { display: flex; flex-wrap: wrap; gap: 8px; }
/* Room grouping (Pete, 2026-07-06): general scenes first, then a labeled section
   per room, then the automated wrap. The header + each room group take a full
   line in the flex-wrap container so rooms stack cleanly. */
/* Match the Shortcuts drawer's section headers (.sc-roomlabel) — bigger, bold,
   white, subtle underline — so scene sections stand out + read the same across
   the app (Pete, 2026-07-07). */
.scene-room-hd {
  flex: 1 1 100%; font-size: 14px; font-weight: 700; color: var(--text);
  margin: 10px 2px 6px; padding-bottom: 3px; border-bottom: 1px solid var(--line);
}
.scene-room-hd:first-child { margin-top: 0; }
.scene-room-grp { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 8px; }
/* Scene buttons: styled to feel like the Shortcuts tiles (same panel bg, border,
   rounded-rect, muted-ish text) but kept as compact BUTTONS, not square icon
   tiles — smaller + lighter than before (Pete, 2026-07-07). */
.scene-btn {
  padding: 8px 13px; border-radius: 13px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  font-size: 12px; font-weight: 500; cursor: pointer;
  transition: transform .08s, border-color .2s;
}
.scene-btn:active { transform: scale(0.96); background: var(--line); }
.scene-btn:disabled { opacity: .5; }
/* While the scene runs, its button becomes a Stop control (red). */
.scene-btn.running {
  background: var(--bad); border-color: var(--bad); color: #fff;
}

.kill-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.kill-name { font-weight: 600; font-size: 15px; }
.kill-sub { color: var(--muted); font-size: 12px; margin-top: 2px; }

/* toggle: green when ON; red when OFF (a kill is a clear danger state) */
.toggle {
  flex: none; width: 50px; height: 28px; border-radius: 999px;
  border: 1px solid var(--line); background: var(--bad); position: relative;
  cursor: pointer; transition: background .15s;
}
.toggle.on { background: var(--ok); }
.toggle .knob {
  position: absolute; top: 2px; left: 2px; width: 22px; height: 22px;
  border-radius: 50%; background: #fff; transition: left .15s;
}
.toggle.on .knob { left: 24px; }
.toggle:disabled { opacity: .6; }

.ctl-msg { font-size: 13px; color: var(--muted); min-height: 18px; white-space: pre-line; }
.ctl-msg.good { color: var(--ok); }
.ctl-msg.warn { color: var(--warn); }

/* --- Nest-style thermostat icon (live temp + heat/cool/idle colour) --- */
.nest-ico { display: inline-block; vertical-align: middle; border-radius: 50%; }
/* In a shortcut chip the icon replaces the emoji glyph; keep it crisp + centred. */
.shortcut .shortcut-ic .nest-ico { width: 30px; height: 30px; }
/* In the home-status thermostat row, the icon prefixes the name. */
.home-k-ico { display: inline-flex; vertical-align: middle; margin-right: 8px; }
.home-k-ico .nest-ico, .home-k-ico .ring-ico { width: 34px; height: 34px; }
/* A subtle glow on the active state so heating/cooling reads at a glance. */
.nest-heating { filter: drop-shadow(0 0 3px rgba(232,145,90,.55)); }
.nest-cooling { filter: drop-shadow(0 0 3px rgba(90,166,232,.55)); }

/* --- Ring alarm icon (chip + popup) --- */
/* Minimal padding so the rounded-square body nearly fills the chip "button". */
/* Tight padding + a big icon so the glyph nearly fills the 60px chip "button". */
.alarm-chip { padding: 0 6px; line-height: 0; }
.alarm-chip-ico { display: inline-flex; }
.alarm-chip-ico .ring-ico { width: 48px; height: 48px; vertical-align: middle; }

/* Doors & Locks combined chip: the door+lock icon, large like the others. */
.doorlock-chip { padding: 0 6px; line-height: 0; }
.doorlock-chip-ico { display: inline-flex; }
.doorlock-chip-ico .doorlock-ico { width: 48px; height: 48px; vertical-align: middle; }

/* Custom button icons (replace the emoji on the top-bar buttons). The buttons are
   60px; the icons fill ~50px to match the visual weight the emoji had. */
#cam-btn, #bed-btn, #home-btn, #controls-btn, #menu-btn { line-height: 0; position: relative; }
#cam-btn .cam-ico, #bed-btn .bed-ico, #home-btn .home-ico,
#controls-btn .scene-ico {
  width: 50px; height: 50px; vertical-align: middle;
}
/* The gear is a touch smaller so the red attention dot reads clearly in the corner. */
#menu-gear-ico { display: inline-flex; }
#menu-gear-ico .gear-ico { width: 42px; height: 42px; vertical-align: middle; }

/* Light fixture icons: mini in the home row, plus the per-light picker grid. */
.home-k-ico.fixmini { margin-right: 8px; }
.home-k-ico.fixmini .fixture-ico,
.home-k-ico.fixmini .shade-ico,
.home-k-ico.fixmini .lock-ico { width: 32px; height: 32px; vertical-align: middle; }
.shortcut .shortcut-ic .fixture-ico,
.shortcut .shortcut-ic .lock-ico,
.shortcut .shortcut-ic .shade-ico { width: 30px; height: 30px; }
.fixture-picker { margin-top: 8px; }
/* Collapsible "Icon" header — chevron + label + a small preview of the current icon.
   Full-width, transparent button so it reads as a section toggle, not a control. */
.fixture-picker-head {
  display: flex; align-items: center; gap: 6px; width: 100%;
  background: none; border: none; padding: 2px 0; margin-bottom: 6px;
  cursor: pointer; color: var(--muted); text-align: left;
}
.fixture-chevron { font-size: 11px; width: 12px; flex: none; color: var(--muted); }
.fixture-picker-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.fixture-head-preview { margin-left: auto; line-height: 0; }
.fixture-head-preview .fixture-ico { width: 22px; height: 22px; }
.fixture-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.fixture-opt {
  width: 48px; height: 48px; padding: 6px; border-radius: 10px; cursor: pointer;
  background: var(--bg, #111); border: 1px solid var(--line); line-height: 0;
}
.fixture-opt .fixture-ico { width: 100%; height: 100%; }
.fixture-opt.sel { border-color: var(--accent, #3a6); box-shadow: inset 0 0 0 1px var(--accent, #3a6); }

/* Settings drawer rows (history / memory / battery). */
.set-row {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 14px 12px; border-radius: 12px; font-size: 16px; text-align: left;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer;
}
.set-row:active { background: var(--line); }
.set-row .menu-item-dot { margin-left: auto; }

/* Manage Access drawer (admin-only). */
.access-input {
  width: 100%; padding: 12px; border-radius: 10px; font-size: 16px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.access-add-btn {
  width: 100%; padding: 13px; border-radius: 12px; font-size: 16px; font-weight: 600;
  background: var(--accent, #3b82f6); border: none; color: #fff; cursor: pointer;
}
.access-add-btn:disabled { opacity: .6; cursor: default; }
.access-link-box {
  margin-top: 6px; padding: 12px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.access-link-hint { font-size: 13px; color: var(--muted, #888); margin-bottom: 8px; }
.access-link {
  font-family: ui-monospace, monospace; font-size: 12px; word-break: break-all;
  color: var(--text); background: var(--bg, #111); padding: 8px; border-radius: 8px;
  border: 1px solid var(--line);
}
.access-link-actions { display: flex; gap: 8px; margin-top: 10px; }
.access-mini-btn {
  flex: 1; padding: 10px; border-radius: 10px; font-size: 15px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); cursor: pointer;
}
.access-mini-btn:active { background: var(--line); }
.access-users { display: flex; flex-direction: column; gap: 8px; }
.access-empty { font-size: 14px; color: var(--muted, #888); padding: 6px 2px; }
.access-user-row {
  display: flex; align-items: center; gap: 10px; padding: 12px;
  border-radius: 12px; background: var(--panel-2); border: 1px solid var(--line);
}
.access-user-info { flex: 1; min-width: 0; }
.access-user-name { font-size: 15px; font-weight: 600; color: var(--text); }
.access-user-sub { font-size: 12px; color: var(--muted, #888); }
.access-badge {
  font-size: 11px; font-weight: 600; padding: 1px 6px; border-radius: 6px;
  background: var(--accent, #3b82f6); color: #fff; vertical-align: middle;
}
.access-del-btn {
  padding: 8px 12px; border-radius: 10px; font-size: 14px;
  background: transparent; border: 1px solid var(--danger, #ef4444);
  color: var(--danger, #ef4444); cursor: pointer;
}
.access-del-btn:active { background: var(--danger, #ef4444); color: #fff; }
/* Per-user card grouping its devices — collapsible. */
.access-user-card {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  background: var(--panel-2);
}
.access-user-card .access-user-row { border: none; border-radius: 0; background: transparent; }
/* Tappable person header (the whole row toggles the device list). */
.access-user-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 13px 12px; background: transparent; border: none; cursor: pointer;
  color: var(--text); text-align: left;
}
.access-user-head:active { background: var(--line); }
.access-chevron { font-size: 12px; color: var(--muted, #888); width: 14px; flex: none; }
.access-user-body { display: flex; flex-direction: column; }
.access-device-empty { font-size: 13px; color: var(--muted, #888); padding: 8px 12px 8px 22px; border-top: 1px solid var(--line); }
.access-remove-all { margin: 10px 12px 12px; align-self: flex-start; }

/* --- admin Cameras panel ------------------------------------------------ */
.ctl-hint { font-size: 12px; color: var(--muted, #888); margin-top: 6px; line-height: 1.4; }
.cam-health {
  margin: 0 0 4px; padding: 10px 12px; border-radius: 10px; font-size: 13px;
  border: 1px solid var(--line); background: var(--panel-2); color: var(--text);
}
.cam-health-ok { border-color: var(--line); }
.cam-health-bad {
  border-color: var(--danger, #ef4444);
  background: color-mix(in srgb, var(--danger, #ef4444) 12%, var(--panel-2));
  color: var(--text); font-weight: 600;
}
.cam-admin-list { display: flex; flex-direction: column; gap: 8px; }
.cam-admin-card {
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
  background: var(--panel-2);
}
.cam-admin-head {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 12px; background: transparent; border: none; cursor: pointer;
  color: var(--text); text-align: left;
}
.cam-admin-head:active { background: var(--line); }
.cam-admin-info { flex: 1; min-width: 0; }
.cam-admin-name { font-size: 15px; font-weight: 600; color: var(--text); }
.cam-admin-sub { font-size: 12px; color: var(--muted, #888); }
.cam-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.cam-dot-on { background: var(--ok, #22c55e); box-shadow: 0 0 5px var(--ok, #22c55e); }
.cam-dot-off { background: var(--muted, #888); opacity: .5; }
.cam-admin-body {
  display: flex; flex-direction: column; gap: 10px;
  padding: 4px 12px 14px; border-top: 1px solid var(--line);
}
.cam-f { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted, #888); }
.cam-f input {
  padding: 9px 10px; border-radius: 9px; font-size: 14px;
  border: 1px solid var(--line); background: var(--panel); color: var(--text);
}
.cam-f-hint { font-weight: 400; opacity: .8; }
.cam-admin-actions { display: flex; gap: 8px; margin-top: 2px; }
.access-device-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px 9px 22px; border-top: 1px solid var(--line);
  background: var(--bg, #0d0d0f);
}
.access-device-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.access-device-name { font-size: 14px; color: var(--text); }
.access-device-sub { font-size: 11px; color: var(--muted, #888); }
.access-dev-edit {
  padding: 6px 9px; border-radius: 8px; font-size: 13px;
  background: transparent; border: 1px solid var(--line); color: var(--muted, #aaa);
  cursor: pointer; flex: none;
}
.access-dev-edit:active { background: var(--line); }
.access-dev-revoke {
  padding: 6px 10px; border-radius: 8px; font-size: 13px;
  background: transparent; border: 1px solid var(--line); color: var(--muted, #aaa);
  cursor: pointer;
}
.access-dev-revoke:active { background: var(--danger, #ef4444); color: #fff; border-color: var(--danger, #ef4444); }
/* Alarm control popup: big icon + state + arm/disarm buttons. */
.alarm-body { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 4px 4px; }
.alarm-ico-big .ring-ico { width: 96px; height: 96px; }
.alarm-state { font-size: 15px; font-weight: 600; color: var(--text); letter-spacing: .02em; }
.alarm-btns { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; width: 100%; }
.alarm-btn {
  flex: 1; min-width: 120px; padding: 14px 16px; border-radius: 12px;
  font-size: 15px; font-weight: 700; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.alarm-btn:active { transform: scale(.97); }
.alarm-btn.alarm-arm { background: var(--bad, #e0524a); border-color: transparent; color: #fff; }
.alarm-btn.alarm-disarm { background: var(--accent, #3a6); border-color: transparent; color: #fff; }

/* Garage control popup: big state icon + Open/Close confirm buttons (the popup IS the
   confirm step, guarding an inadvertent main-screen tap). Mirrors the alarm popup. */
.garage-body { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 8px 4px 4px; }
.garage-ico-big .garage-ico { width: 108px; height: 108px; }
.garage-state { font-size: 15px; font-weight: 700; color: var(--text); letter-spacing: .04em; text-transform: uppercase; }
.garage-btns { display: flex; gap: 10px; width: 100%; }
.garage-btn {
  flex: 1; min-width: 110px; padding: 16px 16px; border-radius: 12px;
  font-size: 16px; font-weight: 700; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.garage-btn:active { transform: scale(.97); }
.garage-btn:disabled { opacity: .4; cursor: default; }
.garage-btn.garage-open  { background: var(--accent, #3a6); border-color: transparent; color: #fff; }
.garage-btn.garage-close { background: #00778a; border-color: transparent; color: #fff; }
/* Mid-travel emergency halt: a single full-width red STOP button replacing Open/Close. */
.garage-btn.garage-stop {
  background: var(--bad, #e0524a); border-color: transparent; color: #fff;
  font-size: 18px; letter-spacing: .06em;
  animation: garagestop 1.3s ease-in-out infinite;
}
@keyframes garagestop {
  0%, 100% { box-shadow: 0 0 0 0 rgba(224,82,74,.55); }
  50%      { box-shadow: 0 0 0 6px rgba(224,82,74,0); }
}
/* Arm-failed notice (open door): the reason + an "Arm anyway" button. */
.alarm-armfail {
  width: 100%; margin-top: 12px; padding: 12px; border-radius: 12px;
  background: rgba(224,82,74,0.12); border: 1px solid rgba(224,82,74,0.4);
  display: flex; flex-direction: column; gap: 10px; align-items: center;
}
.alarm-armfail-why { color: var(--text); font-size: 14px; text-align: center; }

/* --- Thermostat status chips: dial on top, short location BELOW (narrow) --- */
.tstat-chips { display: inline-flex; gap: 6px; align-items: stretch; }
.tstat-chip {
  display: inline-flex; flex-direction: column; align-items: center; gap: 1px;
  padding: 2px 8px;
}
.tstat-chip-ico { display: inline-flex; }
/* Bigger dial so it fills the chip (label sits below, so a touch shorter than the
   single-glyph chips). */
.tstat-chip-ico .nest-ico { width: 38px; height: 38px; }
.tstat-chip-lbl { font-size: 10px; line-height: 1.1; color: var(--muted, #9aa); }

/* Red attention dot on the gear when batteries need attention. */
.menu-badge {
  position: absolute; top: 2px; right: 2px;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--bad, #e0524a); border: 1.5px solid var(--bg, #0d0f12);
}
/* Red dot at the end of a Settings row that needs attention (e.g. low batteries). */
.menu-item-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--bad, #e0524a);
}

/* --- Bed drawer: manual firmness + vertical head/foot, then presets --- */
.bed-panel { width: min(94%, 540px);
  padding: calc(env(safe-area-inset-top, 0px) + 16px) 14px 14px;
  display: flex; flex-direction: column;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* Keep scrolling INSIDE the drawer — never rubber-band through to the page
     behind it (the "parent page scroll" that stole slider drags). */
  overscroll-behavior: contain; }
.bed-body { display: flex; flex-direction: column; gap: 16px; }

.bed-action-btn {
  padding: 11px 14px; border-radius: 10px; font-size: 14px; font-weight: 600;
  background: var(--bg, #111); border: 1px solid var(--line); color: var(--text);
  cursor: pointer;
}
.bed-action-btn:active { transform: scale(.97); background: var(--line); }
.bed-action-btn.bed-save { background: var(--accent, #3a6); border-color: transparent; color: #fff; }
.bed-action-btn:disabled { opacity: .55; }
/* The Move button becomes a red STOP while a manual move is in flight. */
.bed-action-btn.moving {
  background: var(--bad, #e0524a); border-color: transparent; color: #fff;
  animation: bedglow 1.2s ease-in-out infinite;
}

/* Firmness: a big horizontal slider + Set */
.bed-firmness, .bed-manual {
  display: flex; flex-direction: column; gap: 12px;
  padding: 14px; border-radius: 14px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.bed-firm-row { display: flex; flex-direction: column; gap: 10px; }
.bed-slider-label { font-size: 13px; color: var(--muted); display: flex; justify-content: space-between; }
.bed-slider-val { color: var(--text); font-weight: 700; }
.bed-slider { width: 100%; accent-color: var(--accent, #3a6);
  /* Claim the touch gesture for the slider — without this the browser may treat a
     drag as a scroll and the thumb never moves. */
  touch-action: none; }
.bed-slider-lg { height: 30px; }                 /* easier to grab */

/* Manual head/foot: two BIG vertical sliders side by side, then Move */
.bed-vsliders { display: flex; justify-content: space-around; gap: 16px; padding: 6px 0 2px; }
.bed-vslider-col { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.bed-vslider-val { font-weight: 700; font-size: 16px; color: var(--text); }
.bed-vslider-label { font-size: 13px; color: var(--muted); }
/* Vertical range input — orient + appearance-slider-vertical for cross-browser,
   sized big for thumb control. */
.bed-vslider {
  -webkit-appearance: slider-vertical; appearance: slider-vertical;
  writing-mode: vertical-lr; direction: rtl;          /* top = max */
  width: 44px; height: 150px; accent-color: var(--accent, #3a6);  /* 25% shorter */
  /* CRITICAL for vertical sliders: a vertical drag IS what scrolling wants, so
     without touch-action:none the page/drawer scrolls instead of the thumb moving.
     This hands the vertical drag to the slider. */
  touch-action: none;
}
.bed-move-btn { width: 100%; }

.bed-msg { font-size: 13px; color: var(--muted); min-height: 18px; white-space: pre-line; }
.bed-msg.good { color: var(--ok); }
.bed-msg.warn { color: var(--warn); }

/* Presets: header with a ＋, then the list */
.bed-presets-head { display: flex; align-items: center; justify-content: space-between; }
.bed-section-label {
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
}
.bed-add-btn {
  width: 36px; height: 36px; border-radius: 10px; font-size: 20px; line-height: 1;
  background: var(--accent, #3a6); border: none; color: #fff; cursor: pointer;
}
.bed-add-btn:active { transform: scale(.95); }
.bed-presets { display: flex; flex-direction: column; gap: 8px; }
.bed-empty { color: var(--muted); font-size: 13px; padding: 4px 2px; }
/* Hub-offline banner: the bed lost its SleepIQ cloud link, so moves won't land. */
.bed-hub-offline {
  background: rgba(255, 176, 32, .12); border: 1px solid rgba(255, 176, 32, .5);
  color: #ffcf7a; font-size: 13px; line-height: 1.35; font-weight: 600;
  border-radius: 10px; padding: 10px 12px; margin-bottom: 10px;
}
.bed-preset-row { display: flex; gap: 8px; align-items: stretch; }
.bed-preset-run {
  flex: 1; display: flex; flex-direction: column; gap: 2px; text-align: left;
  padding: 12px 14px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer;
}
.bed-preset-run:active { background: var(--line); }
/* The preset the bed is currently in (last-commanded position) — a clear accent
   outline + tint so you can see which setting you're in at a glance. */
.bed-preset-run.active {
  border-color: var(--accent, #3a6);
  box-shadow: inset 0 0 0 1px var(--accent, #3a6);
  background: var(--panel-2);                                   /* fallback */
  background: color-mix(in srgb, var(--accent, #3a6) 16%, var(--panel-2));
}
/* The running (red Stop) state wins over active if both are somehow set. */
.bed-preset-run.running.active { box-shadow: none; }
/* A running preset turns RED and pulses while its move is in flight — it's now the
   STOP control, so the colour must clearly signal "tap to stop", not "active". */
.bed-preset-run.running {
  background: var(--bad, #e0524a); border-color: transparent; color: #fff;
  animation: bedglow 1.2s ease-in-out infinite;
}
@keyframes bedglow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(224,82,74,.0); }
  50%      { box-shadow: 0 0 14px 2px rgba(224,82,74,.7); }
}
.bed-preset-name { font-weight: 600; font-size: 15px; }
.bed-preset-vals { font-size: 12px; color: var(--muted); }
.bed-preset-run.running .bed-preset-vals { color: rgba(255,255,255,.85); }
.bed-preset-edit, .bed-preset-del {
  flex: none; width: 44px; border-radius: 12px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  font-size: 16px; cursor: pointer;
}
.bed-preset-edit:active, .bed-preset-del:active { background: var(--line); }

.bed-name {
  padding: 11px 12px; border-radius: 10px; font-size: 15px; width: 100%;
  background: var(--bg, #111); border: 1px solid var(--line); color: var(--text);
}

/* Name modal (＋ capture / ✎ rename) — centered card over the drawer */
.bed-modal {
  position: fixed; inset: 0; z-index: 60;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.5); padding: 24px;
}
.bed-modal-card {
  width: min(92%, 360px); display: flex; flex-direction: column; gap: 12px;
  padding: 18px; border-radius: 16px;
  background: var(--panel, #1b1b1b); border: 1px solid var(--line);
}
.bed-modal-title { font-weight: 700; font-size: 16px; }
.bed-modal-sub { font-size: 13px; color: var(--muted); }
.bed-modal-sliders { display: flex; flex-direction: column; gap: 12px; }
.bed-slider-row { display: flex; flex-direction: column; gap: 6px; }
.bed-modal-actions { display: flex; gap: 10px; justify-content: flex-end; }

/* --- Daily report drawer (Settings → Daily report) --- */
.set-row-badge {
  margin-left: auto; min-width: 22px; height: 22px; padding: 0 6px;
  border-radius: 11px; background: #b3541e; color: #fff; font-size: 12px;
  font-weight: 800; display: inline-flex; align-items: center; justify-content: center;
}
.report-panel { width: min(94%, 540px); }
.report-body {
  padding: calc(env(safe-area-inset-top, 0px) + 8px) 14px 18px;
  display: flex; flex-direction: column; gap: 10px;
  overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
}
.report-issues-head, .report-section-head {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 14px; font-weight: 800; color: #fff; letter-spacing: .02em;
  padding-bottom: 4px; margin-top: 6px; border-bottom: 1px solid var(--line);
}
.report-section-head { margin-top: 14px; }
.report-mini-btn {
  font-size: 12px; font-weight: 700; padding: 5px 10px; border-radius: 9px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer;
}
.report-mini-btn:active { background: var(--line); }
.report-mini-btn:disabled { opacity: .6; }
.report-issues { display: flex; flex-direction: column; gap: 8px; }
.report-clean { color: var(--muted); font-size: 14px; padding: 12px 2px; }
.report-issue {
  display: flex; align-items: center; gap: 10px; padding: 10px 12px;
  border-radius: 10px; background: var(--panel-2);
  border: 1px solid var(--line); border-left: 3px solid var(--muted);
}
/* Colourblind-safe severity: vivid vermilion (not muddy red) + amber; the left bar is
   3px wide AND the meta line names the severity, so colour isn't the only cue. */
.report-issue.sev-error { border-left-color: #ff5a2c; border-left-width: 4px; }
.report-issue.sev-warn  { border-left-color: #f0b429; border-left-width: 4px; }
.report-issue-main { flex: 1; min-width: 0; }
.report-issue-title { font-size: 14px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-issue-meta { font-size: 11px; color: var(--muted); margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.report-ack {
  flex: none; font-size: 12px; font-weight: 700; padding: 6px 12px;
  border-radius: 9px; background: var(--panel); border: 1px solid var(--line);
  color: var(--text); cursor: pointer;
}
.report-ack:active { background: var(--line); }
.report-ack:disabled { opacity: .6; }
.report-datebar { display: flex; }
.report-date {
  flex: 1; font-size: 14px; padding: 8px 10px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.report-summary { display: flex; flex-direction: column; gap: 8px; }
.report-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.report-chip {
  font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--line); color: var(--text); background: var(--panel-2);
}
/* Colours are colourblind-safe (Okabe–Ito family) AND never the ONLY signal — the chip
   text is the subsystem name and the detail is in the tooltip; the raw report uses ✓/◐/✗
   glyphs too. bad = vivid vermilion (reads clearly for deuter/protanopia, unlike the old
   muddy pink); warn = amber for partial/degraded (some-but-not-all). */
.report-chip.ok   { border-color: #2e8b57; color: #57d98a; }
.report-chip.warn { border-color: #b8860b; color: #ffc84a; background: rgba(240,180,40,.12); }
.report-chip.bad  { border-color: #ff5a2c; color: #ff7a4d; background: rgba(255,90,44,.14); }
.report-chip.unk  { opacity: .6; }
.report-counts { font-size: 12px; color: var(--muted); }
.report-analyze {
  font-size: 13px; font-weight: 700; padding: 9px 12px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; margin-top: 2px;
}
.report-analyze:active { background: var(--line); }
.report-analyze:disabled { opacity: .6; }
.report-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  margin-top: 2px; }
.report-actions .report-analyze { flex: 1; margin-top: 0; }
.report-analysis {
  font-size: 13px; line-height: 1.45; color: var(--text); white-space: pre-wrap;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  padding: 12px;
}
/* "Read the report" — the raw deterministic report, grouped into sections so the actual
   errors are visible directly (and it's what the AI analysis reasons over). */
.report-detail {
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  padding: 10px 12px; display: flex; flex-direction: column; gap: 12px;
}
.rd-sec-h { font-size: 12px; font-weight: 800; color: #fff; letter-spacing: .04em;
  text-transform: uppercase; margin-bottom: 4px; }
.rd-row { display: flex; gap: 8px; align-items: baseline; font-size: 12px;
  padding: 3px 0; line-height: 1.35; }
.rd-row + .rd-row { border-top: 1px solid rgba(255,255,255,.05); }
.rd-mark { flex: none; width: 1.1em; font-weight: 800; }
.rd-mark.ok { color: #57d98a; } .rd-mark.warn { color: #f0b429; }
.rd-mark.bad { color: #ff5a2c; } .rd-mark.unk { color: var(--muted); }
.rd-k { flex: none; min-width: 3.5em; color: var(--muted); font-variant-numeric: tabular-nums; }
.rd-v { flex: 1; min-width: 0; color: var(--text); word-break: break-word; }
.rd-fail .rd-v { color: #ffb59d; }
.rd-sample { color: var(--muted); font-size: 11px; }
.report-config { margin-top: 12px; font-size: 13px; color: var(--muted); }
.report-config summary { cursor: pointer; padding: 6px 0; }
.report-cfg-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 2px; gap: 12px;
}
.report-cfg-row input[type="time"] {
  font-size: 14px; padding: 6px 8px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}

/* --- voice picker (Settings → Voice) --- */
.voice-pick-row { display: flex; gap: 8px; align-items: center; }
.voice-pick-select {
  flex: 1; background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); font: inherit; font-size: 14px; padding: 10px 12px;
  border-radius: 12px;
}
.voice-pick-test {
  flex: none; padding: 10px 14px; border-radius: 12px; cursor: pointer;
  font-size: 14px; font-weight: 600; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
}
.voice-pick-test:active { background: var(--line); }

.voice-fav-list { display: flex; flex-direction: column; gap: 6px; }
.voice-fav-row {
  display: flex; gap: 8px; align-items: center; padding: 10px 12px;
  border-radius: 12px; background: var(--panel-2); border: 1px solid var(--line);
}
.voice-fav-row.chosen { border-color: #ffb347; }
.voice-fav-label {
  flex: 1; display: flex; align-items: center; gap: 8px; cursor: pointer;
  font-size: 14px; color: var(--text); background: none; border: none; text-align: left;
  font: inherit; padding: 0;
}
.voice-fav-radio { flex: none; }
.voice-fav-test {
  flex: none; padding: 6px 10px; border-radius: 10px; cursor: pointer;
  font-size: 13px; color: var(--text); background: var(--panel); border: 1px solid var(--line);
}
.voice-fav-remove {
  flex: none; padding: 4px 8px; border-radius: 10px; cursor: pointer;
  font-size: 13px; color: var(--muted); background: none; border: none;
}

.voice-advanced { margin-top: 10px; display: flex; flex-direction: column; gap: 10px; }
.voice-adv-step {
  flex: none; width: 36px; height: 36px; border-radius: 10px; cursor: pointer;
  font-size: 18px; font-weight: 700; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
}
.voice-adv-step:active { background: var(--line); }
.voice-adv-input {
  flex: 1; min-width: 0; background: var(--panel-2); border: 1px solid var(--line);
  color: var(--text); font: inherit; font-size: 14px; padding: 8px 10px;
  border-radius: 12px; text-align: center;
}

/* --- battery drawer + tappable chip --- */
.chip-tap { cursor: pointer; }
.batt-panel { padding: calc(env(safe-area-inset-top, 0px) + 16px) 14px 14px;
  display: flex; flex-direction: column; gap: 10px;
  overflow-y: auto; -webkit-overflow-scrolling: touch; }
.batt-note { color: var(--muted); font-size: 12px; }
/* Doors & locks drawer — same island clearance + scroll as the battery drawer. */
.sec-panel { padding: calc(env(safe-area-inset-top, 0px) + 16px) 14px 14px;
  display: flex; flex-direction: column; gap: 2px;
  overflow-y: auto; -webkit-overflow-scrolling: touch; }
.batt-list { list-style: none; margin: 0; padding: 0; display: flex;
  flex-direction: column; gap: 8px; }
.batt-item { display: flex; align-items: center; justify-content: space-between;
  gap: 10px; padding: 10px 12px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); }
.batt-item.security { border-color: var(--bad); }
.batt-name { font-size: 14px; }
.batt-lvl { font-weight: 600; font-size: 14px; color: var(--warn); }
.batt-item.security .batt-lvl { color: var(--bad); }
.batt-none { color: var(--muted); font-size: 14px; }

/* --- scene grouping: manual on top, automated below a separator ---
   The "Automated" header matches the room headers (bold/white/14px) for
   consistency, but keeps a top rule since it separates the automated group. */
.scene-sep {
  margin-top: 14px; font-size: 14px; font-weight: 700; color: var(--text);
  border-top: 1px solid var(--line); padding-top: 12px;
}
.scene-sep-note { font-size: 12px; font-weight: 400; color: var(--muted); }
#scene-auto-buttons { margin-top: 8px; }
/* automated scenes look quieter so the everyday manual ones read as primary */
.scene-btn.automated { opacity: .82; }

/* --- collapsible section (Sense & Respond Pathways) --- */
.ctl-collapse {
  width: 100%; display: flex; align-items: center; justify-content: space-between;
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 12px; text-transform: uppercase; letter-spacing: .06em;
  padding: 4px 0;
}
.ctl-chevron { font-size: 11px; }
/* Ring's "paused" badge, shown on the COLLAPSED header. Ring quiet mode is one of the four
   fences and survives a restart, so its state must be readable without expanding the block.
   Warning-coloured rather than muted: this is a kill switch being ENGAGED, not a label. */
.ring-quiet-state { color: var(--warn, #e0a030); text-transform: none; letter-spacing: 0; }
.ctl-collapse-body { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; }

/* --- reverse-scene button (appears after a run) --- */
.ctl-reverse {
  align-self: flex-start; margin-top: 4px;
  padding: 9px 14px; border-radius: 999px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--accent); color: var(--accent);
  font-size: 14px; font-weight: 600;
}
.ctl-reverse:active { background: var(--line); }
.ctl-reverse:disabled { opacity: .5; }

/* --- Home Status drawer (full matrix) --- */
.home-panel { padding: calc(env(safe-area-inset-top, 0px) + 16px) 20px 14px 14px;
  display: flex; flex-direction: column;
  gap: 2px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  /* Wider than the other drawers so each device row is a big, easy tap target on a
     phone — effectively full-screen on a phone, capped on a tablet/desktop. */
  width: min(100%, 560px); }
.home-age { font-size: 11px; color: var(--muted); font-weight: 400; margin-left: auto; }
.drawer-head .icon-btn { width: 60px; height: 60px; font-size: 34px; }
.home-body { display: flex; flex-direction: column; gap: 2px; padding-top: 4px;
  overflow-x: hidden; }
.home-sec {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--muted); margin: 14px 0 4px; border-top: 1px solid var(--line);
  padding-top: 10px;
}
.home-sec:first-child { border-top: none; margin-top: 2px; padding-top: 0; }

/* Collapsible section header (twisty). Larger + tappable across its full width. */
.home-sec-twisty {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  font-size: 13px; padding: 12px 4px 12px 2px; -webkit-tap-highlight-color: transparent;
}
/* One glyph that ROTATES on open — guarantees identical size collapsed vs open
   (▼ and ▶ render at different optical sizes in most fonts). */
.home-twisty { font-size: 22px; line-height: 1; color: var(--accent); width: 26px;
  flex: 0 0 auto; text-align: center; font-weight: 700;
  display: inline-block; transition: transform .12s ease; }
.home-sec-twisty.open .home-twisty { transform: rotate(90deg); }
/* Section/room name reads naturally (not the all-caps the base .home-sec uses). */
.home-sec-label { flex: 1; text-transform: none; letter-spacing: 0; font-size: 17px;
  color: var(--text); font-weight: 700; }
.home-sec-twisty.open { color: var(--text); }
.home-sec-content.hidden { display: none; }

/* By type / By room segmented toggle at the top of the drawer. */
/* Grouping toggle — always present at the top of the drawer. */
.home-grouptoggle { margin: 2px 0 10px; }
.home-grpseg {
  display: flex; gap: 0; border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden;
}
.home-grpbtn {
  flex: 1; padding: 13px 8px; font-size: 16px; font-weight: 600;
  background: var(--panel-2); color: var(--muted); border: none; cursor: pointer;
}
.home-grpbtn.active { background: var(--accent); color: #fff; }

/* Customize (gear) edit mode */
.icon-btn.active { background: var(--accent); color: #fff; }
/* (Gear now matches the uniform .icon-btn / .drawer-head .icon-btn size.) */
.home-edit-hint {
  font-size: 12px; color: var(--muted); line-height: 1.4;
  padding: 6px 10px; margin: 2px 0 8px;
  background: var(--panel-2); border: 1px solid var(--line);
  border-radius: var(--radius);
}
/* Section header carrying a parent (all/some/none) checkbox while editing. */
.home-sec-editrow { display: flex; align-items: center; gap: 8px; }
.home-sec-cb { width: 18px; height: 18px; accent-color: var(--accent); }
/* Per-row checkbox + greying of a hidden item while editing. */
.home-rowwrap.home-editrow { display: flex; align-items: center; gap: 10px; }
.home-rowwrap.home-editrow > .home-row { flex: 1; }
.home-item-cb { width: 18px; height: 18px; accent-color: var(--accent); flex: 0 0 auto; }
.home-rowwrap.home-row-off { opacity: .45; }
.home-row { display: flex; justify-content: space-between; gap: 12px;
  /* Tall, finger-friendly tap targets — the drawer scrolls, so generous rows are fine. */
  padding: 21px 4px; min-height: 40px; border-bottom: 1px solid rgba(255,255,255,.04);
  align-items: center; }
/* Let a long device name wrap instead of forcing a horizontal scrollbar that
   hides the value off-screen. The label takes available width and wraps; the
   value keeps its own line and can wrap too. */
.home-k { color: var(--text); font-size: 17px; flex: 1 1 auto;
  min-width: 0; overflow-wrap: anywhere; }
.home-v { font-size: 16px; text-align: right; flex: 0 1 auto;
  min-width: 0; overflow-wrap: anywhere; color: var(--muted); }
.home-v.ok { color: var(--ok); }
.home-v.warn { color: var(--warn); }
.home-v.bad { color: var(--bad); }
/* Optimistic "issued, not yet confirmed" — the row shows the INTENDED state right
   away, dimmed + gently pulsing, until the verify job reconciles it. */
.home-v.home-v-pending { opacity: .55; animation: vpulse 1.1s ease-in-out infinite; }
@keyframes vpulse { 0%,100% { opacity: .4; } 50% { opacity: .75; } }
.home-batt-link { cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }

/* Device Settings gear on a home row (Sprint 31). Absolutely positioned at the far
   right so it doesn't consume the row's flex space; the row reserves room with extra
   right-padding. Faint by default, brightens on hover/focus. It sits ABOVE the row's
   own click via stopPropagation in JS. The ~44px hit area comes from .dev-gear::before. */
.home-row.has-gear { position: relative; padding-right: 34px; }
.dev-gear.dev-gear-row {
  position: absolute; top: 50%; right: 2px; transform: translateY(-50%);
  width: 26px; height: 26px; background: rgba(255,255,255,.06);
  border: 1px solid rgba(255,255,255,.10); color: var(--muted);
  backdrop-filter: none; opacity: .6;
}
.dev-gear.dev-gear-row:hover, .dev-gear.dev-gear-row:focus-visible {
  opacity: 1; background: rgba(255,255,255,.12); color: var(--text);
}
.dev-gear.dev-gear-row:active { transform: translateY(-50%) scale(.92); }

/* --- Home Status: tappable rows + inline controls --- */
.home-rowwrap { border-bottom: 1px solid rgba(255,255,255,.04); }
.home-rowwrap .home-row { border-bottom: none; }
.home-tappable { cursor: pointer; }
.home-tappable .home-v::after { content: " ›"; color: var(--muted); font-size: 20px; }
.home-tappable:active { background: var(--panel-2); }
.home-ctl {
  display: flex; flex-wrap: wrap; gap: 16px; align-items: center;
  padding: 6px 0 18px;
}
.home-btn {
  padding: 14px 26px; border-radius: 999px; cursor: pointer; font-size: 16px;
  font-weight: 600; background: var(--panel-2); border: 1px solid var(--accent);
  color: var(--accent); min-height: 30px;
}
.home-btn:active { background: var(--line); }
.home-btn:disabled { opacity: .5; }
.home-btn.danger { border-color: var(--bad); color: var(--bad); }
/* A shade Open/Close button while its move is running: it IS the Stop control. */
.home-btn.stopping { border-color: var(--bad); color: var(--bad); font-weight: 600; }

/* The dedicated, always-visible STATUS LIGHT (setCtlStatus) — a mid-century-modern
   JEWEL indicator (think a lit communicator lens): a glossy domed lens with a bright
   highlight, a bezel ring, and a coloured glow when lit. One fixed slot per surface so
   state changes never cause reflow.
   COLOUR-BLIND SAFE: green is a blue-shifted "traffic-light" green (distinguishable from
   red for red/green CVD), and the states ALSO differ by behaviour (idle=dim, working=
   pulsing, done=steady glow, fail=fast urgent pulse) so hue is never the only signal.
     idle    = dim unlit jewel (grey)
     working = blue, gentle pulse
     done    = bluish-green, steady glow
     fail    = red, fast pulse */
/* Register --jewel as a real <color> so it can ANIMATE smoothly (the live jewel fades
   blue↔white). An unregistered custom property snaps at keyframes instead of tweening. */
@property --jewel {
  syntax: "<color>"; inherits: false; initial-value: #6b7280;
}
.ctl-status {
  --jewel: #23262b;                  /* idle lens colour: dark/near-black so a LIT
                                         gem reads as a dramatic bright contrast, not
                                         a bright idle competing with it (Pete, 2026-07-04) */
  --bezel-w: 2.5px;                  /* ring thickness — scaled down per size below */
  display: inline-block; width: 22px; height: 22px; border-radius: 50%;
  flex: 0 0 auto; position: relative; box-sizing: border-box;
  /* Jeweled panel-lamp look (Pete's reference photo, 2026-07-04 — chosen as
     "Variant B" from a 3-way comparison, see howie/design/jewel-redesign/):
     a chrome/nickel bezel ring around a faceted glass gem dome. Kept to ONE
     real element (no child divs) — .ctl-status is used bare across ~9 call
     sites in index.html/app.js — the gem dome is a ::after pseudo-element
     instead.
     TRIED FIRST AND DIDN'T WORK (2026-07-04, kept as a note so it isn't
     re-attempted): layering the ring + gem as separate background LAYERS on
     one element via the `<gradient> border-box, <gradient> padding-box`
     shorthand. That per-layer box keyword only affects the gradient's
     POSITIONING reference and how far content underneath the border shows
     through — it does NOT clip a layer to "just the ring area". The
     border-box conic-gradient layer (being listed first/topmost) painted
     across the ENTIRE circle and fully hid the gem colour underneath —
     every jewel rendered flat grey, verified via screenshot before catching
     it. A real inset ::after circle is the correct way to get a ring you can
     actually see color show through the middle of.
     Saved restore point for the PRE-redesign version: [[howie_jewel_baseline_v173]]. */
  background: conic-gradient(from 45deg, #f2f4f6, #aab0b8, #e4e8ec, #8a919c, #f2f4f6);
  box-shadow: 0 1px 2px rgba(0,0,0,.5);
}
.ctl-status::after {
  content: ''; position: absolute; inset: var(--bezel-w); border-radius: 50%;
  /* Redeclared here, not just inherited from the base .ctl-status: --jewel
     inheritance from a parent into its OWN ::after proved unreliable in
     testing (2026-07-04) — changing the base's idle --jewel had NO effect on
     the actually-rendered ::after until redeclared directly on this
     selector, confirmed via getComputedStyle(el, '::after') on the live
     page. Same root cause as the per-state ::after rules below already
     working around; the idle/default case needed the same treatment. */
  --jewel: #23262b;
  /* Facet wedges (soft-edged stand-in for the mockup's crisp clip-path
     triangles, which needed child elements) blended via overlay so they
     modulate the gem's own brightness — reading as glass catching light —
     INSTEAD of plain alpha-compositing on top, which buried the colour
     almost entirely (a real bug hit + fixed 2026-07-04, before this was a
     ::after with its own clean background stack). */
  /* Facet highlight strength is toned down from the first pass (2026-07-04):
     at .75/.4 opacity via `overlay`, a strong white highlight blended over a
     DARK idle gem still came out looking bright silver — overlay lets a
     bright top layer dominate a dark base more than plain alpha would,
     confirmed by screenshotting the idle jewel after darkening --jewel and
     seeing no visible change. Also cut the gem's own highlight mix from 60%
     white to 35% — the same compounding problem in the base gradient itself,
     independent of the facet layer. Both together let idle read as dark,
     while a bright/saturated LIT colour still shows a clear catch-light.
     ROUND 3 (still 2026-07-04, "just a glint instead of the nearly 50%
     highlight"): dropped the secondary highlight entirely and shrank the
     main one to a small, TIGHT-falloff dot (12%/9% ellipse, transparent by
     40% instead of 65%). Turned out the perceived size barely changed —
     confirmed via a zoomed screenshot — because the SOFT FALLOFF (a smooth
     fade from bright core to transparent) is what `overlay` blend mode
     brightens across, not just the nominal ellipse footprint; shrinking the
     ellipse without tightening the falloff barely shrank the visible glow.
     ROUND 4: a hard colour stop close to the core (opaque out to 60% of the
     ellipse, THEN fades) so it drops off sharply — an actual small dot, not
     a small-labelled-but-still-soft wash. */
  /* The remaining soft-feathered layer (the shadow ellipse, opposite corner
     from the glint) got the same "shrink + dim" pass as the glint itself
     (Pete, 2026-07-04): smaller footprint (60%/50% -> 35%/28%) and lower
     opacity (.5 -> .3) so it reads as a soft shading cue, not another big
     wash competing with the glint.
     ROUND 5 (real iPhone Safari screenshot, 2026-07-04): still a big soft
     halo in real WebKit — noticeably softer/larger than headless-Chromium
     testing here showed (this dev box can't install WebKit's system libs to
     test that engine directly — no sudo). Rather than keep guessing off a
     testing setup that's already under-represented the real device twice,
     cut both layers hard: glint ellipse ~40% smaller again + opacity down;
     shadow ellipse ~45% smaller again + opacity down.
     ROUND 6 (still after the real-iPhone screenshot): dropped `overlay` blend
     mode entirely for the highlight/shadow layers — overlay's exact math is a
     known area where browser engines diverge on colour-space handling, and is
     the likely reason headless-Chromium testing here kept under-representing
     what Safari actually rendered across FIVE rounds. Plain alpha compositing
     (background-blend-mode removed -> defaults to normal) is simple, predictable,
     identical across engines: a rgba(255,255,255,X) layer adds exactly X worth
     of white, full stop, no per-engine "how bright does overlay make this"
     guessing. Cut the glint opacity hard too (.3 -> .16) since normal blending
     is more visually potent than overlay was at the same alpha. */
  background:
    radial-gradient(ellipse 6% 4% at 30% 20%, rgba(255,255,255,.16) 0%, transparent 70%),
    radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--jewel) 35%, #fff) 0%,
      var(--jewel) 45%, color-mix(in srgb, var(--jewel) 60%, #000) 100%);
  box-shadow: inset 0 0 3px rgba(0,0,0,.4);
}
/* In a modal head, let the title take the slack so the jewel sits just left of the ✕. */
.modal-head .modal-title { flex: 1 1 auto; }
.modal-head .ctl-status { margin-right: 10px; }
/* Lit states are a SLOW ON/OFF BLINK (colour fades out and back), no glow pulse.
   Colours are bright NEON; green stays blue-shifted (colour-blind safe) and red is a
   deep, saturated neon red — clearly distinct from the green.
   Everything (colour AND the blink animation) is set on the ::after (the GEM), not
   the base .ctl-status (the RING) — the ring stays a constant, always-full-contrast
   chrome bezel; only the gem lights/blinks/dims. Two real bugs hit + fixed here
   2026-07-04, both only visible on the actual page (not an isolated CSS snippet):
   (1) setting --jewel on the base and relying on inheritance into ::after silently
   failed while the base ALSO had a running `animation:` on it — verified via
   getComputedStyle(el, '::after') reading the IDLE default colour instead of the
   state colour. Setting --jewel directly on ::after sidesteps that. (2) the OLD
   "dim opacity on the base element" approach dimmed the ring too, so idle read as a
   uniform blur instead of a crisp always-lit chrome ring around a dark gem. */
.ctl-status.working::after { --jewel: #12a9ff; opacity: 1; animation: jewelblink-working 1.6s ease-in-out infinite; }  /* neon blue */
.ctl-status.done::after    { --jewel: #00ff9c; opacity: 1; }                 /* neon traffic-light green (Sense/per-panel success) */
/* RESPOND success: a brief bright-WHITE flash (~1s) instead of green — green is the
   Sense colour, so the Respond jewel never goes green. A quick fade-in-out pulse. */
.ctl-status.flash::after   { --jewel: #ffffff; opacity: 1; animation: jewelflash 1s ease-out 1; }
.ctl-status.fail::after    { --jewel: #ff0033; opacity: 1; animation: jewelblink-fail 0.8s ease-in-out infinite; }   /* deep neon red */
/* Thinking jewel: warm neon yellow, gentle blink while Howie's LLM reasons. Distinct
   hue from the blue Activity + green Background jewels, and it BLINKS (behaviour cue too,
   so it's not hue-only). */
.ctl-status.thinking::after { --jewel: #ffd21a; opacity: 1; animation: jewelblink-thinking 1.2s ease-in-out infinite; }
/* Background jewel: neon green with a SLOW blink while a background job/loop runs
   (steady-slow distinguishes "running in the background" from the Activity jewel's
   faster working-blue and its steady done-green). */
.ctl-status.running::after  { --jewel: #00e07a; opacity: 1; animation: jewelblink-running 2.4s ease-in-out infinite; }
/* Background HEARTBEAT: a brief FULL-INTENSITY green blip on each real ~7s house sweep
   (status_live._poll_loop) — a true "Howie is alive + sweeping" tick. Crisp and quick
   (~0.6s): snaps to full-bright green, then relaxes to idle grey. NOT a fake timer — it
   fires only when a real sweep completes, so if Howie dies the blips stop. Distinct from
   .running's steady repeating blink (that's for longer background WORK like a report). */
.ctl-status.beat::after { --jewel: #00e07a; opacity: 1; animation: jewelbeat 0.6s ease-out 1; }
/* Live-stream jewel: a PULSING blue↔white glow meaning "streaming now / on air". The gem
   colour animates blue→white→blue with the halo brightening in step, so it reads as an
   active, breathing "live" light (not a static dot). */
.ctl-status.live::after { opacity: 1; animation: jewellive 1.6s ease-in-out infinite; }
@keyframes jewellive {
  0%, 100% { --jewel: #1e9dff; box-shadow: 0 0 0 1px rgba(0,0,0,.4), 0 0 6px 1px rgba(90,180,255,.7); }
  50%      { --jewel: #eaf6ff; box-shadow: 0 0 0 1px rgba(0,0,0,.4), 0 0 11px 2px rgba(200,235,255,.95); }
}
/* ROUND 6 (2026-07-04, real iPhone screenshot: "when the light blinks it fills the
   bezel as well as the jewel"): filter:brightness() was ITSELF the bug — filter
   forces a new compositing layer that, at least in Safari/WebKit, visibly bled
   past the gem's own circular clip into the ring/bezel during the animation. Ironic
   given filter was originally adopted specifically to AVOID a bleed-through bug
   (opacity letting the ring show through) — it fixed that one but introduced this
   one. Fixed properly this time: since --jewel is @property-registered as a real
   animatable <color> (see the top of this section), animate --jewel DIRECTLY
   between its lit colour and a precomputed dark version of the SAME hue. No
   filter, no opacity — just a colour tween on the property that already drives the
   gem's own background gradient. Nothing to bleed past the gem's box because
   nothing outside the gem's own painted pixels ever changes. One keyframe per
   colour (can't reference var(--jewel) inside a keyframe target value) — this
   redundancy is the cost of avoiding filter/opacity entirely. */
@keyframes jewelblink-working  { 0%,100% { --jewel: #12a9ff; } 50% { --jewel: #063a5c; } }
@keyframes jewelblink-fail     { 0%,100% { --jewel: #ff0033; } 50% { --jewel: #5c0012; } }
@keyframes jewelblink-thinking { 0%,100% { --jewel: #ffd21a; } 50% { --jewel: #5c4a08; } }
@keyframes jewelblink-running  { 0%,100% { --jewel: #00e07a; } 50% { --jewel: #00502b; } }
/* No transform/scale here (Pete, 2026-07-04): the ORIGINAL design (before the
   jeweled-lamp bezel existed) scaled the dot up 1.25x for the bright flash — with
   no bezel to bump into, that read fine. Now that the gem sits INSET inside a
   fixed-size chrome ring, scaling it up 25% makes it visibly overflow past the
   ring's inner edge into the bezel itself. The colour jump alone (dark idle ->
   full-bright green) still reads as a clear flash/heartbeat; dropping the scale
   keeps it contained to the gem's own boundary, which is the part that actually
   needed fixing, not the double-pulse behaviour itself. */
@keyframes jewelbeat {
  0%   { --jewel: #00e07a; }
  100% { --jewel: #23262b; }
}
/* Respond SUCCESS pulse: snap to bright white, then ease back to the idle-dark gem over
   ~1s. Colour-only (no scale), matching the other jewels so the white stays inside the
   chrome ring. */
@keyframes jewelflash {
  0%   { --jewel: #ffffff; }
  60%  { --jewel: #cfe0ea; }
  100% { --jewel: #23262b; }
}

/* The labeled three-jewel strip by the title. Generous gap so the labels don't crowd. */
.jewel-strip { display: inline-flex; align-items: flex-start; gap: 14px; }
.jewel-cell { display: inline-flex; flex-direction: column; align-items: center; gap: 2px; }
.jewel-cell .ctl-status { width: 18px; height: 18px; --bezel-w: 2px; }
.jewel-lbl { font-size: 9px; font-weight: 700; letter-spacing: .02em; color: var(--muted);
  text-transform: uppercase; line-height: 1; }
/* Label-free, larger variant for the main topbar strip (Pete, 2026-07-04 — "in the
   interest of beauty ... drops the labels and is larger, fills the whole row
   vertically"). 30px sits close to the "Howie" title's own 28px font-size, so the
   jewels read as filling the row rather than looking undersized next to it, without
   forcing the row itself taller. align-items:center (not flex-start, unlike the
   labeled strip which top-aligns jewel-over-caption pairs) since there's no caption
   to align against anymore — the jewels centre on the row like the title does. */
/* ONE jewel language everywhere (Pete, 2026-07-06): SENSE → THINK → RESPOND, same
   30px jewel + small caption on the front page AND in every drawer header. */
/* Fixed-width cells so the JEWELS are evenly spaced no matter how wide the label
   text is (otherwise a longer word like "Respond" skews the apparent gaps). Each
   jewel sits centered in its own equal slot; the labels can be wider than the
   jewel without pushing neighbours. */
.jewel-cell { display: inline-flex; flex-direction: column; align-items: center;
  gap: 3px; width: 44px; }
.jewel-cell .ctl-status { width: 30px; height: 30px; --bezel-w: 3px; }
.jewel-lbl { font-size: 9px; font-weight: 700; letter-spacing: .02em;
  color: var(--muted); text-transform: uppercase; line-height: 1; white-space: nowrap; }
.jewel-strip-large { align-items: flex-start; gap: 8px; }
/* Drawer-header strip: same labeled cells, sits just left of the ✕ close button.
   Full-width centered row on its own line beneath the title (see .drawer-head). */
.jewel-strip-drawer { display: flex; align-items: flex-start; gap: 8px;
  flex: 1 1 100%; justify-content: center; }
/* In a plain inline panel (no modal header) anchor the jewel top-right of the panel.
   Inset a touch more so the larger jewel's glow halo isn't clipped at the edge. */
.home-ctl { position: relative; }
.home-ctl > .ctl-status { position: absolute; top: 10px; right: 12px; margin: 0; }
.home-setrow { display: flex; align-items: center; gap: 10px; width: 100%; margin-top: 4px; }
.home-slider { flex: 1; accent-color: var(--accent); height: 28px; touch-action: none; }
.home-slideval { font-size: 13px; color: var(--muted); min-width: 38px; text-align: right; }

/* --- Slider-device popup (shades + dimmable lights): big easy controls --- */
.slider-card {
  width: min(94%, 380px); display: flex; flex-direction: column;
  max-height: 88vh;
}
.slider-body { display: flex; flex-direction: column; align-items: stretch; gap: 14px; }
/* Top: Open/Close or On/Off, big and side-by-side. */
.slider-top { display: flex; gap: 12px; }
.slider-top .home-btn { flex: 1; padding: 14px 0; font-size: 16px; text-align: center; }
/* Middle: the fat vertical slider + its big live %. */
.slider-mid {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 6px 0;
}
.slider-bigval { font-size: 34px; font-weight: 700; color: var(--text); }
.slider-vert {
  /* Vertical orientation that keeps a TALL bounding box (top = max), which
     sliderValueFromTouch already reads. writing-mode is the modern cross-browser way.
     appearance:none on the INPUT (not just the thumb) so desktop Chrome stops drawing
     its tiny native thumb and uses our big white oval — matching the iOS look. */
  -webkit-appearance: none; appearance: none;
  writing-mode: vertical-lr; direction: rtl;
  width: 56px; height: 46vh; max-height: 360px;
  background: transparent; touch-action: none; cursor: pointer;
}
/* The visible track: a wide rounded pill the thumb rides inside. */
.slider-vert::-webkit-slider-runnable-track {
  width: 44px; border-radius: 22px;
  background: var(--panel-2); border: 1px solid var(--line);
}
.slider-vert::-moz-range-track {
  width: 44px; border-radius: 22px;
  background: var(--panel-2); border: 1px solid var(--line);
}
/* The thumb: a big white oval (iOS-style), easy to grab. */
.slider-vert::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 52px; height: 34px; border-radius: 17px;
  background: #fff; border: 1px solid rgba(0,0,0,.18);
  box-shadow: 0 2px 6px rgba(0,0,0,.45);
  margin-left: -5px;          /* centre the 52px thumb over the 44px track */
}
.slider-vert::-moz-range-thumb {
  width: 52px; height: 34px; border-radius: 17px;
  background: #fff; border: 1px solid rgba(0,0,0,.18);
  box-shadow: 0 2px 6px rgba(0,0,0,.45);
}
/* Bottom: Set (wide) + pin. */
.slider-bottom { display: flex; gap: 12px; align-items: center; }
.slider-set { flex: 1; padding: 14px 0; font-size: 16px; text-align: center; }
.home-input {
  flex: 1; padding: 9px 12px; font-size: 15px; border-radius: 10px;
  background: var(--panel); border: 1px solid var(--line); color: var(--text);
}

/* Thermostat radial dial (Nest-style): drag the ring to set the temperature; the
   target shows in the center, mode buttons below. AUTO = two handles (heat-to /
   cool-to), HEAT/COOL = one, OFF = greyed ring. */
.tstat { display: flex; flex-direction: column; align-items: center; gap: 10px;
         width: 100%; margin-top: 6px; }
.tstat-dial { touch-action: none; max-width: 220px; width: 100%; height: auto; }
.tstat-track { stroke: var(--line); }
.tstat-fill { stroke: var(--accent); }
.tstat[data-mode="heat"] .tstat-fill { stroke: #e8915a; }   /* warm */
.tstat[data-mode="cool"] .tstat-fill { stroke: #5aa6e8; }   /* cool */
.tstat[data-mode="auto"] .tstat-fill { stroke: #7bbf8a; }   /* range */
.tstat-h { fill: #fff; stroke: var(--panel-2); stroke-width: 3; cursor: grab; }
.tstat-h:active { cursor: grabbing; }
.tstat-read { fill: var(--text); font-size: 30px; font-weight: 600; }
.tstat-sub { fill: var(--muted); font-size: 12px; }
.tstat-modes { display: flex; gap: 6px; }
.tstat-mode {
  flex: 1; padding: 8px 6px; border-radius: 10px; font-size: 13px;
  text-transform: capitalize; cursor: pointer;
  background: var(--panel); border: 1px solid var(--line); color: var(--muted);
}
.tstat-mode.active {
  color: var(--text); border-color: var(--accent); background: var(--panel-2);
}
.tstat-set { width: 100%; margin: 4px 0 12px; padding: 12px 0; font-size: 15px; text-align: center; }

/* brief highlight when a status chip jumps you to a Home Status row */
.home-flash > .home-row { animation: homeflash 1.2s ease; border-radius: 8px; }
@keyframes homeflash {
  0%, 30% { background: rgba(91,140,255,.22); }
  100% { background: transparent; }
}

/* --- Camera drawer: snapshot grid + live-view modal --------------------- */
/* As wide as the viewport allows so the single-column snapshots render as large
   as possible. On a phone the 100% dominates; the 900px cap only bites on wide
   screens (tablet/desktop), where a full-bleed drawer would be absurdly big. */
.cam-panel { width: min(100%, 900px); }
/* Single column so each snapshot fills the drawer width (bigger image). Grouped
   by home: a .cam-home header precedes each property's cameras. */
.cam-grid {
  flex: 1; overflow-y: auto; padding: 4px 8px 16px;
  display: flex; flex-direction: column; gap: 12px;
}
.cam-home {
  font-size: 13px; font-weight: 800; color: #fff;
  text-transform: uppercase; letter-spacing: .06em;
  padding: 14px 2px 6px; margin-top: 8px;
  border-top: 2px solid rgba(255, 255, 255, .35);
}
.cam-home:first-child { border-top: none; margin-top: 0; padding-top: 2px; }

/* Camera reorder (Edit mode). */
.cam-editbtn {
  margin-left: 8px; padding: 7px 12px; border-radius: 10px; font-size: 13px; font-weight: 600;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text); cursor: pointer;
}
.cam-editbtn.cam-save { background: #00778a; border-color: transparent; color: #fff; }
.cam-editbtn:active { transform: scale(.97); }
.cam-edit-hint { color: var(--muted); font-size: 13px; padding: 6px 2px 10px; }
.cam-edit-row {
  display: flex; align-items: center; gap: 12px; padding: 12px 12px; margin-bottom: 8px;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px;
  cursor: grab; user-select: none;
}
.cam-edit-row.dragging { opacity: .5; cursor: grabbing; border-color: var(--accent); }
/* touch-action:none on the handle stops iOS from scrolling the page when you grab it to
   drag (the browser otherwise claims the pointer-down as a scroll gesture). */
.cam-drag-handle { font-size: 22px; color: var(--muted); cursor: grab; touch-action: none;
  padding: 4px 6px; margin: -4px -2px; }
.cam-edit-name { font-size: 15px; font-weight: 600; flex: 1; }
.cam-edit-home { font-size: 12px; color: var(--muted); }

.cam-tile {
  position: relative;   /* anchor for the settings gear overlay */
  display: flex; flex-direction: column; gap: 6px; padding: 0;
  background: none; border: none; color: var(--text); cursor: pointer;
  text-align: center; -webkit-tap-highlight-color: transparent;
}
.cam-tile:active { transform: scale(.98); }

/* Device Settings gear (Sprint 31): a small semi-transparent overlay in the
   snapshot's top-right. Sits over the frame, brightens on hover/focus. Its own
   click opens settings (the tile body still opens live view — see app.js). */
.dev-gear {
  position: absolute; top: 6px; right: 6px; z-index: 2;
  width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 15px; line-height: 1; color: #fff;
  background: rgba(20, 22, 28, .45); backdrop-filter: blur(2px);
  border: 1px solid rgba(255, 255, 255, .18);
  cursor: pointer; opacity: .72; transition: opacity .12s, background .12s;
  -webkit-tap-highlight-color: transparent;
}
/* Bigger TAP TARGET without a bigger gear: an invisible ~44px hit area (Apple's
   min touch size) centred on the 26px glyph. Extends the clickable region past the
   visible circle so it's easy to hit; the gear itself stays small. */
.dev-gear::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 44px; height: 44px; transform: translate(-50%, -50%);
}
.dev-gear:hover, .dev-gear:focus-visible { opacity: 1; background: rgba(20, 22, 28, .7); outline: none; }
.dev-gear:active { transform: scale(.92); }

/* Recordings reel (Sprint 32): the SAME translucent treatment as the settings gear,
   parked in the snapshot's top-LEFT so the two never collide. Opens that camera's
   recorded playback. Rendered only for playback viewers (Pete/Terri) — see app.js. */
/* Dropped below the gear's top so it clears the camera's own burnt-in timestamp
   overlay (Pete, on the real tiles) — but one line HIGHER than the original 40px,
   which sat lower than it needed to (Pete, 2026-07-21). */
.dev-reel { right: auto; left: 6px; top: 22px; font-size: 14px; padding: 0; }
.dev-reel svg { display: block; }

/* Recordings drawer title: "Recordings" on line 1, the camera name on line 2 — a
   single "Recordings — <name> · <home>" line overflowed a phone's width. */
.rec-title { line-height: 1.15; }
.rec-cam-name {
  display: block; font-size: 15px; font-weight: 600; letter-spacing: 0;
  opacity: 0.82; margin-top: 2px;
  /* One line, ellipsized: a very long name still can't push the drawer wide. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ---- Recordings drawer (Sprint 32) ----
   Opened FROM INSIDE the camera drawer, so it must stack ABOVE it. Plain .drawer is
   z-index 20 — the same layer as the camera drawer — so it rendered behind and looked
   like nothing happened (found on the real iOS client). Sits at 45: above every drawer
   AND above the live modal (40), so it's never buried whatever's already open. */
#rec-drawer { z-index: 45; }
.rec-panel { width: min(100%, 900px); }

/* Date picker (v407): ONE button naming the selected day, opening a popup calendar.
   Replaced a horizontal scrolling strip of day chips — on desktop Chrome that strip's
   scrollbar sat right on top of the timeline (Pete), and it could only ever offer the
   days we'd already loaded. The calendar pages to any month, and a blue dot marks the
   days that have footage. */
.rec-datebar { display: flex; justify-content: center; padding: 2px 0 8px; }
.rec-date-btn {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 7px 14px; border-radius: 999px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  font-size: 14px; font-weight: 600; white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
}
.rec-date-btn:hover { border-color: var(--accent, #3a7afe); }
.rec-date-caret { font-size: 10px; color: var(--muted); }

/* The popup itself. Absolutely positioned over the drawer so opening it never reflows
   the video or the timeline — the layout must not jump when you check a date. */
.rec-cal {
  position: absolute; z-index: 60; left: 50%; transform: translateX(-50%);
  top: 96px; width: min(320px, calc(100% - 32px));
  background: var(--panel, #12141a); border: 1px solid var(--line);
  border-radius: 12px; box-shadow: 0 10px 34px rgba(0,0,0,.55); padding: 10px 12px 12px;
}
.rec-cal.hidden { display: none; }
.rec-cal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.rec-cal-month { font-size: 14px; font-weight: 700; color: var(--text); }
.rec-cal-nav {
  width: 30px; height: 30px; border-radius: 8px; cursor: pointer; font-size: 17px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  -webkit-tap-highlight-color: transparent;
}
.rec-cal-nav:hover { border-color: var(--accent, #3a7afe); }
.rec-cal-dow, .rec-cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.rec-cal-dow span { text-align: center; font-size: 11px; color: var(--muted); padding-bottom: 4px; }
.rec-cal-pad { aspect-ratio: 1; }
/* Each day is a square with room BELOW the number for its dot, so the dot never crowds
   the digits. A day with no footage is disabled, not hidden — the shape of the month
   stays readable. */
.rec-cal-day {
  position: relative; aspect-ratio: 1; border-radius: 8px; cursor: pointer;
  background: transparent; border: 1px solid transparent; color: var(--text);
  font-size: 13px; padding-bottom: 6px; -webkit-tap-highlight-color: transparent;
}
.rec-cal-day:disabled { color: var(--muted); opacity: .38; cursor: default; }
.rec-cal-day.has:hover, .rec-cal-day.stills:hover { background: var(--panel-2); }
.rec-cal-day.today { border-color: var(--line); }
.rec-cal-day.sel { background: var(--accent, #3a7afe); color: #fff; border-color: transparent; }
/* Two dot colours for two kinds of coverage (Pete, 2026-07-28):
     BLUE  = recorded events (clips).
     GREY  = stills only — the keyframe spine, captured on a schedule regardless of
             motion. No events happened, but there IS a timelapse to scrub, which is
             exactly what you want on a quiet day. Selectable, just not blue. */
.rec-cal-day.has::after,
.rec-cal-day.stills::after {
  content: ""; position: absolute; left: 50%; bottom: 5px; transform: translateX(-50%);
  width: 5px; height: 5px; border-radius: 50%; background: var(--accent, #3a7afe);
}
.rec-cal-day.stills::after { background: var(--muted, #8b93a7); }
.rec-cal-day.stills { color: rgba(255,255,255,.78); }
.rec-cal-day.sel::after { background: #fff; }
.rec-cal-key {
  display: flex; align-items: center; gap: 6px; justify-content: center;
  flex-wrap: wrap; margin-top: 9px; font-size: 11px; color: var(--muted);
}
.rec-cal-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent, #3a7afe); }
.rec-cal-dot.stills { background: var(--muted, #8b93a7); }

/* Timeline: a 24h track. Bars = recorded segments, ticks = motion, line = scrub point.
   Pointer events (not html5 drag) — iOS Safari is the primary device. */
/* v407: the timeline sits BELOW the viewport, and the space the day-chip strip used to
   occupy is spent here instead — a real gap between the video and the track. That gap is
   what the loupe floats up into: it renders ABOVE the scrub point (so a finger never
   covers what it's pointing at).
   v410: the gap is now sized to CLEAR the loupe entirely. At 18px the 62px loupe
   overlapped the video by ~52px while scrubbing, covering the very frame you were
   scrubbing to. --rec-loupe-h and --rec-loupe-gap are the single source of truth: the
   margin below and the float offset in app.js are both derived from them, so the two
   cannot drift apart. */
:root { --rec-loupe-h: 62px; --rec-loupe-gap: 10px; }
.rec-timeline-wrap { margin: calc(var(--rec-loupe-h) + var(--rec-loupe-gap) * 2) 0 10px; }
.rec-timeline {
  position: relative; height: 44px; border-radius: 8px; overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line);
  cursor: pointer; touch-action: none;   /* we handle the drag ourselves */
}
.rec-bars, .rec-ticks { position: absolute; inset: 0; }
.rec-bar { position: absolute; top: 8px; height: 20px; background: var(--accent, #3a7afe); opacity: .75; border-radius: 2px; }
.rec-bar.motion { background: #f5a623; }
.rec-tick { position: absolute; bottom: 2px; width: 2px; height: 8px; background: #f5a623; opacity: .9; }
/* Snapshot ticks — one per archived still (~every 5 min). Deliberately QUIETER and SHORTER
   than a detection tick: there are ~280 of them on a full day versus a handful of events,
   so at the same weight they would drown out the thing you are actually looking for. Their
   job is to show COVERAGE — "we were watching and it was quiet" — not to draw the eye. */
.rec-spine-tick {
  position: absolute; bottom: 2px; width: 1px; height: 4px;
  background: var(--muted, #8b93a7); opacity: .45;
}
.rec-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: #fff; box-shadow: 0 0 4px rgba(0,0,0,.6); pointer-events: none; }
.rec-scale { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); padding: 2px 2px 0; }

/* Loupe — floats above the finger during a scrub. A 10-second clip is 0.01% of a 24h
   track; without magnification you cannot land on it. Zoom is ~20x (REC_LOUPE_SPAN). */
.rec-timeline-wrap { position: relative; }
/* The loupe is back to a compact bars-only magnifier (v409). Its thumbnail moved to the
   main viewport, which shows the same still far bigger — but the ZOOM stays, because a
   clip is 0.17–1.4px wide on a 24h track (measured) and otherwise unhittable. */
.rec-loupe {
  position: absolute; z-index: 5; width: 220px; height: 62px;
  transform: translateX(-50%); pointer-events: none;
  background: rgba(18, 20, 26, .96); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 6px 22px rgba(0,0,0,.5);
  padding: 6px; overflow: hidden;
}
.rec-loupe.hidden { display: none; }
.rec-loupe-track { position: absolute; left: 6px; right: 6px; top: 8px; height: 26px; background: var(--panel-2); border-radius: 4px; overflow: hidden; }
.rec-loupe-track .rec-bar { top: 3px; height: 20px; }
/* The crosshair marks the exact instant under the finger. */
.rec-loupe-center { position: absolute; left: 50%; top: 8px; height: 26px; width: 2px; background: #fff; transform: translateX(-50%); }
.rec-loupe-time { position: absolute; left: 0; right: 0; bottom: 6px; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; color: var(--text); }

/* VCR controls */
.rec-controls { display: flex; align-items: center; gap: 10px; justify-content: center; padding: 4px 0 10px; flex-wrap: wrap; }
.rec-btn {
  min-width: 46px; height: 36px; border-radius: 8px; cursor: pointer;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  font-size: 14px; -webkit-tap-highlight-color: transparent;
}
.rec-btn:active { transform: scale(.95); }
.rec-btn[disabled] { opacity: .4; cursor: default; }
.rec-play { min-width: 60px; font-size: 17px; }   /* the primary control — a touch bigger */
/* fullscreen sits inline with the rest — the whole control row stays centered below
   the video (Pete). No margin-left:auto, which shoved it right and broke the centering. */
/* The selected clip's date+time — the single most important readout in the drawer, so
   it's bright white and larger than the surrounding chrome (Pete: "clear and obvious"). */
.rec-time {
  font-variant-numeric: tabular-nums; font-size: 15px; font-weight: 600;
  color: #fff; min-width: 190px; text-align: center; line-height: 1.25;
  text-shadow: 0 1px 2px rgba(0,0,0,.5);
}
.rec-time .rec-time-date { display: block; font-size: 11px; font-weight: 500; color: rgba(255,255,255,.72); }
.rec-time.idle { color: var(--muted); font-weight: 400; text-shadow: none; }
/* PLAYING indicator (2026-07-29). Several cameras burn no timestamp into the frame, so on a
   static scene there is no way to tell running footage from a paused still. A pulsing dot
   beside a ticking clock answers that at a glance — and because the clock is driven by the
   video's own currentTime, both stop the instant the footage does. */
.rec-time.live::before {
  content: ""; display: inline-block; width: 7px; height: 7px; border-radius: 50%;
  background: #4ade80; margin-right: 7px; vertical-align: 1px;
  animation: rec-live-pulse 1.4s ease-in-out infinite;
}
@keyframes rec-live-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) {
  .rec-time.live::before { animation: none; }   /* the dot still marks "playing" */
}

/* Download progress — a Tapo clip is a real SD-card pull, so it needs a real bar. */
.rec-progress { height: 4px; border-radius: 2px; background: var(--line); overflow: hidden; margin-bottom: 8px; }
.rec-progress-bar { height: 100%; width: 0; background: var(--accent, #3a7afe); transition: width .2s ease; }

.rec-stage { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 10px; overflow: hidden; }
.rec-stage video { width: 100%; height: 100%; display: block; object-fit: contain; background: #000; position: relative; z-index: 1; }
/* v408: the scrub still — the frame under the playhead, painted OVER the video while you
   drag so the main viewport shows the instant you're pointing at (not a stale frame from
   the last clip). z-index 2 puts it above the video; recPlay drops it once a real clip
   has decoded. object-fit:contain matches the video so the image doesn't jump size. */
.rec-scrub-still {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: contain; background: #000; z-index: 2; pointer-events: none;
}
/* Snapshot backdrop: fills the stage behind the video so there's an image instantly while
   the first segment buffers. Hidden (via app.js) once real frames play. */
.rec-backdrop { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; z-index: 0; }
.rec-note { font-size: 12px; color: var(--muted); padding: 8px 2px 0; min-height: 18px; text-align: center; }

/* The snapshot frame: fixed 16:9 box so the grid stays even while images load. */
.cam-shot {
  position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden;
  background: var(--panel-2); border: 1px solid var(--line); border-radius: 12px;
}
.cam-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Loading: a soft pulsing placeholder until the first frame paints. */
.cam-shot.loading { animation: campulse 1.3s ease-in-out infinite; }
.cam-shot.loading img { opacity: 0; }
@keyframes campulse {
  0%, 100% { background: var(--panel-2); }
  50% { background: var(--line); }
}
/* Failed/offline: a camera glyph + 'offline', no broken-image icon. */
.cam-shot.failed { animation: none; }
.cam-shot.failed::after {
  content: "📷  offline"; position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 13px; letter-spacing: .02em;
}
.cam-name {
  font-size: 13px; font-weight: 600; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Snapshot age line under the name (Apple-Home style "4m ago"), ticked live.
   NOTE: no `:empty { display:none }` — WebKit/iOS Safari doesn't reliably
   re-evaluate :empty after textContent is set via JS, which hid the line on
   iPhone while Chrome showed it. We always reserve the row (min-height) and just
   leave it blank when there's no age yet. */
.cam-age {
  font-size: 11px; color: var(--muted); line-height: 1.2; min-height: 1.2em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Live-view modal: full-screen, above everything (its own z-index). */
.cam-live {
  position: fixed; inset: 0; z-index: 40; background: rgba(0,0,0,.92);
  display: flex; flex-direction: column;
  padding: calc(env(safe-area-inset-top, 0px) + 8px) 8px
           max(env(safe-area-inset-bottom), 8px);
}
.cam-live.hidden { display: none; }
.cam-live-head {
  display: flex; align-items: center; gap: 10px; padding: 4px 6px 10px;
}
.cam-live-name { font-size: 16px; font-weight: 700; color: var(--text); margin-right: auto; }
/* Header buttons: full-screen + close, both on the right (name takes the left). */
.cam-live-fs { font-size: 22px; }
/* Every interactive control in the live view (header buttons, PTZ pad + presets, action
   buttons): consistently kill the iOS long-press callout/selection/loupe, remove the tap
   highlight, and make taps register immediately (touch-action: manipulation avoids the
   ~300ms double-tap delay). Ensures the controls are foreground + responsive on iOS, where
   the native popups otherwise hijack a press. */
.cam-live-head .icon-btn,
.cam-ptz .ptz-btn,
.cam-ptz .ptz-preset,
.cam-ptz .ptz-action-btn {
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
/* Live indicator BELOW the image: a LARGE jewel + "Live" label. The whole indicator is
   HIDDEN until the stream is actually playing (.on), so "Live" never shows prematurely. */
.cam-live-ind {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  padding: 10px 0 4px; visibility: hidden; min-height: 34px;
}
.cam-live-ind.on { visibility: visible; }
.cam-live-ind .ctl-status { width: 24px; height: 24px; --bezel-w: 2.5px; }
.cam-live-lbl { font-size: 15px; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text); }
.cam-live-stage {
  /* GRID, not flex: the <img> and <video> are stacked in one cell so we can swap
     between them without either leaving layout. See the note below on flicker. */
  flex: 1; display: grid; place-items: center;
  min-height: 0;
}
/* When the video is natively fullscreen (iOS) it detaches from the stage — nothing to
   style here; the browser's own player takes over. Desktop/Android fullscreen the stage. */
.cam-live-stage:fullscreen { background: #000; padding: 0; }
.cam-live-stage:-webkit-full-screen { background: #000; }
/* ⚠️ `object-fit: contain` alone does NOT stop a stretch here. In a flex row, an <img> or
   <video> is a flex ITEM, and `align-items: center` + the browser's default
   `align-self: stretch` fallback can size the BOX to something other than the media's own
   ratio — object-fit then letterboxes INSIDE a wrong-shaped box, which reads as squashed.
   Ring's 1:1-ish frames show it worst (Pete, 2026-08-03: "the aspect ratio of ring is forced
   into a non 16x9 ratio"), because they differ most from the 16:9 the stage assumes.
   `flex: 0 1 auto` + `width/height: auto` keeps the element's INTRINSIC ratio. */
.cam-live-stage img,
.cam-live-stage video {
  flex: 0 1 auto;
  width: auto; height: auto;
  max-width: 100%; max-height: 100%; object-fit: contain;
  border-radius: 12px; background: #000;
}
/* ⚠️ NEVER `display: none` THESE TWO — that is the flicker.
 *
 * The <img> (frozen still) and <video> (live stream) are the SAME picture from the
 * viewer's point of view, and we swap between them whenever a Ring camera drops its
 * uplink. Toggling `display` removes an element from layout, which reflows the other
 * one and paints a blank frame in between — visible as a black blink on every
 * transition, in BOTH directions (Pete, 2026-08-04: "it flickers when switching to
 * the still frame and switching out of the still frame").
 *
 * Instead they are STACKED in the same grid cell and cross-faded with opacity. Both
 * stay laid out at all times, so there is no reflow and never an empty frame; the
 * browser simply shows whichever is opaque. `visibility` keeps the hidden one out of
 * the accessibility tree and off the hit-testing path. */
.cam-live-stage img,
.cam-live-stage video {
  grid-area: 1 / 1;                 /* stack both in one cell */
}
/* ⚠️ NO OPACITY TRANSITION, AND NO `visibility` — both cause the blink.
 *
 * A cross-fade sounds right but is wrong here: for 120ms BOTH layers are partly
 * transparent, and they are stacked over a black background, so the composite is
 * DARKER than either — read as a flicker. `visibility` made it worse still, because
 * visibility is not animatable: it applied instantly while opacity was mid-fade, so
 * the video disappeared before the still was opaque, leaving a genuine gap.
 *
 * An INSTANT opacity swap on two already-laid-out, already-painted layers has no
 * intermediate state at all — one frame shows the video, the next shows the still,
 * with nothing blank or blended in between. The hidden layer stays in the layout and
 * keeps its last painted content; it is simply not composited. */
.cam-live-stage img[hidden],
.cam-live-stage video[hidden] {
  display: block;                   /* override the [hidden] UA rule */
  opacity: 0;
  pointer-events: none;
}

/* Live-view fallback note (e.g. Ring cameras have no live stream → snapshot). */
.cam-live-note {
  color: var(--muted); font-size: 13px; text-align: center;
  padding: 8px 12px 2px; min-height: 1em;
}

/* PTZ controls (only shown for pan/tilt cameras). Preset chips + a NEWS d-pad. */
.cam-ptz { padding: 6px 12px max(env(safe-area-inset-bottom), 10px); }
.cam-ptz.hidden { display: none; }
.cam-ptz-presets {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
  margin-bottom: 12px;
}
/* Preset chips: tap-to-go only (delete lives in Manage, below the d-pad). */
.ptz-preset {
  font-size: 13px; padding: 8px 14px; border-radius: 999px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.ptz-preset:active { transform: scale(.96); background: var(--line); }
/* The view we're currently ON — highlighted like a bed preset (accent ring + tint). */
.ptz-preset.active {
  border-color: var(--accent, #3a6); color: var(--text); font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--accent, #3a6);
  background: var(--panel-2);                                    /* fallback */
  background: color-mix(in srgb, var(--accent, #3a6) 16%, var(--panel-2));
}
.ptz-preset-none { color: var(--muted); font-size: 12px; padding: 8px 2px; }

/* Save / Manage action row — UNDER the d-pad. */
.cam-ptz-actions {
  display: flex; gap: 8px; align-items: center; justify-content: center;
  margin-top: 12px; flex-wrap: wrap;
}
.ptz-save-name {
  flex: 1 1 140px; min-width: 0; max-width: 200px; font-size: 13px;
  padding: 8px 12px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
}
.ptz-action-btn {
  font-size: 13px; padding: 8px 14px; border-radius: 10px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; -webkit-tap-highlight-color: transparent; white-space: nowrap;
}
.ptz-action-btn:active { transform: scale(.97); background: var(--line); }
.ptz-action-btn.active { background: var(--accent, #3a6); color: #fff; border-color: transparent; }

/* Manage (delete) list — each preset with its own clearly-separate Delete. */
.cam-ptz-manage { margin-top: 10px; }
.cam-ptz-manage.hidden { display: none; }
.ptz-manage-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 4px; border-top: 1px solid var(--line);
}
.ptz-manage-name { font-size: 13px; color: var(--text); }
.ptz-manage-del {
  font-size: 12px; padding: 6px 12px; border-radius: 8px;
  background: none; border: 1px solid #e6666644; color: #e66; cursor: pointer;
}
.ptz-manage-del:active { background: #e6666622; }
.ptz-manage-del:disabled { opacity: .5; }
.ptz-manage-empty { color: var(--muted); font-size: 12px; padding: 6px 2px; }

/* Disabled until the live stream is playing — dim + ignore taps. */
.cam-ptz.ptz-pending { opacity: .4; pointer-events: none; }
/* D-pad: a 3x3 grid with arrows on the cross + stop in the middle. */
.cam-ptz-pad {
  display: grid; grid-template-columns: repeat(3, 56px);
  grid-template-rows: repeat(3, 56px); gap: 6px; justify-content: center;
  /* iOS: a HOLD on a PTZ button must NOT trigger the selection highlight, magnifier
     loupe, or callout (copy/look up/translate) menu — they hijack the hold-to-pan.
     Needs the -webkit- prefixes (same as the rover d-pad). */
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.ptz-btn {
  font-size: 20px; border-radius: 12px; background: var(--panel-2);
  border: 1px solid var(--line); color: var(--text); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  -webkit-tap-highlight-color: transparent; touch-action: none;
  -webkit-touch-callout: none; -webkit-user-select: none; user-select: none;
}
.ptz-btn:active { background: var(--accent, #3a6); color: #fff; transform: scale(.95); }
.ptz-up    { grid-column: 2; grid-row: 1; }
.ptz-left  { grid-column: 1; grid-row: 2; }
.ptz-stop  { grid-column: 2; grid-row: 2; font-size: 14px; }
.ptz-right { grid-column: 3; grid-row: 2; }
.ptz-down  { grid-column: 2; grid-row: 3; }

/* ===== FULLSCREEN recordings drawer (.rec-fs on #rec-drawer) ==================
   Mirrors .cam-fs exactly (see below) so the two behave identically: fill the viewport,
   black out, lift above everything. In PORTRAIT with auto-rotate OFF, app.js additionally
   sets an inline 90° rotate + swapped width/height — the SAME _applyCamFsRotation() live
   view uses, so the rule can't drift between them.

   The video and the transport controls stay; the day picker, timeline and notes are hidden
   — in a rotated, filled view you want the picture and ◀◀ ▶ ▶▶, not a 24-hour scrub track
   squeezed sideways. Everything is restored on exit (class + inline styles cleared in JS). */
#rec-drawer.rec-fs {
  z-index: 3000;
  top: 0; left: 0; right: auto; bottom: auto;   /* anchor at 0,0 for the rotate math */
  width: 100vw; height: 100vh; height: 100dvh;
  padding: 0; background: #000;
}
#rec-drawer.rec-fs .drawer-panel {
  width: 100%; height: 100%; max-width: none; max-height: none;
  border-radius: 0; padding: 0; background: #000; display: flex; flex-direction: column;
}
#rec-drawer.rec-fs .rec-stage {
  flex: 1 1 auto; min-height: 0; padding: 0; border-radius: 0; aspect-ratio: auto;
}
#rec-drawer.rec-fs .rec-stage video,
#rec-drawer.rec-fs .rec-backdrop {
  width: 100%; height: 100%; object-fit: contain; border-radius: 0; background: #000;
}
/* Keep the transport row (it's how you step clips); float it over the video, translucent. */
#rec-drawer.rec-fs .rec-controls {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3010;
  justify-content: center; gap: 10px;
  padding: 8px 12px max(env(safe-area-inset-bottom, 0px), 10px);
  background: linear-gradient(to top, rgba(0,0,0,.65), rgba(0,0,0,0));
}
#rec-drawer.rec-fs .rec-btn { background: rgba(0,0,0,.55); border-color: transparent; }
/* Close ✕ floats top-right, above the video. */
#rec-drawer.rec-fs .drawer-head {
  position: absolute; top: 0; right: 0; left: auto; z-index: 3010;
  padding: max(env(safe-area-inset-top, 0px), 10px) 12px 8px;
  background: none; border-bottom: none;
}
#rec-drawer.rec-fs .drawer-title { display: none; }
#rec-drawer.rec-fs #rec-close {
  background: rgba(0,0,0,.5); border-radius: 50%; width: 44px; height: 44px; font-size: 20px;
}
/* Hide the chrome that doesn't belong in a filled, possibly-rotated view. The date bar
   and calendar join the list in v407 — a popup calendar over a rotated fullscreen video
   would be positioned against the untransformed box and land somewhere wrong. */
#rec-drawer.rec-fs .rec-datebar,
#rec-drawer.rec-fs .rec-cal,
#rec-drawer.rec-fs .rec-timeline-wrap,
#rec-drawer.rec-fs .rec-note,
#rec-drawer.rec-fs .rec-time,
#rec-drawer.rec-fs #rec-progress { display: none !important; }

/* The rover drawer's ⛶ sits LEFT of the ✕ (both are absolutely-positioned .icon-btns in
   a centered head, so without this they'd stack on top of each other at right:10px). */
.rover-fs-btn { right: 76px !important; }
/* In fullscreen the ✕ is the only header control that earns its place. */
.rover-fs .rover-fs-btn { display: none; }

/* ===== FULLSCREEN rover piloting view (.rover-fs on #rover-drawer) ============
   The THIRD surface on the shared faux-fullscreen machinery (live view, recordings, now
   the rover) — same _fsTarget()/_applyCamFsRotation() code in app.js, not a copy.

   WHY: in landscape the ordinary drawer layout puts a 4:3 stage above a d-pad and an
   actions row, so on a phone you get mostly CONTROLS and a letterboxed sliver of video —
   backwards for piloting, where the video IS the instrument. Fullscreen inverts that: the
   feed fills the screen and the controls float ON it, translucent.

   The controls AUTO-HIDE after a few seconds and come back on a touch (app.js drives the
   .controls-idle class) — but only ever the OVERLAY chrome. Nothing here may hide the
   d-pad or STOP in a way that outlives a touch: losing the e-stop on a moving robot is
   not an acceptable failure. Idle only FADES them (pointer-events stay live), so even
   mid-fade the STOP is tappable. */
#rover-drawer.rover-fs {
  z-index: 3000;                                 /* above every drawer/overlay */
  top: 0; left: 0; right: auto; bottom: auto;    /* anchor at 0,0 for the rotate math */
  /* Landscape / OS-rotated default. app.js overrides width/height + sets the 90° rotate
     inline for the portrait + rotation-locked case, and clears them on exit. */
  width: 100vw; height: 100vh; height: 100dvh;
  padding: 0; background: #000;
}
.rover-fs .rover-panel {
  position: absolute; inset: 0; width: 100%; max-width: none;
  padding: 0; background: #000; border: 0; box-shadow: none; animation: none;
}
.rover-fs .drawer-scrim { display: none; }       /* nothing behind to dim */
/* The stage becomes the whole screen; the video fills it. object-fit:contain keeps the
   720p feed's aspect honest (letterboxed, never stretched or cropped). */
.rover-fs .rover-live-stage {
  position: absolute; inset: 0; aspect-ratio: auto; padding: 0; border-radius: 0;
}
.rover-fs .rover-live-stage video,
.rover-fs .rover-live-stage img {
  width: 100%; height: 100%; object-fit: contain; border-radius: 0; background: #000;
}
/* Chrome that earns no space over a piloting feed. The telemetry bar goes too — battery
   and link matter, but not at the cost of the video; portrait still has them. */
.rover-fs .drawer-title,
.rover-fs .rover-status,
.rover-fs .rover-speed-lbl,
.rover-fs #rover-video-toggle { display: none !important; }

/* --- the floating overlay controls --------------------------------------- */
/* Everything that stays gets pinned over the video and lifted above it. */
.rover-fs .drawer-head {
  position: absolute; top: 0; right: 0; left: auto; z-index: 3010;
  padding: max(env(safe-area-inset-top, 0px), 10px) 12px 8px; background: none;
}
.rover-fs #rover-close {
  background: rgba(0,0,0,.5); border-radius: 50%; width: 44px; height: 44px;
  font-size: 20px; -webkit-tap-highlight-color: transparent;
}
/* D-pad + speed: bottom-LEFT, the natural thumb position holding a phone in landscape. */
.rover-fs .rover-drive {
  position: absolute; z-index: 3010; margin: 0; width: auto;
  left: calc(max(env(safe-area-inset-left, 0px), 8px) + 10px);
  bottom: calc(max(env(safe-area-inset-bottom, 0px), 8px) + 10px);
  padding: 8px; gap: 14px;
  background: rgba(0,0,0,.28); border-radius: 18px; backdrop-filter: blur(2px);
}
/* Actions (Photo / Record / Follow) → bottom-RIGHT, the other thumb. */
.rover-fs .rover-actions {
  position: absolute; z-index: 3010; margin: 0; padding: 8px;
  right: calc(max(env(safe-area-inset-right, 0px), 8px) + 10px);
  bottom: calc(max(env(safe-area-inset-bottom, 0px), 8px) + 10px);
  background: rgba(0,0,0,.28); border-radius: 18px; backdrop-filter: blur(2px);
  flex-wrap: wrap; justify-content: flex-end; max-width: 46vw;
}
/* Translucent-dark treatment for the controls themselves, so they read over any frame. */
.rover-fs .rover-dpad-btn,
.rover-fs .rover-btn {
  background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.22); color: #fff;
}
.rover-fs .rover-dpad-btn.pressed { background: var(--accent); border-color: transparent; }
.rover-fs .rover-dpad-stop { background: rgba(192,57,43,.92); border-color: transparent; }
.rover-fs .rover-follow[aria-pressed="true"] { background: var(--accent); border-color: transparent; }
.rover-fs .rover-msg { color: #fff; }

/* AUTO-HIDE: fade the overlay chrome when idle, restore on touch (app.js toggles the
   class after ~3s). Opacity ONLY — pointer-events stay enabled so a faded STOP is still
   a working STOP, and the first touch both reveals and acts. */
.rover-fs .drawer-head,
.rover-fs .rover-drive,
.rover-fs .rover-actions { transition: opacity .45s ease; }
.rover-fs.controls-idle .drawer-head,
.rover-fs.controls-idle .rover-drive,
.rover-fs.controls-idle .rover-actions { opacity: 0; }
/* Never fade the controls away while the rover is actually MOVING under manual drive —
   hiding the stick mid-drive is exactly when you need to see it. app.js sets .driving. */
.rover-fs.driving.controls-idle .rover-drive { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .rover-fs .drawer-head, .rover-fs .rover-drive, .rover-fs .rover-actions { transition: none; }
}

/* ===== FULLSCREEN camera live view (.cam-fs on #cam-live) =====================
   CSS-DRIVEN faux-fullscreen (works on iOS, where the Fullscreen API on a <div> doesn't).
   The modal is already position:fixed inset:0; .cam-fs maximizes it to the viewport, blacks
   it out, and lifts it above everything. In PORTRAIT, app.js also sets an inline transform
   that ROTATES the modal 90° and swaps its width/height so the feed fills the screen
   sideways (landscape) regardless of the device rotation-lock — our HTML controls (✕, PTZ)
   stay in the DOM and responsive the whole time. Non-essential chrome (name, LIVE
   indicator, PTZ presets/Save/Manage, notes) is hidden; only the ✕ (top-right) and a
   near-translucent PTZ d-pad (bottom-left) remain. On exit the class + inline transform are
   cleared in JS, so the drawer is never left rotated or zoomed. */
#cam-live.cam-fs {
  z-index: 3000;                 /* above every drawer/overlay */
  top: 0; left: 0; right: auto; bottom: auto;   /* anchor at 0,0 for the rotate math */
  /* Default (landscape viewport / OS auto-rotated for us): fill the screen, no transform.
     For the PORTRAIT case app.js OVERRIDES width/height + sets the 90° rotate transform
     inline; on exit / landscape it clears those inline styles and we fall back to these. */
  width: 100vw; height: 100vh; height: 100dvh;
  padding: 0; background: #000;
}
/* The stage fills the whole modal and centers the media. */
.cam-fs .cam-live-stage { flex: 1 1 auto; padding: 0; min-height: 0; }
.cam-fs .cam-live-stage img,
.cam-fs .cam-live-stage video {
  max-width: 100%; max-height: 100%; width: 100%; height: 100%;
  object-fit: contain; border-radius: 0; background: #000;
}
/* Header: keep only the ✕ (top-right), float it over the image; hide the name + ⛶. */
.cam-fs .cam-live-head {
  position: absolute; top: 0; right: 0; left: auto; z-index: 3010;
  padding: max(env(safe-area-inset-top, 0px), 10px) 12px 8px;
  background: none;
}
.cam-fs .cam-live-name,
.cam-fs .cam-live-fs { display: none; }
.cam-fs #cam-live-close {
  background: rgba(0,0,0,.5); border-radius: 50%; width: 44px; height: 44px;
  font-size: 20px; -webkit-tap-highlight-color: transparent;
}
/* Hide non-essential chrome in fullscreen — LIVE indicator, fallback note, PTZ presets +
   Save/Manage rows. The camera image + (if PTZ) the d-pad are all that's needed. */
.cam-fs .cam-live-ind,
.cam-fs .cam-live-note,
.cam-fs .cam-ptz-presets,
.cam-fs .cam-ptz-actions,
.cam-fs .cam-ptz-manage { display: none !important; }
/* PTZ d-pad → a compact NEAR-TRANSLUCENT overlay pinned bottom-left, ABOVE the image so it
   stays tappable. */
.cam-fs .cam-ptz {
  position: absolute; left: calc(max(env(safe-area-inset-left, 0px), 8px) + 10px);
  bottom: calc(max(env(safe-area-inset-bottom, 0px), 8px) + 10px);
  width: auto; z-index: 3010; padding: 6px; margin: 0;
  background: rgba(0,0,0,.28); border-radius: 16px; backdrop-filter: blur(2px);
}
.cam-fs .cam-ptz-pad {
  grid-template-columns: repeat(3, 46px); grid-template-rows: repeat(3, 46px); gap: 5px;
}
.cam-fs .ptz-btn {
  font-size: 18px; border-radius: 12px;
  background: rgba(30,34,44,.42);        /* near-translucent so the feed shows through */
  border-color: rgba(255,255,255,.18); color: #fff;
}
.cam-fs .ptz-btn:active { background: rgba(79,195,232,.75); }

/* Refresh button: spin while a refresh pass is in flight, and give a tap a
   visible press, so it's obvious the button registered. */
/* Press feedback on the button; the SPIN is on the inner glyph only — so the
   button frame/background stays put and only the ⟳ rotates (smoothly, one way). */
#cam-refresh { transition: background .12s ease; }
#cam-refresh:active { background: var(--line); }
/* SVG icon spins around its own centre (the viewBox is centred), so there's no
   wobble like the text ⟳ glyph had. */
.cam-refresh-glyph { display: block; transform-origin: 50% 50%; }
#cam-refresh.spinning .cam-refresh-glyph { animation: camspin .8s linear infinite; }
@keyframes camspin { from { transform: rotate(0); } to { transform: rotate(360deg); } }

/* Notifications drawer (Web Push, Sprint 26). */
.notify-hint { font-size: 13px; color: var(--muted, #888); margin-top: 10px;
  padding: 10px; border-radius: 10px; background: var(--panel-2); border: 1px solid var(--line); }
.notify-quiet-row { display: flex; align-items: center; gap: 8px; }
#notify-cats { display: flex; flex-direction: column; gap: 4px; }

/* "Share app link" button in a Manage-Access user card (Sprint 18 follow-up). */
.access-applink-btn {
  margin: 10px 12px 4px; padding: 9px 12px; border-radius: 10px; font-size: 14px;
  background: var(--panel-2); border: 1px solid var(--line); color: var(--text);
  cursor: pointer; align-self: flex-start;
}
.access-applink-btn:active { background: var(--line); }

/* Status connection-problem banner (Homebridge down/empty) — honest, visible
   failure instead of chips silently vanishing (Pete, 2026-07-07). */
.status-problem-banner {
  /* ⚠️ SITS ABOVE THE COMPOSER, NOT BETWEEN THE CHIP ROWS. Moved 2026-09-01 —
     in the topbar it split the chip panel in half whenever anything degraded.
     The margin is symmetric here because it is no longer wedged between two
     rows of icons. */
  display: flex; align-items: center; gap: 8px;
  margin: 0 10px 8px; padding: 8px 12px; border-radius: 10px;
  font-size: 13px; line-height: 1.3;
  background: color-mix(in srgb, var(--danger, #ef4444) 12%, var(--panel-2));
  border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--line));
  color: var(--text);
}
.status-problem-banner.hidden { display: none; }
.status-problem-banner .sp-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  background: var(--danger, #ef4444);
  animation: sp-pulse 1.4s ease-in-out infinite;
}
@keyframes sp-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
/* A COLD START, NOT A FAULT. Same banner, neutral tint: the plane is coming up
   and said so (/api/status `starting`). Alarm red on every restart is what
   teaches a reader to ignore the banner that matters. */
.status-problem-banner.sp-starting {
  background: color-mix(in srgb, var(--muted, #94a3b8) 12%, var(--panel-2));
  border-color: color-mix(in srgb, var(--muted, #94a3b8) 40%, var(--line));
}
.status-problem-banner.sp-starting .sp-dot { background: var(--muted, #94a3b8); }
/* Chips in the unknown/unreachable state read as dimmed, not falsely "normal". */
.chip.chip-unknown { opacity: .5; }
.status-strip.status-problem .tstat-chip { opacity: .5; }

/* --- Artifacts: documents Howie builds ------------------------------------
   The viewer reuses .cam-live's full-screen shell; this is just the frame and
   the tappable card that appears in the chat. */
/* Markdown pane: the in-app render target (see index.html for why markdown
   renders here rather than in the sandboxed frame). Scrolls like the frame. */
.art-md { flex: 1; overflow: auto; background: var(--panel, transparent);
          border-radius: 12px; }
.art-md[hidden] { display: none; }
.art-frame[hidden] { display: none; }
.art-frame {
  flex: 1; width: 100%; border: 0; border-radius: 12px;
  background: var(--panel, #fff);
  /* The document is a page in its own right — let it scroll itself. */
  overflow: auto;
}
/* The chat card: how a built document is offered. Tapping opens the viewer;
   nothing opens on its own. */
.art-card {
  display: flex; align-items: center; gap: 10px;
  margin: 6px 0 2px; padding: 11px 13px;
  border-radius: 12px; cursor: pointer; width: 100%;
  text-align: left; font: inherit; color: var(--text);
  background: var(--panel-2); border: 1px solid var(--line);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.art-card:hover, .art-card:focus-visible {
  border-color: color-mix(in srgb, var(--accent, #3b82f6) 55%, var(--line));
}
.art-card .art-ico { font-size: 20px; flex: none; line-height: 1; }
.art-card .art-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.art-card .art-title { font-weight: 600; font-size: 14px; }
.art-card .art-sub {
  font-size: 12px; opacity: .7;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.art-card .art-go { margin-left: auto; opacity: .5; flex: none; font-size: 13px; }

/* Markdown rendered by the viewer's own strict renderer (renderMarkdown in
   app.js — no innerHTML of untrusted text, every node is created explicitly). */
.md-body { padding: 18px 16px 40px; max-width: 780px; margin: 0 auto;
           color: var(--text); line-height: 1.55; font-size: 15px; }
.md-body h1, .md-body h2, .md-body h3 { line-height: 1.25; margin: 1.2em 0 .5em; }
.md-body h1 { font-size: 1.6em; } .md-body h2 { font-size: 1.3em; }
.md-body h3 { font-size: 1.1em; }
.md-body p, .md-body ul, .md-body ol { margin: .6em 0; }
.md-body ul, .md-body ol { padding-left: 1.4em; }
.md-body li { margin: .25em 0; }
.md-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em;
  background: var(--panel-2); padding: .12em .35em; border-radius: 5px;
}
.md-body pre {
  background: var(--panel-2); padding: 12px 14px; border-radius: 10px;
  overflow-x: auto; border: 1px solid var(--line);
}
.md-body pre code { background: none; padding: 0; }
.md-body blockquote {
  margin: .7em 0; padding: .1em 0 .1em 1em; opacity: .85;
  border-left: 3px solid var(--line);
}
/* Tables scroll inside their own container so the page never scrolls sideways. */
.md-table-wrap { overflow-x: auto; margin: .8em 0; }
.md-body table { border-collapse: collapse; width: 100%; font-size: 14px; }
.md-body th, .md-body td {
  border: 1px solid var(--line); padding: 7px 10px; text-align: left;
}
.md-body th { background: var(--panel-2); font-weight: 600; }
.md-body img { max-width: 100%; height: auto; border-radius: 8px; }
.md-body hr { border: 0; border-top: 1px solid var(--line); margin: 1.2em 0; }
.md-body a { color: var(--accent, #3b82f6); }

/* --- Sysops confirmation bubble (the yes/no behind the push) --------------- */
.sysops-cmd {
  margin: 8px 0; padding: 9px 11px; border-radius: 8px;
  background: var(--panel-2); border: 1px solid var(--line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
  white-space: pre-wrap; word-break: break-word; overflow-x: auto;
}
.sysops-expiry { font-size: 12px; opacity: .65; margin-top: 2px; }
.sysops-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
/* Run is the consequential one — it must not look like the default choice. */
.sysops-run {
  background: color-mix(in srgb, var(--danger, #ef4444) 16%, var(--panel-2));
  border-color: color-mix(in srgb, var(--danger, #ef4444) 55%, var(--line));
}
.sysops-decline { opacity: .9; }
