/* Dashboard styling.

   Theme is three settings and two appearances. "Light" and "dark" are pinned by
   the user and remembered across sessions; the default, "system", follows the
   OS. theme.js resolves whichever is in force to a concrete value on
   <html data-theme> before first paint, which is why there are no
   prefers-color-scheme queries below: a media query plus a pinned override
   means writing each palette twice, and two copies of a colour set are two
   things to keep in step.

   The bare :root is the dark palette rather than a neutral base, so a render
   that somehow beats theme.js still looks like the app instead of flashing
   white. Colours are tokens so both appearances stay in step. */

:root {
  color-scheme: dark;
  --bg: #0d0f12;
  --surface: #16191e;
  --surface-2: #1d2127;
  --border: #2a2f37;
  --text: #e6e8eb;
  --muted: #8b929c;
  --accent: #6ea8fe;
  /* Text on an accent-filled control. The accent is light in this theme and
     dark in the other, so a fixed label colour fails one of them. */
  --on-accent: #06101f;
  --ok: #3fb950;
  --warn: #d29922;
  --crit: #f85149;
  --radius: 10px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Chart palette. The two appearances are selected steps of the same hues, not
     an automatic flip -- a light-mode series colour on a dark surface either
     vanishes or glares. Validated for the lightness band, chroma floor, adjacent
     CVD separation, normal-vision separation, and contrast against these exact
     surfaces. Light aqua and yellow sit below 3:1, so every chart carries direct
     labels and a table view rather than relying on hue alone. */
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --grid: #23272e;
  --axis: #333944;
}

/* The status colours are restated darker here, and it is not a stylistic
   choice. The dark-theme green and amber measure 2.54:1 and 2.52:1 against a
   white surface -- unreadable, and previously easy to miss because light was
   reachable only by changing your OS. Now that it is one click away it has to
   hold up. These are the same hues stepped down until each clears 4.5:1 on both
   light surfaces; contrast is symmetric, so they also carry white text when
   used as a button or badge fill. */
:root[data-theme="light"] {
  color-scheme: light;
  --bg: #f6f7f9;
  --surface: #ffffff;
  --surface-2: #f0f2f5;
  --border: #d8dce2;
  --text: #12151a;
  --muted: #5c636d;
  --accent: #0b62d6;
  --on-accent: #ffffff;
  --ok: #1a7f37;
  --warn: #9a6700;
  --crit: #cf222e;

  --series-1: #2a78d6;
  --series-2: #eb6834;
  --series-3: #1baf7a;
  --series-4: #eda100;
  --grid: #e6e8ec;
  --axis: #c8ccd3;
}

* { box-sizing: border-box; }

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so any
   author rule that sets `display` silently beats it. `.login-wrap` sets
   `display: grid`, which left the hidden login screen laid out below the app --
   invisible until you scrolled. Assert it once, globally, rather than trying to
   remember which components happen to set display. */
[hidden] { display: none !important; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

h1, h2, h3 { margin: 0 0 .4rem; font-weight: 600; line-height: 1.25; }
.muted { color: var(--muted); font-size: .875rem; }
.error { color: var(--crit); font-size: .875rem; }
.mono { font-family: var(--mono); }

/* ---------- top bar ---------- */

.topbar {
  display: flex; align-items: center; gap: 1rem;
  padding: .6rem 1rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 10;
}
.brand { font-weight: 700; letter-spacing: .02em; }
.tabs { display: flex; gap: .25rem; flex: 1; }
.topbar-right { display: flex; align-items: center; gap: .5rem; }

.tab {
  background: none; border: 0; color: var(--muted);
  padding: .4rem .7rem; border-radius: var(--radius);
  font: inherit; cursor: pointer;
}
.tab:hover { background: var(--surface-2); color: var(--text); }
.tab[aria-current="page"] { background: var(--surface-2); color: var(--text); font-weight: 600; }

.icon-btn {
  background: none; border: 1px solid var(--border); color: var(--muted);
  width: 2rem; height: 2rem; border-radius: var(--radius);
  cursor: pointer; font-size: 1rem; line-height: 1;
}
.icon-btn:hover { color: var(--text); border-color: var(--muted); }

main { padding: 1rem; max-width: 1100px; margin: 0 auto; }

/* ---------- stat tiles ---------- */

.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; margin-bottom: 1rem; }
@media (max-width: 640px) { .stats { grid-template-columns: repeat(2, 1fr); } }

.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .8rem 1rem;
}
.stat .n { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.stat .l { color: var(--muted); font-size: .8rem; text-transform: uppercase; letter-spacing: .04em; }
.stat.ok .n { color: var(--ok); }
.stat.warn .n { color: var(--warn); }
.stat.crit .n { color: var(--crit); }

/* ---------- lists ---------- */

.toolbar { display: flex; gap: .5rem; margin-bottom: .75rem; }
input[type="search"], input[type="text"], input[type="email"], input[type="password"], input:not([type]), select, textarea {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
  padding: .5rem .7rem; font: inherit; width: 100%;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .8rem 1rem; margin-bottom: .6rem;
}
.row { display: flex; align-items: center; gap: .8rem; }
.row .grow { flex: 1; min-width: 0; }
.row .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.host { cursor: pointer; }
.host:hover { border-color: var(--muted); }

.dot { width: .6rem; height: .6rem; border-radius: 50%; flex: none; }
.dot.online { background: var(--ok); }
.dot.offline { background: var(--muted); }
.dot.degraded { background: var(--warn); }
.dot.pending { background: var(--accent); }

.metric { text-align: right; }
.metric .v { font-weight: 600; }
.metric .l { color: var(--muted); font-size: .7rem; text-transform: uppercase; }

/* ---------- buttons ---------- */

button.primary, button.danger, button.ghost, button.small {
  font: inherit; border-radius: var(--radius); cursor: pointer;
  padding: .45rem .8rem; border: 1px solid var(--border);
}
button.primary { background: var(--accent); color: var(--on-accent); border-color: transparent; font-weight: 600; }
button.danger  { background: var(--crit); color: #fff; border-color: transparent; font-weight: 600; }
button.ghost   { background: transparent; color: var(--text); }
button.small   { background: var(--surface-2); color: var(--text); padding: .25rem .55rem; font-size: .8rem; }
button:disabled { opacity: .5; cursor: not-allowed; }

.actions { display: flex; gap: .35rem; flex: none; }

/* ---------- state ---------- */

.state { font-size: .8rem; padding: .1rem .45rem; border-radius: 999px; border: 1px solid var(--border); }
.state.running { color: var(--ok); border-color: color-mix(in oklab, var(--ok) 40%, transparent); }
.state.failed  { color: var(--crit); border-color: color-mix(in oklab, var(--crit) 40%, transparent); }
.state.stopped { color: var(--muted); }

/* ---------- detail ---------- */

.subtabs { display: flex; gap: .25rem; margin: .75rem 0; border-bottom: 1px solid var(--border); }
.subtab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  color: var(--muted); padding: .5rem .8rem; font: inherit; cursor: pointer;
}
.subtab[aria-current="true"] { color: var(--text); border-bottom-color: var(--accent); font-weight: 600; }

.kv { display: grid; grid-template-columns: max-content 1fr; gap: .35rem 1rem; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; }

pre.console {
  background: #06080b; color: #d6dae0; border: 1px solid var(--border);
  border-radius: var(--radius); padding: .8rem; margin: 0;
  font-family: var(--mono); font-size: .82rem; line-height: 1.45;
  max-height: 26rem; overflow: auto; white-space: pre-wrap; word-break: break-word;
}
pre.console .err { color: #ff9492; }

.console-form { display: flex; gap: .5rem; margin-bottom: .6rem; }
.console-form input { font-family: var(--mono); }

/* ---------- login ---------- */

.login-wrap { min-height: 100dvh; display: grid; place-items: center; padding: 1rem; }
.login-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 14px; padding: 1.6rem; width: min(24rem, 100%);
  display: grid; gap: .7rem;
}
.login-card h1 { text-align: center; font-size: 1.8rem; margin: 0; }
.login-card > p { text-align: center; margin: 0 0 .4rem; }
label { display: grid; gap: .25rem; font-size: .85rem; color: var(--muted); }

/* ---------- misc ---------- */

.badge {
  background: var(--crit); color: #fff; border-radius: 999px;
  padding: 0 .4rem; font-size: .72rem; font-weight: 700;
}
.toast {
  position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .6rem 1rem; z-index: 50;
  max-width: min(30rem, 92vw);
}
.toast.bad { border-color: var(--crit); color: var(--crit); }

dialog {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: 14px;
  padding: 1.2rem; width: min(24rem, 92vw);
}
dialog::backdrop { background: rgba(0,0,0,.6); }
dialog menu { display: flex; gap: .5rem; justify-content: flex-end; margin: 1rem 0 0; padding: 0; }
dialog form { display: grid; gap: .6rem; }

.empty { text-align: center; color: var(--muted); padding: 2.5rem 1rem; }
.spinner { text-align: center; color: var(--muted); padding: 2rem; }


/* ---------- charts ---------- */

.chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr)); gap: .75rem; }

/* Height grows with content so the x-axis band is never cropped into a nested
   scrollbar. */
.chart-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .8rem 1rem 1rem; }
.chart-head { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .1rem; }
.chart-head h3 { margin: 0; font-size: .95rem; }
.chart-head .now { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; }
.chart-sub { color: var(--muted); font-size: .78rem; margin: 0 0 .5rem; }

/* Held at reduced opacity while refetching rather than replaced by a skeleton,
   so nothing jumps. */
.chart-card.stale { opacity: .55; transition: opacity .15s; }

.chart svg { display: block; width: 100%; height: auto; }
.chart .grid line { stroke: var(--grid); stroke-width: 1; }
.chart .axis line { stroke: var(--axis); stroke-width: 1; }
.chart text { fill: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; }
.chart .series-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart .end-label { font-size: 10px; font-weight: 600; }
.chart .crosshair { stroke: var(--axis); stroke-width: 1; }
.chart .marker { stroke: var(--surface); stroke-width: 2; }

.chart-legend { display: flex; flex-wrap: wrap; gap: .1rem .9rem; margin-top: .45rem; }
.chart-legend span { display: inline-flex; align-items: center; gap: .35rem; font-size: .78rem; color: var(--muted); }
.chart-legend i { width: .65rem; height: .65rem; border-radius: 2px; display: inline-block; }

.chart-tip {
  position: absolute; pointer-events: none; z-index: 20;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 8px; padding: .4rem .6rem; font-size: .78rem;
  white-space: nowrap; box-shadow: 0 4px 14px rgba(0,0,0,.25);
}
.chart-tip .k { color: var(--muted); }
.chart-tip .v { font-variant-numeric: tabular-nums; font-weight: 600; }

.chart-table { width: 100%; border-collapse: collapse; font-size: .78rem; margin-top: .5rem; }
.chart-table th, .chart-table td { text-align: right; padding: .2rem .4rem; border-bottom: 1px solid var(--border); font-variant-numeric: tabular-nums; }
.chart-table th:first-child, .chart-table td:first-child { text-align: left; }
.chart-empty { color: var(--muted); font-size: .8rem; padding: 2rem 0; text-align: center; }


/* ---------- patching ---------- */

/* A plain table rather than the card rows used elsewhere: this view exists to
   be scanned down a column -- which hosts are behind, which have security
   updates pending -- and cards make column comparison impossible. */
.data-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.data-table th, .data-table td {
  text-align: left; padding: .4rem .5rem;
  border-bottom: 1px solid var(--border); vertical-align: middle;
}
.data-table th { color: var(--muted); font-weight: 500; font-size: .78rem; white-space: nowrap; }
.data-table td.num, .data-table th.num { text-align: right; font-variant-numeric: tabular-nums; }
.data-table tbody tr:hover { background: var(--surface-2); }
.data-table input[type="checkbox"] { width: auto; margin: 0; }
.data-table .host-link { color: var(--accent); cursor: pointer; text-decoration: none; }
.data-table .host-link:hover { text-decoration: underline; }

/* Wide tables scroll inside their own card rather than pushing the page
   sideways. */
.table-wrap { overflow-x: auto; }

.tag {
  display: inline-block; font-size: .72rem; padding: .05rem .4rem;
  border-radius: 999px; border: 1px solid var(--border); color: var(--muted);
  white-space: nowrap;
}
.tag.warn { color: var(--warn); border-color: color-mix(in oklab, var(--warn) 40%, transparent); }
.tag.crit { color: var(--crit); border-color: color-mix(in oklab, var(--crit) 40%, transparent); }
.tag.ok   { color: var(--ok); border-color: color-mix(in oklab, var(--ok) 40%, transparent); }

/* The bar stays put while a long host list scrolls past it, because the
   selection it acts on is made further down. */
.action-bar {
  display: flex; gap: .4rem; flex-wrap: wrap; align-items: center;
  padding: .5rem 0; position: sticky; top: 3.1rem; z-index: 5;
  background: var(--bg); border-bottom: 1px solid var(--border); margin-bottom: .5rem;
}
.action-bar .count { color: var(--muted); font-size: .8rem; margin-right: auto; }

.form-grid { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
.form-grid .wide { grid-column: 1 / -1; }

.pkg-list { max-height: 26rem; overflow-y: auto; }

/* ---------- patch run progress ---------- */

/* Wider than the confirm dialog it shares a tag with: this one carries terminal
   output, and wrapping apt output into a 24rem column makes it unreadable. */
.run-modal { width: min(64rem, 96vw); max-height: 88vh; overflow: auto; }
.run-modal-head { align-items: baseline; }
.run-modal-head h2 { margin: 0; font-size: 1.05rem; }
.run-summary { font-size: .85rem; margin: .5rem 0 .8rem; }

.run-list { display: grid; gap: .6rem; }
.run {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-2); padding: .6rem .7rem;
}
.run-head { gap: .5rem; align-items: center; }
.run-host { font-weight: 600; }
.run-sub { font-size: .8rem; }
/* Tabular figures so an elapsed counter does not jitter its neighbours as the
   digits change. */
.run-time { font-size: .8rem; font-variant-numeric: tabular-nums; }
.run-body { margin-top: .6rem; }
.run-console { max-height: 18rem; }

.prog {
  position: relative; height: .45rem; background: var(--border);
  border-radius: 999px; overflow: hidden; margin-bottom: .5rem;
}
.prog-bar {
  height: 100%; width: 0; background: var(--accent);
  border-radius: inherit; transition: width .4s ease;
}
.prog-bar.ok   { background: var(--ok); }
.prog-bar.crit { background: var(--crit); }

/* Indeterminate means the host is working and has not said how much work there
   is -- an index refresh, or a manager that never declares a total. A bar
   sitting at 0% would read as stalled, and inventing a percentage to avoid that
   is worse than admitting the number is unknown. */
.prog.indeterminate::after {
  content: ""; position: absolute; inset: 0; width: 35%; border-radius: inherit;
  background: linear-gradient(90deg, transparent,
    color-mix(in oklab, var(--accent) 70%, transparent), transparent);
  animation: run-slide 1.4s ease-in-out infinite;
}
@keyframes run-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(320%); }
}

.run-steps {
  list-style: none; margin: 0 0 .6rem; padding: 0;
  display: grid; gap: .25rem; font-size: .82rem;
}
.run-step { display: flex; align-items: center; gap: .45rem; color: var(--muted); }
.run-step-dot {
  width: .5rem; height: .5rem; border-radius: 50%;
  background: var(--border); flex: none;
}
.run-step.running { color: var(--text); }
.run-step.running .run-step-dot { background: var(--accent); animation: run-pulse 1.2s ease-in-out infinite; }
.run-step.done   .run-step-dot { background: var(--ok); }
.run-step.warned .run-step-dot { background: var(--warn); }
.run-step.failed .run-step-dot { background: var(--crit); }
.run-step.failed { color: var(--crit); }
@keyframes run-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* Both animations here are ambient progress hints rather than information, so
   they are the first thing to drop. State is still carried by colour and text. */
@media (prefers-reduced-motion: reduce) {
  .prog.indeterminate::after { animation: none; opacity: .45; }
  .run-step.running .run-step-dot { animation: none; }
  .prog-bar { transition: none; }
}

/* ---------- asset editing and device enrollment ---------- */

/* A row that is a control has to look and behave like one. */
tr.clickable { cursor: pointer; }
tr.clickable:hover { background: var(--surface-2); }
tr.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.asset-dialog { width: min(30rem, 94vw); }
.asset-dialog textarea {
  font: inherit; width: 100%; background: var(--surface-2); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius); padding: .45rem .6rem;
}

.enroll-result { margin-top: 1rem; border-top: 1px solid var(--border); padding-top: .8rem; }

.enroll-grid { display: flex; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
/* White plate behind the code: a QR inverted by a dark theme will not scan on
   many readers, and the one that fails is on a handset mid-factory-reset. */
.enroll-qr {
  width: 220px; height: 220px; background: #fff; padding: 10px;
  border-radius: var(--radius); flex: none;
}
.enroll-steps { margin: 0; padding-left: 1.1rem; color: var(--muted); font-size: .875rem; }
.enroll-steps li { margin-bottom: .35rem; }

.enroll-row { gap: .5rem; margin: .35rem 0; }
.enroll-label { flex: none; width: 4.5rem; }
.enroll-value {
  flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap;
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .35rem .5rem;
  font-family: var(--mono); font-size: .8rem;
}
.enroll-missing {
  border: 1px solid color-mix(in oklab, var(--warn) 45%, transparent);
  border-radius: var(--radius); padding: .6rem .8rem; margin: .8rem 0;
  font-size: .875rem;
}
.enroll-missing ul { margin: .3rem 0 0; padding-left: 1.1rem; }
