/*
 * Application layout.
 *
 * `tokens.css` owns colour, type, radius, spacing, and the `.money` and
 * `.sr-only` utilities; this file owns arrangement. There are no literal hex
 * values below — if you need a colour that is not a token, the fix is a token,
 * not a literal, or the light/dark pairing drifts.
 *
 * The non-negotiables from the design brief that this file has to honour:
 * money is right-aligned and never truncated (`.money`, from tokens), pending
 * is visually *and* textually distinct, every auto-assigned category shows a
 * correction affordance, tap targets are 44px, and nothing here uses
 * `--c-danger` — spending is not an error state.
 */

.app {
  max-width: 42rem;
  margin: 0 auto;
  /* `env(safe-area-inset-*)` keeps content clear of the notch and the home
   * indicator on iOS, where the app runs full-bleed. Zero everywhere else. */
  padding: calc(var(--space-4) + env(safe-area-inset-top)) var(--space-4)
    calc(var(--space-6) + env(safe-area-inset-bottom));
}

/* ---- Pull to refresh ------------------------------------------------------
 *
 * The webview's own overscroll is suppressed in `tokens.css`, so this is what
 * the user sees instead while their finger is down. It is `fixed` and starts
 * above the top of the screen: the whole element is offscreen at rest and rides
 * down on the transform the gesture writes, which means no layout ever moves
 * and there is nothing to reflow at sixty frames a second.
 *
 * It exists only for the duration of the drag. Once the finger lifts, the sync
 * itself is reported by the status chip a few millimetres below — see
 * `PullIndicator`. */

.pull {
  position: fixed;
  top: calc(env(safe-area-inset-top) - 2.5rem);
  left: 0;
  right: 0;
  z-index: 5;
  display: flex;
  justify-content: center;
  /* The finger owns this element's position, so it must not also be animating
   * towards somewhere else. */
  transition: none;
  pointer-events: none;
}

.pull__label {
  padding: var(--space-2) var(--space-4);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-sm);
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

/* Armed. The wording changes too — the colour is the quick read and the
 * sentence is the one that survives not being able to see the difference. */
.pull--armed .pull__label {
  background: var(--c-accent-soft);
  border-color: var(--c-accent);
  color: var(--c-text);
}

/* ---- Header and status --------------------------------------------------- */

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}

.chip {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-full);
  box-shadow: var(--shadow-sm);
}

.chip__label {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

/* The bunny lives here and nowhere near a number. Fixed size so a pose change
 * cannot reflow the row it sits in. */
.bunny-host {
  width: 2rem;
  height: 2rem;
  flex: none;
  line-height: 0;
}

.bunny-host svg {
  width: 100%;
  height: 100%;
}

.empty .bunny-host,
.signin .bunny-host {
  width: 6rem;
  height: 6rem;
  margin-inline: auto;
}

/* ---- Buttons ------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-5);
  min-height: var(--touch-target);
  border: none;
  background: var(--c-accent);
  color: var(--c-accent-text);
  font-size: var(--text-base);
  font-weight: 600;
  transition: transform var(--dur-fast) var(--ease-spring);
}

.button:active {
  transform: scale(0.97);
}

.button[disabled] {
  opacity: 0.6;
  cursor: default;
}

.button--quiet {
  background: transparent;
  color: var(--c-text-muted);
  font-weight: 500;
  padding-inline: var(--space-3);
}

/* ---- Sign in ------------------------------------------------------------- */

.signin {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: center;
  padding-top: var(--space-6);
}

.signin__title {
  margin: 0;
  font-size: var(--text-2xl);
}

.signin__subtitle {
  margin: 0 0 var(--space-4);
  color: var(--c-text-muted);
}

.signin__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  text-align: start;
}

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

.field__label {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

.field__input {
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-md);
  color: var(--c-text);
  /* 16px minimum, or iOS Safari zooms the viewport on focus and never zooms
   * back out. This is the whole reason the base size is not smaller. */
  font-size: var(--text-base);
  font-family: inherit;
}

/* The one-time code is a number the user is copying digit by digit, so it gets
 * the tabular face and room to breathe. */
.field__input--code {
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.4em;
  text-align: center;
}

/* ---- Notices ------------------------------------------------------------- */

.notice {
  margin: var(--space-4) 0 0;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

/* Rose, not `--c-danger`. A failed sign-in is a typo, not a catastrophe, and
 * the alarm colour is reserved for destructive confirmations. */
.notice--error {
  background: var(--c-rose-soft);
  color: var(--c-text);
}

.banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--c-butter-soft);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
}

/* ---- Empty state --------------------------------------------------------- */

.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-6) var(--space-4);
  text-align: center;
}

.empty__title {
  margin: 0;
  font-size: var(--text-lg);
}

.empty__body {
  margin: 0 0 var(--space-3);
  max-width: 28rem;
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

/* ---- Ledger -------------------------------------------------------------- */

.ledger__day {
  position: sticky;
  top: 0;
  margin: var(--space-5) 0 var(--space-2);
  padding: var(--space-2) 0;
  background: var(--c-bg);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
  font-weight: 600;
}

.ledger__list {
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}

.tx {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-target);
  padding: var(--space-3) var(--space-4);
}

.tx + .tx {
  border-top: 1px solid var(--c-border);
}

.tx__main {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  /* `min-width: 0` lets the merchant name shrink so the amount never has to.
   * Truncating a name is a cosmetic loss; truncating an amount is a lie. */
  min-width: 0;
  flex: 1;
}

.tx__merchant {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tx__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ---- Tags ----------------------------------------------------------------
 * Pending and "needs a category" are stated in words, not merely in colour: a
 * colour-only distinction is invisible to a good share of users, and a pending
 * charge read as settled is the most common way a spend tracker lies. */

.tag {
  padding: 2px var(--space-2);
  border-radius: var(--radius-full);
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.tag--pending {
  background: var(--c-accent-soft);
  color: var(--c-text);
}

/* The review queue's affordance. Mint rather than a warning colour — an
 * unlabelled transaction is an invitation, not a problem the user caused. */
.tag--review {
  background: var(--c-mint-soft);
  color: var(--c-text);
}

/* An automatically assigned category. The dotted underline is the "tap to
 * correct" affordance every non-user-set category is required to carry. */
.tag--auto {
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

/* ---- Navigation ----------------------------------------------------------
 * Three tabs, because there are three things a person does here. Sticky to the
 * top so the review count stays visible while scrolling a long ledger — that
 * number is what makes somebody go and clear it. */

.nav {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
  padding: var(--space-1);
  background: var(--c-surface-sunken);
  border-radius: var(--radius-full);
}

.nav__tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  flex: 1;
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  border: none;
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--c-text-muted);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  transition: background var(--dur-fast) var(--ease-out);
}

/* The selected tab is a filled pill *and* carries `aria-current`, so the
 * distinction survives both a colour-blind reader and a screen reader. */
.nav__tab--on {
  background: var(--c-surface);
  color: var(--c-text);
  box-shadow: var(--shadow-sm);
}

/* Mint, not red. A queue of things to label is an invitation, not a problem the
 * user caused — the same call as `.tag--review`. */
.nav__badge {
  min-width: 1.25rem;
  padding: 0 var(--space-1);
  border-radius: var(--radius-full);
  background: var(--c-mint-soft);
  color: var(--c-text);
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-xs);
}

/* ---- Category picker -----------------------------------------------------
 * A native `<select>`, styled to read as a tag rather than as a form control.
 * Native because it opens the platform's own picker on iOS and Android, which
 * is scrollable, thumb-sized, and already familiar — and because it is the only
 * version that is keyboard- and screen-reader-navigable for free. */

.picker {
  max-width: 100%;
  /* Below the 44px floor on purpose: this sits inline in a dense list and the
   * whole row is comfortably past the minimum. The picker's own hit area is
   * padded out to meet it in `.tx--review`, where it is the primary action. */
  padding: 2px var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
  font-family: inherit;
  font-size: var(--text-xs);
  /* The default arrow doubles the control's width for no information; the
   * dotted underline below is the affordance that matters. */
  appearance: none;
}

.picker:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

/* Automatic, and therefore correctable. Same dotted underline as `.tag--auto`,
 * for the same reason: every non-user-set category must invite a correction. */
.picker--auto {
  text-decoration: underline dotted;
  text-underline-offset: 2px;
}

/* Nothing chosen yet. */
.picker--review {
  background: var(--c-mint-soft);
  color: var(--c-text);
  font-weight: 600;
}

/* Where a category came from, in words. The visible half of the confidence
 * affordance — the dotted underline alone says nothing to a screen reader. */
.tx__why {
  color: var(--c-text-faint);
  font-size: var(--text-xs);
}

/* ---- Review queue -------------------------------------------------------- */

.review__header {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.review__intro {
  margin: 0;
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

/* The queue's rows stack rather than sitting on one line: the picker is the
 * primary action here, not an afterthought beside the amount. */
.tx--review .tx__main {
  gap: var(--space-2);
}

.tx--review .picker {
  align-self: flex-start;
  min-height: var(--touch-target);
  padding-inline: var(--space-3);
  border-color: var(--c-border-strong);
  background: var(--c-surface);
  color: var(--c-text);
  font-size: var(--text-base);
}

.toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-target);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

.toggle input {
  width: 1.25rem;
  height: 1.25rem;
  accent-color: var(--c-accent);
}

.toggle--compact {
  min-height: auto;
  flex: none;
}

/* ---- Rules --------------------------------------------------------------- */

.rule-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
  padding: var(--space-4);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
}

/* The live match count. Shown before saving, including when it is zero — that
 * is the most useful case, because it says the rule does nothing while there is
 * still time to fix it. */
.rule-form__preview {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  background: var(--c-accent-soft);
  border-radius: var(--radius-md);
  color: var(--c-text);
  font-size: var(--text-sm);
}

/* ---- Receipts -------------------------------------------------------------
 *
 * The tone rule again, applied to a screen that is easy to get wrong. A receipt
 * the matcher could not place is not a failure and must not be dressed as one:
 * it is the app declining to guess between two charges it genuinely cannot tell
 * apart, which is the behaviour the user would want if they were asked. So the
 * asking state is mint — the same colour the review queue uses for an invitation
 * — and nothing on this screen is red. */

.receipts {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4) 0;
}

.receipts__capture {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* Full width and thumb-sized: this is the primary action of the screen and, on
 * a phone, the button somebody presses while holding a piece of paper in their
 * other hand. */
.receipts__capture .button--primary {
  width: 100%;
  border-radius: var(--radius-lg);
}

.button--primary {
  background: var(--c-accent);
  color: var(--c-accent-text);
  border-radius: var(--radius-md);
}

.receipts__heading {
  font-size: var(--text-lg);
  font-weight: 700;
  margin: var(--space-2) 0 0;
}

.receipts__note {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
  margin: 0;
  /* Sentences wrap; a note that reads as a warning label is a note nobody
   * reads. */
  max-width: 46ch;
}

.receipts__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.receipt {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-md);
}

.receipt--asking {
  border-color: var(--c-mint);
  background: var(--c-mint-soft);
}

.receipt__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.receipt__merchant {
  font-weight: 600;
}

.receipt__meta {
  font-size: var(--text-sm);
  color: var(--c-text-muted);
}

.receipt__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
}

.receipt__candidates {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* The whole row is the target, not a small button inside it. This is a choice
 * between things, made with a thumb, and a 44px pill beside a description is a
 * smaller target than the description the user is already reading. */
.candidate__pick {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  min-height: var(--touch-target);
  padding: var(--space-2) var(--space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border-strong);
  border-radius: var(--radius-md);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-spring);
}

.candidate__pick:active {
  transform: scale(0.99);
}

.candidate__pick:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}

.candidate__main,
.candidate__figures {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.candidate__figures {
  align-items: flex-end;
  /* Never let the amount shrink or wrap. Money is right-aligned and never
   * truncated, everywhere in this app. */
  flex: 0 0 auto;
}

.candidate__merchant {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.candidate__why,
.candidate__date {
  font-size: var(--text-xs);
  color: var(--c-text-muted);
}

/* The paperclip on a ledger row. Quiet on purpose — it is a fact about the row,
 * not a call to action, and the ledger is for reading. */
.tag--receipt {
  background: var(--c-surface-sunken);
  color: var(--c-text-muted);
}

/* ---- The confirmation sheet ----------------------------------------------
 *
 * Modal, and deliberately so. This is the step that everything the parser is
 * allowed to guess at is borrowed against: it repairs OCR digit confusions and
 * assumes month-first on an ambiguous date, and both are only acceptable
 * because a person looks at the result. Dismissing it by accident would save an
 * unreviewed reading, so there is no click-outside-to-close. */

.sheet {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgb(46 42 38 / 40%);
  padding: var(--space-4);
  z-index: 10;
}

.sheet__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  width: 100%;
  max-width: 32rem;
  /* Never taller than the viewport, and scrollable inside itself: a keyboard
   * appearing on a phone halves the available height, and a sheet whose
   * buttons have gone under it is a sheet the user cannot finish. */
  max-height: 90vh;
  overflow-y: auto;
  padding: var(--space-5);
  background: var(--c-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.sheet__title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: 700;
}

.sheet__actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  margin-top: var(--space-2);
}

.field__hint {
  font-size: var(--text-xs);
  color: var(--c-text-faint);
}

/* Slides up rather than fading in, so the direction matches the gesture that
 * would dismiss it. Honoured only where motion is welcome. */
@media (prefers-reduced-motion: no-preference) {
  .sheet__body {
    animation: sheet-in var(--dur-base) var(--ease-out);
  }

  @keyframes sheet-in {
    from {
      transform: translateY(12px);
      opacity: 0;
    }
  }
}

@media (min-width: 40rem) {
  .sheet {
    align-items: center;
  }
}

/* ---- Budgets -------------------------------------------------------------
 * The colour ramp is `--c-budget-ok` → `--c-budget-near` → `--c-budget-over`,
 * which is mint → butter → peach. Peach, not `--c-danger`: going over a budget
 * is a fact about a month, not an error the user committed, and the alarm
 * colour stays reserved for destructive confirmations. See the tone rule in
 * `bunny.css` — this file is where it either holds or does not.
 *
 * Nothing here is colour-only. Every band has a text equivalent in
 * `format::budget_line`, and the bar itself is `role="progressbar"` carrying
 * the same numbers. */

.budgets {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.budgets__allclear {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--c-mint-soft);
  border-radius: var(--radius-lg);
}

.budgets__allclear-text {
  font-size: var(--text-sm);
}

.budgets__list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.budget {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
}

.budget__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}

.budget__name {
  font-weight: 600;
  /* The name gives way, never the figures. Truncating a budget's label is a
   * cosmetic loss; truncating what is left in it is a lie. */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.budget__bar {
  position: relative;
  height: 10px;
  border-radius: var(--radius-full);
  background: var(--c-surface-sunken);
  overflow: hidden;
}

.budget__fill {
  display: block;
  height: 100%;
  border-radius: var(--radius-full);
  background: var(--c-budget-ok);
}

.budget--watch .budget__fill {
  background: var(--c-budget-near);
}

.budget--over .budget__fill {
  background: var(--c-budget-over);
}

/* Where the bar would be if spending were perfectly even. One tick, and it is
 * what turns "60% used" into "60% used and it is the 3rd" — the comparison the
 * fill alone cannot make, because a full-looking bar on the 29th and one on the
 * 3rd are opposite situations. */
.budget__pace-mark {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--c-border-strong);
}

/* A hatched cap on a bar that is already full, so "at the limit" and "well past
 * it" are not the same picture. The text says the figure either way. */
.budget__overflow {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 18%;
  background: repeating-linear-gradient(
    45deg,
    var(--c-budget-over) 0 4px,
    var(--c-peach-soft) 4px 8px
  );
}

.budget__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.budget__line {
  font-variant-numeric: tabular-nums;
}

.budget__pace {
  color: var(--c-text-muted);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.budget__actions {
  display: flex;
  justify-content: flex-end;
}

/* Butter rather than mint: a budget wanting attention is not the same kind of
 * thing as a queue of transactions to label, and the two badges sit next to each
 * other in the tab bar. Still not peach — a badge is a signpost, and the tab it
 * points at is where the situation gets explained. */
.nav__badge--budget {
  background: var(--c-butter-soft);
}

/* The good-news banner. The only place in the app where a banner is a reward,
 * and mint marks it as such against the butter of an ordinary alert. */
.banner--good {
  background: var(--c-mint-soft);
}

.banner__text {
  min-width: 0;
  flex: 1;
}

.budget-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  background: var(--c-surface-sunken);
  border-radius: var(--radius-lg);
}

.budget-form__title {
  font-size: var(--text-base);
  font-weight: 600;
}

/* The fill animates because a bar that jumps has no direction, and direction is
 * the information — a budget going up and one being corrected downward look
 * identical at rest. Dropped entirely under reduced motion: the figures beside
 * it were never carried by the animation. */
@media (prefers-reduced-motion: no-preference) {
  .budget__fill {
    transition: width var(--dur-slow) var(--ease-out),
      background-color var(--dur-base) var(--ease-out);
  }
}

/* ---- Without an account ---------------------------------------------------
 *
 * Offered below the sign-in form, not beside it. An account is still the better
 * product — it fills itself in and it survives the phone — and presenting the
 * two as equals would be selling somebody the harder one by accident. But it is
 * a real offer rather than a teaser, so it gets the same surface and the same
 * touch targets as everything else, not a footnote's typography. */

.offline-offer {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--c-border);
  text-align: start;
}

.offline-offer__title {
  margin: 0;
  font-size: var(--text-lg);
}

.offline-offer__body {
  margin: 0;
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

/* The trade, stated before the choice rather than discovered after it. Muted
 * rather than red: these are the terms of a reasonable option, not warnings
 * about a dangerous one, and colouring them as alarm would make the honest list
 * read as a reason not to. */
.offline-offer__costs {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-5);
  background: var(--c-surface-sunken);
  border-radius: var(--radius-md);
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

/* ---- Adding an entry by hand ----------------------------------------------
 *
 * The one screen local mode adds, and the one real cost of having no bank. It
 * is styled to be reopened dozens of times rather than admired once: the form
 * stays open after a save, and the trigger is a full-width target because it is
 * the most-tapped control on the ledger for anyone using this mode. */

.add-entry__open {
  width: 100%;
  margin-bottom: var(--space-4);
}

.add-entry {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  margin-bottom: var(--space-5);
  padding: var(--space-4);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-lg);
}

.add-entry__direction {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  border: 0;
}

.add-entry__direction .field__label {
  flex-basis: 100%;
  padding: 0;
}

.choice {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-target);
  font-size: var(--text-base);
}

.choice input {
  width: 1.25rem;
  height: 1.25rem;
  accent-color: var(--c-accent);
}

/* Money the user is typing gets the tabular face for the same reason money the
 * app prints does: a column of figures that shifts as digits are added is one
 * nobody can check at a glance. */
.field__input--amount {
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
}

/* ---- What this device has learned -----------------------------------------
 *
 * Only in local mode, where merchant memory has nowhere else to live. It is
 * listed rather than hidden because a memory nobody can inspect is a memory
 * nobody can correct, and this one is answering questions on the user's behalf
 * every time they add an entry. */

.memory {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

.memory__intro {
  margin: 0;
  color: var(--c-text-muted);
  font-size: var(--text-sm);
}

.memory__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.memory__row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--touch-target);
  padding: 0 var(--space-3);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--radius-md);
}

.memory__merchant {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.memory__count {
  color: var(--c-text-muted);
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
  font-size: var(--text-sm);
}
