/* ============================================================================
   GEMASTIK UNAIR — Attack Defense Platform
   Shared design tokens + primitives.

   One stylesheet for tokens and the components every page uses, so index /
   team / login / submit / guide stop each carrying a near-identical copy of the
   same hex values. Page-specific rules stay in that page's own <style>; only
   what is genuinely shared lives here.

   Visual direction is "instrumentation", not "SaaS dashboard": hard 3px
   corners, hairline rules, reticle ticks on framed surfaces, uppercase
   micro-labels, and tabular numerals everywhere a number can change. Colour
   carries meaning only (health / attack / degraded), never decoration.
   ========================================================================= */

:root {
  /* surfaces — cool ink, near-black at the edges */
  --bg:            #070a10;
  --bg-grid:       #0d1420;
  --surface:       #0c121d;
  --surface-2:     #101825;
  --surface-3:     #16202f;
  --line:          #1c2637;
  --line-strong:   #2a3a52;

  /* ink */
  --ink:           #d7e2f0;
  --ink-strong:    #f2f7ff;
  --ink-dim:       #8496ad;
  --ink-faint:     #5a6a80;

  /* signal — semantic only */
  --sig:           #35e0d0;   /* brand / focus / active */
  --sig-soft:      #35e0d022;
  --ok:            #4ade80;
  --ok-soft:       #4ade8018;
  --warn:          #fbbf24;
  --warn-soft:     #fbbf2418;
  --hot:           #fb5f7d;   /* attack / destructive */
  --hot-soft:      #fb5f7d18;
  --cool:          #60a5fa;
  --cool-soft:     #60a5fa18;

  /* type */
  --mono: 'JetBrains Mono','Fira Code',ui-monospace,SFMono-Regular,Menlo,monospace;
  --sans: 'Inter',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;
  --r:    3px;                 /* hard corners: this is an instrument, not a card */
  --r-lg: 5px;
}

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink);
  background-color: var(--bg);
  /* hairline graph paper — the grid the topology and tables sit on */
  background-image:
    linear-gradient(var(--bg-grid) 1px, transparent 1px),
    linear-gradient(90deg, var(--bg-grid) 1px, transparent 1px);
  background-size: 32px 32px;
  min-height: 100vh;
  padding: 20px;
  -webkit-font-smoothing: antialiased;
}

/* Numbers that change must not reflow their neighbours. */
.num, td, th, .pill, input, button, select { font-variant-numeric: tabular-nums; }

::selection { background: var(--sig); color: #04070c; }

/* ---------- focus: visible, always, never removed ---------- */
:focus-visible {
  outline: 2px solid var(--sig);
  outline-offset: 2px;
  border-radius: var(--r);
}
:focus:not(:focus-visible) { outline: none; }

/* ---------- type helpers ---------- */
h1 { font-size: 15px; font-weight: 700; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-strong); }
h2 { font-size: 12px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; color: var(--sig); }
a  { color: var(--cool); text-decoration-color: #60a5fa55; text-underline-offset: 3px; }
a:hover { color: var(--ink-strong); }
.dim   { color: var(--ink-dim); }
.faint { color: var(--ink-faint); }
.hot   { color: var(--hot); }
.ok    { color: var(--ok); }
.warn  { color: var(--warn); }
.tiny  { font-size: 11px; }
.micro {
  font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600;
}
.nowrap { white-space: nowrap; }
.sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------- header ---------- */
header.app {
  display: flex; justify-content: space-between; align-items: center;
  gap: 14px; flex-wrap: wrap;
  padding-bottom: 14px; margin-bottom: 16px;
  border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand svg { width: 22px; height: 22px; flex: none; }
.brand .sub {
  font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--ink-faint); margin-top: 2px;
}
.app-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }

/* ---------- tabs ---------- */
.tabs {
  display: flex; gap: 2px; margin-bottom: 18px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line); padding-bottom: 0;
}
.tab {
  position: relative;
  display: inline-flex; align-items: center; gap: 7px;
  background: transparent; border: 1px solid transparent; border-bottom: none;
  color: var(--ink-dim);
  padding: 9px 14px; margin-bottom: -1px;
  border-radius: var(--r) var(--r) 0 0;
  font-family: inherit; font-size: 12px; font-weight: 500;
  cursor: pointer; transition: color .15s, background .15s;
}
.tab:hover { color: var(--ink-strong); background: var(--surface-2); }
.tab.active { color: var(--sig); border-color: var(--line); background: var(--surface); }
.tab.active::after {
  content: ''; position: absolute; inset: auto 0 -1px 0; height: 1px; background: var(--surface);
}
.tab svg { width: 14px; height: 14px; opacity: .8; }
.tab.active svg { opacity: 1; }
.tab .count {
  font-size: 10px; padding: 1px 5px; border-radius: 999px;
  background: var(--surface-3); color: var(--ink-dim);
}

/* ---------- buttons ---------- */
button, .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--ink);
  font-family: inherit; font-size: 12px; font-weight: 500;
  padding: 7px 12px; border-radius: var(--r); cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
  text-decoration: none; white-space: nowrap;
}
button:hover, .btn:hover { border-color: var(--sig); color: var(--ink-strong); background: var(--surface-3); }
button:active, .btn:active { transform: translateY(1px); }
button svg, .btn svg { width: 13px; height: 13px; flex: none; }
button.primary, .btn.primary {
  background: var(--sig-soft); border-color: var(--sig); color: var(--sig); font-weight: 600;
}
button.primary:hover, .btn.primary:hover { background: var(--sig); color: #04070c; }
button.danger, .btn.danger { color: var(--hot); border-color: #fb5f7d55; background: var(--hot-soft); }
button.danger:hover, .btn.danger:hover { background: var(--hot); color: #180408; border-color: var(--hot); }
button.ghost { background: transparent; border-color: transparent; color: var(--ink-dim); }
button.ghost:hover { color: var(--sig); background: var(--surface-2); }
button:disabled, .btn[aria-disabled=true] { opacity: .4; pointer-events: none; }
button.sm, .btn.sm { font-size: 11px; padding: 5px 9px; }
button.icon { padding: 6px; }

/* ---------- pills / status ---------- */
.pill {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--surface-2); border: 1px solid var(--line);
  color: var(--ink-dim); font-size: 11px; padding: 4px 9px; border-radius: 999px;
  white-space: nowrap;
}
/* ---- SLA / leaderboard rows ----
   These lived only in team.html's inline <style>, so the admin panel's SLA tab
   rendered them completely unstyled: no flex, no column widths, and the rank
   badge ("Juara 1", "Peringkat 3") overlapped the team name next to it. The
   rows are shared markup, so the rules belong in the shared stylesheet.

   The rank column is 76px because the badge is a phrase, not a digit: "1" fit
   in 28px, "Peringkat 3" does not, and flex: none stops it collapsing and
   pushing the name over the track. */
/* ---- per-service SLA strip on each challenge card ----
   One cell per team, always all of them. The earlier single-status chip could
   only say "this challenge on the one team the global receiver talks to", so a
   dead service looked the same whether one team was down or all five were. */
.sla-strip {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  margin: 8px 0 6px; padding: 7px 8px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r);
}
.sla-lbl {
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint); flex: none;
}
.sla-sum { font-size: 10px; font-weight: 700; flex: none; }
.sla-sum.ok  { color: var(--ok); }
.sla-sum.bad { color: var(--hot); }
.sla-cells { display: flex; flex-wrap: wrap; gap: 4px; flex: 1 1 auto; }
.sla-cell {
  font-size: 10px; font-weight: 600; line-height: 1;
  padding: 4px 6px; border-radius: 999px; border: 1px solid transparent;
  max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sla-cell.ok  { color: var(--ok);   border-color: #4ade8055; background: var(--ok-soft); }
.sla-cell.bad { color: var(--hot);  border-color: #fb5f7d55; background: var(--hot-soft); }
.sla-cell.na  { color: var(--ink-faint); border-color: var(--line); background: var(--surface-3); }

.sla-row { display: flex; align-items: center; gap: 9px; padding: 6px 0; border-bottom: 1px solid var(--line); }
.sla-row:last-child { border-bottom: none; }
.lb-rank {
  flex: none; min-width: 76px; font-weight: 700; font-size: 12px;
  color: var(--ink-dim); white-space: nowrap;
}
.lb-rank.lb-1 { color: #ffd166; }
.lb-rank.lb-2 { color: #c9d4e0; }
.lb-rank.lb-3 { color: #d08b52; }
.sla-name { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: var(--ink-strong); }
.sla-track { flex: 1 1 130px; height: 5px; background: var(--surface-3); border-radius: 3px; overflow: hidden; }
.sla-fill { display: block; height: 100%; background: var(--ok); }
.sla-fill.mid { background: var(--warn); }
.sla-fill.low { background: var(--hot); }

.pill.green { color: var(--ok);  border-color: #4ade8055; background: var(--ok-soft); }
.pill.red   { color: var(--hot); border-color: #fb5f7d55; background: var(--hot-soft); }
.pill.amber { color: var(--warn);border-color: #fbbf2455; background: var(--warn-soft); }
/* informational: a container mid-restart, which is not a fault */
.pill.cool  { color: var(--cool); border-color: #60a5fa55; background: var(--cool-soft); }
.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
.pill.green .dot { animation: beat 2.4s ease-in-out infinite; }
@keyframes beat { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

.status { font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
.status.up   { color: var(--ok); }
.status.down { color: var(--hot); }
.status.unknown { color: var(--ink-faint); }

/* ---------- framed surface with reticle ticks ---------- */
.card {
  position: relative;
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 16px;
  display: flex; flex-direction: column; gap: 10px;
}
.card.dead { border-color: #fb5f7d44; background: color-mix(in srgb, #1a0c14 70%, transparent); }
/* corner ticks: the on-theme detail that stops these reading as generic cards */
.card::before, .card::after {
  content: ''; position: absolute; width: 7px; height: 7px; pointer-events: none;
  border-color: var(--line-strong); border-style: solid;
}
.card::before { top: -1px; left: -1px;  border-width: 1px 0 0 1px; }
.card::after  { bottom: -1px; right: -1px; border-width: 0 1px 1px 0; }
.card.dead::before, .card.dead::after { border-color: var(--hot); }

.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 14px; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.row.tight { gap: 5px; }
.spread { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; }
.stack { display: flex; flex-direction: column; gap: 6px; }

/* ---------- forms ---------- */
label.field, .lbl {
  display: block; font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600; margin-bottom: 5px;
}
input[type=text], input[type=number], input[type=password], input:not([type]), textarea, select {
  width: 100%;
  background: #070b12; border: 1px solid var(--line); color: var(--ink-strong);
  border-radius: var(--r); padding: 9px 11px;
  font-family: inherit; font-size: 13px; outline: none;
  transition: border-color .15s, box-shadow .15s;
}
textarea { resize: vertical; }
input:focus, textarea:focus, select:focus {
  border-color: var(--sig); box-shadow: 0 0 0 3px var(--sig-soft);
}
input::placeholder, textarea::placeholder { color: var(--ink-faint); }
input[type=checkbox] { accent-color: var(--sig); width: 14px; height: 14px; }
select { cursor: pointer; }

/* search box: the one control that makes a 28-challenge list usable */
.search { position: relative; flex: 1 1 220px; min-width: 180px; }
.search svg { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); width: 13px; height: 13px; color: var(--ink-faint); pointer-events: none; }
.search input { padding-left: 29px; }
.search kbd { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); }

kbd {
  font-family: inherit; font-size: 10px; color: var(--ink-faint);
  border: 1px solid var(--line-strong); border-bottom-width: 2px;
  border-radius: 3px; padding: 1px 5px; background: var(--surface-2);
}

/* ---------- data display ---------- */
.flag {
  background: #060910; border: 1px dashed var(--line-strong); border-radius: var(--r);
  padding: 9px 11px; font-size: 12px; color: var(--sig); word-break: break-all; min-height: 17px;
}
.code, .history-box {
  background: #060910; border: 1px solid var(--line); border-radius: var(--r);
  padding: 12px; font-size: 11px; color: var(--cool);
  max-height: 300px; overflow: auto; white-space: pre-wrap; word-break: break-word;
}
.kv { display: grid; grid-template-columns: minmax(90px, 130px) 1fr; gap: 5px 12px; font-size: 12px; }
.kv b { color: var(--ink-dim); font-weight: 600; }
.kv span { color: var(--ink-strong); word-break: break-all; }
.ch-name { font-size: 14px; font-weight: 700; color: var(--ink-strong); letter-spacing: .02em; }
.ch-cat {
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase; padding: 2px 6px;
  border-radius: 2px; background: var(--surface-3); color: var(--ink-dim); font-weight: 600;
}

table { width: 100%; border-collapse: collapse; font-size: 12px; }
th {
  text-align: left; font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 600; padding: 7px 9px;
  border-bottom: 1px solid var(--line-strong); white-space: nowrap;
}
td { padding: 7px 9px; border-bottom: 1px solid var(--line); color: var(--ink); }
tbody tr:hover { background: var(--surface-2); }
tbody tr:last-child td { border-bottom: none; }

/* ---------- views ---------- */
.view { display: none; }
.view.active { display: block; animation: fade .18s ease-out; }
@keyframes fade { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }

/* ---------- modal ---------- */
.modal-back {
  position: fixed; inset: 0; background: #04070ce0; backdrop-filter: blur(3px);
  display: none; align-items: center; justify-content: center; z-index: 50; padding: 20px;
}
.modal-back.open { display: flex; }
/* A modal must be able to hold more content than fits the viewport.
   The credentials modal lists every challenge on a team (16+ rows) and had no
   height cap, so on a laptop it grew past the bottom of the screen with the
   content clipped and no way to reach it: the page behind could not scroll
   either, because the backdrop is position:fixed over everything.

   So: cap the modal, scroll the BODY, and keep the title and the action row
   pinned outside the scroll area. The header and footer stay put while the
   middle scrolls, which is what makes a long list usable rather than just
   reachable. */
.modal {
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-lg); padding: 22px; width: 460px; max-width: 100%;
  /* vh is the real constraint: `inset:0` on the backdrop means the viewport IS
     the available height, so cap against it rather than against content. */
  max-height: calc(100vh - 40px);
  display: flex; flex-direction: column; min-height: 0;
  box-shadow: 0 24px 70px #000a;
}
.modal h2 {
  margin-bottom: 14px; flex: none;      /* never scrolls away */
  overflow-wrap: anywhere;
}
/* The body is the only scrolling part. */
.modal-scroll {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  margin-right: -6px; padding-right: 6px;   /* keep the scrollbar off the padding */
  -webkit-overflow-scrolling: touch;
}
/* Two actions in the footer: keep the secondary one left, the dismiss right. */
.modal .actions {
  display: flex; gap: 8px; justify-content: flex-end;
  margin-top: 18px; flex: none;       /* pinned */
  padding-top: 14px; border-top: 1px solid var(--line);
}
.modal .actions .grow { margin-right: auto; }
/* A long unbroken credential is the thing most likely to blow the width out. */
.modal .kv span { overflow-wrap: anywhere; word-break: break-word; }

/* ---------- toast ---------- */
.toast {
  position: fixed; bottom: 20px; right: 20px; z-index: 80;
  background: var(--surface-2); border: 1px solid var(--sig); border-left-width: 3px;
  color: var(--ink-strong); padding: 11px 15px; border-radius: var(--r);
  font-size: 12px; max-width: min(420px, calc(100vw - 40px));
  opacity: 0; transform: translateY(8px); transition: opacity .2s, transform .2s;
  box-shadow: 0 12px 34px #000a;
}
.toast.show { opacity: 1; transform: none; }
.toast.err { border-color: var(--hot); color: #ffc2cd; }

/* ---------- action toast ----------
   The progress surface for long operations, and the replacement for the
   full-screen loading overlay. The overlay covered the whole viewport and
   swallowed clicks, so during a multi-minute build the operator could not scroll,
   look at anything else, or change their mind. This is cornered, non-blocking,
   and dismissible, so the work stays observable without the UI being held
   hostage by it. */
.toast.action {
  display: flex; align-items: flex-start; gap: 10px;
  min-width: min(300px, calc(100vw - 40px));
  max-width: min(470px, calc(100vw - 40px));
  padding: 12px 34px 12px 14px;   /* right room for the absolute close button */
  transform: none;              /* always in place, no slide-in to fight with */
  pointer-events: auto;
}
.toast.action:not(.show) { opacity: 0; pointer-events: none; }
.toast.action .spin { flex: none; margin-top: 3px; }
.toast.action.err { border-color: var(--hot); }
.toast.action.ok  { border-color: var(--ok); }
.toast.action .act-body { flex: 1; min-width: 0; }
.toast.action .act-title {
  font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  font-weight: 700; color: var(--sig); margin-bottom: 3px;
}
.toast.action.err .act-title { color: var(--hot); }
.toast.action.ok  .act-title { color: var(--ok); }
.toast.action .act-detail { font-size: 12px; color: var(--ink-strong); }
.toast.action .act-sub { font-size: 10.5px; color: var(--ink-faint); margin-top: 4px; }
/* the close button is the ONLY control here, so it must not inherit the
   generic button hover/fill that would swallow the click.
   Positioned rather than a flex item: as a flex child it wrapped onto its own
   row under a narrow toast, which put "x" under the text instead of beside it. */
.toast.action .act-x {
  position: absolute; top: 6px; right: 7px;
  background: none; border: none; color: var(--ink-faint);
  padding: 0 4px; font-size: 16px; line-height: 1; cursor: pointer;
  font-family: inherit;
}
.toast.action .act-x:hover { color: var(--hot); background: none; border: none; }
/* an indeterminate bar so it is obvious work is still happening even when the
   detail text has not changed between polls */
.toast.action .act-bar {
  height: 2px; margin-top: 8px; background: var(--surface-3);
  border-radius: 2px; overflow: hidden;
}
.toast.action .act-bar::after {
  content: ''; display: block; height: 100%; width: 34%;
  background: var(--sig); border-radius: 2px;
  animation: act-slide 1.15s linear infinite;
}
.toast.action.err .act-bar::after,
.toast.action.ok  .act-bar::after { animation: none; width: 100%; }
.toast.action.err .act-bar::after { background: var(--hot); }
.toast.action.ok  .act-bar::after { background: var(--ok); }
@keyframes act-slide { from { transform: translateX(-100%); } to { transform: translateX(394%); } }

/* ---------- blocking overlay ---------- */
#loadingOverlay {
  display: none; position: fixed; inset: 0; background: #04070cf0; z-index: 2000;
  align-items: center; justify-content: center; flex-direction: column; gap: 16px; padding: 24px;
}
#loadingOverlay.show { display: flex; }
.loader {
  width: 38px; height: 38px; border: 2px solid var(--line-strong);
  border-top-color: var(--sig); border-radius: 50%; animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
#loadingText { color: var(--ink); font-size: 13px; text-align: center; line-height: 1.7; }

.spin {
  display: inline-block; width: 11px; height: 11px; border: 2px solid var(--line-strong);
  border-top-color: var(--sig); border-radius: 50%; animation: spin .7s linear infinite;
}

/* ---------- footer ---------- */
.footer-note { color: var(--ink-faint); font-size: 10px; margin-top: 22px; text-align: center; letter-spacing: .1em; }

/* ---------- scrollbars ---------- */
* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 9px; height: 9px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; border: 2px solid var(--bg); }
*::-webkit-scrollbar-thumb:hover { background: var(--sig); }

/* ============================================================================
   Topology
   A <svg>, not a <canvas>: it reflows, its text is real text, and the attack
   pulse is a CSS keyframe instead of a JS render loop.
   ========================================================================= */

.topo-wrap {
  position: relative;
  background:
    radial-gradient(ellipse at 50% 0%, #0e1726 0%, transparent 70%),
    #070b12;
  border: 1px solid var(--line); border-radius: var(--r-lg);
  overflow: hidden;
}
.topo-svg { width: 100%; height: clamp(380px, 58vh, 640px); display: block; }
.topo-canvas { width: 100%; height: 100%; display: block; touch-action: none; cursor: grab; }
.topo-canvas:active { cursor: grabbing; }

.topo-dot {
  fill: var(--surface);
  stroke: var(--line-strong);
  stroke-width: 1.5;
  transition: stroke .15s, fill .15s;
}
.topo-node-infra .topo-dot { fill: var(--surface-2); }
.topo-node-panel .topo-dot { fill: var(--sig-soft);  stroke: var(--sig); }
.topo-node-team  .topo-dot { fill: var(--surface-3); stroke: var(--cool); stroke-width: 2; }
.topo-node-challenge .topo-dot { fill: var(--surface); }

.topo-node { cursor: move; outline: none; }
.topo-node:hover .topo-dot,

/* Health tint, from /api/health. A team or challenge that is down must be
   findable without hovering 71 nodes, so the disc itself carries the state. */
/* Pointer target only. Never painted, never read for layout. */
.topo-hit { fill: transparent; stroke: none; }
.topo-node.state-ok   .topo-dot { stroke: var(--ok);   fill: var(--ok-soft); }
.topo-node.state-warn .topo-dot { stroke: var(--warn); fill: var(--warn-soft); }
.topo-node.state-bad  .topo-dot { stroke: var(--hot);  fill: var(--hot-soft); }
.topo-node.state-off  .topo-dot { stroke: var(--line-strong); fill: var(--surface-2); opacity: .55; }
.topo-node.state-bad .topo-initial { fill: var(--hot); }
.topo-node.state-ok  .topo-initial { fill: var(--ok); }
.topo-node:focus-visible .topo-dot { stroke: var(--sig); stroke-width: 2.5; }
.topo-node:focus-visible .topo-focus { stroke: var(--sig); stroke-dasharray: 3 3; }
.topo-node:hover .topo-name { fill: var(--ink-strong); }

.topo-name {
  fill: var(--ink); font-family: var(--mono); font-size: 11px;
  text-anchor: middle; font-weight: 600; pointer-events: none;
}
.topo-node-panel .topo-name { fill: var(--sig); font-size: 10px; letter-spacing: .14em; }
.topo-node-team  .topo-name { fill: var(--ink-strong); font-size: 12px; }
.topo-node-infra .topo-name { fill: var(--ink-dim); font-size: 10px; }
.topo-sub {
  fill: var(--ink-faint); font-family: var(--mono); font-size: 10px;
  text-anchor: middle; pointer-events: none;
}
.topo-initial {
  fill: var(--ink-dim); font-family: var(--mono); font-size: 7px;
  font-weight: 700; pointer-events: none; user-select: none;
}
.topo-node-challenge:hover .topo-initial { fill: var(--sig); }

/* Full name + port only on hover/focus. Two-letter initials stay at rest
   because 64 permanent labels at this scale is an unreadable pile, but
   "ca" is ambiguous between carbeat / cdn, so the hover state has to exist. */
.topo-chall-name, .topo-chall-port {
  font-family: var(--mono); font-size: 9.5px; pointer-events: none;
  user-select: none; opacity: 0; transition: opacity .12s;
}
.topo-chall-name { fill: var(--ink-strong); font-weight: 600; }
.topo-chall-port { fill: var(--ink-faint); font-size: 8.5px; }
.topo-node-challenge:hover .topo-chall-name,
.topo-node-challenge:focus-visible .topo-chall-name,
.topo-node-challenge:hover .topo-chall-port,
.topo-node-challenge:focus-visible .topo-chall-port { opacity: 1; }

.topo-edge {
  fill: none; stroke: var(--line-strong); stroke-width: 1.2;
  opacity: .75; pointer-events: none;
}
.topo-edge-team { stroke: #2f4a6d; }

.topo-attack-line {
  fill: none; stroke: currentColor; stroke-width: 1.8;
  pointer-events: none; marker-end: url(#ad-arrow);
}
.topo-attack.ok   { color: var(--hot); }
.topo-attack.fail { color: var(--warn); }
.topo-attack-label {
  fill: currentColor; font-family: var(--mono); font-size: 9.5px;
  font-weight: 700; pointer-events: none; user-select: none;
}
.topo-attack.ok   .topo-attack-line { animation: ad-strike 1.1s ease-in-out infinite; }
.topo-attack.fail .topo-attack-line { animation: ad-strike 1.8s ease-in-out infinite; opacity: .6; }
@keyframes ad-strike { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

.topo-bar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.topo-legend { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 10px; }
.topo-legend span { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; color: var(--ink-faint); }
.topo-legend i { width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid; display: inline-block; }
.topo-legend .l-panel i  { border-color: var(--sig);  background: var(--sig-soft); }
.topo-legend .l-team i   { border-color: var(--cool); background: var(--surface-3); }
.topo-legend .l-chall i  { border-color: var(--line-strong); background: var(--surface); }
.topo-legend .l-atk i    { border-color: var(--hot); background: var(--hot-soft); border-radius: 2px; }
.topo-hint { font-size: 11px; color: var(--ink-faint); margin-top: 10px; line-height: 1.8; }
.topo-hint kbd { margin: 0 1px; }
.topo-empty {
  position: absolute; inset: 0; display: grid; place-content: center;
  color: var(--ink-faint); font-size: 12px; text-align: center; gap: 6px;
}

/* ---------- motion is decoration; drop it on request ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important; scroll-behavior: auto !important;
  }
}

@media (max-width: 720px) {
  body { padding: 14px; }
  .kv { grid-template-columns: 1fr; gap: 2px 0; }
  .kv b { margin-top: 6px; }
}
