/* TICK operator UI — inherits from i4seer.com style system.
   See OPERATOR_UI_PLAN.md "Visual design — inherit from i4seer.com".
   Source of truth for tokens: C:\SEERWEB\style.css */

/* Self-hosted fonts so the CSP can stay default-src 'self'. Both files
   are variable fonts covering weights 100..900 in a single woff2 (the
   `Latin` subset Google Fonts serves). To refresh: pull the latin
   subset URLs from https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap
   with a Chrome User-Agent and re-download. */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/inter-latin.woff2') format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url('/static/fonts/jetbrains-mono-latin.woff2') format('woff2');
}

:root {
  --paper:        #fafafa;
  --paper-2:      #f3f4f6;
  --surface:      #ffffff;
  --ink:          #111418;
  --ink-soft:     #4b5563;
  --ink-mute:     #6b7280;
  --line:         #e5e7eb;
  --line-strong:  #d1d5db;
  --teal:         #0aa39a;
  --teal-bg:      #e0f7f5;
  --teal-deep:    #047a72;
  --green:        #16a34a;
  --gold:         #b88a2c;
  --crimson:      #b91c1c;
  --shadow:       0 1px 2px rgba(15,23,42,.04), 0 1px 3px rgba(15,23,42,.06);
}

* { box-sizing: border-box; }

html {
  scrollbar-color: var(--line-strong) var(--paper);
  scrollbar-width: thin;
}
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--paper); }
::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 6px;
  border: 2px solid var(--paper);
}
::-webkit-scrollbar-thumb:hover { background: var(--ink-mute); }

body {
  margin: 0;
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-size: 15px;
  line-height: 1.55;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

.mono, code {
  font-family: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.92em;
}

/* ============================================================
   HEADER — same pattern as i4seer studio-header
   ============================================================ */
.studio-header {
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 10;
}
.studio-footer {
  border-top: 1px solid var(--line);
  background: var(--surface);
  padding: 0.6rem 1.2rem;
  text-align: center;
  font-size: 0.7rem;
  color: var(--ink-mute);
  letter-spacing: 0.04em;
}
.studio-header-inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0.85rem 1.5rem;
  display: flex;
  align-items: center;
  gap: 2rem;
}
.studio-logo {
  font-family: "JetBrains Mono", monospace;
  font-weight: 700;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}
.studio-logo::before {
  content: "";
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--teal);
  margin-right: 0.5rem;
  vertical-align: middle;
  position: relative;
  top: -1px;
}
.studio-tagline {
  color: var(--ink-mute);
  font-size: 0.9rem;
  flex: 1;
}

/* ============================================================
   OPERATOR LAYOUT
   ============================================================ */
.op-root {
  max-width: 1180px;
  margin: 0 auto;
  padding: 1.5rem;
}
.op-section {
  margin-bottom: 2rem;
}
.op-section-title {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 0.85rem;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 6px;
  box-shadow: var(--shadow);
  padding: 1rem 1.25rem;
}

#status-text {
  font-size: 0.78rem;
  letter-spacing: 0.03em;
}
#status-text[data-kind="ok"]   { color: var(--green); }
#status-text[data-kind="fail"] { color: var(--crimson); }
#status-text[data-kind="info"] { color: var(--ink-soft); }

/* ============================================================
   DEVICE LIST
   ============================================================ */
.device-list {
  display: flex;
  flex-direction: column;
}
.device-list-empty {
  color: var(--ink-mute);
  padding: 0.5rem 0;
  font-size: 0.85rem;
}
.device-row {
  display: grid;
  grid-template-columns: 130px 80px 1fr 70px;
  align-items: center;
  gap: 1rem;
  padding: 0.55rem 0.5rem;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  transition: background 100ms;
}
.device-row:last-child { border-bottom: 0; }
.device-row:hover { background: var(--paper-2); }
.device-row.selected { background: var(--teal-bg); }
.device-row-id {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--ink);
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  min-width: 0;   /* let the label truncate inside a flex parent */
}
/* Two-line label + id subline stack inside the row. Label is primary
   (sans-serif, full weight); id subline is smaller mono in muted ink. */
.device-row-stack {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.05rem;
  min-width: 0;
  line-height: 1.1;
}
.device-row-label {
  font-family: "Inter", sans-serif;
  font-size: 0.88rem;
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 26ch;
}
.device-row-id-sub {
  font-size: 0.7rem;
  color: var(--ink-mute);
  font-weight: 400;
  letter-spacing: 0.02em;
}
/* Colored "identity" dot per device — same hue as the fleet-pulse ball
   that travels across the top diagram. Clickable: opens the latest
   state push payload in a modal (or a waiting message if none yet). */
.device-row-circle {
  display: inline-block;
  width: 10px;
  height: 10px;
  flex: 0 0 10px;          /* don't let flex squeeze the circle out of round */
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8),
              0 0 0 3px var(--line);
  transition: transform 120ms;
}
.device-row-circle:hover { transform: scale(1.2); }
.device-row-fw {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.78rem;
  color: var(--ink-soft);
}
.device-row-fw.ota-active {
  /* Slight emphasis so the operator notices a mid-OTA device at a
     glance — purple matches the OTA-in-flight LED on the device. */
  color: #b95cf3;
  font-weight: 600;
}
.device-row-last {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.78rem;
  color: var(--ink-mute);
}
.device-row-status {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  justify-content: flex-end;
  font-family: "JetBrains Mono", monospace;
  font-size: 0.7rem;
  color: var(--ink-mute);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.device-row-status.clickable {
  cursor: pointer;
  padding: 0.15rem 0.45rem;
  border-radius: 4px;
  transition: background 100ms;
}
.device-row-status.clickable:hover {
  background: var(--paper-2);
  color: var(--ink);
}
.device-row-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-mute);
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3px var(--line);
}
.device-row-dot.healthy { background: var(--green); }
.device-row-dot.warn    { background: var(--gold); }
.device-row-dot.offline { background: var(--ink-mute); }

/* ============================================================
   SPLIT: device list + event detail
   ============================================================ */
.op-section.split {
  display: grid;
  grid-template-columns: minmax(360px, 1fr) minmax(420px, 1.4fr);
  gap: 1.5rem;
  align-items: start;
}
.split-left,
.split-right {
  min-width: 0;             /* let grid columns shrink */
}
@media (max-width: 900px) {
  .op-section.split {
    grid-template-columns: 1fr;
  }
}

.op-section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.detail-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-transform: none;
  letter-spacing: 0;
  font-size: 0.7rem;
}
.detail-toolbar label {
  color: var(--ink-mute);
}
.detail-toolbar select {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.72rem;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 0.18rem 0.4rem;
  color: var(--ink);
  cursor: pointer;
}

/* Sort slider toggle: two labels flanking a small track with a thumb
   that slides between them. CSS-only state via [data-mode]. */
.device-sort-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.7rem;
  cursor: pointer;
  user-select: none;
  color: var(--ink-mute);
  outline: none;
}
.device-sort-toggle:focus { color: var(--ink); }
.device-sort-toggle .device-sort-track {
  position: relative;
  width: 28px;
  height: 14px;
  border-radius: 7px;
  background: var(--line);
  transition: background 120ms;
}
.device-sort-toggle .device-sort-thumb {
  position: absolute;
  top: 1px;
  left: 1px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(0,0,0,0.2);
  transition: transform 120ms;
}
.device-sort-toggle[data-mode="name"] .device-sort-thumb {
  transform: translateX(14px);
}
.device-sort-toggle[data-mode="name"] .device-sort-track {
  background: var(--teal);
}
/* Highlight the active-side label */
.device-sort-toggle[data-mode="last_seen"] .device-sort-label-left,
.device-sort-toggle[data-mode="name"]      .device-sort-label-right {
  color: var(--ink);
  font-weight: 600;
}

/* ============================================================
   EVENT LIST
   ============================================================ */
.event-list {
  display: flex;
  flex-direction: column;
  max-height: 70vh;
  overflow-y: auto;
}
.event-list-empty {
  color: var(--ink-mute);
  padding: 0.5rem 0;
  font-size: 0.85rem;
}
/* Event-type filter. When #event-list[data-filter="<op>"] is set, hide
   every .event-row whose data-op attribute doesn't match. Empty
   data-filter (or absent) shows all rows. */
.event-list[data-filter]:not([data-filter=""]) .event-row {
  display: none;
}
.event-list[data-filter]:not([data-filter=""]) .event-row.match {
  display: grid;
}
.event-row {
  display: grid;
  /* ts        op    body  chip
     120px     130px 1fr   auto
     TS column widened to 120px so "MM/DD HH:MM:SS" fits on one
     line without wrapping. Op column at 130px holds the longest
     op names (firmware_target, reading_push, auto_register)
     without bleeding into the body. Trailing `auto` column reserves
     space for an optional chip (errlog_summary today; other event
     types may render decorations there later). Non-chip rows have
     no 4th child and take zero width in that column. */
  grid-template-columns: 120px 130px 1fr auto;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.2rem 0.5rem;
  font-family: "JetBrains Mono", monospace;
  font-size: 0.78rem;
  line-height: 1.3;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  min-width: 0;   /* lets .ev-body actually shrink + ellipsize inside the grid */
}
.event-row:last-child   { border-bottom: 0; }
.event-row:hover        { background: var(--paper-2); }
.event-row.fail         { background: #fdecec; }
.event-row.fail:hover   { background: #fbdada; }
.event-row.fail .ev-op  { color: var(--crimson); }
.event-row .ev-ts       { color: var(--ink-mute);
                          white-space: nowrap; }
.event-row .ev-op       { color: var(--ink); font-weight: 600;
                          white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.event-row .ev-body     { color: var(--ink-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }

.event-row.live {
  /* Brief flash when a fresh event lands. Pulse from teal-bg to none. */
  animation: liveFlash 1.2s ease-out 1;
}
@keyframes liveFlash {
  0%   { background: var(--teal-bg); }
  100% { background: transparent; }
}

/* ============================================================
   BUTTONS — match i4seer's quiet style: light border, no fill
   until hover/primary.
   ============================================================ */
.btn {
  font-family: "Inter", sans-serif;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 0.4rem 0.85rem;
  cursor: pointer;
  transition: background 100ms, border-color 100ms, opacity 100ms;
}
.btn:hover:not(:disabled)   { background: var(--paper-2); border-color: var(--ink-mute); }
.btn:disabled                { opacity: 0.45; cursor: not-allowed; }
.btn.primary {
  background: var(--teal);
  color: white;
  border-color: var(--teal-deep);
}
.btn.primary:hover:not(:disabled) { background: var(--teal-deep); }
.btn.danger  { color: var(--crimson); border-color: var(--crimson); }
.btn.danger:hover:not(:disabled) { background: var(--crimson); color: white; }

/* ============================================================
   QUEUE COMMAND FORM
   ============================================================ */
.queue-form {
  display: grid;
  grid-template-columns: 1fr 90px auto auto auto;
  gap: 0.5rem;
  align-items: center;
}
.queue-input {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.85rem;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  background: var(--surface);
  color: var(--ink);
  outline: none;
  transition: border-color 100ms;
}
.queue-input:focus { border-color: var(--teal); }
.queue-input:disabled { background: var(--paper-2); }
.queue-via {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.82rem;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  padding: 0.4rem 0.5rem;
  color: var(--ink);
  cursor: pointer;
}
.queue-via:disabled { background: var(--paper-2); cursor: not-allowed; opacity: 0.6; }
.queue-pending {
  margin-top: 0.6rem;
  font-size: 0.74rem;
  color: var(--ink-mute);
  min-height: 1.1em;
}
.queue-pending[data-kind="ok"]   { color: var(--green); }
.queue-pending[data-kind="fail"] { color: var(--crimson); }
.queue-help-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4em;
  height: 1.4em;
  border-radius: 50%;
  color: var(--ink-mute);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  user-select: none;
  transition: color 100ms, background 100ms;
}
.queue-help-glyph:hover,
.queue-help-glyph:focus {
  color: var(--teal);
  background: var(--paper-2);
  outline: none;
}
.queue-help-card {
  max-width: 640px;
  width: 90vw;
}
.queue-help-hint {
  padding: 0.4rem 1rem 0.6rem;
  color: var(--ink-mute);
  font-size: 0.75rem;
}
.queue-help-list {
  display: flex;
  flex-direction: column;
  max-height: 60vh;
  overflow-y: auto;
  padding: 0 0.5rem 0.5rem;
}
.queue-help-row {
  display: grid;
  grid-template-columns: 180px 1fr;
  gap: 0.8rem;
  align-items: baseline;
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--line);
  font-size: 0.78rem;
  cursor: pointer;
  transition: background 100ms;
}
.queue-help-row:last-child { border-bottom: 0; }
.queue-help-row:hover      { background: var(--paper-2); }
.queue-help-cmd  { color: var(--ink); font-weight: 600; }
.queue-help-desc { color: var(--ink-mute); }

/* Group header inside the device-commands modal. Mirrors the yellow
 * "  state" / "  wifi" / ... section labels in `tick.ps1 cmds`. Not
 * clickable; visually delimits groups of rows. */
.queue-help-group {
  padding: 0.7rem 0.5rem 0.3rem;
  font-size: 0.78rem;
  font-weight: 700;
  color: var(--gold);
  border-bottom: 1px solid var(--line);
  cursor: default;
}
.queue-help-group:first-child { padding-top: 0.3rem; }

@media (max-width: 700px) {
  .queue-form { grid-template-columns: 1fr; }
  .queue-help-row { grid-template-columns: 1fr; }
}

/* ============================================================
   FIRMWARE LIST
   ============================================================ */
.firmware-list {
  display: flex;
  flex-direction: column;
}
.firmware-list-empty {
  color: var(--ink-mute);
  padding: 0.5rem 0;
  font-size: 0.85rem;
}
.firmware-row {
  display: grid;
  /* star | version | sha | comp | targeted-by | when | btn */
  grid-template-columns: 18px 60px 1fr auto auto auto auto;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.4rem;
  border-bottom: 1px solid var(--line);
  font-family: "JetBrains Mono", monospace;
  font-size: 0.76rem;
  cursor: pointer;
  transition: background 100ms;
}
.firmware-row:last-child { border-bottom: 0; }
.firmware-row:hover      { background: var(--paper-2); }
.firmware-row.targeted   { background: var(--teal-bg); }
.firmware-row.running    { background: var(--paper-2); }
.firmware-row .fw-star    { color: var(--teal); font-size: 0.95rem; line-height: 1;
                            min-width: 1em; text-align: center; }
.firmware-row.running .fw-star { color: var(--green); }
.firmware-row .fw-version { color: var(--ink); font-weight: 600; }
.firmware-row .fw-sha     { color: var(--ink-mute); overflow: hidden;
                            text-overflow: ellipsis; white-space: nowrap; }
.firmware-row .fw-comp    { color: var(--ink-mute); white-space: nowrap;
                            font-size: 0.72rem; }
.firmware-row .fw-by      { color: var(--ink-mute); white-space: nowrap;
                            font-size: 0.72rem; }
.firmware-row .fw-when    { color: var(--ink-mute); white-space: nowrap;
                            font-size: 0.72rem; }
.firmware-row .fw-set-btn {
  font-family: "Inter", sans-serif;
  font-size: 0.75rem;
  padding: 0.3rem 0.65rem;
}
.firmware-row .fw-set-btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Stacked section headers in the right column (events under queue,
   firmware under events) get breathing room so the boxes don't visually
   collide. */
.events-section-title,
.firmware-section-title {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 1.4rem;
}
.firmware-section-sub {
  color: var(--ink-mute);
  font-size: 0.75rem;
  font-weight: 400;
  margin-left: 0.3rem;
}

.firmware-status {
  margin-top: 0.6rem;
  font-size: 0.74rem;
  color: var(--ink-mute);
  min-height: 1.1em;
}
.firmware-status[data-kind="ok"]   { color: var(--green); }
.firmware-status[data-kind="fail"] { color: var(--crimson); }

@media (max-width: 900px) {
  .firmware-row {
    grid-template-columns: 18px 1fr auto;
    row-gap: 0.2rem;
  }
  .firmware-row .fw-sha,
  .firmware-row .fw-by,
  .firmware-row .fw-when {
    grid-column: 2 / 3;
  }
}

/* ============================================================
   DEVICE FLOW VISUAL — copied verbatim from i4seer.com/style.css.
   This is the "Device → Network → Portal → ..." row with animated
   dot between stages. Same DOM markup, same CSS classes.
   ============================================================ */
.tick-device-visual {
  margin-top: 0.5rem;
  max-width: 100%;
  overflow: hidden;
}
.tick-device-row {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto 1fr auto 1fr auto;
  align-items: center;
  gap: 0;
  position: relative;   /* so .tick-pulse-ball can absolute-position over it */
}
.tick-device-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  padding: 0.6rem 0.4rem;
  min-width: 60px;
}
.tick-node-dot {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--ink-mute);
  box-shadow: 0 0 0 3px var(--surface), 0 0 0 4px var(--line);
  transition: background 200ms;
}
.tick-node-green   .tick-node-dot { background: var(--green); }
.tick-node-blue    .tick-node-dot { background: var(--teal); }
.tick-node-gateway .tick-node-dot { background: var(--ink-soft); }
.tick-node-cloud   .tick-node-dot { background: var(--teal-deep); }
.tick-node-console .tick-node-dot { background: var(--ink); }
.tick-node-label {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.7rem;
  letter-spacing: 0.04em;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}
.tick-node-sub {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.6rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-mute);
  white-space: nowrap;
}
/* The Device node's sub-label flashes with the current device id when
   chkins arrive (see flashDeviceLabel). Device ids are ~14 chars where
   "TICK" is 4 — letting that text drive the grid column would shove
   every downstream stage to the right each time. We zero its layout
   width and let the text overflow rightward into the link line so the
   stage dots stay put. The text is still readable because overflow:
   visible keeps it rendered, and text-align: left anchors the growth
   to the right. */
.tick-node-green .tick-node-sub {
  width: 0;
  overflow: visible;
  text-align: left;
  /* Nudge left so the default "TICK" still looks centered under the
     dot; longer device ids will extend past the right edge of the
     column over the link line. */
  margin-left: -1.2em;
}

/* Stage-to-stage link line. Previously had a continuously-traveling
   teal dot decoration here; removed in favor of the event-driven
   .tick-pulse-ball that fires only when a real chkin arrives. */
.tick-device-link {
  position: relative;
  height: 2px;
  background: var(--line);
  margin: 0 0.25rem;
  border-radius: 2px;
}

/* Real-time fleet pulse: one ball per incoming chkin, traveling left to
   right across the entire diagram. Background color is set inline by
   colorForDevice() so each device has a stable hue. Element is removed
   from the DOM ~1.6s after creation by the JS that spawned it. */
.tick-pulse-ball {
  position: absolute;
  top: 50%;
  left: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: 2;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.6),
              0 0 12px 2px currentColor;
  /* Direction is set by the .reverse modifier — see below. Default is
     left-to-right (Device -> Events) for device-originated events.
     Smooth gentle ease, no per-stage plateaus. */
  animation: tickPulseTravelLR 4s cubic-bezier(.4,.0,.2,1) forwards;
}
.tick-pulse-ball.reverse {
  /* Right-to-left for server-/operator-originated events (cmd_queue,
     firmware_target, etc.) — visualizes the command traveling toward
     the device. */
  animation-name: tickPulseTravelRL;
}
@keyframes tickPulseTravelLR {
  0%   { left: 0%;   opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  8%   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  92%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { left: 100%; opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}
@keyframes tickPulseTravelRL {
  0%   { left: 100%; opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  8%   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  92%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { left: 0%;   opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}

/* Parked ball: travels Events -> Backend (right edge to ~50%) then
   hangs out at the Backend node with a gentle pulse, indicating the
   queued cmd is sitting on the server waiting for the device to chkin
   and pick it up. Indefinite loop; JS removes the element when the
   matching cmd_result arrives (or queue_clear fades it). */
.tick-pulse-ball.park {
  animation: tickPulseTravelRLPark 1.6s cubic-bezier(.4,.0,.2,1) forwards,
             tickPulsePark 1.4s ease-in-out 1.6s infinite;
}
@keyframes tickPulseTravelRLPark {
  0%   { left: 100%; opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
  10%  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  100% { left: 50%;  opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes tickPulsePark {
  0%, 100% { transform: translate(-50%, -50%) scale(1);   opacity: 1; }
  50%      { transform: translate(-50%, -50%) scale(1.4); opacity: 0.65; }
}

/* Finish the parked journey: take the ball from its parked position
   (50%) down to the Device node (0%) and fade out. JS adds the
   .complete class when cmd_result arrives for the parked cmd_id. */
.tick-pulse-ball.park.complete {
  /* Cancel the parked keyframes by overriding the animation list. */
  animation: tickPulseParkComplete 1.4s cubic-bezier(.4,.0,.2,1) forwards;
}
@keyframes tickPulseParkComplete {
  0%   { left: 50%; opacity: 1; transform: translate(-50%, -50%) scale(1); }
  90%  { opacity: 1; }
  100% { left: 0%;  opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
}

/* queue_clear cancels parked balls — they retreat back to the Events
   node, visiting Handler on the way (50% -> 75% -> 100%), visually
   undoing the cmd_queue. No separate ball is spawned for the
   queue_clear event itself; the retreating balls ARE the animation.
   The brief slow-down at 75% makes the Handler stop legible. */
.tick-pulse-ball.park.canceled {
  animation: tickPulseParkCanceled 1.6s ease-in-out forwards;
}
@keyframes tickPulseParkCanceled {
  0%   { left: 50%;  opacity: 1; transform: translate(-50%, -50%) scale(1); }
  45%  { left: 75%;  opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
  55%  { left: 75%;  opacity: 1; transform: translate(-50%, -50%) scale(1); }
  90%  { opacity: 1; }
  100% { left: 100%; opacity: 0; transform: translate(-50%, -50%) scale(0.6); }
}

/* Device-node sublabel transition when flashDeviceLabel() swaps in the
   chkin-ing device_id. Keeps the same monospace metrics so the diagram
   doesn't reflow. */
.tick-node-green .tick-node-sub { transition: color 200ms; }

@media (max-width: 700px) {
  .tick-device-row {
    grid-template-columns: 1fr;
    gap: 0;
  }
  .tick-device-node {
    flex-direction: row;
    justify-content: flex-start;
    gap: 0.6rem;
    padding: 0.4rem 0.5rem;
  }
  .tick-device-link {
    width: 2px;
    height: 18px;
    margin: 0.15rem 0 0.15rem 1.1rem;
  }
}

/* Phone screens: hide the fleet-status diagram entirely. The diagram is
   horizontal-wide and not particularly useful on narrow viewports — the
   devices list + events panel below have everything operators need on
   a phone. Threshold is below the tablet breakpoint above so tablets
   keep the stacked layout. */
@media (max-width: 640px) {
  #fleet-status-section { display: none; }
}

/* ============================================================
   AUTH — login card + header logout
   ============================================================ */
.studio-header-right {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.75rem;
}
.studio-header-right[hidden] { display: none; }
/* Metrics button matches the session button's compact styling -
 * keeps the auth bar visually consistent (small btn + small btn).
 * The operator name no longer renders in the auth bar; it lives
 * inside the session popover (first row). */
.auth-metrics-btn {
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
}

/* Session-info popover. Anchored to the "session" button in the
 * auth bar; toggles open on click, closes on outside-click or Esc.
 * Shows token age, last sign-in, and TTL remaining derived from
 * STATE.session (populated by HELLO_ACK). */
.auth-session-wrap {
  position: relative;
}
.auth-session-btn {
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
}
.auth-session-pop {
  position: absolute;
  top: calc(100% + 0.4rem);
  right: 0;
  min-width: 240px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 4px;
  box-shadow: var(--shadow);
  padding: 0.6rem 0.7rem;
  z-index: 200;
}
.auth-session-pop[hidden] { display: none; }
.auth-session-grid {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.78rem;
  margin-bottom: 0.6rem;
}
.auth-session-row {
  display: flex;
  justify-content: space-between;
  gap: 0.8rem;
}
.auth-session-key { color: var(--ink-mute); }
.auth-session-val { color: var(--ink); }
.auth-session-action {
  width: 100%;
  font-size: 0.78rem;
}

/* Change-passphrase modal. Reuses .modal-backdrop / .modal-card from
 * the event-detail modal; body is a vertical form. */
.change-pass-card {
  max-width: 380px;
  width: 90vw;
}
#change-pass-form {
  padding: 0.4rem 1rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
#change-pass-form .login-label { margin-top: 0.4rem; }
.change-pass-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 0.8rem;
}

/* Metrics modal — reuses .modal-backdrop / .modal-card from the
   event-detail modal but renders the JSON response as a small grid
   of cards instead of raw stringified JSON. */
.metrics-card {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0.75rem;
  margin-top: 0.5rem;
}
.metric-tile {
  background: var(--panel);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.65rem 0.75rem;
}
.metric-tile-label {
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-transform: lowercase;
  letter-spacing: 0.02em;
}
.metric-tile-value {
  font-size: 1.25rem;
  font-weight: 600;
  color: var(--ink);
  margin-top: 0.2rem;
}
.metric-section-title {
  font-size: 0.75rem;
  color: var(--ink-soft);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 1rem 0 0.25rem;
}
.metric-section-title:first-child { margin-top: 0; }
.metric-refresh {
  font-size: 0.7rem;
  color: var(--ink-soft);
  margin-top: 0.5rem;
}

.login-shell {
  max-width: 460px;
  margin: 4rem auto;
  padding: 0 1.5rem;
}
.login-shell[hidden] { display: none; }

.login-card {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1.5rem;
  box-shadow: var(--shadow);
}
.login-title {
  font-size: 1.1rem;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 0.5rem;
}
.login-label {
  font-size: 0.78rem;
  color: var(--ink-mute);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-top: 0.4rem;
}
.login-card input {
  font-family: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.95rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--paper);
  color: var(--ink);
}
.login-card input:focus {
  outline: none;
  border-color: var(--teal);
  background: var(--surface);
}
.login-hint {
  font-size: 0.78rem;
  color: var(--ink-mute);
  margin-top: 0.4rem;
}
.login-hint code {
  background: var(--paper-2);
  padding: 0.05rem 0.35rem;
  border-radius: 3px;
}
.login-card .btn.primary { margin-top: 0.8rem; }
.login-status {
  min-height: 1.2em;
  font-size: 0.85rem;
}
.login-status[data-kind="fail"] { color: var(--crimson); }
.login-status[data-kind="ok"]   { color: var(--teal-deep); }
.login-status[data-kind="info"] { color: var(--ink-mute); }

/* Passphrase strength hint (under the passphrase field on first-login).
 * Same data-kind values as login-status so the colors stay consistent
 * across the form. */
.login-pass-strength {
  min-height: 1.1em;
  font-size: 0.72rem;
  margin-top: 0.2rem;
  color: var(--ink-mute);
}
.login-pass-strength[data-kind="fail"] { color: var(--crimson); }
.login-pass-strength[data-kind="ok"]   { color: var(--teal-deep); }

/* "signed in as op-X (last unlock 4h ago)" header on the resume-login
 * variant. Reuses login-hint sizing/color but with a tiny bit more
 * vertical padding so it reads as a section header. */
.login-resume-greeting {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-bottom: 0.6rem;
}

/* "forget this device" anchor inside the resume-login hint line. */
.login-hint a {
  color: var(--teal-deep);
  text-decoration: underline;
  cursor: pointer;
}
.login-hint a:hover { color: var(--crimson); }

/* ============================================================
   EVENT DETAIL MODAL — click an event row to see the raw JSON
   ============================================================ */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(15, 23, 42, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 50;
  padding: 1rem;
}
.modal-backdrop[hidden] { display: none; }

.modal-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 10px 32px rgba(15, 23, 42, .18);
  max-width: 720px;
  width: 100%;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
}
.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line);
}
.modal-title {
  font-size: 0.85rem;
  color: var(--ink);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding-right: 1rem;
}
.modal-body {
  margin: 0;
  padding: 1rem;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--ink);
  background: var(--paper);
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-all;
  flex: 1 1 auto;
  border-bottom-left-radius: 8px;
  border-bottom-right-radius: 8px;
}

/* Give the events toolbar a touch more room when both filter and
   backfill selects are visible side by side. */
.detail-toolbar { display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.autoscroll-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  cursor: pointer;
  user-select: none;
}
.autoscroll-toggle input[type="checkbox"] {
  margin: 0;
  accent-color: var(--teal);
  cursor: pointer;
}

/* ============================================================
   DEVICE LIFECYCLE — pending and blocked rows
   ============================================================ */

/* Pending row: yellow left border + warm tint. Sorted to the top by
   _deviceSortKey so it can't be missed. The circle stays full-color
   (the device_id color hash still applies). */
.device-row.pending {
  border-left: 3px solid var(--gold);
  background: #fff8e5;
}
.device-row.pending:hover { background: #fff2cc; }

/* Blocked row: greyed out, sorted to the bottom. */
.device-row.blocked {
  border-left: 3px solid var(--ink-mute);
  background: var(--paper-2);
  opacity: 0.7;
}
.device-row.blocked:hover { background: var(--line); }

/* State pill in the status column. Replaces the chkin-age dot+text
   for non-active devices so the lifecycle status is the dominant signal. */
.device-state-pill {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  padding: 0.15rem 0.45rem;
  border-radius: 3px;
  text-transform: uppercase;
}
.device-state-pill.pending {
  background: var(--gold);
  color: white;
}
.device-state-pill.blocked {
  background: var(--ink-mute);
  color: white;
}

/* "catching up" pill rendered next to the status dot when the server
 * has detected a reading_drain_summary{phase:start} (lag-based catch-
 * up detection from the readings flash partition). Lower-key visual
 * weight than the state pills since catch-up is informational, not a
 * fault. Teal hue to differentiate from gold (pending) and red (error). */
.device-drain-pill {
  font-family: "JetBrains Mono", monospace;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
  margin-left: 0.4rem;
  background: var(--teal-bg);
  color: var(--teal-deep);
  cursor: help;
}

/* Label editor block inside the state modal — single-line input + save
   button for setting / clearing the human-friendly device label. */
.state-label-editor {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--line);
}
.state-label-editor-label {
  font-size: 0.78rem;
  color: var(--ink-mute);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  min-width: 3.5em;
}
.state-label-editor-input {
  flex: 1;
  font-family: "Inter", sans-serif;
  font-size: 0.88rem;
  padding: 0.35rem 0.55rem;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  outline: none;
  background: var(--surface);
  color: var(--ink);
  transition: border-color 100ms;
}
.state-label-editor-input:focus { border-color: var(--teal); }
.state-label-save-btn {
  font-size: 0.78rem;
  padding: 0.35rem 0.8rem;
}

/* Approve/block action bar inside the state modal — only present for
   pending and blocked devices. */
.state-action-bar {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
.state-action-bar[data-kind="pending"] {
  background: #fff8e5;
  border-left: 3px solid var(--gold);
}
.state-action-bar[data-kind="blocked"] {
  background: var(--paper-2);
  border-left: 3px solid var(--ink-mute);
}
.state-action-banner {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--ink);
  text-transform: uppercase;
}
.state-action-bar[data-kind="pending"] .state-action-banner { color: var(--gold); }
.state-action-bar[data-kind="blocked"] .state-action-banner { color: var(--ink-mute); }
.state-action-hint {
  font-size: 0.78rem;
  color: var(--ink-soft);
  line-height: 1.45;
}
.state-action-hint code {
  background: var(--paper);
  padding: 0.05rem 0.3rem;
  border-radius: 3px;
}
.state-action-buttons {
  display: flex;
  gap: 0.6rem;
  margin-top: 0.4rem;
}

/* errlog summary chip in the events panel. Tinted by severity of the
 * latest entry. Hover surfaces the full breadcrumb (head/delta/code).
 * Tied to events with op="errlog_summary". */
.errlog-chip {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: 3px;
  font-size: 0.78rem;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.06);
  vertical-align: middle;
}
.ev-sev-info  { color: #88c0d0; }
.ev-sev-warn  { color: #ebcb8b; }
.ev-sev-error { color: #d08770; }
.ev-sev-fatal {
  color: #1b1f23;
  background: #bf616a;
}
