/* PrimeGrid Owner Panel — stylesheet.
 *
 * Every colour, radius and gap is a variable declared once in :root. That is the whole
 * point of this file: the old panel carried 306 hardcoded style="..." attributes across
 * its templates, so a colour change touched dozens of places and a light theme was
 * impossible — an inline style outranks any class. Nothing below hardcodes a colour;
 * when the light theme arrives it only redefines this block.
 *
 * Order matters a second way. The shared vocabulary comes first, then one section per
 * page: a class more than one page uses is declared above the page sections, because a
 * page section has to be deletable together with its page — and it is not, if three
 * other pages are drawing themselves out of it. A test holds this.
 */
:root {
  --bg: #070a11;
  --bg-sidebar: #0a0f18;
  --panel: #0e1522;
  --panel-head: #111a29;
  --panel-hover: #141d2e;
  --border: #1b2434;
  --border-soft: #16202e;

  --text: #e7edf7;
  --text-muted: #8a97ab;
  --text-dim: #5b6879;

  --accent: #3b82f6;
  --accent-soft: rgba(59, 130, 246, 0.14);
  --accent-line: rgba(59, 130, 246, 0.45);

  --green: #22c55e;
  --green-soft: rgba(34, 197, 94, 0.14);
  --red: #ef4444;
  --red-soft: rgba(239, 68, 68, 0.14);
  --amber: #f59e0b;
  --amber-soft: rgba(245, 158, 11, 0.14);
  --violet: #8b5cf6;
  --violet-soft: rgba(139, 92, 246, 0.14);
  --brand: #7dd93f;

  /* Text on a filled accent button, and the shadow under a raised card. Declared
     here for the same reason as every other colour: a value written into a rule is
     invisible to the light theme, which redefines this block and nothing else. */
  --on-accent: #06101f;
  --shadow-strong: #000;

  --radius: 14px;
  --radius-sm: 8px;
  --radius-pill: 999px;

  --gap-xs: 6px;
  --gap-sm: 10px;
  --gap: 16px;
  --gap-lg: 24px;

  --sidebar-w: 268px;   /* wide enough for the logo lockup at full size */
  --font: "Segoe UI", system-ui, -apple-system, sans-serif;
  --fs-sm: 0.78rem;
  --fs-base: 0.875rem;
  --fs-lg: 1.05rem;
  --fs-metric: 1.9rem;
}

/* Light theme: the same tokens, different values — nothing below this block changes.
   That is the whole return on never writing a colour into the markup.

   The values are not the dark ones inverted. On white, saturated accents shout and thin
   greys disappear, so the accent and the state colours are taken a shade deeper while
   the greys are lightened; the aim is the same contrast, not the same numbers. */
:root[data-theme="light"] {
  --bg: #f2f5f9;
  --bg-sidebar: #ffffff;
  --panel: #ffffff;
  --panel-head: #f4f7fb;
  --panel-hover: #eef3f9;
  --border: #dce3ec;
  --border-soft: #e9eef5;

  --text: #16202e;
  --text-muted: #5a6879;
  --text-dim: #8894a5;

  --accent: #2563eb;
  --accent-soft: rgba(37, 99, 235, 0.10);
  --accent-line: rgba(37, 99, 235, 0.40);

  --green: #15803d;
  --green-soft: rgba(21, 128, 61, 0.12);
  --red: #dc2626;
  --red-soft: rgba(220, 38, 38, 0.10);
  --amber: #b45309;
  --amber-soft: rgba(180, 83, 9, 0.12);
  --violet: #6d28d9;
  --violet-soft: rgba(109, 40, 217, 0.10);
  --brand: #5f9e28;
}

/* Everything the page starts from, in one rule. The scrollbar pair used to live in a
   second `*` block further down, beside the ::-webkit- rules it belongs with by subject
   — but two rules under one selector is the sheet's own duplicate, and the audit that
   reported "no duplicate selectors" had simply not looked. */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  /* Firefox's half of the scrollbar styling; Chromium's ::-webkit- half is below, with
     the note explaining why both are needed. */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}

body {
  font-family: var(--font);
  font-size: var(--fs-base);
  background: var(--bg);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* Scrollbars, in the panel's own colours.
 *
 * Left alone they are drawn by the system: a pale Windows bar down the side of a dark
 * error box, and the one part of the page that ignored the theme entirely. Declared
 * globally rather than per element — the panel scrolls in five places (a long table, an
 * error box, the sidebar on a tall page) and a half-themed set reads worse than none.
 *
 * Two syntaxes, because no browser understands both: scrollbar-color is Firefox's and
 * lives in the reset at the top of this file; the ::-webkit- pseudo-elements below are
 * Chromium's and Safari's.
 */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }

/* The padding is drawn with a transparent border rather than with padding: a scrollbar
   thumb has no box model of its own, and background-clip is what keeps the colour off
   that border. */
::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--radius-pill);
  border: 2px solid transparent;
  background-clip: content-box;
}

::-webkit-scrollbar-thumb:hover { background: var(--text-dim); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* ---------------------------------------------------------------- layout */

.shell {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
  min-height: 100vh;
}

.content {
  min-width: 0;                 /* lets wide tables scroll instead of stretching */
  padding: var(--gap-lg);
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* --------------------------------------------------------------- sidebar */

/* Pinned: the menu is how you leave the page, and on a long card it used to scroll away
   with the content, leaving the system line stranded at the very bottom. Sticky rather
   than fixed, so it still sits in the layout and the content keeps its own width.
   The sidebar scrolls inside itself only if it outgrows the window. */
.sidebar {
  background: var(--bg-sidebar);
  border-right: 1px solid var(--border);
  padding: var(--gap-lg) var(--gap);
  display: flex;
  flex-direction: column;
  gap: var(--gap-lg);
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
}

/* The mark and the word have to read as one lockup: same height, barely a gap.
   Capitals sit at roughly 0.72 of the font size, so a 24px mark needs type around 2rem
   — that is where the sidebar width comes from, not the other way round. */
.brand {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 var(--gap-xs);
  font-size: 2rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
}

/* Matched to the capitals next to it — literally: 1cap is the cap height of the font in
   use, so the mark is exactly as tall as the letters and stays that way if the type
   size changes. The pixel value above it is the fallback for browsers without the unit.
   (The shipped asset had to be cropped to its own edges first: in the original the mark
   floats in a large transparent canvas, which made it look smaller than the text.) */
.brand-mark {
  height: 27px;
  height: 1.15cap;   /* a shade taller than the capitals: measured, the mark's own edges
                        sit just inside its box, so 1cap renders visibly shorter */
  width: auto;
  flex: none;
}

.brand-prime { color: var(--text); }
.brand-grid { color: var(--brand); }

.nav {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  padding: var(--gap-sm) var(--gap-sm);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  border: 1px solid transparent;
}

.nav-item:hover { background: var(--panel-hover); color: var(--text); }

.nav-item.is-active {
  background: var(--accent-soft);
  border-color: var(--accent-line);
  color: var(--text);
}

.nav-item svg { flex: none; opacity: 0.85; }
.nav-label { flex: 1; }

.sidebar-foot { margin-top: auto; }

.system-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  padding: var(--gap);
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
}

.system-title { font-weight: 600; }
.system-note { color: var(--text-muted); font-size: var(--fs-sm); }

.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
  background: var(--text-dim);
}

.dot-ok { background: var(--green); box-shadow: 0 0 0 4px var(--green-soft); }

/* ---------------------------------------------------------------- topbar */

.topbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap);
}

.page-title { font-size: 1.55rem; font-weight: 700; }
.page-subtitle { color: var(--text-muted); font-size: var(--fs-sm); margin-top: 2px; }

.topbar-actions { display: flex; align-items: center; gap: var(--gap); }

.who { text-align: right; line-height: 1.25; }
.who-name { font-weight: 600; }
/* «Выйти» is a submit button, because signing out changes something and a GET must not.
   Stripped back to the link it used to be so the corner looks unchanged. */
.who-form { display: inline; }
.who-link {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  /* Both inherited on purpose: a button does NOT take these from its surroundings the
     way the <a> here used to — the browser gives form controls their own — and without
     them the word sits a pixel or two off the name above it. */
  line-height: inherit;
  text-align: inherit;
  cursor: pointer;
}
.who-link:hover { color: var(--accent); }

.theme-switch {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-xs);
  padding: 7px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
}

.theme-switch:hover { border-color: var(--accent-line); color: var(--text); }

/* ----------------------------------------------------------- stat cards */

.metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: var(--gap);
}

.metric {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
  display: flex;
  gap: var(--gap-sm);
  align-items: flex-start;
}

.metric-icon {
  width: 38px;
  height: 38px;
  border-radius: var(--radius-sm);
  display: grid;
  place-items: center;
  flex: none;
}

.metric-icon.tone-accent { background: var(--accent-soft); color: var(--accent); }
.metric-icon.tone-green { background: var(--green-soft); color: var(--green); }
.metric-icon.tone-amber { background: var(--amber-soft); color: var(--amber); }
.metric-icon.tone-violet { background: var(--violet-soft); color: var(--violet); }

.metric-body { min-width: 0; }
.metric-label { color: var(--text-muted); font-size: var(--fs-sm); }
.metric-value { font-size: var(--fs-metric); font-weight: 700; line-height: 1.2; }
.metric-note { color: var(--text-dim); font-size: var(--fs-sm); margin-top: 2px; }

/* Three panels abreast for the account card: the summaries are label/value lists, and
   three of them side by side beat one long column that pushes the tables off screen. */
.grid-3 {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap);
  align-items: stretch;
}

.grid-3 > .panel { display: flex; flex-direction: column; }
.grid-3 > .panel .panel-scroll { flex: 1; }

@media (max-width: 1400px) {
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 1000px) {
  .grid-3 { grid-template-columns: minmax(0, 1fr); }
}

/* A label/value list. Not a table: these are single facts, and a table would put a
   border between every one of them. */
.facts { display: flex; flex-direction: column; }

.fact {
  display: flex;
  gap: var(--gap-sm);
  padding: 7px var(--gap);
  border-bottom: 1px solid var(--border-soft);
}

.fact:last-child { border-bottom: none; }
.fact-label { color: var(--text-muted); flex: 0 0 42%; }
.fact-value { flex: 1; min-width: 0; }
.fact-note { color: var(--text-dim); font-size: var(--fs-sm); }

.panel-note {
  padding: var(--gap);
  border-top: 1px solid var(--border-soft);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* Breadcrumbs live in the subtitle slot of the shell. */
.crumb { color: var(--text-muted); }
.crumb:hover { color: var(--accent); }

/* A one-line message above the page: the result of an action, or a job in flight. */
.banner {
  padding: var(--gap-sm) var(--gap);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

.banner-ok { background: var(--green-soft); border-color: var(--green); color: var(--green); }
.banner-warn { background: var(--red-soft); border-color: var(--red); color: var(--red); }
.banner-info { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }

/* The answer to the last action, with a cross to take it down (_components.html). */
.banner-result {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-sm);
}
.banner-close {
  flex: none;
  border: none;
  background: none;
  color: inherit;
  font-size: 1.15rem;
  line-height: 1;
  padding: 0 2px;
  cursor: pointer;
  opacity: 0.7;
}
.banner-close:hover { opacity: 1; }

/* The headline strip carries more, shorter values than the dashboard's cards. */
.metrics-tight { grid-template-columns: repeat(auto-fit, minmax(165px, 1fr)); }
.metrics-tight .metric-value { font-size: 1.35rem; }

/* --------------------------------------------------------------- panels */

/* Panels go straight into this grid, two per row. Grid levels every cell in a row by
   itself, which is what pairs the summary with the sessions above it and the weekly
   cycle with the ranking — nesting columns instead made a short panel stretch to the
   height of the whole column.
   Exactly two, not auto-fit: on a wide screen auto-fit found room for a third column
   and broke the pairing, putting three panels on the first row and one alone below. */
.grid-2 {
  display: grid;
  /* Not equal halves: the left column carries the tables — item names and account
     labels — while the right one holds short label/number pairs that need far less
     room. Giving the tables the extra width is what stops the names being cut. */
  grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr);
  gap: var(--gap);
  align-items: stretch;
}

/* Two panels of the same weight side by side: on the account card, the snapshot history
   and the snapshot-task history, both of about the same seven columns. The 1.45 split
   above suits a table beside a short list, and would cut one of these two. */
.grid-2.even { grid-template-columns: repeat(2, minmax(0, 1fr)); }

/* The account card's three histories: their wrapped columns (a drop's name, an error)
   take whatever width is left instead of the usual 220px floor. That floor pushed the
   snapshot-task table a few dozen pixels past its half of the row, and the play-session
   table past a laptop screen, and hung a sideways scrollbar under each for nothing
   (owner, 28.09). On a wide screen the table still hands them the spare width. */
.fit-wrap td.wrap { min-width: 0; }

@media (max-width: 1100px) {
  .grid-2 { grid-template-columns: minmax(0, 1fr); }
}

/* The even pair holds two tables of seven columns, and on a 1366px laptop each half is
   too narrow for its table: measured, the snapshot-task table stuck out by 48px there.
   Stacked below this width, as the other pairs are below 1100px. */
@media (max-width: 1400px) {
  .grid-2.even { grid-template-columns: minmax(0, 1fr); }
}

.grid-2 > .panel { display: flex; flex-direction: column; }
.grid-2 > .panel .panel-scroll { flex: 1; }

/* A cell of the grid holding more than one block, stacked. The grid spaces its columns
   and nothing else, so without this the two panels in the right column of the groups
   page sat against each other with no gap at all — the class was on the markup but the
   sheet had no rule for it. */
.col {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding: var(--gap) var(--gap);
  border-bottom: 1px solid var(--border-soft);
}

.panel-title { font-size: var(--fs-lg); font-weight: 600; }

.panel-action {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.panel-action:hover { color: var(--accent); }

.panel-scroll { overflow-x: auto; }

/* A long list scrolls inside its own panel. Nothing is cut off any more — the queries
   fetch everything the account has, and the panel decides only how much is on screen at
   once. Height is counted in rows so the two panels of a pair line up: fifty rows for
   the inventory pair, twenty-five for the histories. Rows carrying an item picture are
   taller, hence the second variable. */
.panel-scroll.scroll-y { overflow-y: auto; }
/* Twenty for the play sessions on the account card: they run across the full width
   right under the inventory, and past that they scroll rather than push the rest of the
   card down (owner, 28.09). */
.rows-20 { max-height: calc(20 * var(--row-h, 2.05rem) + 2.6rem); }
.rows-25 { max-height: calc(25 * var(--row-h, 2.05rem) + 2.6rem); }
.rows-50 { max-height: calc(50 * var(--row-h, 2.05rem) + 2.6rem); }
.rows-tall { --row-h: 3.1rem; }

/* The header stays put while the rows move under it. */
.scroll-y thead th { position: sticky; top: 0; z-index: 1; }

/* A list that fills the rest of the window and scrolls inside itself, the way a
   spreadsheet does (the accounts list, 28.09, owner). It is wider than any screen, and
   its sideways scrollbar sat under the last row: to move right you first scrolled to the
   bottom, then back up. Now the window is the list's frame — the sideways bar is at the
   bottom of the screen from the first moment, the header row stays on top, and the first
   column, whose account the row is, stays on the left.

   A page opts in with body.page-fill. Below a usable height the list keeps a floor and
   the page scrolls as it used to, rather than squeezing the list to a strip. */
.page-fill .shell { height: 100vh; }
.page-fill .content { height: 100vh; overflow-y: auto; }
.page-fill .content > .panel:last-child {
  flex: 1 0 360px;
  display: flex;
  flex-direction: column;
}

.list-fill {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.table-frame { flex: 1; min-height: 0; overflow: auto; }

/* Separate borders: a collapsed border belongs to the table, not the cell, and a cell
   that stays put while the rest scrolls would leave its row lines behind. */
.table-frame table { border-collapse: separate; border-spacing: 0; }
.table-frame thead th { position: sticky; top: 0; z-index: 2; }
.table-frame th:first-child,
.table-frame td:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--panel);
  box-shadow: inset -1px 0 0 var(--border-soft);
}
.table-frame thead th:first-child { z-index: 3; background: var(--panel-head); }
.table-frame tbody tr:hover td:first-child { background: var(--panel-hover); }

/* The "no group" card: one number that has to be noticed, and the way to act on it. */
.unassigned {
  padding: var(--gap);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-xs);
}

.unassigned-count { font-size: var(--fs-metric); font-weight: 700; line-height: 1.1; }
.unassigned .btn { margin-top: var(--gap-xs); }

/* Icon, title and a line of explanation — what a panel that performs an action leads
   with. Six pages open a panel this way; a page that needs the lead wider says so
   itself, next to its own form. */
.panel-lead { display: flex; align-items: center; gap: var(--gap-sm); }
.panel-lead-title { font-weight: 600; }
.panel-lead-note { color: var(--text-muted); font-size: var(--fs-sm); }

/* An empty list says what would be here and what puts it here — "нет данных" alone
   leaves the reader unable to tell a working page from a broken one. */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--gap-xs);
  padding: var(--gap-lg);
  color: var(--text-muted);
  text-align: center;
}

.empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: 600; }

input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); }

/* --------------------------------------------------------------- tables */

table { width: 100%; border-collapse: collapse; }

th {
  text-align: left;
  padding: var(--gap-sm) var(--gap);
  color: var(--text-dim);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
  background: var(--panel-head);
}

td {
  padding: var(--gap-sm) var(--gap);
  border-top: 1px solid var(--border-soft);
  white-space: nowrap;
}

/* Long free text — an item name is two items joined by a plus — wraps instead of
   pushing the table wider than its panel and raising a scrollbar. */
td.wrap { white-space: normal; min-width: 220px; }

tbody tr:hover td { background: var(--panel-hover); }

.cell-empty {
  text-align: center;
  color: var(--text-dim);
  padding: var(--gap-lg);
}

.num { font-variant-numeric: tabular-nums; }
.rank { color: var(--text-dim); }
.muted { color: var(--text-muted); }

/* A count that should have been zero. The operations page used to borrow .amount-out
   for this, which is the ledger's word for money going out — the right red for the
   wrong reason, and it tied a page that has nothing to do with money to the ledger's
   section of this sheet. */
.num-alert { color: var(--red); }

/* A dense table: eighteen columns of account data have to fit the screen without a
   horizontal scrollbar. Padding is the whole problem — at 16px a side that is nearly
   600px of air alone — so it is halved here, headings are allowed to wrap onto two
   lines, and the in-row controls shrink to match. Applied per table, not globally:
   the dashboard's short tables read better with the roomier default. */
.table-dense th,
.table-dense td { padding: 7px 9px; }
.table-dense th { white-space: normal; }
.table-dense { font-size: var(--fs-sm); }
.table-dense select { padding: 4px 6px; font-size: var(--fs-sm); }
.table-dense .btn-sm { padding: 3px 8px; }
.table-dense .drop-pic { width: 34px; height: 34px; }

/* The controls that live in a table cell — buttons, a rename box and its OK. They wrap
   onto a second line on a narrow screen instead of widening the table. */
.row-actions { display: flex; flex-wrap: wrap; gap: var(--gap-xs); align-items: center; }
.row-actions input[type="text"] { width: 132px; }

/* A column of ticks or counts, centred under a heading that is centred with it. */
th.center, td.center { text-align: center; }

/* --------------------------------------------------------------- badges */

.badge {
  display: inline-block;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--fs-sm);
  font-weight: 600;
  background: var(--panel-hover);
  color: var(--text-muted);
}

/* Every state the panel can show is named here, and the name is the state's own. A
   status with no rule of its own still renders — grey, from .badge above — but grey is
   what "we have no opinion" looks like, so a state that has an opinion belongs in one
   of these four lists. A test holds every status of an account and of a snapshot task
   to that.
   The snapshot statuses used to be translated in Python into the names below instead:
   succeeded borrowed "completed", partial borrowed "pending", and everything the
   translation did not know fell through to "new". A cancelled task and one whose source
   never answered were both painted the blue of a task that has not started. */
.badge-active,
.badge-completed,
.badge-succeeded { background: var(--green-soft); color: var(--green); }

/* Work not finished: waiting for a worker, or being done right now. The snapshot task
   walks through seven of these on its way, and each is a step, not a result. */
.badge-new,
.badge-started,
.badge-assigned,
.badge-queued,
.badge-claimed,
.badge-authenticating,
.badge-web_authenticated,
.badge-family_view_unlocked,
.badge-collecting,
.badge-normalizing,
.badge-submitting { background: var(--accent-soft); color: var(--accent); }

.badge-failed,
.badge-vac_banned,
.badge-permanent_ban,
.badge-blocked { background: var(--red-soft); color: var(--red); }

.badge-cancelled,
.badge-archived,
.badge-rate_limited,
.badge-requires_review,
.badge-partial,
.badge-pending { background: var(--amber-soft); color: var(--amber); }

/* The ways a snapshot task can end badly, each named by the worker. Before these rules
   the whole column was painted red by hand, which made a cancelled task — not a
   malfunction at all — look like a failure. */
.badge-auth_failed,
.badge-guard_failed,
.badge-family_view_locked,
.badge-source_unavailable { background: var(--red-soft); color: var(--red); }

/* An agent that has stopped reporting is not a failure — it is a machine that is off.
   Amber says "not working right now" without the alarm red carries. */
.badge-offline { background: var(--amber-soft); color: var(--amber); }

/* The same colours without the pill: a status shown as a plain word under a name. It
   reuses .badge-<status> for the colour, so a status never has to be listed twice. */
.badge.status-plain {
  background: none;
  padding: 0;
  font-weight: 400;
}

/* A dot inside the pill. It takes currentColor, so a status needs no rule of its own
   for the dot to be the right colour — it is always the colour of the word beside it. */
.badge-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  margin-right: 6px;
  vertical-align: 1px;
}
.badge-disabled { background: var(--panel-hover); color: var(--text-dim); }

/* ------------------------------------------------------ operational summary */

.summary {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  height: 100%;
}

.summary-side {
  padding: var(--gap);
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  justify-content: space-evenly;   /* fills the height it was given, evenly */
}
.summary-side + .summary-side { border-left: 1px solid var(--border-soft); }

.stat-line { display: flex; align-items: center; gap: var(--gap-sm); }
.stat-line-icon { color: var(--text-dim); flex: none; display: flex; }
.stat-line-body { flex: 1; min-width: 0; }

.stat-line-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gap-sm);
}

.stat-line-label { color: var(--text-muted); }
.stat-line-value { font-weight: 700; }

.bar {
  margin-top: 6px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: var(--panel-head);
  overflow: hidden;
}

.bar-fill { height: 100%; border-radius: var(--radius-pill); background: var(--accent); }
.bar-fill.tone-green { background: var(--green); }
.bar-fill.tone-amber { background: var(--amber); }
.bar-fill.tone-red { background: var(--red); }

/* --------------------------------------------------------------- controls */

.toolbar {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--gap);
}

/* The toolbar laid out along one line instead of stacked: the filter on the left, and
   whatever the page sets beside it — tiles, a count, an action — on the right. Beside
   the filter rather than above it, so it does not read as something the filter changed. */
.toolbar-row {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
}

.toolbar-actions { display: flex; align-items: center; gap: var(--gap-sm); flex-wrap: wrap; }

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--gap-xs);
  padding: 8px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-head);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}

.btn:hover { border-color: var(--accent-line); }

.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; }
.btn-primary:hover { filter: brightness(1.08); }

.btn-sm { padding: 4px 10px; font-size: var(--fs-sm); }

.btn-danger { color: var(--red); border-color: transparent; background: var(--red-soft); }
.btn-danger:hover { border-color: var(--red); }

/* Every text-like field, listed by type. A password left off this list falls back to
   the browser default — a white box in a dark panel — and worse, changes appearance
   the moment the eye reveals it, because a revealed password is type="text". */
select,
input[type="text"],
input[type="search"],
input[type="number"],
input[type="password"],
textarea {
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  padding: 7px 10px;
}

/* A field the browser filled in itself — a saved sign-in — is repainted by the browser's
   own stylesheet: a pale box in a dark panel, with dark text, over the rule above (owner,
   25.09). Its background cannot be taken back, so the field is flooded from the inside
   with the same colour instead, and the text is given the panel's colour. The long
   transition keeps Chromium from fading its own colour in underneath. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px var(--panel-head) inset;
  box-shadow: 0 0 0 1000px var(--panel-head) inset;
  caret-color: var(--text);
  transition: background-color 9999s ease-out 0s;
}

select:focus, input:focus { outline: 1px solid var(--accent-line); }

/* The browser's own number spinner cannot be restyled — Chromium lets a page hide it and
   nothing more, so its grey arrows appeared on hover in the middle of a dark field. It is
   switched off here and drawn again below as two buttons that take their colours from the
   same tokens as everything else. */
input[type="number"] { appearance: textfield; -moz-appearance: textfield; }

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  appearance: none;
  margin: 0;
}

/* Field and arrows share one border, so the pair reads as a single control: the input
   gives up its own outline and the wrapper carries it. */
.stepper {
  display: flex;
  align-items: stretch;
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.stepper:focus-within { outline: 1px solid var(--accent-line); }

.stepper input[type="number"] {
  border: none;
  border-radius: 0;
  background: transparent;
  min-width: 0;
  flex: 1;
}

.stepper input[type="number"]:focus { outline: none; }

.stepper-buttons {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--border);
  flex: none;
}

.stepper-btn {
  display: grid;
  place-items: center;
  width: 26px;
  flex: 1;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}

.stepper-btn + .stepper-btn { border-top: 1px solid var(--border); }
.stepper-btn:hover { background: var(--panel-hover); color: var(--text); }

.row-form { display: flex; gap: 4px; align-items: center; }

/* The filter bar, two rows: the search and the page's actions on top, the filters
   themselves below. Each filter keeps its caption above the control — with five of them
   in a row, a bare select says nothing about what it narrows. */
.toolbar-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  flex-wrap: wrap;
}

.toolbar-filters {
  display: flex;
  align-items: flex-end;
  gap: var(--gap-sm);
  flex-wrap: wrap;
}

.filter { display: flex; flex-direction: column; gap: 4px; }
/* The caption is a <span> where the whole field is wrapped in a label, and a real
   <label for> where the control is a group the label cannot wrap. Same look either way. */
.filter > span,
.filter > label { color: var(--text-muted); font-size: var(--fs-sm); }
.filter select { min-width: 150px; }
.filter-search { min-width: 280px; }
/* A field that must not stretch to the width of the panel it sits in. */
.filter-narrow { max-width: 320px; }

/* ------------------------------------------------------------- pagination */

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  flex-wrap: wrap;
  padding: var(--gap);
  border-top: 1px solid var(--border-soft);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.pager-pages { display: flex; align-items: center; gap: var(--gap-xs); }

.page-btn {
  min-width: 34px;
  padding: 5px 10px;
  text-align: center;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
}

.page-btn:hover { border-color: var(--accent-line); color: var(--text); }
.page-btn.is-current { background: var(--accent-soft); border-color: var(--accent-line); color: var(--text); }
.page-btn.is-disabled { opacity: 0.4; pointer-events: none; }

/* The version of the running backend, where a desktop app would put it. */
.sidebar-version { color: var(--text-dim); font-size: var(--fs-sm); padding: 0 var(--gap-xs); }

/* --------------------------------------------------------- drop pictures */

/* A reward is a group of items, so a row carries one picture per item, side by side.
   The name gets a column of its own with min-width: 0 — without it a long name refuses
   to shrink below its longest word, overflows the cell and wraps under the pictures
   instead of staying beside them. */
.drop { display: flex; align-items: center; gap: var(--gap-sm); }
.drop-pics { display: flex; gap: 4px; flex: none; }
/* Two lines at most, then an ellipsis — the full name is in the cell's tooltip.
   The row height is set by whichever is taller, the picture or the text, and a
   three-line name was making the block grow more than any icon size did. */
.drop-name {
  flex: 1;
  min-width: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.drop-pic {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--panel-head);
  display: grid;
  place-items: center;
  overflow: hidden;
  color: var(--text-dim);
}

.drop-pic img { width: 100%; height: 100%; object-fit: contain; }

/* A session that picked two rewards (owner, 05.10): one line each, one under the other —
   never side by side, which would widen a column and push the table past the screen.
   A value line is as tall as a reward line (the picture sets its height: two lines of
   name are shorter), so each price sits level with its own reward. */
.drop-stack { display: flex; flex-direction: column; gap: 6px; }
.drop-value {
  min-height: 48px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.table-dense .drop-value { min-height: 34px; }
/* The sum under the values, and an empty line of the same height under the rewards:
   both stacks are then equally tall, centre alike, and stay level line by line. */
.drop-total,
.drop-total-pad { height: 1.3em; line-height: 1.3em; }
/* The same on the account card, where a session's drop is words alone: every line
   two lines of text high, the names cut to two, so names and values stay level. */
.drop-line {
  height: 2.6em;
  display: flex;
  align-items: center;
}
.drop-line.drop-value { min-height: 0; }

/* ------------------------------------------------------------------ forms */

/* A form that fills a panel of its own: heading, a row of fields, then the button. The
   fields are a grid rather than a flex row so all three keep the same width whatever
   their captions say. */
.form-panel { display: flex; flex-direction: column; gap: var(--gap); padding: var(--gap); }

.form-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--gap);
}

.form-grid input { width: 100%; }

/* Four fields where the operator form has three. Same grid, one more column. */
.form-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 1200px) {
  .form-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 900px) {
  .form-grid { grid-template-columns: minmax(0, 1fr); }
  .form-grid-4 { grid-template-columns: minmax(0, 1fr); }
}

/* One row of controls, wrapping rather than overflowing; the submit sits at the far
   end so it is in the same place whatever the row wraps to. */
.form-row { display: flex; align-items: center; gap: var(--gap-sm); flex-wrap: wrap; }
.form-row .grow { flex: 1 1 220px; }
.form-row .btn-primary { margin-left: auto; }
.form-row .stepper-narrow { width: 96px; }

/* The password and the button that reveals it share one border, like the stepper. */
.password-field { display: flex; align-items: stretch; }

.password-field input {
  flex: 1;
  min-width: 0;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right: none;
}

.password-eye {
  display: grid;
  place-items: center;
  width: 38px;
  flex: none;
  padding: 0;
  border: 1px solid var(--border);
  border-left: none;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: var(--panel-head);
  color: var(--text-dim);
  cursor: pointer;
}

.password-eye:hover { color: var(--text); }

/* A checkbox and its words, kept on one line. Shared rather than the problem types
   page's own since the operators page ticks groups with it (05.10). */
.check {
  display: flex;
  align-items: center;
  gap: var(--gap-xs);
  color: var(--text-muted);
  font-size: var(--fs-sm);
  white-space: nowrap;
}

/* ------------------------------------------------------------------- text */

/* A standing note under a list — not a banner: a banner reports the result of an
   action and goes away, this explains the page and always applies. */
.hint {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-sm);
  padding: var(--gap);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.hint-icon { color: var(--accent); flex: none; display: flex; }

/* An identifier the system matches on, shown as one. */
.code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  color: var(--accent);
}

/* Text that is also a way in — a count that leads to what it counted, a name that
   leads to the thing named. */
.link { color: var(--accent); }
.link:hover { text-decoration: underline; }

/* A verdict is an icon and a word, with .verdict-note underneath it for the why. Both
   verdicts in the sessions column use these, so the drop check and the ban check cannot
   drift into looking like different kinds of thing. Shared rather than the sessions
   page's own since the agents page marks a client that cannot update itself with it
   (02.10) — filed under one page, it would go when that page did. */
.verdict { display: flex; align-items: center; gap: var(--gap-xs); }
.verdict.is-ok { color: var(--green); }
.verdict.is-warn { color: var(--amber); }

/* The line under a verdict or a row that says why it came out that way. */
.verdict-note { color: var(--text-muted); font-size: var(--fs-sm); margin-top: 2px; }

/* Money in and money out must not look alike at a glance: a ledger is read by sign
   before it is read by number. Shared rather than the ledger's own since the
   operators list started colouring balances with it. */
.amount-in { color: var(--green); }
.amount-out { color: var(--red); }

/* ---------------------------------------------------------------- agents */

/* The token form is one row: what it does on the left, its two fields in the middle,
   the button on the right. A panel rather than the .toolbar used elsewhere — this is
   not a filter bar over a list, it is an action with its own explanation. */
.token-form {
  display: flex;
  align-items: flex-end;
  gap: var(--gap-lg);
  flex-wrap: wrap;
  padding: var(--gap);
}

/* The shared lead, given a width that belongs to this row and to no other page. */
.token-form .panel-lead { flex: 1 1 260px; }
.token-form .filter { flex: 0 1 240px; }
.token-form input { width: 100%; }

/* The raw token is shown once and stored nowhere — it is hashed on the way in. Hence a
   block of its own rather than a line of text: this is the only moment it exists. */
.token-reveal { padding: var(--gap); display: flex; flex-direction: column; gap: var(--gap-xs); }

.token-value {
  display: block;
  background: var(--panel-head);
  border: 1px solid var(--green);
  border-radius: var(--radius-sm);
  padding: var(--gap-sm) var(--gap);
  color: var(--green);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  word-break: break-all;
}

/* Steam gets its own mark rather than a word: one picture reads across sixteen rows in
   a glance, where a column of "да/нет" does not. A logo cannot take a colour from us,
   so the state is told by draining it instead — full colour is running, grey is not,
   and the two greys differ in weight. The title spells it out on hover either way. */
.state-logo { width: 20px; height: 20px; display: block; }
.state-logo.is-idle { filter: grayscale(1); }
.state-logo.is-off { filter: grayscale(1); opacity: 0.28; }

/* Load is a pair of bars, not a pair of numbers: the point of the column is to spot the
   machine that is straining, and a bar is read without being parsed. */
.hw { display: flex; flex-direction: column; gap: 5px; min-width: 132px; }
.hw-line { display: flex; align-items: center; gap: var(--gap-xs); }
.hw-name { color: var(--text-dim); font-size: var(--fs-sm); width: 30px; flex: none; }
.hw-value { width: 34px; flex: none; text-align: right; font-size: var(--fs-sm); }
.hw .bar { margin-top: 0; flex: 1; min-width: 46px; }

/* The last error is free text from the agent and can be a paragraph. One line with an
   ellipsis, the whole of it in the tooltip. */
.err-cell { max-width: 220px; overflow: hidden; text-overflow: ellipsis; color: var(--red); }

/* ------------------------------------------------------------- operators */

/* The groups an operator is handed first, edited where they are read (owner, 05.10).
   Drawn as the panel's own select box — same ground, border and arrow as the filters —
   because that is what it is; a <details> underneath, since several groups may be
   ticked and a real <select> offers that only as a tall multi-line list. */
.pool-select { display: inline-block; }

.pool-select-box {
  position: relative;
  display: block;
  min-width: 150px;
  max-width: 220px;
  padding: 7px 30px 7px 10px;
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pool-select-box::-webkit-details-marker { display: none; }
.pool-select-box:hover { border-color: var(--accent-line); }
.pool-select[open] > .pool-select-box { outline: 1px solid var(--accent-line); }

/* The arrow a select draws, drawn: a chevron of two borders. */
.pool-select-box::after {
  content: "";
  position: absolute;
  right: 12px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-right: 1.5px solid var(--text-muted);
  border-bottom: 1.5px solid var(--text-muted);
  transform: translateY(-70%) rotate(45deg);
}

/* The list opens over the page: the panel clips what leaves it, so it is placed by the
   page's script under its box (operators.html) rather than inside the row. */
.pool-select-menu {
  position: fixed;
  z-index: 50;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.pool-select-option {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  padding: 7px 10px;
  border-radius: var(--radius-sm);
  color: var(--text);
  cursor: pointer;
  white-space: nowrap;
}

.pool-select-option:hover { background: var(--panel-hover); }
.pool-select-option:has(input:checked) { background: var(--accent-soft); }
.pool-select-empty { padding: 7px 10px; font-size: var(--fs-sm); white-space: normal; max-width: 260px; }

.pool-select-actions {
  display: flex;
  justify-content: flex-end;
  padding: 6px 4px 2px;
  margin-top: 2px;
  border-top: 1px solid var(--border-soft);
}

/* Where an operator plays, on one line: the machine in use or the latest, and how many
   more the week had; the full list is in the title. */
.machine-line { display: inline-flex; align-items: center; gap: var(--gap-xs); white-space: nowrap; }

/* An owner, marked beside the login (06.10): small, so the cell keeps its width. */
.role-tag {
  margin-left: 4px;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--fs-sm);
  font-weight: 600;
}

/* The row's actions behind one box (06.10): the role joined the deactivation, and two
   buttons side by side do not fit. The groups box's look, narrower; its entries are
   buttons, each its own form. */
.actions-select .pool-select-box { min-width: 0; }

.menu-action {
  display: flex;
  align-items: center;
  gap: var(--gap-xs);
  width: 100%;
  padding: 7px 10px;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.menu-action:hover { background: var(--panel-hover); }
.menu-action-danger { color: var(--red); }

/* ------------------------------------------------------------- run feed */

/* A run's own history, folded under its row. A <details>, so it costs no script and the
   browser remembers nothing to go wrong — the same way the bulk-add page hides its
   format notes.

   Shared, not a section of one page: the sessions list draws it under a play session and
   the account card under a snapshot task. It lived among the sessions rules while only
   that page used it, and the day the card started drawing the same feed those rules
   became a shared thing filed under someone else's name — delete that page one day and
   the card silently loses its layout. A test holds this.

   Collapsed by default on purpose: the list is read to find a row, and a dozen events per
   row would bury the twenty rows it is there to show. */
.run-feed-row > td { padding-top: 0; border-top: none; }
.run-feed > summary { cursor: pointer; color: var(--text-muted); font-size: var(--fs-sm); }
.run-feed > summary:hover { color: var(--text); }
.run-feed[open] > summary { margin-bottom: var(--gap-xs); }

/* Three columns, because the middle one is the point: the gap since the previous event
   is what turns a list of names into "signing in took 28 seconds and then CS2 was gone
   for six minutes". Fixed widths so the titles line up down the whole run. */
.run-feed-line {
  display: grid;
  grid-template-columns: 68px 92px 1fr;
  gap: var(--gap-xs);
  padding: 2px 0;
  font-size: var(--fs-sm);
}

.run-feed-time { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.run-feed-gap { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.run-feed-line.is-failure .run-feed-title { color: var(--red); }
.run-feed-detail { grid-column: 3; color: var(--text-muted); overflow-wrap: anywhere; }
.run-feed-total { margin-top: var(--gap-xs); padding-top: var(--gap-xs); border-top: 1px solid var(--border-soft); color: var(--text-muted); font-size: var(--fs-sm); }

/* --------------------------------------------------------------- sessions */

/* The same cards, laid out in a line instead of a full-width grid: here they share a
   row with the filter and must take only the width they need. */
.metrics-inline {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap-sm);
}

.metrics-inline .metric { padding: var(--gap-sm) var(--gap); min-width: 148px; }
.metrics-inline .metric-icon { width: 32px; height: 32px; }

.verdict-block { margin-top: var(--gap-xs); padding-top: var(--gap-xs); border-top: 1px solid var(--border-soft); }

/* -------------------------------------------------------- problem reports */

/* The reason, its recorded code and what the type does to an account, on one line.
   Wrapping rather than overflowing: three labels do not fit a narrow column. */
.reason { display: flex; align-items: center; gap: var(--gap-xs); flex-wrap: wrap; }

/* Wide enough for a sentence — the note is free text an operator typed in a hurry. */
.note-input { width: 260px; }

/* ------------------------------------------------------- problem types */

/* The rules a form works by, under the form. */
.form-hints {
  display: flex;
  flex-direction: column;
  gap: 4px;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* Archived rows stay in the table, greyed: they still resolve for any client that
   sends their code, so hiding them would misrepresent what the server accepts. */
tr.is-archived { opacity: 0.55; }

.order-input { width: 72px; text-align: center; }

/* -------------------------------------------------------------- ledger */

.amount-input { width: 120px; text-align: right; }

/* The three sums under the journal. Right-aligned as a group so they sit under the
   amount column they summarise, and read as a footer of the table rather than as a
   fourth control. */
.totals-row {
  display: flex;
  justify-content: flex-end;
  gap: 28px;
  padding: 14px 16px;
  border-top: 1px solid var(--border);
}

.total { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; }
.total-label { font-size: 12px; color: var(--text-muted); }
.total-value { font-size: 16px; font-weight: 600; }

/* ---------------------------------------------------------- operations */

/* An initial in a circle beside the provider's name — enough to tell two rows apart at
   a glance without shipping anyone's logo. */
.provider { display: flex; align-items: center; gap: var(--gap-sm); }

.provider-mark {
  width: 26px;
  height: 26px;
  flex: none;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
  font-size: var(--fs-sm);
}

/* A worker's error is whatever a remote service said — sometimes a sentence, sometimes
   a page of JSON. Boxed and scrolled inside its own cell, it cannot stretch the table;
   before this the longest one decided the width of every column. */
.error-box {
  display: flex;
  align-items: flex-start;
  gap: var(--gap-xs);
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--gap-xs) var(--gap-sm);
  max-width: 620px;
  max-height: 6.5rem;
  overflow: auto;
}

.error-box code {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: var(--fs-sm);
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text-muted);
  flex: 1;
  min-width: 0;
}

.error-copy {
  flex: none;
  padding: 2px;
  border: none;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  position: sticky;
  top: 0;
}

.error-copy:hover { color: var(--accent); }

/* ------------------------------------------------------- bulk account add */

/* The list is typed, pasted and read back, so it is monospace and roomy: four fields a
   line only line up in a fixed pitch, and a mistyped one is found by looking down the
   column. */
.account-text {
  width: 100%;
  min-height: 260px;
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: var(--fs-sm);
  line-height: 1.6;
  resize: vertical;
}

/* The format, spelled out on demand. A <details> rather than a dialog: no script, and
   it stays open while the list beside it is being fixed. */
.examples { border: 1px solid var(--border); border-radius: var(--radius-sm); }
.examples[open] { background: var(--panel); }

.examples > summary {
  list-style: none;
  cursor: pointer;
  padding: 6px 12px;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  display: flex;
  align-items: center;
  gap: var(--gap-xs);
}

.examples > summary::-webkit-details-marker { display: none; }
.examples > summary:hover { color: var(--text); }

.examples-body {
  padding: 0 12px 10px;
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* The same two steps every inset block in the panel uses: the panel's surface, and one
   shade lighter for what sits on it. It was painted with --bg — the page's own ground,
   which no component wears — and read as a hole cut in the card. */
.examples-body pre {
  margin: 6px 0 0;
  padding: 8px 10px;
  background: var(--panel-head);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  overflow-x: auto;
  color: var(--text);
}

/* The head of the list block: what it is on the left, the format opener on the right. */
.block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  flex-wrap: wrap;
}

/* ----------------------------------------------------------------- login */

/* The only screen with no shell around it, so it carries its own background. */
.login-screen {
  position: relative;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: var(--gap-lg);
  background: var(--bg);
}

/* The pattern is drawn in gradients rather than shipped as a picture: dots and hatching
   in the mark's two colours, one set to each corner. It is faded, and masked away from
   the middle — the card has to sit on a clean ground, not on a texture. */
.login-screen::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  opacity: 0.18;
  background-image:
    radial-gradient(circle, var(--accent) 1px, transparent 1px),
    repeating-linear-gradient(-45deg, var(--accent) 0 1px, transparent 1px 28px),
    radial-gradient(circle, var(--brand) 1px, transparent 1px),
    repeating-linear-gradient(45deg, var(--brand) 0 1px, transparent 1px 28px);
  background-size: 28px 28px, auto, 28px 28px, auto;
  background-position: left top, left top, right bottom, right bottom;
  background-repeat: repeat;
  mask-image: radial-gradient(ellipse 60% 60% at 50% 50%, transparent 25%, #000 90%);
  -webkit-mask-image: radial-gradient(ellipse 60% 60% at 50% 50%, transparent 25%, #000 90%);
}

/* The card. Its edge is a gradient rather than a border colour: two backgrounds, one
   clipped to the padding box and one to the border box, which is the only way to put a
   gradient on a rounded border without an extra element. */
.login-card {
  position: relative;
  z-index: 1;
  width: min(440px, 100%);
  padding: var(--gap-lg);
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  border: 1px solid transparent;
  border-radius: 20px;
  background-image:
    linear-gradient(var(--panel), var(--panel)),
    linear-gradient(160deg, var(--accent), var(--brand));
  background-origin: border-box;
  background-clip: padding-box, border-box;
  box-shadow: 0 0 60px -12px var(--accent-line), 0 24px 60px -30px var(--shadow-strong);
}

.login-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--gap-sm);
  font-size: 1.9rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.login-brand .brand-mark { height: 1.15cap; width: auto; }

/* A caption with a rule running out to both sides. */
.login-rule {
  display: flex;
  align-items: center;
  gap: var(--gap-sm);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

.login-rule::before,
.login-rule::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.login-rule > span { display: inline-flex; align-items: center; gap: var(--gap-xs); }

.login-form { display: flex; flex-direction: column; gap: var(--gap); }

/* A field with a mark inside it: the icon sits on top of the input rather than beside
   it, so the box stays one box. */
.field { position: relative; display: flex; align-items: center; }

.field-icon {
  position: absolute;
  left: 12px;
  display: flex;
  color: var(--text-dim);
  pointer-events: none;
}

.field input { width: 100%; padding-left: 38px; }
.field input.has-eye { padding-right: 42px; }

.field-eye {
  position: absolute;
  right: 8px;
  display: grid;
  place-items: center;
  padding: 4px;
  border: none;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}

.field-eye:hover { color: var(--text); }

/* The one gradient button in the panel, in the two colours of the mark. Dark text
   because both ends of the gradient are bright. */
.login-submit {
  justify-content: center;
  padding: 12px;
  font-size: var(--fs-lg);
  font-weight: 700;
  color: var(--on-accent);
  border-color: transparent;
  background: linear-gradient(90deg, var(--accent), var(--brand));
}

.login-submit:hover { filter: brightness(1.08); border-color: transparent; }

.login-foot { margin-top: var(--gap-xs); }
