/*
 * The SSR UI. Tokens first, then base, then components - nothing below the token block
 * hard-codes a colour or a spacing value. Split this file past ~600 lines.
 */

:root {
  color-scheme: light dark;
  --bg:#f6f7f9; --surface:#fff; --surface-2:#eef0f3; --text:#161a20; --muted:#5d6672; --border:#e4e7ec;
  --accent:#2563eb; --accent-soft:#eaf1ff;
  --ok:#16a34a; --warn:#d97706; --danger:#dc2626;
  --ok-soft:#e7f7ec; --warn-soft:#fdf3e2; --danger-soft:#fdecec;
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px;
  --fs-sm:13px; --fs:14px; --fs-lg:18px; --fs-xl:24px;
  --radius:8px; --shadow:0 1px 2px rgb(16 24 40 / .06), 0 1px 3px rgb(16 24 40 / .1);
  --sidebar-w:220px; --content-w:1200px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg:#0f1115; --surface:#161a20; --surface-2:#1c2128; --text:#e8eaed; --muted:#9aa4b2; --border:#252b34;
    --accent-soft:rgb(37 99 235 / .2);
    --ok-soft:rgb(22 163 74 / .2); --warn-soft:rgb(217 119 6 / .2); --danger-soft:rgb(220 38 38 / .2);
  }
}

/* base */

*, *::before, *::after { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: var(--fs)/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

a { color: var(--accent); }

h1, h2 { margin: 0 0 var(--s4); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* shell - sidebar + content, sticky sidebar, no layout JS */

.shell { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); min-height: 100vh; }

.sidebar {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
  background: var(--surface);
  border-right: 1px solid var(--border);
}

.sidebar__brand { font-weight: 600; margin-bottom: var(--s3); color: inherit; text-decoration: none; }

.sidebar__nav { display: flex; flex-direction: column; gap: var(--s1); }

/* Group labels, vd_demo's menu-title / Filament's navigation groups: one flat list of 11
   links scans as a wall. Uppercase + muted + letterspaced, tokens only. */
.sidebar__section {
  margin: var(--s4) 0 var(--s1);
  padding: 0 var(--s3);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--muted);
}
.sidebar__section:first-child { margin-top: 0; }

.sidebar__item {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: 40px;
  color: var(--muted);
  text-decoration: none;
  padding: var(--s2) var(--s3);
  border-radius: var(--radius);
}

.sidebar__item[aria-current="page"] { background: var(--accent-soft); color: var(--accent); }

.sidebar__account {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  border-top: 1px solid var(--border);
  padding-top: var(--s3);
}

@media (max-width: 800px) {
  .shell { display: block; }
  .content { padding: var(--s4); }
  .sidebar {
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    overflow-x: auto;
    border-right: none;
    border-bottom: 1px solid var(--border);
  }
  .sidebar__brand { margin-bottom: 0; flex-shrink: 0; }
  .sidebar__nav { flex-direction: row; }
  .sidebar__section { display: none; }
  .sidebar__item { white-space: nowrap; }
  .sidebar__account { margin-top: 0; margin-left: auto; flex-shrink: 0; flex-direction: row; align-items: center; }
}

/* The inline <svg> sprite itself renders nothing; only its <use> references do. */
.sprite { display: none; }

.sidebar__icon { flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* Tab strip - the pages of one sidebar group. No margin-bottom: the .content sibling rule
   below already spaces it from the first card. */
.tabs {
  display: flex;
  gap: var(--s1);
  overflow-x: auto;
  border-bottom: 1px solid var(--border);
}

.tabs__item {
  padding: var(--s2) var(--s3);
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.tabs__item:hover { color: var(--text); }
.tabs__item[aria-current="page"] { color: var(--accent); border-bottom-color: var(--accent); }

/* Status dot. Never the only carrier of the fact: the age in words sits next to it, so the
   fleet reads the same with colour vision or without it. */
.dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: var(--s2);
  vertical-align: baseline;
  background: var(--muted);
}

.dot--green { background: var(--ok); }
.dot--amber { background: var(--warn); }
.dot--red   { background: var(--danger); }
.dot--grey  { background: var(--muted); }

/* A read-only fact list: label left, value right, one row each. */
.facts { margin: 0; }
.facts__row { display: flex; gap: var(--s4); padding: var(--s2) 0; border-bottom: 1px solid var(--border); }
.facts__row:last-child { border-bottom: none; }
.facts dt { flex: 0 0 200px; color: var(--muted); }
.facts dd { margin: 0; }

@media (max-width: 800px) {
  .facts__row { flex-direction: column; gap: 0; }
  .facts dt { flex: none; }
}

/* width:100% - a grid item with auto margins shrinks to its content instead of stretching */
.content { width: 100%; max-width: var(--content-w); margin: 0 auto; padding: var(--s5); }

/* page head */

.page__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s4);
  margin-bottom: var(--s5);
}

.page__lead { min-width: 0; }

.page__title { margin: 0; font-size: var(--fs-xl); }

.page__actions { display: flex; gap: var(--s2); flex-shrink: 0; }

.crumbs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: 0 0 var(--s1);
}

.crumbs a { color: inherit; text-decoration: none; }
.crumbs a:hover { text-decoration: underline; }

/* card */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--s5);
}

.card__title { margin: 0 0 var(--s4); font-size: var(--fs-lg); }

/* Stacked blocks (list card + anything below it): the content column is plain block,
   so siblings after the page head need their own rhythm. The head keeps its margin-bottom,
   so pairs starting from it are excluded to avoid a double gap. */
.content > :not(.page__head) + :not(.page__head) { margin-top: var(--s4); }

/* A long reference list inside a card, collapsed until it is asked for. */
summary { cursor: pointer; color: var(--muted); font-size: var(--fs-sm); padding: var(--s1) 0; }
summary:hover { color: var(--text); }

/* toolbar / filter */

.toolbar { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; margin-bottom: var(--s4); }

.toolbar .field { flex: 1 1 260px; }

/* table - the product is tables, so density matters */

.table { width: 100%; border-collapse: collapse; font-size: var(--fs); }

.table a { text-decoration: none; }
.table a:hover { text-decoration: underline; }

.table th {
  position: sticky;
  top: 0;
  background: var(--surface);
  text-align: left;
  font-size: var(--fs-sm);
  color: var(--muted);
  font-weight: 600;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.table th a.sort { color: inherit; text-decoration: none; }

.table th[aria-sort="ascending"] a::after { content: " \25B2"; }
.table th[aria-sort="descending"] a::after { content: " \25BC"; }

.table td { padding: var(--s1) var(--s3); height: 36px; border-bottom: 1px solid var(--border); vertical-align: middle; overflow-wrap: anywhere; }

.table th.num, .table td.num { text-align: right; font-variant-numeric: tabular-nums; }

.table td.actions { text-align: right; white-space: nowrap; overflow-wrap: normal; }

.table tbody tr:hover { background: var(--surface-2); }

.htmx-request #rows { opacity: .5; }

@media (max-width: 800px) {
  /* Sticky th breaks inside an overflow container, so scrolling only turns on below the
     breakpoint where the sidebar has already stopped being a fixed column. */
  .table__scroll { overflow-x: auto; }
}

/* pagination */

/* A form that must sit in a table cell like a button. inline-flex since 15.3: the group card
   puts a field and a button in one, and inline let them wrap apart mid-row. */
.inline { display: inline-flex; gap: var(--s2); align-items: center; flex-wrap: nowrap; }
.inline .btn { white-space: nowrap; flex-shrink: 0; }
.inline .field { min-width: 0; }

.pager {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s4);
  font-size: var(--fs-sm);
  color: var(--muted);
}

.pager__info { margin-right: auto; }

/* forms */

.form {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s4);
}

@media (min-width: 900px) {
  .form { grid-template-columns: 1fr 1fr; }
  .field-group--wide { grid-column: 1 / -1; }
}

.form.form--narrow { grid-template-columns: 1fr; max-width: 640px; }

/* A picker inside a two-column form always spans the row: half a column of checkboxes is
   unreadable, and the list has its own inner grid. Covers the users form once it drops --narrow. */
.form > .checkboxes { grid-column: 1 / -1; }

/*
  The configuration editor: 57 controls split across section cards. The .form grid's gap falls
  between the cards, so the field groups inside one card get none - they need their own. Cards
  span the whole form, and their fields take the two columns, because one 520px column of 57
  controls is ten screens of scrolling. The save bar sticks: two read-only cards follow the form,
  so the bottom of the page is not the bottom of the form.
*/
.form--sections .card { display: grid; gap: var(--s4); grid-column: 1 / -1; }
.form--sections .card__title { margin-bottom: 0; }

.form--sections .form__actions {
  position: sticky;
  bottom: 0;
  background: var(--bg);
  border-top: 1px solid var(--border);
  padding: var(--s3) 0;
}

@media (min-width: 900px) {
  .form--sections .card { grid-template-columns: 1fr 1fr; }
  /* Full width: the title, anything with its own list, and the free-text boxes. */
  .form--sections .card__title,
  .form--sections .checkboxes,
  .form--sections .field-group:has(textarea) { grid-column: 1 / -1; }
}

.field-group { display: flex; flex-direction: column; gap: var(--s1); }

.label { display: block; font-size: var(--fs-sm); color: var(--muted); }

.field {
  width: 100%;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s2) var(--s3);
  min-width: 0;
}

select.field { appearance: auto; }
textarea.field { resize: vertical; }

.field__help { margin: 0; font-size: var(--fs-sm); color: var(--muted); }
.field__error { margin: 0; font-size: var(--fs-sm); color: var(--danger); }

.field-group--invalid .field { border-color: var(--danger); }

.check { display: flex; align-items: center; gap: var(--s2); font-size: var(--fs); }

/* <@ui.checkboxes>: the multi-select. A scrolling column of native checkboxes - the permission
   list runs to ~35 entries and an unbounded fieldset would push the save button off the page.
   The fieldset itself is reset: its legend is the label, the inner list owns the only border. */
.checkboxes { border: 0; margin: 0; padding: 0; }
.checkboxes__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--s1) var(--s3);
  max-height: 22rem;
  overflow-y: auto;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* The access pickers apply only when the matching "all" box is clear, so hide them when it is
   ticked. :has() rather than a script: the form has to work with no JavaScript, and which fields
   are relevant is a view concern. The server ignores the submitted values either way. */
.form:has(input[name="allDevicesAvailable"]:checked) .field-group--devices,
.form:has(input[name="allConfigAvailable"]:checked) .field-group--configs { display: none; }

/* The files form switches between an upload and a URL on one checkbox, so unlike the pickers
   above both directions are hidden: exactly one of the two is relevant at a time. Same reasoning
   as above - the server reads the same checkbox, so a browser without :has() shows both fields
   and still saves the right one. */
.form:has(input[name="external"]:checked) .switched--upload,
.form:not(:has(input[name="external"]:checked)) .switched--external { display: none; }

/* The application form is one form for two types, and the type select decides which field groups
   are relevant. option:checked is how CSS reads a <select>'s current value - there is no
   :has(select[value=...]) - and it tracks the user's choice live, which is the whole point.
   Same contract as the pickers above: the server reads the same select, so a browser without
   :has() shows every group and still saves the right thing. */
.form:not(:has(select[name="type"] option[value="app"]:checked)) .switched--app,
.form:not(:has(select[name="type"] option[value="web"]:checked)) .switched--web,
.form:not(:has(input[name="showIcon"]:checked)) .switched--icon { display: none; }

/* The version edit form's three URL fields: exactly one pair applies, and which one is the split
   checkbox's business. The labels already disambiguate them, so this is the tidy half of the same
   contract - the server reads the same checkbox, and a browser without :has() shows all three and
   still saves the right pair. */
.form:has(input[name="split"]:checked) .switched--single,
.form:not(:has(input[name="split"]:checked)) .switched--split { display: none; }

/* The settings page: the language select applies only when the browser-language box is clear,
   and the header template only when the TEMPLATE header is chosen. Same contract as above -
   the server reads the same controls, so a browser without :has() shows both and still saves
   the right thing. */
.form:has(input[name="useDefaultLanguage"]:checked) .switched--lang,
.form:not(:has(select[name="desktopHeader"] option[value="TEMPLATE"]:checked)) .switched--headertemplate { display: none; }

/* The configuration link editor: a table of selects, so the rows line up and the action column
   does not stretch to the width of its longest label. */
.table.links td { vertical-align: middle; }
.table.links td .field { width: auto; }
.table.links .label { margin: 0; color: var(--text); font-size: var(--fs); }

/* A value the form shows but will not let you change (the stored filename). Styled as the field
   it replaces so the column does not jump, but plainly not an input. */
.field__static {
  margin: 0;
  padding: var(--s2) 0;
  font-family: var(--mono, ui-monospace, monospace);
  word-break: break-all;
}

.form__actions {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--s2);
}

/* buttons */

.btn {
  font: inherit;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: var(--radius);
  padding: var(--s2) var(--s4);
  cursor: pointer;
  display: inline-block;
  text-decoration: none;   /* a link styled as a button */
}

.btn:hover { background: var(--surface-2); }

.btn--primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn--primary:hover { opacity: .92; }
.btn--danger { color: var(--danger); }
.btn--quiet { color: var(--muted); }
.btn--block { display: block; width: 100%; margin-top: var(--s5); }
.btn--sm { padding: var(--s1) var(--s3); font-size: var(--fs-sm); }

.btn[disabled] { opacity: .5; cursor: not-allowed; }

/* badge */

.badge {
  display: inline-block;
  padding: var(--s1) var(--s3);
  border-radius: 999px;
  font-size: var(--fs-sm);
  font-weight: 600;
}

.badge--ok { background: var(--ok-soft); color: var(--ok); }
.badge--warn { background: var(--warn-soft); color: var(--warn); }
.badge--danger { background: var(--danger-soft); color: var(--danger); }
.badge--muted { background: var(--surface-2); color: var(--muted); }

/* alert */

.alert {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin: 0 0 var(--s4);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius);
  border: 1px solid var(--danger);
  color: var(--danger);
}

.alert__close {
  margin-left: auto;
  flex-shrink: 0;
  padding: 0 var(--s1);
  font-size: var(--fs);
  line-height: 1;
  color: inherit;
  background: none;
  border: none;
  cursor: pointer;
}

.alert--error { border-color: var(--danger); color: var(--danger); background: var(--danger-soft); }
.alert--ok { border-color: var(--ok); color: var(--ok); background: var(--ok-soft); }
.alert--warn { border-color: var(--warn); color: var(--warn); background: var(--warn-soft); }

/* empty */

.empty { color: var(--muted); text-align: center; }

/* dialog - the only one: server-rendered confirm in the layout */

.dialog {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--s5);
  color: var(--text);
  background: var(--surface);
  width: min(400px, 92vw);
}

.dialog::backdrop { background: rgb(16 24 40 / .4); }

.dialog__actions { display: flex; justify-content: flex-end; gap: var(--s2); margin-top: var(--s5); }

/* busy - htmx global indicator, driven by hx-indicator="#busy" on <body> */

.busy {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  opacity: 0;
  z-index: 1000;
}

.busy.htmx-request { opacity: 1; }

/* login */

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

.login__box {
  width: min(360px, 92vw);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: var(--s6);
}

.login__title { margin: 0 0 var(--s5); font-size: var(--fs-xl); text-align: center; }

/* The inputs default to their intrinsic size, which is narrower than the button. */
.login__box .field { width: 100%; }

/* toasts - raised from frontend/ts/app.ts for the responses that carry no page to render */

.toasts {
  position: fixed;
  top: var(--s4);
  right: var(--s4);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  /* The container spans the gap between toasts; only the toasts themselves take the pointer,
     or the strip would swallow clicks on the page underneath it. */
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: min(380px, calc(100vw - 2 * var(--s4)));
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--muted);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  cursor: pointer;
}

.toast--error   { border-left-color: var(--danger); }
.toast--success { border-left-color: var(--ok); }

.toast__close {
  margin-left: auto;
  flex-shrink: 0;
  padding: 0 var(--s1);
  font-size: var(--fs);
  line-height: 1;
  color: var(--muted);
  background: none;
  border: none;
  cursor: pointer;
}

@media (prefers-reduced-motion: no-preference) {
  .toast { animation: toast-in 150ms ease-out; }
  @keyframes toast-in { from { opacity: 0; transform: translateY(-6px); } }
}

/* The configuration editor's dependent controls. Same contract as every .switched rule above:
   the server reads the same control, so a browser without :has() shows all of them - and every
   one of these is written conditionally on the flag that gives it meaning, so showing a control
   that does not apply cannot write a value into a column that should stay null.

   The editor is three <section class="card"> inside one .form, so the selector is the form, not
   the card: a card is not an ancestor of the control that switches it. */
.form:not(:has(select[name="pushOptions"] option[value="mqttAlarm"]:checked)) .switched--mqtt,
.form:not(:has(select[name="autoBrightness"] option[value="false"]:checked)) .switched--brightness,
.form:not(:has(input[name="manageTimeout"]:checked)) .switched--timeout,
.form:not(:has(input[name="manageVolume"]:checked)) .switched--volume,
.form:not(:has(select[name="timeZoneMode"] option[value="manual"]:checked)) .switched--tz,
.form:not(:has(select[name="systemUpdateType"] option[value="2"]:checked)) .switched--sysupdate,
.form:not(:has(input[name="scheduleAppUpdate"]:checked)) .switched--appupdate,
.form:not(:has(input[name="kioskMode"]:checked)) .switched--kiosk,
.form:has(input[name="useDefaultDesignSettings"]:checked) .switched--design,
.form:not(:has(select[name="desktopHeader"] option[value="TEMPLATE"]:checked)) .switched--header { display: none; }

/* The QR PNG already carries its own quiet zone, so it renders at native size with no
   extra padding or background: on dark mode any white slab around it reads as a bar. */
.qr { text-align: center; margin-bottom: var(--s3); }
.qr__image { width: min(400px, 100%); height: auto; }
.qr + .field__help { text-align: center; }
