/* 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);
}

/* ---------- the Dino ID cell (2026-08-26, Frontier Prime) ----------
   "there is no current way to find the dino_ID from our end" - asked TWICE,
   the second time while the column was already built and already live. The
   reason it read as absent is above this block: every `.admin-table td` is
   `white-space: nowrap`, and this owner's ids run to 63 characters
   (`fp:shop:<steam64>:<32 hex>:dino`). One unbreakable 63-character cell
   widens the table past the viewport, the wrapper scrolls sideways rather
   than the page - correctly - and the column lands off the right edge, where
   a staff member who does not think to scroll a table never finds it.

   So: the ONLY cell in this sheet that is allowed to wrap, bounded so it can
   never widen the table again, mono so an id reads as an id, and carrying a
   visible affordance because a cell that copies on click and looks exactly
   like text is a feature nobody presses. */
/* ★ min-width IS LOAD-BEARING AND WAS MISSING IN THE FIRST DRAFT. Caught by
   RENDERING it, not by reading it: `overflow-wrap: anywhere` tells the table's
   auto-layout that this cell's MIN-CONTENT width is one character, so the
   algorithm handed the column ~9ch and the 63-character id came out as six
   stacked lines. The floor is what makes the column a column; the ceiling is
   what stops it widening the table the way `nowrap` did. */
.admin-table td.inv-id {
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 25ch;
  max-width: 30ch;
  cursor: pointer;
  vertical-align: top;
}
.admin-table td.inv-id .inv-id-text {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  color: var(--fg);
}
.admin-table td.inv-id .inv-id-copy {
  display: block;
  margin-top: 3px;
  font-family: var(--font-display, var(--font-ui));
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.16em;
  color: var(--fg-mute);
  opacity: .75;
}
.admin-table td.inv-id:hover .inv-id-copy,
.admin-table td.inv-id:focus-visible .inv-id-copy { color: var(--accent); opacity: 1; }
/* The focus ring is the browser's job to place and ours to keep visible: this
   cell is reachable by keyboard and a suppressed ring would strand it. */
.admin-table td.inv-id:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
@media (max-width: 760px) {
  .admin-table td.inv-id { min-width: 17ch; max-width: 20ch; }
}

/* 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 wheel's dino rungs ----------
   ★ 2026-08-28: "they should pick the specie in the drop down". This is the
   one knob on the wheel tab that is a MAP rather than a number, so it is the
   one field that is a GROUP of controls - three rung cards inside the fields
   grid, each holding the dinos on that rung and one dropdown to add another.
   It spans the whole grid because a 230px column cannot hold a rung. */
.wheel-dinos { grid-column: 1 / -1; }

.wheel-dino-tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 12px;
  margin: 2px 0 4px;
}
.wheel-dino-tier {
  padding: 12px 14px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-s);
  min-width: 0;
}
/* The rung's own name and its count, in the field-label voice: this is a
   heading INSIDE a field, never a second page heading. */
.wheel-dino-head { margin-bottom: 8px; }

.wheel-dino-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 10px;
  padding: 0;
  list-style: none;
  min-width: 0;
}
.wheel-dino-chips[hidden] { display: none !important; }

/* One dino on a rung. The name and the way to take it off travel together,
   so there is never a row whose Remove could act on a different animal. */
.wheel-dino-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 4px 3px 10px;
  border: 1px solid var(--border-2);
  border-radius: var(--r-pill);
  background: var(--surface);
}
.wheel-dino-name {
  font-family: var(--font-ui);
  font-size: 12.5px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* A stored name this box cannot hand over is PRINTED AS STORED and marked -
   the mono face because what is shown is the raw class string, which is the
   thing the owner has to recognise and replace. */
.wheel-dino-unknown { border-color: var(--warn); }
.wheel-dino-unknown .wheel-dino-name {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--warn);
}
/* Compact inside a pill, and still a real 26px target. */
.wheel-dino-chip .wheel-dino-remove {
  padding: 5px 9px;
  min-height: 26px;
  font-size: 11px;
  border-radius: var(--r-pill);
  box-shadow: none;
}
.wheel-dino-chip .wheel-dino-remove:hover { transform: none; }

.wheel-dino-add { width: 100%; }
.wheel-dino-add:disabled { opacity: 0.55; cursor: not-allowed; }

@media (max-width: 520px) {
  /* One rung at a time on a phone: three cards side by side would put the
     dropdown and the chips it feeds on different screens. */
  .wheel-dino-tiers { grid-template-columns: 1fr; }
}

/* ---------- 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. */
/* ★ THE PANEL'S OWN HEAD ALREADY SAYS IT, so rentals.js's head is hidden -
   and this needs the id, plus !important, to actually happen (found 2026-08-28
   BY RENDERING IT, not by reading it). The bare `.rentals-head { display:none }`
   that stood here is specificity 0,1,0, and a themed sheet outranks it:

     hatch.css :: html[data-site-theme="hatch"] .sec-head { display: flex; ... }

   is 0,2,1 and WINS. So on every owner running a themed sheet - New Age Dinos
   is one - the temp-skins tab has been printing its title and lede TWICE, once
   from the fragment and once from the panel, for as long as the theme has been
   live. The markup was correct the whole time, which is why only pixels could
   find it. */
#admin-root .rentals-head { display: none !important; }

.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); }

/* ---------- copy skin (2026-08-28) ---------- */
/* ★ THE PAGE-WIDE NOTICE IS HIDDEN ON THIS TAB ONLY. His ruling: the
   "everything on this page is live / there is no undo" block goes, and the
   copied-skins list takes that space. The element lives at the bottom of the
   whole console rather than inside this panel, so it is hidden by the tab
   stamp `js/admin.mjs::selectTab` writes onto #admin-root - which leaves the
   warning exactly where it still belongs on every other tab. */
#admin-root[data-active-tab="rentals"] #admin-notice { display: none; }

/* The player field owns a drop-down, so it has to establish a positioning
   context and sit above the fields after it. */
.rentals-field-player { position: relative; }
.rentals-picks {
  position: absolute;
  z-index: 30;
  top: 100%;
  left: 0;
  right: 0;
  margin: 2px 0 0;
  padding: 4px;
  list-style: none;
  max-height: 220px;
  overflow-y: auto;
  /* Raised on a BORDER and a surface step, never on a drop shadow: this sheet
     is flat everywhere else and its only elevation idiom is the inset accent
     bar. A shadow here would be the one soft edge on the page. */
  background: var(--surface-2);
  border: 1px solid var(--border-2);
  border-radius: var(--r-s);
}
.rentals-pick {
  padding: 6px 8px;
  border-radius: var(--r-xs);
  font-family: var(--font-ui);
  font-size: 13px;
  color: var(--fg);
  cursor: pointer;
}
.rentals-pick:hover,
.rentals-pick:focus { background: var(--surface-hover); }
/* A suggestion with no display name is stated as such, not silently blank. */
.rentals-pick.is-idonly { color: var(--fg-mute); font-family: var(--font-mono); }

.rentals-actions { display: flex; gap: 8px; align-items: end; }

/* THE [hidden] CANCELS for the two things this panel hides, per the law at the
   top of this sheet: the Days field disappears for a copied skin (it has no
   clock), and the "show more" button disappears when the whole list is on
   screen. Neither element carries an author `display`, so the UA rule would
   win today - these are here so it keeps winning if one ever gains one. */
.rentals-field[hidden] { display: none !important; }
.rentals-copies-more[hidden] { display: none !important; }
.rentals-picks[hidden] { display: none !important; }

.rentals-copies { margin-top: 18px; padding-top: 14px;
                  border-top: 1px solid var(--border); }
.rentals-copies h3 {
  margin: 0;
  font-family: var(--font-ui);
  font-size: var(--tm-h-panel);
  font-weight: 600;
  color: var(--fg);
}
.rentals-copies-lede { margin: 3px 0 8px; font-size: 12px; }
.rentals-copies-list .dim { font-family: var(--font-ui); font-size: 12px; }

.rentals-copy-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.rentals-copy-main { flex: 1 1 240px; min-width: 0; }
.rentals-copy-main h4 {
  margin: 0;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
}
/* A player with no name on record reads as dim prose, never as a fake name. */
.rentals-copy-main h4.is-dim {
  color: var(--fg-mute);
  font-weight: 400;
  font-style: italic;
}
/* The SteamID64 is the audit half: monospace so seventeen digits can be
   compared by eye, and selectable so it can be copied out of the log. */
.rentals-copy-id {
  margin: 2px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-dim);
  user-select: all;
  overflow-wrap: anywhere;
}
.rentals-copy-facts {
  margin: 3px 0 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--fg-mute);
}
.rentals-copies-more { margin-top: 10px; }

/* ---------- 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%; }
}

/* ---------- NEWS (2026-08-20) ----------
   The home page's news board, edited here: one form above the list of posts.
   Rows follow the bans list's grammar (hairline between rows, title and a
   dim fact line, the actions on the right) so an admin who has used one
   panel has used them all. */
.newsadmin-form { margin-bottom: 22px; }
.newsadmin-form textarea {
  width: 100%;
  min-height: 150px;
  resize: vertical;
}
.newsadmin-form .admin-inline { margin-top: 12px; }
.newsadmin-rows { list-style: none; margin: 0; padding: 0; }
.newsadmin-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}
.newsadmin-row.is-editing { box-shadow: inset 2px 0 0 var(--accent); padding-left: 10px; }
.newsadmin-row-main { flex: 1 1 240px; min-width: 0; }
.newsadmin-row-main b { display: block; overflow-wrap: anywhere; }
.newsadmin-row-main .dim { display: block; margin-top: 2px; font-size: 12px; }
.newsadmin-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(--accent);
}
.newsadmin-act { display: flex; flex-wrap: wrap; gap: 8px; }
.newsadmin-rows[hidden], .newsadmin-form[hidden] { display: none !important; }
/* =========================================================================
   SKIN SALES panel (2026-08-20). The shelf rows that used to be injected into
   the studio's glitch tab; same shapes, this sheet's tokens. r2 (the same
   day, Frontier Prime): ONE staff-run list - the one-buyer/every-buyer
   choice on the add form and a read-only price chip on a row staff cannot
   reprice (a player's legacy row, a sold one-buyer row).
   ========================================================================= */
.sa-rows { display: grid; gap: 8px; margin: 10px 0; }
.sa-row {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 8px; border: 1px solid var(--border); border-radius: var(--r-xs, 6px);
  background: var(--surface-2, transparent);
}
.sa-row.sa-off { opacity: .55; }
.sa-meta { display: flex; flex-direction: column; min-width: 140px; flex: 1; }
.sa-thumb {
  width: 80px; height: 45px; object-fit: cover; border-radius: 4px;
  border: 1px solid var(--border); flex-shrink: 0;
  background: var(--surface, transparent);
}
.sa-thumb-empty {
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--fg-mute); font: 600 10px/1.1 var(--font-mono, monospace);
  text-transform: uppercase; letter-spacing: .06em; text-align: center;
}
.sa-price { display: inline-flex; align-items: center; gap: 6px; }
.sa-price input { width: 9ch; }
.sa-file input[type="file"] { display: none; }
.sa-add { display: grid; gap: 6px; margin-top: 12px; max-width: 640px; }
.sa-add[hidden] { display: none; }
.sa-add textarea { font: 12px/1.4 var(--font-mono, monospace); }
/* 2026-08-20 r2: the one-buyer/every-buyer choice + read-only price chips. */
.sa-mode-lab { display: grid; gap: 4px; font-size: 12px; color: var(--fg-mute); }
.sa-mode-lab select { padding: 6px 8px; }
.sa-price-fixed {
  font: 600 12px/1 var(--font-mono, monospace); color: var(--fg-mute);
  min-width: 9ch; text-align: right;
}
/* THE PICTURE ON THE LISTING PRESS (2026-08-26, Frontier Prime). A distinct
   class from `.sa-file`: that one is the per-ROW button and hides its input
   behind a `.btn` label, while this is a plain visible field inside the add
   form - a staff member who cannot SEE a file box does not know a picture can
   ride the press, which is the whole defect. */
.sa-add-file { display: grid; gap: 4px; font-size: 12px; color: var(--fg-mute); }
.sa-add-file input[type="file"] { font-size: 12px; }
/* `hidden` must beat the author `display` above, the same reason the row
   below spells it out - an attribute alone loses to any author display. */
.sa-add-file[hidden] { display: none !important; }
/* `#skm-fixed-row` is show()n off the house's staff_only flag - under a
   staff-only house the player fixed price steers nothing. */
#skm-facts[hidden], #skm-actions[hidden], #skm-done[hidden], #skm-fixed-row[hidden] { display: none !important; }
@media (max-width: 760px) {
  .sa-row { align-items: flex-start; }
}

/* ---------- news / updates toggle (2026-08-20) ---------- */
.newsadmin-kinds { display: flex; gap: 8px; margin-bottom: 18px; }
.newsadmin-kind {
  padding: 5px 14px;
  border: 1px solid var(--border);
  background: none;
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--fg-mute);
}
.newsadmin-kind:hover { color: var(--fg); }
.newsadmin-kind.is-on { color: var(--fg); border-color: var(--accent); box-shadow: inset 0 -2px 0 var(--accent); }

/* =========================================================================
   VAULTS panel (2026-08-23, Jurassic Den). Staff open any player's vault by
   id or name and add / edit / remove / restore / wipe. Every value a token:
   the owner's palette drives all of it, not one colour literal. The player
   block is KEY:VALUE readings (dim label, bright value) - the instrument
   signature, never a decorated chip. Remove is two presses and the armed
   press wears the bad tone; wipe is a typed word behind the danger plate.
   ========================================================================= */
.va-search { margin-bottom: 14px; }
.va-search input { flex: 1 1 260px; min-width: 0; }
.va-sr {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.va-matches { list-style: none; margin: 0; padding: 0; }
.va-match {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 10px 0; border-top: 1px solid var(--border);
}
.va-match-who { flex: 1 1 240px; min-width: 0; }
.va-match-who strong { display: block; overflow-wrap: anywhere; }
.va-match-who .ident { margin-top: 2px; }

.va-player {
  display: grid; grid-template-columns: max-content 1fr; gap: 4px 18px;
  align-items: baseline; margin: 0 0 16px; padding: 12px 14px;
  border: 1px solid var(--border); background: var(--surface);
}
.va-player dt {
  font-family: var(--font-display); font-size: 10px; font-weight: 600;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--fg-mute);
}
.va-player dd { margin: 0; color: var(--fg); overflow-wrap: anywhere; }
.va-player dd.va-mono { font-family: var(--font-mono); font-size: 12px; }
.va-player dt.va-player-name { display: none; }
.va-player dd.va-player-name {
  grid-column: 1 / -1; font-family: var(--font-display); font-size: var(--tm-h-sec);
  font-weight: 600; letter-spacing: 0.04em; padding-bottom: 6px;
  border-bottom: 1px solid var(--border);
}

.va-actions { margin-bottom: 14px; flex-wrap: wrap; }
.va-wipe { margin: -6px 0 14px; flex-wrap: wrap; }
.va-wipe input { flex: 0 1 180px; }
.va-wipe[hidden] { display: none !important; }

.va-form {
  margin: 0 0 18px; padding: 14px; border: 1px solid var(--border);
  background: var(--surface);
}
.va-form[hidden] { display: none !important; }
.va-form .admin-sub { margin-top: 0; }
.va-form .admin-fields { margin-bottom: 14px; }
.va-checks { display: flex; flex-wrap: wrap; gap: 8px 18px; align-self: end; }
.va-check { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--fg-dim); }
.va-check[hidden] { display: none !important; }
.va-check input { width: auto; margin: 0; }
.va-family { margin: 0 0 12px; }
.va-slots { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; }
.va-slots select { width: 100%; font-size: 12px; }
.va-slots-elder { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.va-lineage { margin: 0 0 12px; }
.va-lineage > summary {
  cursor: pointer; font-family: var(--font-ui); font-size: var(--tm-h-panel);
  font-weight: 500; color: var(--fg-dim); margin-bottom: 8px;
}
.va-lineage[open] > summary { color: var(--fg); }
.va-note { margin-bottom: 14px; }

.va-table td.va-muts { white-space: normal; max-width: 340px; }
.va-table td.va-act { text-align: right; white-space: nowrap; }
.va-table td.va-act .btn + .btn { margin-left: 6px; }
.va-table td.va-state-redeeming { color: var(--warn); }
.va-table td.va-state-review { color: var(--warn); }
.va-table td.va-state-deleted,
.va-table td.va-state-released { color: var(--fg-mute); }
.va-table-gone td.name { color: var(--fg-dim); }
.va-chip {
  display: inline-block; margin-left: 8px; padding: 1px 7px;
  border: 1px solid var(--border-2); border-radius: var(--r-pill);
  font-family: var(--font-display); font-size: 10px; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-dim);
  vertical-align: middle;
}
.va-chip-prime { border-color: var(--accent); color: var(--accent); }
.va-chip-listed { border-color: var(--ok); color: var(--ok); }
.va-chip-locked { border-color: var(--warn); color: var(--warn); }
.va-remove.va-armed { border-color: var(--bad); color: var(--bad); }
#va-matches[hidden], #va-vault[hidden], #va-gone[hidden], #va-empty[hidden] { display: none !important; }
@media (max-width: 760px) {
  .va-player { grid-template-columns: 1fr; gap: 2px; }
  .va-table td.va-act { text-align: left; }
}

/* ---------------------------------------------------------------------- *
 * LEGACY ADMIN (2026-08-23): five door groups, one form, one result card.
 * ---------------------------------------------------------------------- */
.la-groups {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 12px;
  margin: 4px 0 18px;
}
.la-group {
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px 12px 10px;
}
.la-group h4 {
  margin: 0 0 6px;
  font-size: .78rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.la-group .btn {
  display: block;
  width: 100%;
  text-align: left;
  margin: 6px 0 0;
}
.la-group .btn.active {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}
.la-group .btn[disabled] { opacity: .45; cursor: not-allowed; }
/* The two destructive doors keep the readable ghost face and wear the
   bad token as their signal - a dark danger fill on a dark theme read as
   'disabled' on the first live screenshot. */
.la-group .btn.la-danger { color: var(--bad); border-color: var(--bad-dim); }
.la-group .btn.la-danger:hover { color: var(--bad); }
#la-run.la-danger { background: var(--bad); border-color: var(--bad); }
.la-form[hidden] { display: none !important; }
.la-fields { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.la-result { border-left: 3px solid var(--accent); }
.la-result.ok { border-left-color: var(--ok); }
.la-result.warn { border-left-color: var(--warn); }
.la-result[hidden] { display: none !important; }
.la-result h3 { margin: 0 0 6px; }
.la-result p { margin: 0 0 8px; }
.la-fields-out { display: grid; gap: 8px; }
.la-field b { display: block; font-size: .78rem; letter-spacing: .04em; text-transform: uppercase; color: var(--fg-dim); }
.la-mention { color: var(--accent); }
.la-when { color: var(--fg-dim); font-size: .85rem; white-space: nowrap; }
.la-recent .la-row { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: baseline; }
.la-recent code, .la-result code { font-size: .85em; }
@media (max-width: 640px) {
  .la-recent .la-row { grid-template-columns: 1fr; gap: 2px; }
}

/* ---------- THE DINO STORE PRICE SHEET (2026-08-23, New Age Dinos) ----------
   One row per species: name, shelf, the points price, the supporter price
   (blank = not sold for it), on/off, Save. Inputs are the site's own; the
   sheet only lays the columns out. No raw colour literals. */
.st-toolbar { margin-bottom: 12px; }
.st-filter { flex: 0 1 280px; min-width: 160px; }
.st-sheet { display: grid; gap: 6px; }
.st-row {
  display: grid;
  grid-template-columns: minmax(180px, 1.6fr) minmax(90px, .7fr) minmax(110px, .8fr) minmax(110px, .8fr) 72px minmax(150px, 1.2fr);
  gap: 10px;
  align-items: center;
  padding: 8px 10px;
  border: 1px solid var(--tm-hair, var(--edge));
  background: var(--surface);
  border-radius: var(--r-s);
}
.st-row.st-row-head {
  background: transparent;
  border-color: transparent;
  padding-bottom: 2px;
  color: var(--fg-mute);
  font: 600 11px/1.2 var(--font-ui);
  letter-spacing: .08em;
  text-transform: uppercase;
}
.st-row.st-off { opacity: .6; }
.st-row.st-dirty { border-color: var(--accent); }
.st-row.st-busy { opacity: .7; }
.st-cell { min-width: 0; }
.st-cell input[type="number"] { width: 100%; }
.st-species b { display: block; font-size: 14.5px; }
.st-species .dim { display: block; font-size: 11.5px; }
.st-onsale { text-align: center; }
.st-onsale input { width: 18px; height: 18px; }
.st-act { display: flex; align-items: center; gap: 10px; }
.st-row-note { font-size: 12.5px; color: var(--fg-mute); min-width: 0; }
.st-row-note.ok { color: var(--ok); }
.st-row-note.warn { color: var(--warn); }
@media (max-width: 900px) {
  .st-row { grid-template-columns: 1fr 1fr; }
  .st-row.st-row-head { display: none; }
  .st-species { grid-column: 1 / -1; }
  .st-act { grid-column: 1 / -1; }
}

/* THE PAGING BUTTONS SHIP `hidden` AND A BUTTON CARRIES `.btn`, so the class
   needs the same cancel every other hideable element on this page has. Without
   it the History's Newer/Older keys would be VISIBLE wherever an author rule
   gives a `.btn` a display of its own (`.la-group .btn` does) - the exact
   recorded defect this sheet's [hidden] cancels exist for, and the reason
   test_admin_fragment derives the hideable set from the markup rather than
   from a list somebody maintains. */
.btn[hidden] { display: none !important; }

/* ---------- location teleport: the destinations grid ---------- */
/* The 27 places, ticked or not. A GRID rather than a list because 27 stacked
   checkboxes is a scroll an owner gives up on, and auto-fit means the same
   markup is four columns on a desktop and one on a phone with no second
   breakpoint to keep in step. `minmax(0, ...)` so a long name wraps inside
   its own cell instead of widening the column and pushing the last one off
   the edge - the failure the admin table's own phone rules exist for. */
.ltp-places-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
  gap: 6px 18px;
  margin: 6px 0 16px;
}
.ltp-places-grid .admin-switch { align-items: flex-start; }
.ltp-places-grid .admin-switch span { overflow-wrap: anywhere; }
#ltp-species-rows input[type="number"] { width: 100%; max-width: 130px; }
/* The trail's own reading aid: a refusal and a refund are the two rows an
   owner scans for, and colour is the only thing that finds them in a wall of
   timestamps. Text carries the word as well, so this is never the only cue. */
.admin-table tr[data-outcome="refused"] td:nth-child(5) { color: var(--warn); }
.admin-table tr[data-outcome="refunded"] td:nth-child(5) { color: var(--accent); }

/* ---------- two wallets on one card (2026-08-29) ----------
   Dino Queen's Lair runs a points economy AND a supporter currency, and the
   points console drew one. Each wallet's tiles now sit under a head that is
   the currency's OWN NAME (data from the console gate, never a word from a
   sheet or a script), the press carries a two-way picker for which wallet it
   moves, and the amount box says what unit it is in. Every value below is one
   this sheet already uses; the picker is the only new shape, and it is the
   segmented control the live page's own tabs are. */

.pts-wallet-head {
  margin: 18px 0 10px;
  font-family: var(--font-display);
  font-size: var(--tm-h-micro);
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--fg-dim);
}
.pts-wallet-head[hidden] { display: none !important; }
.pts-wallet-head-credits { color: var(--accent-hi); }

/* the second wallet's tiles carry the accent, so the two rows read as two
   wallets at a glance and not as one long row of numbers */
.pts-credits-odds .admin-odd { border-color: color-mix(in srgb, var(--accent) 42%, var(--border)); }
.pts-credits-odds .admin-odd b { color: var(--accent-hi); }

.admin-odd b { font-size: 22px; letter-spacing: 0.01em; }
.admin-odd { transition: border-color .18s ease; }
.admin-odd:hover { border-color: var(--accent); }

.admin-seg {
  display: inline-flex;
  gap: 2px;
  margin: 4px 0 14px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--r-pill, 999px);
  background: var(--surface-2);
}
.admin-seg[hidden] { display: none !important; }
.admin-seg-btn {
  border: 0;
  background: transparent;
  color: var(--fg-dim);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 7px 16px;
  border-radius: var(--r-pill, 999px);
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.admin-seg-btn:hover { color: var(--fg); }
.admin-seg-btn.active {
  background: var(--accent);
  color: var(--bg-deep);
}
.admin-seg-btn:focus-visible { outline: 2px solid var(--accent-hi); outline-offset: 2px; }

.pts-amount-unit {
  margin-left: 6px;
  font-weight: 600;
  color: var(--accent-hi);
}

/* quick amounts: a chip fills the box, it never presses Give */
.admin-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.admin-chips[hidden] { display: none !important; }
.admin-chip {
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--fg-dim);
  font-family: var(--font-mono);
  font-size: 11px;
  padding: 4px 10px;
  border-radius: var(--r-pill, 999px);
  cursor: pointer;
  transition: border-color .15s ease, color .15s ease;
}
.admin-chip:hover,
.admin-chip:focus-visible { border-color: var(--accent); color: var(--fg); }

/* a sub-section head with an action on its right (the CSV door) */
.admin-sub-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.admin-sub-head .page-h3 { margin: 0 0 10px; }
.pts-wallet-tag {
  margin-left: 8px;
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fg-mute);
}
.pts-wallet-tag-credits { color: var(--accent-hi); }
#pts-export[hidden] { display: none !important; }
