/*
  Teisora operator console.

  Deliberately its own visual language rather than teisoraweb's: an operator
  should never be able to mistake this tab for the customer site, because the
  buttons here rewrite what every MCP client reads. Dense, monospace-leaning,
  and obviously a tool.

  Light and dark both, driven by prefers-color-scheme — an internal tool gets
  used at odd hours and a forced-light console at 2am is its own small hostility.
*/

:root {
  --bg: #fafaf9;
  --surface: #ffffff;
  --surface-2: #f4f5f3;
  --border: #e6e7e3;
  --border-strong: #c9cbc4;
  --text: #171c19;
  --text-dim: #6a716c;
  /* Green, replacing the terracotta this console started with. #15803d clears
     4.5:1 on white in both directions, so it can carry button text and link
     text without a second, darker variant. */
  --accent: #15803d;
  --accent-text: #ffffff;
  /* The soft pair is the rail's active pill, the identity chip and the tile
     glyph plates — a tint that reads as "selected" without becoming a button. */
  --accent-soft: #e8f4ec;
  --accent-soft-text: #146134;
  --danger: #a02216;
  --danger-bg: #fdf0ee;
  --ok: #15803d;
  --warn-bg: #fdf7e3;
  --warn-border: #e6d08a;
  --focus: #1f6feb;
  --radius: 10px;
  --radius-sm: 8px;
  --rail: 15rem;
  /* The chart mark is its OWN token, not --accent. A mark and a piece of text
     are held to different thresholds, and the dark-mode accent (#4ade80) fails
     the chart lightness band at L 0.80 — too light to read as a filled column
     against the dark surface. Both values below were run through the dataviz
     validator rather than picked by eye. */
  --chart-mark: #15803d;
  --chart-mark-hover: #146134;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #141715;
    --surface: #1c201d;
    --surface-2: #252a26;
    --border: #333a35;
    --border-strong: #4a5249;
    --text: #e7ebe8;
    --text-dim: #98a09a;
    --accent: #4ade80;
    --accent-text: #10231a;
    --accent-soft: #1d3327;
    --accent-soft-text: #86e0a8;
    --chart-mark: #16a34a;
    --chart-mark-hover: #22c55e;
    --danger: #e5705f;
    --danger-bg: #2a1c1a;
    --ok: #6cc08a;
    --warn-bg: #2b2718;
    --warn-border: #5c5230;
    --focus: #64a0ff;
  }
}

*, *::before, *::after { box-sizing: border-box; }

/* Tell the browser both themes are supported, so the controls it still draws
   itself — the date picker, the number spinners, scrollbars — follow the page
   instead of staying light on a dark one. */
:root { color-scheme: light dark; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* ---------- form controls ---------- */

/* ⚠ THERE WAS NO BASE RULE FOR THESE AT ALL. Every button in this console was
   given the design's border, radius and colours; every input was left to the
   user agent, so a field and the button beside it sat side by side with a dark
   ~2px-radius browser border against a light 10px-radius one. It reads as
   "crooked" before you can name what is wrong, and it was on every page with a
   form — sign-in included, which is the first screen anyone is shown.
   file inputs are excluded: .file-pick styles the label around them, and a
   border here would draw a second box around the first. */
input:not([type="file"]), select, textarea {
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 0.4rem 0.6rem;
}
input:not([type="file"]):focus-visible, select:focus-visible, textarea:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
  border-color: var(--focus);
}
input::placeholder, textarea::placeholder { color: var(--text-dim); }
input[disabled], select[disabled], textarea[disabled] {
  opacity: 0.6;
  cursor: not-allowed;
}

/* Base links.
   Until the operator tables (ADR-0026) every anchor in this console carried its
   own class — .crumbs a, .card-link, .nav-link — so no bare <a> rule was ever
   needed and none existed. The first unclassed anchors, a column of account
   emails, therefore rendered in the user-agent default #0000EE on a #16161a
   background: blue on black, effectively unreadable, and purple once visited.
   Found by opening the page, which is the only thing that could have found it —
   no assertion about markup can see a colour. */
a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

/* Visited stays the same colour on purpose: an operator re-reads the same
   accounts all day, so a fading link list carries no information here and only
   costs contrast. */
a:visited { color: var(--accent); }
a:hover { text-decoration-thickness: 2px; }

/* A visible focus ring everywhere. Keyboard is how anyone edits text quickly. */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 3px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-text);
  padding: 0.75rem 1rem;
  z-index: 10;
}
.skip:focus { left: 0; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.wrap {
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 1.25rem;
}

/* ---------- app frame: rail + body ---------- */

.app {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100vh;
}

.sidebar {
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 0.75rem 0.75rem 1.5rem;
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  /* The rail outlives the page's scroll: an operator half way down a 250-row
     account table still has every section one click away. A fixed 100vh rather
     than max-height — the grid row is only as tall as its content, so a rail
     sized to itself stopped a third of the way down and left the border
     hanging in mid-air. */
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.4rem;
  color: var(--text);
  text-decoration: none;
  border-radius: var(--radius-sm);
}
.brand:hover { background: var(--surface-2); }
.brand:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.brand-mark {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: var(--accent-text);
}
.brand-mark svg { width: 17px; height: 17px; }
.brand-text {
  font-weight: 650;
  letter-spacing: -0.01em;
  font-size: 0.98rem;
  line-height: 1.2;
}
.brand-text span {
  display: block;
  font-family: var(--mono);
  font-weight: 400;
  font-size: 0.68rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.side-nav {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.side-link {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  /* 44px tall: a touch target that survives the narrow layout below. */
  min-height: 44px;
  padding: 0.5rem 0.7rem;
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  text-decoration: none;
  font-size: 0.93rem;
  font-weight: 500;
}
.side-link:hover { background: var(--surface-2); color: var(--text); }
.side-link:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.side-link.current {
  background: var(--accent-soft);
  color: var(--accent-soft-text);
  font-weight: 600;
}
.side-icon { width: 18px; height: 18px; flex: none; }

.app-body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The rail's foot: who is signed in, and the way out. Pushed down by auto so it
   sits on the bottom edge whatever the nav above it holds. */
.rail-foot {
  margin-top: auto;
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.45rem;
}
/* An address is longer than a 15rem rail. Ellipsis rather than wrap, with the
   full value on the title attribute — two wrapped lines of mono would push the
   sign-out button around as the operator changes. */
.who {
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--text-dim);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-foot .inline { width: 100%; }
.rail-out { display: inline-flex; align-items: center; justify-content: center; gap: 0.4rem; }
.btn-icon { width: 15px; height: 15px; flex: none; }

.content {
  padding: 1.75rem 1.5rem 4rem;
  max-width: 1180px;
  width: 100%;
}

/* Below 60rem the rail lies down: same sections, same order, one tap each,
   scrolling sideways rather than taking a phone's whole width. */
@media (max-width: 60rem) {
  .app { grid-template-columns: minmax(0, 1fr); }
  .sidebar {
    position: static;
    /* ⚠ BOTH of these, not just max-height. The desktop rail sets height:100vh;
       overriding only max-height left a full-screen-tall strip with the nav
       floating in the middle of it and the page pushed below the fold. */
    height: auto;
    max-height: none;
    flex-direction: row;
    align-items: center;
    gap: 0.75rem;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    padding: 0.5rem 1rem;
    overflow-x: auto;
  }
  .brand-text span { display: none; }
  .side-nav { flex-direction: row; gap: 0.25rem; }
  .side-link { white-space: nowrap; }
  /* Laid down, the foot sits at the far end of the strip rather than below the
     nav: margin-top:auto means nothing in a row, so it is margin-left that
     pushes it over. The address is dropped — it is the least useful thing on a
     phone and the widest. */
  .rail-foot {
    flex-direction: row;
    align-items: center;
    margin: 0 0 0 auto;
    padding: 0 0 0 0.75rem;
    border-top: 0;
    border-left: 1px solid var(--border);
  }
  .rail-foot .who { display: none; }
  .rail-foot .inline { width: auto; }
  .rail-out { white-space: nowrap; }
  .content { padding: 1.25rem 1rem 3rem; }
}

/* ---------- typography ---------- */

h1 { font-size: 1.5rem; margin: 0 0 0.35rem; letter-spacing: -0.015em; }
h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
p { margin: 0 0 0.6rem; }
.sub { color: var(--text-dim); max-width: 62ch; margin: 0; }
.dim { color: var(--text-dim); font-size: 0.85rem; }
code {
  font-family: var(--mono);
  font-size: 0.86em;
  background: var(--surface-2);
  padding: 0.1em 0.35em;
  border-radius: 3px;
}

.crumbs {
  font-size: 0.85rem;
  color: var(--text-dim);
  margin-bottom: 0.75rem;
}
.crumbs a { color: var(--text-dim); }

.page-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}
.page-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.version-line {
  margin: 0.6rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
}

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

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  /* 44px minimum target: this gets used on a tablet on a desk, not only a mouse. */
  min-height: 44px;
  padding: 0 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background 120ms ease, border-color 120ms ease;
}
.button:hover { background: var(--surface-2); }
.button:active { transform: translateY(1px); }
.button[disabled] { opacity: 0.55; cursor: not-allowed; transform: none; }
.button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-text);
  font-weight: 550;
}
.button.primary:hover { filter: brightness(1.08); }
.button.ghost { background: transparent; border-color: var(--border); }
.button.block { width: 100%; }
.button.small { min-height: 34px; padding: 0 0.7rem; font-size: 0.82rem; }
.button.danger {
  border-color: var(--danger);
  color: var(--danger);
  background: transparent;
}
.button.danger:hover { background: var(--danger-bg); }

form.inline { display: inline; margin: 0; }

.field { margin-bottom: 0.9rem; }
.field label {
  display: block;
  font-size: 0.82rem;
  font-weight: 550;
  margin-bottom: 0.3rem;
  color: var(--text-dim);
}
.element {
  width: 100%;
  min-height: 44px;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.92rem;
}
.element:focus-visible { border-color: var(--focus); }

.upload {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}
/* The native file input is unstyleable and ugly; the label is the control. */
.file-pick {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.9rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 0.86rem;
  cursor: pointer;
  max-width: 22rem;
}
.file-pick:hover { background: var(--surface-2); }
.file-pick:focus-within { outline: 2px solid var(--focus); outline-offset: 2px; }
.file-pick input[type="file"] {
  font: inherit;
  font-size: 0.8rem;
  max-width: 12rem;
  margin-left: 0.5rem;
  color: var(--text-dim);
}
/* The browser's own "Choose file" chrome is the one control on the page that
   looks like nothing else on it. Restyling the selector button costs four
   declarations and is the difference between a console and a form. */
.file-pick input[type="file"]::file-selector-button {
  font: inherit;
  font-size: 0.8rem;
  margin-right: 0.55rem;
  padding: 0.25rem 0.65rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
}
.file-pick input[type="file"]::file-selector-button:hover { background: var(--border); }
.file-pick-label { font-weight: 550; white-space: nowrap; }

/* ---------- daily trend charts ---------- */

/* ⚠ NAMED trend-*, NOT chart-*. A .chart / .chart-bar pair already exists for the
   per-account activity list further down this file, and being later it silently
   won: the columns came out grey (background: currentColor) with the card's
   padding removed. Third collision of this kind in one sitting — see the guard
   test in internal/server. */

.trends { display: grid; gap: 1.25rem; }

.trend {
  margin: 0;
  padding: 1rem 1.1rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.trend figcaption { margin-bottom: 0.85rem; }
.trend h2 { font-size: 1rem; margin: 0 0 0.15rem; }
.trend-facts { margin: 0; font-size: 0.84rem; color: var(--text-dim); }
.trend-facts strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* The plot is a fixed height so three charts down the page share one visual
   rhythm; each series is scaled to ITS OWN maximum, never to the largest of the
   three — requests and people are not comparable quantities. */
/* ⚠ THE PADDING IS THE PEAK LABEL'S ROOM, not decoration. The label sits above
   its column, and a column at 100% reaches the top of the plot — so with no
   headroom the number climbed out of the box and landed on the caption above
   it. It only showed up once the peak happened to be the FIRST column, where
   the caption text is still running; on the test data the peak was on the right
   and the collision hid behind a gap in the sentence. */
.trend-plot {
  height: 150px;
  padding-top: 18px;
  display: flex;
  align-items: flex-end;
}
.trend-bars {
  display: flex;
  align-items: flex-end;
  /* 2px between columns, the surface showing through — adjacent fills that touch
     read as one block. */
  gap: 2px;
  width: 100%;
  height: 100%;
}
/* One column per day: full height, so the whole strip is the hover target even
   when the bar inside it is a hairline. */
.trend-col {
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  display: flex;
  align-items: flex-end;
  position: relative;
}
.trend-col:hover { background: var(--surface-2); }

.trend-bar {
  width: 100%;
  /* Anchored to the baseline, rounded at the data end only: a rounded foot would
     lift the column off its own axis. */
  border-radius: 4px 4px 0 0;
  background: var(--chart-mark);
  position: relative;
  transition: background 100ms ease;
}
.trend-col:hover .trend-bar { background: var(--chart-mark-hover); }
/* A day with one request must not render identically to a day with none; the
   floor is applied in Go, this keeps the empty ones visible as a hairline. */
.trend-col .trend-bar[style*="height:0.00%"] {
  height: 1px !important;
  background: var(--border);
  border-radius: 0;
}

/* Only the peak carries a printed number. A value on every column is noise, and
   the peak is the one a reader goes looking for. */
.trend-peak {
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  transform: translateX(-50%);
  /* A centred label on the first or last column would hang off the plot. Those
     two anchor inward instead — the only two that can. */
  pointer-events: none;
  font-size: 0.72rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: var(--surface);
  padding: 0 0.15rem;
  white-space: nowrap;
}
/* The value readout. A CSS tooltip rather than the browser's own: the native one
   needs a second of dwell and cannot be styled, and on a chart the whole point is
   to sweep across the columns and read them. title= stays on the element as the
   accessible fallback. */
.trend-col::after {
  content: attr(data-v);
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
  margin-bottom: 4px;
  padding: 0.2rem 0.45rem;
  border-radius: var(--radius-sm);
  background: var(--text);
  color: var(--bg);
  font-size: 0.74rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 80ms ease;
  z-index: 3;
}
.trend-col:hover::after, .trend-col:focus-visible::after { opacity: 1; }
/* At the ends the tooltip and the peak label anchor inward — centred, they would
   hang off the plot. */
.trend-col:first-child::after, .trend-col:first-child .trend-peak { left: 0; transform: none; }
.trend-col:last-child::after, .trend-col:last-child .trend-peak { left: auto; right: 0; transform: none; }

/* Only the ends of the axis are labelled: twenty-eight dates across a narrow
   chart collide into a grey smear. */
.trend-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 0.4rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--border);
  font-size: 0.75rem;
  color: var(--text-dim);
}

.trend-data { margin-top: 0.6rem; }
.trend-data summary {
  font-size: 0.8rem;
  color: var(--text-dim);
  cursor: pointer;
  min-height: 32px;
  display: inline-flex;
  align-items: center;
}
.trend-data summary:hover { color: var(--text); }
.trend-data[open] { margin-bottom: 0.4rem; }
.trend-data .table-scroll { max-height: 18rem; overflow-y: auto; }

/* Two across once there is room; one column on a phone, where a 28-bar chart is
   already as narrow as it can usefully be. */
@media (min-width: 62rem) {
  .trends { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .trends > .chart:first-child { grid-column: 1 / -1; }
}

/* ---------- section tabs ---------- */

/* Views WITHIN one section, as opposed to .side-nav, which is the sections
   themselves. The underline sits on the shared rule, so the current view reads
   as part of the page below it rather than as a button floating above it. */
.tabs {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--border);
  margin: -0.5rem 0 1.25rem;
  overflow-x: auto;
}
.tab {
  display: inline-flex;
  align-items: center;
  /* 44px, like every other target in the console. */
  min-height: 44px;
  padding: 0 0.85rem;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 0.93rem;
  font-weight: 500;
  white-space: nowrap;
  border-bottom: 2px solid transparent;
  /* Pull the 2px marker over the container's 1px rule so the two do not stack
     into a 3px step under the active tab. */
  margin-bottom: -1px;
}
.tab:hover { color: var(--text); }
.tab:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
.tab.current {
  color: var(--accent);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* ---------- affiliate: headline numbers ---------- */

/* Two strips rather than one eight-up row: the counts and the money do not add
   up to each other, and a single undifferentiated grid invites reading the
   gross and the commission as parts of one total. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.75rem;
  margin: 0 0 0.75rem;
}

.stat {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.stat-k {
  font-size: 0.78rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.stat-n {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.stat-n.ok { color: var(--accent); }
.stat-n.warn { color: var(--text); }
.stat-n.dim { color: var(--text-dim); font-weight: 500; }
/* The caption carries the unit. "11" is unreadable without "iš jų mokėjo", so
   this is content, not decoration, and never shrinks away. */
.stat-c { font-size: 0.78rem; color: var(--text-dim); }

.section-h {
  font-size: 1rem;
  margin: 1.5rem 0 0.6rem;
}

/* ---------- status ---------- */

.notice, .error, p.ok {
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius);
  font-size: 0.9rem;
  margin: 0 0 1rem;
}
.notice { background: var(--surface-2); border: 1px solid var(--border); }
.notice.warn { background: var(--warn-bg); border-color: var(--warn-border); }
.error { background: var(--danger-bg); border: 1px solid var(--danger); color: var(--danger); }
/* ⚠ ELEMENT-SCOPED ON PURPOSE. A bare .ok also matched <span class="badge ok">
   and <span class="summary-n ok">, which are TONE modifiers, not status
   blocks — so a coloured number was given a bordered box of its own. The
   status message is only ever a <p>, so scoping it there costs nothing and
   makes the collision impossible to re-create. */
p.ok { background: var(--surface-2); border: 1px solid var(--ok); color: var(--ok); }

.badge {
  display: inline-block;
  font-family: var(--mono);
  font-size: 0.74rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  white-space: nowrap;
}
.badge.current { border-color: var(--ok); color: var(--ok); }
.badge.history { border-color: var(--warn-border); color: var(--text-dim); }
/* Tone modifiers for the affiliate ledger. They exist because scoping the
   status block to p.ok left `class="badge ok"` matching nothing at all — the
   badge rendered, in the default grey, and the tone was silently inert. */
.badge.ok { border-color: var(--ok); color: var(--ok); }
.badge.danger { border-color: var(--danger); color: var(--danger); }

/* ---------- empty states ---------- */

.empty {
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  padding: 2.5rem 1.5rem;
  text-align: center;
  color: var(--text-dim);
  background: var(--surface);
}
.empty h2 { color: var(--text); }
.empty p { max-width: 48ch; margin: 0.5rem auto 0; }
.empty.small { padding: 3rem 1rem; }

/* ---------- catalogue ---------- */

.cards {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 1rem;
}
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}
.card:hover { border-color: var(--border-strong); }
.card-link { text-decoration: none; color: inherit; }
.card-link h2 { font-family: var(--mono); font-size: 1rem; }
.card .desc {
  color: var(--text-dim);
  font-size: 0.88rem;
  margin: 0.3rem 0 0;
  display: -webkit-box;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.meta {
  display: flex;
  gap: 1.1rem;
  margin: auto 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
  font-size: 0.78rem;
}
.meta dt { color: var(--text-dim); margin: 0; }
.meta dd { margin: 0; font-family: var(--mono); }

/* ---------- editor ---------- */

.editor-grid {
  display: grid;
  grid-template-columns: minmax(200px, 260px) 1fr;
  gap: 1.25rem;
  align-items: start;
}
@media (max-width: 760px) {
  .editor-grid { grid-template-columns: 1fr; }
}

.pane-title {
  font-family: var(--mono);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-dim);
  margin: 0 0 0.6rem;
}

.files {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.9rem;
  position: sticky;
  top: 76px;
}
.files ul { list-style: none; margin: 0; padding: 0; }
.file {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  min-height: 40px;
  padding: 0.35rem 0.5rem;
  border-radius: 4px;
  text-decoration: none;
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.8rem;
}
.file:hover { background: var(--surface-2); }
.file.current {
  background: var(--surface-2);
  box-shadow: inset 2px 0 0 var(--accent);
  font-weight: 600;
}
.file-path { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-size { color: var(--text-dim); font-size: 0.72rem; flex-shrink: 0; }
.pane-foot {
  margin: 0.8rem 0 0;
  padding-top: 0.7rem;
  border-top: 1px solid var(--border);
  font-size: 0.76rem;
  color: var(--text-dim);
}

.editor {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem;
}
.editor-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
.editor-head .pane-title { text-transform: none; font-size: 0.9rem; color: var(--text); }
textarea.code {
  width: 100%;
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.65;
  padding: 0.8rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--text);
  resize: vertical;
  min-height: 24rem;
  tab-size: 2;
}
.editor-foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.9rem;
  flex-wrap: wrap;
}
.inline-field { flex: 1 1 20rem; margin: 0; }

/* ---------- inline price editor (plans) ---------- */

.price-edit {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  flex-wrap: wrap;
}
.price-edit label { font-size: 0.86rem; color: var(--text-dim); }

/* The field is sized to a price, not to the column. A four-character amount in
   a 20rem box reads as an empty form waiting to be filled in, rather than a
   value that is already set and can be adjusted. */
.price-input {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
/* The field and the button are pinned to ONE height rather than left to their
   own minimums (38 and 36), which put a two-pixel step between two controls
   sitting side by side — the kind of misalignment you read as "off" before you
   can say why. */
.price-edit .price-input input,
.price-edit .button {
  height: 38px;
  min-height: 38px;
}
.price-input input {
  width: 6.5rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.price-cur { color: var(--text-dim); font-size: 0.9rem; }

/* ⚠ THE BASIS IS A WIDTH IN A ROW AND A HEIGHT IN A COLUMN, and .card is a
   column. So `flex: 1 1 20rem` claimed 320px of HEIGHT inside every plan card:
   a card whose content is 160px tall rendered 459px tall, two thirds of it
   empty. Measured, not guessed — the form's own box was 320px while its
   children summed to a fraction of that. Row contexts keep the basis. */
.card > .inline-field { flex: 0 0 auto; }

/* ---------- history ---------- */

.history { margin-top: 2rem; }
.versions { list-style: none; margin: 0; padding: 0; }
.version {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: 0.4rem;
}
.version.is-current { border-color: var(--ok); }
.version-open {
  flex: 1;
  display: grid;
  grid-template-columns: 3.5rem 1fr auto;
  gap: 0.75rem;
  align-items: center;
  text-decoration: none;
  color: inherit;
  min-height: 36px;
  overflow: hidden;
}
.version .v { font-family: var(--mono); font-weight: 600; font-size: 0.85rem; }
.version .note {
  color: var(--text-dim);
  font-size: 0.85rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.version .when { font-family: var(--mono); font-size: 0.76rem; color: var(--text-dim); }
@media (max-width: 620px) {
  .version-open { grid-template-columns: 3rem 1fr; }
  .version .when { grid-column: 2; }
}

/* ---------- danger zone ---------- */

/* ⚠ ELEMENT-SCOPED, same reason as p.ok above and a worse symptom. A bare
   .danger also matched <span class="badge danger"> — every expiry date in the
   accounts table — and handed it margin-top: 2.5rem, which pushed the date
   onto a line of its own and tripled the height of every row. The zone is
   always a <section>. */
section.danger {
  margin-top: 2.5rem;
  padding: 1rem 1.1rem;
  border: 1px solid var(--border);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius);
  background: var(--surface);
}
section.danger p { color: var(--text-dim); font-size: 0.86rem; max-width: 62ch; }

/* ---------- sign-in ---------- */

.auth-main {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 2rem 1.25rem;
}
.auth-card {
  width: 100%;
  max-width: 26rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 2rem;
}
.auth-head { margin-bottom: 1.5rem; }
.auth-head h1 { font-size: 1.25rem; }
.auth-head p { color: var(--text-dim); font-size: 0.88rem; margin: 0; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .button:active { transform: none; }
}

/* ── Operator pages (ADR-0026) ─────────────────────────────────────────────
   The accounts browser, the account detail page, plans and the audit log.
   Same tokens as the rest of the console; nothing here introduces a palette. */

/* The horizontal masthead nav (.nav/.nav-link) lived here until the rail
   replaced it. Removed rather than left behind: no template references it, and
   a dead selector is indistinguishable from one whose element you forgot. */

.search { display: flex; align-items: center; gap: 0.5rem; margin: 1rem 0; }
.search input { flex: 1; min-height: 44px; }

.spinner {
  /* Hidden at rest, in CSS rather than by datastar — see the comment on the
     element. .on is added by data-class while a search is in flight. */
  display: none;
  width: 1rem;
  height: 1rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}
.spinner.on { display: block; }

@keyframes spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; opacity: 0.5; }
}

/* A wide table must scroll inside its own box; the page never scrolls
   sideways. */
.table-scroll { overflow-x: auto; }

.grid { width: 100%; border-collapse: collapse; font-size: 0.94rem; }
.grid th, .grid td { padding: 0.55rem 0.6rem; text-align: left; vertical-align: top; }

/* A whole column of links is not body text with a link in it. Accent-coloured
   and hard-underlined, forty account emails read as forty warnings; the eye has
   nowhere to rest and the green stops meaning "this is the action". So: normal
   text colour with a quiet underline, going full strength on hover and focus.
   The underline STAYS at rest on purpose — colour alone must never be the only
   thing marking a link (WCAG 1.4.1). */
.grid a { color: var(--text); text-decoration-color: var(--border-strong); }
.grid a:hover, .grid a:focus-visible { color: var(--accent); text-decoration-color: currentColor; }
.grid tbody tr:hover { background: var(--surface-2); }
.grid thead th { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.03em; opacity: 0.7; }

/* Sortable headers. The control fills the cell so the whole header is the
   target, not just the word — a 12px label is not a thing to aim at. */
.grid thead th:has(.sort) { padding: 0; }
.sort {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  width: 100%;
  min-height: 40px;
  padding: 0.35rem 0.6rem;
  background: none;
  border: 0;
  border-radius: 0;
  font: inherit;
  color: inherit;
  text-align: inherit;
  text-decoration: none;
  cursor: pointer;
}
.grid .num .sort { justify-content: flex-end; }
.sort:hover { background: var(--surface-2); color: var(--text); }
.sort:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }
/* The arrow keeps its width whether or not it is showing, so the labels do not
   jump sideways as the sorted column changes. */
.sort-mark {
  display: inline-block;
  min-width: 0.7em;
  color: var(--accent);
  font-weight: 700;
}
.grid thead th[aria-sort="ascending"] .sort,
.grid thead th[aria-sort="descending"] .sort { color: var(--accent); }
.grid tbody tr { border-top: 1px solid rgba(127, 127, 127, 0.22); }
/* An amount and a timestamp each read as ONE token even though they contain a
   space. Wrapped, "28.76 €" becomes two lines and "2026-09-10 21:21" becomes
   two more, so a table of money and dates renders at triple height with the
   currency sign orphaned under its number. */
.grid .num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.grid .when { white-space: nowrap; }

.chip {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  margin: 0 0.2rem 0.2rem 0;
  border-radius: 999px;
  font-size: 0.8rem;
  background: rgba(127, 127, 127, 0.18);
}

.pager { display: flex; align-items: center; gap: 0.75rem; margin: 1rem 0; }

/* The accounts summary strip: a toolbar of counts, each one a filter.
   Wraps rather than scrolls, so on a phone it becomes rows of tiles instead of
   a horizontal scroller nobody discovers. */
.summary {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0 0 1rem;
}

.summary-tile {
  flex: 1 1 7.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-height: 44px; /* a touch target, not just a label */
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.summary-tile:hover { border-color: var(--border-strong); }
.summary-tile:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.summary-tile:active { transform: translateY(1px); }

/* The selected bucket. Border and a left rule rather than a fill, so the
   number stays as readable as the ones beside it. */
.summary-tile.is-on {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
  background: var(--surface-2);
}

.summary-n {
  font-size: 1.35rem;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.summary-n.ok { color: var(--ok); }
.summary-n.danger { color: var(--danger); }
.summary-n.warn { color: var(--accent); }

.summary-k {
  font-size: 0.78rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

@media (prefers-reduced-motion: reduce) {
  .summary-tile:active { transform: none; }
}

.standing {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1rem;
  margin: 1rem 0 1.5rem;
}

.standing dt, .windows dt { font-size: 0.8rem; opacity: 0.7; }
.standing dd, .windows dd { margin: 0.15rem 0 0; font-size: 1.35rem; font-variant-numeric: tabular-nums; }

.panel { margin: 2rem 0; }

.windows {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: 0.75rem;
}

/* The activity chart is CSS bars, not a library: this console loads no
   JavaScript of its own. */
.chart { list-style: none; padding: 0; margin: 0.5rem 0 0; }
.chart li { display: grid; grid-template-columns: 6rem 1fr 3rem; gap: 0.5rem; align-items: center; }
.chart-date { font-size: 0.8rem; opacity: 0.7; font-variant-numeric: tabular-nums; }
.chart-track { background: rgba(127, 127, 127, 0.15); border-radius: 3px; height: 0.7rem; }
.chart-bar { display: block; height: 100%; border-radius: 3px; background: currentColor; opacity: 0.6; }
.chart-count { text-align: right; font-variant-numeric: tabular-nums; font-size: 0.85rem; }

.actions-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1rem;
}

.action-card {
  /* Was a hardcoded rgba grey and a 0.6rem radius, which stopped matching the
     rest of the console the moment the tokens moved. Tokens now. */
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/* The cards in a row are as tall as the tallest, so a short card's button
   floated wherever its content ended and the row of actions came out ragged —
   three commit buttons at three different heights. Pushing the last row down
   lands them on one line whatever each card holds above it. */
.action-card > .row:last-child,
.action-card > button:last-child { margin-top: auto; }

.action-card h3 { margin: 0; }
.action-card .row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: flex-end; }
.action-card .field { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 5rem; }
.action-card input, .action-card select { min-height: 44px; }

/* The sessions card states the pool it is about to change (ADR-0034). Smaller
   than .windows on purpose: it is context for the input below it, not a
   headline figure competing with the usage counts. */
.session-state {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
  gap: 0.5rem;
  margin: 0;
}
.session-state dt { font-size: 0.8rem; opacity: 0.7; }
.session-state dd { margin: 0.15rem 0 0; font-size: 1.05rem; font-variant-numeric: tabular-nums; }

.stack { display: flex; flex-direction: column; gap: 0.75rem; max-width: 28rem; }
.stack .field { display: flex; flex-direction: column; gap: 0.25rem; }
.stack input { min-height: 44px; }

.inline-field.tight { gap: 0.25rem; }
.button.small { min-height: 36px; padding: 0.25rem 0.6rem; font-size: 0.85rem; }

/* The once-shown credential. Loud on purpose: it cannot be recovered, so an
   operator who scrolls past it has lost it. */
.minted {
  border: 2px solid currentColor;
  border-radius: 0.6rem;
  padding: 1rem 1.25rem;
  margin: 1rem 0;
}

.minted h2 { margin-top: 0; }
.minted code { user-select: all; word-break: break-all; }
.minted dl div { display: grid; grid-template-columns: 5rem 1fr; gap: 0.5rem; margin: 0.35rem 0; }

/* Under 48rem the account table stops being a table and becomes stacked
   cards: eleven numeric columns on a phone is unreadable otherwise. */
@media (max-width: 48rem) {
  .grid thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
  .grid tbody tr { display: block; padding: 0.6rem 0; }
  .grid td, .grid th { display: flex; justify-content: space-between; gap: 1rem; padding: 0.25rem 0; }
  .grid td::before, .grid th::before { content: attr(data-label); font-weight: 600; opacity: 0.7; }
  .grid .num { text-align: right; }
  .chart li { grid-template-columns: 5rem 1fr 2.5rem; }
}

/* The accounts hover card.
   Seven columns fit; twelve did not. What is left out — the credential label,
   the operator tags, and the 7d/30d/month-to-date counts that interpolate
   between the columns that ARE shown — lives here.

   Deliberately an element and not a title attribute: a title cannot hold rows,
   is unreachable by Ctrl+F, is announced unreliably, and NEVER appears on a
   touch device. This shows on hover and on keyboard focus, and below 48rem it
   is simply always visible, because a phone has no hover at all. No JavaScript;
   this console has none of its own. */
.has-card { position: relative; display: inline-block; }

.hovercard {
  display: none;
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  min-width: 15rem;
  padding: 0.6rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  font-size: 0.88rem;
  line-height: 1.5;
  /* The cell is nowrap-ish and narrow; the card must not inherit that. */
  white-space: normal;
}

.has-card:hover .hovercard,
.has-card:focus-within .hovercard { display: block; }

.hovercard-row { display: flex; justify-content: space-between; gap: 1rem; }
.hovercard-k { color: var(--text-dim); }

@media (max-width: 48rem) {
  /* No hover on touch: the card becomes part of the stacked card instead of a
     popup nobody can summon. */
  .has-card { display: block; }
  .hovercard {
    display: block;
    position: static;
    margin-top: 0.4rem;
    box-shadow: none;
    background: var(--surface-2);
    min-width: 0;
  }
}

/* A pointer user gets the card; a keyboard user gets it on focus. Neither
   should be able to lose it behind the table's own scroll container. */
.table-scroll:has(.has-card:hover),
.table-scroll:has(.has-card:focus-within) { overflow: visible; }
