/*
 * Hand-written rather than a vendored CSS framework.
 *
 * The design called for Pico.css, but this screen is a dense data table plus a toolbar, a drawer
 * and a few modals — almost none of what a classless framework provides survives contact with
 * that, and nearly all of it would have to be overridden. Writing what is needed is less code
 * than vendoring 80KB and fighting it, and it leaves the deployment with no third-party assets
 * at all.
 */

:root {
  --bg: #f6f7f9;
  --surface: #ffffff;
  --border: #d8dce2;
  --border-strong: #b9c0c9;
  --text: #1c2024;
  --text-muted: #6b7280;
  --accent: #2563eb;
  --accent-text: #ffffff;
  --danger: #b42318;
  --danger-bg: #fef3f2;
  --ok: #067647;
  --warn: #b54708;
  --row-hover: #f0f4fb;
  --shadow: 0 4px 16px rgba(15, 23, 42, 0.12);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171a;
    --surface: #1c2024;
    --border: #2e343b;
    --border-strong: #454d57;
    --text: #e6e8eb;
    --text-muted: #9aa3ad;
    --accent: #4f83f1;
    --accent-text: #0b1020;
    --danger: #f97066;
    --danger-bg: #2c1614;
    --ok: #47c78b;
    --warn: #f79009;
    --row-hover: #232a31;
    --shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
  }
}

* { box-sizing: border-box; }

/*
 * An app shell: the header, the toolbar and the task panel hold their place, and the table
 * scrolls inside its own box.
 *
 * Letting the document scroll instead cannot work on the horizontal axis. Every surface here is
 * as wide as the window and no wider, so scrolling sideways slid the header and the toolbar out
 * of view and left bare canvas beside them, while the task panel — fixed to the viewport — did
 * not move at all. One gesture, three different answers.
 */
html, body { height: 100%; }

body {
  margin: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/*
 * The app shell is served to anyone: static files sit in front of authentication so that
 * login.html is reachable at all. A browser with no session therefore paints the header, the
 * toolbar and the empty table, and only then does main.js ask who it is and send it to the login
 * page — a flash of an app the visitor is not signed in to. Staying blank until the answer comes
 * back is the whole fix.
 *
 * visibility rather than display: the layout is computed either way, so revealing costs no
 * reflow and nothing jumps. main.js takes the class off the moment the session is confirmed, and
 * before anything else can fail — a later error then shows the shell with an alert in it, the
 * same as it always did. If the module never runs at all the page stays blank, which is a fair
 * picture of an app whose script did not load.
 */
html.checking-session body { visibility: hidden; }

h1, h2, h3 { margin: 0; font-weight: 600; }

/* ---- layout ------------------------------------------------------------------------- */

.app-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 16px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.app-header h1 { font-size: 16px; margin-right: auto; }

/*
 * The bottom padding keeps the last rows clear of the task panel, which is pinned to the viewport
 * and so takes no space of its own. It follows the panel rather than being a constant, because a
 * fixed 220px would hold that much back from the table whether or not the panel is showing it.
 * The attribute is written by renderTaskPanel.
 */
.app-main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 12px 16px 16px;
}
body[data-task-panel="collapsed"] .app-main { padding-bottom: 64px; }
body[data-task-panel="open"] .app-main { padding-bottom: 220px; }

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-bottom: 10px;
}

.toolbar .spacer { margin-left: auto; }

/*
 * A field is a caption stacked over its control, so it stands some 20px taller than a bare button
 * beside it. Centring the row therefore aligns nothing useful — every button and the cache age
 * floated above the controls they sit next to. Aligning to the end puts them on the control row,
 * and giving the items that are not control-shaped a control's height (33px: 14px text, 5px
 * padding, 1px border) leaves them centred on it rather than a few pixels low.
 */
.toolbar { align-items: end; }

.toolbar > button, .toolbar > .age {
  display: inline-flex;
  align-items: center;
  min-height: 33px;
}

/* The rule above outranks the browser's own [hidden] { display: none }, so say it again here. */
.toolbar > [hidden] { display: none; }

.bulk-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  margin-bottom: 10px;
  background: var(--surface);
  border: 1px solid var(--accent);
  border-radius: 6px;
}

.bulk-bar .count { font-weight: 600; min-width: 170px; }
.bulk-bar .count:not(.active) { font-weight: 400; color: var(--text-muted); }

/* ---- controls ----------------------------------------------------------------------- */

button, select, input[type="text"], input[type="password"], input[type="search"], input[type="number"] {
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  padding: 5px 9px;
}

button { cursor: pointer; }
button:hover:not(:disabled) { border-color: var(--accent); }
button:disabled { opacity: 0.45; cursor: not-allowed; }

button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 600;
}

button.danger { color: var(--danger); border-color: var(--danger); }
button.danger.primary { background: var(--danger); border-color: var(--danger); color: #fff; }
button.link { border: none; background: none; color: var(--accent); padding: 2px 4px; }

label.field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--text-muted); }
label.field > select, label.field > input { color: var(--text); font-size: 14px; }

.muted { color: var(--text-muted); }
.small { font-size: 12px; }

/* ---- alerts ------------------------------------------------------------------------- */

.alerts { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.alerts:empty { display: none; }

.alert {
  padding: 8px 12px;
  border-radius: 6px;
  border: 1px solid var(--warn);
  background: var(--danger-bg);
  color: var(--text);
}

.alert.error { border-color: var(--danger); }

/* ---- table -------------------------------------------------------------------------- */

/*
 * The scroll container, on both axes. The table is the only thing on the page wide enough to
 * need one, and scoping it here is what keeps the surrounding chrome still.
 *
 * This box was deliberately not a scroll container for a long time, so that the page could be
 * the only thing that scrolled. What that bought in practice was a page that slid sideways as a
 * whole — see the note on `body`. The two objections that argued for it are both answered now:
 *
 *   - The sticky header sticks to this box rather than to the viewport. Since the box fills the
 *     window, that looks the same as it did.
 *   - A row menu near the bottom edge would be clipped rather than overflow. It opens upward
 *     instead when there is no room below — positionMenu() in instanceTable.js.
 */
.table-wrap {
  flex: 1;
  min-height: 0;
  overflow: auto;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
}

table { width: 100%; border-collapse: collapse; }

th, td {
  padding: 7px 10px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

th {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  cursor: pointer;
  user-select: none;

  position: sticky;
  top: 0;
  z-index: 1;

  /* Opaque, or the rows scrolling underneath show through. */
  background: var(--surface);
}

th.plain { cursor: default; }

/* Which column is sorted, and which way. Only the sorted one carries data-dir. */
th[data-dir]::after { margin-left: 4px; font-size: 10px; }
th[data-dir="asc"]::after { content: "▲"; }
th[data-dir="desc"]::after { content: "▼"; }
tbody tr:hover { background: var(--row-hover); }
tbody tr:last-child td { border-bottom: none; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
td.name { font-weight: 600; cursor: pointer; }
td.check, th.check { width: 1%; }

.state { display: inline-flex; align-items: center; gap: 6px; }

/*
 * line-height on the dot, not just a size. A 20px glyph left at the body's 1.45 makes a 29px line
 * box and every row in the table grows with it; pinned to 1 the box is 20px, which the 14px text
 * beside it already occupies, so the dot doubles and the row does not move.
 */
.state::before { content: "●"; font-size: 20px; line-height: 1; }
.state-running::before { color: var(--ok); }
.state-stopped::before { color: var(--text-muted); }
.state-pending::before, .state-stopping::before { color: var(--warn); }
.state-deleting::before, .state-terminated::before, .state-error::before { color: var(--danger); }
.state-unknown::before { color: var(--text-muted); }

.busy { color: var(--warn); }

/*
 * Health, drawn exactly like State above: same dot, same gap, same three colours. They answer
 * neighbouring questions — the cloud's view of the machine, and the agent's — so reading one
 * should not mean learning a second vocabulary.
 */
.health { display: inline-flex; align-items: center; gap: 6px; }
.health::before { content: "●"; font-size: 20px; line-height: 1; }
.health-healthy::before { color: var(--ok); }
.health-unhealthy::before { color: var(--danger); }
.health-unknown::before { color: var(--text-muted); }

/* Fainter than unknown: nothing was asked, as opposed to asked and not answered. */
.health-unchecked::before { color: var(--text-muted); opacity: 0.5; }
.empty { padding: 28px; text-align: center; color: var(--text-muted); }

/* ---- row menu ----------------------------------------------------------------------- */

.row-menu { position: relative; display: inline-block; }
.row-menu > summary { list-style: none; cursor: pointer; padding: 2px 8px; border-radius: 4px; }
.row-menu > summary::-webkit-details-marker { display: none; }
.row-menu > summary:hover { background: var(--border); }

.row-menu .menu {
  position: absolute;
  right: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  min-width: 170px;
  padding: 4px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  box-shadow: var(--shadow);
}

/* Set by positionMenu() when the menu would not fit below the row. */
.row-menu[data-drop="up"] .menu { bottom: 100%; margin-bottom: 4px; }

.row-menu .menu button {
  border: none;
  background: none;
  text-align: left;
  padding: 6px 10px;
  border-radius: 4px;
}

.row-menu .menu button:hover:not(:disabled) { background: var(--row-hover); }

/* ---- task panel --------------------------------------------------------------------- */

.tasks {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 45vh;
  overflow-y: auto;
  background: var(--surface);
  border-top: 1px solid var(--border);
  padding: 8px 16px;
}

.tasks header { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
.tasks header h2 { font-size: 13px; text-transform: uppercase; color: var(--text-muted); }
.task { display: flex; align-items: center; gap: 10px; padding: 4px 0; border-top: 1px solid var(--border); }
.task .what { font-weight: 600; min-width: 210px; }
.task .progress { flex: 1; color: var(--text-muted); }
.task.failed .progress { color: var(--danger); }
.task.succeeded .progress { color: var(--ok); }

/* ---- dialogs ------------------------------------------------------------------------ */

dialog {
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  padding: 0;
  max-width: 720px;
  width: calc(100% - 32px);
  box-shadow: var(--shadow);
}

dialog::backdrop { background: rgba(15, 23, 42, 0.45); }
dialog .body { padding: 16px; max-height: 70vh; overflow-y: auto; }
dialog .head { padding: 12px 16px; border-bottom: 1px solid var(--border); font-weight: 600; }
dialog .foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

dialog .foot .spacer { margin-right: auto; }

.form-grid { display: grid; grid-template-columns: 1fr; gap: 12px; }
.form-grid select, .form-grid input { width: 100%; }

/* The template's own fields. Empty for most templates, and then it must not leave a gap behind. */
.params { display: grid; gap: 12px; }
.params:empty { display: none; }

.resolved {
  margin-top: 14px;
  padding: 10px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: 6px;
  background: var(--bg);
}

.resolved h3 { font-size: 12px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 6px; }
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 14px; font-size: 13px; }
.kv dt { color: var(--text-muted); }
.kv dd { margin: 0; word-break: break-all; }

pre.code {
  margin: 10px 0 0;
  padding: 10px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow-x: auto;
  font: 12px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace;
  white-space: pre;
}

.warning {
  margin-top: 10px;
  padding: 8px 12px;
  border-left: 3px solid var(--warn);
  background: var(--danger-bg);
  border-radius: 0 4px 4px 0;
}

/* ---- detail drawer ------------------------------------------------------------------ */

dialog.drawer {
  margin-left: auto;
  margin-right: 0;
  height: 100%;
  max-height: 100%;
  width: min(520px, 100%);
  border-radius: 0;
  border-right: none;
}

/*
 * Scoped to [open] on purpose: `display` on a dialog is what the UA stylesheet uses to hide a
 * closed one, so an unscoped `display: flex` here would leave the drawer permanently on screen.
 *
 * The point of the flex column is the body: the shared dialog rule caps it at 70vh, which in a
 * full-height drawer means a scrollbar with a hand's width of unused space below it. Here it takes
 * whatever is left instead, and only scrolls once the content genuinely does not fit.
 */
dialog.drawer[open] {
  display: flex;
  flex-direction: column;
}

dialog.drawer .head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

dialog.drawer .body {
  max-height: none;
  flex: 1;
  min-height: 0;
}

.head .close {
  padding: 2px 8px;
  line-height: 1.2;
  border-radius: 4px;
}

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

body.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
}

.login-card {
  width: min(360px, calc(100% - 32px));
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow);
}

.login-card h1 { font-size: 18px; margin-bottom: 4px; }
.login-card form { display: flex; flex-direction: column; gap: 12px; margin-top: 18px; }
.login-card input { width: 100%; padding: 8px 10px; }
.login-card .error { color: var(--danger); min-height: 20px; font-size: 13px; }
