/* components.css — what sits inside a page: cards, toolbars, tables, forms,
 * badges, the record view, the alert dock and the sign-in gate.
 *
 * Class names are shared with the page scripts, which build rows and badges in
 * JavaScript. Renaming one here is renaming it there.
 */

/* ── Card ───────────────────────────────────────────────────────────
 *
 * One object: a heading strip, a body, and the controls that act on what is in
 * it. `.block` is the same thing under its old name, kept because several page
 * scripts address it.
 */
.card, .block {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  overflow: hidden;
}

.card + .card, .block + .block, .card + .block, .block + .card {
  margin-top: var(--sp-4);
}

/* A card that leads with a .page-head rather than a .card-head (Storage's alert
   card) still needs its title and sub-line inset off the card edges. */
.card > .page-head {
  padding: var(--sp-4) var(--sp-4) 0;
  margin-bottom: var(--sp-3);
}

.card-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule-soft);
  background: var(--tint-soft);
}

.card-head h2, .card-head h3, .block > h2 {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 14px;
  font-weight: 700;
}

.block > h2 {
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule-soft);
  background: var(--tint-soft);
}

.card-head .head-sub {
  flex: none;
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-60);
}

.card-head .head-actions { display: flex; gap: var(--sp-2); align-items: center; }

.card-body { padding: var(--sp-4); }
.card-body > :first-child { margin-top: 0; }

.card-foot, .block-actions {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border-top: 1px solid var(--rule-soft);
  background: var(--tint-soft);
}

.block-actions .msg { flex: 1 1 12rem; margin: 0; }

/* A card-like block nested INSIDE a card -- a small form (the article-history
   sighting recorder) that sits above a card's table without being a card of its
   own. No shadow, and a margin so it insets off the host card's edges. */
.sub-card {
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin: var(--sp-4);
  overflow: hidden;
}
.sub-card > h3 { margin: var(--sp-4) var(--sp-4) var(--sp-2); }
.sub-card > .head-sub { display: block; margin: 0 var(--sp-4) var(--sp-3); }
.sub-card > .grid { padding: 0 var(--sp-4) var(--sp-4); }

/* ── Pager ──────────────────────────────────────────────────────────
 *
 * The strip under a table: how much there is, how many rows to a page, and
 * the way to the next one. It lives in `.card-foot`, so it inherits the rule
 * and the tint and sits on one line with the message beside it.
 *
 * The count is pushed left and the steps right, with the size chooser between
 * them, because the count is read and the steps are aimed at.
 */
.pager {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  margin-inline-start: auto;  /* the message in the foot keeps the start */
}

.pager-count {
  color: var(--ink-60);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.pager-size { display: flex; align-items: center; gap: var(--sp-2); }
.pager-size .lbl { margin: 0; font-size: 12px; color: var(--ink-60); }
.pager-size select { height: 28px; padding: 0 var(--sp-2); font-size: 12px; }

.pager-steps { display: flex; align-items: center; gap: 4px; }

/* Fixed-width steps, so the strip does not shift sideways when First and
   Previous become available on the way off page one. */
.pager-steps .btn { min-width: 28px; padding: 0 6px; }

.pager-at { display: flex; align-items: center; gap: 6px; margin: 0 4px; }

.pager-at input {
  width: 3.5rem;
  height: 28px;
  padding: 0 6px;
  font-size: 12px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.pager-of { color: var(--ink-60); font-size: 12px; white-space: nowrap; }

/* On a narrow screen the count goes on its own line above the controls
   rather than squeezing them: the controls are what somebody is reaching
   for. */
@media (max-width: 640px) {
  .pager { width: 100%; margin-inline-start: 0; }
  .pager-count { flex: 1 1 100%; }

  /* A card head is a row on a desktop — title on the left, its one-line note
     off to the right — and the note does not shrink. At a phone's width the
     note then demands the whole line and starves the title to nothing, so a
     heading like "Waiting for somebody" breaks to one word a line under the
     note laid across it. Stacked, each gets its own line and its full width. */
  .card-head { flex-direction: column; align-items: flex-start; gap: 2px; }
  .card-head .head-actions { margin-top: var(--sp-2); }
}

/* A section inside a card. */
.group { border-top: 1px solid var(--rule-soft); }
.group:first-child { border-top: 0; }

.group > h3 {
  margin: 0;
  padding: var(--sp-3) var(--sp-4) 0;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-60);
}

/* ── Toolbar ────────────────────────────────────────────────────────
 *
 * The filters over a table, on one line where they fit. It is a strip inside
 * the card rather than a panel above it, because it acts on the rows below and
 * nothing else.
 */
.toolbar {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--rule-soft);
}

.toolbar .field { min-width: 9rem; }
.toolbar .field.wide { flex: 1 1 18rem; }
.toolbar .spacer { flex: 1; }

/* ── Fields ─────────────────────────────────────────────────────────
 *
 * `.grid` is the form layout: label above control, as many columns as fit.
 * `.cell` is one field, and it is also the read-only half of the record view,
 * which is why the label markup is the same in both.
 */
.grid, .form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: var(--sp-3) var(--sp-4);
  padding: var(--sp-4);
}

.grid-stacked { grid-template-columns: 1fr; }
.grid.tight { gap: var(--sp-2) var(--sp-4); }

.cell, .field { min-width: 0; }
.cell label, .field label { display: block; cursor: pointer; }
/* A field given the whole row (resource.js `wide`) rather than one column. */
.cell-wide { grid-column: 1 / -1; }

.lbl {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-60);
}

.lbl-sub {
  display: block;
  margin-bottom: 3px;
  font-size: 11.5px;
  color: var(--ink-45);
}

.lbl-sub:empty { margin-bottom: 3px; }

.req { color: var(--err); margin-inline-start: 2px; }

/* The value in a read-only cell: a stat, or one field of a record. */
.val {
  display: block;
  margin-top: 2px;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.val.mute { color: var(--ink-45); font-weight: 400; }

input[type=text], input[type=search], input[type=password], input[type=email],
input[type=number], input[type=date], input:not([type]), select, textarea {
  width: 100%;
  height: var(--btn-height);
  padding: 0 9px;
  border: 1px solid var(--ink-25);
  border-radius: var(--radius-field);
  background: var(--paper);
  color: var(--ink);
  font: inherit;
}

textarea { height: auto; min-height: 72px; padding: 7px 9px; line-height: 1.5; }

input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent-dark);
  outline-offset: -1px;
  border-color: var(--accent-dark);
}

input[disabled], select[disabled], textarea[disabled] {
  background: var(--tint);
  color: var(--ink-60);
  cursor: not-allowed;
}

input[type=checkbox] { width: auto; height: auto; }

.check {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  height: var(--btn-height);
  font-size: 13px;
}

.check input { margin: 0; }

.row { display: flex; gap: var(--sp-2); align-items: center; }
.row input { flex: 1 1 18rem; width: auto; }

/* ── Buttons ────────────────────────────────────────────────────── */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  height: var(--btn-height);
  padding: 0 14px;
  border: 1px solid var(--ink-25);
  border-radius: var(--radius-field);
  background: var(--paper);
  color: var(--ink);
  font: 600 13px/1 var(--sans);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.btn:hover { background: var(--tint); color: var(--ink); border-color: var(--ink-45); }

/* The one thing you are meant to press, in Vietjet's own call-to-action
   yellow under dark text -- a far stronger contrast than white on red, and it
   leaves red to mean alarm. */
.btn.primary {
  border-color: var(--accent-dark);
  background: var(--accent-gradient);
  color: var(--on-accent);
}

.btn.primary:hover { background: var(--accent-dark); color: var(--on-accent); }

.btn.danger { border-color: var(--err-line); background: var(--err-bg); color: var(--err); }
.btn.danger:hover { background: var(--err); color: #fff; border-color: var(--err); }

.btn.ghost { border-color: transparent; background: none; color: var(--ink-60); }
.btn.ghost:hover { background: var(--tint); color: var(--ink); }

.btn[disabled] { opacity: .5; cursor: not-allowed; }
.btn[disabled]:hover { background: var(--paper); border-color: var(--ink-25); }

.btn-small { height: 28px; padding: 0 10px; font-size: 12px; }
.btn .mark { font-size: 13px; opacity: .8; }

/* A control that is a sentence, not a button: "close by hand", "clear the
   filters". Quiet on purpose -- it is the second choice on its row. */
.linkish {
  border: 0;
  background: none;
  padding: 0 6px;
  color: var(--ink-60);
  font: inherit;
  font-size: 12px;
  text-decoration: underline;
  cursor: pointer;
}

.linkish:hover { color: var(--brand); }

/* ── Tables ─────────────────────────────────────────────────────── */

.scroll, .table-wrap { overflow-x: auto; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 8px var(--sp-3);
  border-bottom: 1px solid var(--rule);
  background: var(--tint);
  color: var(--ink-60);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  text-align: start;
  white-space: nowrap;
}

/* A table whose columns fall into named groups draws a top tier over the label
   row -- one cell per group, spanning its columns, with a blank spacer over any
   ungrouped column. This band scrolls away with the page; the label row beneath
   it is the one that stays sticky, so the two never fight over `top: 0`. */
thead tr.colgroups th {
  position: static;
  padding: 5px var(--sp-3);
  background: var(--tint);
  text-align: center;
  color: var(--ink-45);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
  border-bottom: 0;
}

/* Where two groups meet: one rule that runs the full height of the table -- the
   group band, the sticky label row, and every body cell in the boundary column
   all carry it, so the eye reads two halves rather than one wide strip. */
thead th.col-divider,
tbody td.col-divider { border-inline-start: 2px solid var(--rule); }

tbody td {
  padding: 8px var(--sp-3);
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: top;
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td { background: var(--tint-soft); }

/* A row that opens something. The whole row is the target, so the cursor says
   so -- a link in one column is a smaller target for the same result. */
tbody tr.clickable { cursor: pointer; }
tbody tr.clickable:focus-visible { outline: 2px solid var(--accent-dark); outline-offset: -2px; }
tbody tr[aria-selected="true"] td { background: var(--warn-bg); }

td.empty, .empty {
  padding: var(--sp-5) var(--sp-3);
  color: var(--ink-45);
  font-style: italic;
  text-align: center;
}

/* An identifier: a serial, an account code, a card uid, an error name. Read
   character by character, so it gets the monospace face. */
.code-no { font-family: var(--mono); font-size: 12px; }

td.actions { white-space: nowrap; text-align: end; }
td.actions .btn + .btn { margin-inline-start: 4px; }

.num { text-align: end; font-variant-numeric: tabular-nums; }

/* Row tints. The whole row, not one cell: a reader scanning thirty rows is
   reading the list, and a coloured word inside a row is invisible until you
   are already reading that row. */
tr.row-error td { background: var(--err-bg); }
tr.row-warning td { background: var(--warn-bg); }
tr.row-info td { color: var(--ink-60); }
tr.row-critical td { background: var(--err-bg); font-weight: 600; }

/* The row the Alerts page linked to, focused for a moment on arrival so the
   eye lands on the alert that was clicked rather than on the top of the list. */
tr.row-focus td {
  box-shadow: inset 2px 0 0 var(--brand);
  animation: row-focus-fade 2.4s ease-out 1;
}
@keyframes row-focus-fade {
  0%, 40% { background: var(--warn-bg); }
  100% { background: transparent; }
}

/* The Recommission form opened under a Missing Article row: the cabinet to
   assign, the remark, and the two buttons, on one wrapping line. */
.inline-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--sp-3);
  padding: var(--sp-2) 0;
}
.inline-form .field { margin: 0; }
.inline-form .msg { flex-basis: 100%; margin: 0; }

/* ── Status vocabulary ──────────────────────────────────────────── */

.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  white-space: nowrap;
}

.badge.on   { background: var(--ok-bg);   color: var(--ok); }
.badge.warn { background: var(--warn-bg); color: var(--warn); }
.badge.off  { background: var(--err-bg);  color: var(--err); }
.badge.mute { background: var(--tint);    color: var(--ink-60); }
.badge.info { background: var(--info-bg); color: var(--info); }

/* Several in one cell -- a ledger line can be offline AND flagged AND a
   correction -- so they need a gap and somewhere to wrap to. */
.badge + .badge { margin-inline-start: 4px; }

/* A run of badges that is a SET rather than a list of states: the roles one
   person holds. Wraps onto a second line with the same gap it has on the
   first, which the margin above cannot do on its own. */
.tag-row { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.tag-row .badge + .badge { margin-inline-start: 0; }

/* An RFID tag's EPC. A bare hex run reads as one more serial in a page full of
   serials; drawn as a chip -- a little luggage-tag, then the grouped hex on a
   tinted card with an edge -- it reads as the physical label it names. The mark
   carries the meaning, so it comes first and takes the surrounding colour; the
   code keeps the monospace face it is compared digit by digit in. The whole
   chip is inline and baseline-aligned, so it drops into a table cell, a
   sentence or a heading without disturbing the line. */
.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 8px 1px 6px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-field);
  background: var(--tint-soft);
  vertical-align: baseline;
  line-height: 1.6;
}
.tag-mark {
  flex: 0 0 auto;
  font-size: 12px;
  line-height: 1;
}
.tag-chip-code {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .02em;
  color: var(--ink);
  white-space: nowrap;
}
/* On a row the whole point of which is to be clicked, the chip picks up the
   hover so it reads as part of the live surface, not a sticker on top of it.
   The mark is an emoji now, fixed in colour, so only the chip frame responds. */
tr.clickable:hover .tag-chip { border-color: var(--ink-25); background: var(--tint); }

/* The select column of a table somebody acts on several rows of at once. Kept
   narrow and centred so the ticks read as a column rather than as the first
   piece of data. */
th.tick, td.tick { width: 1%; padding-inline-end: 0; text-align: center; }

.sev-info     { color: var(--ink-45); }
.sev-warning  { color: var(--warn); font-weight: 600; }
.sev-error    { color: var(--err); font-weight: 600; }
.sev-critical { color: var(--err); font-weight: 700; text-transform: uppercase; }

.msg { margin: 0; font-size: 12.5px; color: var(--ink-60); }
.msg.ok, .msg.good { color: var(--ok); }
.msg.warn { color: var(--warn); }
.msg.bad  { color: var(--err); }

.detail { font-size: 11.5px; color: var(--ink-60); }

/* A tag's or an article's lifecycle: one line per event, a badge for the kind
   and the account beside it. */
.timeline { display: flex; flex-direction: column; }
.timeline-row {
  display: flex;
  gap: var(--sp-3);
  align-items: baseline;
  padding: var(--sp-2) 0;
  border-top: 1px solid var(--rule);
}
.timeline-row:first-child { border-top: 0; }
.timeline-row .badge { flex: 0 0 auto; }
.timeline-body { min-width: 0; }

/* How old the numbers are. Turns amber once they are old enough to mislead. */
.freshness {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  margin: 0;
  padding: var(--sp-2) var(--sp-4);
  border-bottom: 1px solid var(--rule-soft);
  font-size: 12px;
  color: var(--ink-60);
}

.freshness .spacer { flex: 1; }
.freshness.stale { background: var(--warn-bg); color: var(--warn); }

/* ── Banner ─────────────────────────────────────────────────────────
 *
 * Something is wrong with the whole system and there is one thing to do about
 * it. Above the content, never inside a card.
 */
.banner {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--warn-line);
  border-inline-start: 4px solid var(--warn);
  border-radius: var(--radius);
  background: var(--warn-bg);
  margin-bottom: var(--sp-4);
}

.banner-body { flex: 1 1 20rem; }
.banner strong { display: block; margin-bottom: 2px; }
.banner .detail { color: var(--ink); }

.banner.setup { border-color: var(--info-line); border-inline-start-color: var(--info); background: var(--info-bg); }
.banner.degraded { border-color: var(--err-line); border-inline-start-color: var(--err); background: var(--err-bg); }

/* ── The record view ────────────────────────────────────────────────
 *
 * One item, on its own page, with the same layout whether it is being read or
 * edited: the fields do not move when Edit is pressed, so nobody has to find
 * them again. The read state is a value, the edit state is an input, and both
 * sit under the same label in the same cell.
 */
.record-head {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-bottom: var(--sp-4);
}

.record-head .titles { flex: 1; min-width: 0; }

.record-head h1 {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  font-size: 20px;
}

.record-head .key {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-60);
}

.record-head .sub { margin: 4px 0 0; font-size: 13px; color: var(--ink-60); }

.back {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-bottom: var(--sp-2);
  font-size: 12.5px;
  color: var(--ink-60);
  text-decoration: none;
}

.back:hover { color: var(--brand); }

/* The bar that appears while a record is being edited. Sticky at the foot of
   the window: a long record must not need scrolling to save. */
.editbar {
  position: sticky;
  /* Above the folded alert dock, which is fixed to the foot of the window. */
  bottom: 38px;
  z-index: 26;
  z-index: 15;
  /* Its in-flow position is right after the last card, so at the very bottom of
     the scroll it would settle flush against it -- this keeps the gap there. */
  margin-top: var(--sp-4);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--accent-dark);
  border-radius: var(--radius);
  background: var(--warn-bg);
  box-shadow: var(--shadow-pop);
}

.editbar .msg { flex: 1; }

/* Facts about the record that are not fields: counts, dates, links. */
.facts { display: flex; gap: var(--sp-5); flex-wrap: wrap; }
.facts .fact { min-width: 7rem; }
.facts .fact .lbl { margin-bottom: 2px; }

/* ── Tabs ───────────────────────────────────────────────────────── */

.tabs {
  display: flex;
  gap: var(--sp-1);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--sp-4);
}

.tab {
  padding: 8px var(--sp-3);
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--ink-60);
  font: 600 13px/1 var(--sans);
  cursor: pointer;
}

.tab:hover { color: var(--ink); }
.tab.on { border-bottom-color: var(--brand); color: var(--ink); }

/* ── Stat tiles ─────────────────────────────────────────────────────
 *
 * The dashboard's numbers. A tile is a cell with room around it; the value is
 * the largest thing on the page, because that is what somebody came to read.
 */
.kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--sp-4);
}

.kpi {
  padding: var(--sp-4);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-card);
}

.kpi .lbl { font-size: 11.5px; letter-spacing: .04em; text-transform: uppercase; }
.kpi .val { margin-top: var(--sp-2); font-size: 26px; font-weight: 700; letter-spacing: -.02em; }
.kpi .lbl-sub { margin: 2px 0 0; }

/* ── Permission list ────────────────────────────────────────────── */

.perm-group { border-top: 1px solid var(--rule-soft); padding: var(--sp-3) var(--sp-4); }
.perm-group:first-child { border-top: 0; }

.perm-group > h3 {
  margin: 0 0 var(--sp-2);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-60);
}

.perm-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--sp-1) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
}

.perm {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  padding: 3px 0;
  font-size: 12.5px;
  cursor: pointer;
}

.perm input { margin: 0; }
.perm code { flex: none; font-size: 11.5px; color: var(--ink); }
.perm .what { color: var(--ink-60); }
.perm .badge { margin-inline-start: 6px; }
.perm.planned code { color: var(--ink-45); }
.perm.dangerous code { color: var(--err); }
.perm input[disabled] { cursor: not-allowed; }
.perm:has(input[disabled]) { cursor: default; }

/* ── Rights (a role's permissions, grouped and collapsible) ─────────
 *
 * The read-only view of what a chosen group allows, on the New User page: a
 * category per row, opened to reveal the actions under it (Manage, Read). A
 * disclosure marker, not a checkbox -- nothing here is toggled. */
.rights { display: flex; flex-direction: column; gap: var(--sp-1); width: 100%; }

.rights-group {
  border: 1px solid var(--rule-soft);
  border-radius: var(--radius-field);
  background: var(--tint-soft);
}

.rights-group > summary {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px var(--sp-3);
  cursor: pointer;
  font-size: 12.5px;
  font-weight: 600;
  list-style: none;
}
.rights-group > summary::-webkit-details-marker { display: none; }
.rights-group > summary::before {
  content: "+";
  width: 1em;
  color: var(--ink-45);
  font-weight: 700;
  text-align: center;
}
.rights-group[open] > summary::before { content: "\2013"; }

.rights-cat { flex: 1 1 auto; }
.rights-count {
  flex: none;
  min-width: 1.4em;
  padding: 0 6px;
  border-radius: 999px;
  background: var(--tint);
  color: var(--ink-60);
  font-size: 11px;
  font-weight: 600;
  text-align: center;
}

.rights-items {
  display: grid;
  gap: 2px;
  padding: var(--sp-1) var(--sp-3) var(--sp-3) calc(var(--sp-3) + 1em);
}
.rights-item {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  font-size: 12px;
}
.rights-action { flex: none; min-width: 96px; font-weight: 600; color: var(--ink); }
.rights-item.planned .rights-action { color: var(--ink-45); }
.rights-what { color: var(--ink-60); }

/* ── The groups a new user starts in (create form) ──────────────────
 *
 * A tag control: the chosen groups as removable chips, a dropdown to add more,
 * then the role chosen from any group. Built to stay usable as the number of
 * groups grows. */
.ug-tags {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  align-items: center;
  min-height: 30px;
}
.ug-empty { color: var(--ink-45); font-size: 12.5px; }

.ug-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px 4px 3px 10px;
  border-radius: 999px;
  background: var(--tint);
  color: var(--ink);
  font-size: 12.5px;
  line-height: 1.5;
}
.ug-chip.is-role {
  background: var(--ok-bg);
  color: var(--ok);
  font-weight: 600;
}
.ug-chip-label { white-space: nowrap; }
.ug-chip-x {
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--ink-45);
  font-size: 15px;
  line-height: 1;
  padding: 0 4px;
  border-radius: 999px;
}
.ug-chip-x:hover { color: var(--err); }
.ug-chip.is-role .ug-chip-x { color: var(--ok); }
.ug-chip.is-role .ug-chip-x:hover { color: var(--err); }

/* The add dropdown sits inline as the last item of the chip row, sized to its
   placeholder so it reads as part of the same tag control. */
.ug-add-select { width: auto; }

.ug-primary {
  margin-top: var(--sp-3);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--rule-soft);
}
.ug-primary select { max-width: 320px; }

/* ── Cards held by a person ─────────────────────────────────────── */

tr.card-dead td { color: var(--ink-45); }
.card-uid { font-weight: 600; }

/* ── Empty state ────────────────────────────────────────────────── */

.empty-state {
  padding: var(--sp-6) var(--sp-4);
  text-align: center;
  color: var(--ink-60);
}

.empty-state h3 { margin-bottom: var(--sp-2); font-size: 15px; color: var(--ink); }
.empty-state p { margin: 0 auto var(--sp-4); max-width: 44ch; font-size: 13px; }

/* ── The module launcher ──────────────────────────────────────────
 *
 * The screen a session starts on (login.js), and the screen the brand mark
 * comes back to. Compact tiles — a mark and a name — because a launcher is
 * read by scanning it, and a paragraph under each of thirteen tiles turns
 * scanning into reading. The sentence is on the tile's `title`, and on the
 * page each one opens.
 */

/* `width: 100%` is not redundant: .page is a flex item in a column flex
   container, where an auto cross-axis margin replaces stretch with
   shrink-to-fit -- which collapsed the grid to two columns. */
.launcher { width: 100%; max-width: 1120px; margin-inline: auto; }

.launcher-head {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-bottom: var(--sp-6);
}

/* Larger here, and in the brand red: on white paper the mark is the one
   piece of identity on the screen, where in the column it sits on the red
   bar and is white against it. `.mark` takes its colour from the element,
   so this is the whole of the difference. */
.launcher-head .mark { width: 34px; height: 34px; color: var(--brand); }
.launcher-head h1 { font-size: 22px; margin: 0; }
.launcher-head .page-sub { margin: 2px 0 0; }

.module-section { margin-bottom: var(--sp-6); }

.module-section > h2 {
  margin: 0 0 var(--sp-3);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-45);
}

.modules {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
  gap: var(--sp-3);
}

.module {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--sp-2);
  padding: var(--sp-4) var(--sp-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-card);
  color: var(--ink);
  text-align: center;
  text-decoration: none;
}

.module:hover { border-color: var(--accent-dark); box-shadow: var(--shadow-pop); color: var(--ink); }

.module .glyph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border-radius: 12px;
  background: var(--tint);
  font-size: 24px;
  line-height: 1;
  color: var(--brand);
}

.module-label { font-size: 13px; font-weight: 600; }
.module-note { font-size: 11px; color: var(--ink-45); }

/* Planned, not built. Dimmed and inert rather than hidden: hiding it is a lie
   by omission to somebody who has read the plan, and letting it be pressed is
   a lie outright. */
.module.soon { opacity: .55; cursor: default; }
.module.soon:hover { border-color: var(--rule); box-shadow: var(--shadow-card); }
.module.soon .glyph { color: var(--ink-45); }

/* An empty container's one-line "nothing here" message, used by empty() in
 * alerts.js for any non-table container. The name is historical: this began on
 * the alert dock, which Task 3 replaced with the Alerts page (alertlist.js). */
.dock-empty { padding: var(--sp-4); margin: 0; color: var(--ink-60); font-size: 12.5px; }

.sev-mark { width: 24px; text-align: center; font-weight: 700; }
tr.seen td { opacity: .72; }

/* ── The gate ───────────────────────────────────────────────────────
 *
 * Sign-in is the one screen that is not the application: a panel floating on
 * sky under a solid red bar, which is Vietjet's own sign-in page. It is the
 * front door -- nothing is filed there and nobody is watching an alert from
 * it -- so it carries the identity the working screens deliberately keep out
 * of the operator's eye.
 */
.gate { min-height: 100vh; display: flex; flex-direction: column; background: var(--sky); }

.gate-bar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  height: 52px;
  flex: none;
  padding: 0 var(--sp-5);
  background: var(--brand-bar);
  color: #fff;
}

.gate-mark { width: 24px; height: auto; display: block; }

.gate-org {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.gate-bar-meta { margin-inline-start: auto; font-size: 12px; opacity: .85; }

.gate-body { flex: 1; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); }

.gate-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  width: min(880px, 100%);
  border-radius: var(--radius);
  background: var(--paper);
  box-shadow: var(--shadow-float);
  overflow: hidden;
}

.gate-form { padding: var(--sp-6); }
.gate-form > * { width: 100%; max-width: 23rem; margin-inline: auto; }

.gate-title { margin-bottom: var(--sp-5); font-size: 19px; text-align: center; }
.gate-field { display: block; margin-bottom: var(--sp-3); cursor: pointer; }
.gate-field .lbl { margin-bottom: 4px; }
.gate-field input { height: 42px; }
.gate-submit { height: 44px; width: 100%; font-size: 14px; }
.gate-form .msg { margin-top: var(--sp-3); text-align: center; }
.gate-help { margin-top: var(--sp-3); font-size: 12px; color: var(--ink-60); text-align: center; }

.gate-remember {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin-bottom: var(--sp-3);
  font-size: 12.5px;
  color: var(--ink-60);
  cursor: pointer;
}

.gate-remember input { width: auto; height: auto; margin: 0; }

.gate-aside {
  position: relative;
  padding: var(--sp-6);
  background: linear-gradient(150deg, var(--brand) 0%, var(--brand-dark) 100%);
  color: #fff;
}

.gate-aside h2 { font-size: 17px; margin-bottom: var(--sp-3); }
.gate-aside p { margin: 0 0 var(--sp-3); font-size: 13px; opacity: .9; }
.gate-aside .gate-aside-foot { margin: var(--sp-5) 0 0; font-size: 11.5px; opacity: .75; }

.gate-provenance { margin-top: var(--sp-4); font-size: 11.5px; color: var(--ink-45); text-align: center; }
.gate-provenance b { color: var(--ink-60); }

@media (max-width: 760px) {
  .gate-card { grid-template-columns: minmax(0, 1fr); }
  .gate-aside { display: none; }
}

/* ── A page that is one short form ──────────────────────────────────
 *
 * Setup and Change password. They are not tables and do not want the width.
 */
.narrow { max-width: 780px; }

/* ── A row of commands ──────────────────────────────────────────────
 *
 * One button per command, wrapping onto as many lines as it needs. It replaced
 * a select and a send button: pressing the button IS the action, so the row
 * has to be readable at a glance rather than compact.
 *
 * `.wrap` and not a grid, because the buttons are different widths — the
 * labels are verbs, and "Reboot" is not "Resync the catalogue". A grid would
 * pad them all to the widest and leave a field of mostly-empty boxes.
 */

.actions.wrap {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-top: var(--sp-3);
}

/* The dangerous ones sit at the end of the row and are already tinted; this
   keeps a gap in front of the first of them, so the hand does not arrive at
   one while aiming at the last everyday command. */
.actions.wrap .btn.danger:first-of-type { margin-inline-start: var(--sp-3); }

/* Disabled means "not yet", most of the time on the Control screen: nothing
   is ticked. Dimmed rather than hidden, because a row that changed length
   when a checkbox moved would be a row nobody could learn. */
.actions.wrap .btn:disabled { opacity: .45; cursor: default; }

/* The language switcher on the sign-in page. Plain text links: the account
   menu's <select> hangs off a menu, and this page has no chrome to hang one
   from. Each language is written in itself -- see nav.js. */
.gate-languages {
  margin-top: var(--sp-3);
  font-size: 12px;
  color: var(--ink-60);
  text-align: center;
}
.gate-languages a { color: var(--ink-60); }
.gate-languages b { color: var(--ink); }

/* ── The photographs of an article ──────────────────────────────────
 *
 * A strip somebody LOOKS at, which is the whole reason the card exists — a
 * table of dates with a download link beside each would be a table nobody
 * opens. So the picture is the element and the words are its caption.
 *
 * Wraps rather than scrolls sideways: a horizontal scroller hides whatever is
 * past the edge, and the second photograph of a tool is exactly the one worth
 * noticing. Six of them on one row on a desk, two on a tablet, and none of
 * them off-screen.
 */
.photo-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  padding: 14px 16px 16px;
}

.photo {
  margin: 0;
  inline-size: 160px;
}

/* A fixed box with the image contained in it, so a strip of photographs taken
 * portrait and landscape lines up instead of stepping up and down. `contain`
 * and not `cover`: this is evidence, and cropping a serial plate out of the
 * edge of a thumbnail is how somebody concludes the picture does not show it. */
.photo a {
  display: block;
  block-size: 160px;
  border: 1px solid var(--rule);
  border-radius: var(--radius-field);
  background: var(--tint);
  overflow: hidden;
}

.photo img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: contain;
  display: block;
}

.photo figcaption {
  margin-block-start: 6px;
  font-size: .78rem;
  line-height: 1.35;
}

.photo figcaption .muted { color: var(--ink-60); }

/* ── A code to sign in with on a device ─────────────────────────────
 *
 * The QR beside the characters and not instead of them. A scanner is faster
 * when it works, and when it does not -- a cracked lens, a camera the app has
 * not been given, a device with no camera at all -- somebody types eight
 * characters. Showing only one of the two makes the other route look absent.
 */
.devicecode-body {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  align-items: flex-start;
  padding: 16px;
}

.devicecode-qr {
  flex: 0 0 auto;
  line-height: 0;
}

/* The white is painted by the SVG itself, so the border is the only thing
 * separating it from a card that may be any colour a theme chooses. */
.devicecode-qr svg {
  border: 1px solid var(--rule);
  border-radius: var(--radius-field);
}

.devicecode-text {
  flex: 1 1 260px;
  min-inline-size: 0;
}

/* Big, monospaced and spaced in fours, because the failure this is designed
 * against is somebody losing their place half way through typing it into a
 * keypad they are holding at arm's length. */
.devicecode-value {
  margin: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: .12em;
  color: var(--ink);
}

/* Spent, not gone. Somebody half way through typing needs to see WHY it
 * stopped working, and a panel that emptied itself does not say. */
.devicecode-value.spent {
  color: var(--ink-45);
  text-decoration: line-through;
}

.devicecode-life {
  margin: 4px 0 0;
  font-size: .85rem;
  color: var(--ink-60);
}

.devicecode-how {
  margin: 14px 0 0;
  padding-inline-start: 20px;
  font-size: .85rem;
  line-height: 1.6;
  color: var(--ink-60);
}

/* The two values a device has to be given, beside the QR that carries them.
 *
 * Monospaced, because the whole reason they are printed here is that somebody
 * may have to type them — and a hostname in a proportional face is where an
 * l becomes a 1 and an O becomes a 0. */
.provision-pair { margin-block-end: 10px; }

.provision-label {
  font-size: .72rem;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--ink-60);
}

.provision-value {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .95rem;
  color: var(--ink);
  word-break: break-all;
}


/* ── Asking somebody to pick one ────────────────────────────────────
 *
 * `window.prompt` is what this console asks a reason with, and it cannot do
 * this: the answer is one of a set the server will accept, and a free-text
 * box asking for a cabinet's name is a box somebody mistypes. So an action
 * with `ask: 'choice'` opens the element the platform already has — there is
 * no dialog component behind this, and the few rules below are what it takes
 * to make the default one look like a card.
 *
 * The backdrop is the same scrim the alert dock uses, so a page with one
 * open does not have two ideas about how far it is taken down. */
dialog.ask {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow-float);
  background: var(--paper);
  color: var(--ink);
  padding: var(--sp-5);
  max-width: min(30rem, calc(100vw - var(--sp-5)));
}

dialog.ask::backdrop { background: rgba(40, 40, 40, .38); }

dialog.ask form { display: flex; flex-direction: column; gap: var(--sp-3); }

/* Both full width, because they are the question. A select sized to its
   longest option makes the dialog jump about as the list changes. */
dialog.ask select,
dialog.ask input { width: 100%; }

dialog.ask .sub { margin: 0; }

dialog.ask .page-actions { justify-content: flex-end; }

/* Small text utilities the live feed and the tag history table lean on.
   Colours and measures come from the tokens in theme.css. */
.mono { font-family: var(--mono); }
.nowrap { white-space: nowrap; }

/* Tag history (tag-history.html): the short list of tags a search matched, each
   a button opening that tag's timeline. */
.match-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: 0;
  border-radius: var(--radius-field);
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.match-row:hover { background: var(--tint); }
.match-row + .match-row { border-top: 1px solid var(--rule-soft); }
.match-what { color: var(--ink-60); }
