/* The ops console, drawn from the design system's admin kit.
 *
 * Dense internal desktop: hairlines rather than shadows, documents rather than
 * pills, and every state paired with a word and an icon rather than signalled
 * by colour alone. The tokens live in ops_tokens.css, vendored from the design
 * system; nothing here invents a value.
 *
 * It is picked up by the layout's own stylesheet manifest, which serves every
 * sheet in this directory to every HTML surface — so nothing in here may be
 * written as a bare element rule. See the note below.
 */

/* Every rule here is `ops-` prefixed and every selector is a class, because
 * this sheet is served on every HTML surface in the application: a bare
 * element rule would restyle somebody else's page from a file they never
 * asked for. The console's own element defaults live inside these classes.
 */

.ops-eyebrow {
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  color: var(--text-faint);
}

.ops-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --- chrome ------------------------------------------------------------- */

/* The shell: a rail on the left, the screen on the right. The design system
 * draws one rail for Admin and the Builder portal — the mark, the destinations
 * with what is waiting behind each, and who is about to sign. */

.ops-shell {
  display: flex;
  min-height: 100vh;
  background: var(--surface-page);
}

.ops-rail {
  width: var(--sidenav-w);
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  padding: var(--sp-4) var(--sp-3);
  background: var(--surface-card);
  border-right: var(--border-w) solid var(--border-hairline);
  box-sizing: border-box;
}

.ops-rail .rail-mark {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2);
}

.ops-rail .rail-wordmark {
  font-family: var(--font-display);
  font-size: 18px;
  font-weight: var(--fw-semibold);
  letter-spacing: -0.015em;
  color: var(--text-strong);
}

.ops-rail .rail-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.ops-rail .rail-label {
  padding: 0 var(--sp-2);
  margin-bottom: var(--sp-1);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  color: var(--text-faint);
}

.ops-rail .rail-item {
  display: flex;
  align-items: center;
  gap: var(--sp-2h);
  height: 34px;
  padding: 0 var(--sp-2);
  border-radius: var(--r-field);
  font-size: var(--text-sm);
  color: var(--text-muted);
  text-decoration: none;
  transition: var(--transition-control);
}

.ops-rail .rail-item:hover { background: var(--surface-sunken); color: var(--text-strong); }

.ops-rail .rail-item[aria-current="page"] {
  background: var(--surface-sunken);
  color: var(--text-strong);
  font-weight: var(--fw-semibold);
}

.ops-rail .rail-item-label {
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Monospace so a queue that grows from 9 to 10 does not shift the label. */
.ops-rail .rail-count {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-faint);
}

.ops-rail .rail-foot {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-2);
  border-top: var(--border-w) solid var(--border-hairline);
}

.ops-rail .rail-who {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-record);
  color: var(--text-faint);
}

/* button_to puts the class on the button itself, not on the form it wraps it
   in, so this is `.rail-out` and not `.rail-out button`. */
.ops-rail .rail-out {
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-decoration: underline;
  cursor: pointer;
}

.ops-rail .rail-out:hover { color: var(--text-strong); }

/* The screen itself owns the rhythm between its parts. Left to themselves the
 * head, the tile deck, the caption and each card carry no margin at all, so
 * two consecutive cards sit edge to edge with nothing between them and the
 * deck touches the paragraph under it. A gap here is the one place that can
 * space a screen without every screen repeating it. */

.ops-main {
  flex: 1;
  min-width: 0;
  padding: var(--sp-6) var(--gutter-desktop) var(--sp-9);
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

/* The sign-in page, which has no rail because there is nowhere to go yet. */

.ops-gate {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--sp-5);
  background: var(--surface-page);
}

.gate-card {
  width: 100%;
  max-width: 380px;
  padding: var(--sp-6);
  background: var(--surface-card);
  border: var(--border-w) solid var(--border-hairline);
  border-radius: var(--r-card);
}

.gate-card .gate-mark { display: flex; align-items: center; gap: var(--sp-2h); }

.gate-card .gate-wordmark {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: var(--fw-semibold);
  letter-spacing: -0.015em;
  color: var(--text-strong);
}

.gate-card h1 {
  margin: var(--sp-5) 0 0;
  font-size: var(--text-h2);
  color: var(--text-strong);
}

.gate-card .gate-lede {
  margin: var(--sp-2) 0 var(--sp-5);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

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

.gate-card .field { display: flex; flex-direction: column; gap: var(--sp-1); }

.gate-card .field label {
  font-size: var(--text-xs);
  font-weight: var(--fw-semibold);
  color: var(--text-body);
}

.gate-card .field input {
  height: 38px;
  padding: 0 var(--sp-2h);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--text-strong);
  background: var(--surface-card);
  border: var(--border-w) solid var(--border-strong);
  border-radius: var(--r-field);
}

.gate-card .field input:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 1px;
}

.gate-card .field-inline {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.gate-card .gate-submit {
  height: 40px;
  margin-top: var(--sp-2);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: var(--fw-semibold);
  color: var(--action-primary-fg);
  background: var(--action-primary-bg);
  border: none;
  border-radius: var(--r-button);
  cursor: pointer;
}

.gate-card .gate-submit:hover { background: var(--action-primary-bg-hover); }

.gate-card .gate-aside {
  display: inline-block;
  margin-top: var(--sp-4);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.ops-head h1 { font-size: var(--text-h1); }
.ops-head p { margin: var(--sp-2) 0 0; color: var(--text-muted); font-size: var(--text-sm); max-width: var(--prose-max); }

/* --- flash: one key per refusal reason, one sentence per key ------------- */

/* --- what the console says back ------------------------------------------ */

.ops-toasts {
  position: fixed;
  right: var(--gutter-desktop);
  bottom: var(--sp-5);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  pointer-events: none;
}

.ops-toast {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-2h);
  width: 380px;
  max-width: calc(100vw - var(--sp-5) * 2);
  box-sizing: border-box;
  padding: var(--sp-2h) var(--sp-3);
  border: var(--border-w) solid transparent;
  border-radius: var(--r-md);
  box-shadow: var(--shadow-3);
  font-size: var(--text-sm);
  pointer-events: auto;
  animation: ops-toast-in var(--dur-base) var(--ease-out) both;
}

.ops-toast--success {
  background: var(--surface-inverse);
  color: var(--text-inverse);
}

.ops-toast--error {
  background: var(--status-error-bg);
  border-color: var(--status-error-border);
  color: var(--status-error-fg);
}

.ops-toast .toast-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.ops-toast .toast-reason {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-record);
  text-transform: uppercase;
}

.ops-toast--success .toast-reason { color: var(--text-inverse-muted); }
.ops-toast--error .toast-reason { color: var(--text-faint); }

.ops-toast .toast-dismiss {
  flex: 0 0 auto;
  display: inline-flex;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
}

.ops-toast .toast-dismiss:hover { opacity: 1; }

.ops-toast.is-leaving { animation: ops-toast-out var(--dur-fast) var(--ease-in) both; }

@keyframes ops-toast-in {
  from { opacity: 0; transform: translateY(var(--sp-2)); }
  to { opacity: 1; transform: none; }
}

@keyframes ops-toast-out {
  from { opacity: 1; }
  to { opacity: 0; transform: translateY(var(--sp-1)); }
}

/* Somebody who has asked for less motion still gets the toast, just without
   it sliding — and the leaving animation still fires, so it still removes. */
@media (prefers-reduced-motion: reduce) {
  .ops-toast { animation-name: ops-toast-fade; }
  .ops-toast.is-leaving { animation-name: ops-toast-fade-out; }
}

@keyframes ops-toast-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ops-toast-fade-out { from { opacity: 1; } to { opacity: 0; } }

/* --- card --------------------------------------------------------------- */

.ops-card {
  background: var(--surface-card);
  border: var(--border-w) solid var(--border-hairline);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-1);
}

.ops-card > header {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  padding: var(--sp-3) var(--sp-4);
  border-bottom: var(--border-w) solid var(--border-hairline);
}

.ops-card > header h2 { font-size: var(--text-h3); }
.ops-card > header .ops-note { margin-left: auto; font-size: var(--text-sm); color: var(--text-muted); }
.ops-card-body { padding: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3); }

/* A row is three things: what the record is, what it is worth, and what a
 * person may do about it. They are stacked by default and set side by side
 * once there is width to pay for it — mobile-first, as every other media query
 * in this repository is written.
 *
 * NO TRACK IS EVER LEFT UNCAPPED. `auto` as a track maximum means max-content,
 * and intrinsic tracks are sized before `fr` expansion, so one unbounded track
 * takes what it likes and the `fr` tracks collapse toward their base. Both
 * columns that would otherwise do this hold something a person can make
 * arbitrarily long: the action column holds a sentence, and the figures column
 * holds `listing.builder.name`, which is free text with no length validation. */

.ops-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
  padding: var(--sp-3) var(--sp-4);
  border-top: var(--border-w) solid var(--border-hairline);
}

.ops-row:first-child { border-top: 0; }

.ops-row-main { display: flex; flex-direction: column; gap: var(--sp-1h); min-width: 0; }

/* `align-items: start` so a pill hugs its own label. A flex column stretches
 * its children by default, which drew the status pills as bars the full width
 * of the column. */
.ops-row-figures { display: flex; flex-direction: column; gap: var(--sp-half); min-width: 0; align-items: start; }

/* `stretch` is what makes a control fill its column when the row is set in
 * columns. It is safe across the full-width band below, because no control is
 * ever a direct child here — every one of the three action columns wraps its
 * buttons in a `form_with` or a `details`, and a block wrapper is what a
 * disclosure needs in order to keep its width. */
.ops-row-actions { display: flex; flex-direction: column; gap: var(--sp-2); align-items: stretch; min-width: 0; }

/* `min-width: 0` on all three is defensive rather than currently load-bearing:
 * every track below declares a literal `0` minimum, which already suppresses
 * the content-based automatic minimum these would otherwise carry. It is kept
 * so that a track changed to `auto` later cannot silently reintroduce a column
 * that refuses to shrink.
 *
 * The breakpoints sit AFTER the declarations they override. They carry the
 * same specificity, so placed before them the plain rules would win and the
 * breakpoint would appear to do nothing at all. */

/* Two columns: the record beside what it is worth, with the actions on their
 * own full-width band beneath, where the consent sentence has room to be read
 * before it is agreed to.
 *
 * Both tracks are `fr`. A fixed maximum here would be paid for by the other
 * track, and the other track is the record — measured at 768px, a `20rem`
 * figures column left the Listing name, URL, status and brief sharing 83px.
 *
 * 52rem, not 48, because the rail costs a flat 236px that never comes back:
 * the threshold has to clear what the row needs PLUS the rail, and 48rem
 * cleared only the row. */
@media (min-width: 52rem) {
  .ops-row { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
  .ops-row-actions { grid-column: 1 / -1; }
}

/* Three columns, once there is width for the actions to sit beside the record
 * without starving it. The action track is capped because it holds a sentence;
 * see the note above the base rule. */
@media (min-width: 75rem) {
  .ops-row { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 20rem); }
  .ops-row-actions { grid-column: auto; }
}

.ops-row-title { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.ops-row-title strong { font-size: var(--text-md); color: var(--text-strong); }
.ops-row-detail { font-size: var(--text-sm); color: var(--text-muted); }
.ops-row.is-flagged { border-left: var(--rule-w) solid var(--status-risk-border); }

/* --- tiles -------------------------------------------------------------- */

/* Deliberately `auto-fit` and NOT a fixed column count. Decks are not all the
 * same size: Listings intake and Attribution cases carry four tiles, and the
 * Ledger carries five — four reads plus the chain-verification tile the screen
 * rests on. Any `repeat(<n>, …)` stands one deck up correctly and strands a
 * tile on the other.
 *
 * The floor is load-bearing rather than decorative, and it is sized from the
 * widest thing a tile has to hold. `ops_hero_money` sets the shillings at
 * `--text-num-hero` and `white-space: nowrap`, so a tile narrower than the
 * figure does not wrap it — the figure runs out of the tile, and `.ops-tile`
 * sets no `overflow`, so the neighbouring tile's opaque background paints over
 * the digits. It is the shillings that get cut, which is the half a reader in
 * Kampala is actually reading.
 *
 * 300px, because a seven-figure sum measures 266px at `--text-num-hero` and
 * the tile spends 32px of its width on padding. The previous 240px floor was
 * below that, so the overflow this prevents was reachable before as well —
 * measured at 1440px, a 276px tile against a 266px figure in 242px of
 * interior. `min()` keeps the floor from exceeding a narrow container and
 * forcing the whole document to scroll sideways. */

.ops-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: var(--sp-3);
}

.ops-tile {
  background: var(--surface-card);
  border: var(--border-w) solid var(--border-hairline);
  border-radius: var(--r-card);
  padding: var(--sp-3) var(--sp-4);
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
}

/* A dual-currency figure is two numbers and a separator, and the shilling half
   grows with the amount. Left to one line it clipped the dollars off the end —
   the half a reader without a feel for shillings is relying on. It wraps at the
   separator instead, and the tiles are wide enough that most never do. */
.ops-tile .ops-tile-value {
  font-size: var(--text-num-hero);
  font-family: var(--font-mono);
  color: var(--text-strong);
  line-height: var(--lh-tight);
  overflow-wrap: anywhere;
}
.ops-tile .ops-tile-secondary { font-size: var(--text-xs); color: var(--text-faint); }
.ops-tile.is-verified { border-color: var(--status-active-border); }
.ops-tile.is-broken { border-color: var(--status-error-border); }

/* --- money, records, timestamps ----------------------------------------- */

.ops-money { font-family: var(--font-mono); font-size: var(--text-num); color: var(--money-out); white-space: nowrap; }
.ops-money.is-held { color: var(--money-held); }
.ops-money.is-in { color: var(--money-in); }
/* A hero figure is two elements: the shillings at the headline size, the
   dollars small beside them. Set as one string at 34px they overflow the tile
   and the dollar half is cut off, so the two halves wrap against each other
   rather than against the edge of the card. */
.ops-money.is-hero {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--sp-1h);
  font-size: var(--text-num-hero);
  white-space: normal;
}
.ops-money.is-hero .ops-money-primary { white-space: nowrap; }
.ops-money.is-hero .ops-money-secondary {
  font-size: var(--text-sm);
  color: var(--money-secondary);
  white-space: nowrap;
}

.ops-ref, .ops-stamp-time {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-record);
  color: var(--text-faint);
}

/* --- chips and pills ---------------------------------------------------- */

.ops-chip, .ops-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border-radius: var(--r-sm);
  border: var(--border-w) solid var(--border-strong);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  color: var(--text-muted);
  white-space: nowrap;
}

.ops-pill.is-active { color: var(--status-active-fg); background: var(--status-active-bg); border-color: var(--status-active-border); }
.ops-pill.is-hold { color: var(--status-hold-fg); background: var(--status-hold-bg); border-color: var(--status-hold-border); }
.ops-pill.is-risk { color: var(--status-risk-fg); background: var(--status-risk-bg); border-color: var(--status-risk-border); }
.ops-pill.is-error { color: var(--status-error-fg); background: var(--status-error-bg); border-color: var(--status-error-border); }
.ops-pill.is-quiet { color: var(--status-expired-fg); background: var(--status-expired-bg); border-color: var(--status-expired-border); }

/* --- provenance: the label and the stamp, never interchangeable ---------- */

.ops-provenance {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-2) var(--sp-2h);
  border-radius: var(--r-md);
  font-size: var(--text-sm);
}

.ops-provenance .ops-provenance-label {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1h);
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
}

.ops-provenance.is-ai { background: var(--agent-bg); border: var(--border-w) solid var(--agent-border); color: var(--text-body); }
.ops-provenance.is-ai .ops-provenance-label { color: var(--agent-fg); }
.ops-provenance.is-system { background: var(--surface-sunken); border: var(--border-w) solid var(--border-hairline); color: var(--text-body); }
.ops-provenance.is-system .ops-provenance-label { color: var(--text-muted); }
.ops-provenance.is-absent { background: transparent; border: var(--border-w) dashed var(--border-strong); color: var(--text-faint); }
.ops-provenance ul { margin: 0; padding-left: var(--sp-4); }

.ops-stamp {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2h);
  border-radius: var(--r-sm);
  background: var(--human-bg);
  border: var(--border-w) solid var(--human-border);
  color: var(--human-fg);
  font-size: var(--text-xs);
}

.ops-stamp .ops-stamp-initials { font-family: var(--font-mono); font-weight: var(--fw-semibold); letter-spacing: var(--ls-record); }

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

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

.ops-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-1h);
  padding: var(--sp-1h) var(--sp-3);
  border-radius: var(--r-button);
  border: var(--border-w) solid transparent;
  background: var(--action-primary-bg);
  color: var(--action-primary-fg);
  font-size: var(--text-sm);
  font-weight: var(--fw-medium);
  font-family: inherit;
  cursor: pointer;
  transition: var(--transition-control);
}

.ops-button:hover { background: var(--action-primary-bg-hover); }
.ops-button.is-secondary { background: var(--action-secondary-bg); color: var(--action-secondary-fg); border-color: var(--action-secondary-border); }
.ops-button.is-secondary:hover { background: var(--action-secondary-bg-hover); }
.ops-button.is-seal { background: var(--action-seal-bg); color: var(--action-seal-fg); }
.ops-button.is-seal:hover { background: var(--action-seal-bg-hover); }
.ops-button.is-danger { background: var(--danger-600); color: #fff; }
.ops-button.is-danger:hover { background: var(--danger-700); }
.ops-button[disabled] { background: var(--action-disabled-bg); color: var(--action-disabled-fg); border-color: var(--border-hairline); cursor: not-allowed; }

/* Margin zeroed because half its uses are a `<p>`, whose default margins
 * fought whatever holds it — the screen's own rhythm on Listings intake, the
 * Ledger and Approvals. The other three are a `<span>` inside an action
 * column, where this is simply inert. */
.ops-why {
  font-size: var(--text-xs);
  color: var(--text-muted);
  max-width: 44ch;
  margin: 0;
}

.ops-field { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--text-sm); }
.ops-field > span { color: var(--text-muted); font-size: var(--text-xs); }

/* `.ops-field` carries two shapes. Label-above-input is the common one — text,
 * date and select — and the column direction above is right for it.
 *
 * A checkbox is the other, and stacking it above its own sentence reads as an
 * unrelated stray control. On Listings intake that control lapses every
 * standing Badge on the Listing, so being read as unrelated is the worst
 * available outcome.
 *
 * Selected on the control rather than on a modifier class so that the two
 * shapes cannot be got wrong: a field is laid out by what it actually
 * contains, and a checkbox added to any field in the console is drawn
 * correctly without anybody remembering to label it. */

.ops-field:has(input[type="checkbox"]) {
  flex-direction: row;
  align-items: flex-start;
  gap: var(--sp-2);
}

/* All four sides set, because the user-agent shorthand is `3px 3px 3px 4px`:
 * overriding the top alone leaves a right and left margin that widen the
 * declared gap and inset the box from everything above it. */
.ops-field:has(input[type="checkbox"]) input[type="checkbox"] {
  flex: 0 0 auto;
  margin: var(--sp-half) 0 0;
}

.ops-field input[type="text"],
.ops-field input[type="date"],
.ops-field textarea,
.ops-field select {
  padding: var(--sp-1h) var(--sp-2h);
  border: var(--border-w) solid var(--border-strong);
  border-radius: var(--r-field);
  background: var(--surface-card);
  color: var(--text-body);
  font: inherit;
  font-size: var(--text-sm);
  box-shadow: var(--shadow-inset-field);
}

.ops-filters { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--sp-3); align-items: end; }

/* --- ledger table ------------------------------------------------------- */

.ops-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.ops-table th {
  padding: var(--sp-1h) var(--sp-3);
  text-align: left;
  font-size: var(--text-2xs);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  font-weight: var(--fw-semibold);
  color: var(--text-faint);
  background: var(--surface-sunken);
  border-bottom: var(--border-w) solid var(--border-ledger);
}

.ops-table td { padding: var(--sp-2) var(--sp-3); border-bottom: var(--border-w) solid var(--border-ledger); vertical-align: top; }
.ops-table tbody tr:nth-child(even) { background: var(--surface-ledger-alt); }
.ops-table .is-numeric { text-align: right; font-family: var(--font-mono); white-space: nowrap; }
.ops-table caption { text-align: left; padding: var(--sp-3) var(--sp-4); color: var(--text-muted); font-size: var(--text-sm); }

/* --- notices and empty states ------------------------------------------ */

.ops-notice {
  display: flex;
  flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  border: var(--border-w) solid var(--border-hairline);
  background: var(--surface-sunken);
  font-size: var(--text-sm);
  color: var(--text-body);
}

.ops-notice strong { color: var(--text-strong); }
.ops-notice.is-warning { border-color: var(--status-hold-border); background: var(--status-hold-bg); color: var(--status-hold-fg); }

.ops-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-8) var(--sp-4);
  color: var(--text-muted);
  text-align: center;
}

.ops-icon { flex: 0 0 auto; display: block; }

.ops-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2h); }
.ops-list > li { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; padding-bottom: var(--sp-2); border-bottom: var(--border-w) solid var(--border-hairline); }
.ops-list > li:last-child { border-bottom: 0; padding-bottom: 0; }

.ops-claims { display: flex; flex-direction: column; gap: var(--sp-2); }
.ops-claim { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; font-size: var(--text-sm); }
