/* ---- Colour tokens ----
   Every colour that differs between light and dark used to be written twice:
   once in the rule and once again in a `.dark-mode` copy of that rule, which is
   what made a third of this file a second copy of the other two thirds. The
   pairs live here instead, so a rule names the role and the theme decides the
   value.

   A colour that is the same in both themes stays a literal in its rule. There
   is nothing for a token to decide about it. */
:root {
  /* What the browser and the operating system draw themselves: the option list
     of a select, scrollbars, radios, checkboxes, spin buttons, the focus ring.
     None of it is reachable from this file, and this is the only lever on it.
     Without it a dark page opens a light grey popup with grey text in it. */
  color-scheme: light;

  /* Text */
  --text: #333;
  --text-soft: #333;
  --text-muted: #888;
  --text-quiet: #777;
  --text-state: #444;

  /* Surfaces */
  --page-bg: #f4f4f4;
  --card-bg: #fff;
  --panel-bg: #fff;
  --menu-bg: #fff;
  --control-bg: #fff;
  --control-hover-bg: #eee;
  --input-bg: #fff;
  --row-bg: #fff;
  --row-hover-bg: #f6f9fc;
  --head-bg: #f7f7f7;
  --state-bg: #ececec;
  --log-bg: #f9f9f9;
  --tooltip-bg: #2c2c2c;
  --track-bg: #bbb;

  /* Borders */
  --border: #e0e0e0;
  --border-strong: #ccc;
  --border-control: #bbb;
  --border-input: #bbb;
  --border-soft: #eee;
  --border-row: #eee;
  --border-card: #ddd;

  /* Accent */
  --accent: #1c6fd6;
  --accent-text: #1c5fa8;
  --accent-bg: #1c6fd6;
  --accent-bg-hover: #1559ad;

  /* Meaning */
  --ok: #1e7e44;
  --ok-bg: #e3f6e9;
  --bad: #b3261e;
  --bad-bg: #fbe4e4;
  --warn: #9a6a00;
  --warn-bg: #fdf0db;
  --info-bg: #e3f0fb;
  --danger-solid: #b3261e;
  --danger-solid-hover: #8f1d17;
  --danger-border: #d9a5a1;

  /* The plain tables of the older pages */
  --table-bg: transparent;
  --table-th-bg: #eee;
  --table-td-bg: transparent;
  --table-cell-border: none;

  /* Shadows. Dark drops the card shadow entirely and deepens the others. */
  --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.06);
  --shadow-menu: 0 4px 14px rgba(0, 0, 0, 0.12);
  --shadow-dialog: 0px 4px 6px rgba(0, 0, 0, 0.1);

  /* The brand green, Bambu Lab's: "Sync" in the wordmark and the line under
     the current tab. The same in both themes, it is the one colour that is
     the brand rather than the surface. */
  --brand: #00ae42;
}

.dark-mode {
  --text: #f1f1f1;
  --text-soft: #ccc;
  --text-muted: #999;
  --text-quiet: #aaa;
  --text-state: #c9c9c9;

  --page-bg: #1e1e1e;
  --card-bg: #1e1e1e;
  --panel-bg: #1f1f1f;
  --menu-bg: #262626;
  --control-bg: #2c2c2c;
  --control-hover-bg: #3a3a3a;
  --input-bg: #2a2a2a;
  --row-bg: #2a2a2a;
  --row-hover-bg: #262626;
  --head-bg: #2a2a2a;
  --state-bg: #2b2b2b;
  --log-bg: #2e2e2e;
  --tooltip-bg: #3a3a3a;
  --track-bg: #555;

  --border: #3a3a3a;
  --border-strong: #555;
  --border-control: #4a4a4a;
  --border-input: #555;
  --border-soft: #333;
  --border-row: #3a3a3a;
  --border-card: #444;

  --accent: #6fb0ec;
  --accent-text: #6fb0ec;
  --accent-bg: #2f6fc4;
  --accent-bg-hover: #26599e;

  --ok: #5fd089;
  --ok-bg: #14331f;
  --bad: #f08a82;
  --bad-bg: #3a1b1a;
  --warn: #e0b65f;
  --warn-bg: #34290f;
  --info-bg: #16293c;
  --danger-solid: #c4382f;
  --danger-solid-hover: #a32c25;
  --danger-border: #6b3a36;

  --table-bg: #2e2e2e;
  --table-th-bg: #444;
  --table-td-bg: #333;
  --table-cell-border: 1px solid #555;

  --shadow-card: none;
  --shadow-menu: 0 4px 14px rgba(0, 0, 0, 0.5);
  --shadow-dialog: 0px 4px 6px rgba(0, 0, 0, 0.6);
}

/* Anything the JS hides with the `hidden` attribute stays hidden.
   The attribute is only `display: none` in the browser's own stylesheet, so
   every rule in this file that sets a display wins over it. That is what showed
   the log out entry on installations without a password, and what the mobile
   `caption.ams-env { display: block }` would do to a unit header that reports
   nothing. Every panel of the menu bar is hidden this way too. */
[hidden] {
  display: none !important;
}

body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 20px;
  background-color: var(--page-bg);
  color: var(--text);
}

h1 {
  text-align: center;
}

#status {
  margin: 20px 0;
}

/* ---- Status card ---- */
.status-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card-bg);
  padding: 14px 18px;
  box-shadow: var(--shadow-card);
}

.status-head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-soft);
}

/* The title in the middle, the two things that are not the title at the two
   ends. Both ends take the same share of what is left over, which is what puts
   the title in the actual centre of the card rather than in the middle of
   whatever space the wider of the two happens to leave. */
.status-head-side {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  min-width: 0;
}

.status-head-side-end { justify-content: flex-end; }

/* A block rather than a flex row: the words and the picker have to wrap as one
   sentence when the card is narrow, instead of standing next to each other as
   two columns that each wrap on their own. */
.status-title {
  font-size: 1.28rem;
  font-weight: 700;
  margin: 0;
  text-align: center;
}

.status-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 0.88rem;
  white-space: nowrap;
  cursor: pointer;
}

/* Facts of one size, in the middle of the card.
   Every cell is the same width, so a wrapped row lines up under the one above
   it instead of every line starting somewhere else. The tracks have a fixed
   size rather than 1fr, which is what keeps them from being stretched apart
   over the whole card, and the whole block is centred rather than left packed,
   so the empty space of a half filled last row sits on both sides. */
.status-grid {
  display: grid;
  /* 165 and 24: six of them plus five gaps are 1110px, which is what a card at
     the 1180px page width has room for, so the whole state of the printer is
     one line and not five and a orphan. */
  grid-template-columns: repeat(auto-fit, minmax(150px, 165px));
  justify-content: center;
  gap: 12px 24px;
}

.status-item {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.status-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.status-value {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--text);
  word-break: break-word;
}

/* ---- Status pills / badges ---- */
.pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.4;
  white-space: nowrap;
}
.pill-ok    { background: var(--ok-bg);   color: var(--ok); }
.pill-bad   { background: var(--bad-bg);  color: var(--bad); }
.pill-gcode { background: var(--info-bg); color: var(--accent-text); }
.pill-legacy{ background: var(--warn-bg); color: var(--warn); }

/* ---- G-code dashboard inline indicators (dark-mode readable) ---- */
.gc-link     { color: var(--accent); text-decoration: none; }
.gc-link:hover { text-decoration: underline; }
.gc-ok       { color: var(--ok); font-weight: 600; }
.gc-bad      { color: var(--bad); font-weight: 600; }
.gc-warn     { color: var(--warn); }
.gc-muted    { opacity: 0.6; }
.gc-required { color: var(--bad); }
.gc-error { margin: 10px 0 0; }
/* The line under the card head that says the sliced file was not found:
   grey while attempts are left, red (gc-required) after the last one */
.gc-card-lookup { margin: 10px 0 0; text-align: center; }
.gc-card-lookup-open { color: var(--text-muted); }

/* The i at the end of a lookup line, opens the help dialog. Drawn in the line's
   own colour, so it is red on the final line and grey while attempts are left */
.gc-card-help {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  margin-left: 8px;
  padding: 0;
  border: 1px solid currentColor;
  border-radius: 50%;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 0.72em;
  font-weight: 700;
  font-style: italic;
  line-height: 1;
  vertical-align: middle;
  cursor: pointer;
  opacity: 0.8;
}
.gc-card-help:hover { opacity: 1; }

/* The printer's answer inside the help dialog, as the log has it */
.gc-help-reason {
  margin: 8px 0;
  padding: 8px 10px;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: var(--control-bg);
  font-size: 0.8em;
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 140px;
  overflow: auto;
}
#lookup-help-content ol { padding-left: 1.4em; }
#lookup-help-content li { margin: 6px 0; }

/* The toast after an action, bottom right, fades out on its own */
.action-note {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  padding: 0.75rem 1.25rem;
  border-radius: 6px;
  font-size: 0.9rem;
  z-index: 9999;
  background: #27ae60;
  color: #fff;
  opacity: 0;
  transition: opacity 0.4s;
}
.action-note-error { background: #c0392b; }
.action-note-shown { opacity: 1; }


/* ---- G-code print card ---- */
.gc-card {
  border: 1px solid #8884;
  border-radius: 8px;
  padding: 12px 16px;
  margin-bottom: 16px;
}
.gc-card-head {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.gc-card-note {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
}

/* The booking label and the countdown next to it are buttons rather than text,
   because both do something: one opens the summary, the other clears the
   result now. They are styled as links so a card that only reports a finished
   print does not grow two more grey boxes. */
.gc-card-link {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.gc-card-link:hover { opacity: 0.75; }
.gc-card-link:disabled { cursor: default; opacity: 0.5; }
.gc-card-booked { color: var(--ok); font-weight: 600; text-decoration-color: #8886; }
.gc-card-unbooked { color: var(--bad); font-weight: 600; text-decoration-color: #8886; }
.gc-card-partly { color: var(--warn); font-weight: 600; text-decoration-color: #8886; }
.gc-card-nothing { color: var(--text-muted); font-weight: 600; text-decoration-color: #8886; }

/* A clock that is rewritten every second. Tabular figures are the whole of it:
   every shape formatCounter() produces is fixed length within itself, so the
   digits changing cannot change the width and no reserve is needed. The label
   only resizes where the shape itself changes, at an hour and at a day, which
   happens once each and never at all for the ten minute default. */
.gc-counter { font-variant-numeric: tabular-nums; }

/* ---- The closing report of a print ---- */

/* Label and value pairs on one line, wrapping to as many lines as the width
   needs. Used by the summary dialog and by the running print on the card, so
   the two read alike. The pair is one flex item, so a value never wraps away
   from the label it belongs to. */
.gc-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin: 0 0 14px;
  font-size: 0.85em;
}
.gc-fact-term { color: var(--text-muted); }

/* Says where the rows of the table come from, so a "not booked" line reads as
   a filament of the plate rather than as something the service mislaid. */
.gc-summary-lead {
  margin: 0 0 6px;
  font-size: 0.85em;
  color: var(--text-muted);
}

.gc-summary-table { margin-top: 4px; }
/* The reason a filament was not booked, under its result rather than in a
   column of its own: it is a sentence, and only some rows carry one. */
.gc-summary-note {
  font-size: 0.8em;
  color: var(--text-muted);
  margin-top: 2px;
  max-width: 34ch;
}

#print-summary-content { max-height: 60vh; overflow-y: auto; }

/* Print state badge. Colors follow the .pill light/dark pairs above so the
   badge stays readable in both themes. */
.gc-state {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 10px;
  font-size: 0.85em;
  font-weight: 600;
  background: var(--state-bg);
  color: var(--text-state);
}
.gc-state-running { background: var(--ok-bg);   color: var(--ok); }
.gc-state-done    { background: var(--info-bg); color: var(--accent-text); }
.gc-state-error   { background: var(--bad-bg);  color: var(--bad); }
.gc-state-paused  { background: var(--warn-bg); color: var(--warn); }
/* A print that has started but is not laying down filament yet: heating,
   homing, levelling, loading. Amber, because it is neither running nor a
   problem, and it is what the card looks like for the first minutes of a job. */
.gc-state-prepare { background: var(--warn-bg); color: var(--warn); }
/* The stage sits next to the state and must not read as a second state. */
.gc-stage { font-weight: 500; }

/* The same pairs the summary dialog uses, here under the layer bar. Only the
   spacing differs: on the card they follow something, in the dialog they lead. */
.gc-card .gc-facts { margin: 10px 0 0; }


/* ---- Layer progress ---- */
.gc-progress { margin-top: 10px; }
.gc-progress-labels {
  display: flex;
  justify-content: space-between;
  font-size: 0.85em;
  opacity: 0.8;
}
.gc-progress-track {
  height: 8px;
  background: #8883;
  border-radius: 4px;
  overflow: hidden;
  margin-top: 3px;
}
.gc-progress-bar {
  height: 100%;
  background: var(--ok);
}

/* ---- Spool assignment / creation dialog ----
   Follows the settings page: bold field labels, the same input shape and the
   same focus colour, so the two forms in this UI look like one. */
.sp-tabs {
  display: flex;
  gap: 4px;
  margin: 12px 0;
  border-bottom: 1px solid #8883;
}

.sp-tab {
  border: none;
  background: none;
  padding: 7px 12px;
  cursor: pointer;
  font-size: 0.9em;
  color: inherit;
  opacity: 0.6;
  border-bottom: 2px solid transparent;
  border-radius: 6px 6px 0 0;
  transition: background-color 0.2s, opacity 0.2s;
}

.sp-tab:hover { opacity: 0.9; background: #8881; }

.sp-tab-active,
.sp-tab-active:hover {
  opacity: 1;
  font-weight: 600;
  background: none;
  border-bottom-color: var(--accent);
}


/* The picker searches a whole inventory, so the field sits above the list it
   filters rather than inside the scrolling area. */
.sp-search {
  display: block;
  margin: 4px 0 10px;
}

.sp-search input {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 10px;
  border: 1px solid var(--border-input);
  border-radius: 6px;
  background: var(--input-bg);
  color: inherit;
  font-family: inherit;
  font-size: 0.9em;
}

.sp-search input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px #1c6fd633;
}



/* The dialog is capped in height, so the form scrolls rather than the page */
.sp-scroll {
  max-height: 46vh;
  overflow-y: auto;
  text-align: left;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 18px;
  padding-right: 6px;
}

.sp-section {
  grid-column: 1 / -1;
  font-size: 0.78em;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.6;
  margin-top: 8px;
  border-bottom: 1px solid #8882;
  padding-bottom: 4px;
}

.sp-scroll > .sp-section:first-child { margin-top: 0; }

/* Inside the new filament form: its data, its colours, its specifications and
   its weights. Lighter than a section heading, because the form is one thing
   and these only keep its ten fields apart. */
.sp-subsection {
  grid-column: 1 / -1;
  font-size: 0.82em;
  font-weight: 600;
  opacity: 0.75;
  margin-top: 6px;
  padding-bottom: 3px;
  border-bottom: 1px solid #8882;
}

.sp-wide, .sp-note, #sp-error { grid-column: 1 / -1; }
.sp-note { font-size: 0.83em; opacity: 0.75; margin: 8px 0 0; }
#sp-error:empty { display: none; }

/* #sp-filament-fields spans the grid and repeats it, so its inputs line up
   with the spool fields below instead of forming a second, narrower column. */
#sp-filament-fields {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 18px;
}

.sp-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 0.88em;
  min-width: 0;
}

.sp-field > span {
  font-weight: 600;
  min-height: 20px;
}

.sp-field input,
.sp-field select {
  padding: 5px 8px;
  border: 1px solid #8886;
  border-radius: 5px;
  /* A field colour rather than none. A transparent select is also what
     Chromium paints its option list with, and on Windows that comes out white
     over a dark page. See the `option` rule further down. */
  background: var(--input-bg);
  color: inherit;
  font: inherit;
  font-size: 0.95em;
  min-width: 0;
}

.sp-field input:focus,
.sp-field select:focus {
  outline: none;
  border-color: var(--accent);
}


.sp-field small {
  opacity: 0.7;
  font-size: 0.84em;
  line-height: 1.3;
}

.sp-field small:empty { display: none; }

/* The catalogue is narrowed down in three steps, so they are framed together
   and read as one block rather than as three more fields of the form. */
.sp-catalogue {
  padding: 12px;
  border: 1px solid #8883;
  border-radius: 8px;
  background: #8881;
}

.sp-catalogue-title {
  font-size: 0.85em;
  font-weight: 600;
  margin-bottom: 10px;
}

.sp-catalogue-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 12px 18px;
}

.sp-catalogue small { display: block; margin-top: 8px; }

/* One row per colour, so a multi colour filament can be entered the way the
   AMS and the catalogue report it. */
.sp-colours {
  display: grid;
  /* auto-fill with a cap, so one colour stays a field and does not stretch
     across the whole dialog the way a single auto-fit column would. */
  grid-template-columns: repeat(auto-fill, minmax(160px, 240px));
  gap: 8px;
}

.sp-colours-single .sp-colour-remove { display: none; }

.sp-colour-remove {
  border: none;
  background: none;
  margin: 0;
  padding: 0 4px;
  cursor: pointer;
  color: inherit;
  opacity: 0.5;
  font-size: 0.95em;
  line-height: 1;
}

.sp-colour-remove:hover { opacity: 1; }

.sp-colour-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}

.sp-colour-actions .btn { margin: 0; }

.sp-colour-actions select {
  padding: 4px 8px;
  border: 1px solid var(--border-input);
  border-radius: 6px;
  background: var(--input-bg);
  color: inherit;
  font-family: inherit;
  font-size: 0.85em;
}


.sp-colour { display: flex; gap: 6px; align-items: center; }
.sp-colour input[type="color"] { padding: 0; width: 34px; height: 30px; flex: none; cursor: pointer; }
/* The hex field beside the swatch. Styled like every other field of the
   dialog rather than left to the browser, which draws its own box and, under a
   dark colour scheme, its own grey that matches nothing around it. */
.sp-colour input:not([type="color"]) {
  flex: 1;
  text-transform: uppercase;
  padding: 5px 8px;
  border: 1px solid #8886;
  border-radius: 5px;
  background: var(--input-bg);
  color: inherit;
  font: inherit;
  font-size: 0.95em;
  min-width: 0;
}

.sp-colour input:not([type="color"]):focus {
  outline: none;
  border-color: var(--accent);
}

/* One pickable spool per row, so the whole row reacts rather than just the
   radio button next to it. Two lines: the name, and under it what is known
   about that spool. On one line the weight and the badges wrapped into the
   name, which is long enough on its own. */
.sp-pick {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  padding: 7px 10px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 0.9em;
  transition: background-color 0.2s;
}

.sp-pick input[type="radio"] {
  flex: none;
  /* On the line of the name rather than in the middle of both lines */
  margin-top: 3px;
}

/* One line while the two fit next to each other, two when they do not. Both
   start at the same edge, next to the radio button and not under it, so a
   wrapped second line reads as a continuation of the first. */
.sp-pick-text {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 1px 7px;
  min-width: 0;
}

/* Moves as one piece, so the weight never ends up on a different line than the
   badge that qualifies it. */
.sp-pick-meta {
  display: inline-flex;
  align-items: baseline;
  gap: 4px 10px;
}

.sp-pick:hover { background: #8881; }
.sp-pick:has(input:checked) { background: var(--info-bg); }

.sp-scroll > .sp-pick { grid-column: 1 / -1; }

/* ---- Colour swatch shared by spool rows and the "required" list ---- */
.gc-swatch {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 1px solid #888;
  border-radius: 2px;
  vertical-align: middle;
  margin-right: 5px;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 20px;
  background-color: var(--table-bg);
}

/* ---- AMS unit header ----
   What the unit says about itself, above its slot table: humidity, temperature
   and the drying cycle. Hidden (and empty) for a unit that reports none of it,
   which is the AMS Lite and the external spool holder. */
caption.ams-env {
  caption-side: top;
  text-align: left;
  padding: 0 2px 6px;
  font-size: 0.85em;
  color: var(--text-muted);
}

/* Spaced with a margin rather than a flex gap: a caption given `display:flex`
   leaves the table layout and is drawn below the header row instead of above
   the table. */
caption.ams-env > span {
  margin-right: 12px;
}

caption.ams-env .ams-env-unit {
  font-weight: 600;
  color: var(--text-soft);
}

caption.ams-env .ams-env-drying {
  color: var(--warn);
  font-weight: 600;
}

/* The readings sit in a span of their own, so each needs the spacing the
   direct children have. */
caption.ams-env .ams-env-readings > span {
  margin-right: 12px;
}

/* ---- Folding a unit ----
   The header is the control: the whole line folds the slots away and back,
   the chevron at its right end says so and points down while open, right
   while folded. Folded, the table body is gone and the header turns into a
   slim card carrying the swatches of the loaded slots. */
caption.ams-env {
  cursor: pointer;
  user-select: none;
}

.ams-toggle {
  float: right;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 22px;
  margin-left: 8px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
}

.ams-toggle svg { transition: transform 0.2s; }

caption.ams-env:hover .ams-toggle { color: var(--text); }

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

.spool-table.is-collapsed .ams-toggle svg { transform: rotate(-90deg); }

.spool-table.is-collapsed thead,
.spool-table.is-collapsed tbody { display: none; }

.ams-summary { display: none; }

.spool-table.is-collapsed .ams-summary {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  vertical-align: middle;
}

.ams-summary .gc-swatch { margin-right: 3px; }

.ams-summary-empty {
  font-style: italic;
  opacity: 0.8;
}

/* The folded header as a slim card of its own, where the open table's card
   stands otherwise. The table box itself is empty while folded and draws
   nothing. */
.spool-table.is-collapsed {
  border: 0;
  box-shadow: none;
}

.spool-table.is-collapsed caption.ams-env {
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--card-bg);
  box-shadow: var(--shadow-card);
}

/* Keep content clear of the fixed footer so the last table isn't overlapped */
body.show-footer #spool-list {
  margin-bottom: 80px;
}

table th, table td {
  border: 1px solid var(--border-strong);
  background-color: var(--table-td-bg);
  padding: 10px;
  text-align: left;
  transition: background-color 0.3s, color 0.3s;
}

table th {
  background-color: var(--table-th-bg);
  color: var(--text-soft);
}


table td.color-cell {
  background-color: transparent !important;
  color: inherit !important;
}

/* ---- Card-styled spool tables (desktop) ---- */
.spool-table {
  border: 1px solid var(--border);
  border-radius: 10px;
  border-collapse: separate;
  border-spacing: 0;
  overflow: hidden;
  background: var(--card-bg);
  box-shadow: var(--shadow-card);
}

/* The generic table rules above paint every cell, which would draw a full grid
   over the card and a header background on every row. The card wants row
   separators alone, so both are reset here rather than a second time per
   theme. */
.spool-table th,
.spool-table td {
  border: none;
  border-bottom: 1px solid var(--border-soft);
  background: transparent;
}

.spool-table thead th {
  background: var(--head-bg);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-quiet);
  font-weight: 600;
}

.spool-table tbody tr:last-child td {
  border-bottom: none;
}

.spool-table tbody tr:hover td {
  background: var(--row-hover-bg);
}

/* The action column holds one control per row, and the labels on those buttons
   differ in length ("Assign Spool" against "No actions available"), so left
   aligned they never stood under each other. Centred they do, and the header
   follows them. Not on a phone, where every cell is its own labelled line and
   a centred button would sit away from the label that names it. */
@media (min-width: 761px) {
  .spool-table th:last-child,
  .spool-table td.action-cell {
    text-align: center;
  }
}


/* The entries of an open select.
   Chromium draws the list with the colours of the select itself, so a page that
   leaves them to the browser gets the white list of the default canvas over its
   dark page: that is what a dark Windows install showed. Named here so the list
   is a dark list, `color-scheme` above so the scrollbar and the frame around it
   follow as well. macOS draws the list itself and ignores both, which is why it
   looked right there. */
option {
  background-color: var(--input-bg);
  color: var(--text);
}

html.transition-enabled,
html.transition-enabled body {
  transition: background-color 0.3s, color 0.3s;
}

/* The class sits on <html> so the theme is set before the first paint, and it
   paints the root itself so the canvas has a colour before the body has any
   height. The body takes the same two values from the tokens. */
html.dark-mode {
  /* Tells the browser to draw its own controls dark as well, see :root */
  color-scheme: dark;
  background-color: var(--page-bg);
  color: var(--text);
}




dialog {
  background-color: var(--page-bg);
  color: var(--text);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 20px;
  /* Counted with the padding and the border, or a dialog sized to what the
     screen allows ends up 42px wider than that and hangs over the edge of a
     phone. The cap keeps the desktop width it always had. */
  box-sizing: border-box;
  max-width: min(800px, calc(100vw - 20px));
  text-align: left;
  box-shadow: var(--shadow-dialog);
  transition: background-color 0.3s, color 0.3s;
}

dialog h3 {
  margin-top: 0;
}

dialog button {
  margin-top: 20px;
}


.button-container {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin-top: 10px;
}

/* `.btn` sits on a button on every page, and the .btn rule further down sets
   its own size, so this is the plain button that carries no class. */
button {
  font-family: Arial, sans-serif;
  font-size: 14px;
}

/* ---- Log page toolbar ----
   One line with every choice the page offers: the source, the switch between a
   printer's log and its raw MQTT trace, and the download with a line that says
   what it would carry. The picker used to be the headline and the trace an
   entry inside its menu, and the first person who needed a trace did not find
   it: a choice has to look like a control, not like a heading. */
.log-toolbar {
  display: flex;
  align-items: center;
  gap: 10px 22px;
  flex-wrap: wrap;
  margin-top: 14px;
  padding: 10px 14px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
}

.log-field {
  display: flex;
  align-items: center;
  gap: 8px;
}

.log-field-label {
  font-size: 0.85em;
  color: var(--text-muted);
}

/* The download, on the right whatever wraps to the left. */
.log-toolbar-end {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  margin-left: auto;
  flex: 1 1 auto;
}

.log-meta {
  font-size: 0.85em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* In the toolbar the picker is one control among others, so it gets the frame
   and the surface every other button here has instead of the dotted line the
   dashboard headline uses, and its menu opens under its left edge. */
.log-toolbar .title-pick {
  background: var(--control-bg);
  border-color: var(--border-control);
  border-radius: 6px;
  padding: 5px 12px;
  font-size: 0.88em;
  font-weight: 400;
  min-width: 150px;
  justify-content: space-between;
  transition: background-color 0.2s, border-color 0.2s;
}

.log-toolbar .title-pick > span { text-decoration-line: none; }
.log-toolbar .title-pick.menu-caret::after { font-size: 1em; }
.log-toolbar .title-pick-plain { padding: 5px 12px; cursor: default; }
.log-toolbar .title-pick-plain:hover { background: var(--control-bg); }

.log-toolbar .title-panel {
  left: 0;
  transform: none;
}

/* Two buttons welded into one control, the pressed one filled. Two states of
   one thing rather than two actions, which is why it is not two buttons with
   a gap between them. */
.log-switch {
  display: inline-flex;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  overflow: hidden;
}

.log-switch button {
  background: var(--control-bg);
  color: var(--text);
  border: none;
  border-right: 1px solid var(--border-control);
  padding: 6px 14px;
  font-size: 0.88em;
  cursor: pointer;
  transition: background-color 0.2s;
}

.log-switch button:last-child { border-right: none; }
.log-switch button:hover { background: var(--control-hover-bg); }

.log-switch button[aria-pressed="true"] {
  background: var(--accent-bg);
  color: #fff;
  cursor: default;
}

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

/* The name of what the box shows and the file it comes from. Left, under the
   toolbar, because the toolbar is where the eye already is. */
.log-title {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  margin: 14px 0 0;
}

.log-file {
  font-family: monospace;
  font-size: 0.78em;
  font-weight: 400;
  color: var(--text-muted);
}

/* At the right end of the title row, in the weight of a note; on a phone it
   wraps under the name. */
.log-title .log-meta {
  margin-left: auto;
  font-weight: 400;
  font-size: 0.72em;
}

/* The log page is a column the height of the window: the box takes what is left
   after the bar, the toolbar and the title, so the whole box is on screen
   whatever the log contains and however far the toolbar wraps. A fixed
   max-height had to guess all three. */
body.logs-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  box-sizing: border-box;
}

/* An auto margin on a flex item wins over the stretch it would get otherwise,
   so both of these have to be told to take the width; the margin then only
   centres them within the 1180px. */
body.logs-page #menubar {
  flex: none;
  width: 100%;
}

body.logs-page .log-page { width: 100%; }

.log-page {
  display: flex;
  flex-direction: column;
  flex: 1;
  /* Or the box refuses to shrink below the height of the log inside it */
  min-height: 0;
}

#log-box {
  margin-top: 8px;
  padding: 10px;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  background-color: var(--log-bg);
  color: var(--text);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}



#logs {
  font-family: monospace;
  font-size: 14px;
  color: var(--text-soft);
}


/* ---- Menu bar ----
   Same card language as the panels below it: light surface, soft border,
   rounded corners. The menu button and the entries follow the shared button
   style so the whole header matches the rest of the UI. */
#menubar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  background: var(--panel-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
  /* No vertical padding: the tabs run the full height of the bar, so the
     line under the current one sits on the bar's edge. */
  padding: 0 12px;
  transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}

/* The settings page holds its content in a narrower column, so the bar lines
   up with it instead of running wider than everything below it. */
/* One measure for every page. The bar and the content under it share it, so
   they line up, and a wide screen does not stretch a spool table, a log line or
   a headline across the whole desk. `box-sizing` so the max width counts the
   padding and the bar lines up with what is below it. */
#menubar,
.page-width,
.set-page {
  max-width: 1180px;
  margin: 0 auto;
  box-sizing: border-box;
}


#dark-mode-toggle {
  background: none;
  border: none;
  cursor: pointer;
  border-radius: 8px;
  padding: 5px;
  line-height: 0;
  transition: background-color 0.2s;
}

#dark-mode-toggle:hover {
  background: #8881;
}

/* The language switch next to it: as quiet as the button, a native select so
   the choice stays usable with a keyboard and on a phone. */
#dark-mode-icon {
  display: inline-block;
  width: 22px;
  height: 22px;
  color: inherit;
}

#dark-mode-icon svg {
  display: block;
  width: 22px;
  height: 22px;
}

/* ---- Menu bar ----
   The brand at the left end, the dashboard and the logs as tabs next to it,
   and the tools at the right end: the theme and the settings gear, which is a
   link or, with a session to end, a menu. The page you are on is the
   underlined tab, or the filled gear. `#menu-root` takes the free width so its
   groups can sit at the two ends of the bar. */
#menu-root {
  flex: 1;
  min-width: 0;
}

.menunav {
  position: relative;
  display: flex;
  align-items: stretch;
  gap: 14px;
  min-width: 0;
  height: 50px;
}

.menu-pages,
.menu-end {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
}

/* The tabs stretch to the bar's height, so their underline sits on its edge. */
.menu-pages {
  align-items: stretch;
  gap: 2px;
}

.menu-end { margin-left: auto; }

/* ---- The brand ----
   Logo and name, the way home. The name is in the body font and weight 700,
   with "Sync" in the brand green; the flanges of the spool take the text
   colour through currentColor, see BRAND_ICON in menu.js. */
.menu-brand {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--text);
  text-decoration: none;
  border-radius: 8px;
  padding: 0 4px;
  flex: none;
}

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

.menu-brand-icon {
  display: block;
  flex: none;
}

.menu-wordmark {
  font-weight: 700;
  font-size: 1.08rem;
  letter-spacing: -0.02em;
  line-height: 1;
  white-space: nowrap;
}

.menu-wordmark b {
  color: var(--brand);
  font-weight: 700;
}

/* Every clickable thing in the bar, whether it is a link to a page or a button
   that opens a panel. */
.menu-item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 6px 12px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 0.88em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}

.menu-item:hover { background: var(--control-hover-bg); }

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

/* The page you are on. Border and weight rather than colour alone, so it is
   still the marked one for somebody who cannot tell the two greys apart. */
.menu-item[aria-current] {
  background: var(--control-hover-bg);
  border-color: var(--border-control);
  font-weight: 700;
}

.menu-item[disabled] {
  cursor: default;
  opacity: 1;
}

.menu-item[disabled]:hover { background: none; }

/* ---- The tabs ----
   A page is a tab: quiet text with a glyph in front, the current one in the
   text colour, bold, with the brand green under it. The line is the mark, not
   a frame or a fill, which is what makes two of them read as one row. The
   top border of the same size keeps the label centred. */
.menu-tab {
  border: 0;
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  padding: 0 12px;
  color: var(--text-quiet);
  font-size: 0.9em;
}

.menu-tab:hover {
  background: none;
  color: var(--text);
}

.menu-tab[aria-current] {
  background: none;
  color: var(--text);
  font-weight: 700;
  /* The shared current mark frames the item; here only the bottom edge is the
     mark, so the top stays clear. */
  border-top-color: transparent;
  border-bottom-color: var(--brand);
}

.menu-glyph {
  flex: none;
  opacity: 0.8;
}

/* ---- The tools ----
   The gear is drawn like a tab: quiet, the full height of the bar, and
   underlined in the brand green while the settings page is open, so the
   mark of the open page is the same at both ends of the bar. As a menu it
   grows a caret and opens towards the middle of the bar. */
.menu-tool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  align-self: stretch;
  flex: none;
  padding: 0 8px;
  border: 0;
  border-top: 2px solid transparent;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--text-quiet);
  font: inherit;
  text-decoration: none;
  cursor: pointer;
  transition: color 0.2s;
}

.menu-tool:hover { color: var(--text); }

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

.menu-tool[aria-current] {
  color: var(--text);
  border-bottom-color: var(--brand);
}

.menu-tool .menu-glyph {
  width: 18px;
  height: 18px;
  opacity: 0.9;
}

.menu-tool.menu-caret::after { font-size: 0.95em; }

/* The host of the gear menu stretches with the bar like the one of the logs,
   so its panel opens under the bar's edge rather than under the button. */
.menu-end .menu-host {
  align-self: stretch;
  align-items: stretch;
}

.menu-tools { display: contents; }

/* The lines of the gear menu: a glyph in front of each entry, a rule between
   the actions and the foot, and the foot in the small quiet type of a panel
   heading. */
.menu-entry .menu-glyph {
  width: 15px;
  height: 15px;
}


.menu-sep {
  height: 1px;
  margin: 4px 0;
  background: var(--border);
}

.menu-foot {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px 10px 4px 8px;
  font-size: 0.74em;
  color: var(--text-muted);
  white-space: nowrap;
}

.menu-foot-update {
  color: var(--accent-text);
  text-decoration: none;
}

a.menu-foot-update:hover { text-decoration: underline; }

.menu-caret::after {
  content: "\203A";
  display: inline-block;
  transform: rotate(90deg);
  opacity: 0.6;
}

/* ---- Panels ---- */
.menu-host {
  position: relative;
  /* Stretches with the row, so the tab inside it reaches the bar's edge and
     the panel opens below the bar rather than below the label. */
  display: flex;
  align-items: stretch;
}

.menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  /* Wide enough for the longest heading a panel carries, "Show on the
     dashboard", so it stays on one line. */
  min-width: 212px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--menu-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-menu);
  padding: 5px;
  z-index: 1000;
}

/* Opens towards the middle of the bar rather than off its right edge. */
.menu-panel-end {
  left: auto;
  right: 0;
}

.menu-entry {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 7px 10px 7px 8px;
  border-radius: 6px;
  color: var(--text);
  font-size: 0.88em;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.menu-entry:hover { background: var(--control-hover-bg); }

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

/* The one that is open or shown carries a line at its left edge in the brand
   green, the mark the tabs carry at their bottom edge, and is bold. Every
   entry has the line, transparent where it is not the one, so the labels
   line up. */
.menu-entry {
  border-left: 3px solid transparent;
}

.menu-entry[aria-current] {
  border-left-color: var(--brand);
  font-weight: 700;
}

/* ---- The picker in the headline ----
   The dashboard headline names the printer and the log viewer names the log,
   and that name is the control. It has to read as one: a heading you can click,
   not a button dropped into a sentence. Hence the dotted underline, which the
   hover trades for the same background every other menu entry uses. */
.title-pick-host {
  display: inline-flex;
  align-items: baseline;
  gap: 10px;
  position: relative;
  /* Sits in a line of text, so it goes on that line rather than pushing it */
  vertical-align: baseline;
}

.title-pick {
  font: inherit;
  color: inherit;
  background: none;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 2px 8px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* The line sits under the name and not under the arrow, so the two read as a
   word and a control rather than as one underlined lump. */
.title-pick > span {
  text-decoration-line: underline;
  text-decoration-style: dotted;
  text-decoration-thickness: 1px;
  text-underline-offset: 5px;
  text-decoration-color: var(--border-control);
}

.title-pick:hover {
  background: var(--control-hover-bg);
}

.title-pick:hover > span { text-decoration-color: transparent; }

/* Set in the size of a heading, the shared caret would be as heavy as the
   words next to it. */
.title-pick.menu-caret::after {
  font-size: 0.62em;
  opacity: 0.5;
}

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

/* Nothing to choose: the name is what the page says, and says it as text. */
.title-pick-plain {
  cursor: default;
  padding: 0;
}

.title-pick-plain > span { text-decoration-line: none; }
.title-pick-plain:hover { background: none; }

/* Centred under the headline it belongs to, rather than under its left edge. */
.title-panel {
  left: 50%;
  transform: translateX(-50%);
  font-weight: 400;
  font-size: 1rem;
  text-align: left;
}

.menu-panel-head {
  padding: 7px 10px 3px 8px;
  font-size: 0.72em;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Not the first thing in a panel: a heading under entries needs a line above
   it, one at the very top does not. */
.menu-panel-head:not(:first-child) {
  margin-top: 4px;
  border-top: 1px solid var(--border);
  padding-top: 8px;
}

/* ---- Narrow screens ----
   The pages fold into one button next to the brand; the theme and the gear
   stay in the bar, and what the page is showing is in the page itself either
   way. */
.menu-burger {
  display: none;
}

@media (max-width: 760px) {
  /* Two of the fixed 170px tracks do not fit a phone, and one of them centred
     leaves a column with margins on both sides. Equal shares of the width
     instead: still two cells of the same size, still lined up. */
  .status-grid {
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    gap: 12px 18px;
  }

  /* Three across does not fit a phone, so the title takes the first line and
     the two that describe the state share the second, centred under it. The
     order is the reading order: what this is, then how it is doing. */
  .status-head {
    justify-content: center;
    gap: 8px 14px;
  }

  .status-title {
    order: -1;
    flex-basis: 100%;
  }

  .status-head-side { flex: 0 1 auto; }
  .status-head-side-end { justify-content: center; }

  /* The button that folds the pages is drawn like the gear at the other end:
     quiet, the full height of the bar, underlined while one of the pages it
     holds is the open one, so the mark of the open page reads the same on a
     phone as the tabs do on a desk. */
  .menu-burger {
    display: inline-flex;
    align-self: stretch;
    gap: 7px;
    padding: 0 8px;
    border: 0;
    border-top: 2px solid transparent;
    border-bottom: 2px solid transparent;
    border-radius: 0;
    background: none;
    color: var(--text-quiet);
    font-size: 0.9em;
  }

  .menu-burger:hover {
    background: none;
    color: var(--text);
  }

  .menu-burger[data-current="true"] {
    color: var(--text);
    font-weight: 700;
    border-bottom-color: var(--brand);
  }

  .menunav { gap: 8px; }

  .menu-pages {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 1000;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    min-width: 200px;
    background: var(--menu-bg);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--shadow-menu);
    padding: 5px;
  }

  .menunav.pages-open .menu-pages { display: flex; }

  /* In the folded menu a page is an entry of a list again, the current one
     filled: an underline under one line of a list marks nothing. */
  .menu-pages .menu-item {
    border: 1px solid transparent;
    border-radius: 6px;
    padding: 8px 12px;
    color: var(--text);
    font-size: 0.88em;
    justify-content: flex-start;
  }

  .menu-pages .menu-item:hover { background: var(--control-hover-bg); }

  .menu-pages .menu-item[aria-current] {
    background: var(--control-hover-bg);
    border-color: var(--border-control);
  }

  .menu-pages .menu-caret::after { margin-left: auto; }

  .menu-pages .menu-host { display: block; }

  /* The log list opens inside the folded menu rather than over it, so one
     panel does not cover the other on a screen this size. */
  .menunav.pages-open .menu-pages .menu-panel {
    position: static;
    max-height: none;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0 0 4px 14px;
    min-width: 0;
  }
}

#monitoring-control {
    margin-top: 20px;
    display: flex;
    align-items: center;
    gap: 10px;
}


.log-button {
  background: var(--control-bg);
  color: var(--text);
  border: 1px solid var(--border-control);
  border-radius: 6px;
  padding: 6px 14px;
  font-size: 0.88em;
  cursor: pointer;
  transition: background-color 0.2s, border-color 0.2s;
}

.log-button:hover { background: var(--control-hover-bg); }

/* Nothing to hand out yet: the button stays where it is, so the toolbar does
   not jump, and says so by fading rather than by disappearing. */
.log-button:disabled {
  opacity: 0.5;
  cursor: default;
}

.log-button:disabled:hover { background: var(--control-bg); }



/* Pinned to both edges rather than given a width: at 99% of the viewport it
   started at the padding of the body and ended 16px past the right edge of a
   phone, which cut the last words of the line off. */
.footer {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 10px 16px;
  background-color: var(--page-bg);
  color: var(--text);
}


.footer .row {
  justify-content: center;
}

.footer a {
  color: #007bff; /* Farbe der Links */
  text-decoration: none;
}

.footer a:hover {
  text-decoration: underline;
}
/* ---- Settings page ---- */
.set-page {
  padding-bottom: 6px;
}

.set-page-head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  margin: 18px 0 4px;
}

.set-page-head h1 {
  text-align: left;
  font-size: 1.5em;
  margin: 0;
}

.set-page-note {
  font-size: 0.82em;
  opacity: 0.7;
}

/* ---- Sections ----
   The navigation card at the left names the sections, the open one filled
   like the open page of a folded menu, and the cards of that section sit at
   the right. The card stays put while the section scrolls. A green dot on
   an entry marks a section with unsaved changes, so they are not lost from
   sight behind another section. */
.set-side {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
  /* The margins a card carries, so the navigation card sits where a card
     would, and the save bar keeps its distance from it when the open section
     is the shorter of the two. */
  margin: 14px 0;
}

/* A section spaces its cards with a gap rather than with the cards' own
   margins: the first card then starts level with the navigation card, the
   last ends where the grid ends, and two cards that sit in two group hosts
   keep the same distance as two in one. */
.set-section,
.set-section > [data-group] {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.set-section .set-card { margin: 0; }
.set-section > [data-group]:empty { display: none; }

.set-nav {
  position: sticky;
  top: 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  background: var(--panel-bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: var(--shadow-card);
}

.set-nav-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  font: inherit;
  font-size: 0.9em;
  color: var(--text);
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
}

.set-nav-item:hover { background: var(--control-hover-bg); }

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

.set-nav-item[aria-current="true"] {
  background: var(--control-hover-bg);
  border-color: var(--border-control);
  font-weight: 700;
}

.set-nav-glyph {
  width: 15px;
  height: 15px;
  flex: none;
  opacity: 0.8;
}

.set-dot {
  width: 7px;
  height: 7px;
  margin-left: auto;
  flex: none;
  border-radius: 50%;
  background: var(--brand);
}

.set-section[hidden] { display: none; }

/* The sections are hidden rather than unmounted and share one form, so the
   first card of a section starts where the cards did before. */
.set-sections { min-width: 0; }

/* Below a desk's width the card becomes a row above the content that scrolls
   sideways, the entries never wrapping: seven of them do not fit a phone in
   one line. The break is well above the phone's: next to a 200px card the
   printer table and the three column field grids have too little room long
   before the layout of a phone is called for. */
@media (max-width: 1000px) {
  .set-side { grid-template-columns: minmax(0, 1fr); }

  .set-nav {
    position: static;
    flex-direction: row;
    overflow-x: auto;
    scrollbar-width: none;
  }

  .set-nav::-webkit-scrollbar { display: none; }

  .set-dot { margin-left: 4px; }
}

.set-card {
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-bg);
  padding: 12px 16px 14px;
  box-shadow: var(--shadow-card);
  margin: 14px 0;
}


.set-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  padding-bottom: 8px;
  border-bottom: 1px solid #8882;
}

.set-card-head h2 {
  margin: 0;
  font-size: 1em;
}

.set-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px 20px;
  margin-top: 12px;
}

.set-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 0.88em;
  min-width: 0;
}

.set-field-label {
  font-weight: 600;
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  /* Keeps the inputs of a row aligned whether or not the label carries a pill */
  min-height: 22px;
}

.set-field-label .pill {
  font-size: 0.7rem;
  padding: 1px 7px;
  font-weight: 600;
}

.set-field input[type="text"],
.set-field input[type="number"],
.set-field input[type="password"],
.set-field select {
  padding: 5px 8px;
  border: 1px solid #8886;
  border-radius: 5px;
  /* See .sp-field above: the background of a select is what its option list is
     painted with. */
  background: var(--input-bg);
  color: inherit;
  font: inherit;
  font-size: 0.95em;
  min-width: 0;
}

.set-field input:disabled {
  opacity: 0.6;
  background: #8881;
  cursor: not-allowed;
}

.set-field input:focus,
.set-field select:focus {
  outline: none;
  border-color: var(--accent);
}


.set-field small {
  opacity: 0.7;
  font-size: 0.84em;
  line-height: 1.3;
}

/* ---- Log detail dialog ----
   The level is a ladder rather than a list of unrelated choices, so it is drawn
   as a slider with its steps written underneath. */
.set-slider {
  width: 100%;
  accent-color: var(--accent);
  margin: 2px 0 0;
}

.set-slider-scale {
  display: flex;
  justify-content: space-between;
  font-size: 0.78em;
  opacity: 0.75;
  /* The thumb sits inside the track, so the outer labels are pulled in to stay
     over the position they name rather than over the end of the box */
  padding: 0 2px;
}

.set-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
}

.set-check {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.95em;
}

.set-check input { accent-color: var(--accent); }

/* The export row of a printer's log detail dialog: the two ticks and the
   button that acts on them share a line, centred on each other, with the
   button set apart from the ticks it reads. */
.set-export-row { align-items: center; }
/* "dialog button" gives every button in a dialog a top margin, meant for the
   Save and Cancel row; this one sits on a line with the ticks. */
.set-export-row .btn { margin: 0 0 0 6px; }

/* One column: the dialog is narrow and every field here carries a description
   long enough that two columns would break the reading order. */
#logdetail-dialog { min-width: min(520px, 92vw); }
#logdetail-dialog .set-form { grid-template-columns: 1fr; }

/* A toggle reads better next to its label than under it */
.set-field-toggle {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 2px 10px;
}

.set-field-toggle .set-field-label { grid-column: 1; }
.set-field-toggle .set-switch { grid-column: 2; grid-row: 1 / span 2; }
.set-field-toggle small { grid-column: 1; }

/* Small variant of the monitoring switch */
.set-switch {
  position: relative;
  display: inline-block;
  width: 38px;
  height: 20px;
  flex: none;
}

.set-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.set-switch-track {
  position: absolute;
  inset: 0;
  background-color: var(--track-bg);
  border-radius: 999px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.set-switch-track::before {
  content: "";
  position: absolute;
  height: 14px;
  width: 14px;
  left: 3px;
  top: 3px;
  background-color: #fff;
  border-radius: 50%;
  transition: transform 0.2s;
}

.set-switch input:checked + .set-switch-track { background-color: #2ea35c; }
.set-switch input:checked + .set-switch-track::before { transform: translateX(18px); }

/* The one switch whose "off" is not a preference but a stopped service: while
   the sync is off nothing is read from the printer and nothing reaches
   Spoolman, so it stays red rather than going quiet grey like the settings. */
.set-switch-alarm .set-switch-track { background-color: var(--danger-solid); }

/* ---- A switch that belongs to the whole card, in its header ---- */
.set-head-field {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.88em;
  font-weight: 600;
}

.set-head-field label:not(.set-switch) {
  cursor: pointer;
}

/* Carries the description the field grid would have shown underneath */
.set-info {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border: 1px solid currentColor;
  border-radius: 50%;
  font-size: 0.68rem;
  font-weight: 700;
  font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
  opacity: 0.55;
  cursor: help;
  position: relative;
}

.set-info:hover,
.set-info:focus-visible {
  opacity: 1;
}

/* The same icon as a link, next to the API keys heading: it opens the API
   page rather than only telling. Inherits the colour so it reads as part of
   the heading, and a hand rather than the question mark cursor says it goes
   somewhere. */
.set-info-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  margin-left: 6px;
  vertical-align: 1px;
}

/* Opens to the right: the heading sits at the left edge of the card, so the
   tip would otherwise leave the page on that side. */
.set-info-link::after {
  left: -6px;
  right: auto;
}

/* Opens towards the page, the icon sits at the right edge of the card */
.set-info::after {
  content: attr(data-tip);
  position: absolute;
  top: calc(100% + 8px);
  right: -6px;
  z-index: 1200;
  width: max-content;
  max-width: 280px;
  padding: 7px 10px;
  border-radius: 8px;
  background: var(--tooltip-bg);
  color: #f1f1f1;
  font: inherit;
  font-size: 0.78rem;
  font-style: normal;
  font-weight: 400;
  font-family: Arial, sans-serif;
  line-height: 1.35;
  text-align: left;
  white-space: normal;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s;
}

.set-info:hover::after,
.set-info:focus-visible::after {
  opacity: 1;
  visibility: visible;
}


/* ---- Collapsed part of a group ---- */
.set-advanced {
  margin-top: 12px;
  border-top: 1px solid #8882;
  padding-top: 8px;
}

.set-advanced > summary {
  cursor: pointer;
  font-size: 0.84em;
  font-weight: 600;
  opacity: 0.75;
  list-style: none;
  display: flex;
  align-items: center;
  gap: 6px;
}

.set-advanced > summary::-webkit-details-marker { display: none; }

.set-advanced > summary::before {
  content: "\203A";
  display: inline-block;
  transition: transform 0.2s;
}

.set-advanced[open] > summary::before { transform: rotate(90deg); }
.set-advanced > summary:hover { opacity: 1; }

/* ---- Shared light table ----
   Used by the printer list, the dashboard dialogs and the "required but not
   loaded" list. Row separators only, no cell grid, no panel of its own, and
   identical in both themes. */
.data-table {
  width: 100%;
  border-collapse: collapse;
  margin: 12px 0 0;
  font-size: 0.9em;
  background: transparent;
}

.data-table th,
.data-table td {
  border: none;
  border-bottom: 1px solid #8882;
  padding: 7px 10px;
  text-align: left;
  background: transparent;
  color: inherit;
}

/* Column headers. A th outside a thead is a row label and keeps its size. */
.data-table thead th {
  font-size: 0.78em;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.6;
  padding-bottom: 5px;
}

.data-table th {
  font-weight: 600;
  white-space: nowrap;
}

.data-table tbody tr:last-child td,
.data-table tbody tr:last-child th,
.data-table tr:last-child td,
.data-table tr:last-child th { border-bottom: none; }

.data-table td:first-child { font-weight: 600; }

/* A value can carry a run of characters with nothing to break at — the tray
   UUID of a spool is 32 of them — and a cell cannot shrink below that. In the
   confirmation dialogs it pushed the table past the width of a phone, where
   the dialog cut the rest of the value off rather than scrolling to it. */
.data-table td { overflow-wrap: anywhere; }


/* The "required but not loaded" list sits inside the print card, so it stays
   tighter than a table of its own. */
.gc-required-table {
  font-size: 0.86em;
  margin-top: 6px;
}

.gc-required-table td { padding: 4px 0; }
.gc-required-table td:first-child { font-weight: normal; }
.gc-required-amount { text-align: right; white-space: nowrap; }

/* ---- API keys ---- */
/* A second block inside the Network access card rather than a card of its own:
   a key answers the same question the two fields above it do. */
.set-subsection {
  margin-top: 18px;
  padding-top: 12px;
  border-top: 1px solid #8882;
}

.set-subhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.set-subhead h3 {
  margin: 0;
  font-size: 0.95em;
}

/* Scrolls inside the card on a narrow screen, like the printer table */
#apikey-table {
  overflow-x: auto;
}

#apikey-table .data-table {
  min-width: 460px;
}

/* The key and its copy button on one line, the field taking what is left */
.set-key-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0;
}

.set-key-row input {
  flex: 1 1 auto;
  min-width: 0;
}

/* ---- Printer table ---- */
/* Scrolls inside the card on a narrow screen instead of widening the page */
#printer-table {
  overflow-x: auto;
}

#printer-table .data-table {
  min-width: 520px;
}

/* A printer's name is one word to the eye, so it is not broken inside like
   the long serials and addresses may be; the table scrolls instead. */
#printer-table .data-table td:first-child {
  overflow-wrap: normal;
  white-space: nowrap;
}

#printer-table .data-table tbody tr:hover td { background: #8881; }

.set-mono {
  font-family: monospace;
  font-size: 0.95em;
  opacity: 0.85;
}

/* Stays a normal table cell. As a flex container it sized itself to its
   buttons and its separator ended up a couple of pixels above the rest. */
.set-row-actions {
  text-align: right;
  white-space: nowrap;
}

.set-row-actions .btn + .btn {
  margin-left: 6px;
}

/* Puts a field back to the value the schema documents. Reads as a link rather
   than a button, it sits inside a field label. */
.set-reset {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 400;
  color: var(--accent);
  opacity: 0.8;
  cursor: pointer;
  text-decoration: underline;
}

.set-reset:hover { opacity: 1; }

/* ---- Buttons ---- */
.btn {
  padding: 6px 14px;
  border: 1px solid var(--border-control);
  border-radius: 6px;
  background: var(--control-bg);
  color: var(--text);
  cursor: pointer;
  font-size: 0.88em;
  transition: background-color 0.2s, border-color 0.2s, opacity 0.2s;
}

.btn:hover:not(:disabled) { background: var(--control-hover-bg); }

.btn:disabled {
  opacity: 0.45;
  cursor: default;
}

.btn-small {
  padding: 3px 10px;
  font-size: 0.82em;
}

.btn-primary {
  background: var(--accent-bg);
  border-color: var(--accent-bg);
  color: #fff;
  font-weight: 600;
}

.btn-primary:hover:not(:disabled) { background: var(--accent-bg-hover); }

.btn-danger {
  color: var(--bad);
  border-color: var(--danger-border);
}

.btn-danger:hover:not(:disabled) { background: var(--bad-bg); }

/* Filled rather than outlined, for the one action on a page that ends the
   process. .btn-danger is the quiet variant next to a table row; this one has
   to be found without looking for it. */
.btn-danger-solid {
  background: var(--danger-solid);
  border-color: var(--danger-solid);
  color: #fff;
  font-weight: 600;
}

.btn-danger-solid:hover:not(:disabled) {
  background: var(--danger-solid-hover);
  border-color: var(--danger-solid-hover);
}









/* ---- Action bar ---- */
.set-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 0 16px;
  background: var(--page-bg);
  border-top: 1px solid #8882;
  /* Lifts the bar off the card it scrolls over */
  box-shadow: 0 -6px 10px -6px rgba(0, 0, 0, 0.25);
}


/* A note that is a warning: the deprecated image name under the facts. */
.set-note-warn {
  color: var(--warn);
  font-weight: 600;
}

/* ---- Service card ---- */
/* Named "facts", not "info": .set-info is already taken by the round tooltip
   badge further up, and sharing the name left this list 16 px wide and round. */
/* A definition list rather than a table: the facts are label and value pairs,
   and the grid lets them reflow to one column on a phone without a card layout
   of their own. */
.set-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 8px 20px;
  margin: 14px 0 10px;
}

.set-fact dt {
  font-size: 0.78em;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.65;
}

.set-fact dd {
  margin: 2px 0 0;
  font-size: 0.95em;
}

/* Its own row rather than .set-row-actions, which is built for a table cell and
   never wraps. These are three full width labels and have to on a phone. */
.set-service-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}

/* Pushes the restart to the far end of the row, away from the three actions
   that leave the process running. On a narrow screen the row wraps and it ends
   up on its own line, still right aligned, which says the same thing. */
.set-action-end {
  margin-left: auto;
}

/* Used by the export dialog to spell out what anonymising replaces */
.set-list {
  margin: 6px 0 10px;
  padding-left: 20px;
  font-size: 0.9em;
  line-height: 1.6;
}

.set-actions-hint {
  font-size: 0.82em;
  opacity: 0.7;
}

/* ---- Connection test ---- */
/* Sits below the collapsed section, because the host and port in there are
   part of what is tested. Stretching the row keeps the result chip exactly as
   tall as the button next to it. */
.set-test-row {
  display: flex;
  align-items: stretch;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.set-test-result {
  font-size: 0.84em;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.set-test-result:empty { display: none; }

.set-test-result .pill {
  font-size: 0.74rem;
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
}

/* One line: chip first, reason behind it */
.set-test-row .set-test-result {
  flex-direction: row;
  align-items: stretch;
  gap: 8px;
}

.set-test-row .set-test-reason {
  display: flex;
  align-items: center;
  margin-left: 0;
}

.set-test-reason {
  opacity: 0.75;
  margin-left: 6px;
}

.set-test-failed { color: var(--bad); opacity: 1; }

#printer-test-result {
  margin-top: 12px;
}

/* ---- Notes, errors, banner ---- */
.set-note {
  font-size: 0.83em;
  opacity: 0.75;
  margin: 10px 0 0;
}

.set-note code {
  font-size: 0.95em;
  padding: 1px 5px;
  border-radius: 4px;
  background: #8881;
}

.set-note-warn {
  color: var(--warn);
  opacity: 1;
}


.set-error {
  color: var(--bad);
  font-size: 0.88em;
  margin: 10px 0 0;
}

.set-error:empty { display: none; }

.set-banner {
  border-radius: 8px;
  padding: 9px 14px;
  margin: 12px 0;
  font-size: 0.9em;
}

.set-banner:empty { display: none; }

/* The standing note at the head of the settings page, next to the badges that
   mark a single field. Blue rather than amber, because it describes how this
   installation is configured and not something that went wrong, and it stays
   on the page instead of being dismissed: the dashboard dialog is shown once
   per installation, and the rule it carries matters every time somebody edits
   a compose file. */
.set-page-info {
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--info-bg);
  color: var(--text);
  padding: 10px 14px;
  margin: 12px 0;
  font-size: 0.9em;
}

.set-page-info h2 {
  margin: 0 0 4px;
  font-size: 0.95em;
  color: var(--accent-text);
}

.set-page-info p { margin: 4px 0; }

.set-page-info p:last-child { margin-bottom: 0; }

.set-banner-ok   { background: var(--ok-bg);   color: var(--ok); }
.set-banner-warn { background: var(--warn-bg); color: var(--warn); }
.set-banner-bad  { background: var(--bad-bg);  color: var(--bad); }


#printer-dialog {
  min-width: min(560px, 86vw);
}

/* Links outside the footer and the menus, currently the pointer to the
   settings page shown on an empty dashboard. */
#spool-list a { color: var(--accent); }

/* ---- Spool detail dialog ----
   The filament name in both tables is the control that opens it, so it has to
   read as clickable without turning every row into a page of links. */
.spool-name-link {
  font: inherit;
  font-weight: bold;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  cursor: pointer;
  text-align: left;
  text-decoration: underline;
  text-decoration-color: #8886;
  text-underline-offset: 3px;
}

.spool-name-link:hover,
.spool-name-link:focus-visible {
  text-decoration-color: currentColor;
}

.sd-head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin: 6px 0 4px;
  padding-bottom: 14px;
  border-bottom: 1px solid #8883;
}

.sd-name { font-weight: 600; font-size: 1.08em; }
.sd-sub { font-size: 0.83em; margin-top: 4px; }

/* The way out to Spoolman belongs to the tab it is about, so it sits with the
   name instead of in the button row, where it read as a third action. */
.sd-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 0.83em;
  text-decoration: none;
}

.sd-link:hover { text-decoration: underline; }

/* Large enough to tell two shades of one filament apart, which the 12px swatch
   in the table cannot do. Same border and radius family as .gc-swatch. */
.sd-swatch {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  border: 1px solid #888;
  border-radius: 6px;
}

/* Capped the same way the assignment dialog is, so the dialog scrolls its body
   rather than growing past the viewport. */
.sd-scroll {
  max-height: 56vh;
  overflow-y: auto;
  text-align: left;
  padding-right: 8px;
}

/* Heavier and further apart than the section labels in the assignment dialog:
   this dialog stacks three blocks of dense rows, and they ran into each other. */
.sd-section {
  font-size: 0.92em;
  font-weight: 700;
  letter-spacing: 0.02em;
  margin: 26px 0 10px;
  padding-bottom: 6px;
  border-bottom: 2px solid var(--accent);
}

.sd-scroll > .sd-section:first-child { margin-top: 6px; }

.sd-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 28px;
}

.sd-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 7px 0;
  border-bottom: 1px solid #8882;
  font-size: 0.88em;
}

.sd-label { opacity: 0.7; white-space: nowrap; }

/* A row is value plus an optional pencil, laid out as a row of its own: as
   inline content the pencil kept breaking short values onto a second line. */
.sd-value {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  text-align: right;
  word-break: break-word;
}

/* The three writable fields are edited where they are read, so each of them
   carries a pencil instead of being repeated in a form of its own. It stands in
   front of the value, in the space every row has between label and value, so
   the values keep one right edge without every row reserving a column for it.
   Faint rather than hidden: appearing on hover alone made the rows look
   different from one another. */

.sd-edit,
.sd-confirm,
.sd-cancel {
  flex: 0 0 auto;
  border: none;
  background: none;
  padding: 0 2px;
  /* `dialog button` pushes every button in a dialog down by 20px, which is
     right for the row at the bottom and wrong for an icon inside a line. */
  margin: 0;
  cursor: pointer;
  color: inherit;
  font-size: 0.95em;
  line-height: 1;
}

.sd-edit { opacity: 0.4; transition: opacity 0.15s; }

.sd-row:hover .sd-edit,
.sd-edit:focus-visible { opacity: 0.75; }

.sd-edit:hover { opacity: 1; }

.sd-confirm { color: var(--accent); }
.sd-cancel { opacity: 0.6; }

/* A row being edited gives its whole width to the input, and stacks the reason
   a value was refused under it rather than beside it. */
.sd-row-editing { align-items: flex-start; }

.sd-row-editing .sd-value {
  flex: 1;
  flex-direction: column;
  align-items: stretch;
  gap: 4px;
}

.sd-editing {
  display: flex;
  align-items: center;
  gap: 4px;
  justify-content: flex-end;
  flex: 1;
}

.sd-input {
  flex: 1;
  min-width: 0;
  max-width: 240px;
  padding: 5px 8px;
  border: 1px solid var(--border-input);
  border-radius: 6px;
  background: var(--input-bg);
  color: inherit;
  font-size: 1em;
  font-family: inherit;
}

.sd-input:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px #1c6fd633;
}

.sd-input:disabled { opacity: 0.55; }



/* Sits under the input, in the row itself, so the reason stays next to the
   value it is about. */
.sd-inline-error {
  display: block;
  font-size: 0.85em;
  margin-top: 4px;
  text-align: right;
}

.sd-inline-error:empty { display: none; }

.sd-note { font-size: 0.83em; margin: 10px 0; line-height: 1.45; }
#sd-error:empty { display: none; }

#spool-detail-dialog { min-width: min(640px, 92vw); }
#spool-detail-title { margin-bottom: 4px; }

@media (max-width: 760px) {
  .sd-grid { grid-template-columns: 1fr; }
  .sd-swatch { width: 48px; height: 48px; }
  .sd-edit { opacity: 0.65; }
}

/* ---- Login page ---- */

.login-card {
  max-width: 380px;
  margin: 12vh auto 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-bg);
  padding: 22px 24px 26px;
  box-shadow: var(--shadow-card);
}

.login-card h1 {
  margin: 0;
  font-size: 1.15em;
  text-align: center;
}

.login-lead {
  margin: 0 0 6px;
  text-align: center;
  font-size: 0.88em;
  opacity: 0.75;
}

.login-label {
  font-size: 0.88em;
  font-weight: 600;
}

.login-card input[type="password"] {
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--control-bg);
  color: inherit;
  font: inherit;
  width: 100%;
  box-sizing: border-box;
}

.login-card input[type="password"]:focus {
  outline: none;
  border-color: var(--accent);
}

.login-card button {
  margin-top: 4px;
}

.login-error {
  margin: 0;
  font-size: 0.85em;
  color: var(--danger-solid);
  text-align: center;
}


/* ---- Responsive tables: stack each row as a card on small screens ---- */
@media (max-width: 760px) {
  /* Both the classic AMS table and the G-code table share .spool-table */
  .spool-table thead {
    /* hide the header row, labels come from data-label per cell */
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .spool-table,
  .spool-table tbody,
  .spool-table tr,
  .spool-table td {
    display: block;
    width: 100% !important;       /* override JS column-sync inline widths */
    min-width: 0 !important;
    box-sizing: border-box;
  }

  /* One unit, one card. The slots used to be four free floating tiles with the
     header hovering above them, so nothing but their order said they belonged
     to the same AMS. The card is the unit now: its header is the band, and the
     slots are sections inside it, parted by a line rather than by air. */
  .spool-table {
    margin-top: 22px;
    border: 1px solid var(--border-card);
    border-radius: 10px;
    background: var(--row-bg);
    box-shadow: var(--shadow-card);
    overflow: hidden;
  }

  .spool-table tr {
    border: none;
    background: transparent;
  }

  /* The one line inside the card that separates two slots. The fields within a
     slot carry no lines at all, so a line always means "next slot". */
  .spool-table tr + tr {
    border-top: 1px solid var(--border-card);
  }

  .spool-table td {
    /* The desktop rule draws a row separator, which inside a card would be a
       line between two fields of the same slot. */
    border: none;
    padding: 5px 14px;
    /* !important, because the weight columns carry an inline text-align: right
       written by the row markup. On a phone every cell is its own labelled
       line, so the value belongs under the label rather than at the far edge. */
    text-align: left !important;
  }

  /* Air around a slot rather than between its fields */
  .spool-table tr td:first-child { padding-top: 12px; }
  .spool-table tr td:last-child { padding-bottom: 14px; }

  /* Per-cell label above the value (robust for multi-line cell content) */
  .spool-table td::before {
    content: attr(data-label);
    display: block;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    font-weight: 600;
    margin-bottom: 2px;
  }

  /* The unit header, on a table whose rows are cards. A caption keeps its table
     display even when everything around it is a block, and shrink wraps into a
     column one word wide, so it is laid out by hand here.

     On a phone the slots of two units are a long column of identical cards with
     nothing but this line between them, so it is a band rather than a line of
     small print: it names the unit it opens and shows where the one above it
     ended. */
  caption.ams-env {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* The head of the unit card, so it sits on it rather than above it */
    margin: 0;
    border: none;
    border-bottom: 1px solid var(--border-card);
    border-radius: 0;
    /* Centred, both lines: the band spans the whole width, and its readings
       left packed under a centred card head looked like a leftover. */
    justify-content: center;
    text-align: center;
    gap: 4px 14px;
    padding: 9px 12px;
    background: var(--head-bg);
  }

  /* The gap spaces them now, and a trailing margin would push the band open on
     the right. */
  caption.ams-env > span,
  caption.ams-env .ams-env-readings > span { margin-right: 0; }

  caption.ams-env .ams-env-readings {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 14px;
  }

  caption.ams-env .ams-env-readings:empty { display: none; }

  /* The chevron at the right edge of the band, the rest stays centred. */
  .ams-toggle {
    float: none;
    margin-left: auto;
    order: 10;
  }

  /* Folded, the band is the whole card, so its own card frame is not needed */
  .spool-table.is-collapsed { border: 1px solid var(--border); }

  .spool-table.is-collapsed caption.ams-env {
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  caption.ams-env .ams-env-unit {
    font-size: 1.06em;
    color: var(--text);
  }

  /* The drying cycle carries a target and a countdown, which is more than fits
     next to the humidity on a phone. Its own line, under the readings it
     belongs to. */
  caption.ams-env .ams-env-drying {
    flex-basis: 100%;
    text-align: center;
  }



  /* Make the action button comfortably tappable */
  .spool-table td.action-cell button {
    min-height: 38px;
    padding: 6px 14px;
  }

  /* ---- The two tables of the settings page ----
     The printer list and the API keys are read left to right on a desktop and
     do not fit a phone at all: the status and the buttons sat behind a
     sideways scroll, which is the one place a phone will not look. Each row
     becomes a block with its labels above the values, the same way a spool
     row does. */
  .data-table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  /* The desktop min-width of these two tables would keep forcing the sideways
     scroll the cards exist to remove. */
  #printer-table .data-table,
  #apikey-table .data-table {
    min-width: 0;
  }

  #printer-table,
  #apikey-table { overflow-x: visible; }

  #printer-table .data-table,
  #printer-table tbody,
  #printer-table tr,
  #printer-table td,
  #apikey-table .data-table,
  #apikey-table tbody,
  #apikey-table tr,
  #apikey-table td {
    display: block;
    width: 100%;
    box-sizing: border-box;
  }

  #printer-table tr,
  #apikey-table tr {
    border: 1px solid var(--border-card);
    border-radius: 8px;
    background: var(--row-bg);
    padding: 4px 0 10px;
    margin-bottom: 12px;
  }

  #printer-table td,
  #apikey-table td {
    border: none;
    padding: 5px 12px;
    text-align: left;
  }

  #printer-table td::before,
  #apikey-table td::before {
    content: attr(data-label);
    display: block;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--text-muted);
    font-weight: 600;
    margin-bottom: 2px;
  }

  /* The name is the heading of its card, so it carries no label of its own
     and is set like one. */
  #printer-table td:first-child,
  #apikey-table td:first-child {
    font-size: 1.02em;
    font-weight: 700;
    padding-top: 9px;
  }

  #printer-table td:first-child::before,
  #apikey-table td:first-child::before { content: none; }

  /* An empty data-label leaves an empty line above the buttons */
  #printer-table td[data-label=""]::before,
  #apikey-table td[data-label=""]::before { display: none; }

  #printer-table .set-row-actions,
  #apikey-table .set-row-actions {
    display: flex;
    gap: 8px;
    margin-top: 4px;
  }

  #printer-table .set-row-actions .btn,
  #apikey-table .set-row-actions .btn {
    flex: 1;
    margin-left: 0;
    min-height: 36px;
  }

  /* The facts of the Service card, two to a line like the state of a printer
     on the dashboard rather than one per line. */
  .set-facts { grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); }

  /* The confirmation dialogs read as a label and a value per row. Held in two
     columns the label keeps a third of a phone to itself and the value is
     broken over five ragged lines next to it, so the row becomes a block here
     as well: the label over the value it names. */
  #dialog-content .data-table,
  #dialog-content .data-table tbody,
  #dialog-content .data-table tr,
  #dialog-content .data-table th,
  #dialog-content .data-table td {
    display: block;
    width: 100%;
    box-sizing: border-box;
  }

  #dialog-content .data-table th {
    /* The desktop rule keeps the label on one line, which is what made the
       column wide in the first place. */
    white-space: normal;
    border-bottom: none;
    padding: 8px 0 0;
  }

  #dialog-content .data-table td { padding: 2px 0 8px; }

  #dialog-content .data-table tr:last-child td { padding-bottom: 0; }

  /* The key of a new API key is long and has to be copied off the screen it is
     shown on, so it gets the full width and the button its own line under it. */
  .set-key-row { flex-wrap: wrap; }
  .set-key-row input { flex: 1 0 100%; }
  .set-key-row .btn { flex: 1; min-height: 36px; }

}

/* ---- API page ----
   Every route of the service as a row that opens into a form. Built on the
   settings page's cards and fields, so it reads as the fourth page of the same
   UI rather than as a tool bolted on. */
.api-description {
  font-size: 0.9em;
  line-height: 1.5;
  margin-top: 10px;
}

.api-description p { margin: 0 0 8px; }

/* At the right end of the headline, on the same baseline as the note. */
.api-back {
  margin-left: auto;
  text-decoration: none;
  white-space: nowrap;
}

/* The toolbar: two fields and the three buttons on one line, everything the
   same height and aligned along its bottom edge. Sticks under the top of the
   window while the routes scroll past. */
.api-toolbar {
  position: sticky;
  top: 8px;
  z-index: 5;
  display: flex;
  align-items: flex-end;
  gap: 14px 18px;
  flex-wrap: wrap;
}

.api-tool {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.api-tool-grow {
  flex: 1 1 220px;
}

.api-tool .set-field-label {
  font-size: 0.88em;
}

.api-tool input,
.api-form input[type="text"],
.api-form select,
.api-body {
  padding: 5px 10px;
  border: 1px solid #8886;
  border-radius: 5px;
  background: var(--input-bg);
  color: inherit;
  font: inherit;
  font-size: 0.95em;
  min-width: 0;
  box-sizing: border-box;
}

.api-tool input {
  height: 34px;
}

.api-tool input:focus,
.api-form input:focus,
.api-form select:focus,
.api-body:focus {
  outline: none;
  border-color: var(--accent);
}

.api-form small {
  opacity: 0.7;
  font-size: 0.84em;
  line-height: 1.3;
}

/* The tooltip of the key field opens to the right: the icon sits mid page
   rather than at the right edge of a card, where the settings page has it. */
.api-toolbar .set-info::after {
  left: -6px;
  right: auto;
}

.api-tool-buttons {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.api-tool-buttons .btn {
  height: 34px;
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  text-decoration: none;
}

.api-group-note {
  font-size: 0.82em;
  opacity: 0.7;
}

/* One route. The summary is the row; the body only exists once it is open. */
.api-op {
  border-bottom: 1px solid var(--border-row);
}

.api-op:last-child { border-bottom: none; }

.api-op-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 4px;
  cursor: pointer;
  list-style: none;
  min-width: 0;
}

.api-op-head::-webkit-details-marker { display: none; }

.api-op-head:hover { background: var(--row-hover-bg); }

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

.api-method {
  flex: none;
  width: 62px;
  text-align: center;
  padding: 3px 0;
  border-radius: 5px;
  font-size: 0.74em;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: #fff;
}

.api-method-get    { background: #1e7e44; }
.api-method-post   { background: #1c6fd6; }
.api-method-put    { background: #b26a00; }
.api-method-patch  { background: #8a5a00; }
.api-method-delete { background: #b3261e; }

.api-path {
  font-family: monospace;
  font-size: 0.92em;
  white-space: nowrap;
}

.api-summary {
  flex: 1;
  min-width: 0;
  font-size: 0.88em;
  opacity: 0.8;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.api-pill {
  background: var(--state-bg);
  color: var(--text-state);
  font-size: 0.72rem;
  padding: 2px 8px;
}

.api-anchor {
  flex: none;
  color: var(--text-muted);
  text-decoration: none;
  padding: 0 4px;
  opacity: 0;
}

.api-op-head:hover .api-anchor,
.api-anchor:focus-visible { opacity: 1; }

.api-op-body {
  padding: 4px 4px 14px 76px;
}

.api-desc {
  font-size: 0.88em;
  line-height: 1.5;
  margin: 0 0 10px;
}

.api-desc p { margin: 0 0 6px; }

.api-h4 {
  margin: 12px 0 6px;
  font-size: 0.82em;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  opacity: 0.75;
}

/* The status line of a response carries a sentence, and a sentence in capitals
   turns a field name in it into shouting. */
.api-h4 .pill,
.api-h4 .api-h4-text { text-transform: none; letter-spacing: 0; }

.api-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px 20px;
}

.api-body {
  width: 100%;
  resize: vertical;
  font-family: monospace;
  font-size: 0.88em;
  line-height: 1.4;
  tab-size: 2;
}

.api-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.api-muted {
  font-size: 0.84em;
  opacity: 0.7;
}

.api-warn { color: var(--warn); opacity: 1; }

.api-form-error:empty { display: none; }

/* The curl line, the headers and the shapes: folded by default, so the row
   opens into the form and nothing else. */
.api-curl,
.api-headers,
.api-schemas {
  margin-top: 10px;
  font-size: 0.86em;
}

.api-curl > summary,
.api-headers > summary,
.api-schemas > summary {
  cursor: pointer;
  opacity: 0.75;
  padding: 2px 0;
}

.api-pre {
  margin: 6px 0 0;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 5px;
  background: var(--log-bg);
  font-family: monospace;
  font-size: 0.9em;
  line-height: 1.45;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 480px;
  overflow: auto;
}

.api-response { margin-top: 12px; }

.api-response-head {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.api-response-body { margin-top: 8px; }

/* The shapes as a tree: the field name, its type, what it means, then its own
   fields one level in. */
.api-tree {
  margin: 2px 0 4px;
  padding-left: 18px;
  list-style: none;
  border-left: 1px solid var(--border-row);
}

.api-tree li {
  margin: 3px 0;
  line-height: 1.45;
}

.api-field {
  font-family: monospace;
  font-size: 0.95em;
  font-weight: 600;
}

.api-required::after {
  content: "*";
  color: var(--bad);
  margin-left: 1px;
}

.api-type {
  color: var(--accent-text);
  font-family: monospace;
  font-size: 0.9em;
}

.api-type-name {
  font-weight: 600;
  margin-right: 4px;
}

.api-field-desc { opacity: 0.75; }

@media (max-width: 760px) {
  .api-back { margin-left: 0; }
  .api-tool-grow { flex-basis: 100%; }
  .api-tool-buttons { width: 100%; }
  .api-tool-buttons .btn { flex: 1 1 auto; justify-content: center; white-space: nowrap; }
  /* Nothing to stick under on a phone, the bar would cover half the screen */
  .api-toolbar { position: static; }

  .api-op-head { flex-wrap: wrap; }

  .api-summary {
    flex: 1 0 100%;
    white-space: normal;
  }

  .api-op-body { padding-left: 4px; }

  /* Hover does not exist on a touch screen, so the link would sit there as a
     stray character. The address bar has the link once the row is open. */
  .api-anchor { display: none; }
}
