/* styles.css — Nyxup hub, dark ops aesthetic. v0.4.0 */
/* First-party only: system font stacks, no imports, no external assets. */

:root {
  /* This app is dark-only — there is no light theme to switch to. Without this
     declaration the browser still assumes LIGHT for everything it draws itself, and
     paints them from the light palette on top of our dark one: scrollbar tracks come out
     white, and any UA background that resolves to the `Canvas` system colour (notably
     inside scroll containers and the top-layer <dialog>) renders white with black text.
     That is the white table area in the Job Details modal. One declaration fixes the whole
     class — scrollbars, form controls, autofill, spinners, date pickers and text-selection
     colours all follow it. */
  color-scheme: dark;

  --bg: #0a0d12;
  --bg-raise: #10151d;
  --bg-panel: #141a24;
  --bg-input: #0d1219;
  --border: #232c3a;
  --border-strong: #31405a;
  --text: #dbe4f0;
  --text-dim: #8494ab;
  --accent: #8b5cf6;
  --accent-soft: rgba(139, 92, 246, .16);
  --ok: #3fd68f;
  --warn: #ffb454;
  --err: #ff5d5d;
  --sans: -apple-system, "Segoe UI", system-ui, Roboto, "Helvetica Neue", sans-serif;
  --mono: ui-monospace, "Cascadia Code", Consolas, "SF Mono", monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.45;
}

.hidden { display: none !important; }
.mono { font-family: var(--mono); }
.dim { color: var(--text-dim); }

h1, h2 { margin: 0; font-weight: 600; letter-spacing: 0.01em; }
h1.page-title { font-size: 1.25rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--text); }
h2 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); }

/* ---------------- chrome ---------------- */

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 1.4rem;
  background: var(--bg-raise);
  border-bottom: 1px solid var(--border);
  box-shadow: inset 0 2px 0 var(--accent);
}

.topnav { display: flex; align-items: center; gap: 0.8rem; }
.pilot-name { font-size: 0.8rem; color: var(--text-dim); }

.wordmark {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 1.6rem;
  letter-spacing: 0.14em;
  color: var(--text);
}
.wordmark span { color: var(--accent); font-weight: 400; }
.wordmark.sm { font-size: 1.05rem; }

.container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 1.4rem;
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
}

.page-head { margin-top: 0.2rem; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

.grid2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.1rem;
}
@media (max-width: 820px) { .grid2 { grid-template-columns: 1fr; } }

.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1.1rem 1.2rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.hint { margin: 0; font-size: 0.82rem; color: var(--text-dim); }

.banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 1rem;
  border: 1px solid var(--warn);
  border-left-width: 4px;
  border-radius: 6px;
  background: rgba(255, 180, 84, 0.08);
  font-size: 0.88rem;
}

/* ---------------- forms + buttons ---------------- */

label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.8rem; color: var(--text-dim); }

input[type="text"], input[type="email"], input[type="password"], select {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  padding: 0.5rem 0.6rem;
  font-size: 0.9rem;
  font-family: var(--sans);
}
input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }

.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; }
.stack-form { display: flex; flex-direction: column; gap: 0.7rem; }

.btn {
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  color: var(--text);
  padding: 0.5rem 1rem;
  font-size: 0.85rem;
  font-family: var(--sans);
  cursor: pointer;
  transition: background 120ms, border-color 120ms;
}
.btn:hover { border-color: var(--accent); background: var(--accent-soft); }
.btn:disabled { opacity: 0.55; cursor: default; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #06111f; font-weight: 600; }
.btn-primary:hover { background: #a78bfa; }
.btn-ghost { background: transparent; }
.btn-warn { border-color: var(--warn); color: var(--warn); background: transparent; }
.btn-warn:hover { background: rgba(255, 180, 84, 0.12); border-color: var(--warn); }
.btn-sm { padding: 0.3rem 0.7rem; font-size: 0.78rem; }
.btn-block { width: 100%; }

.conc-label {
  flex-direction: row;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.conc-label select { width: 4.2rem; }

/* ---------------- dropzones ---------------- */

.dropzone {
  border: 2px dashed var(--border-strong);
  border-radius: 8px;
  padding: 2rem 1rem;
  text-align: center;
  cursor: pointer;
  transition: border-color 120ms, background 120ms;
  user-select: none;
}
.dropzone:hover, .dropzone.drag { border-color: var(--accent); background: var(--accent-soft); }
.dz-main { font-size: 1rem; font-weight: 600; letter-spacing: 0.03em; }
.dz-sub { margin-top: 0.35rem; font-size: 0.8rem; color: var(--text-dim); }
.dz-sub a { color: var(--accent); text-decoration: none; }
.dz-sub a:hover { text-decoration: underline; }

/* ---------------- progress ---------------- */

.bar {
  height: 14px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 7px;
  overflow: hidden;
}
.bar-sm { height: 8px; border-radius: 4px; }
.bar-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #2f6fce, var(--accent));
  transition: width 300ms linear;
}

.stats { display: flex; flex-wrap: wrap; gap: 1.4rem; font-size: 0.85rem; }
.stat .dim { text-transform: uppercase; font-size: 0.7rem; letter-spacing: 0.08em; margin-right: 0.25rem; }
.stat b { font-weight: 600; color: var(--text); }

.file-list {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  max-height: 320px;
  overflow-y: auto;
  padding-right: 0.3rem;
}
.file-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 160px 130px;
  align-items: center;
  gap: 0.8rem;
  padding: 0.25rem 0.4rem;
  border-radius: 4px;
  font-size: 0.8rem;
}
.file-row:nth-child(odd) { background: rgba(255, 255, 255, 0.02); }
.file-name { font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-status { font-family: var(--mono); color: var(--text-dim); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.file-row[data-status="done"] .file-status { color: var(--ok); }
.file-row[data-status="error"] .file-status { color: var(--err); }
.file-row[data-status="uploading"] .file-status { color: var(--accent); }

/* ---------------- tables ---------------- */

/* Table state rows — loading / empty / error. One visual language so a stalled request
   never reads the same as a finished empty one. The left rule carries the meaning:
   dim for pending, red for failed, nothing for a genuine empty result. */
.state-cell {
  text-align: center;
  padding: 1.1rem 1rem;
  color: var(--text-dim);
}
.state-cell .btn { margin-left: 0.7rem; }
.state-loading { color: var(--text-dim); }
.state-loading::before {
  content: "";
  display: inline-block;
  width: 0.7em; height: 0.7em;
  margin-right: 0.5rem;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: -1px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* A spinner that never stops is the thing being fixed here — respect the OS setting and
   fall back to a static ring rather than animating for people who asked us not to. */
@media (prefers-reduced-motion: reduce) {
  .state-loading::before { animation: none; }
}
.state-error { color: var(--err); }

/* Wide dialog: the job drill-in carries a full file path plus two 16-char hashes. At the
   default 640px those columns are unreadable and the whole table scrolls sideways. */
dialog.card.wide { max-width: min(1400px, 96vw); }

/* Explicit grounds as well as `color-scheme: dark`, not instead of it. These are scroll
   containers inside a top-layer <dialog>; leaving them transparent means the only thing
   between the text and a UA-painted surface is inheritance, and anything that resolves to
   a system colour lands white. Cheap insurance on a dark-only app. */
.table-wrap { overflow-x: auto; background: var(--bg-panel); }

/* Cap a long table at N rows and scroll the rest. Sized in rem from the row box
   (0.45rem padding top and bottom + ~1.2rem line) rather than a pixel guess, so it still
   shows exactly N rows if the base font size changes. `--rows` is the only knob. */
.table-wrap.rows-6  { --rows: 6;  max-height: calc((var(--rows) + 1) * 2.1rem + 0.5rem); overflow-y: auto; }
.table-wrap.rows-10 { --rows: 10; max-height: calc((var(--rows) + 1) * 2.1rem + 0.5rem); overflow-y: auto; }

/* Job drill-in header. The full UUID used to be the first thing on the line, 36 characters
   of noise before anything a person came to read. Now the facts lead and the id is a
   click-to-copy chip at the end — present when you need it to grep a log, silent otherwise. */
.job-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem 0.9rem;
  margin: 0.2rem 0 0.9rem;
  color: var(--text);
  font-size: 0.92rem;
}
.job-meta .k { color: var(--text-dim); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; margin-right: 0.3rem; }
.job-meta .v { font-weight: 600; }
.job-id-chip {
  font-family: var(--mono);
  font-size: 0.78rem;
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: 0.15rem 0.45rem;
  color: var(--text-dim);
  cursor: pointer;
}
.job-id-chip:hover { color: var(--text); border-color: var(--accent); }

/* Keep the header visible while the body scrolls — without it the columns are unlabelled
   the moment you scroll, which is worse than no scrolling at all. */
.table.sticky-head thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--bg-panel); /* opaque: rows must not show through the header */
}
.table { width: 100%; border-collapse: collapse; font-size: 0.84rem; background: var(--bg-panel); color: var(--text); }
.table th {
  text-align: left;
  padding: 0.45rem 0.6rem;
  color: var(--text-dim);
  text-transform: uppercase;
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  border-bottom: 1px solid var(--border-strong);
}
.table td { padding: 0.45rem 0.6rem; border-bottom: 1px solid var(--border); }
.table tr:hover td { background: rgba(255, 255, 255, 0.02); }
.table .empty { color: var(--text-dim); text-align: center; padding: 1rem; }

/* clickable Job id — opens the admin drill-in modal */
.row-id-btn {
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  font-family: var(--mono);
  color: var(--accent);
  text-decoration: underline dotted;
  cursor: pointer;
}
.row-id-btn:hover { color: #a78bfa; }

/* THE ONE THAT BROKE EVERY DIALOG IN THIS APP.
   `.card` sets `display: flex`, and every dialog here is <dialog class="card">. The browser
   hides a closed dialog with `dialog:not([open]) { display: none }` — but that lives in the
   USER-AGENT stylesheet, and author styles beat the UA sheet outright, specificity aside.
   So `.card`'s display:flex overrode it and EVERY dialog rendered inline in the page,
   permanently, open or closed.
   That single line produced all of it: dialogs that looked "already open", a Close button
   that appeared dead (close() clears [open], but display:flex kept it on screen), an empty
   claim-code box sitting in the page, a Job Details panel stuck on its static "Loading…"
   rows because nothing had ever opened it, and two dialogs looking simultaneously open.
   Restoring the closed-state rule at author level is the whole fix. */
dialog.card:not([open]) { display: none; }
dialog.card { max-width: 640px; width: calc(100% - 2rem); }
dialog.card::backdrop { background: rgba(4, 7, 12, 0.65); }

/* mission status pills — one class per pipeline state */
.pill {
  display: inline-block;
  padding: 0.1rem 0.55rem;
  border-radius: 10px;
  font-size: 0.72rem;
  font-family: var(--mono);
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
}
.pill.st-uploading { color: var(--accent); border-color: var(--accent); }
.pill.st-complete, .pill.st-hashing, .pill.st-moving { color: var(--warn); border-color: var(--warn); }
.pill.st-verified, .pill.st-notified { color: var(--ok); border-color: var(--ok); }
.pill.st-purged { color: var(--text-dim); }
.pill.st-failed { color: var(--err); border-color: var(--err); }

/* ---------------- toasts ---------------- */

.toasts {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: 100;
  max-width: 380px;
}
.toast {
  background: var(--bg-raise);
  border: 1px solid var(--border-strong);
  border-left-width: 4px;
  border-radius: 6px;
  padding: 0.7rem 0.9rem;
  font-size: 0.85rem;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
  cursor: pointer;
}
.toast.ok { border-left-color: var(--ok); }
.toast.warn { border-left-color: var(--warn); }
.toast.err { border-left-color: var(--err); }

/* ---------------- login ---------------- */

.login-body { display: flex; min-height: 100vh; }
.login-wrap {
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  width: 100%;
  max-width: 380px;
}
.login-card { width: 100%; padding: 1.6rem; gap: 0.4rem; }
.login-card .tagline { margin: 0 0 0.8rem; font-size: 0.8rem; color: var(--text-dim); }
.login-card form { display: flex; flex-direction: column; gap: 0.7rem; }
.login-foot { font-size: 0.72rem; color: var(--text-dim); }
.form-error {
  color: var(--err);
  font-size: 0.82rem;
  border: 1px solid var(--err);
  border-radius: 5px;
  padding: 0.45rem 0.6rem;
  background: rgba(255, 93, 93, 0.08);
}

/* admin layout tweak: form narrower than table */
.admin-grid { grid-template-columns: minmax(280px, 1fr) 1.6fr; }
@media (max-width: 900px) { .admin-grid { grid-template-columns: 1fr; } }

/* ---------------- device authorization ("My Computers") ---------------- */

/* Reuses the palette's existing tokens: --border for hairlines, --warn for the
   expiry-warning accent, --bg-input/--border-strong/--text-dim for the badge chip —
   no new custom properties needed. */
/* The one-time code is shown ONCE and the server keeps only its hash, so the sentence
   telling a pilot to write it down is the most consequential text in the product. It was
   marked `.hint` — 0.82rem at --text-dim, i.e. the styling reserved for text you may
   safely ignore. Full-size --text with a --warn rule instead: it has to out-rank the
   surrounding copy, not hide under it. */
.callout-warn {
  margin: 0;
  color: var(--text);
  background: rgba(255, 180, 84, 0.09);
  border-left: 3px solid var(--warn);
  border-radius: 0 4px 4px 0;
  padding: 0.6rem 0.8rem;
  line-height: 1.5;
}

/* Boxed so it reads as a value to copy rather than a line of prose, and so the accent
   colour sits on --bg-input (a darker ground) where it carries more contrast. */
.claim-code {
  font-size: 1.6rem;
  letter-spacing: 0.1em;
  color: var(--accent);
  background: var(--bg-input);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  padding: 0.7rem 0.9rem;
  margin: 0.2rem 0;
  text-align: center;
  overflow-wrap: anywhere;
  user-select: all; /* one triple-click selects the whole code */
}

/* Dialog body copy is instruction, not decoration — keep it at full contrast.
   `.hint` inside a dialog is still allowed, but it no longer drops to --text-dim. */
dialog.card p { color: var(--text); line-height: 1.5; }
dialog.card p.hint { color: var(--text-dim); }
.device-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.8rem;
  align-items: baseline;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--border);
}
.device-row:last-child { border-bottom: none; }
.device-row[data-expiry="warning"] { border-left: 3px solid var(--warn); padding-left: 0.6rem; }
/* De-emphasised, but a pilot still has to READ why it is dead. 0.55 over --text-dim
   dropped the reason text below legibility on this palette. */
.device-row[data-expiry="expired"] { opacity: 0.78; }

/* The sentence explaining why "Authorize a computer" is disabled. A greyed-out control
   with an unreadable explanation is the support call the explanation exists to prevent,
   so this is --text at full size, not `.hint`. */
.blocked-reason { color: var(--text); font-size: 0.9rem; }
.device-badge {
  font-size: 0.72rem;
  font-family: var(--mono);
  padding: 0.1rem 0.45rem;
  border-radius: 10px;
  border: 1px solid var(--border-strong);
  color: var(--text-dim);
  background: var(--bg-input);
}
