/* FusionCore design tokens - per FDB section 9.12 (Tesla-app-inspired).
   Dark is the primary theme, not a variant. One accent colour. */

/* The FUSION CORE wordmark font, self-hosted so it renders on every
   device (phone, other computers) offline. */
@font-face {
  font-family: "Eurostile MN Extended Bold";
  src: url("fonts/eurostile-mn-extended-bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/* The sensor readout font (GK, 21 Jul 2026, second cut - B612 Mono read as
   "nasty" at display size): Inter, light weight, TABULAR figures. The clean
   geometric face of the app's Tesla-inspired look - airy rather than chunky
   at 130px, and tabular-nums keeps the number from shifting as digits change,
   which is what the monospace was really for. Variable font, self-hosted
   (OFL licence beside the file) so it renders offline on every device. */
@font-face {
  font-family: "Inter";
  src: url("fonts/InterVariable.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

/* Roboto for the sensor readout (GK, 22 Jul 2026 - third cut, Bold this
   time). Variable font, self-hosted like Inter (OFL licence beside the
   file) so the readout renders offline on every device. Roboto's digits
   are tabular by default, so the number never shifts as it changes. */
@font-face {
  font-family: "Roboto";
  src: url("fonts/RobotoVariable.ttf") format("truetype");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --bg: #0b0c0e;             /* near-black app background */
  --surface: #16181c;        /* card surface */
  --surface-raised: #1c1f24;
  --border: #26292f;
  --text: #f2f3f5;
  --text-secondary: #9aa0a8;
  --text-dim: #5c626b;
  --accent: #2e6bff;         /* single electric blue */
  --accent-glow: rgba(46, 107, 255, 0.35);
  --radius-card: 20px;
  --radius-tile: 14px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The [hidden] attribute must always win, even over elements that set their
   own display (e.g. .btn uses display:inline-flex, which would otherwise
   override the UA hidden rule). */
[hidden] { display: none !important; }

/* Reserve the scrollbar gutter on every page so a page that scrolls and
   one that doesn't render at the same width - no shift between pages.
   Chrome/Firefox honour this; Safari uses overlay scrollbars (already no
   shift), which is why it looked consistent there. */
html { scrollbar-gutter: stable; }

/* Scrollbars dressed for the app (GK, 21 Jul 2026 - the OS default's square
   white track and grey arrows fought the dark theme). A slim rounded thumb
   on the app's own background, no stepper buttons at all, and the accent
   blue only under the hand - so the bar whispers until it is being used.
   The ::-webkit-* rules cover Chrome/Edge (the bench + phone); the
   scrollbar-* properties are the Firefox fallback - and they MUST stay
   Firefox-scoped: a modern Chrome that sees standard scrollbar-color/width
   switches its ::-webkit-scrollbar styling OFF entirely (measured: the bar
   snapped back to the 15px OS default). The @supports matches Firefox alone. */
@supports (-moz-appearance: none) {
  html {
    scrollbar-color: #333a45 var(--bg, #0b0c0e);
    scrollbar-width: thin;
  }
}
::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}
/* The track is INVISIBLE and inset from its ends (GK, 21 Jul 2026: a solid
   track column ran the full height and cut across the header's border and
   every card corner it met). Transparent, the gutter simply shows whatever
   surface it sits on; the margins stop the pill short of the ends so it
   never touches a border. */
::-webkit-scrollbar-track {
  background: transparent;
  margin: 4px;
}
::-webkit-scrollbar-thumb {
  background: #333a45;
  border-radius: 999px;
  /* A transparent border over background-clip floats the pill off the
     track edges, so it reads as a rounded handle rather than a column. */
  border: 3px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: #4a5260;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:active {
  background: var(--accent, #3f86ff);
  background-clip: padding-box;
}
::-webkit-scrollbar-button { display: none; height: 0; width: 0; }
::-webkit-scrollbar-corner { background: transparent; }
/* The textarea resize grip (GK, 21 Jul 2026): no box, two diagonal grip
   lines in the scrollbar's own grey. The grip is painted at a FIXED
   10x10px in the corner - the resizer's BOX tracks the scrollbar (11px
   with one, ~16px without), and percentage stops made the grip grow and
   shrink with it (GK: one size, always). */
::-webkit-resizer {
  background:
    linear-gradient(135deg,
      transparent 0 42%,
      #333a45 42% 54%,
      transparent 54% 66%,
      #333a45 66% 78%,
      transparent 78% 100%)
    bottom right / 10px 10px no-repeat;
  border-radius: 0 0 6px 0;
}

body {
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", sans-serif;
  min-height: 100vh;
  display: grid;
  place-items: center;
}

.hero { text-align: center; padding: 24px; max-width: 560px; width: 100%; }

.brand {
  font-size: 13px;
  letter-spacing: 0.35em;
  color: var(--text-secondary);
  margin-bottom: 16px;
}

.brand-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent-glow);
  margin-right: 10px;
  vertical-align: 1px;
}

h1 { font-size: 34px; font-weight: 700; margin-bottom: 28px; }

.status-card {
  background: linear-gradient(135deg, var(--surface) 0%, #131b2e 100%);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 28px;
  text-align: left;
  box-shadow: 0 0 40px rgba(46, 107, 255, 0.08);
}

.micro-label {
  font-size: 11px;
  letter-spacing: 0.25em;
  color: var(--text-dim);
  margin-bottom: 6px;
}

.status-headline { font-size: 26px; font-weight: 700; margin-bottom: 22px; }

.status-row { display: flex; gap: 40px; }

.status-value { font-size: 17px; font-weight: 600; }

.status-value.dim { color: var(--text-dim); font-weight: 400; }

.footnote { margin-top: 24px; font-size: 13px; color: var(--text-dim); }

/* --- App chrome (Phase 2) --------------------------------------------- */

body.app { display: block; }

.topbar {
  display: flex;
  flex-direction: column;
  width: 100%;   /* span the window on every page (sticky can shrink-wrap) */
  gap: 14px;
  padding: 16px 24px;
  background: rgba(11, 12, 14, 0.9);
  position: sticky;
  top: 0;
  z-index: 10;
}
.topbar-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
}
/* The app clock (GK, 21 Jul 2026): rides the SERVER stage row, left of
   the name and dot, in the stage's own font and size - it inherits
   everything from .stage so the row reads as one line. Boxed (GK):
   4px padding in a 1px border, nudged 5px further left of SERVER. */
.header-clock {
  cursor: default;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: 4px;
  /* The box must not inflate its row: negative vertical margins equal to
     the padding + border (5px) keep the row's height set by the TEXT, so
     every stage gap stays the LINK-CONTROLLER distance (GK, 20 Jul 2026). */
  margin: -5px 5px -5px 0;
}
/* Collapsible Setup sections (GK, 20 Jul 2026): click the heading to
   fold a card to just its title; a CSS chevron shows which way it is. */
/* ONE zipping section for the whole app (GK, 21 Jul 2026): a container marked
   .collapsible with its heading marked .fold-head. The head is a flex row so
   the chevron is a flex item at its end - which lets a badge or a Customised
   flag sit beside it, and works whether the head is an h2, a div or a label
   (the device page styles labels as flex columns, where a floated chevron
   would drop under the text). Everything but the head hides when folded.
   Behaviour lives in static/fold.js. */
/* The box comes from .form-card - every zipping section carries that class,
   so there is one definition of a section, not copies of its values. This
   rule only handles the HEAD, and it has to out-specify page-level heading
   rules (the device page styles every label as a 13px flex column, which
   would otherwise give Device Details a different head from the rest) - hence
   the doubled class. */
.collapsible > .fold-head.fold-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* One head height whether or not a ? button or badge rides in it - the
     controls are the tallest thing (26px), so a bare-text head (About
     FusionCore AI, Zones and Actions) must not sit 5px shorter zipped. */
  min-height: 26px;
  /* The air under every head, set once here (GK, 21 Jul 2026) - the AI prose
     rows used to carry their own 21px, which is what made those sections sit
     differently from AI Permissions and the rest. */
  margin: 0 0 12px;
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
}
.collapsible > .fold-head::after {
  content: "";
  flex: none;
  align-self: center;
  width: 8px;
  height: 8px;
  margin: 0 4px 0 10px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(45deg);
  transition: transform .15s;
}
.collapsed > .fold-head::after { transform: rotate(-45deg); }
.collapsed > .fold-head.fold-head { margin-bottom: 0; }
/* The gap under an OPEN section's head is the h2's 12px and nothing else -
   whatever content comes first (a footnote, a settings row, a textarea) must
   not add its own top margin, or every section opens differently. */
.collapsible > .fold-head + * { margin-top: 0; }
/* Several cards keep a HIDDEN help paragraph right after the head - the
   first VISIBLE content sits one sibling later and must lose its top margin
   the same way, or those sections open with a double gap. */
.collapsible > .fold-head + [hidden] + * { margin-top: 0; }
/* The title text stretches, so anything else in the head (a Customised flag)
   and the chevron sit at the right edge. */
.fold-head > .fold-title { flex: 1; }
/* The collapse is a STATE and must always win, like the global [hidden]
   rule: a strongly-selected child (the device page's label rules
   out-specify a plain class selector) must not stay painted on a folded
   card - the leak GK caught on the Options checkbox (22 Jul 2026). */
.collapsed > *:not(.fold-head) { display: none !important; }
/* Sub-headings inside a card (GK, 22 Jul 2026): ONE rule for every card's
   h3, so any section that groups its contents does it identically. Sized
   into the card's own scale - h2 16, h3 15 semibold bright, captions 15
   regular secondary (the first small-caps cut read ridiculous under the
   bigger captions - GK). The generous top margin is what separates the
   groups; the fold rules strip it from the first heading so a section
   still opens flush. */
.form-card h3 {
  font-size: 15px; font-weight: 600; color: var(--text);
  margin: 28px 0 20px;
}
/* The ? answers even on a FOLDED section (GK, 21 Jul 2026): a revealed
   comment box escapes the collapse - the title line plus its aside, nothing
   else - and folds away again with a second press as usual. */
.collapsed > .help-info:not([hidden]) { display: block !important; }
/* An h2 head keeps its own bottom margin when open - the shared rule only
   removes it once folded. */
/* The heading is capped to the content area's width (GK item 6, 20 Jul
   2026): the bar's background still spans the window, but its brand, stages
   and nav align with the page content (960px - 2*24px padding = 912px). */
.topbar-row,
.topbar .nav {
  width: 100%;
  max-width: 912px;
  margin-left: auto;
  margin-right: auto;
}

/* Logo (swirl) + two-line FUSION CORE wordmark, nudged down 10px so it
   sits a touch below the status stages. */
.brand {
  display: flex;
  align-items: center;
  gap: 16px;
  text-decoration: none;
  margin-top: 10px;
}
.brand-logo { height: 64px; width: auto; display: block; }
.brand-word {
  position: relative;
  top: 2px; /* text sits 2px lower than the logo centre */
  /* Eurostile MN Bold where installed (self-host the file to guarantee it
     on every device - see INSTALL/notes); clean sans fallback otherwise. */
  font-family: "Eurostile MN Extended Bold", "Eurostile MN",
    "Eurostile LT Std", "Eurostile", "Helvetica Neue", Arial, sans-serif;
  font-size: 25px;
  font-weight: 700;
  line-height: 0.94;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
}

.nav { display: flex; gap: 22px; flex-wrap: wrap; margin-top: 0; }
.nav-link {
  color: var(--text-dim);
  text-decoration: none;
  font-size: 15px;
  font-weight: 600;
  padding: 4px 0;
}
.nav-link:hover { color: var(--text-secondary); }
.nav-link.active { color: var(--text); }

/* Three status stages, stacked and right-aligned: LABEL then dot. */
.stages { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
.stage {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-secondary);
}
.stage-name { text-transform: uppercase; }
.stage-dot {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--text-dim);
}
/* Status LEDs: pure green = OK, pure red = a proven problem - read at a glance
   like real indicator lamps, so both glow the same way (GK, 17 Jul 2026). Not
   --accent: the electric blue is the UI's action colour, not a health signal. */
.stage-dot.dot-good { background: #00ff00; box-shadow: 0 0 6px 1px #00ff00, 0 0 16px 4px rgba(0, 255, 0, 0.9); }
.stage-dot.dot-bad { background: #ff0000; box-shadow: 0 0 6px 1px #ff0000, 0 0 16px 4px rgba(255, 0, 0, 0.9); }

/* The single page container: fills to a fixed max-width and centres, so
   every page is exactly the same width (was shrink-wrapping to content). */
/* Top padding matches the Raw page (16px) so every page starts the same
   distance below the header. */
.content { width: 100%; max-width: 960px; margin: 0 auto; padding: 16px 24px; }
.hero { margin-top: 10vh; }

/* The nav names the page, so page titles are gone (Decision 33); the
   page-head now holds just its controls, kept to the right. */
.page-head {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 18px;
}

/* Actions page: the New Action / Import / Export controls sit below the
   tiles, so give them clear separation from the grid above. */
body.actions-page .page-head { margin-top: 36px; justify-content: flex-start; }
/* A page led by its Back button (GK, 21 Jul 2026 - AI Setup): the blue box
   sits top-LEFT, the title follows it - on ONE line: the global h1 keeps a
   28px bottom margin that shoved the title off the button's centreline, and
   34px read too big beside a small button (GK: drop 5). */
.page-head-back { justify-content: flex-start; }
/* +25px on top of the head's 12px gap between Back and the title (GK). */
.page-head-back h1 { font-size: 29px; margin-bottom: 0; margin-left: 25px; }
/* Add Action: a lighter grey than the other buttons so it subtly stands
   out as the primary create control, without the full accent blue. */
body.actions-page #new-btn { background: #2b2f37; border-color: #3a404b; }
body.actions-page #new-btn:hover { background: #333843; }

.connect-controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
/* Uniform control height across the row - the port selector matches the
   buttons beside it. */
.connect-controls .select,
.connect-controls .btn { height: 44px; }

/* Connection: the port selector sits full-width on its own row, with the
   Scan/Name/Connect/Disconnect buttons on the row beneath it. */
.connect-selector { display: block; width: 100%; margin-bottom: 8px; }

/* Setup page: one uniform height for every button and box (Graham). */
body.setup-page .btn,
body.setup-page .select,
body.setup-page .input-basic { height: 41px; }

.select, .input-basic {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 12px;
  font-size: 14px;
}
/* Themed focus: replace Chrome's doubled UA ring (blue inner + white outer)
   with a single on-theme accent border + soft glow. Kept visible (not
   outline:none alone) so keyboard focus stays obvious - FDB accessibility. */
.select:focus, .input-basic:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

.btn {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 9px 16px;
  font-size: 14px;
  cursor: pointer;
  text-decoration: none;  /* anchors styled as buttons (e.g. Download log) */
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.btn:hover { background: var(--surface-raised); }
.btn:disabled { opacity: 0.4; cursor: default; }
.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.btn-primary:hover { background: #3d78ff; }
/* Disconnect: a muted red - clearly a "stop" action, not alarming, but a
   proper red rather than brown. */
.btn-danger {
  background: #b23636;
  border-color: #c85252;
  color: #fff;
}
.btn-danger:hover { background: #c24040; }

/* A small "connected" dot beside the Connection card title - green when
   the Link is live, off otherwise. Says "connected" without repeating the
   target (which is already in the selector). */
.conn-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--text-dim);
  margin-left: 10px;
  vertical-align: middle;
}
.conn-dot.connected {
  background: #3ecf6b;
  box-shadow: 0 0 9px rgba(62, 207, 107, 0.6);
}
/* A connected link's Connect button: darker blue, clearly "already done". */
.btn-primary:disabled {
  background: #1e3a80;
  border-color: #1e3a80;
  color: rgba(255, 255, 255, 0.55);
  opacity: 1;
}
.btn-small { padding: 5px 10px; font-size: 12px; }
/* Mic button while recording: a clear red pulse so the owner knows it is
   live and listening (and how to stop). */
.btn.recording { background: var(--danger, #b84a4a); color: #fff; animation: mic-pulse 1.2s ease-in-out infinite; }
@keyframes mic-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

.console-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.console-toolbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
}
.live-badge {
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 2px 8px;
}
.live-badge.paused { color: #e8a13c; border-color: #e8a13c; }
.toggle { font-size: 13px; color: var(--text-secondary); }
/* Checkboxes follow the dark theme: dark fill, lighter border, accent tick. */
.toggle input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  width: 15px;
  height: 15px;
  border: 1px solid #3d434c;
  border-radius: 4px;
  background: var(--surface-raised);
  cursor: pointer;
  position: relative;
  vertical-align: -3px;
  margin-right: 4px;
}
.toggle input[type="checkbox"]:hover { border-color: #545b66; }
.toggle input[type="checkbox"]:checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 4px;
  height: 8px;
  border: solid var(--accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.console-out {
  margin: 0;
  padding: 16px;
  height: 420px;
  overflow-y: auto;
  font-family: "SF Mono", ui-monospace, Menlo, monospace;
  font-size: 13px;
  line-height: 1.6;
  background: var(--bg);
}
.line-tx { color: var(--accent); }
.line-rx { color: var(--text); }
.line-err { color: #e05252; }
.line-info { color: var(--text-dim); }

.console-input-row {
  display: flex;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}
.console-input-row .input-basic { flex: 1; }

.text-link { color: var(--accent); text-decoration: none; }

/* --- Raw Console: full-window shell at every size (Decision 27) -------- */
/* The Raw Console fills the window at any width - the log is the only
   scrollable region, the command box is pinned to the bottom - so it
   never collapses into a short, narrow strip on a wider browser. Height
   binds to the visual viewport in JS so the box rides above the on-screen
   keyboard. */
html:has(body.raw-page) { overflow: hidden; }
body.raw-page {
  height: 100vh;    /* fallback before JS / without dvh */
  height: 100dvh;
  min-height: 0;    /* clear the app-wide min-height:100vh */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
body.raw-page .content {
  flex: 1;
  min-height: 0;
  /* width:100% is required: the base .content has margin:0 auto, and auto
     side-margins on a flex item suppress stretch - without an explicit
     width it collapses to its intrinsic width and centres, leaving the
     console narrow. */
  width: 100%;
  max-width: 960px;    /* same width as every other page (consistency) */
  padding: 16px 24px 20px;
  display: flex;
}
body.raw-page .console-page {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
body.raw-page .console-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
body.raw-page .console-out { flex: 1; min-height: 0; height: auto; }

/* --- Actions page (Phase 3) ------------------------------------------- */

.actions-grid {
  display: grid;
  /* Cards are wide enough that the button row never wraps; min(...) keeps
     the grid from overflowing on screens narrower than a full card. */
  grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  gap: 14px;
}
.action-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-tile);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.action-card.disabled { opacity: 0.55; }
.action-head { display: flex; align-items: center; gap: 10px; }
.action-head h3 { font-size: 16px; margin: 0; flex: 1; }
.slot-badge {
  font-family: "SF Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  color: var(--accent);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 2px 7px;
}
.action-desc { font-size: 13px; color: var(--text-secondary); margin: 0; }
.action-data {
  font-family: "SF Mono", ui-monospace, Menlo, monospace;
  font-size: 12px; color: var(--text-dim); margin: 0;
}
.action-notes { font-size: 12px; color: var(--text-dim); margin: 0; font-style: italic; }
.action-buttons { display: flex; gap: 8px; flex-wrap: nowrap; margin-top: 4px; }
/* Outcome LED per action card (Decision 25): lights on the result,
   holds a moment, then fades. The reason stays on hover (title). */
.action-led {
  display: inline-block;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: transparent;
}
.action-led.led-ok {
  background: #3ecf6b;
  box-shadow: 0 0 10px rgba(62, 207, 107, 0.6);
  animation: led-fade 1.8s ease-out forwards;
}
.action-led.led-fail {
  background: #e05252;
  box-shadow: 0 0 10px rgba(224, 82, 82, 0.6);
  animation: led-fade 1.8s ease-out forwards;
}
.action-led.led-warn {
  background: #e8a13c;
  box-shadow: 0 0 10px rgba(232, 161, 60, 0.6);
  animation: led-fade 1.8s ease-out forwards;
}
@keyframes led-fade {
  0%, 56% { opacity: 1; } /* solid ~1 s of the 1.8 s run, then fade */
  100% { opacity: 0; }
}
.btn-pressed { animation: btn-press 0.25s ease-out; }
@keyframes btn-press {
  0% { transform: scale(1); }
  40% { transform: scale(0.93); filter: brightness(1.35); }
  100% { transform: scale(1); }
}

.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: 18px 20px;
  /* ONE section rhythm app-wide, tuned by GK's eye (26 -> 16, 21 Jul 2026):
     no page-local overrides. */
  margin-bottom: 16px;
}
.form-card h2 { font-size: 16px; margin: 0 0 12px; }
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px 16px;
}
.form-grid label { font-size: 12px; color: var(--text-secondary);
  display: flex; flex-direction: column; gap: 5px; }
.form-grid .form-wide { grid-column: 1 / -1; }
.form-grid .form-check { flex-direction: row; align-items: center; }
.form-actions { display: flex; gap: 10px; align-items: center; margin-top: 14px; }
/* The Security card's Sign out sits apart from Set/Change password - it is
   a session action, not part of the password form. */
.form-actions .btn-signout { margin-left: auto; }
.form-error { color: #e05252; font-size: 13px; }
.form-ok { color: var(--accent); font-size: 13px; }

/* Systems list: one selectable row per You.InControl system, the active
   one marked with the accent left-border (matches the Devices directory). */
.system-list { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; }
.system-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-dim);
  border-radius: var(--radius-tile);
  padding: 10px 14px;
}
.system-row.active { border-left-color: var(--accent); }
.system-row-main {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
  cursor: pointer;
}
.system-row-main input[type="radio"]:disabled { cursor: default; }
.system-row-name {
  font-size: 15px;
  font-weight: 500;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.system-row.active .system-row-name { color: var(--text); }

/* --- Setup diagnostics (Phase 4) --------------------------------------- */

/* Ping result is plain information below the buttons - not a bordered box
   that reads like a text input. */
.ping-result { margin-top: 14px; }
.ping-result h3 { font-size: 14px; color: var(--text); margin: 0 0 8px; }
.ping-result pre {
  font-family: "SF Mono", ui-monospace, Menlo, monospace;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0;
  overflow-x: auto;
}

/* --- Phone layout (V1 feedback, 11 Jul 2026) --------------------------- */
/* Sound-mobile pass (Decision 27): >=44px touch targets (Apple HIG / WCAG
   2.5.5), 16px form fonts so iOS Safari never auto-zooms on focus,
   safe-area insets for the home indicator, and a fixed-height Raw
   Console whose only scrollable region is the log, command box pinned
   to the bottom. The keyboard resizes the layout, not the page, via the
   viewport meta (interactive-widget=resizes-content). */

/* --- Devices (Phase 6 - directory) ------------------------------------- */

/* Search + filter controls share a taller, prominent height. */
.search-box {
  display: block;
  width: 100%;
  height: 52px;
  font-size: 16px;
  margin-bottom: 12px;
}
/* Search box shares its row with the Show hidden toggle, pinned to the
   right edge. margin-left:auto keeps it right even when the search box is
   hidden (a dimension is active). */
.search-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.search-row .search-box { flex: 1; min-width: 0; margin-bottom: 0; }
.search-row .toggle { margin-left: auto; white-space: nowrap; }
.search-row #refresh-devices { height: 52px; }
.filter-bar {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 12px;
}
.filter-bar .select {
  height: 52px;
  font-size: 15px;
  flex: 1 1 240px;
  /* Custom chevron so its distance from the right edge is controllable
     (the native select arrow sits hard against the edge and can't be
     moved). Inline SVG keeps it offline/self-contained. */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%239aa0a8' stroke-width='2'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
  padding-right: 40px;
}
/* When a dimension is active the chips are the search mechanism and stand
   on their own - no enclosing box. */
.filter-values { margin-bottom: 18px; }
.value-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.value-chip {
  background: var(--surface);
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 7px 14px;
  font-size: 13px;
  cursor: pointer;
}
.value-chip:hover { background: var(--surface-raised); }
.value-chip.selected {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.group-heading {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
  margin: 20px 0 8px;
}
.device-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-dim);
  border-radius: var(--radius-tile);
  padding: 12px 14px;
  margin-bottom: 8px;
}
.device-row:hover { background: var(--surface-raised); }
.device-row.hidden-device { opacity: 0.5; }
.device-row-main {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
  min-width: 0;
  text-decoration: none;
  color: var(--text);
}
.device-row-name {
  font-size: 15px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.device-row-main .type-badge { margin-left: auto; }
.hide-btn { flex: none; }
.type-badge {
  font-size: 11px;
  color: var(--text-secondary);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 2px 8px;
  white-space: nowrap;
}
/* In the listing, the type label matches the Hide button's height. */
.device-row .type-badge { display: inline-flex; align-items: center; height: 28px; }
.device-row .hide-btn { height: 28px; }
/* A disabled device is clearly marked (and its name muted). */
.disabled-badge {
  flex: none;
  font-size: 11px;
  color: #e8a13c;
  background: rgba(232, 161, 60, 0.12);
  border: 1px solid rgba(232, 161, 60, 0.35);
  border-radius: 8px;
  padding: 2px 8px;
  margin-left: 10px;
  white-space: nowrap;
}
/* In the listing, the Disabled badge sits just left of the type badge:
   it takes the auto margin (pushing itself + the type badge to the right)
   and the type badge only gaps after it. */
.device-row-main .disabled-badge { margin-left: auto; }
.disabled-device .device-row-main .type-badge { margin-left: 0; }
/* GK (21 Jul 2026): a disabled device is greyed out properly in the
   listing - muted name and type on a darkened face, no colour accent
   (devices.js drops the edge colour too) - with the amber Disabled tag
   left the one bright thing saying why. */
.disabled-device { background: #101215; }
.disabled-device:hover { background: var(--surface); }
.disabled-device .device-row-name,
.disabled-device .device-row-main .type-badge { color: var(--text-dim); }

/* AI Context: the example is a faint hint (much dimmer than a real value);
   typed text reads clearly. */
#ai-context { color: var(--text); }
#ai-context::placeholder { color: var(--text-dim); opacity: 0.55; }

/* Name: a normal edit box (inherits .input-basic) so it clearly reads as
   editable; value shown bold white, placeholder is the type when unnamed. */
.name-title { font-size: 20px; font-weight: 600; color: #fff; }
.name-title::placeholder { color: var(--text-secondary); font-weight: 400; }

/* Read-only identity row: type badge + "Device N", with a Disabled badge
   pushed to the top-right when the device is disabled. */
.device-meta { display: flex; align-items: center; gap: 10px; }
.device-meta .type-badge { font-size: 13px; padding: 4px 12px; }
/* Match the type badge's height and text size; keep the amber colours. */
.device-meta .disabled-badge { margin-left: auto; font-size: 13px; padding: 4px 12px; }
.device-ident { font-size: 13px; color: var(--text-dim); }
/* Device enable/disable switch, top-right of the card (GK): the state is spelled
   out beside it - amber when disabled, carrying over the old badge's meaning. */
.dev-enable { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.dev-enable-text { font-size: 13px; color: var(--text-secondary); white-space: nowrap; }
.dev-enable-text.is-disabled { color: #e8a13c; }
/* A little extra breathing room below the name field. */
.device-edit .form-card .name-field { margin-bottom: 8px; }

/* The two device toggles share one row, each sized to its own text so the
   click target does not span the card width (a Save tap can no longer land
   on Enabled). Extra 15px below separates them from Save/Cancel. */
.toggle-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 24px;
  margin-bottom: 15px;
}

/* Interaction devices (e.g. a Remote): a digital-twin press per channel,
   stacked vertically with clear separation from the identity row above. */
.interaction-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  margin-top: 14px;
}
/* On a mixed device (outputs + inputs, e.g. Relay-1) tighten the gap between
   the output list and the inputs by 25px. Scoped via the adjacent out-msg so
   pure-interaction devices (Remote/Switch/UniSwitch) are unaffected. */
#out-msg + .interaction-block { margin-top: -11px; }
.interaction-buttons { display: flex; flex-direction: column; gap: 24px; }
.interaction-buttons .btn { min-width: 160px; }
/* Six gesture buttons (S/SS/SSS/L/SL/SSL) per interaction channel, on one row
   with its channel label - a mimic-press of the physical device's input. */
.interaction-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.interaction-label { min-width: 120px; font-weight: 500; }
.gesture-buttons { display: flex; gap: 6px; flex-wrap: wrap; }
.gesture-buttons .interaction-btn { min-width: 46px; padding: 6px 10px; }
/* Reserve every press gesture (single/double/triple/long) for our own
   handlers later: kill text selection, the iOS long-press callout, and
   the double-tap-to-zoom / tap delay. The context menu is cancelled in JS
   (device.js). Cross-browser: -webkit-* cover Safari/iOS, user-select is
   standard, touch-action:manipulation is honoured by all modern engines. */
.interaction-btn {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  touch-action: manipulation;
}

/* Output control (Relay-1/8, 240V Hub/Outlet): one compact row per channel -
   number, editable name, then Off / state-toggle / On. The colours cue the
   available action (Off red when on, On blue when off); the toggle mirrors
   the reflected state. Short-press switches; long-press On opens a timed
   run. State is driven by data-on on the row so JS only flips one flag. */
.output-list { display: flex; flex-direction: column; gap: 18px; }
/* The row stacks its control line and (when long-pressed) the timed-on
   popup; the line itself is the horizontal control cluster. */
.output-row { display: flex; flex-direction: column; }
.output-line { display: flex; align-items: center; gap: 8px; }
.out-num {
  font-family: "SF Mono", ui-monospace, Menlo, monospace;
  font-size: 13px; color: var(--text-dim); min-width: 16px; text-align: center;
}
.out-name { flex: 1; min-width: 0; height: 34px; font-weight: 500; }
.out-off, .out-on {
  height: 34px; border-radius: 8px; padding: 0 22px; flex: none;
  font-size: 14px; font-weight: 500; cursor: pointer;
  background: transparent; color: var(--text-secondary); border: 1px solid var(--border);
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  touch-action: manipulation;
}
.out-toggle {
  display: inline-flex; flex: none; width: 56px; height: 34px; margin: 0 5px;
  border-radius: 999px; position: relative;
  background: #20242b; border: 1px solid #3a404b;
}
.out-knob {
  position: absolute; top: 3px; left: 3px; width: 26px; height: 26px;
  border-radius: 50%; background: var(--text-dim); transition: left .15s, background .15s;
}
.output-row[data-on="true"] .out-off { background: #b23636; border-color: #c85252; color: #fff; }
.output-row[data-on="true"] .out-toggle { background: rgba(62, 207, 107, 0.16); border-color: #3ecf6b; }
.output-row[data-on="true"] .out-knob { left: 27px; background: #3ecf6b; }
.output-row[data-on="false"] .out-on { background: var(--accent); border-color: var(--accent); color: #fff; }
.output-row.busy { opacity: 0.55; }

/* Automations as rows (like Devices / Outputs): name + context cog + Trigger. */
/* Automations use the Devices-list listing style (GK): a bordered card with a
   blue accent bar, an instantly-editable name, a settings cog and a Trigger.
   The details panel tucks flush under the card when the cog is open. */
.automation-list { display: flex; flex-direction: column; gap: 10px; }
.automation-row { display: flex; flex-direction: column; }
.automation-line {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-tile); padding: 12px 14px;
}
.automation-line:hover { background: var(--surface-raised); }
.auto-badge { flex: none; font-family: "SF Mono", ui-monospace, Menlo, monospace; font-size: 13px; font-weight: 600; color: var(--accent); min-width: 18px; text-align: center; }
/* Reads like the Devices-list name, but edits instantly (GK's instant rename). */
.auto-name { flex: 1; min-width: 0; height: 34px; font-size: 15px; font-weight: 500; background: transparent; border: 1px solid transparent; border-radius: 8px; padding: 0 8px; }
.auto-name:hover { border-color: var(--border); }
.auto-name:focus { background: var(--surface-raised); border-color: var(--accent); }
.automation-line .btn { height: 34px; padding: 0 22px; }
/* Cog open: the details panel tucks flush under the card, and the blue accent
   bar runs the FULL height - card and panel as one bordered area (GK, 21 Jul
   2026: "the Blue left border should encompass the whole area"). */
.automation-row.open .automation-line { border-radius: var(--radius-tile) var(--radius-tile) 0 0; }
.automation-row.open .out-settings {
  margin-top: 0; border-top: none;
  border-radius: 0 0 var(--radius-tile) var(--radius-tile);
  border-left: 3px solid var(--accent);
}
/* A disabled automation reads muted in the list, exactly like a disabled
   device: the name loses its weight, the number its accent. The Trigger stays
   - the Controller refuses it honestly if it is truly off. */
.automation-row.is-disabled .auto-name { color: var(--text-dim); }
.automation-row.is-disabled .auto-badge { color: var(--text-dim); }

/* Input rows (Remote / Switch / UniSwitch / Relay-1's two). Deliberately the
   same row as an output - number, editable name, cog - with the Off / state /
   On cluster replaced by the six press combos, so the two read as one system.
   A combo is grey until the Controller says that gesture has an action list
   bound to it (60), then it is the same solid accent the On button uses: the
   row tells you at a glance which presses actually do something. */
.input-list { display: flex; flex-direction: column; gap: 18px; }
.input-row .output-line { flex-wrap: wrap; }
/* Without a floor the name box collapses to nothing: .out-name is flex:1, so it
   gives up all its width to the combos before they ever wrap. */
.input-row .out-name { min-width: 150px; }
.in-combos { display: flex; gap: 6px; flex: none; flex-wrap: wrap; margin-left: auto; }
/* When the combos wrap under the name (a phone, or many inputs), the wrapped
   line starts at the name box's left edge: the number's 16px + the 8px gap.
   device.js measures the wrap per row (width alone cannot say) and sets the
   class; basis + margin = 100% keeps it on its own line without overflow. */
.in-combos.wrapped { flex-basis: calc(100% - 24px); margin-left: 24px; }
/* An input the Controller has not set up (never activated - the 540 read
   skips it): no press buttons, a neutral "Not active" tag instead (GK).
   Neutral grey, not the amber of Disabled - Disabled is an owner's lock;
   this is simply "nothing here yet". */
.input-row.in-inactive .in-combos { display: none; }
.input-row.in-inactive .out-name { color: var(--text-secondary); }
.out-disabled-tag.tag-inactive {
  color: var(--text-dim);
  background: rgba(160, 160, 160, 0.10);
  border-color: rgba(160, 160, 160, 0.30);
}
.in-combo {
  height: 34px; min-width: 44px; padding: 0 10px; border-radius: 8px;
  font-size: 14px; font-weight: 500; cursor: pointer;
  background: transparent; color: var(--text-secondary); border: 1px solid var(--border);
  -webkit-user-select: none; -moz-user-select: none; user-select: none;
  -webkit-touch-callout: none; touch-action: manipulation;
}
.in-combo[data-bound="true"] {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
/* A disabled input is flagged the way a disabled output is: muted name. */
.input-row.in-disabled .out-name { color: var(--text-secondary); }

/* Per-output settings cog + its dropdown (context now, more later). */
/* No box around the cog (GK) - just the symbol; hover lifts its colour. */
.out-cog {
  flex: none; width: 34px; height: 34px; display: inline-flex;
  align-items: center; justify-content: center; border-radius: 8px;
  background: transparent; color: var(--text-dim); border: none; padding: 0;
  cursor: pointer; transition: color .15s;
}
.out-cog:hover { color: var(--text-secondary); }
.out-settings { margin-top: 10px; background: #141619; border: 1px solid var(--border); border-radius: 12px; padding: 14px; }
/* The Details field fills the box width, styled like the AI Setup boxes. */
.out-details-lab { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 6px; }
.out-settings .out-details { width: 100%; box-sizing: border-box; resize: vertical; min-height: 84px; font-family: inherit; line-height: 1.5; }
/* Per-output Enabled lock: a compact on/off switch (smaller than the main
   output toggle), press-on / press-off. State is app-side best-effort until the
   Controller reports Output_Enabled on the 41 read (firmware follow-up). */
.out-en-row { display: flex; align-items: center; gap: 10px; margin-top: 12px; }
/* Settings-panel header: title on the left, the Enabled lock top-right
   (GK, 20 Jul 2026). Inside it the row loses its below-the-box margin. */
.out-pop-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
.out-pop-head .out-pop-title { margin-bottom: 0; }
/* The panel-head note ("Set up on Controller" on a Not-active input): the
   title's size and line so the head reads as one level, muted colour. */
.out-pop-head .out-pop-note { font-size: 13px; font-weight: 500; color: var(--text-dim); }
.out-pop-head .out-en-row { margin-top: 0; }
.out-en-toggle { position: relative; flex: none; width: 38px; height: 20px; padding: 0; border-radius: 10px; border: 1px solid var(--border); background: var(--surface-raised); cursor: pointer; transition: background .15s, border-color .15s; }
.out-en-toggle .out-en-knob { position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%; background: var(--text-dim); transition: left .15s, background .15s; }
.out-en-toggle[data-on="true"] { background: var(--accent); border-color: var(--accent); }
.out-en-toggle[data-on="true"] .out-en-knob { left: 20px; background: #fff; }
/* An output whose Enabled lock is off is flagged in the list (name muted + tag). */
.output-row.out-disabled .out-name { color: var(--text-secondary); }
/* A disabled output's controls are REMOVED and the Disabled tag stands in
   their place (GK, 20 Jul 2026): the greyed controls plus the tag together
   crushed the flexed name to nothing on a phone. device.js also refuses the
   clicks (belt and braces while the row re-dresses). The cog stays - it is
   the way back to re-enable. */
.output-row.out-disabled .out-off,
.output-row.out-disabled .out-toggle,
.output-row.out-disabled .out-on { display: none; }
/* Same rule for a disabled input's six press combos. */
.input-row.in-disabled .in-combos { display: none; }
.out-disabled-tag, .share-unread { flex: none; font-size: 11px; color: #e8a13c; background: rgba(232, 161, 60, 0.12); border: 1px solid rgba(232, 161, 60, 0.35); border-radius: 8px; padding: 2px 8px; white-space: nowrap; }

/* Timed-on popup (long-press On): one input method per row. */
.out-pop { margin-top: 10px; background: #141619; border: 1px solid var(--border); border-radius: 12px; padding: 14px; }
.out-pop-title { font-size: 13px; font-weight: 500; margin-bottom: 12px; }
.out-pop-row { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
.out-pop-lab { font-size: 12px; color: var(--text-dim); width: 52px; flex: none; }
.out-chip { font-size: 13px; color: var(--text-secondary); background: var(--surface-raised); border: 1px solid var(--border); border-radius: 999px; padding: 6px 12px; cursor: pointer; transition: background-color .35s, color .35s, border-color .35s; }
/* A momentary press confirmation: goes blue, then eases back to grey (the JS
   removes .flash after ~0.9s; the transition does the fade). Not a sticky state. */
.out-chip.flash { color: #fff; background: var(--accent); border-color: var(--accent); }
.out-units { display: flex; flex-wrap: wrap; gap: 16px; }
.out-unit { display: flex; align-items: center; gap: 5px; }
.out-unit .unit-label { font-size: 13px; color: var(--text-dim); width: 12px; text-align: center; }
.out-stp { width: 30px; height: 32px; flex: none; color: var(--text-secondary); background: var(--surface-raised); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; font-size: 16px; line-height: 1; }
/* Fixed two-digit width - must out-specify .device-edit .form-card
   .input-basic { width: 100% }. */
.device-edit .form-card .out-units .out-num-in {
  width: 40px; flex: none; height: 32px; text-align: center; padding: 0 4px;
}
.out-until { width: 132px; height: 32px; }

/* The edit form stacks its fields cleanly: each label's text sits above a
   full-width control on its own row. */
/* DIRECT child only: the page's one wrapper card is a 16px-gap flex column,
   but the accordion cards nested inside it (Zones, Device Details) must NOT
   inherit that - a descendant match turned them into gap-16 columns and made
   their internal spacing unlike every other zipping section (GK). */
.device-edit > .form-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 16px;
}
/* Nested accordion cards: the wrapper's gap IS their rhythm - their own
   card margins would double it. */
.device-edit > .form-card .form-card { margin: 0; }
.device-edit .form-card label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  font-size: 13px;
  color: var(--text-secondary);
}
.device-edit .form-card .input-basic { width: 100%; }
/* GK (20 Jul 2026): ONE section gap - 36px - between every section of the
   device page, taken from the Relay-8 last-output -> Device Details gap.
   The card is a 16px-gap flex column, so each section boundary carries
   20px of margin: 16 + 20 = 36. Empty flash paragraphs must not hold
   space (they sat in the rhythm and stretched input pages to 78px). */
#out-msg:empty,
#press-msg:empty,
#sensor-msg:empty { display: none; }
/* GK (21 Jul 2026): "Device Details" reads as a section title - the h2
   treatment, like the Output Details panel's title - and its box uses the
   page font (a bare textarea falls back to the browser's monospace). */
.device-edit .form-card .device-details {
  display: flex; flex-direction: column; align-items: stretch;
}
/* Box and title come from the shared .collapsible / .fold-head rule, which
   also turns this <label> back into a row (the page styles labels as flex
   columns, which would drop the chevron under the text). */
.device-edit .form-card .device-details textarea {
  font-family: inherit; line-height: 1.5;
}
.device-edit .form-card .field-block { margin: 20px 0; }
.device-edit .form-card .toggle-row { margin-bottom: 0; }
.device-edit .form-card .form-actions { margin-top: 20px; }
/* The outputs->inputs gap on a mixed device (Relay-1) equals the 18px
   between rows (GK item 5, 20 Jul 2026). */
.output-list ~ .input-list { margin-top: 2px; }
.device-edit .form-card label.form-check {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.field-block { margin: 4px 0 14px; }
.field-label {
  display: block;
  font-size: 13px;
  color: var(--text-secondary);
  margin-bottom: 8px;
}
.swatches { display: flex; gap: 10px; flex-wrap: wrap; }
.swatch {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 2px solid var(--border);
  cursor: pointer;
  padding: 0;
}
.swatch.selected {
  border-color: var(--text);
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--text);
}
.swatch-none {
  background:
    linear-gradient(45deg, transparent 45%, var(--text-dim) 45%,
    var(--text-dim) 55%, transparent 55%),
    var(--surface);
}

@media (max-width: 640px) {
  /* Every element edge is verified to fit the viewport; clip so nothing
     (including emulated-scrollbar rounding) can pan the page sideways. */
  html, body { overflow-x: clip; }
  .filter-bar { gap: 8px; }
  .filter-bar .select, .filter-bar .input-basic { flex: 1 1 100%; }
  .topbar {
    gap: 12px;
    padding: 12px 14px;
    padding-top: calc(12px + env(safe-area-inset-top));
  }
  .brand-logo { height: 52px; }
  .brand-word { font-size: 21px; }
  .nav { gap: 18px; margin-bottom: 5px; }  /* 5px extra gap under the menu */
  .nav-link { padding: 8px 0; }  /* thumb-sized tap target */
  /* 5px less each side on a phone (GK): tightens the gap left of the row
     number and right of the On button, giving the row ~10px more width. */
  .content { padding: 18px 9px; }
  /* And 5px off each side inside the card itself (20px -> 15px), same intent:
     the gap left of the row number and right of the On button. */
  .form-card { padding-left: 15px; padding-right: 15px; }
  .page-head h1 { font-size: 22px; }
  /* One column on a phone: the desktop track min (min(360px, 100%))
     resolves circularly at widths below 360px and overshoots by ~2px,
     which lets the whole page pan sideways. */
  .actions-grid { grid-template-columns: 1fr; }
  .connect-controls { flex-wrap: wrap; }
  /* A select's minimum width follows its longest option (e.g. a long
     serial-port path) and would force the whole page wider than the
     screen - clamp it and let the option text truncate instead. */
  .connect-controls .select {
    flex-basis: 100%;
    width: 100%;
    min-width: 0;
    max-width: 100%;
  }

  /* Touch targets and zoom-proof fonts (Apple HIG 44px; iOS zooms any
     focused field under 16px). */
  .btn, .btn-small, .select, .input-basic { min-height: 44px; }
  .input-basic, .select { font-size: 16px; }
  .btn-small { font-size: 13px; padding: 8px 12px; }
  /* Keep the listing type label level with the taller mobile Hide button. */
  .device-row .type-badge, .device-row .hide-btn { height: 44px; }

  .console-toolbar { flex-wrap: wrap; gap: 10px 12px; }
  /* Long log lines wrap on a phone; side-scrolling a console is worse. */
  .console-out {
    height: 320px;
    font-size: 12px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }
  .status-row { flex-wrap: wrap; gap: 18px 28px; }

  /* Raw Console phone overrides (the full-window shell itself is defined
     once for all sizes, above). Full-bleed padding with extra bottom
     clearance for the keyboard / gesture bar - max() guarantees a solid
     gap even where the browser reports no safe-area inset (Chrome on
     Android often reports 0), which had left the box obscured on a
     Pixel 5. Live settings belong to Setup on a phone. */
  body.raw-page .content {
    max-width: none;
    padding: 12px 14px max(30px, calc(14px + env(safe-area-inset-bottom)));
  }
  body.raw-page .console-page { gap: 10px; }
  body.raw-page .page-head { margin-bottom: 0; }
  body.raw-page .page-head h1 { font-size: 20px; }

  /* Output row controls are each ~10px narrower on a phone (GK), giving the
     output name back ~30px. Off/On lose 5px of padding a side; the switch
     loses 10px of width, so the knob's "on" position moves in by the same
     10px to stay flush against its right edge. */
  .out-off, .out-on { padding: 0 17px; }
  .out-toggle { width: 46px; }
  .output-row[data-on="true"] .out-knob { left: 17px; }
}

/* --- Chat page (doc 10, Phase C1) -------------------------------------- */
/* Same full-window shell as the Raw Console: only the transcript scrolls,
   the input bar is pinned to the bottom (and rides above the on-screen
   keyboard via the JS viewport sync). */
html:has(body.chat-page) { overflow: hidden; }
body.chat-page {
  height: 100vh;    /* fallback before JS / without dvh */
  height: 100dvh;
  min-height: 0;    /* clear the app-wide min-height:100vh */
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
body.chat-page .content {
  flex: 1;
  min-height: 0;
  width: 100%;         /* required: auto side-margins suppress flex stretch */
  max-width: 960px;    /* same width as every other page */
  padding: 16px 24px 20px;
  display: flex;
}
body.chat-page .chat-shell {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.chat-card {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
}
.chat-toolbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--border);
}
.chat-toolbar .btn-small { margin-left: auto; }  /* push Setup + Clear right */

.chat-log {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  background: var(--bg);
}

/* A message row aligns its bubble left (assistant) or right (owner). */
.chat-row { display: flex; }
.chat-row.from-user { justify-content: flex-end; }
.chat-row.from-bot { justify-content: flex-start; }
.chat-bubble {
  max-width: 78%;
  padding: 10px 13px;
  border-radius: 16px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
}
.chat-row.from-user .chat-bubble {
  background: var(--accent);
  border-color: var(--accent);
}
.chat-text { font-size: 15px; line-height: 1.45; white-space: pre-wrap; }
.chat-meta {
  font-size: 11px;
  color: var(--text-dim);
  margin-top: 4px;
  text-align: right;
}
.chat-row.from-user .chat-meta { color: rgba(255, 255, 255, 0.7); }

/* An action-outcome card: a full-width strip with a live LED, so the owner
   sees what the assistant actually did and whether the Controller confirmed
   it (the same LED vocabulary as the Actions page). */
.chat-outcome {
  display: flex;
  align-items: center;
  gap: 10px;
  align-self: stretch;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  font-size: 13px;
  color: var(--text-secondary);
}
.chat-outcome-text { flex: 1; }

/* A confirm prompt for an owner-marked dangerous action. */
.chat-confirm {
  align-self: stretch;
  padding: 12px;
  border: 1px solid var(--accent);
  border-radius: 12px;
  background: var(--surface);
}
.chat-confirm-buttons { display: flex; gap: 8px; margin-top: 10px; }

.chat-input-row {
  display: flex;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}
.chat-input-row .input-basic { flex: 1; }

@media (max-width: 640px) {
  body.chat-page .content {
    max-width: none;
    padding: 12px 14px max(30px, calc(14px + env(safe-area-inset-bottom)));
  }
  body.chat-page .chat-shell { gap: 10px; }
  .chat-bubble { max-width: 88%; }
}

/* --- Chat toolbar: Setup pushed right, Clear an even gap beside it (GK,
   21 Jul 2026: "nicely spaced" - one 14px toolbar gap does the spacing, no
   extra per-button margins). ------------------------------------------- */
.chat-toolbar a.btn-small { margin-left: auto; }
.chat-toolbar button.btn-small { margin-left: 0; }

/* --- AI Setup page (doc 10) -------------------------------------------- */
/* The About card's body (GK, 21 Jul 2026 - the loose intro became a
   section like the rest). */
.ai-about-text {
  color: var(--text-secondary);
  font-size: 14px;
  line-height: 1.5;
  margin: 0;
  max-width: 640px;
}
/* .ai-field-head is retired (GK, 21 Jul 2026): the prose cards' h2 is now
   the head itself, exactly like every other zipping section, so there is no
   wrapper left to style differently. */
.ai-badge {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 6px;
  border: 1px solid var(--border);
}
.ai-default-note { font-size: 13px; color: var(--text-dim); align-self: center; }
.ai-badge.custom { color: var(--accent); border-color: var(--accent); }
.ai-help {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0 0 10px;
  line-height: 1.45;
}
.ai-textarea {
  width: 100%;
  resize: vertical;
  min-height: 110px;
  font-family: inherit;
  line-height: 1.5;
}
.ai-field-buttons {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
.ai-status { font-size: 13px; color: var(--text-dim); }

/* --- Sensors: temperature dial + water vessel -------------------------- */
/* A sensor is read, never driven, so it gets a reading surface rather than a
   control row: the number large enough to read across a shed, what it means
   (per cent full, litres held), which way it is going, and when it last came
   in. The drawing differs by sensor - a dial for a temperature, a tank or a
   dam for a water level - but the frame around it is the same, so the two
   read as one family. Colours stay in the app's vocabulary; the only new one
   is water, which is deliberately NOT --accent (blue is the action colour,
   and water is a reading, not a button). */

/* ONE 16px rhythm between every window in the stack (GK, 21 Jul 2026) - the
   readout, the History chart, Zones and Actions - matching the 16px the
   page's cards already use. The gaps live HERE; the windows themselves carry
   no vertical margins of their own. */
.sensor-list { display: flex; flex-direction: column; gap: 16px; }
.sensor-row { display: flex; flex-direction: column; gap: 16px; }

/* The drawing sits left at a fixed size so a row of sensors line up. */

/* The readout column takes the rest. */
.sensor-read { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
/* Instantly editable, exactly like an automation name (GK's "gold" rename). */

/* The number. Tabular figures so a changing reading does not jiggle. */

/* One line of derived meaning - per cent full, litres held, capacity. */

/* Freshness, the quietest line. (The trend chip that lived beside it moved
   into the Statistics section - GK, 22 Jul 2026.) */
.sensor-age { font-size: 12px; color: var(--text-dim); }

/* Thresholds read as a list of what the Controller will DO, not as numbers. */
.sensor-thresholds { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.sensor-threshold {
  display: flex; align-items: center; gap: 10px;
  font-size: 13px; color: var(--text-secondary);
  background: var(--surface-raised); border: 1px solid var(--border);
  border-radius: 8px; padding: 7px 10px;
}
.sensor-threshold .th-dir { flex: none; width: 16px; text-align: center; font-weight: 700; }
.sensor-threshold.up .th-dir { color: #3ecf6b; }
.sensor-threshold.down .th-dir { color: #e8a13c; }
.sensor-threshold .th-at { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.sensor-threshold .th-note { margin-left: auto; color: var(--text-dim); font-size: 12px; }

/* The vessel setup form inside the cog panel. */
/* The graph-limit rows in Options take the padding row's exact shape (GK,
   22 Jul 2026) - .pad-box/.pad-lab/.thr-unit, one look, not a copy - and
   the Statistics captions are .pad-lab too (GK: one size and style). The
   fixed caption width keeps every box and value in one column across both
   sections; scoped so the ladder's own padding row is untouched. */
.sensor-details .pad-lab,
.stats .pad-lab { min-width: 90px; }
/* --- Temperature: the LCD readout and the zone ladder ------------------ */
/* A temperature is self-describing, so it is shown as a reading rather than
   drawn on a gauge. An LCD is deliberately ONE colour whatever it reads: we
   never know what is being measured - a freezer at -18 is healthy, a kiln at
   100 is stone cold - so colouring by value would claim a meaning we do not
   have. The thresholds below are where meaning legitimately comes from,
   because the owner set them. */

.lcd {
  background: #0e1013;
  border: 1px solid #2b3038;
  border-radius: 12px;
  padding: 14px 16px 11px;
  box-shadow: inset 0 2px 14px rgba(0, 0, 0, 0.7);
}
/* Per-type readout colour: the temperature keeps GK's orange; a water level
   reads in the water cyan so the two sibling pages tell themselves apart. */
.sensor-list { --readout: #ffb641; }
.sensor-list[data-device-type="WTR"] { --readout: #2ec5c5; }
/* Check now keeps the right edge it had beside the removed cog (GK,
   22 Jul 2026 - the chart's cog is the settings' one door now). */
.lcd-top { display: flex; align-items: center; justify-content: flex-end; margin-bottom: 4px; }
/* Ask the sensor to read now - the on-demand request the device page makes. */
.lcd-check {
  font-size: 12px; font-weight: 500; color: #ffb641;
  background: transparent; border: 1px solid rgba(255, 182, 65, 0.4);
  border-radius: 8px; padding: 5px 14px; cursor: pointer;
}
.lcd-check:hover { background: rgba(255, 182, 65, 0.12); }
.lcd-check:disabled { opacity: 0.5; cursor: default; }
/* The reading, typed in Roboto Bold (GK, 22 Jul 2026 - the Inter Light cut
   retired in turn; weight 900 = Black if Bold reads too light). Centered,
   the orange kept. One colour whatever it reads. The Light cut's negative
   tracking goes with it - Bold digits need their own spacing. */
.lcd-reading {
  font-family: "Roboto", -apple-system, "Segoe UI", sans-serif;
  font-weight: 700;
  font-size: 132px;
  line-height: 1;
  letter-spacing: normal;
  color: var(--readout, #ffb641);
  text-align: center;
  font-variant-numeric: tabular-nums;
  padding: 4px 0 2px;
}
.lcd-reading-unit {
  font-size: 32px;
  font-weight: 500;  /* a step lighter than the digits, same as the Light cut's ratio */
  letter-spacing: 0;
  margin-left: 10px;
  vertical-align: 2.0em; /* em of the unit's own size - rides high, like a degree mark */
}
.lcd-idle { padding: 26px 0 22px; text-align: center; font-size: 15px; color: #454f5c; }
.lcd-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: 10px; margin-top: 6px; font-size: 11px; color: #55606e; flex-wrap: wrap;
}
.lcd-meta { display: flex; align-items: center; gap: 10px; }
.lcd .sensor-age { color: #55606e; }

/* Box and title come from the shared .collapsible / .fold-head rule - the
   only thing this section owns is its place in the page rhythm. */
.zones-help { font-size: 14px; color: var(--text-dim); margin-bottom: 12px; line-height: 1.45; }
.zones-empty, .zones-more {
  font-size: 12px; color: var(--text-dim); text-align: center;
  padding: 10px; border: 1px dashed var(--border); border-radius: 8px;
}
.zones-more { margin-top: 8px; }
/* The padding: a plain row at the top of Zones and Actions (GK, 21 Jul
   2026), no box of its own - it writes over the same 57 the thresholds use,
   slot 0 - with a boxed ? that explains it. */
.pad-box {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin-bottom: 12px;
}
.pad-lab { font-size: 15px; color: var(--text-secondary); }
/* The Options graph-limit and custom-date boxes share this rule (GK,
   22 Jul 2026) - the same box as the padding's, not a copy of its
   numbers. */
.device-edit .form-card .pad-val,
.device-edit .form-card .pad-box .sensor-input,
.device-edit .form-card .pad-box .date-box {
  width: 92px; flex: none; height: 32px; font-size: 17px; font-weight: 600;
  text-align: center; font-variant-numeric: tabular-nums; padding: 0 8px;
}
/* The date boxes carry NO overrides (GK, 22 Jul 2026: same font, same
   widths, same heights) - they are the shared rule above, identical to the
   Graph high/low boxes in every way. */
/* The example text in these boxes reads MUCH lighter than a value (GK,
   22 Jul 2026) - a hint, not a reading. */
.thr-val::placeholder, .pad-val::placeholder,
.sensor-input::placeholder, .date-box::placeholder {
  color: #4a5462; font-weight: 400;
}
.pad-help {
  flex: none; width: 28px; height: 28px; margin-left: auto;
  border: 1px solid var(--border); border-radius: 8px;
  background: transparent; color: var(--text-secondary);
  font-size: 15px; font-weight: 700; cursor: pointer;
}
.pad-help:hover { color: var(--text); border-color: #3d434c; }
.pad-info {
  width: 100%; font-size: 14px; color: var(--text-dim); line-height: 1.45;
}

/* A band between two thresholds, drawn as a band: its own surface and a left
   bar, so the ladder reads as zone / threshold / zone rather than a list. The
   one the reading is sitting in is marked - the app's only claim here, and it
   is a fact, not a judgement. */
.zone {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border-radius: 8px;
  background: rgba(255, 255, 255, 0.015);
  border: 1px solid rgba(63, 134, 255, 0.32);
  border-left: 3px solid #3f86ff;
}
.zone.here {
  background: rgba(63, 134, 255, 0.16);
  border-color: rgba(63, 134, 255, 0.5);
  border-left-color: #5b9bff;
}
.zone-id { font-size: 14px; font-weight: 600; color: #6fa8ff; }
.zone.here .zone-id { color: #8cbaff; }
.zone-txt { font-size: 14px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
.zone.here .zone-txt { color: var(--text); font-weight: 500; }
.zone-now {
  margin-left: auto; font-size: 13px; font-weight: 600; color: #fff;
  background: var(--accent); border-radius: 999px; padding: 3px 10px;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

/* A threshold is a boxed card in the Devices-list idiom - bordered, tile
   radius, a left accent bar - and it is laid out the way the Controller's page
   lays it out: what happens going UP above the line, the threshold as the line,
   what happens going DOWN below it. */
.thr {
  margin: 6px 0; border: 1px solid var(--border);
  border-left: 3px solid #ffb641; /* the readout's own orange (GK, 21 Jul 2026) */
  border-radius: var(--radius-tile); background: var(--surface); overflow: hidden;
}
.thr-line {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px; background: var(--surface-raised);
  border-top: 1px solid var(--border); border-bottom: 1px solid var(--border);
}
.thr-id { font-size: 15px; color: #ffb641; }
/* The threshold value is editable in place. The Controller still owns it -
   it clamps to the neighbouring thresholds and pushes the set to the device -
   so the ladder is re-read after every write. */
.device-edit .form-card .thr-val {
  width: 92px; flex: none; height: 32px; font-size: 17px; font-weight: 600;
  text-align: center; font-variant-numeric: tabular-nums; padding: 0 8px;
}
/* No spinners: the value is typed, and the little arrows only invited a nudge
   past a neighbour (GK). The graph limit cells share the rule (GK, 22 Jul
   2026) - one rule, not a lookalike copy. */
.thr-val::-webkit-outer-spin-button,
.thr-val::-webkit-inner-spin-button,
.pad-val::-webkit-outer-spin-button,
.pad-val::-webkit-inner-spin-button,
.sensor-input::-webkit-outer-spin-button,
.sensor-input::-webkit-inner-spin-button,
.date-box::-webkit-outer-spin-button,
.date-box::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.thr-val, .pad-val, .sensor-input, .date-box { -moz-appearance: textfield; appearance: textfield; }
/* An unset slot reads as empty rather than as something switched off. */
.thr-id.is-unset, .thr-unit.is-unset { color: var(--text-dim); }
.thr-unit { font-size: 14px; color: var(--text-dim); }
.thr-clear {
  margin-left: auto; font-size: 13px; color: var(--text-dim);
  background: transparent; border: 1px solid var(--border);
  border-radius: 8px; padding: 4px 10px; cursor: pointer;
}
.thr-clear:hover { color: var(--text-secondary); border-color: #3d434c; }
/* An unused slot carries no orange at all - the orange means "this threshold is
   doing something", and an empty one is not. */
.thr-empty { border-color: #2a2f38; border-left-color: #3d434c; }
.thr-empty .thr-line { background: transparent; }

.act {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 12px; font-size: 14px;
}
.act-dir {
  font-size: 15px; width: 52px; flex: none;
  color: #ffb641; letter-spacing: 0.01em;
}
/* The arrow carries the direction, so it is the strong mark on the line. */
.act-arrow {
  flex: none; width: 20px; text-align: center;
  font-size: 20px; font-weight: 800; line-height: 1;
  color: #ffb641; text-shadow: 0 0 10px rgba(255, 182, 65, 0.4);
}

/* Trigger fires the list beside it. Grey and inert when nothing is bound -
   there is no point offering to run an empty list. */
.act-cog { margin-left: auto; width: 28px; height: 28px; }
.act-run {
  flex: none;
  font-size: 14px; font-weight: 500; color: #fff;
  background: #3f86ff; border: 1px solid #3f86ff;
  border-radius: 8px; padding: 5px 14px; cursor: pointer;
}
.act-run:hover { background: #5b9bff; border-color: #5b9bff; }
.act-run.is-idle {
  color: var(--text-dim); background: transparent;
  border-color: var(--border); cursor: default;
}
.act-what { color: var(--text); font-size: 15px; font-weight: 600;
            font-variant-numeric: tabular-nums; }
.act-what.none { color: var(--text-dim); font-weight: 500; }

@media (max-width: 640px) {
  .lcd { padding: 12px 12px 10px; }
  .lcd-reading { font-size: 92px; }
  .lcd-reading-unit { font-size: 24px; }
    .act-dir { width: 46px; font-size: 14px; }
  .act-run { min-height: 40px; }
  .device-edit .form-card .thr-val { width: 84px; min-height: 44px; }
  .thr-clear { min-height: 44px; }
  /* A whole From/To row - caption, three boxes, its button - fits ONE
     phone row (GK, 22 Jul 2026): every box in the family narrows to one
     shared width and a 16px face (measured: the row has 293px to give and
     needs every one of these), the caption column and gaps tighten with
     them. */
  .sensor-details .pad-box { gap: 6px; }
  .sensor-details .pad-lab, .stats .pad-lab { min-width: 72px; }
  .device-edit .form-card .pad-box .sensor-input,
  .device-edit .form-card .pad-box .date-box {
    width: 47px; padding: 0 3px; font-size: 16px;
  }
}

/* --- The sensor history graph (GK, 21 Jul 2026) --------------------------- */
/* The bounded reading history on the readout's own dark face; the line takes
   the per-type readout colour. The vertical range is the owner's (the cog's
   app-only graph limits), or the data's when unset. */
.sensor-graph {
  background: #0e1013; border: 1px solid #2b3038;
  border-radius: 12px; padding: 14px;
}
.graph-title { font-size: 17px; font-weight: 500; margin-bottom: 8px; }
/* The span chips and the download, under the chart's title (GK, 21 Jul 2026).
   They wrap rather than scroll, so a 375px phone shows all five and the
   download drops to its own line instead of overflowing the card. */
.graph-spans {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  margin: 0 0 10px;
}
.span-chip {
  background: transparent; color: var(--text-secondary);
  border: 1px solid var(--border); border-radius: 999px;
  padding: 4px 11px; font-size: 12px; font-family: inherit;
  cursor: pointer; transition: color .15s, border-color .15s, background .15s;
}
.span-chip:hover { color: var(--text); border-color: #3a414c; }
.span-chip.is-on {
  color: var(--text); border-color: var(--accent);
  background: rgba(46, 107, 255, 0.14);
}
/* Standalone under its own DOWNLOAD heading since 22 Jul 2026 (GK) - the
   inline-block replaces the flex-row margin it used to lean on. */
.graph-dl {
  display: inline-block; font-size: 12px; color: var(--text-secondary);
  text-decoration: none; border: 1px solid var(--border);
  border-radius: 999px; padding: 4px 11px; transition: color .15s, border-color .15s;
}
.graph-dl:hover { color: var(--text); border-color: #3a414c; }

/* Opened from the chart's cog the panel sits INSIDE the chart card (GK), so
   it drops its own card dressing and is divided off by a rule instead - one
   box, a chart with its settings under it, not a card within a card. */
.sensor-graph .out-settings {
  background: transparent; border: none; border-radius: 0;
  border-top: 1px solid var(--border);
  margin: 12px -14px 0; padding: 12px 14px 0;
}
.graph-span { font-size: 13px; color: var(--text-dim); font-weight: 400; margin-left: 8px; }
/* The axis titles sit hard against the viewBox edges now that the padding is
   tight - their ink reaches -0.5 and 193.5 of a 0..192 box. The card's own
   14px padding has room for that, so let it paint rather than clip (the UA
   default for an SVG viewport is overflow: hidden). Nothing else in the
   chart reaches the edge: the trace is bounded by its own geometry. */
.graph-svg { display: block; width: 100%; height: auto; overflow: visible; }
/* Zones and Actions folds on the shared .collapsible / .fold-head rule - no
   rules of its own beyond the title's existing look. */
/* The visual hierarchy of the chart's furniture (GK, 21 Jul 2026): the two
   AXES are solid and brighter, the interior break-up lines are DASHED and
   dim, and the white thresholds sit brighter still - three layers a glance
   can tell apart. */
.graph-grid { stroke: #2b3240; stroke-width: 1; stroke-dasharray: 3 5; }
.graph-axisline { stroke: #8a93a0; stroke-width: 1.2; }
.graph-axis-title { fill: #8a93a0; font-size: 9px; letter-spacing: 0.06em; }
/* The window's low and high moved to the Statistics section (GK, 22 Jul
   2026) - the chart's title row keeps only its own furniture. */
/* Statistics rows ARE .pad-box rows with .pad-lab captions (GK, 22 Jul
   2026: one size and style) - the only rule of their own is the value's
   look, and the rows' shared bottom margin already spaces the note. */
.stat-val { font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat-note { font-size: 12px; color: var(--text-dim); }
.graph-pager { float: right; display: inline-flex; gap: 4px; margin: -2px 0 0 8px; }
.graph-page {
  background: transparent; color: var(--text-secondary);
  border: 1px solid var(--border); border-radius: 8px;
  min-width: 26px; height: 26px; padding: 0 7px; font-size: 12px;
  font-family: inherit; cursor: pointer; transition: color .15s, border-color .15s;
}
.graph-page:hover:not(:disabled) { color: var(--text); border-color: #3a414c; }
.graph-page:disabled { opacity: 0.35; cursor: default; }
.graph-page-now { color: var(--accent); border-color: var(--accent); }
/* The reading line is the readout's colour (GK, 21 Jul 2026, reversing the
   white-line trial of the same day): the line, the fill under it and the big
   number all say "this is the reading", and the THRESHOLDS are the white
   reference lines it is judged against. */
.graph-line {
  fill: none; stroke: var(--readout, #ffb641); stroke-width: 2.5;
  stroke-linejoin: round; stroke-linecap: round;
}
.graph-dot { fill: var(--readout, #ffb641); }
.graph-axis { fill: #55606e; font-size: 9px; font-variant-numeric: tabular-nums; }
/* SVG text is sized in viewBox units, so it shrinks with the chart: on a
   375px phone the card renders the 560-unit viewBox at ~53%, which put the
   axis at 6.3 real pixels - unreadable, and the time ticks exist to BE read.
   Scaling the user-unit size back up lands them near 11px on a phone while
   desktop is untouched. */
@media (max-width: 640px) {
  .graph-axis { font-size: 16px; }
  .graph-thr-lab { font-size: 14px; }
  .graph-axis-title { font-size: 15px; }
}
/* Thresholds/zones on the graph (GK, 21 Jul 2026): the ladder's boundaries
   over the history, in the same orange, only when the owner turns them on. */
.graph-thr { stroke: #f2f3f5; stroke-width: 1; stroke-dasharray: 5 4; opacity: 0.7; }
.graph-thr-lab { fill: #f2f3f5; font-size: 11px; font-variant-numeric: tabular-nums; opacity: 0.85; }
.graph-zone { fill: #f2f3f5; opacity: 0.04; }
/* A checkbox row in the settings panel. The row layout itself comes from the
   shared label.form-check exception - the device page's label rule stacks
   every label into a column (caption above box), and form-check is the
   established way back to box-beside-words for a checkbox. */
.sensor-check { margin-top: 12px; cursor: pointer; }
.sensor-check input { cursor: pointer; }

/* --- Camera (GK, 21 Jul 2026): the development live view ------------------ */
.camera-stage {
  position: relative; overflow: hidden; border-radius: var(--radius-tile);
  border: 1px solid var(--border); background: #000;
  aspect-ratio: 16 / 9; touch-action: none; user-select: none;
}
.camera-stage img {
  width: 100%; height: 100%; object-fit: contain; display: block;
  transform-origin: 0 0; will-change: transform; -webkit-user-drag: none;
}
.camera-controls { display: flex; gap: 8px; align-items: center; margin-top: 12px; }
.camera-controls .btn { min-width: 44px; }

/* --- Sign in (GK item 8, 20 Jul 2026) ------------------------------------ */
.login-shell { display: flex; justify-content: center; padding-top: 12vh; }
.login-card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: var(--panel, #141619);
  border: 1px solid var(--border, #26292e);
  border-radius: 12px;
  padding: 28px;
}
.login-title { font-size: 20px; letter-spacing: 2px; margin: 0; }
.login-sub { margin: 0; color: var(--text-dim, #9aa3ad); font-size: 13px; }
/* Forgot password: a quiet text link, not another button (the card's one
   action is Sign in). The recovery box it opens reuses the card's stack. */
.login-forgot {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--text-secondary); font-size: 13px; text-align: left;
  text-decoration: underline;
}
.login-forgot:hover { color: var(--text); }
#recover-box { display: flex; flex-direction: column; gap: 10px; }
.nav-logout { margin-left: auto; }

/* --- AI Setup sections (GK, 21 Jul 2026) --------------------------------- */
/* Each section's explanatory text tucks behind a boxed ? beside the title -
   the page reads as clean titles until you ask. The ? is the padding-box
   idiom, generalised. (Section spacing is the app-wide .form-card rhythm.) */
/* A CIRCLE, vertically centred on the title's line, 15px off the text
   (GK, 21 Jul 2026). The inline-flex centres the ? glyph in the circle. */
.help-btn {
  flex: none; width: 26px; height: 26px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 50%;
  background: transparent; color: var(--text-secondary);
  font-size: 14px; font-weight: 700; line-height: 1; cursor: pointer;
  padding: 0;
}
.help-btn:hover { color: var(--text); border-color: #3d434c; }
/* The revealed info is a COMMENT BOX (GK, 21 Jul 2026): its own raised
   bubble with a caret up toward the ? line, so it reads as an aside the
   button spoke rather than page prose. (The warning stays loud outside.) */
.form-card .help-info {
  position: relative;
  margin-top: 12px;
  padding: 10px 14px;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-secondary);
}
.form-card .help-info::before {
  content: "";
  position: absolute;
  top: -5px;
  left: 26px;
  width: 8px;
  height: 8px;
  background: var(--surface-raised);
  border-left: 1px solid var(--border);
  border-top: 1px solid var(--border);
  transform: rotate(45deg);
}
/* The ? sits beside the title text; inside a plain h2 it flows inline, so
   centre it on the text line (vertical-align: middle lands 3px low against
   the measured glyph centre at this size - the nudge trues it up). */
h2 .help-btn { margin-left: 15px; vertical-align: middle; }
.fold-head .help-btn { transform: translateY(-3px); }
/* The scope card's h2 is flex (its fold chevron is the ::after, which flex
   turns into the last item); the auto margin walks the chevron to the far
   edge, leaving the ? beside the title. */

/* --- AI Permissions (GK, 21 Jul 2026) ------------------------------------ */
/* The warning is the one loud thing on the page - amber, bordered, and it
   does not fold away into grey footnote text. */
.ai-warning {
  margin: 10px 0 0;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: #ffb641;
  background: rgba(255, 182, 65, 0.07);
  border: 1px solid rgba(255, 182, 65, 0.35);
  border-left: 3px solid #ffb641;
  border-radius: 8px;
}
/* The one-tap levers, spaced off the warning (GK: "provide a space"). */
.ai-scope-levers { display: flex; gap: 10px; margin: 16px 0 14px; }

/* --- Sharing (GK item 9, 20 Jul 2026) ------------------------------------ */
.share-devices { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.share-device-head { display: flex; align-items: center; gap: 8px; font-size: 14px; }
.share-outputs { display: flex; flex-wrap: wrap; gap: 6px 14px; margin: 6px 0 2px 26px; }
.share-output { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-secondary); }
.share-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.share-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.share-who { flex: none; font-size: 13px; min-width: 180px; }
.share-link { flex: 1; min-width: 120px; font-size: 12px; }
/* The guest's access code beside the link (GK) - monospace so it reads as
   a code; Copy takes link + code together. */
.share-code { flex: none; font-family: "SF Mono", ui-monospace, Menlo, monospace; font-size: 12px; color: var(--text-secondary); white-space: nowrap; }
.share-expired .share-who { color: var(--text-dim); }
/* The review line under each share: exactly what it reaches, by name. */
.share-scope { flex-basis: 100%; margin-top: 2px; }
/* The owner's instructions for the guest (GK, 22 Jul 2026) - one shared
   rule for the Setup card and the share's own page. */
.share-instructions { width: 100%; margin-top: 4px; resize: vertical; }
/* The guest-owner thread (GK, 22 Jul 2026) - the share page and the
   Guest Portal's Messages section share these rules. */
.message-list { display: flex; flex-direction: column; gap: 10px; max-height: 320px; overflow-y: auto; }
.message-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.msg-who { flex: none; font-size: 12px; font-weight: 600; }
.msg-owner .msg-who { color: var(--accent); }
.msg-when { flex: none; }
.msg-text { flex-basis: 100%; margin: 2px 0 0; font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
.message-compose { display: flex; align-items: flex-end; gap: 8px; margin-top: 12px; }
.message-compose textarea { flex: 1; resize: vertical; }
/* The access log (GK, 22 Jul 2026): when / what happened / from where. */
.access-list { display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
.access-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.access-when { flex: none; color: var(--text-secondary); min-width: 150px; }
.access-outcome { flex: none; }
.access-outcome[data-outcome="wrong_code"],
.access-outcome[data-outcome="throttled"] { color: #e8a13c; }
.access-from { flex: 1; min-width: 120px; text-align: right; }
/* Access-time rules (GK, 20 Jul 2026): day chips + a daily range per row. */
.share-times { margin-top: 12px; }
.share-rule { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 8px 0; }
.share-rule-days { display: flex; gap: 4px; flex-wrap: wrap; }
.share-day { padding: 4px 8px; font-size: 12px; }
.share-day[data-on="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
.share-rule-from, .share-rule-to { width: 110px; flex: none; }
.guest-name { font-size: inherit; }
/* --- The Guest Portal (GK, 22 Jul 2026) ----------------------------------- */
/* The page's title is a TAG (GK: "make it a nice tag") - the accent pill,
   sitting above the folding cards. */
.guest-portal-tag { display: inline-block; margin-bottom: 16px; padding: 6px 16px; font-size: 15px; font-weight: 600; letter-spacing: 0.4px; color: var(--accent); background: rgba(46, 107, 255, 0.12); border: 1px solid var(--accent); border-radius: 999px; }
.guest-instructions { margin: 0; font-size: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
.guest-quick { display: flex; gap: 8px; flex-wrap: wrap; }
.guest-revoke { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
/* The guest dashboard (GK, 20 Jul 2026): one block per shared device,
   rows in the device page's own style. */
.dash-device { margin-top: 24px; display: flex; flex-direction: column; gap: 18px; }
.dash-device:first-child { margin-top: 0; }
.dash-device-name { font-weight: 600; font-size: 15px; color: var(--text-secondary); }

/* --- The Alarms & Alerts DEMO page (GK overnight list, 22 Jul 2026) ------- */
/* Alarms stay pinned while the rest scrolls under them (GK: "Alarms remain
   up the top, pinned. Alerts can scroll"). */
.demo-note { margin-bottom: 16px; }
.alarm-card {
  position: sticky; top: 10px; z-index: 5;
  border-left: 3px solid #e05252;
}
.demo-alarm-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 12px; margin-bottom: 8px;
  border: 1px solid rgba(224, 82, 82, 0.5); border-radius: 8px;
  background: rgba(224, 82, 82, 0.08);
}
.alert-scroll {
  max-height: 320px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 8px;
}
.demo-alert-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 6px 12px; border: 1px solid var(--border); border-radius: 8px;
}
.demo-when {
  flex: none; color: var(--text-dim); font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.demo-controls {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
.demo-thresh { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
.demo-thresh .input-basic { width: 64px; text-align: center; }
.missed-row { display: flex; align-items: baseline; gap: 10px; padding: 5px 0; }
.missed-lab { min-width: 90px; color: var(--text-secondary); flex: none; }
.missed-text { color: var(--text-dim); font-size: 14px; }

/* --- A share's own page (GK, 22 Jul 2026) --------------------------------- */
.invite-lines { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.invite-line { display: flex; align-items: center; gap: 10px; }
.invite-line .pad-lab { min-width: 100px; }
.invite-link { flex: 1; min-width: 0; }
/* The access code is the thing a guest must not miss - big and clear. */
.invite-code {
  font-size: 22px; font-weight: 700; letter-spacing: 0.12em;
  font-variant-numeric: tabular-nums; color: var(--accent);
}
.invite-preview {
  width: 100%; box-sizing: border-box; font-family: inherit;
  line-height: 1.5; resize: vertical; margin-bottom: 12px;
}
