/* ctos-ui · entry point. Link this file (or dist/ctos.css, the same thing in one request).
   Every rule lives in a ctos.* cascade layer, so any unlayered app CSS wins without !important. */

@layer ctos.reset, ctos.tokens, ctos.base, ctos.materials, ctos.components, ctos.adapters, ctos.utilities;

/* Self-hosted so a LAN without internet still gets the faces. Paths are relative to src/css;
   the build rewrites them for dist/. Licences: fonts/OFL-*.txt */

@font-face {
  font-family: "Departure Mono";
  src: url("../fonts/departure-mono-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-ext-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-ext-500-normal.woff2") format("woff2");
  font-weight: 500;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-600-normal.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Barlow Semi Condensed";
  src: url("../fonts/barlow-semi-condensed-latin-ext-600-normal.woff2") format("woff2");
  font-weight: 600;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@layer ctos.tokens {
/* Tokens in three tiers: primitives (raw ramps), semantic (what a colour means),
   component (one knob per component). Apps override the semantic tier, never primitives. */

:root {
  color-scheme: dark;

  /* ---- primitives: blue-black ink ramp, hue ~255 ---- */
  --ink-0: oklch(0.12 0.022 258);
  --ink-1: oklch(0.15 0.026 258);
  --ink-2: oklch(0.18 0.031 257);
  --ink-3: oklch(0.22 0.037 257);
  --ink-4: oklch(0.28 0.046 256);
  --ink-5: oklch(0.37 0.06 255);
  --ink-6: oklch(0.5 0.075 253);
  --ink-7: oklch(0.68 0.06 250);
  --ink-8: oklch(0.78 0.045 248);
  --ink-9: oklch(0.9 0.026 245);
  --ink-10: oklch(0.97 0.012 240);

  --scan-3: oklch(0.5 0.19 262);
  --scan-5: oklch(0.7 0.16 248);
  --scan-7: oklch(0.84 0.11 232);
  --hot-white: oklch(0.985 0.008 95); /* near-white: every network and status colour has 15× its chroma */

  --amber-5: oklch(0.8 0.15 75);
  --red-5: oklch(0.66 0.21 24);
  --red-7: oklch(0.78 0.13 22);

  /* ---- semantic ---- */
  --surface-void: var(--ink-0);
  --surface-floor: var(--ink-1);
  --surface-panel: var(--ink-2);
  --surface-raised: var(--ink-3);
  --surface-veil: oklch(0.12 0.022 258 / 0.86);

  --wire-faint: var(--ink-4);
  --wire: var(--ink-5);
  --wire-strong: var(--ink-6); /* ≥3:1 on void: use for borders that carry meaning */
  --wire-hot: var(--scan-5);

  --text: var(--ink-9);
  --text-hot: var(--ink-10);
  --text-dim: var(--ink-8);
  --text-faint: var(--ink-7); /* still ≥4.5:1 on panel */
  --text-on-signal: var(--ink-0);

  --signal: var(--scan-5);       /* lit, connected, primary action */
  --signal-hot: var(--scan-7);   /* hover, focus, active */
  --signal-deep: var(--scan-3);
  --lock: var(--scan-7);         /* lock-on brackets */
  --route: var(--hot-white);     /* reserved: the active route, nothing else */
  --warn: var(--amber-5);
  --crit: var(--red-5);
  --crit-text: var(--red-7);

  /* Six network slots: same lightness, distinct hue. Each also owns a dash pattern
     so networks stay distinct without colour (see --net-dash-*). */
  --net-0: oklch(0.72 0.14 250);
  --net-1: oklch(0.76 0.12 200);
  --net-2: oklch(0.72 0.15 295);
  --net-3: oklch(0.76 0.13 160);
  --net-4: oklch(0.72 0.15 340);
  --net-5: oklch(0.8 0.13 115);
  --net-dash-0: none;
  --net-dash-1: 8 4;
  --net-dash-2: 2 3;
  --net-dash-3: 12 3 2 3;
  --net-dash-4: 4 4;
  --net-dash-5: 16 3 2 3 2 3;

  --grid-dot: oklch(0.42 0.06 255 / 0.42);
  --grid-line: oklch(0.42 0.06 255 / 0.12);
  --scanline: oklch(0 0 0 / 0.22);
  --shadow: 0 14px 36px -10px oklch(0.05 0.02 258 / 0.85), 0 2px 6px oklch(0.05 0.02 258 / 0.6);

  /* ---- geometry: everything is a multiple of one voxel ---- */
  --vx: 4px;
  --s-1: 4px;
  --s-2: 8px;
  --s-3: 12px;
  --s-4: 16px;
  --s-5: 24px;
  --s-6: 32px;
  --s-7: 48px;
  --s-8: 64px;
  --cut: 8px;          /* 45° corner cut */
  --hit: 36px;         /* control height */
  --hit-coarse: 44px;

  /* ---- type ---- */
  --font-ui: "Barlow Semi Condensed", "Bahnschrift", "DIN Alternate", "Roboto Condensed", system-ui, sans-serif;
  --font-data: "Departure Mono", ui-monospace, "Cascadia Mono", "Consolas", monospace;
  /* Departure Mono is drawn on an 11px grid: sizes stay on multiples of 11. */
  --t-data-s: 11px;
  --t-data-l: 22px;
  --t-data-xl: 33px;
  --t-data-xxl: 44px;
  --t-12: 12.5px;
  --t-13: 13.5px;
  --t-15: 15px;
  --t-17: 17px;
  --t-20: 20px;
  --t-24: 24px;
  --t-30: 30px;
  --track-label: 0.08em;

  /* ---- motion ---- */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --snap: steps(3, end);   /* pixel-stepped motion for lock-on and reveals */
  --t-fast: 120ms;
  --t-med: 220ms;
  --t-glitch: 240ms;

  /* ---- component knobs ---- */
  --cx-lock-arm: 8px;
  --cx-lock-weight: 2px;
  --cx-lock-gap: 4px;
  --cx-meter-seg: 3px;
  --cx-meter-gap: 1px;
}

@media (pointer: coarse) {
  :root { --hit: var(--hit-coarse); }
}
}
/* Reset and the browser surfaces nobody draws: selection, caret, scrollbars, focus, numerals. */

@layer ctos.reset {
  *, *::before, *::after { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
  body { margin: 0; }
  h1, h2, h3, h4, p, ul, ol, dl, dd, figure, blockquote { margin: 0; }
  ul[class], ol[class] { padding: 0; list-style: none; }
  img, svg, canvas, video { display: block; max-width: 100%; }
  input, button, textarea, select { font: inherit; color: inherit; }
  button { cursor: pointer; }
  [hidden] { display: none !important; }
}

@layer ctos.base {
  html {
    background: var(--surface-void);
    color: var(--text);
    font: 400 var(--t-15) / 1.45 var(--font-ui);
    font-synthesis: none;
  }
  /* scrollbar-color would switch off the ::-webkit-scrollbar rules below in Chrome. */
  @supports not selector(::-webkit-scrollbar) {
    html { scrollbar-color: var(--wire) var(--surface-void); }
  }

  body { min-height: 100dvh; background: var(--surface-void); }

  h1, h2, h3, h4 { color: var(--text-hot); font-weight: 600; line-height: 1.15; text-wrap: balance; }
  h1 { font-size: var(--t-30); }
  h2 { font-size: var(--t-24); }
  h3 { font-size: var(--t-17); }
  h4 { font-size: var(--t-15); }
  p { text-wrap: pretty; }

  a { color: var(--signal-hot); text-decoration-color: color-mix(in oklch, var(--signal) 55%, transparent); text-underline-offset: 3px; text-decoration-thickness: 1px; }
  a:hover { color: var(--text-hot); text-decoration-color: var(--signal-hot); }

  code, kbd, samp, pre { font-family: var(--font-data); font-size: var(--t-data-s); }
  table, output, time, data { font-variant-numeric: tabular-nums; }

  ::selection { background: var(--signal); color: var(--text-on-signal); }
  input, textarea { caret-color: var(--signal-hot); }
  ::placeholder { color: var(--text-faint); opacity: 1; }

  /* Fallback ring for anything without lock-on brackets (see materials.css). */
  :focus-visible { outline: 1px solid var(--lock); outline-offset: 2px; }

  ::-webkit-scrollbar { width: 10px; height: 10px; }
  ::-webkit-scrollbar-track { background: var(--surface-void); }
  ::-webkit-scrollbar-thumb { background: var(--wire); border: 3px solid var(--surface-void); }
  ::-webkit-scrollbar-thumb:hover { background: var(--wire-strong); }
  ::-webkit-scrollbar-corner { background: var(--surface-void); }

  hr { height: 1px; margin: var(--s-5) 0; border: 0; background: var(--wire-faint); }
}
@layer ctos.materials {
/* The world's materials: voxel floor, scanlines, cut corners, lock-on brackets,
   glitch tear, crit inversion, scan sweep, pixel meter. Components compose these. */

/* ---- voxel floor: an 8px dot grid with a faint 64px bay grid over it ---- */
.cx-floor {
  background-color: var(--surface-void);
  background-image:
    linear-gradient(var(--grid-line) 1px, transparent 1px),
    linear-gradient(90deg, var(--grid-line) 1px, transparent 1px),
    radial-gradient(circle at 1px 1px, var(--grid-dot) 0.75px, transparent 1.25px);
  background-size: 64px 64px, 64px 64px, 8px 8px;
  background-position: -1px -1px, -1px -1px, 0 0;
}

/* A canvas from voxel-field.js sits behind content; content stays readable without it. */
.cx-field-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  --vx-lo: var(--wire);
  --vx-hi: var(--signal-hot);
  --vx-pulse: var(--route);
}

/* ---- scanlines: opt-in overlay, never over body copy ---- */
.cx-scanlines { position: relative; isolation: isolate; }
.cx-scanlines::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: repeating-linear-gradient(to bottom, transparent 0 2px, var(--scanline) 2px 3px);
  mix-blend-mode: multiply;
}

/* ---- 45° cut corner (top-right) ----
   corner-shape draws a real bevel, border included, without clipping children (so lock-on
   brackets near the corner survive). Browsers without it keep a square corner. */
@supports (corner-shape: bevel) {
  .cx-cut { border-top-right-radius: var(--cut); corner-shape: bevel; }
}

/* ---- lock-on brackets: the signature focus and selection state ----
   Eight one-colour gradients draw four corner brackets on ::after. They sit wide and
   transparent, then snap inward in three pixel steps. */
:is(.cx-btn, .cx-iconbtn, .cx-tab, .cx-field, .cx-check, .cx-switch, .cx-tag, .cx-lockable) {
  position: relative;

  &::after {
    --a: var(--cx-lock-arm);
    --w: var(--cx-lock-weight);
    content: "";
    position: absolute;
    inset: calc(-1 * var(--cx-lock-gap) - 6px);
    pointer-events: none;
    opacity: 0;
    background:
      linear-gradient(var(--lock) 0 0) 0 0 / var(--a) var(--w),
      linear-gradient(var(--lock) 0 0) 0 0 / var(--w) var(--a),
      linear-gradient(var(--lock) 0 0) 100% 0 / var(--a) var(--w),
      linear-gradient(var(--lock) 0 0) 100% 0 / var(--w) var(--a),
      linear-gradient(var(--lock) 0 0) 0 100% / var(--a) var(--w),
      linear-gradient(var(--lock) 0 0) 0 100% / var(--w) var(--a),
      linear-gradient(var(--lock) 0 0) 100% 100% / var(--a) var(--w),
      linear-gradient(var(--lock) 0 0) 100% 100% / var(--w) var(--a);
    background-repeat: no-repeat;
    transition: inset var(--t-fast) var(--snap), opacity var(--t-fast) var(--snap);
  }
  &:not(.cx-field):focus-visible { outline: none; }
  &:not(.cx-field):focus-visible::after,
  &.cx-field:has(:focus-visible)::after { inset: calc(-1 * var(--cx-lock-gap)); opacity: 1; }
}
.cx-field:focus-within :is(input, textarea, select) { outline: none; }

/* Selected (locked) things keep their brackets; unfocused they use the quieter signal blue. */
:is(.cx-tag, .cx-lockable, .cx-tab):is(.is-locked, [aria-selected="true"], [aria-current]:not([aria-current="false"])) {
  &::after { inset: calc(-1 * var(--cx-lock-gap)); opacity: 1; }
  &:not(:focus-visible)::after { --lock: var(--signal); }
}

/* ---- glitch tear: one-shot on state change. Add .cx-glitch, remove on animationend ---- */
@keyframes cx-glitch {
  0%   { clip-path: none; transform: translateX(0); }
  20%  { clip-path: inset(12% 0 58% 0); transform: translateX(-4px); }
  40%  { clip-path: inset(64% 0 8% 0); transform: translateX(4px); }
  60%  { clip-path: inset(30% 0 40% 0); transform: translateX(-2px); filter: hue-rotate(40deg); }
  80%  { clip-path: none; transform: translateX(1px); }
  100% { clip-path: none; transform: translateX(0); }
}
.cx-glitch { animation: cx-glitch var(--t-glitch) steps(5, end) 1; }

/* ---- crit inversion: the only full negative in the system. One frame, then held as an inverted outline ---- */
@keyframes cx-invert {
  0%, 30% { filter: invert(1) hue-rotate(180deg); }
  31%, 100% { filter: none; }
}
.cx-crit-flash { animation: cx-invert 200ms steps(1, end) 1; }

/* ---- scan sweep: loading. A lit band crosses the area top to bottom ---- */
@keyframes cx-sweep { from { transform: translateY(-100%); } to { transform: translateY(100%); } }
.cx-scan { position: relative; overflow: hidden; }
.cx-scan::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent 0 40%, color-mix(in oklch, var(--signal) 18%, transparent) 49%, color-mix(in oklch, var(--signal-hot) 55%, transparent) 50%, transparent 50.5%);
  animation: cx-sweep 1.8s linear infinite;
}

/* ---- route flow: dots marching along a route path ---- */
@keyframes cx-flow { to { stroke-dashoffset: -16; } }

/* ---- change persistence: lit until the operator acknowledges it ---- */
.cx-changed:not(.is-acked) {
  background-color: color-mix(in oklch, var(--route) 9%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--route) 70%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .cx-glitch, .cx-crit-flash { animation: none; }
  .cx-scan::before { animation: none; transform: translateY(0); background: linear-gradient(to bottom, transparent 0 49%, color-mix(in oklch, var(--signal-hot) 55%, transparent) 50%, transparent 50.5%); }
  :is(.cx-btn, .cx-iconbtn, .cx-tab, .cx-field, .cx-check, .cx-switch, .cx-tag, .cx-lockable)::after { transition: none; }
}
}
@layer ctos.components {
/* Buttons, icon buttons, fields, checks, switches. Square corners, 1px wire, labels in caps.
   Focus is the lock-on bracket from materials.css; no outline rings. */

/* ---- button ---- */
.cx-btn {
  --btn-wire: var(--wire-strong);
  --btn-ink: var(--text-hot);
  --btn-fill: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--hit);
  padding: 0 var(--s-4);
  border: 1px solid var(--btn-wire);
  border-radius: 0;
  background: var(--btn-fill);
  color: var(--btn-ink);
  font: 600 var(--t-13) / 1 var(--font-ui);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  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);
}
.cx-btn:hover { --btn-wire: var(--signal-hot); --btn-fill: color-mix(in oklch, var(--signal) 12%, transparent); }
.cx-btn:active { --btn-fill: color-mix(in oklch, var(--signal) 22%, transparent); }

.cx-btn--primary { --btn-wire: var(--signal); --btn-fill: var(--signal); --btn-ink: var(--text-on-signal); }
.cx-btn--primary:hover { --btn-wire: var(--signal-hot); --btn-fill: var(--signal-hot); }
.cx-btn--primary:active { --btn-fill: var(--signal); }

.cx-btn--ghost { --btn-wire: transparent; --btn-ink: var(--text-dim); }
.cx-btn--ghost:hover { --btn-wire: var(--wire); --btn-ink: var(--text-hot); }

.cx-btn--danger { --btn-wire: var(--crit); --btn-ink: var(--crit-text); }
.cx-btn--danger:hover { --btn-wire: var(--crit); --btn-fill: var(--crit); --btn-ink: var(--surface-void); }

.cx-btn--sm { min-height: 28px; padding: 0 var(--s-3); font-size: var(--t-12); }
@media (pointer: coarse) {
  .cx-btn--sm, .cx-check, .cx-switch { min-height: var(--hit-coarse); }
}

.cx-btn:disabled, .cx-btn[aria-disabled="true"] {
  --btn-wire: var(--wire);
  --btn-fill: transparent;
  --btn-ink: var(--text-faint);
  border-style: dashed;
  cursor: not-allowed;
}

.cx-btn[aria-busy="true"] { cursor: progress; }
.cx-btn[aria-busy="true"]::before {
  content: "";
  width: 8px;
  height: 8px;
  background: currentColor;
}
@media (prefers-reduced-motion: no-preference) {
  .cx-btn[aria-busy="true"]::before { animation: cx-blink 0.6s steps(2, jump-none) infinite; }
}
@keyframes cx-blink { 50% { opacity: 0.15; } }

/* ---- icon button ---- */
.cx-iconbtn {
  display: inline-grid;
  place-items: center;
  width: var(--hit);
  height: var(--hit);
  padding: 0;
  border: 1px solid var(--wire);
  border-radius: 0;
  background: var(--surface-veil);
  color: var(--text);
  transition: border-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.cx-iconbtn:hover { border-color: var(--signal-hot); color: var(--text-hot); }
.cx-iconbtn[aria-pressed="true"], .cx-iconbtn.is-on { border-color: var(--signal); color: var(--signal-hot); }
.cx-iconbtn--bare { border-color: transparent; background: transparent; }
.cx-iconbtn--bare:hover { border-color: var(--wire); }
.cx-iconbtn svg, .cx-btn svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: square; stroke-linejoin: miter; }

/* ---- field: label + control + help/error ---- */
.cx-field { display: grid; gap: var(--s-1); }
.cx-label {
  color: var(--text-dim);
  font: 600 var(--t-12) / 1.2 var(--font-ui);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.cx-help { color: var(--text-faint); font-size: var(--t-12); }
.cx-error { color: var(--crit-text); font-size: var(--t-12); font-weight: 600; }
.cx-error::before { content: ""; display: inline-block; width: 6px; height: 6px; margin-right: 6px; background: var(--crit); vertical-align: 1px; }

.cx-input, .cx-select, .cx-textarea {
  width: 100%;
  min-height: var(--hit);
  padding: 0 var(--s-3);
  border: 1px solid var(--wire-strong);
  border-radius: 0;
  background: var(--surface-floor);
  color: var(--text-hot);
  font: 400 var(--t-15) / 1.3 var(--font-ui);
  transition: border-color var(--t-fast) var(--ease-out), background-color var(--t-fast) var(--ease-out);
}
.cx-textarea { min-height: 96px; padding: var(--s-2) var(--s-3); resize: vertical; }
.cx-input--data { font: 400 var(--t-data-s) / 1 var(--font-data); letter-spacing: 0.02em; }
.cx-input:hover, .cx-select:hover, .cx-textarea:hover { border-color: var(--signal); }
.cx-input:focus, .cx-select:focus, .cx-textarea:focus { border-color: var(--signal-hot); background: var(--surface-panel); outline: none; }
/* Outside a .cx-field there are no brackets: a doubled wire carries keyboard focus. */
.cx-input:focus-visible, .cx-select:focus-visible, .cx-textarea:focus-visible { box-shadow: 0 0 0 1px var(--signal-hot); }
.cx-input[aria-invalid="true"], .cx-textarea[aria-invalid="true"] { border-color: var(--crit); }
.cx-input:disabled, .cx-select:disabled, .cx-textarea:disabled { border-style: dashed; border-color: var(--wire); color: var(--text-faint); cursor: not-allowed; }

.cx-select {
  appearance: none;
  padding-right: var(--s-7);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
    linear-gradient(-45deg, transparent 50%, var(--text-dim) 50%);
  background-position: calc(100% - 18px) 50%, calc(100% - 13px) 50%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
}
.cx-select option { background: var(--surface-panel); color: var(--text); }

/* ---- checkbox / radio: a voxel ---- */
.cx-check {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 28px;
  cursor: pointer;
}
.cx-check input {
  appearance: none;
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  margin: 0;
  border: 1px solid var(--wire-strong);
  background: var(--surface-floor);
  cursor: inherit;
}
.cx-check input[type="radio"] { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); border: 0; background: var(--wire-strong); }
.cx-check input[type="radio"]::before { content: ""; width: 12px; height: 12px; clip-path: inherit; background: var(--surface-floor); }
.cx-check input::after { content: ""; width: 8px; height: 8px; background: var(--signal-hot); transform: scale(0); transition: transform var(--t-fast) var(--snap); grid-area: 1 / 1; }
.cx-check input[type="radio"]::after { clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); }
.cx-check input:checked { border-color: var(--signal); }
.cx-check input:checked::after { transform: scale(1); }
.cx-check input:focus-visible { outline: none; }
.cx-check:has(input:focus-visible)::after { inset: calc(-1 * var(--cx-lock-gap)); opacity: 1; }
.cx-check:has(input:disabled) { color: var(--text-faint); cursor: not-allowed; }

/* ---- switch: a pixel track with a sliding voxel ---- */
.cx-switch { display: inline-flex; align-items: center; gap: var(--s-2); min-height: 28px; cursor: pointer; }
.cx-switch input {
  appearance: none;
  position: relative;
  width: 36px;
  height: 16px;
  margin: 0;
  border: 1px solid var(--wire-strong);
  background: repeating-linear-gradient(90deg, var(--wire-faint) 0 2px, transparent 2px 4px) var(--surface-floor);
  cursor: inherit;
}
.cx-switch input::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 10px;
  height: 10px;
  background: var(--text-dim);
  transition: transform var(--t-med) var(--snap), background-color var(--t-fast);
}
.cx-switch input:checked { border-color: var(--signal); background: color-mix(in oklch, var(--signal) 22%, var(--surface-floor)); }
.cx-switch input:checked::after { transform: translateX(20px); background: var(--signal-hot); }
.cx-switch input:focus-visible { outline: none; }
.cx-switch:has(input:focus-visible)::after { inset: calc(-1 * var(--cx-lock-gap)); opacity: 1; }

/* ---- keyboard key ---- */
.cx-kbd {
  display: inline-block;
  min-width: 20px;
  padding: 3px 5px 2px;
  border: 1px solid var(--wire);
  border-bottom-width: 2px;
  color: var(--text);
  font: 400 var(--t-data-s) / 1 var(--font-data);
  text-align: center;
}
}
@layer ctos.components {
/* Chips, route pills, badges, meters, toasts, empty states, device IDs.
   State is never colour alone: every level also changes shape or pattern. */

/* ---- device ID: U-07, GW-02. Shown once per thing, never repeated as a label ---- */
.cx-id {
  display: inline-block;
  color: var(--signal-hot);
  font: 400 var(--t-data-s) / 1 var(--font-data);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
.cx-id--dim { color: var(--text-faint); }

/* ---- chip: alerts and status. Marker shape carries the level ----
   info: hollow voxel · warn: hatched voxel · crit: whole chip inverts */
.cx-chip {
  --chip: var(--text-dim);
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 24px;
  padding: 0 var(--s-2);
  border: 1px solid color-mix(in oklch, var(--chip) 70%, transparent);
  color: var(--chip);
  font: 600 var(--t-12) / 1.2 var(--font-ui);
  letter-spacing: 0.03em;
}
.cx-chip::before {
  content: "";
  flex: none;
  width: 8px;
  height: 8px;
  border: 1px solid currentColor;
}
.cx-chip--ok { --chip: var(--signal-hot); }
.cx-chip--ok::before { background: currentColor; }
.cx-chip--warn { --chip: var(--warn); }
.cx-chip--warn::before { background: repeating-linear-gradient(135deg, currentColor 0 1.5px, transparent 1.5px 3px); }
.cx-chip--crit { --chip: var(--crit); background: var(--crit); color: var(--surface-void); border-color: var(--crit); }
.cx-chip--crit::before { background: var(--surface-void); border-color: var(--surface-void); }

/* ---- route pill: how a node is reached. The reserved route colour belongs to "via" ---- */
.cx-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  min-height: 22px;
  padding: 0 var(--s-2);
  border: 1px solid var(--wire-strong);
  color: var(--text);
  font: 400 var(--t-data-s) / 1 var(--font-data);
  white-space: nowrap;
}
.cx-pill--direct { border-color: var(--signal); color: var(--signal-hot); }
.cx-pill--via { border-color: var(--route); color: var(--route); background: color-mix(in oklch, var(--route) 10%, transparent); }
.cx-pill--none, .cx-pill--unknown { border-style: dashed; border-color: var(--wire-strong); color: var(--text-faint); }

/* ---- count badge ---- */
.cx-badge {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  background: var(--crit);
  color: var(--surface-void);
  font: 400 var(--t-data-s) / 1 var(--font-data);
}
.cx-badge--signal { background: var(--signal); }

/* ---- pixel meter: segmented bar, fill snaps to whole segments ----
   <div class="cx-meter" role="meter" aria-valuenow="62" style="--value: 62"> */
.cx-meter {
  --value: 0;
  --seg: calc(var(--cx-meter-seg) + var(--cx-meter-gap));
  --fill: var(--signal);
  position: relative;
  height: 8px;
  background: repeating-linear-gradient(90deg, var(--wire-faint) 0 var(--cx-meter-seg), transparent var(--cx-meter-seg) var(--seg));
}
.cx-meter::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: calc(var(--value) * 1%);
  width: round(down, calc(var(--value) * 1%), var(--seg));
  background: repeating-linear-gradient(90deg, var(--fill) 0 var(--cx-meter-seg), transparent var(--cx-meter-seg) var(--seg));
}
.cx-meter[data-level="warn"] { --fill: var(--warn); }
.cx-meter[data-level="warn"]::before { mask: repeating-linear-gradient(to bottom, #000 0 2px, transparent 2px 3px); }
.cx-meter[data-level="crit"] { --fill: var(--crit); outline: 1px solid var(--crit); outline-offset: 2px; }
.cx-meter--lg { height: 14px; }

/* Meter row: label · bar · value, as in a node's stats. */
.cx-stat { display: grid; grid-template-columns: 4ch 1fr 7ch; align-items: center; gap: var(--s-2); font: 400 var(--t-data-s) / 1 var(--font-data); }
.cx-stat > :first-child { color: var(--text-faint); }
.cx-stat > :last-child { color: var(--text); text-align: right; }

/* ---- toast ---- */
.cx-toast {
  position: relative;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 2px var(--s-3);
  width: min(360px, 100%);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--wire);
  background: var(--surface-panel);
  box-shadow: var(--shadow);
  color: var(--text);
  text-align: left;
}
.cx-toast::before { content: ""; position: absolute; inset: -1px -1px auto; height: 1px; background: var(--signal-hot); }
.cx-toast > .cx-id { grid-row: 1 / span 2; padding-top: 3px; }
.cx-toast strong { color: var(--text-hot); font-size: var(--t-13); font-weight: 600; }
.cx-toast span { overflow: hidden; color: var(--text-dim); font-size: var(--t-13); white-space: nowrap; text-overflow: ellipsis; }
@keyframes cx-drop { from { opacity: 0; transform: translateY(-8px); } }
@media (prefers-reduced-motion: no-preference) { .cx-toast { animation: cx-drop var(--t-med) var(--snap); } }

/* ---- empty state: an unlit voxel and one line of what to do ---- */
.cx-empty {
  display: grid;
  justify-items: center;
  gap: var(--s-3);
  padding: var(--s-6) var(--s-4);
  color: var(--text-dim);
  text-align: center;
}
.cx-empty::before {
  content: "";
  width: 24px;
  height: 24px;
  border: 1px dashed var(--wire-strong);
  background: radial-gradient(circle at 50% 50%, var(--wire) 2px, transparent 2.5px);
}
.cx-empty p { max-width: 36ch; }

/* ---- profiler tag: a locked-on thing's identity card ---- */
.cx-tag {
  display: grid;
  gap: 2px;
  min-width: 168px;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--wire);
  background: var(--surface-veil);
  color: var(--text);
  text-align: left;
}
.cx-tag__name { color: var(--text-hot); font-size: var(--t-15); font-weight: 600; line-height: 1.2; }
.cx-tag__meta { color: var(--text-dim); font-size: var(--t-12); }
.cx-tag[data-state="down"] { border-style: dashed; color: var(--text-faint); }
.cx-tag[data-state="down"] .cx-tag__name { color: var(--text-dim); }
.cx-tag[data-state="crit"] { border-color: var(--crit); }
}
@layer ctos.components {
/* Panels, popovers, dialogs, tooltips, tabs, the header bar.
   Containers are wire bays with a 45° cut corner, not rounded cards. */

/* ---- panel ---- */
.cx-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--wire);
  background: var(--surface-panel);
}
@supports (corner-shape: bevel) {
  .cx-panel, .cx-popover, .cx-dialog { border-top-right-radius: var(--cut); corner-shape: bevel; }
}

.cx-panel__head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 44px;
  padding: var(--s-1) var(--s-2) var(--s-1) var(--s-4);
  border-bottom: 1px solid var(--wire-faint);
}
.cx-panel__head h2, .cx-panel__head h3 { flex: 1; min-width: 0; overflow: hidden; font-size: var(--t-15); white-space: nowrap; text-overflow: ellipsis; }
.cx-panel__body { flex: 1; min-height: 0; padding: var(--s-4); overflow: auto; }
.cx-panel__foot { display: flex; gap: var(--s-2); padding: var(--s-3) var(--s-4); border-top: 1px solid var(--wire-faint); }

/* ---- popover: a raised panel ---- */
.cx-popover {
  border: 1px solid var(--wire-strong);
  background: var(--surface-panel);
  box-shadow: var(--shadow);
}
@media (prefers-reduced-motion: no-preference) {
  .cx-popover { animation: cx-drop var(--t-med) var(--snap); }
}

/* ---- dialog (native <dialog>) ---- */
.cx-dialog {
  width: min(480px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--wire-strong);
  background: var(--surface-panel);
  color: var(--text);
  box-shadow: var(--shadow);
}
.cx-dialog::backdrop {
  background:
    repeating-linear-gradient(to bottom, transparent 0 2px, var(--scanline) 2px 3px),
    oklch(0.08 0.02 258 / 0.78);
}
.cx-dialog__body { padding: var(--s-5) var(--s-4) var(--s-4); }
.cx-dialog__body h2 { margin-bottom: var(--s-2); font-size: var(--t-20); }
.cx-dialog__body p { color: var(--text-dim); }
.cx-dialog__actions { display: flex; justify-content: flex-end; gap: var(--s-2); padding: var(--s-3) var(--s-4); border-top: 1px solid var(--wire-faint); }

/* ---- tooltip ---- */
.cx-tooltip {
  max-width: 260px;
  padding: var(--s-1) var(--s-2);
  border: 1px solid var(--wire-strong);
  background: var(--surface-raised);
  color: var(--text);
  font-size: var(--t-12);
  box-shadow: var(--shadow);
}

/* ---- tabs: the selected tab is locked on ---- */
.cx-tabs { display: flex; gap: var(--s-1); padding: var(--s-1) 0; border-bottom: 1px solid var(--wire-faint); overflow-x: auto; }
.cx-tab {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: var(--hit);
  padding: 0 var(--s-3);
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: 600 var(--t-13) / 1 var(--font-ui);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  white-space: nowrap;
}
.cx-tab:hover { color: var(--text-hot); }
.cx-tab[aria-selected="true"], .cx-tab[aria-current]:not([aria-current="false"]) { color: var(--text-hot); }
/* Tabs sit in an overflow-x strip: brackets stay inset so the strip never clips or scrolls them. */
.cx-tab::after { inset: 4px; }

/* ---- header bar: the thin strip over a full-bleed surface ---- */
.cx-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-4);
  min-height: 52px;
  padding: var(--s-2) var(--s-4);
  border-bottom: 1px solid var(--wire);
  background: var(--surface-void);
}
.cx-bar__title {
  color: var(--text-hot);
  font: 400 var(--t-data-l) / 1 var(--font-data);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.cx-bar__meta { color: var(--text-dim); font-size: var(--t-13); }
.cx-bar__end { display: flex; align-items: center; gap: var(--s-2); margin-left: auto; }

/* Stack and cluster helpers used by components and pages. */
.cx-stack { display: grid; gap: var(--gap, var(--s-3)); align-content: start; }
.cx-cluster { display: flex; flex-wrap: wrap; align-items: center; gap: var(--gap, var(--s-2)); }
}
@layer ctos.components {
/* Tables, key/value inspectors, logs, chat messages. Numbers are tabular, IDs are pixel mono. */

/* ---- table ---- */
.cx-table { width: 100%; border-collapse: collapse; font-size: var(--t-13); }
.cx-table th {
  padding: var(--s-2) var(--s-3);
  border-bottom: 1px solid var(--wire);
  color: var(--text-dim);
  font: 600 var(--t-12) / 1.2 var(--font-ui);
  letter-spacing: var(--track-label);
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}
.cx-table td { padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--wire-faint); vertical-align: middle; }
.cx-table td.is-num, .cx-table th.is-num { text-align: right; font-variant-numeric: tabular-nums; }
.cx-table td.is-data { font: 400 var(--t-data-s) / 1.2 var(--font-data); }
.cx-table tbody tr { transition: background-color var(--t-fast); }
.cx-table tbody tr:hover { background: color-mix(in oklch, var(--signal) 6%, transparent); }
.cx-table tbody tr[aria-selected="true"] { background: color-mix(in oklch, var(--signal) 13%, transparent); box-shadow: inset 0 0 0 1px var(--signal); }
.cx-table tbody tr.is-down td { color: var(--text-faint); }
.cx-table-wrap { overflow-x: auto; }

/* ---- key/value inspector ---- */
.cx-kv { display: grid; grid-template-columns: max-content 1fr; gap: var(--s-1) var(--s-4); font-size: var(--t-13); }
.cx-kv dt { color: var(--text-faint); font: 600 var(--t-12) / 1.6 var(--font-ui); letter-spacing: var(--track-label); text-transform: uppercase; }
.cx-kv dd { min-width: 0; color: var(--text); overflow-wrap: anywhere; }

/* ---- log / console ---- */
.cx-log {
  margin: 0;
  padding: var(--s-2) 0;
  background: var(--surface-floor);
  font: 400 var(--t-data-s) / 1.6 var(--font-data);
  overflow: auto;
}
.cx-log > li { display: grid; grid-template-columns: 9ch 5ch 1fr; gap: var(--s-3); padding: 1px var(--s-3); }
.cx-log time { color: var(--text-faint); }
.cx-log .lvl { color: var(--text-dim); }
.cx-log [data-level="warn"] .lvl { color: var(--warn); }
.cx-log [data-level="crit"] { background: color-mix(in oklch, var(--crit) 14%, transparent); }
.cx-log [data-level="crit"] .lvl { background: var(--crit); color: var(--surface-void); text-align: center; }
.cx-log [data-level="ok"] .lvl { color: var(--signal-hot); }

/* ---- chat messages ---- */
.cx-msgs { display: flex; flex-direction: column; gap: var(--s-2); margin: 0; padding: var(--s-3); list-style: none; }
.cx-msg {
  max-width: 85%;
  padding: var(--s-2) var(--s-3);
  border: 1px solid var(--wire);
  background: var(--surface-floor);
  overflow-wrap: anywhere;
}
.cx-msg--out { align-self: flex-end; border-color: color-mix(in oklch, var(--signal) 60%, transparent); background: color-mix(in oklch, var(--signal) 12%, var(--surface-panel)); }
.cx-msg--in { align-self: flex-start; }
.cx-msg p { white-space: pre-wrap; }
.cx-msg time { display: block; margin-top: 2px; color: var(--text-faint); font: 400 var(--t-data-s) / 1.4 var(--font-data); }
.cx-msg--out time { text-align: right; }
}
@layer ctos.components {
/* City map (city-map.js): the network as a voxel point-cloud city. Districts are networks,
   nodes stand on rooftops on a beacon, routes arc over the skyline. The canvas, the arc
   layer and the marks all share the host's screen space; city-map.js positions the marks. */

.cx-citymap {
  /* Dot palette: city-map.js reads these, mixed per district with --net-N. */
  --city-body: var(--ink-5);
  --city-roof: var(--ink-8);
  --city-window: var(--scan-7);
  --city-street: var(--ink-6);
  --city-major: var(--ink-7);     /* boulevards, the ring highway, bridges */
  --city-border: var(--signal);   /* a real city's administrative boundary */
  --city-water: var(--scan-3);
  --city-park: oklch(0.55 0.1 170);

  position: relative;
  isolation: isolate; /* node z-indexes stay inside the map, under the page's panels */
  overflow: hidden;
  min-height: 320px;
  background:
    radial-gradient(ellipse 70% 60% at 55% 55%, oklch(0.17 0.04 258), transparent 70%),
    var(--surface-void);
  cursor: grab;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}
.cx-citymap.is-dragging { cursor: grabbing; }
.cx-citymap:focus-visible { outline: 1px solid var(--lock); outline-offset: -4px; }

.cx-citymap__city, .cx-citymap__links, .cx-citymap__marks { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; }
.cx-citymap__links { overflow: visible; pointer-events: none; }

/* Hover readout: the address or street under the cursor (real maps only). */
.cx-citymap__tip {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 6000;
  display: grid;
  gap: 1px;
  pointer-events: none;
  color: var(--text-hot);
  font-weight: 600;
  white-space: nowrap;
}
.cx-citymap__tip span { color: var(--text-dim); font: 400 var(--t-data-s) / 1.4 var(--font-data); text-transform: uppercase; }

/* Network slots for anything inside the map. */
.cx-citymap [data-net="0"] { --net: var(--net-0); --dash: var(--net-dash-0); }
.cx-citymap [data-net="1"] { --net: var(--net-1); --dash: var(--net-dash-1); }
.cx-citymap [data-net="2"] { --net: var(--net-2); --dash: var(--net-dash-2); }
.cx-citymap [data-net="3"] { --net: var(--net-3); --dash: var(--net-dash-3); }
.cx-citymap [data-net="4"] { --net: var(--net-4); --dash: var(--net-dash-4); }
.cx-citymap [data-net="5"] { --net: var(--net-5); --dash: var(--net-dash-5); }

/* District boundary on the ground: each network's own dash, so districts never rely on colour. */
.cx-ring-back { fill: none; stroke: var(--surface-void); stroke-width: 4; opacity: 0.6; vector-effect: non-scaling-stroke; }
.cx-ring { fill: none; stroke: var(--net); stroke-width: 1.5; stroke-dasharray: var(--dash); opacity: 0.75; vector-effect: non-scaling-stroke; }

/* ---- links ---- */
.cx-relay { fill: none; stroke: var(--net); stroke-width: 1; stroke-dasharray: 1 3; opacity: 0.7; vector-effect: non-scaling-stroke; }
.cx-route { fill: none; stroke: var(--route); stroke-width: 1.75; stroke-linecap: square; stroke-dasharray: 2 6; opacity: 0.9; vector-effect: non-scaling-stroke; }
.cx-route--direct { stroke: var(--signal-hot); stroke-width: 1.5; stroke-dasharray: none; opacity: 0.8; }
@media (prefers-reduced-motion: no-preference) {
  .cx-route--via { animation: cx-flow 0.9s steps(4, end) infinite; }
}

/* ---- marks: zero-size anchors; city-map.js sets their transform to a screen point ---- */
.cx-node, .cx-district {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
}

/* District caption: hangs under the district's ground point, above rooftop markers
   (z-index over node anchors; tags already steer clear of it in the collision pass). */
.cx-district {
  z-index: 4000;
  display: grid;
  justify-items: center;
  pointer-events: none;
}
.cx-district > * { transform: translate(calc(-50% + var(--cap-dx, 0px)), var(--cap-dy, 10px)); } /* caption hangs below the district's ground point; city-map.js lowers it past markers */
.cx-district__name {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 6px;
  border: 1px solid color-mix(in oklch, var(--net) 60%, transparent);
  background: var(--surface-void);
  color: var(--text-hot);
  font: 400 var(--t-data-s) / 1.4 var(--font-data);
  white-space: nowrap;
}
.cx-district__name::before { content: ""; width: 6px; height: 6px; background: var(--net); }
.cx-district__meta { margin-top: 2px; padding: 1px 6px; background: var(--surface-void); color: var(--text-dim); font: 400 var(--t-data-s) / 1.2 var(--font-data); white-space: nowrap; }

/* Node: a beacon rising from the roof, a voxel marker on the roof, a tag on top. */
.cx-node__beacon {
  position: absolute;
  left: -0.5px;
  bottom: 0;
  width: 1px;
  height: var(--beacon, 24px);
  background: linear-gradient(to top, var(--net), color-mix(in oklch, var(--net) 30%, transparent));
}
.cx-node__mark {
  position: absolute;
  left: -4px;
  top: -4px;
  width: 8px;
  height: 8px;
  background: var(--net);
  box-shadow: 0 0 0 2px var(--surface-void);
}
.cx-node[data-kind="gateway"] .cx-node__mark {
  left: -6px;
  top: -6px;
  width: 12px;
  height: 12px;
  border: 2px solid var(--net);
  background: var(--surface-void);
  transform: rotate(45deg);
}
.cx-node.is-self .cx-node__mark { background: var(--text-hot); }

.cx-node__tag {
  position: absolute;
  left: 0;
  bottom: calc(var(--beacon, 24px) + 2px);
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 3px 7px;
  border: 1px solid color-mix(in oklch, var(--net) 55%, var(--wire));
  background: var(--surface-void);
  color: var(--text-hot);
  font: 600 var(--t-13) / 1.2 var(--font-ui);
  white-space: nowrap;
  transform: translateX(-50%);
  cursor: pointer;
}
.cx-node__tag:hover { border-color: var(--signal-hot); }
@media (pointer: coarse) {
  .cx-node__tag::before { content: ""; position: absolute; inset: -10px -6px; } /* 44px touch target */
}
.cx-node__tag .cx-id { color: var(--net); }
.cx-node.is-locked .cx-node__tag { border-color: var(--signal-hot); background: var(--surface-panel); }

/* Ping ring on the locked node's marker. */
.cx-node.is-locked .cx-node__mark::after {
  content: "";
  position: absolute;
  inset: -6px;
  border: 1px solid var(--signal-hot);
  pointer-events: none;
}
@keyframes cx-ping { from { inset: -4px; opacity: 1; } to { inset: -22px; opacity: 0; } }
@media (prefers-reduced-motion: no-preference) {
  .cx-node.is-locked .cx-node__mark::after { animation: cx-ping 1.6s steps(8, end) infinite; }
}

/* Down / no contact: dashed beacon, hollow marker, dim. Never colour alone. */
.cx-node[data-state="down"], .cx-node[data-state="none"] { opacity: 0.6; }
.cx-node[data-state="down"] .cx-node__beacon { background: repeating-linear-gradient(to top, var(--wire-strong) 0 3px, transparent 3px 6px); }
.cx-node[data-state="down"] .cx-node__mark { background: var(--surface-void); outline: 1px dashed var(--wire-strong); box-shadow: none; }
.cx-node[data-state="down"] .cx-node__tag { border-style: dashed; color: var(--text-dim); }
.cx-node[data-state="none"] .cx-node__beacon { background: repeating-linear-gradient(to top, var(--text-faint) 0 1px, transparent 1px 4px); }

/* A locked node dims everything it is not routed to. Tags dim by colour, never by opacity,
   so their dark backing stays solid over the dots. */
.cx-citymap.has-selection .cx-node:not(.is-locked):not(.is-related) {
  & .cx-node__beacon, & .cx-node__mark { opacity: 0.45; }
  & .cx-node__tag { border-color: var(--wire); color: var(--text-dim); }
  & .cx-node__tag .cx-id { color: color-mix(in oklch, var(--net) 60%, var(--text-faint)); }
}

/* Label collision pass (city-map.js): crowded tags shrink to their ID, then hide until focused. */
.cx-node.is-crowded .cx-node__name { display: none; }
.cx-node.is-hidden:not(:focus-within) .cx-node__tag { opacity: 0; pointer-events: none; }
.cx-district.is-hidden .cx-district__meta { display: none; }

/* Far zoom: tags shrink to their device ID; the locked node keeps its name. */
.cx-citymap.is-far .cx-node:not(.is-locked) .cx-node__name { display: none; }
.cx-citymap.is-far .cx-district__meta { display: none; }
}
@layer ctos.utilities {
/* Small single-purpose helpers. Last layer, so they win over components. */

.cx-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.cx-data { font-family: var(--font-data); font-size: var(--t-data-s); letter-spacing: 0.02em; }
.cx-dim { color: var(--text-dim); }
.cx-faint { color: var(--text-faint); }
.cx-hot { color: var(--text-hot); }
.cx-num { font-variant-numeric: tabular-nums; }
.cx-display { color: var(--text-hot); font: 400 var(--t-data-xl) / 1 var(--font-data); letter-spacing: 0.01em; text-transform: uppercase; }
}
