/* admin.css — the staff console's own surface.
 *
 * Loaded ONLY by the admin page. Nothing here dresses a player-facing surface,
 * so a console that grows a control does not widen the sheet every other page
 * downloads.
 *
 * IT DRESSES FROM THE SHARED SHEET WHEREVER ONE EXISTS. Buttons are .btn /
 * .btn-primary / .btn-ghost, inputs are bare elements (site.css styles them by
 * tag), and the page frame is .wrap / .section / .sec-head / .eyebrow /
 * .page-h1 / .dim. What is HERE is what site.css genuinely does not have: a
 * panel, a data table, a field grid, and an odds readout.
 *
 * ZERO COLOUR LITERALS. Role tokens only, per webcore's stylesheet law — the
 * frontend suite scans this file for a hex or an rgb() and fails on either.
 *
 * =========================================================================
 * THE [hidden] CANCELS ARE LOAD-BEARING, NOT TIDINESS
 * =========================================================================
 * The browser's own `[hidden] { display: none }` is a UA rule, and ANY author
 * `display` outranks it. adminrail.css gives `#admin-root` a `display: grid`
 * and this sheet gives the panels and rows a `display` of their own, so every
 * one of them needs its own cancel or it ships permanently visible. That is
 * not hypothetical: a console in this fleet leaked an admin tab to every
 * visitor because a `display` rule beat the `hidden` attribute, and the
 * attribute looked correct in the markup the whole time.
 *
 * `!important` here beats even an inline style, which is deliberate: the
 * fragment ALSO ships `style="display:none"` so the console stays dark on a
 * page whose stylesheet has not arrived, and the two must agree in both
 * directions rather than fight.
 */

/* ---------- the shell ---------- */

#admin-root { margin-top: 30px; }
#admin-root[hidden] { display: none !important; }

.admin-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px 18px;
  margin-bottom: 18px;
}
.admin-head[hidden] { display: none !important; }

#admin-who {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.04em;
}

/* ---------- one panel ---------- */

#admin-root > .panel {
  padding: 22px 22px 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r);
  box-shadow: none;
}
#admin-root > .panel[hidden] { display: none !important; }

.admin-panel-head { margin-bottom: 18px; }
.admin-panel-head p { margin-top: 6px; }

/* ---------- the action bar ---------- */

.admin-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  padding-bottom: 16px;
  margin-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.admin-bar[hidden] { display: none !important; }
.admin-spacer { flex: 1 1 auto; }

.admin-filter { min-width: 0; flex: 1 1 190px; max-width: 320px; }

.admin-switch {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--fg-dim);
}
.admin-switch input { margin: 0; }
.admin-switch:hover { color: var(--fg); }

/* ---------- status, refusal, hint ---------- */

.admin-note {
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--fg-mute);
}
.admin-note[hidden] { display: none !important; }
.admin-note.ok { color: var(--ok); }
.admin-note.warn { color: var(--warn); }

/* A refusal is the SERVER'S sentence, rendered verbatim. It never replaces the
   status line — an owner has to be able to see both what was refused and what
   the panel currently holds. */
.admin-error {
  margin-top: 10px;
  padding: 10px 13px;
  border: 1px solid var(--bad-dim);
  border-radius: var(--r-s);
  background: var(--surface-sink);
  color: var(--bad);
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.45;
}
.admin-error[hidden] { display: none !important; }

.admin-hint {
  margin-top: 6px;
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--fg-mute);
  line-height: 1.45;
}

/* ---------- the data table ---------- */
/* site.css defines no table rules at all, so every one of these is this
   sheet's own. The wrapper scrolls rather than the page: a console table is
   wide by nature and a body that scrolls sideways is the defect that hides. */

.admin-table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.admin-table {
  width: 100%;
  border-collapse: collapse;
  font-family: var(--font-ui);
  font-size: 13px;
}
.admin-table[hidden] { display: none !important; }

.admin-table th,
.admin-table td {
  padding: 9px 12px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.admin-table th {
  font-family: var(--font-display);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-mute);
  border-bottom-color: var(--border-2);
}
.admin-table th.num,
.admin-table td.num { text-align: right; }
.admin-table th.mid,
.admin-table td.mid { text-align: center; }
.admin-table tbody tr:hover { background: var(--surface-hover); }
.admin-table tr[hidden] { display: none !important; }

.admin-table td.name {
  white-space: normal;
  color: var(--fg);
  font-weight: 500;
}
.admin-table .diet {
  display: block;
  font-size: 11px;
  color: var(--fg-mute);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* A row's own id under its label. Every refusal the quest lane returns names
   the row BY ID, so the id has to be on screen exactly as it is written -
   mono, and never folded to the label's case. */
.admin-table .ident {
  display: block;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}

/* A cell an owner has edited but not saved. The mark is a left edge rather
   than a fill: a fill on a row that is also hovered reads as selection. */
.admin-table tr.dirty td:first-child { box-shadow: inset 2px 0 0 var(--accent); }

.admin-table input[type="number"] { width: 92px; text-align: right; }

/* NO CAP is the honest word for a cap of zero or less, and it is written where
   the number would be so nobody has to remember which way round it reads. */
.admin-nocap {
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-mute);
}
/* It is toggled by the controller, and it is the label that carries the whole
   meaning of a zero. Nothing may leave it standing beside a real cap. */
.admin-nocap[hidden] { display: none !important; }

.admin-live {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--fg-dim);
}
.admin-live.hot { color: var(--warn); }
.admin-live.unknown { color: var(--fg-mute); }

.btn-small {
  padding: 4px 10px;
  font-size: 11px;
}
.admin-table td .btn-small { margin-left: 6px; }

/* A switched-off quest names its state where staff read the name. */
.admin-table .diet.off { color: var(--warn); }

/* ================= THE QUEST LEDGER (2026-08-17) =========================
   Ishaq: *"there should be no d_full_belly ... the names already show. instead
   it should be for example first steps then under it it explains what the
   quest it. I want it advanced."*

   A quest is not a table row. It is a NAMED THING with a sentence, a price and
   a switch, and a table forced every one of those into a column of its own -
   which is how the id ended up printed under the name and the live reward
   ended up printed twice. This is a LIST OF OBJECTS: identity on the left
   where reading starts, the price in the middle where it is compared down the
   column, the controls on the right where the hand goes. Deliberately NOT a
   card grid - the rows share a hairline and sit flush, so twenty quests still
   read as one catalogue rather than twenty boxes.

   ★ IT TAKES THE HOUSE GRAMMAR, NOT ITS OWN. The first draft of this panel
   drew stadium chips and a rounded iOS-style switch, and `test_house_look.py`
   refused all six of them by name: this site's radius is 2px, its labels are
   tracked mono micro-caps, its controls are a hairline that goes accent when
   active. Every plate below is registered in terminal.css section 6b and every
   size is one of the four interior heading steps - a console that invented its
   own switch would be the eleventh surface that stopped matching.
   ========================================================================= */

.quest-group { margin-bottom: 26px; }
.quest-group[hidden] { display: none !important; }

/* The heading and its count sit on ONE baseline with a rule between them and
   the rows, so a period reads as a section rather than a floating label. */
.quest-group-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  padding-bottom: 9px;
  border-bottom: 1px solid var(--border-2);
}
.quest-group-count {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--fg-mute);
  font-variant-numeric: tabular-nums;
}

.quest-list { margin: 0; padding: 0; list-style: none; }

.quest {
  position: relative;
  border-bottom: 1px solid var(--border);
}
.quest[hidden] { display: none !important; }

/* THE THREE ZONES. `auto` on the price and the controls rather than a fraction:
   a reward box is a fixed thing, and giving it a share of the row makes it
   drift wider on a catalogue with long names. The name column is the only one
   that flexes. */
.quest-face {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 12px 22px;
  padding: 13px 12px 13px 14px;
  transition: background 140ms ease-out;
}
.quest:hover > .quest-face { background: var(--surface-hover); }

.quest-id { min-width: 0; }

/* The name carries the row, at the house's row-head step. */
.quest-name {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--tm-h-item);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.005em;
  color: var(--fg);
}

/* ★ THE LINE THIS REDESIGN EXISTS FOR. Where the id used to be printed under
   the name there is now the sentence the quest actually asks. It WRAPS - a
   quest whose objective needs two lines gets two lines, because a truncated
   objective is the same defect as no objective at all. */
.quest-what {
  margin: 3px 0 0;
  max-width: 62ch;
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.5;
  color: var(--fg-dim);
}
/* ★ THE DERIVED SENTENCE IS NOT DIMMED, and the first draft's `.derived {
   color: var(--fg-mute) }` was a real defect: MOST rows have no owner
   description, so dimming the derived ones put this panel's PRIMARY CONTENT on
   the faintest ink in the palette. The class still rides the element - it is a
   true fact about the row and the rig reads it - and the place that distinction
   actually helps is the EDITOR, where it tells an admin what will be shown if
   they leave the box empty (`.quest-preview.derived`, below). */

.quest-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

/* The chips are STATES, not decoration, so each is drawn from the meaning it
   carries: a diet is neutral, a hidden quest is accented, a quest waiting on a
   world switch and a quest switched off are both warnings. The pill grammar
   (radius, face, tracking) comes from terminal.css section 6b. */
/* Radius, face, size and tracking all come from the register's pill group -
   what is left here is the box it sits in and what each tone MEANS. */
.quest-tag {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  border: 1px solid var(--tm-hair);
  color: var(--fg-dim);
  white-space: nowrap;
}
.quest-tag[data-tone="secret"] {
  color: var(--accent-hi);
  border-color: var(--tm-warm);
}
.quest-tag[data-tone="wait"],
.quest-tag[data-tone="off"] {
  color: var(--warn);
  border-color: var(--warn-dim);
}

/* ---------- the price ---------- */

.quest-pay {
  display: grid;
  gap: 3px;
  justify-items: end;
}
.quest-pay-label {
  font-family: var(--font-display);
  font-size: var(--tm-h-micro);
  font-weight: 600;
  letter-spacing: var(--tm-track);
  text-transform: uppercase;
  color: var(--fg-mute);
}
.quest-pay-lane { display: flex; align-items: center; gap: 8px; }
.quest-reward {
  width: 104px;
  text-align: right;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.quest-reward[aria-invalid="true"] { border-color: var(--bad); }

/* WHAT MOVED, and in which direction. Hidden while the box still holds the live
   value - a delta of zero beside every unedited row is twenty numbers saying
   nothing. */
.quest-delta {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  font-weight: 600;
  color: var(--ok);
}
.quest-delta[data-way="down"] { color: var(--warn); }
.quest-delta[hidden] { display: none !important; }

/* The value the bot is STILL paying, shown only once the box stops holding it.
   This is the old "Now" column, and it appears exactly when it stops being a
   second copy of the number beside it. */
.quest-was {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--fg-mute);
}
.quest-was[hidden] { display: none !important; }

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

.quest-controls {
  display: flex;
  align-items: center;
  gap: 8px;
}
.quest-controls .btn-small { margin: 0; }
.btn-small.armed { border-color: var(--bad); color: var(--bad); }

/* THE SWITCH. A button carrying role=switch, so it says what it IS rather than
   what pressing it would do - the control this replaced was labelled "Turn off"
   while the quest was ON, which is the state read backwards. Square, hairline,
   accent when live: the house's control grammar, not a borrowed phone toggle. */
.qswitch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 3px 6px 3px 3px;
  border: 1px solid transparent;
  background: none;
  cursor: pointer;
  /* Face and tracking are the register's (controls group). */
  font-size: var(--tm-h-micro);
  font-weight: 600;
  text-transform: uppercase;
  color: var(--fg-mute);
}
.qswitch:hover { color: var(--fg-dim); }
.qswitch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.qswitch[disabled] { cursor: progress; opacity: .6; }

.qswitch-track {
  position: relative;
  display: block;
  width: 32px;
  height: 18px;
  background: var(--surface-sink);
  border: 1px solid var(--tm-hair);
  transition: background 160ms ease-out, border-color 160ms ease-out;
}
.qswitch-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 12px;
  height: 12px;
  background: var(--fg-mute);
  transition: transform 160ms cubic-bezier(.2, .8, .3, 1),
              background 160ms ease-out;
}
.qswitch[aria-checked="true"] { color: var(--accent-hi); }
.qswitch[aria-checked="true"] .qswitch-track {
  background: var(--tm-faint);
  border-color: var(--tm-hot);
}
.qswitch[aria-checked="true"] .qswitch-thumb {
  transform: translateX(14px);
  background: var(--accent-hi);
}
.qswitch-word { min-width: 2.2em; text-align: left; }

/* A retired quest reads as retired at a glance - the identity dims, the price
   and the switch stay at full contrast because they are what brings it back. */
.quest.is-off .quest-name,
.quest.is-off .quest-what { color: var(--fg-mute); }

/* An edited row, marked on the edge rather than by a fill: a fill on a row that
   is also hovered reads as selection. */
.quest.dirty::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 2px;
  background: var(--accent);
}

/* ---------- the editor, opened where the quest is ---------- */

.quest-editor {
  padding: 4px 14px 20px;
  border: 1px solid var(--tm-hair);
  border-top-style: dashed;
  background: var(--surface-sink);
}
.quest-editor[hidden] { display: none !important; }
.quest-editor .admin-fields { margin-top: 14px; }
.quest-editor textarea,
#questadmin-author textarea { width: 100%; resize: vertical; }
#questadmin-author { margin-top: 16px; }
.quest-blurb-field { margin-top: 16px; }
.quest-editor-bar { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }

/* WHAT THE CARD WILL SAY once this is saved, while it is being typed. */
.quest-preview {
  margin: 8px 0 0;
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.5;
  color: var(--fg-dim);
}
.quest-preview.derived { color: var(--fg-mute); }
.quest-preview:empty { display: none; }

/* THE ONE AUTHORED MOMENT on this panel: the drawer unrolls from the row it
   belongs to, so the eye keeps the row it pressed. Nothing else here animates. */
@media (prefers-reduced-motion: no-preference) {
  .quest-editor { animation: quest-unroll 190ms cubic-bezier(.2, .8, .3, 1); }
  @keyframes quest-unroll {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
  }
}

/* ---------- nothing to show ---------- */

.quest-empty {
  padding: 26px 16px;
  border: 1px dashed var(--tm-hair);
  background: var(--surface-sink);
  text-align: center;
}
.quest-empty[hidden] { display: none !important; }
.quest-empty-title {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--tm-h-panel);
  font-weight: 600;
  color: var(--fg-dim);
}
.quest-empty-body {
  margin: 6px auto 0;
  max-width: 56ch;
  font-family: var(--font-ui);
  font-size: 13px;
  line-height: 1.55;
  color: var(--fg-mute);
}

/* ---------- narrow ---------- */
/* The three zones stack rather than shrink. A reward box squeezed to fit beside
   a name on a phone is a box nobody can read the number in, and this console is
   opened on a phone the moment an owner is away from their desk. */
@media (max-width: 720px) {
  .quest-face {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
    padding: 14px 12px;
  }
  .quest-pay { justify-items: start; }
  .quest-controls { flex-wrap: wrap; }
}

/* ---------- the field grid ---------- */

.admin-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 16px 20px;
}
.admin-fields[hidden] { display: none !important; }

.admin-field { min-width: 0; }
.admin-field label,
.admin-field-label {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-ui);
  font-size: var(--tm-h-panel);
  font-weight: 500;
  color: var(--fg-dim);
}
.admin-field input,
.admin-field select { width: 100%; }
.admin-field.dirty label { color: var(--accent); }
.admin-field[hidden] { display: none !important; }

.admin-inline {
  display: flex;
  gap: 10px;
  align-items: center;
}
/* The kill-reward species ROW TEMPLATE ships as a hidden .admin-inline and is
   cloned per row, so this class joined the hideable set on 2026-08-14 - the
   cancel below is what keeps the template from rendering as an empty row. */
.admin-inline[hidden] { display: none !important; }
.admin-inline input { flex: 1 1 auto; min-width: 0; }
/* The prefilled kill-reward roster row: the species name is a fixed left
   column so 20+ rows scan as a table, and the box takes the rest. */
.kp-roster-name {
  flex: 0 0 46%;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- the knob anatomy ----------
   One settings row now answers, in reading order: what is this called, what
   does it do, what is the literal key, what is typed here, what is IN FORCE
   versus the SHIPPED DEFAULT, and where the number came from. Every line is
   built by admin.mjs from the payload; this sheet only paces them. */

/* The one sentence of meaning, between the name and the key. */
.admin-knob-what {
  margin: -2px 0 6px;
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--fg-mute);
  line-height: 1.45;
}
.admin-knob-what[hidden] { display: none !important; }

/* The literal key. Mono because it is a string an owner GREPS, block so it
   never shoulders into a sentence, and wrap-anywhere because a dotted key on
   a phone must fold rather than force the whole grid wide. The translation
   engine skips <code> on purpose - a translated key greps nothing. */
.admin-knob-key {
  display: block;
  margin: 0 0 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--fg-mute);
  overflow-wrap: anywhere;
}
.admin-knob-key[hidden] { display: none !important; }

/* What is IN FORCE, and the shipped default beside it. When the two differ
   the row carries the same accent edge a dirty table row wears - the WORDS
   already say "the shipped default is ...", so colour is the second signal,
   never the only one. */
.admin-knob-now {
  margin-top: 6px;
  font-family: var(--font-ui);
  font-size: 12px;
  line-height: 1.45;
  color: var(--fg-dim);
  font-variant-numeric: tabular-nums;
}
.admin-knob-now[hidden] { display: none !important; }
.admin-knob-now.diff {
  color: var(--fg);
  padding-left: 8px;
  box-shadow: inset 2px 0 0 var(--accent);
}

/* The honesty line: a number whose game is switched off is idle, and the row
   says so instead of letting an owner tune a dead dial. */
.admin-knob-idle {
  margin-top: 6px;
  font-family: var(--font-ui);
  font-size: 12px;
  line-height: 1.45;
  color: var(--warn);
}
.admin-knob-idle[hidden] { display: none !important; }

.admin-figure {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 19px;
  color: var(--fg);
}

/* ---------- the odds readout ---------- */
/* Every number in here is computed by the SERVER from the same values the
   tables deal on. This sheet only lays them out. */

.admin-odds {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  margin-bottom: 18px;
}
.admin-odds[hidden] { display: none !important; }

.admin-odd {
  padding: 12px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-s);
}
/* ★ ONE TILE can now be hidden on its own - the points console drops the SPIN
   tile for a box whose wheel has never run - and the grid's own cancel does not
   reach a child. `.admin-odd b` carries a `display`, and an author `display`
   outranks the browser's `[hidden]` rule; this is the same protection
   `.admin-odds` has always had, one level down. */
.admin-odd[hidden] { display: none !important; }
.admin-odd b {
  display: block;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 18px;
  color: var(--fg);
}
.admin-odd span {
  display: block;
  margin-top: 3px;
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fg-mute);
}
/* An edge at or below zero is the house losing money on every round. It is
   never merely a number on this page. */
.admin-odd.bad { border-color: var(--bad-dim); }
.admin-odd.bad b { color: var(--bad); }

.admin-games {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin-bottom: 18px;
}
.admin-games[hidden] { display: none !important; }

/* ---------- the shop price list ---------- */

.admin-shop-section { margin-bottom: 26px; }
.admin-shop-section h3 {
  font-family: var(--font-display);
  font-size: var(--tm-h-panel);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-mute);
  margin-bottom: 10px;
}
.admin-price-lane {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.admin-price-lane em {
  font-style: normal;
  font-family: var(--font-ui);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-mute);
}
.admin-price-lane input[type="number"] { width: 104px; text-align: right; }

/* ---------- sub-blocks ----------
   A `.admin-sub` is a second section INSIDE one tab: the tick interval and the
   tier ladder under Earnings, the spin bank and the history under Points. The
   class has been in the markup since the Earnings tab grew its three sections
   and NEVER HAD A RULE — every one of those blocks ran straight into the one
   above it, so a panel with three headings read as one wall of controls. The
   rule is separation only (space and a hairline); it invents no colour and no
   type, so the blocks that already shipped keep their exact appearance apart
   from finally being told apart. */

.admin-sub {
  margin-top: 28px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
}
.admin-sub[hidden] { display: none !important; }
/* The first block under a panel head does not need a line above it: the head's
   own lede already separates it, and a rule there reads as a stray divider. */
.admin-panel-head + .admin-sub { border-top: 0; padding-top: 0; }

/* ---------- points console ----------
   A MOVEMENT IS READ BY ITS SIGN BEFORE IT IS READ AS A NUMBER. The site has no
   "good" token, so a credit takes the accent (its one positive colour) and a
   debit takes `--bad`; the SIGN is always printed too, because colour alone is
   not a readable difference for everybody and a red 500 next to a green 500 is
   the same glyph. */

.admin-move { font-variant-numeric: tabular-nums; }
.admin-move.up { color: var(--accent); }
.admin-move.down { color: var(--bad); }

/* The name-search results. Each row is a BUTTON, not a link: picking one runs a
   second lookup in place, and a link would be a navigation that loses the
   panel. */
.admin-pick {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--fg);
  cursor: pointer;
}
.admin-pick:hover,
.admin-pick:focus-visible { color: var(--accent-hi); }
.admin-pick .tail {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
  margin-left: 8px;
}

/* ---------- lists (recent changes, hidden-colour admins) ---------- */

.admin-recent { margin-top: 24px; }
.admin-recent[hidden] { display: none !important; }
.admin-recent h3 {
  font-family: var(--font-display);
  font-size: var(--tm-h-micro);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-mute);
  margin-bottom: 8px;
}
.admin-recent ul,
.admin-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.admin-list[hidden] { display: none !important; }
.admin-recent li,
.admin-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--fg-dim);
}
.admin-recent li time,
.admin-list li .who {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}

/* ---------- temp skins ----------
   The panel's markup is built by js/rentals.js, which shipped with NO
   stylesheet at all: on the shop page it rendered as a stack of bare labels
   and a naked select, which is most of why it read as something that had been
   left lying there. It lives in the Staff console now (his ruling 2026-08-09)
   and it is dressed like the rest of it - the same field grid, the same list
   rules, the same note colours - so it reads as one console rather than as a
   widget somebody parked. Nothing here is a new visual idea: every value below
   is one already used above in this file. */
.rentals-head { display: none; }          /* the panel's own head says it */

.rentals-form {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px 18px;
  align-items: end;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}
.rentals-field { min-width: 0; }
.rentals-field span {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 500;
  color: var(--fg-dim);
}
.rentals-field input,
.rentals-field select { width: 100%; }
/* The button is a field of its own in the grid, so it lines up with the
   inputs' baseline instead of hanging under them. */
.rentals-form .btn { align-self: end; }

.rentals-status {
  margin: 12px 0 0;
  font-family: var(--font-ui);
  font-size: 12px;
  color: var(--fg-dim);
}
.rentals-status:empty { display: none; }
.rentals-status.is-bad { color: var(--bad); }

.rentals-list { margin-top: 6px; }
.rentals-list .dim { font-family: var(--font-ui); font-size: 12px; }

.rentals-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.rentals-row-main { flex: 1 1 220px; min-width: 0; }
.rentals-row-main h3 {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--tm-h-panel);
  font-weight: 600;
  color: var(--fg);
}
.rentals-row-facts {
  margin: 3px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}
/* A row that is no longer running is stated in ink, never by fill alone. */
.rentals-row.is-expired .rentals-row-main h3,
.rentals-row.is-revoked .rentals-row-main h3 { color: var(--fg-dim); }

/* ---------- zombie console ---------- */
/* The one panel on this page whose rows are PEOPLE rather than settings, so a
   row has to read as a person: the name in ink, the Steam ID under it in the
   mono face an admin copies from. */

.zom-who { display: block; min-width: 0; }
.zom-who b {
  display: block;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
  overflow-wrap: anywhere;
}
.zom-sid {
  display: block;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}

/* THE TIER IS STATED IN A WORD, NEVER IN COLOUR ALONE. The badge carries its
   own text, so it survives a monochrome display, a colour-blind reader and a
   printed screenshot; the tint is the second signal, not the only one. */
.zom-badge {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  font-family: var(--font-ui);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--fg-dim);
}
.zom-badge[data-tier="zombie"] {
  color: var(--accent-hi);
  border-color: var(--accent);
}
.zom-badge[data-tier="infected"] {
  color: var(--fg-dim);
  border-style: dashed;
}

/* The paste box is a Steam ID field: mono, and wide enough for all 17 digits
   plus a little, so a pasted value is readable end to end before it is sent. */
#zom-sid {
  font-family: var(--font-mono);
  letter-spacing: .04em;
  max-width: 26ch;
}

/* ---------- supporter grants (owner only) ----------
   The pick list and the identity card. Same bones as every other panel: the
   tokens are the site's, nothing here names a colour of its own, and every
   block hides honestly with [hidden]. */

.supgrants-matches { margin-top: 8px; display: grid; gap: 6px; }
.supgrants-match {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  color: var(--fg);
  cursor: pointer;
  font: inherit;
}
.supgrants-match:hover:not(:disabled) { border-color: var(--accent); }
.supgrants-match:disabled { cursor: default; opacity: .7; }
.supgrants-match .dim { display: block; margin-top: 4px; font-size: 12px; }
.supgrants-match .dim[hidden] { display: none !important; }

.supgrants-id { display: flex; align-items: center; gap: 10px; min-width: 0; }
.supgrants-face {
  width: 34px; height: 34px;
  border-radius: var(--r-round);
  border: 1px solid var(--border);
  flex: 0 0 auto;
}
.supgrants-id b { display: block; }
.supgrants-ids,
.supgrants-state {
  display: block;
  font-size: 12px;
  overflow-wrap: anywhere;
}

/* The three expiry shapes. One radio group; the input beside each shape is
   editable only while that shape is the picked one (the script disables the
   others), so what is typed and what is sent cannot disagree. */
.supgrants-expiry { display: grid; gap: 6px; }
/* ★ WINS THE FIELD GRID'S OWN LABEL RULE. `.admin-field label` is (0,1,1)
   and sets display:block + a bottom margin; a bare `.supgrants-choice`
   (0,1,0) loses to it and the radio drifts off its words. The compound
   selector is (0,2,1), so the choice row is a flex row wherever it lives. */
.admin-field label.supgrants-choice,
label.supgrants-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  margin-bottom: 0;
}
/* site.css widths bare inputs to 100%; a radio with a full-width box shoves
   its own words to the far edge of the row. Pin all three input kinds. */
.supgrants-choice input[type="radio"] { width: auto; flex: 0 0 auto; }
.supgrants-choice input[type="number"] { width: 84px; flex: 0 0 auto; }
.supgrants-choice input[type="datetime-local"] { width: auto; max-width: 24ch; flex: 0 0 auto; }
.supgrants-choice input:disabled { opacity: .5; }

#supgrants-form { display: grid; gap: 12px; }
#supgrants-form[hidden] { display: none !important; }
#supgrants-find { max-width: 34ch; }

/* ---------- bans (owner only) ----------
   The drop-down, the person card, the confirm card and the ban rows. Every
   value below already exists in this file or in the site sheets: the match
   rows are the supgrants pick pattern, the rows are the admin-list pattern,
   the faces are the supgrants face. Nothing here names a colour of its own. */

.bans-find-field { position: relative; min-width: 0; }
#bans-find { max-width: 42ch; }

/* The drop-down floats UNDER the box it belongs to, over the page - a list
   that shoves the whole panel down on every keystroke reads as flicker. */
.bans-matches {
  position: absolute;
  z-index: 30;
  top: 100%;
  left: 0;
  right: 0;
  max-width: 42ch;
  margin-top: 4px;
  display: grid;
  gap: 4px;
  padding: 6px;
  /* It floats OVER page content, so it needs an opaque ground; the lift and
     the hairline are the HOUSE's to speak - terminal.css 6b names this class
     and dresses both, exactly so this sheet cannot drift them. */
  background: var(--surface-2, var(--bg));
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  max-height: 320px;
  overflow-y: auto;
}
.bans-matches[hidden] { display: none !important; }

.bans-match {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  color: var(--fg);
  cursor: pointer;
  font: inherit;
}
.bans-match:hover,
.bans-match.hot { border-color: var(--accent); }
.bans-match-main { min-width: 0; flex: 1 1 auto; }
.bans-match-main b { display: block; overflow-wrap: anywhere; }
.bans-match-main .dim { display: block; margin-top: 2px; font-size: 12px; }

/* The "banned already" flag: a word in a hairline, never colour alone. */
.bans-chip {
  flex: 0 0 auto;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--r-xs);
  font-family: var(--font-ui);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--fg-dim);
}

.bans-face {
  width: 32px;
  height: 32px;
  flex: 0 0 auto;
  border-radius: var(--r-round);
  border: 1px solid var(--border);
  object-fit: cover;
}
.bans-face-letter {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-ui);
  font-weight: 600;
  color: var(--fg-dim);
}

.bans-form { display: grid; gap: 12px; }
.bans-form[hidden] { display: none !important; }
.bans-person { display: flex; align-items: center; gap: 10px; min-width: 0; }
.bans-person .bans-face { width: 40px; height: 40px; }
.bans-person-who b { display: block; overflow-wrap: anywhere; }
.bans-person-who .dim { display: block; font-size: 12px; }

.bans-length { display: flex; flex-wrap: wrap; gap: 8px 18px; }
.bans-length label,
.bans-extras label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-ui);
  font-size: 13px;
  margin-bottom: 0;
}
.bans-length input[type="radio"],
.bans-extras input[type="checkbox"] { width: auto; flex: 0 0 auto; }
.bans-length input[type="number"] { width: 74px; flex: 0 0 auto; }
.bans-length input:disabled { opacity: .5; }
.bans-extras { display: grid; gap: 6px; }
#bans-reason { max-width: 42ch; }

.bans-act { display: flex; flex-wrap: wrap; gap: 10px; }

.bans-confirm {
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-s);
}
.bans-confirm[hidden] { display: none !important; }
.bans-confirm h3 { margin: 0 0 6px; }
.bans-confirm p { margin: 0 0 10px; }

.bans-rows { list-style: none; margin: 0; padding: 0; }
.bans-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.bans-row-main { flex: 1 1 220px; min-width: 0; }
.bans-row-main b { display: block; overflow-wrap: anywhere; }
.bans-row-main .dim { display: block; margin-top: 2px; font-size: 12px; }

#bans-history-body { margin-top: 10px; }

/* ---------- phone ---------- */

@media (max-width: 720px) {
  #admin-root > .panel { padding: 16px 12px 18px; }
  .admin-bar { gap: 8px 10px; }
  .admin-filter { flex: 1 1 100%; max-width: none; }
  .admin-spacer { display: none; }
  .admin-bar .btn { flex: 1 1 auto; min-width: 0; }
  .admin-table th,
  .admin-table td { padding: 8px 6px; }
  /* THE WHOLE ROW HAS TO FIT A PHONE. The wrapper scrolls, but a control an
     owner has to discover by swiping sideways is a control they do not find,
     and the cap and the lock are the two this page exists for. So NO CAP
     stacks ABOVE its input here instead of sitting beside it, which buys back
     the ~54px that was pushing the lock column off the edge. */
  .admin-nocap { display: block; margin-bottom: 3px; font-size: 10px; }
  .admin-table input[type="number"] { width: 62px; }
  .admin-figure { font-size: 17px; }
  /* The ban search and its drop-down take the whole phone width, and the row
     buttons wrap under their row instead of shoving the name off the edge. */
  #bans-find, .bans-matches, #bans-reason { max-width: none; }
  .bans-row .btn { flex: 1 1 100%; }
}
