/**
 * cx-base — shared component primitives
 * =====================================
 * The shadcn/ui component grammar (radius scale, 1px hairline borders, the
 * muted-foreground text rank, focus ring, card structure) reimplemented in
 * plain CSS over the tokens in design-system.css.
 *
 * This frontend has no build step, so React — and therefore shadcn itself —
 * is not available here; the values are shadcn's, only the delivery differs.
 *
 * Extracted from connections.css once a SECOND module needed the same
 * primitives. Two copies of a button would have drifted on the first change
 * to either. Anything used by more than one module belongs here; anything
 * used by exactly one belongs in that module's stylesheet.
 *
 * Consumers scope themselves with `.cx-root` on their container.
 */


/* ============================================================
   1. LOCAL TOKENS
   Scoped to the module so nothing here can leak into other views.
   ============================================================ */
/* The token block is on :root, NOT on .cx-root, and that placement is
   load-bearing. The kebab menu is appended to `document.body` so it can
   escape the rail's overflow clipping — which puts it OUTSIDE .cx-root,
   where none of these resolved. Measured: `background:
   var(--cx-surface-2)` fell back to `rgba(0, 0, 0, 0)` and
   `border-radius: var(--cx-r-md)` to `0px`, so the menu rendered as bare
   floating text with the rail showing straight through it and no hover
   feedback (`--cx-muted` was unset too). Its buttons were still
   clickable, which is what made it read as "opens but is broken".

   An unresolved custom property is silently the property's initial
   value — transparent, 0, currentColor — so this failure mode is
   invisible to every source-level check and only appears on screen.
   Anything else portalled to the body inherits the tokens now.

   Layout stays on .cx-root below: a portalled menu must not become a
   flex column. */
:root {
  /* shadcn radius scale, mapped onto the existing ramp */
  --cx-r-sm: var(--r-sm);      /* badges, chips        */
  --cx-r-md: var(--r-md);      /* buttons, inputs      */
  --cx-r-lg: var(--r-lg);      /* cards, panels        */

  /* shadcn surface ranks */
  --cx-surface:   var(--bg-card);
  --cx-surface-2: var(--bg-elevated);
  --cx-muted:     rgba(255, 255, 255, 0.035);
  --cx-hairline:  var(--border);
  --cx-hairline-strong: var(--border-mid);

  /* shadcn muted-foreground: the single most load-bearing colour in the
     system. Everything that is not the answer to the user's question is
     rendered in it. */
  --cx-fg:        var(--text-1);
  --cx-fg-muted:  var(--text-3);
  --cx-fg-subtle: var(--text-4);

  --cx-ring: rgba(211, 158, 87, 0.55);

  /* Rail width. Named so the media queries below have one place to change. */
  --cx-rail-w: 312px;
}

.cx-root {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-height: 0;
}

/* Light theme.
   The dark values above are white alphas, which are invisible on a white
   surface — the filter-chip counters rendered as bare numbers with no pill
   until this block existed. Only the tokens whose polarity flips are
   redeclared; everything else already resolves through design-system.css. */
/* Scoped to the same element the tokens are declared on, for the same
   reason: `[data-theme="light"] .cx-root` never matched the portalled
   menu, so on the light theme it kept the DARK white-alpha hover. */
:root[data-theme="light"] {
  --cx-muted:           rgba(15, 23, 42, 0.045);
  --cx-hairline:        rgba(15, 23, 42, 0.10);
  --cx-hairline-strong: rgba(15, 23, 42, 0.16);
  --cx-fg-muted:        var(--text-3);
  --cx-fg-subtle:       #7c8ea1;
}
[data-theme="light"] .cx-skel {
  background: linear-gradient(
    90deg,
    rgba(15, 23, 42, 0.05) 25%,
    rgba(15, 23, 42, 0.10) 37%,
    rgba(15, 23, 42, 0.05) 63%
  );
  background-size: 400% 100%;
}
/* Module-specific light overrides live in that module's stylesheet, not here
   — see connections.css for the topology canvas and the log tail. */

/* ============================================================
   2. PRIMITIVES — shadcn component grammar
   ============================================================ */

/* -- visually-hidden label -------------------------------------------
   For controls whose visible affordance is an icon. The text stays in the
   accessibility tree; clip-path rather than display:none because a hidden
   element is not announced. */
.cx-root .sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* -- focus ring -------------------------------------------------------
   Never removed, only restyled. `:focus-visible` so a mouse click does
   not paint a ring but a Tab key does. */
.cx-root :focus-visible {
  outline: 2px solid var(--cx-ring);
  outline-offset: 2px;
  border-radius: var(--cx-r-sm);
}

/* -- button ----------------------------------------------------------- */
.cx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: 34px;
  padding: 0 12px;
  border-radius: var(--cx-r-md);
  border: 1px solid transparent;
  background: transparent;
  color: var(--cx-fg);
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 500;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
  transition:
    background-color var(--t-fast) var(--ease-out),
    border-color var(--t-fast) var(--ease-out),
    color var(--t-fast) var(--ease-out),
    transform var(--t-fast) var(--ease-out),
    box-shadow var(--t-fast) var(--ease-out);
}
.cx-btn svg { flex: 0 0 auto; }
.cx-btn:active { transform: translateY(1px); }
.cx-btn:disabled,
.cx-btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

.cx-btn-primary {
  background: var(--accent);
  color: var(--text-inverse);
  font-weight: 600;
}
.cx-btn-primary:hover { background: var(--accent-light); }

.cx-btn-outline {
  border-color: var(--cx-hairline-strong);
  color: var(--text-2);
}
.cx-btn-outline:hover {
  background: var(--cx-muted);
  border-color: var(--border-strong);
  color: var(--cx-fg);
}

.cx-btn-ghost { color: var(--text-2); }
.cx-btn-ghost:hover { background: var(--cx-muted); color: var(--cx-fg); }

.cx-btn-danger {
  border-color: var(--danger-border);
  color: var(--danger);
}
.cx-btn-danger:hover { background: var(--danger-bg); border-color: var(--danger); }

.cx-btn-sm { height: 28px; padding: 0 9px; font-size: 12px; }
.cx-btn-icon { width: 34px; padding: 0; }
.cx-btn-icon.cx-btn-sm { width: 28px; }

/* -- input ------------------------------------------------------------ */
.cx-input {
  width: 100%;
  height: 34px;
  padding: 0 10px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline);
  background: var(--bg-input);
  color: var(--cx-fg);
  font-family: var(--font-ui);
  font-size: 13px;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out);
}
.cx-input::placeholder { color: var(--cx-fg-subtle); }
.cx-input:hover { border-color: var(--cx-hairline-strong); }
.cx-input:focus {
  outline: none;
  border-color: var(--border-accent);
  box-shadow: 0 0 0 3px rgba(211, 158, 87, 0.12);
}

.cx-input-icon { position: relative; }
.cx-input-icon > svg {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--cx-fg-subtle);
  pointer-events: none;
}
.cx-input-icon > .cx-input { padding-left: 32px; }

.cx-select {
  height: 30px;
  padding: 0 26px 0 10px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline);
  background: var(--bg-input);
  color: var(--text-2);
  font-family: var(--font-ui);
  font-size: 12px;
  cursor: pointer;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%236e8fa8' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
  transition: border-color var(--t-fast) var(--ease-out);
}
.cx-select:hover { border-color: var(--cx-hairline-strong); }

/* -- badge ------------------------------------------------------------ */
.cx-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border-radius: var(--cx-r-sm);
  border: 1px solid transparent;
  font-size: 11px;
  font-weight: 500;
  line-height: 16px;
  letter-spacing: 0.01em;
  white-space: nowrap;
}
.cx-badge-success { background: var(--success-bg); border-color: var(--success-border); color: var(--success); }
.cx-badge-accent  { background: var(--accent-dim); border-color: var(--border-accent);  color: var(--accent); }
.cx-badge-warning { background: var(--warning-bg); border-color: var(--warning-border); color: var(--warning); }
.cx-badge-danger  { background: var(--danger-bg);  border-color: var(--danger-border);  color: var(--danger); }
.cx-badge-info    { background: var(--info-bg);    border-color: var(--info-border);    color: var(--info); }
.cx-badge-muted   { background: var(--cx-muted);   border-color: var(--cx-hairline);    color: var(--cx-fg-muted); }

/* A 6px status dot. Colour alone never carries the meaning — every dot
   sits beside a text label, per the accessibility rule. */
.cx-dot {
  width: 6px; height: 6px;
  border-radius: var(--r-full);
  flex: 0 0 auto;
  background: var(--cx-fg-subtle);
}
.cx-dot-success { background: var(--success); box-shadow: 0 0 0 3px var(--success-bg); }
.cx-dot-danger  { background: var(--danger);  box-shadow: 0 0 0 3px var(--danger-bg); }
.cx-dot-warning { background: var(--warning); box-shadow: 0 0 0 3px var(--warning-bg); }
.cx-dot-idle    { background: var(--text-4); }

/* -- card -------------------------------------------------------------
   Horizontal padding exceeds vertical. Equal padding on all four sides
   reads as optically uneven — the ratio here is ~1.2:1. */
/* Same card grammar as Vantage and the HA fabric: a 2px rail across the
   top edge. `data-status` is optional — a card that has no state to report
   keeps the neutral rail, so the three surfaces read as one product
   instead of three. */
.cx-card {
  background: var(--cx-surface);
  border: 1px solid var(--cx-hairline);
  border-top: 2px solid var(--cx-hairline-strong, var(--border-mid));
  border-radius: var(--cx-r-lg);
  overflow: hidden;
}
.cx-card[data-status="ok"]   { border-top-color: var(--success); }
.cx-card[data-status="warn"] { border-top-color: var(--warning); }
.cx-card[data-status="bad"]  { border-top-color: var(--danger); }
.cx-card[data-status="none"] { border-top-color: var(--border-mid); }
.cx-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: 14px 18px 12px;
}
.cx-card-title {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--cx-fg-muted);
  margin: 0;
}
.cx-card-body { padding: 0 18px 16px; }
.cx-card-body-flush { padding: 0; }

/* -- switch (auto-refresh) -------------------------------------------- */
.cx-switch {
  position: relative;
  width: 34px;
  height: 19px;
  border-radius: var(--r-full);
  border: 1px solid var(--cx-hairline-strong);
  background: var(--bg-input);
  cursor: pointer;
  padding: 0;
  flex: 0 0 auto;
  transition: background-color var(--t-base) var(--ease-out),
              border-color var(--t-base) var(--ease-out);
}
.cx-switch::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 2px;
  width: 13px; height: 13px;
  border-radius: var(--r-full);
  background: var(--text-3);
  transform: translateY(-50%);
  transition: transform var(--t-base) var(--ease-spring),
              background-color var(--t-base) var(--ease-out);
}
.cx-switch[aria-checked="true"] {
  background: var(--accent);
  border-color: var(--accent);
}
.cx-switch[aria-checked="true"]::after {
  transform: translateY(-50%) translateX(15px);
  background: var(--text-inverse);
}

/* -- skeleton ---------------------------------------------------------
   First paint only. Not layered onto every refresh — a skeleton that
   reappears on each poll reads as the page breaking. */
.cx-skel {
  background: linear-gradient(
    90deg,
    var(--cx-muted) 25%,
    rgba(255, 255, 255, 0.07) 37%,
    var(--cx-muted) 63%
  );
  background-size: 400% 100%;
  border-radius: var(--cx-r-sm);
  animation: cx-shimmer 1.4s ease infinite;
}
@keyframes cx-shimmer {
  0%   { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* -- stat rows ---------------------------------------------------------
   A label/value pair. Used by every module that reports a metric, which is
   why it lives here rather than in whichever stylesheet happened to need it
   first. `width: 100%` is explicit because these are frequently dropped into
   a flex item, where a block child does not otherwise fill the line. */
.cx-stat-rows { display: flex; flex-direction: column; gap: 9px; width: 100%; }
.cx-stat-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: 12.5px;
  min-height: 18px;
  width: 100%;
}
/* The label yields, the number never does. A wrapped label is legible; a
   wrapped number reads as two numbers. */
.cx-stat-row > span:first-child {
  color: var(--cx-fg-muted);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cx-stat-row > span:last-child {
  font-weight: 600;
  color: var(--cx-fg);
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
  flex: 0 0 auto;
}

/* -- semantic value tones ---------------------------------------------- */
.cx-ok   { color: var(--success) !important; }
.cx-warn { color: var(--warning) !important; }
.cx-bad  { color: var(--danger)  !important; }
.cx-na   { color: var(--cx-fg-subtle) !important; font-weight: 500 !important; }

/* -- meter -------------------------------------------------------------- */
.cx-meter {
  height: 4px;
  border-radius: var(--r-full);
  background: var(--cx-muted);
  overflow: hidden;
  margin-top: 4px;
  width: 100%;
}
.cx-meter-fill {
  height: 100%;
  border-radius: inherit;
  transition: width var(--t-slow) var(--ease-out), background-color var(--t-base) var(--ease-out);
}

/* -- scrollbar --------------------------------------------------------- */
.cx-scroll { scrollbar-width: thin; scrollbar-color: var(--border-mid) transparent; }
.cx-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
.cx-scroll::-webkit-scrollbar-track { background: transparent; }
.cx-scroll::-webkit-scrollbar-thumb {
  background: var(--border-mid);
  border-radius: var(--r-full);
  border: 2px solid transparent;
  background-clip: content-box;
}
.cx-scroll::-webkit-scrollbar-thumb:hover { background: var(--border-strong); background-clip: content-box; }


/* ============================================================
   8. CHARTS
   ============================================================ */
.cx-chart { width: 100%; height: 220px; }
.cx-chart-tall { height: 260px; }


/* ============================================================
   9. STATES — empty, error, loading
   ============================================================ */
.cx-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 48px 24px;
  text-align: center;
  color: var(--cx-fg-muted);
}
.cx-empty svg { color: var(--cx-fg-subtle); }
.cx-empty-title { font-size: 14px; font-weight: 600; color: var(--text-2); }
.cx-empty-sub { font-size: 12.5px; max-width: 46ch; line-height: 1.6; }

/* An empty list and a failed list are different facts and must never share
   a rendering. This is the failed one. */
.cx-error {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--danger-border);
  background: var(--danger-bg);
  color: var(--text-2);
  font-size: 12.5px;
  line-height: 1.55;
}
.cx-error svg { color: var(--danger); flex: 0 0 auto; margin-top: 1px; }
.cx-error-title { font-weight: 600; color: var(--danger); display: block; margin-bottom: 2px; }

.cx-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 9px 12px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline);
  background: var(--cx-muted);
  color: var(--cx-fg-muted);
  font-size: 12px;
  line-height: 1.55;
}
.cx-note svg { flex: 0 0 auto; margin-top: 1px; }

.cx-spin { animation: cx-rotate 900ms linear infinite; }
@keyframes cx-rotate { to { transform: rotate(360deg); } }

/* -- background refresh -------------------------------------------------
   A poll is not news. It gets the refresh glyph turning and nothing else:
   no skeleton over data that is already correct, no dimming, no reflow.
   The class is applied on a delay by the module, so a fast response never
   shows it at all — the only refresh the operator notices is a slow one. */
.is-refreshing [data-cx-act="refresh-now"] svg,
.is-refreshing [data-dash-act="refresh"] svg {
  animation: cx-rotate 900ms linear infinite;
}


/* ============================================================
   11. MENU (kebab)
   ============================================================ */
.cx-menu {
  position: fixed;
  z-index: 1200;
  min-width: 186px;
  padding: 4px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline-strong);
  background: var(--cx-surface-2);
  box-shadow: var(--shadow-lg);
  animation: cx-menu-in 120ms var(--ease-out) both;
}
@keyframes cx-menu-in {
  from { opacity: 0; transform: scale(0.97) translateY(-2px); }
  to   { opacity: 1; transform: none; }
}
.cx-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: var(--cx-r-sm);
  background: transparent;
  color: var(--text-2);
  font-family: var(--font-ui);
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.cx-menu-item:hover { background: var(--cx-muted); color: var(--cx-fg); }
.cx-menu-item-danger { color: var(--danger); }
.cx-menu-item-danger:hover { background: var(--danger-bg); color: var(--danger); }
.cx-menu-sep { height: 1px; margin: 4px 2px; background: var(--cx-hairline); }


/* ============================================================
   13. MOTION / CONTRAST PREFERENCES
   ============================================================ */
@media (prefers-reduced-motion: reduce) {
  .cx-root *,
  .cx-root *::before,
  .cx-root *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .cx-edge-flow { stroke-dasharray: none; }
  .cx-rail-list { scroll-behavior: auto; }
}
@media (prefers-contrast: more) {
  /* :root, not .cx-root — an operator who asked for more contrast must
     get it on the portalled menu too, and that is the surface where a
     missing hairline is hardest to see. */
  :root {
    --cx-hairline: rgba(255, 255, 255, 0.22);
    --cx-hairline-strong: rgba(255, 255, 255, 0.38);
    --cx-fg-muted: #c3d5e4;
    --cx-fg-subtle: #9ab0c2;
  }
}

/* ==========================================================================
   cxzoom — the shared zoom control (lib/cx-dom.js `cxzoom`)
   ==========================================================================
   Connections > Architecture had +/-/fit and nothing else did, so the HA
   fabric, Cortex and the ERD grew with the window with no way to shrink
   just that card. Styled once here so all four read as the same control.

   The percentage readout is a BUTTON, not a label: it is the reset, and it
   is also the only thing that tells a keyboard or screen-reader user what
   the +/- did. */
.cx-zoom-tools {
  position: absolute;
  right: 12px;
  top: 12px;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 3px;
  border-radius: 8px;
  background: var(--bg-card, #fff);
  border: 1px solid var(--border);
  box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, .08));
}
.cx-zoom-btn {
  min-width: 30px;
  height: 26px;
  padding: 0 4px;
  font: inherit;
  font-size: 13px;
  line-height: 1;
  color: var(--text-2);
  background: transparent;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
}
.cx-zoom-btn:hover { background: var(--bg-elevated); color: var(--text-1); }
.cx-zoom-btn:active { background: var(--accent-dim); color: var(--accent); }
.cx-zoom-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* The readout carries the current value, so it is the widest of the three
   and must not reflow the group as the number changes width (100% -> 90%). */
.cx-zoom-level {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  border-radius: 0;
}

/* The stage is what gets the transform. Its viewport clips, so a zoomed-in
   card cannot spill over the page and start a horizontal scrollbar. */
.cx-zoom-viewport {
  position: relative;
  overflow: auto;
  overscroll-behavior: contain;
}
.cx-zoom-stage {
  transform-origin: 0 0;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .cx-zoom-stage { transition: none; }
}

/* Inline variant: sits in a toolbar row rather than floating over a canvas.
   Cortex puts it beside Clear, where the other view controls already are. */
.cx-zoom-tools.cx-zoom-inline {
  position: static;
  flex-direction: row;
  align-items: center;
  gap: 0;
  margin-left: 6px;
}
.cx-zoom-inline .cx-zoom-btn { height: 24px; min-width: 26px; }
.cx-zoom-inline .cx-zoom-level {
  border: 0;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
  min-width: 42px;
}

/* Pan affordance. `grab` says the surface is draggable before the operator
   tries it; `grabbing` confirms the drag took. Transform-mode viewports
   only — a reflowed text surface is not pannable.

   Selection is disabled DURING the drag rather than always: a topology
   card's host:port is worth being able to select when you are not
   dragging. */
.cx-zoom-viewport { cursor: grab; }
.cx-zoom-viewport.is-panning { cursor: grabbing; }
.cx-zoom-viewport.is-panning,
.cx-zoom-viewport.is-panning * { user-select: none; }

/* The stage is the only thing that moves. Transform and opacity only, and
   no transition on the drag path — a tweened pan lags the pointer. */
@media (prefers-reduced-motion: reduce) {
  .cx-zoom-stage { transition: none; }
}
