/* TL console. Dark, dense, readable across a monitoring desk. Deliberately plain CSS —
   no framework, no build step (see hub/app/console.py for the reasoning). */

:root {
  --bg: #0b0f14; --panel: #121821; --line: #232c38; --ink: #e6eef8; --dim: #8b9bb0;
  --ok: #2ec27e; --warn: #f5a623; --crit: #e4452f; --idle: #5b6675;
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--ink);
  font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
}
body.centred { display: grid; place-items: center; min-height: 100vh; }
h2 { font-size: 15px; margin: 22px 0 8px; font-weight: 600; }
h2 .meta { font-weight: 400; font-size: 12px; color: var(--dim); margin-left: 8px; }
.muted, .meta { color: var(--dim); }
.mono { font-family: ui-monospace, "Cascadia Code", monospace; font-size: 12px; color: var(--dim); }
.grow { flex: 1; }
.hidden { display: none !important; }
.note {
  font-size: 12.5px; color: #cbd5e1; background: #161d26; border-left: 3px solid var(--warn);
  padding: 8px 12px; margin: 0 0 10px; border-radius: 4px;
}

/* ---- chrome ---- */
.topbar {
  display: flex; align-items: center; gap: 18px; padding: 10px 18px;
  background: var(--panel); border-bottom: 1px solid var(--line); flex-wrap: wrap;
}
.brand { font-weight: 700; letter-spacing: .01em; }
.brand span { font-weight: 400; color: var(--dim); }
.brand.big { font-size: 20px; }
.tabs { display: flex; gap: 4px; }
.tabs a {
  color: var(--dim); text-decoration: none; padding: 6px 11px; border-radius: 6px;
  font-size: 13px; display: flex; align-items: center; gap: 6px;
}
.tabs a:hover { background: #1a222c; color: var(--ink); }
.tabs a.on { background: #1d2938; color: var(--ink); }
.who { margin-left: auto; display: flex; align-items: center; gap: 12px;
       font-size: 12px; color: var(--dim); }
.clock { font-family: ui-monospace, monospace; }
.inline { display: inline; }
main { padding: 14px 18px 40px; }

button, .small {
  background: #223044; color: var(--ink); border: 1px solid #2f3f56; border-radius: 6px;
  padding: 6px 11px; font: inherit; font-size: 12.5px; cursor: pointer;
}
button:hover { background: #2c3d55; }
button:disabled { opacity: .5; cursor: default; }
/* `ghost` and `danger` are asked for by the markup in several places. A dismissal and a
   deletion should not carry the same weight as the action they sit beside. */
button.ghost { background: transparent; border-color: var(--line); color: var(--dim); }
button.ghost:hover { background: #1a222c; color: var(--ink); }
/* Removing a contact is routine. On a console where red means "a person must act now" — which
   is what the activation blocker directly above these rows is saying — a permanently red button
   in every row spends the alarm colour on a control, and the two then compete for the same eye.
   It earns the colour on hover, at the point it is about to be pressed. */
button.danger { background: transparent; border-color: var(--line); color: var(--dim); }
button.danger:hover, button.danger:focus-visible {
  background: #2a1714; border-color: #5d2a20; color: #ff9b86;
}
input, select, textarea {
  background: #0d1219; color: var(--ink); border: 1px solid #2a3543; border-radius: 6px;
  padding: 7px 9px; font: inherit; font-size: 13px;
}
textarea { resize: vertical; min-height: 62px; line-height: 1.5; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: #3d5a80; box-shadow: 0 0 0 2px rgba(61, 90, 128, .35);
}
input:disabled, select:disabled, textarea:disabled {
  opacity: .65; cursor: default; background: #0b0f14;
}
input::placeholder, textarea::placeholder { color: #5b6675; }

/* ---- labelled form fields ----
   Used by the client and staff intake forms. These were referenced by the markup but never
   defined, so every <label> fell back to display:inline — caption and field ran together on
   one line and no two fields in a grid shared a baseline. The caption belongs above the field:
   it stays readable once the field is full, which a placeholder does not. */
.cfg-row { display: flex; flex-direction: column; gap: 4px; min-width: 0;
           font-size: 11.5px; color: var(--dim); }
.cfg-row > input, .cfg-row > select, .cfg-row > textarea { width: 100%; color: var(--ink); }
.cfg-check { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px;
             padding: 7px 0; cursor: pointer; }
.cfg-check input { margin: 0; accent-color: var(--ok); width: 15px; height: 15px; }

/* ---- login ---- */
.card {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px;
}
.card.login { width: min(360px, 92vw); display: flex; flex-direction: column; gap: 10px; }
.card.login .sub { color: var(--dim); font-size: 13px; margin-bottom: 4px; }
.card.login label { display: flex; flex-direction: column; gap: 4px; font-size: 12px;
                    color: var(--dim); }
.err { color: #ff8f7a; font-size: 12.5px; }
/* The intake forms ask for this one by name. Undefined, a rejected save reported its reason in
   ordinary body text and read as a caption rather than as a refusal. Empty stays invisible. */
.login-err { color: #ff8f7a; font-size: 12.5px; }
.login-err:not(:empty) { display: inline-block; padding: 2px 0; }

/* ---- rows & strips ---- */
.rows { display: flex; flex-direction: column; gap: 4px; }
.row {
  display: flex; align-items: center; gap: 10px; padding: 7px 10px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 6px; font-size: 13px;
}
.row.warn { border-color: #6b5320; background: #1e1a11; }
.row.crit { border-color: #7a2a1e; background: #22120f; }

/* Settled history. If every row is loud then nothing is, and the row that actually needs a
   person is the one that gets missed — so anything already dealt with, or that never needed
   a decision, is deliberately quiet. It stays fully readable; it just stops competing. */
.row.quiet {
  background: transparent; border-color: #1b232e; color: var(--dim); font-size: 12.5px;
  padding: 5px 10px;
}
.row.quiet .pill { opacity: .75; }
.row.quiet:hover { background: var(--panel); color: inherit; }
.row.quiet:hover .pill { opacity: 1; }

/* Repeat counter. Reads as "this happened N times", not as another alarm — so it must not
   borrow the crit palette however high the number goes. */
.pill.rep { background: #2b3546; color: #b9c7d8; }

/* Another team's work. Dimmed and set back rather than hidden: a team leader can act on it,
   and doing so is recorded, so it should read as somebody else's without being unreadable
   (D-016). Never applied to an alert strip entry — an unacknowledged FALL is nobody's
   background. */
.row.notmine, .card.notmine {
  opacity: .62; border-style: dashed;
}
.row.notmine:hover, .card.notmine:hover { opacity: 1; }
.strip { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }

.pill {
  font-size: 10.5px; font-weight: 700; padding: 2px 7px; border-radius: 9px;
  background: #2a3340; color: var(--dim); text-transform: uppercase; letter-spacing: .03em;
  white-space: nowrap;
}
.pill.ok, .pill.online, .pill.new { background: #14301f; color: #6ee7a8; }
.pill.warn, .pill.stale, .pill.needs_info, .pill.degraded { background: #3a3320; color: #f0c674; }
.pill.crit, .pill.bad, .pill.offline, .pill.revoked { background: #3a1a15; color: #ff9b86; }
.pill.idle, .pill.never, .pill.closed, .pill.ack { background: #232c38; color: #9fb0c4; }

/* ---- station cards ---- */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 10px; }
.card.station { padding: 12px; display: flex; flex-direction: column; gap: 6px; }
.card.station.offline { border-color: #7a2a1e; }
.card.station.stale { border-color: #6b5320; }
.station-head { display: flex; align-items: center; gap: 8px; }
.station-head b { flex: 1; }
.station-sub { font-size: 11.5px; color: var(--dim); display: flex; align-items: center;
               gap: 6px; flex-wrap: wrap; }
.cams { display: flex; flex-direction: column; gap: 3px; margin-top: 4px; }
.cam {
  display: flex; justify-content: space-between; gap: 8px; font-size: 12px;
  padding: 5px 8px; border-radius: 5px; background: #0f151d; border-left: 3px solid var(--idle);
}
.cam .camstate { color: var(--dim); font-size: 11px; }
.cam.ok { border-left-color: var(--ok); }
.cam.warn { border-left-color: var(--warn); }
.cam.crit { border-left-color: var(--crit); background: #1d1211; }
.cam.bad { border-left-color: #7a4a3e; }
.cam.idle { border-left-color: var(--idle); }

/* ---- reports ---- */
.card.report { margin-bottom: 8px; display: flex; flex-direction: column; gap: 7px; }
.card.report.help { border-color: #7a2a1e; background: #1b1210; }
.report-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; }
.report-body { font-size: 13px; line-height: 1.6; }
.tl-note { font-size: 12.5px; background: #16202b; border-radius: 5px; padding: 6px 9px; }
.report-actions { display: flex; gap: 6px; align-items: center; }
.report-actions .note { flex: 1; }

/* ---- stations tab ---- */
.inline-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.code-out { width: 100%; margin-top: 8px; }
.code {
  font-family: ui-monospace, monospace; font-size: 26px; letter-spacing: .12em;
  color: #6ee7a8; background: #0d1219; border: 1px dashed #2f5f45; border-radius: 8px;
  padding: 12px; text-align: center; user-select: all;
}
.badge {
  background: var(--crit); color: #fff; font-size: 10.5px; font-weight: 700;
  border-radius: 9px; padding: 1px 6px; min-width: 17px; text-align: center;
}

/* ALPHA ONLY (D-019) — a readable password on an account row. Marked rather than blended in:
   it should be obvious at a glance that this screen is showing something it normally cannot,
   and `user-select: all` makes it one click to copy onto the other workstation. */
.alpha-pw {
  display: inline-flex; align-items: center; gap: 4px; flex: none;
  border: 1px dashed #7a5c1f; background: #1a1508; border-radius: 6px;
  padding: 2px 4px 2px 8px; white-space: nowrap;
}
.alpha-pw .mono { color: #f0c674; font-size: 13px; user-select: all; }

/* ---- notifications: bell + dropdown ----
   Replaces the strip that used to sit above the board. The count stays visible from every
   tab; the detail is one click away. A permanent list that grows without limit pushes the
   work off the screen — which is how you end up unable to see the households you are
   supposed to be watching. */
.notif { position: relative; display: flex; align-items: center; }
.bell {
  position: relative; background: transparent; border: 1px solid transparent;
  border-radius: 8px; padding: 4px 8px; cursor: pointer; line-height: 1;
  display: flex; align-items: center; gap: 2px;
}
.bell:hover { background: #1a222c; border-color: var(--line); }
.bell-icon { font-size: 16px; filter: grayscale(1) opacity(.75); }
.bell.has-crit .bell-icon { filter: none; animation: bell-nudge 2.2s ease-in-out infinite; }
.bell .badge { position: relative; top: -7px; margin-left: -4px; }
@keyframes bell-nudge {
  0%, 88%, 100% { transform: rotate(0); }
  91% { transform: rotate(-11deg); }
  95% { transform: rotate(9deg); }
}
@media (prefers-reduced-motion: reduce) {
  .bell.has-crit .bell-icon { animation: none; }
}

.notif-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  width: min(400px, 92vw); background: var(--panel);
  border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 14px 40px rgba(0, 0, 0, .55); overflow: hidden;
}
.notif-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 10px 12px; border-bottom: 1px solid var(--line);
  font-size: 12.5px; font-weight: 600; color: var(--ink);
}
.notif-list { max-height: min(60vh, 460px); overflow-y: auto; }
.notif-empty { padding: 18px 12px; text-align: center; color: var(--dim); font-size: 12.5px; }
.notif-item {
  display: block; padding: 9px 12px; border-bottom: 1px solid #1a222c;
  text-decoration: none; color: var(--ink); border-left: 3px solid transparent;
}
.notif-item:last-child { border-bottom: 0; }
.notif-item:hover { background: #172030; }
.notif-item.warn { border-left-color: var(--warn); }
.notif-item.crit { border-left-color: var(--crit); }
.notif-item-top { display: flex; align-items: center; gap: 6px; }
.notif-item-detail { font-size: 12px; color: var(--dim); margin-top: 3px; }
.notif-age { font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }

/* ---- toasts ----
   Bottom right, no auto-dismiss. A repeat increments the counter on the toast already there
   rather than adding another: stacking identical toasts recreates the same wall of noise in
   a different corner of the screen. */
.toasts {
  position: fixed; right: 16px; bottom: 16px; z-index: 90;
  display: flex; flex-direction: column-reverse; gap: 8px;
  max-height: calc(100vh - 32px); overflow-y: auto;
  width: min(360px, calc(100vw - 32px));
}
.toast {
  display: flex; align-items: flex-start; gap: 6px;
  background: #161d26; border: 1px solid var(--line); border-left: 3px solid var(--warn);
  border-radius: 8px; box-shadow: 0 10px 28px rgba(0, 0, 0, .5);
}
.toast.crit { border-left-color: var(--crit); background: #1d1311; }
.toast-body { flex: 1; padding: 10px 4px 10px 11px; cursor: pointer; min-width: 0; }
.toast-body:focus-visible { outline: 2px solid var(--warn); outline-offset: -2px; }
.toast-title {
  display: flex; align-items: center; gap: 6px;
  font-size: 12.5px; font-weight: 700; color: var(--ink);
}
.toast-detail { font-size: 12px; color: var(--dim); margin-top: 3px; word-break: break-word; }
.toast-go { font-size: 11px; color: var(--dim); margin-top: 6px; opacity: .75; }
.toast-body:hover .toast-go { color: var(--ink); opacity: 1; }
.toast-x {
  background: transparent; border: 0; color: var(--dim); cursor: pointer;
  font-size: 13px; line-height: 1; padding: 9px 10px; border-radius: 6px;
}
.toast-x:hover { background: #232c38; color: var(--ink); }
.toast-in { animation: toast-in .18s ease-out; }
@keyframes toast-in {
  from { transform: translateY(6px); opacity: 0; }
  to   { transform: translateY(0);   opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .toast-in { animation: none; }
}

/* ---- agents / credential caches ---- */
.chips { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; }
.chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px;
  background: #1a222c; border: 1px solid var(--line); border-radius: 12px;
  padding: 3px 9px; cursor: pointer; white-space: nowrap;
}
.chip:hover { background: #223044; }
.chip input { margin: 0; accent-color: var(--ok); }
/* An assignment to a household that is no longer active. Shown rather than hidden: a hidden
   assignment is one that gets deleted by the next click on any other chip in the row. */
.chip-unlisted { border-style: dashed; border-color: #6b5a2a; color: var(--dim); }
.ag-edit { display: flex; align-items: center; gap: 6px; }

/* Households and team, truncated so every row is the same height whatever the list length.
   The full text is on the element's title, and the editable copy is in the Households panel. */
.ag-sum {
  max-width: 22ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: none;
}

/* ---- icon buttons + row popovers ----
   An account row carried nine controls on one line and wrapped on any normal screen. The ones
   set once and rarely revisited moved behind an icon; the buttons that destroy something kept
   the space, because those are the ones that must not be hit by accident.

   Deliberately not a modal — no backdrop, nothing blocked. A monitoring console should never
   stop being readable because somebody left a panel open. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; flex: none;
  background: transparent; border: 1px solid transparent; border-radius: 5px;
  color: var(--dim); cursor: pointer;
}
.icon-btn:hover { background: #223044; border-color: var(--line); color: var(--ink); }
.icon-btn:focus-visible { outline: 2px solid #3d5a80; outline-offset: 1px; }
.icon-btn.pop-open { background: #223044; border-color: #3d5a80; color: var(--ink); }
.icon-btn svg { width: 15px; height: 15px; display: block; }

.pop {
  position: absolute; z-index: 60; min-width: 250px; max-width: 340px;
  background: var(--panel); border: 1px solid var(--line); border-radius: 7px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, .55); padding: 10px 12px 12px;
}
.pop-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  color: var(--dim); padding-bottom: 7px; margin-bottom: 9px;
  border-bottom: 1px solid var(--line);
}
.pop-body { display: flex; flex-direction: column; gap: 11px; }
.pop-field { display: flex; flex-direction: column; gap: 4px; }
.pop-field > span { font-size: 11.5px; color: var(--dim); }
.pop-field select, .pop-field input { width: 100%; }
.pop-sec { font-size: 11.5px; color: var(--dim); line-height: 1.4; }
.pop .chips { gap: 5px; }

/* The dots are a shoulder-surfing measure only: while D-019's switch is on the value is
   readable to this admin by design, and the toggle must not read as more than it is. */
.alpha-pw .pw-dots { letter-spacing: 2px; user-select: none; }
.alpha-pw .pw-eye { width: 22px; height: 20px; color: #b08a3a; }
.alpha-pw .pw-eye:hover { background: #2a2110; border-color: #7a5c1f; color: #f0c674; }
.alpha-pw .pw-eye svg { width: 13px; height: 13px; }
.report-actions .reply { flex: 1; }

/* ---- metrics tiles ---- */
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.tile {
  background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px;
}
.tile.ok   { border-left: 3px solid var(--ok); }
.tile.warn { border-left: 3px solid var(--warn); }
.tile.crit { border-left: 3px solid var(--crit); }
.tile-v { font-size: 24px; font-weight: 700; font-variant-numeric: tabular-nums; }
.tile-l { font-size: 12px; color: var(--dim); text-transform: uppercase; letter-spacing: .03em; }
.tile-s { font-size: 11px; color: var(--dim); margin-top: 2px; }
.policy {
  flex: 1 1 100%; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  margin-top: 6px; padding-top: 8px; border-top: 1px dashed var(--line); font-size: 12px;
}
.policy label { display: inline-flex; align-items: center; gap: 5px; color: var(--dim); }

/* ---- clients (D-017) ---- */
/* A form is not a board. The rest of the console spends its width on rows that benefit from
   it; a field spread across a 1900px monitor just puts its caption an inch from its value. */
/* 1112 − 32px of card padding = 1080, the same width .cl-grid caps itself at. Everything in
   the drawer then shares one right edge instead of the grid stopping 8px short of the rows. */
#clDetail { max-width: 1112px; margin-top: 8px; }
#clDetail .cl-grid { max-width: none; }
.cl-head { display: flex; align-items: center; gap: 10px; padding-bottom: 10px;
           margin-bottom: 14px; border-bottom: 1px solid var(--line); }
.cl-head .brand { font-size: 15px; }
.cl-head .mono { font-size: 12px; }
/* Section headers, borrowing the tile-label treatment already used on Metrics: this drawer
   holds four unrelated things and they should not arrive at the same weight as a field. */
.cl-sub {
  font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
  color: var(--dim); margin: 24px 0 10px; padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}
/* Capped here rather than on the drawer alone so the staff intake on Agents, which uses the
   same grid, gets the same treatment. */
.cl-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px 14px;
           margin-bottom: 12px; max-width: 1080px; }
.cl-grid .wide { grid-column: span 2; }
@media (max-width: 1180px) { .cl-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 860px)  { .cl-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 580px)  {
  .cl-grid { grid-template-columns: 1fr; }
  .cl-grid .wide { grid-column: auto; }
}
.cl-notes { max-width: 640px; }
.cl-actions { display: flex; align-items: center; gap: 8px; margin-top: 20px;
              padding-top: 14px; border-top: 1px solid var(--line); flex-wrap: wrap; }
.cl-actions #dSave { background: #1d4030; border-color: #2f6349; color: #9ff0c4; }
.cl-actions #dSave:hover { background: #245039; }

/* An activation blocker is a state somebody has to act on. The prose around it is only
   explanation. Left in the same amber box they read as four warnings, and the page's own rule
   applies — if everything is loud, the one that needs a person is the one that gets missed. */
.blockers {
  font-size: 12.5px; color: #ffd9d0; background: #22120f;
  border: 1px solid #7a2a1e; border-left: 3px solid var(--crit);
  padding: 9px 12px; margin: 0 0 14px; border-radius: 6px;
}
.note.explain { background: #10161e; border-left-color: #33415a; color: var(--dim); }

/* Contact rows are an ordered list the agent will be told to work down, so they need to read
   as a sequence rather than as a form — and the columns must line up with a caption, because
   once the fields are full "Ana" / "daughter" / "not after 9pm" are indistinguishable. */
#dContacts .row, .ct-head {
  display: grid; grid-template-columns: 26px 1.3fr 1fr 1fr 1fr 32px;
  gap: 8px; align-items: center;
}
#dContacts .row.ct-empty { display: flex; }
#dContacts .row input { min-width: 0; }
#dContacts .row .pill { min-width: 22px; text-align: center; }

/* Cameras (D-020). Same shape as a contact row, but the stream address gets the room: it is
   long, and a truncated URL is one you cannot check for a typo. */
#dCameras .row, .cam-head {
  display: grid; grid-template-columns: 26px 1fr 2.2fr 62px 32px;
  gap: 8px; align-items: center;
}
#dCameras .row.cam-empty { display: flex; }
#dCameras .row input { min-width: 0; }
#dCameras .row .pill { min-width: 22px; text-align: center; }
#dCameras .row .cm-url { font-family: ui-monospace, "Cascadia Code", monospace; font-size: 12px; }
/* Deliberately the same treatment as a .cfg-row caption, not as a .cl-sub. These name fields,
   exactly like "Full name" above them; set in small caps they instead read as a section
   heading and compete with the real one two lines up. */
.ct-head { padding: 0 10px 2px; font-size: 11.5px; font-weight: 400; color: var(--dim); }
.doc-upload {
  display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; margin-top: 10px;
  padding-top: 10px; border-top: 1px dashed var(--line);
}
.doc-upload .cfg-row { flex: 0 0 auto; }
.doc-upload .cfg-row.grow { flex: 1 1 220px; }
.doc-upload input[type=file] { min-width: 180px; }
.cl-add { margin-top: 8px; }
#agCreateCard .chips { margin-bottom: 6px; }
/* Captioned fields are taller than a bare input, so the button has to sit on the baseline of
   the fields rather than in the middle of them. */
#clCreateCard { align-items: flex-end; }
#clCreateCard .cfg-row { flex: 1 1 180px; }
#clCreateCard .login-err { flex: 1 1 100%; }
