/* ─────────────────────────────────────────────────────────────
   APP — the new visual language applied to the app's existing
   Bootstrap 5.3 markup.

   WHY THIS FILE EXISTS. ~100 pages ship Bootstrap class names. The
   cheapest honest route to DESIGN.md across all of them is to restyle
   those class names once, here, rather than rewrite a hundred files.
   No markup, class, id or data attribute changes anywhere.

   HOW IT WINS WITHOUT !important. Bootstrap 5.3 drives nearly every
   component from its own custom properties (--bs-btn-bg, --bs-table-*,
   --bs-card-*, --bs-modal-*). Overriding those properties is both
   shorter and more robust than re-declaring the properties they feed,
   so that is the first tool used here. Where a real declaration is
   needed, app.css is linked AFTER bootstrap.min.css and matches or
   exceeds Bootstrap's specificity, so the cascade decides it.

   WHAT IS OUT OF REACH. Bootstrap's *utilities* (.text-danger,
   .bg-success, .shadow-sm, .fs-4 …) ship `!important`, and several of
   them resolve through rgb triplets (`rgba(var(--bs-danger-rgb), …)`)
   that cannot be fed from a colour token. Those keep Bootstrap's
   palette. The one exception made here is `.badge.bg-*`, remapped via
   `background-image` — see the status chip section. `.text-muted` is
   reachable because it reads `var(--bs-secondary-color)` directly.

   NOT SCOPED to [data-rl], unlike base.css and ledger.css: this file
   IS the app-wide adoption, linked from includes/header.php. base.css
   stays scoped and still owns the opted-in [data-rl] subtrees.

   Tokens only. Layer 2 (tokens.semantic.css) exclusively — never a
   layer-1 primitive. Gated by tools/lint-tokens.php.
   ───────────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────────
   1. BOOTSTRAP BRIDGE
   The global Bootstrap variables, pointed at tokens. Everything
   downstream of these is restyled without naming a single component.
   ───────────────────────────────────────────────────────────── */
:root {
  --bs-body-bg: var(--color-bg-page);
  --bs-body-color: var(--color-fg-default);
  --bs-body-font-family: var(--font-body);
  --bs-body-font-size: var(--text-body);
  --bs-body-line-height: var(--leading-body);
  --bs-font-monospace: var(--font-ledger);

  --bs-emphasis-color: var(--color-fg-default);
  --bs-secondary-color: var(--color-fg-muted);   /* drives .text-muted */
  --bs-tertiary-color: var(--color-fg-muted);
  --bs-secondary-bg: var(--color-bg-sunken);
  --bs-tertiary-bg: var(--color-bg-sunken);      /* drives dropdown item hover */

  /* Decorative by default. Inputs are given --color-border-default
     explicitly further down; never the subtle one (DESIGN.md §2). */
  --bs-border-color: var(--color-border-subtle);
  --bs-border-color-translucent: var(--color-border-subtle);

  /* Radius ladder (§6): sm for controls, md for cards/menus/dialogs. */
  --bs-border-radius: var(--radius-sm);
  --bs-border-radius-sm: var(--radius-sm);
  --bs-border-radius-lg: var(--radius-md);
  --bs-border-radius-xl: var(--radius-md);
  --bs-border-radius-2xl: var(--radius-md);
  --bs-border-radius-pill: var(--radius-full);

  --bs-nav-link-color: var(--color-action-default);
  --bs-nav-link-hover-color: var(--color-action-hover);

  /* ── Bootstrap's !important colour utilities ──
     .text-success, .bg-danger and ~1,000 siblings in this app resolve as
     `rgba(var(--bs-success-rgb), …) !important`. They cannot be beaten by
     specificity and cannot be handed a colour token — only a triplet, only
     through these variables. Repointing them here converts every one of
     them at once, with no !important of our own and no markup change.

     The mapping folds Bootstrap's SEVEN semantic colours onto DESIGN.md's
     five states (§7: no sixth status colour):
       primary   -> the accent (actions, links, focus — §7)
       success   -> paid          warning -> due soon
       danger    -> overdue
       info      -> muted ink. Bootstrap treats info as a sixth colour;
                    this app does not have one, so it reads as neutral.
       secondary -> muted ink     light   -> the sunken well
       dark      -> default ink

     Usage counts that made this worth doing: text-success 176,
     text-danger 153, text-primary 125, bg-success 103, bg-primary 99. */
  --bs-primary-rgb:   var(--color-action-default-rgb);
  --bs-success-rgb:   var(--color-status-paid-fg-rgb);
  --bs-warning-rgb:   var(--color-status-due-fg-rgb);
  --bs-danger-rgb:    var(--color-status-overdue-fg-rgb);
  --bs-info-rgb:      var(--color-fg-muted-rgb);
  --bs-secondary-rgb: var(--color-fg-muted-rgb);
  --bs-light-rgb:     var(--color-bg-sunken-rgb);
  --bs-dark-rgb:      var(--color-fg-default-rgb);
  --bs-body-color-rgb: var(--color-fg-default-rgb);
  --bs-body-bg-rgb:   var(--color-bg-sunken-rgb);
}

/* ─────────────────────────────────────────────────────────────
   2. BASE ELEMENTS AND THE TYPE SCALE
   ───────────────────────────────────────────────────────────── */
body {
  background-color: var(--color-bg-page);
  color: var(--color-fg-default);
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
}

/* Bootstrap's heading utilities are remapped alongside the elements,
   because the chrome writes <h1 class="h3"> and the utility would
   otherwise beat the element rule. There is no display size: this
   product has no hero (§3). */
h1, .h1 {
  font-size: var(--text-h1);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-h1);
  font-weight: var(--weight-medium);
}
h2, .h2 {
  font-size: var(--text-h2);
  line-height: var(--leading-h2);
  font-weight: var(--weight-medium);
}
h3, h4, h5, h6,
.h3, .h4, .h5, .h6 {
  font-size: var(--text-h3);
  line-height: var(--leading-h3);
  font-weight: var(--weight-medium);
}

/* The page title is one per route and always carries .h3 from the
   chrome. Named here so it reads at h1 size without touching markup. */
.page-header h1 {
  font-size: var(--text-h1);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-h1);
}

small, .small { font-size: var(--text-small); }
code, kbd, samp, pre { font-family: var(--font-ledger); }

/* The accent, on links — one of only three permitted uses (§7). */
a { color: var(--color-action-default); }
a:hover { color: var(--color-action-hover); }

hr {
  border-top: 1px solid var(--color-border-subtle);
  opacity: 1;
}

/* Focus is never removed, only restyled, and must read against both
   the element and the page (§4). */
:focus-visible {
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
}

/* ─────────────────────────────────────────────────────────────
   3. CHROME — manager console (§5)
   Persistent left navigation, content filling the remainder to a
   1600px maximum.
   ───────────────────────────────────────────────────────────── */
.sidebar {
  background-color: var(--color-bg-sunken);
  min-height: 100vh;
  border-right: 1px solid var(--color-border-subtle);
}

.sidebar .nav-link {
  color: var(--color-fg-default);
  padding: var(--space-8) var(--space-12);
  margin-bottom: var(--space-2);
  border-radius: var(--radius-sm);
  transition: background-color var(--duration-press) var(--ease-out),
              color var(--duration-press) var(--ease-out);
}

.sidebar .nav-link:hover {
  background-color: var(--color-bg-surface);
  color: var(--color-action-default);
}

/* Selection is a background shift, never a border (§4). */
.sidebar .nav-link.active {
  background-color: var(--color-action-subtle);
  color: var(--color-action-default);
  font-weight: var(--weight-medium);
}

.sidebar .nav-link i {
  width: var(--space-16);
  text-align: center;
}

.sidebar h6 {
  font-size: var(--text-small);
  font-weight: var(--weight-medium);
  color: var(--color-fg-muted);
  letter-spacing: 0.03em;
}

/* 1600px, expressed in rem so it survives the no-raw-px rule and
   the user's own text-size preference (§5). */
.main-content-wrapper { max-width: 100rem; }

/* header.php previously gave this a shadow AND a radius. A page
   container is not an elevated object; separation comes from the
   border, and never a border and a shadow on the same edge (§6). */
.main-content {
  background-color: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-24);
}

.page-header {
  background-color: transparent;
  border-bottom: 1px solid var(--color-border-subtle);
  padding: var(--space-16) 0;
  margin-bottom: var(--space-24);
}

.breadcrumb {
  background-color: transparent;
  padding: 0;
  margin: 0;
  font-size: var(--text-small);
}

.breadcrumb-item + .breadcrumb-item::before {
  content: "\203A";
  color: var(--color-fg-muted);
}

/* ─────────────────────────────────────────────────────────────
   4. BUTTONS (§4)
   primary = accent fill · secondary = surface + meaningful border ·
   ghost = text only. Bootstrap's own variables carry all of it.
   ───────────────────────────────────────────────────────────── */
.btn {
  --bs-btn-border-radius: var(--radius-sm);
  --bs-btn-font-weight: var(--weight-medium);
  /* Bootstrap's blue glow is replaced by the outline ring below. */
  --bs-btn-focus-box-shadow: none;
  --bs-btn-active-shadow: none;
  --bs-btn-box-shadow: none;
  transition: color var(--duration-press) var(--ease-out),
              background-color var(--duration-press) var(--ease-out),
              border-color var(--duration-press) var(--ease-out),
              transform var(--duration-press) var(--ease-out);
}

.btn:focus-visible {
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
  box-shadow: none;
}

.btn:active { transform: scale(0.97); }

/* Disabled is a muted fill with muted text, not a faded copy of the
   enabled state — an opacity fade takes the label below contrast. */
.btn:disabled,
.btn.disabled,
fieldset:disabled .btn {
  --bs-btn-disabled-opacity: 1;
  --bs-btn-disabled-bg: var(--color-action-disabled);
  --bs-btn-disabled-color: var(--color-fg-muted);
  --bs-btn-disabled-border-color: var(--color-border-subtle);
  transform: none;
}

.btn-primary {
  --bs-btn-bg: var(--color-action-default);
  --bs-btn-border-color: var(--color-action-default);
  --bs-btn-color: var(--color-fg-on-action);
  --bs-btn-hover-bg: var(--color-action-hover);
  --bs-btn-hover-border-color: var(--color-action-hover);
  --bs-btn-hover-color: var(--color-fg-on-action);
  --bs-btn-active-bg: var(--color-action-hover);
  --bs-btn-active-border-color: var(--color-action-hover);
  --bs-btn-active-color: var(--color-fg-on-action);
}

/* Secondary: a surface with a meaningful boundary, not a grey fill.
   .btn-secondary and .btn-outline-secondary land in the same place —
   the product has one secondary button, not two. */
.btn-secondary,
.btn-outline-secondary,
.btn-light,
.btn-outline-primary {
  --bs-btn-bg: var(--color-bg-surface);
  --bs-btn-border-color: var(--color-border-default);
  --bs-btn-color: var(--color-fg-default);
  --bs-btn-hover-bg: var(--color-bg-sunken);
  --bs-btn-hover-border-color: var(--color-border-default);
  --bs-btn-hover-color: var(--color-fg-default);
  --bs-btn-active-bg: var(--color-bg-sunken);
  --bs-btn-active-border-color: var(--color-border-default);
  --bs-btn-active-color: var(--color-fg-default);
}

/* …with one difference: an outline-primary is still an accented
   action, so its label carries the accent. */
.btn-outline-primary {
  --bs-btn-color: var(--color-action-default);
  --bs-btn-hover-color: var(--color-action-hover);
  --bs-btn-active-color: var(--color-action-hover);
}

/* Ghost — text only. */
.btn-link {
  --bs-btn-color: var(--color-action-default);
  --bs-btn-hover-color: var(--color-action-hover);
  --bs-btn-active-color: var(--color-action-hover);
  --bs-btn-bg: transparent;
  --bs-btn-border-color: transparent;
}

/* Destructive and the two feedback tints. These reuse the status
   values rather than introducing button colours of their own — there
   is no sixth colour anywhere in this product (§7). */
.btn-danger {
  --bs-btn-bg: var(--color-status-overdue-fg);
  --bs-btn-border-color: var(--color-status-overdue-fg);
  --bs-btn-color: var(--color-fg-inverse);
  --bs-btn-hover-bg: var(--color-status-overdue-fg);
  --bs-btn-hover-border-color: var(--color-status-overdue-fg);
  --bs-btn-hover-color: var(--color-fg-inverse);
  --bs-btn-active-bg: var(--color-status-overdue-fg);
  --bs-btn-active-border-color: var(--color-status-overdue-fg);
  --bs-btn-active-color: var(--color-fg-inverse);
}

.btn-success {
  --bs-btn-bg: var(--color-status-paid-fg);
  --bs-btn-border-color: var(--color-status-paid-fg);
  --bs-btn-color: var(--color-fg-inverse);
  --bs-btn-hover-bg: var(--color-status-paid-fg);
  --bs-btn-hover-border-color: var(--color-status-paid-fg);
  --bs-btn-hover-color: var(--color-fg-inverse);
  --bs-btn-active-bg: var(--color-status-paid-fg);
  --bs-btn-active-border-color: var(--color-status-paid-fg);
  --bs-btn-active-color: var(--color-fg-inverse);
}

.btn-warning {
  --bs-btn-bg: var(--color-status-due-fg);
  --bs-btn-border-color: var(--color-status-due-fg);
  --bs-btn-color: var(--color-fg-inverse);
  --bs-btn-hover-bg: var(--color-status-due-fg);
  --bs-btn-hover-border-color: var(--color-status-due-fg);
  --bs-btn-hover-color: var(--color-fg-inverse);
  --bs-btn-active-bg: var(--color-status-due-fg);
  --bs-btn-active-border-color: var(--color-status-due-fg);
  --bs-btn-active-color: var(--color-fg-inverse);
}

/* Outlined destructive / feedback: the status colour on the label and
   the boundary, a background shift on hover. */
.btn-outline-danger {
  --bs-btn-color: var(--color-status-overdue-fg);
  --bs-btn-border-color: var(--color-status-overdue-fg);
  --bs-btn-bg: var(--color-bg-surface);
  --bs-btn-hover-bg: var(--color-status-overdue-bg);
  --bs-btn-hover-border-color: var(--color-status-overdue-fg);
  --bs-btn-hover-color: var(--color-status-overdue-fg);
  --bs-btn-active-bg: var(--color-status-overdue-bg);
  --bs-btn-active-border-color: var(--color-status-overdue-fg);
  --bs-btn-active-color: var(--color-status-overdue-fg);
}

.btn-outline-success {
  --bs-btn-color: var(--color-status-paid-fg);
  --bs-btn-border-color: var(--color-status-paid-fg);
  --bs-btn-bg: var(--color-bg-surface);
  --bs-btn-hover-bg: var(--color-status-paid-bg);
  --bs-btn-hover-border-color: var(--color-status-paid-fg);
  --bs-btn-hover-color: var(--color-status-paid-fg);
  --bs-btn-active-bg: var(--color-status-paid-bg);
  --bs-btn-active-border-color: var(--color-status-paid-fg);
  --bs-btn-active-color: var(--color-status-paid-fg);
}

.btn-outline-warning {
  --bs-btn-color: var(--color-status-due-fg);
  --bs-btn-border-color: var(--color-status-due-fg);
  --bs-btn-bg: var(--color-bg-surface);
  --bs-btn-hover-bg: var(--color-status-due-bg);
  --bs-btn-hover-border-color: var(--color-status-due-fg);
  --bs-btn-hover-color: var(--color-status-due-fg);
  --bs-btn-active-bg: var(--color-status-due-bg);
  --bs-btn-active-border-color: var(--color-status-due-fg);
  --bs-btn-active-color: var(--color-status-due-fg);
}

/* Info has no state of its own in this palette. It reads as a plain
   secondary rather than becoming a sixth colour. */
.btn-info,
.btn-outline-info {
  --bs-btn-bg: var(--color-bg-surface);
  --bs-btn-border-color: var(--color-border-default);
  --bs-btn-color: var(--color-fg-default);
  --bs-btn-hover-bg: var(--color-bg-sunken);
  --bs-btn-hover-border-color: var(--color-border-default);
  --bs-btn-hover-color: var(--color-fg-default);
  --bs-btn-active-bg: var(--color-bg-sunken);
  --bs-btn-active-border-color: var(--color-border-default);
  --bs-btn-active-color: var(--color-fg-default);
}

/* ─────────────────────────────────────────────────────────────
   5. CARD (§4)
   Surface, one hairline, --radius-md. No shadow: a card is not an
   elevated object here, and a border plus a shadow on the same edge
   is forbidden (§6).
   ───────────────────────────────────────────────────────────── */
.card {
  --bs-card-bg: var(--color-bg-surface);
  --bs-card-border-color: var(--color-border-subtle);
  --bs-card-border-radius: var(--radius-md);
  --bs-card-inner-border-radius: var(--radius-md);
  --bs-card-cap-bg: transparent;
  --bs-card-cap-color: var(--color-fg-default);
  --bs-card-color: var(--color-fg-default);
  --bs-card-spacer-y: var(--space-16);
  --bs-card-spacer-x: var(--space-16);
  --bs-card-cap-padding-y: var(--space-12);
  --bs-card-cap-padding-x: var(--space-16);
  box-shadow: none;
}

.card-header,
.card-footer {
  border-color: var(--color-border-subtle);
}

.card-title { font-size: var(--text-h3); font-weight: var(--weight-medium); }
.card-subtitle { color: var(--color-fg-muted); }

/* ─────────────────────────────────────────────────────────────
   6. TABLE — the primary layout of this product (§5)
   ───────────────────────────────────────────────────────────── */
.table {
  --bs-table-bg: var(--color-bg-surface);
  --bs-table-color: var(--color-fg-default);
  --bs-table-border-color: var(--color-border-subtle);
  --bs-table-striped-bg: var(--color-bg-sunken);
  --bs-table-striped-color: var(--color-fg-default);
  --bs-table-hover-bg: var(--color-bg-sunken);
  --bs-table-hover-color: var(--color-fg-default);
  --bs-table-active-bg: var(--color-bg-sunken);
  --bs-table-active-color: var(--color-fg-default);
  margin-bottom: 0;
  /* A column of rent amounts cannot be scanned with proportional
     figures (§3). Digits only: letters are unaffected, so this is safe
     to apply to the whole table rather than guessing which columns are
     currency, date or reference. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* Density lives here: compact tightens spacing and cell type, never
   body type (§1, §3). 14px is the floor and --text-table is 14px. */
.table > :not(caption) > * > * {
  padding: var(--row-padding-y) var(--cell-padding-x);
  font-size: var(--text-cell);
  line-height: var(--leading-table);
}

/* .table-sm keeps its intent: it sits one step tighter than whatever
   the density gives. Restated because the rule above would otherwise
   outrank Bootstrap's at equal specificity. */
.table-sm > :not(caption) > * > * {
  padding: var(--space-4) var(--space-8);
}

.table > thead > tr > th {
  font-weight: var(--weight-medium);
  color: var(--color-fg-muted);
  border-bottom-color: var(--color-border-default);
}

/* Row stripe on even rows. Set through Bootstrap's own striping
   variable so it composes with hover and selection exactly as
   .table-striped would, without the class appearing in markup. */
.table > tbody > tr:nth-of-type(even) > * {
  --bs-table-bg-type: var(--color-bg-sunken);
}

/* Hover is a BACKGROUND SHIFT, never a border (§4): in a dense grid,
   row borders out-shout the data they surround.

   The shift INVERTS the stripe rather than darkening past it. DESIGN.md
   §4 gives the stripe and the hover the same --muted value, which makes
   hover invisible on every even row — half the table gets no feedback at
   all. Darkening one step further is not available either: muted text is
   already at 4.70:1 on the stripe, so any darker ground drops secondary
   text below the 4.5:1 floor. tools/check-contrast.php was run against a
   third background and rejected it at 4.02:1.

   Flipping means the hovered row is always the one breaking the striping,
   in whichever direction, using only the two values the brief names and
   adding no third background. */
.table-hover > tbody > tr:nth-of-type(odd):hover > * {
  --bs-table-bg-state: var(--color-bg-sunken);
}

.table-hover > tbody > tr:nth-of-type(even):hover > * {
  --bs-table-bg-state: var(--color-bg-surface);
}

/* No .table-active / .selected rules: row selection does not exist in
   this app (0 files use either class). A component before its second
   use is not a component — add them with the first real selectable
   table, where the interaction can actually be designed. */

/* Header rows carry .table-light throughout the app. Remapped to the
   sunken well rather than Bootstrap's grey. */
.table-light {
  --bs-table-bg: var(--color-bg-sunken);
  --bs-table-color: var(--color-fg-default);
  --bs-table-border-color: var(--color-border-subtle);
  --bs-table-striped-bg: var(--color-bg-sunken);
  --bs-table-striped-color: var(--color-fg-default);
  --bs-table-hover-bg: var(--color-bg-sunken);
  --bs-table-hover-color: var(--color-fg-default);
  --bs-table-active-bg: var(--color-bg-sunken);
  --bs-table-active-color: var(--color-fg-default);
  color: var(--color-fg-default);
  border-color: var(--color-border-subtle);
}

/* Right-aligned numeric columns get right-aligned headers (§5). The
   alignment class is on the cell in existing markup; this makes the
   header follow it wherever a page has already opted in. */
.table th.text-end,
.table td.text-end {
  text-align: right;
}

/* A wide table scrolls inside its own container so the page body never
   scrolls sideways (§8). Bootstrap's .table-responsive already does
   this; it only needs the radius reset — tables are --radius-none. */
.table-responsive { border-radius: var(--radius-none); }

/* No transition anywhere on a table row. Sorting, filtering,
   pagination and row selection never animate (§9). */

/* ─────────────────────────────────────────────────────────────
   7. STATUS CHIPS (§2)
   Five treatments, no sixth. Bootstrap's contextual badge classes and
   the app's own .status-* classes are folded onto the same five.

   Bootstrap ships `.bg-*` as `background-color: … !important`, which
   no amount of specificity reaches. `background-image` paints above
   `background-color` and needs neither !important nor a restatement of
   a token's value, so the fill is delivered that way. The flat
   two-stop gradient is a fill, not a gradient.
   ───────────────────────────────────────────────────────────── */
.badge {
  --bs-badge-border-radius: var(--radius-full);
  --bs-badge-font-size: var(--text-small);
  --bs-badge-font-weight: var(--weight-medium);
  --bs-badge-padding-x: var(--space-8);
  --bs-badge-padding-y: var(--space-4);
  --bs-badge-color: var(--color-fg-default);
  line-height: var(--leading-table);
}

/* Current / paid — filled chip. */
.badge.bg-success,
.badge.status-paid,
.badge.status-occupied,
.rl-chip--paid {
  color: var(--color-status-paid-fg);
  background-image: linear-gradient(var(--color-status-paid-bg), var(--color-status-paid-bg));
}

/* Due soon — filled chip. */
.badge.bg-warning,
.badge.status-partial,
.rl-chip--due {
  color: var(--color-status-due-fg);
  background-image: linear-gradient(var(--color-status-due-bg), var(--color-status-due-bg));
}

/* Overdue, escalated — filled chip. */
.badge.bg-danger,
.badge.status-unpaid,
.rl-chip--overdue {
  color: var(--color-status-overdue-fg);
  background-image: linear-gradient(var(--color-status-overdue-bg), var(--color-status-overdue-bg));
}

/* Vacant / ended — outlined chip, no tint. */
.badge.bg-secondary,
.badge.bg-light,
.badge.status-vacant,
.rl-chip--ended {
  color: var(--color-status-ended-fg);
  border: 1px solid var(--color-border-default);
  background-image: linear-gradient(var(--color-bg-surface), var(--color-bg-surface));
}

/* In progress / notice given — outlined chip, no tint. bg-primary and
   bg-info land here rather than taking the accent: the accent is for
   actions, links and focus and nothing else (§7), and a badge is none
   of those. */
.badge.bg-primary,
.badge.bg-info,
.badge.status-notice,
.badge.status-maintenance,
.rl-chip--progress {
  color: var(--color-status-progress-fg);
  border: 1px solid var(--color-border-default);
  background-image: linear-gradient(var(--color-bg-surface), var(--color-bg-surface));
}

/* The .status-* classes are also used on non-badge elements (unit
   cards), where they were a bare background colour. Same five
   treatments, applied as a tint. */
.status-paid:not(.badge),
.status-occupied:not(.badge) { background-color: var(--color-status-paid-bg); }
.status-partial:not(.badge)  { background-color: var(--color-status-due-bg); }
.status-unpaid:not(.badge)   { background-color: var(--color-status-overdue-bg); }
.status-vacant:not(.badge),
.status-notice:not(.badge),
.status-maintenance:not(.badge) { background-color: var(--color-bg-sunken); }

/* ─────────────────────────────────────────────────────────────
   7b. SERVER-PARTIAL CLASSES — .rl-chip and .rl-empty, UNSCOPED

   includes/ui/status.php and includes/ui/empty.php emit .rl-chip and
   .rl-empty. css/rl/components.css already styles both, but every rule
   in that file is scoped to [data-rl] and only the standalone shell in
   includes/ui/shell.php sets that attribute — so on the 75 pages that
   come through includes/header.php the partials rendered UNSTYLED, and
   that is why nothing called them.

   Resolved here rather than by unscoping components.css or by touching
   header.php: the [data-rl] copy stays exactly as it is, and this file
   — which header.php does load — grows a second, unscoped copy.

   NO SECOND LOOK. The five tones above take .rl-chip--* as extra
   selectors on the SAME declarations, so a chip and the Bootstrap badge
   it replaces cannot drift apart. Only the box is restated, because
   .badge gets its box from Bootstrap and .rl-chip has nothing to get it
   from. Values are the ones components.css uses.
   ───────────────────────────────────────────────────────────── */
.rl-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-4);
  padding-block: var(--space-2);
  padding-inline: var(--space-8);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  font-size: var(--text-small);
  line-height: var(--leading-table);
  font-weight: var(--weight-medium);
  /* A status is never what gets truncated (§8). */
  white-space: nowrap;
}

/* A value that is not in the closed map in status.php. Not a sixth
   tone — it borrows overdue and adds a dashed edge — so a new migration
   enum is impossible to miss instead of falling through to grey. */
.rl-chip--unknown {
  border-style: dashed;
  border-color: currentColor;
}

/* ── Empty states (§7): two screens, never one with two captions. ── */

.rl-empty {
  padding-block: var(--space-16);
  padding-inline: var(--space-16);
}

.rl-empty__title {
  font-size: var(--text-h3);
  font-weight: var(--weight-medium);
}

.rl-empty__hint { color: var(--color-fg-muted); }

.rl-empty__action { margin-block-start: var(--space-16); }

/* FIRST RUN — nothing here yet, so the invitation IS the page. */
.rl-empty--first-run {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-8);
  padding-block: var(--space-48);
  max-inline-size: var(--measure);
  margin-inline: auto;
}

/* NO MATCH — a filter is hiding the rows. Left-aligned and tight,
   sitting where the rows were, under the control that caused it. */
.rl-empty--filtered {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
  padding-block: var(--space-24);
  border-block-start: 1px solid var(--color-border-subtle);
}

.rl-empty--filtered .rl-empty__title { font-size: var(--text-body); }

.rl-empty--filtered .rl-empty__action { margin-block-start: var(--space-12); }

/* ── The empty state's own button. A closed set of intents. ── */

.rl-btn {
  display: inline-block;
  /* 44px thumb target delivered by padding; the visual box stays quiet. */
  min-height: 2.75rem;
  padding-block: var(--space-12);
  padding-inline: var(--space-24);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.rl-btn--primary {
  background-color: var(--color-action-default);
  color: var(--color-fg-on-action);
}

.rl-btn--primary:hover { background-color: var(--color-action-hover); }

.rl-btn--secondary {
  background-color: transparent;
  color: var(--color-fg-default);
  border-color: var(--color-border-default);
}

.rl-btn--secondary:hover { border-color: var(--color-action-hover); }

/* ─────────────────────────────────────────────────────────────
   8. FORMS (§4)
   A meaningful boundary, never the decorative hairline (§2, §7).
   ───────────────────────────────────────────────────────────── */
.form-control,
.form-select,
.form-check-input,
.input-group-text {
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-sm);
  background-color: var(--color-bg-surface);
  color: var(--color-fg-default);
  font-size: var(--text-body);
  line-height: var(--leading-body);
}

.input-group-text { background-color: var(--color-bg-sunken); }

.form-control:hover:not(:disabled):not([readonly]),
.form-select:hover:not(:disabled) {
  border-color: var(--color-fg-default);
}

.form-control:focus,
.form-select:focus,
.form-check-input:focus {
  border-color: var(--color-border-default);
  background-color: var(--color-bg-surface);
  color: var(--color-fg-default);
  box-shadow: none;
  outline: 2px solid var(--color-border-focus);
  outline-offset: 2px;
}

.form-control:disabled,
.form-select:disabled,
.form-control[readonly] {
  background-color: var(--color-action-disabled);
  color: var(--color-fg-muted);
}

.form-control::placeholder { color: var(--color-fg-muted); }

.form-label,
.form-check-label,
.col-form-label {
  color: var(--color-fg-default);
  font-weight: var(--weight-medium);
}

.form-text { color: var(--color-fg-muted); font-size: var(--text-small); }

.form-check-input:checked {
  background-color: var(--color-action-default);
  border-color: var(--color-action-default);
}

.form-switch .form-check-input:checked {
  background-color: var(--color-action-default);
}

/* Invalid. The icon is dropped because colour never travels alone
   here anyway — .invalid-feedback carries the reason in words (§2). */
.was-validated .form-control:invalid,
.form-control.is-invalid,
.was-validated .form-select:invalid,
.form-select.is-invalid {
  border-color: var(--color-status-overdue-fg);
  background-image: none;
  padding-right: var(--space-12);
}

.was-validated .form-control:invalid:focus,
.form-control.is-invalid:focus,
.was-validated .form-select:invalid:focus,
.form-select.is-invalid:focus {
  box-shadow: none;
  outline: 2px solid var(--color-status-overdue-fg);
  outline-offset: 2px;
}

.invalid-feedback, .form-control.is-invalid ~ .invalid-feedback {
  color: var(--color-status-overdue-fg);
  font-size: var(--text-small);
}

.valid-feedback { color: var(--color-status-paid-fg); font-size: var(--text-small); }

/* ─────────────────────────────────────────────────────────────
   9. OVERLAYS (§4, §6)
   Dialog = --radius-md + --shadow-overlay. Dropdown = --shadow-floating.
   Each drops its border: never a border and a shadow on the same edge.
   ───────────────────────────────────────────────────────────── */
.modal {
  --bs-modal-bg: var(--color-bg-surface);
  --bs-modal-color: var(--color-fg-default);
  --bs-modal-border-color: transparent;
  --bs-modal-border-radius: var(--radius-md);
  --bs-modal-inner-border-radius: var(--radius-md);
  --bs-modal-box-shadow: var(--shadow-overlay);
  --bs-modal-padding: var(--space-24);
  --bs-modal-header-padding: var(--space-16) var(--space-24);
  --bs-modal-header-border-color: var(--color-border-subtle);
  --bs-modal-footer-border-color: var(--color-border-subtle);
  --bs-modal-footer-gap: var(--space-8);
}

.modal-content { box-shadow: var(--shadow-overlay); }

/* Origin centred, 260ms in (§9). Bootstrap's own .fade transition is
   retyped in tokens rather than replaced, so its JS still works. */
.modal.fade .modal-dialog {
  transition: transform var(--duration-slow) var(--ease-out),
              opacity var(--duration-slow) var(--ease-out);
}

.dropdown-menu {
  --bs-dropdown-bg: var(--color-bg-surface);
  --bs-dropdown-color: var(--color-fg-default);
  --bs-dropdown-border-color: transparent;
  --bs-dropdown-border-radius: var(--radius-md);
  --bs-dropdown-inner-border-radius: var(--radius-md);
  --bs-dropdown-link-color: var(--color-fg-default);
  --bs-dropdown-link-hover-color: var(--color-fg-default);
  --bs-dropdown-link-hover-bg: var(--color-bg-sunken);
  --bs-dropdown-link-active-color: var(--color-fg-default);
  --bs-dropdown-link-active-bg: var(--color-bg-sunken);
  --bs-dropdown-link-disabled-color: var(--color-fg-muted);
  --bs-dropdown-header-color: var(--color-fg-muted);
  --bs-dropdown-item-padding-x: var(--space-16);
  --bs-dropdown-item-padding-y: var(--space-8);
  --bs-dropdown-divider-bg: var(--color-border-subtle);
  box-shadow: var(--shadow-floating);
}

.toast {
  --bs-toast-bg: var(--color-bg-surface);
  --bs-toast-color: var(--color-fg-default);
  --bs-toast-border-color: transparent;
  --bs-toast-border-radius: var(--radius-md);
  --bs-toast-box-shadow: var(--shadow-floating);
  --bs-toast-header-bg: var(--color-bg-surface);
  --bs-toast-header-color: var(--color-fg-default);
  --bs-toast-header-border-color: var(--color-border-subtle);
  --bs-toast-font-size: var(--text-body);
}

.alert {
  --bs-alert-border-radius: var(--radius-md);
  --bs-alert-padding-x: var(--space-16);
  --bs-alert-padding-y: var(--space-12);
  --bs-alert-border-color: transparent;
}

.alert-success {
  --bs-alert-color: var(--color-status-paid-fg);
  --bs-alert-bg: var(--color-status-paid-bg);
}
.alert-warning {
  --bs-alert-color: var(--color-status-due-fg);
  --bs-alert-bg: var(--color-status-due-bg);
}
.alert-danger {
  --bs-alert-color: var(--color-status-overdue-fg);
  --bs-alert-bg: var(--color-status-overdue-bg);
}
/* info / primary / secondary / light / dark carry no state of their
   own. They read as a quiet well rather than a sixth colour (§7). */
.alert-info,
.alert-primary,
.alert-secondary,
.alert-light,
.alert-dark {
  --bs-alert-color: var(--color-fg-default);
  --bs-alert-bg: var(--color-bg-sunken);
}

/* Pagination — the accent on the current page only. Never animated:
   a manager pages through a ledger dozens of times a day (§9). */
.page-link {
  --bs-pagination-color: var(--color-action-default);
  --bs-pagination-bg: var(--color-bg-surface);
  --bs-pagination-border-color: var(--color-border-subtle);
  --bs-pagination-hover-color: var(--color-action-hover);
  --bs-pagination-hover-bg: var(--color-bg-sunken);
  --bs-pagination-hover-border-color: var(--color-border-subtle);
  --bs-pagination-focus-bg: var(--color-bg-sunken);
  --bs-pagination-focus-box-shadow: none;
  --bs-pagination-active-bg: var(--color-action-default);
  --bs-pagination-active-border-color: var(--color-action-default);
  --bs-pagination-active-color: var(--color-fg-on-action);
  --bs-pagination-disabled-color: var(--color-fg-muted);
  --bs-pagination-disabled-bg: var(--color-bg-sunken);
  --bs-pagination-disabled-border-color: var(--color-border-subtle);
  transition: none;
}

/* Tabs. Switching a tab never animates (§9). */
.nav-tabs {
  --bs-nav-tabs-border-color: var(--color-border-subtle);
  --bs-nav-tabs-link-active-color: var(--color-fg-default);
  --bs-nav-tabs-link-active-bg: var(--color-bg-surface);
  --bs-nav-tabs-link-active-border-color: var(--color-border-subtle);
  --bs-nav-tabs-link-hover-border-color: var(--color-border-subtle);
  --bs-nav-tabs-border-radius: var(--radius-sm);
}
.nav-tabs .nav-link { transition: none; }

.list-group {
  --bs-list-group-bg: var(--color-bg-surface);
  --bs-list-group-color: var(--color-fg-default);
  --bs-list-group-border-color: var(--color-border-subtle);
  --bs-list-group-border-radius: var(--radius-md);
  --bs-list-group-action-hover-bg: var(--color-bg-sunken);
  --bs-list-group-action-hover-color: var(--color-fg-default);
  --bs-list-group-active-bg: var(--color-bg-sunken);
  --bs-list-group-active-color: var(--color-fg-default);
  --bs-list-group-active-border-color: var(--color-border-subtle);
}

/* ─────────────────────────────────────────────────────────────
   10. NOTIFICATION BELL AND USER MENU
   Ported from the header's inline block onto tokens. These class
   names appear in page markup and in footer.php's JS, so every one
   of them is kept.
   ───────────────────────────────────────────────────────────── */
.notification-badge {
  position: absolute;
  top: calc(var(--space-4) * -1);
  right: calc(var(--space-4) * -1);
  /* See .badge-count below: an unread count is chronic, not urgent (§7). */
  background-color: var(--color-bg-sunken);
  color: var(--color-fg-default);
  border-radius: var(--radius-full);
  width: var(--space-16);
  height: var(--space-16);
  font-size: var(--text-small);
  display: flex;
  align-items: center;
  justify-content: center;
}

.notification-bell {
  position: relative;
  padding: var(--space-8);
  cursor: pointer;
  border-radius: var(--radius-full);
  transition: background-color var(--duration-press) var(--ease-out);
}

.notification-bell:hover { background-color: var(--color-bg-sunken); }

.notification-bell .bell-icon {
  font-size: var(--text-h3);
  color: var(--color-fg-default);
}

/* The pulse animation is deliberately gone. An unread count is a
   permanent condition on a busy console, and §9 forbids animating
   anything a manager sees dozens of times a day. */
.notification-bell .badge-count {
  position: absolute;
  top: 0;
  right: 0;
  /* Not red. An unread count is permanent on a busy console, and DESIGN.md
     §7 is explicit: "don't use red for a chronic condition… a permanently red
     dashboard trains the manager to ignore exactly the signal you built."
     Red here would compete with real arrears for the same attention. */
  background-color: var(--color-fg-default);
  color: var(--color-fg-inverse);
  border-radius: var(--radius-full);
  min-width: var(--space-16);
  height: var(--space-16);
  font-size: var(--text-small);
  font-weight: var(--weight-medium);
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-4);
}

.notification-bell .badge-count.hidden { display: none; }

.notification-dropdown {
  width: 22.5rem;
  max-width: calc(100vw - var(--space-32));
  max-height: 28rem;
  overflow: hidden;
  padding: 0;
  border: 0;
  box-shadow: var(--shadow-floating);
  border-radius: var(--radius-md);
}

.notification-dropdown-header {
  padding: var(--space-12) var(--space-16);
  background-color: var(--color-bg-sunken);
  color: var(--color-fg-default);
  border-bottom: 1px solid var(--color-border-subtle);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.notification-dropdown-header h6 {
  margin: 0;
  font-weight: var(--weight-medium);
}

.notification-dropdown-body {
  max-height: 20rem;
  overflow-y: auto;
}

.notification-dropdown-body::-webkit-scrollbar { width: 0.375rem; }

.notification-dropdown-body::-webkit-scrollbar-thumb {
  background-color: var(--color-border-default);
  border-radius: var(--radius-full);
}

.notification-item-dropdown {
  padding: var(--space-12) var(--space-16);
  border-bottom: 1px solid var(--color-border-subtle);
  cursor: pointer;
  transition: background-color var(--duration-press) var(--ease-out);
}

.notification-item-dropdown:hover { background-color: var(--color-bg-sunken); }

/* Unread is a background shift plus a left marker. The marker is the
   accent because opening a notification is the action on offer. */
.notification-item-dropdown.unread {
  background-color: var(--color-action-subtle);
  border-left: 3px solid var(--color-action-default);
}

.notification-item-dropdown .title {
  font-weight: var(--weight-medium);
  font-size: var(--text-table);
  color: var(--color-fg-default);
  margin-bottom: var(--space-4);
}

.notification-item-dropdown .message {
  font-size: var(--text-small);
  color: var(--color-fg-muted);
  margin-bottom: var(--space-4);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.notification-item-dropdown .time {
  font-size: var(--text-small);
  color: var(--color-fg-muted);
}

.notification-dropdown-footer {
  padding: var(--space-12) var(--space-16);
  text-align: center;
  border-top: 1px solid var(--color-border-subtle);
  background-color: var(--color-bg-sunken);
}

.notification-dropdown-footer a {
  color: var(--color-action-default);
  text-decoration: none;
  font-weight: var(--weight-medium);
  font-size: var(--text-table);
}

.notification-dropdown-footer a:hover { text-decoration: underline; }

/* "No notifications yet" and "nothing matches this filter" are
   different screens; the copy is the page's job, the frame is this. */
.notification-empty {
  padding: var(--space-32) var(--space-24);
  text-align: center;
  color: var(--color-fg-muted);
}

.notification-empty i {
  font-size: var(--text-h1);
  margin-bottom: var(--space-8);
  display: block;
}

.user-dropdown { position: relative; }

.user-info {
  display: flex;
  align-items: center;
  padding: var(--space-8) var(--space-16);
  color: var(--color-fg-default);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: background-color var(--duration-press) var(--ease-out);
}

.user-info:hover {
  background-color: var(--color-bg-sunken);
  color: var(--color-fg-default);
  text-decoration: none;
}

/* Quiet, not accented: an avatar is not an action (§7). */
.user-avatar {
  width: var(--space-32);
  height: var(--space-32);
  background-color: var(--color-bg-sunken);
  border: 1px solid var(--color-border-default);
  border-radius: var(--radius-full);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-fg-default);
  font-weight: var(--weight-medium);
  margin-right: var(--space-8);
}

/* ─────────────────────────────────────────────────────────────
   11. BELOW --bp-lg: navigation collapses to a drawer (§8)
   Expressed in em rather than px: the lint forbids raw px outside
   hairlines, and an em query also survives the user's own text-size
   setting. 63.99em == 1023.84px, immediately below --bp-lg.

   This used to read 47.99em — Bootstrap's md. DESIGN.md §8 says the
   manager console targets 1024px AND UP, so between 768 and 1024 the
   console was showing a persistent sidebar the brief does not want.
   The boundary can no longer be Bootstrap's, because Bootstrap's lg
   is 992px: the visibility utilities that used to pair with this
   query (.d-none.d-md-block / .d-md-none) are replaced by .rl-at-lg
   and .rl-below-lg in section 16, which land on the same 63.99em.

   footer.php toggles .show on .sidebar and .sidebar-overlay. Both
   class names and both behaviours are unchanged; only the width at
   which its resize handler force-closes the drawer moved with this.
   ───────────────────────────────────────────────────────────── */
@media (max-width: 63.99em) {
  .sidebar {
    position: fixed;
    top: 0;
    left: -100%;
    width: min(17.5rem, 85vw);
    height: 100vh;
    overflow-y: auto;
    z-index: var(--z-modal);
    transition: left var(--duration-slow) var(--ease-drawer);
  }

  .sidebar.show { left: 0; }

  .sidebar-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: var(--color-bg-overlay);
    z-index: var(--z-overlay);
    display: none;
  }

  .sidebar-overlay.show { display: block; }

  /* header.php carried `margin-left: 0 !important` here. Nothing in
     the app sets a margin-left on this element — the override was
     defensive and dead, so it is dropped rather than reproduced. */

  /* Tables get room back on a phone. Body type never shrinks (§7). */
  .table > :not(caption) > * > * {
    padding: var(--space-8) var(--space-8);
  }
}

/* ─────────────────────────────────────────────────────────────
   12. DASHBOARD FURNITURE
   Classes the console's overview pages already carry in markup.
   Previously defined in a 176-line <style> block inside
   dashboard.php, on a third palette of its own.

   MONOCHROME ON PURPOSE. The old version gave each of the eight
   stat tiles its own colour — navy, emerald, amber, red, indigo.
   DESIGN.md §1: colour is spent almost entirely on STATE, and a
   screen where nothing is wrong should be quiet and nearly
   monochrome. §7: do not colour-code beyond the five states.
   Category is not a state, so a tile that merely counts units
   earns no colour at all. The figure carries the meaning; the
   icon is a signpost.

   The old colour-variant classes (.rentlys-navy, .bg-success-light
   and friends) are left in the markup and are now inert — nothing
   defines them. That is deliberate: it keeps this a CSS-only change
   and leaves the markup for the screen's own redesign.
   ───────────────────────────────────────────────────────────── */
.stat-icon,
.stats-icon {
  flex: 0 0 auto;
  width: var(--space-48);
  height: var(--space-48);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--color-bg-sunken);
  color: var(--color-fg-muted);
  border-radius: var(--radius-md);
  font-size: var(--text-h2);
}

.stat-icon--sm {
  width: var(--space-32);
  height: var(--space-32);
  font-size: var(--text-h3);
}

/* Quick actions are actions, so they may carry the accent — but four
   loud tiles in a row is not "spend boldness once". They read as quiet
   surfaces and take the accent only on hover and focus. No transform:
   §9 forbids animating what a manager does dozens of times a day, and
   the old rule lifted these 3px on every pass. */
.quick-action-btn {
  display: block;
  padding: var(--space-24) var(--space-16);
  text-align: center;
  background-color: var(--color-bg-surface);
  color: var(--color-fg-default);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
  text-decoration: none;
  transition: background-color var(--duration-press) var(--ease-out),
              border-color var(--duration-press) var(--ease-out);
}

/* Focus only. The hover half lives in the (hover: hover) query in §15 —
   on a touchscreen a :hover state latches after a tap and stays until
   something else is tapped, so hover styling must never be unconditional
   (DESIGN.md §9, last line). Focus-visible has no such problem. */
.quick-action-btn:focus-visible {
  background-color: var(--color-bg-sunken);
  border-color: var(--color-border-default);
  color: var(--color-action-default);
}

.quick-action-btn i {
  display: block;
  margin-bottom: var(--space-8);
  font-size: var(--text-h2);
  color: var(--color-fg-muted);
}

.quick-action-btn:focus-visible i { color: var(--color-action-default); }

/* The SMS quota track. A progress bar is legitimate here — it is a
   quota, not rent collection, which §7 forbids putting a bar on. */
.rl-progress { height: var(--space-8); }

/* ─────────────────────────────────────────────────────────────
   13. NOTIFICATIONS LIST
   The notifications index and the 28 per-type pages under
   public/notifications/ each shipped the SAME <style> block
   (identical in all 28; a longer variant on the index). All 29
   are deleted; what is genuinely shared and genuinely new lands
   here, on tokens.

   WHAT WAS DROPPED RATHER THAN PORTED, and why:

     * .list-group-item hover / .active / .collapse borders — a
       third styling of a component section 9 already derives from
       --bs-list-group-*. Re-declaring it here would fork it.
     * .fade-in and @keyframes fadeIn — it fired on every filter
       and every page of results. Section 9: filtering and
       pagination never animate. The class is left in the JS
       markup and is now inert.
     * translateX on row hover — section 4, a row reacts with a
       background shift, never movement, and section 9 forbids
       animating what a manager does dozens of times a day.
     * ::-webkit-scrollbar paint on the category rail —
       decoration, one browser engine, no information carried.
     * .loading-overlay — never present in any markup. Dead.
     * transition: all — repaints every property on every hover.

   UNREAD READS LIKE THE BELL DROPDOWN. Section 10 already settled
   this exact question for .notification-item-dropdown: a ground
   shift plus a left marker in the accent, because opening a
   notification is the action on offer. Same problem, same answer,
   so the rail and the list agree.
   ───────────────────────────────────────────────────────────── */
.notification-item {
  border-left: 3px solid transparent;
  transition: background-color var(--duration-press) var(--ease-out);
}

.notification-item.unread {
  background-color: var(--color-action-subtle);
  border-left-color: var(--color-action-default);
}

/* Only an UNREAD row does anything when clicked — the index's
   handler returns early on a read one. So only an unread row gets
   the pointer and the hover. Claiming otherwise on the other rows
   would be a lie the markup cannot honour, and the 28 per-type
   pages have no row handler at all.

   Hover INVERTS the unread ground rather than darkening past it,
   the same resolution reached for the table row stripe: both
   values are already contrast-checked, and no third ground is
   needed. */
.notification-item[data-unread="1"] { cursor: pointer; }

.notification-item[data-unread="1"]:hover {
  background-color: var(--color-bg-surface);
}

/* The category rail on the notifications index. Single-use today,
   which is why it is four declarations and no more. It follows
   the page because the rail is the filter and the list below it
   is long. */
.sticky-sidebar {
  position: sticky;
  top: var(--space-16);
  max-height: calc(100vh - var(--space-96));
  overflow-y: auto;
}

/* Empty-state icon. Every one of these pages carried it as
   style="font-size: 3rem; color: #ccc" — a value the token layer
   can never reach, repeated 29 times. The markup carries no class
   to hang it on, so it is matched structurally: the icon opening a
   `.text-center.py-5` block IS the empty state, everywhere here.
   Sized and coloured to agree with .notification-empty above, so
   the bell dropdown and the page say the same thing.

   THE SELECTOR IS DELIBERATELY TWO SHAPES. The first version of
   this rule required `.card-body > .text-center.py-5 > .bi` and
   matched exactly ONE of the seven non-notification sites, so six
   empty states — "no tenants yet", "no properties yet", "unit is
   vacant" — shipped with a body-text-sized glyph where a 3rem one
   had been. Those are the screens a new landlord sees first.

   Two things were wrong with it. `.card-body >` excluded
   properties.php, whose block is not in a card at all, and
   unit_view.php, where `.card-body` and `.text-center.py-5` are
   the SAME element so the child chain needs an element that does
   not exist. And tenants.php and properties.php wrap the icon in
   a `.mb-4`. Both shapes are named below rather than reaching for
   a descendant selector, which would also catch icons inside
   buttons that happen to sit in a centred block. */
.text-center.py-5 > .bi,
.text-center.py-5 > .mb-4 > .bi {
  display: block;
  font-size: var(--text-h1);
  color: var(--color-fg-muted);
  margin-bottom: var(--space-8);
}

/* ─────────────────────────────────────────────────────────────
   14. RECORD AND UNIT FURNITURE
   Classes carried by the table and detail screens a landlord works
   every day — tenants, units, properties, property_view, unit_view.
   They arrived as five separate <style> blocks, each declaring the
   same third palette (--rentlys-navy / -green / -gray) on its own
   :root. Everything those blocks re-invented — .card, .card-header,
   .btn-primary, .modal-header, .form-label, .form-control:focus,
   .table th, .page-link, .badge.bg-* — is deleted, not moved: this
   file already derives all of it from tokens. What lands here is
   only what appears on two or more pages.

   COMMON DEFECTS REMOVED, not carried over:
     * green/navy gradients on .btn-primary and the property header —
       gradient-as-decoration is a forbidden default, and green is
       reserved for paid (§7)
     * translateY lifts on card, tile and row hover — §9 forbids
       animating what a manager does dozens of times a day
     * radii off the ladder in §6 (none / sm / md / full)
     * a shadow AND a border on the same edge (§6)
     * per-category colour on stat tiles — category is not a state (§7)
   ───────────────────────────────────────────────────────────── */

/* Tenant and user rows. A monogram well, not a coloured identity. */
.avatar-circle {
  flex: 0 0 auto;
  width: var(--space-32);
  height: var(--space-32);
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  background-color: var(--color-bg-sunken);
  color: var(--color-fg-muted);
}

/* Figure tiles. Separation by border, so no shadow (§6). The figure
   carries the meaning; nothing here is colour-coded by category. */
.stat-card {
  display: flex;
  align-items: center;
  gap: var(--space-16);
  padding: var(--space-16);
  background-color: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

.stat-value {
  font-size: var(--text-h1);
  font-weight: var(--weight-medium);
  line-height: var(--leading-heading);
  color: var(--color-fg-default);
  font-variant-numeric: tabular-nums;
}

.stat-label {
  margin-top: var(--space-4);
  font-size: var(--text-small);
  color: var(--color-fg-muted);
}

/* Bootstrap paints .bg-* with !important, which no specificity
   reaches; background-image paints above background-color and needs
   neither. Same device as the status chips, used here to UNDO a
   colour rather than to place one — a tile that counts units is not
   a state and earns no colour (§7). */
.stat-icon.bg-primary,
.stat-icon.bg-success,
.stat-icon.bg-warning,
.stat-icon.bg-danger,
.stat-icon.bg-info {
  color: var(--color-fg-muted);
  background-image: linear-gradient(var(--color-bg-sunken), var(--color-bg-sunken));
}

/* A unit tile. The status tint arrives from the .status-* rules in
   section 7 — those are more specific, so they win over the surface
   colour set here, which is what a vacant or unpaid unit needs. */
.unit-card {
  padding: var(--space-16);
  background-color: var(--color-bg-surface);
  border: 1px solid var(--color-border-subtle);
  border-radius: var(--radius-md);
}

.unit-tenant {
  margin-top: var(--space-8);
  font-size: var(--text-small);
  color: var(--color-fg-muted);
}

/* The `badge-<status>` set that unit_view.php and property_view.php
   used to print is GONE, not moved. Both pages now render the unit
   chip through includes/ui/status.php, which emits .rl-chip and takes
   its colour from the five treatments in section 7 — so there is one
   status vocabulary and one place that paints it, instead of a hand
   map per page. Nothing in the app carries these class names now. */

/* Empty-state glyph. Every empty state in the console is a .text-center
   block whose first mark is one Bootstrap icon, and each one was sized
   by a style attribute of its own. Sized here instead, and sized down:
   a 64-unit illustration is decoration, and this product has none (§1). */
.text-center > .bi,
.text-center > .mb-4 > .bi {
  font-size: var(--text-h1);
}


/* ─────────────────────────────────────────────────────────────
   15. MOTION SAFETY — app-wide

   base.css carries both of these but is scoped to [data-rl], and
   arrears.php is the only page that sets it. Everything else in the
   console — every page in this file's reach — had neither. That is
   an accessibility defect, not a polish item: §9 requires reduced
   motion to remove movement while preserving the state change, and
   requires hover motion to sit behind a pointer query so it cannot
   "stick" on a touch device.
   ───────────────────────────────────────────────────────────── */

/* Hover effects only where there is a real pointer. On a touchscreen a
   :hover state latches after a tap and stays until something else is
   tapped (§9, last line). */
@media (hover: hover) and (pointer: fine) {
  .quick-action-btn:hover {
    background-color: var(--color-bg-sunken);
    border-color: var(--color-border-default);
    color: var(--color-action-default);
  }
  .quick-action-btn:hover i { color: var(--color-action-default); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    /* Documented utility override, §17 CS1: movement is removed, but colour
       and opacity changes survive so no state change is lost. */
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
  /* The one transform in this file. Removed outright rather than shortened —
     a 1ms scale is still movement. */
  .btn:active { transform: none; }
}

/* The premium-lock glyph in the sidebar. Was an inline style attribute at
   0.8rem = 12.8px, below the 13px floor in DESIGN.md §3. It is a signpost on
   a locked item, so it takes muted ink rather than the due-soon amber it
   used to carry — a locked feature is not a deadline (§7). */
.sidebar .rl-lock {
  font-size: var(--text-small);
  color: var(--color-fg-muted);
}


/* ─────────────────────────────────────────────────────────────
   16. BELOW --bp-lg: TABLES BECOME STACKED RECORD CARDS (§8)

   "Below --bp-lg, navigation collapses to a drawer and tables become
   stacked record cards … Tables do not scroll horizontally on a
   phone; that is not a usable rent ledger." Until now every table in
   the console was .table-responsive, i.e. a sideways scroll, which is
   the thing that sentence forbids by name.

   THE MECHANISM, and why this one.
   The textbook stacked-table needs data-label on every cell — a
   markup change in ~36 files. The alternative, a pure-CSS stack with
   no labels, needs no markup at all but ships an unlabelled column of
   values: on a tenants row, "A101" and "Active" become two anonymous
   badges on two anonymous lines. Worse, stacking a table means giving
   its elements display:block, which strips the table roles from the
   accessibility tree — so a screen reader loses the header
   association at exactly the moment the visible header is hidden.
   An unlabelled cell then reads as a bare value to everyone.

   So the rule is keyed on ONE opt-in class and reads the label from
   the cell, if there is one:

       <div class="table-responsive rl-stack"> … <td data-label="Status">

   A table that opts in with labels gets a labelled record card. A
   table that opts in with no labels still gets a bordered block with
   its first cell as the title — option (a), free, as the degraded
   case. Opting in is one class; labelling is one attribute per cell,
   paid per table by whoever migrates it, not all at once.

   Nothing is hidden and nothing is truncated: the whole row is shown,
   stacked. §8's "the rest behind a disclosure" is NOT built — a
   CSS-only disclosure needs a <details> per row, which is the
   per-page markup change this rule exists to avoid. Showing every
   field is the conservative half of that sentence, and it keeps the
   last line of §8 ("nothing essential is hidden at any width, a
   status is never what gets truncated") true by construction.

   Type: cells go UP to --text-body here, never down. The 14px table
   floor and 13px absolute floor (§3) are both respected — the label
   is --text-small, which IS the 13px floor.
   ───────────────────────────────────────────────────────────── */

/* Visibility pair, replacing .d-none.d-md-block / .d-md-none in the
   console chrome. Bootstrap's display utilities ship !important and
   are hard-wired to its own lg = 992px, 32px short of --bp-lg. A
   custom pair is the only way to land on 1024 without !important. */
.rl-below-lg { display: none; }

@media (max-width: 63.99em) {
  .rl-at-lg { display: none; }
  .rl-below-lg { display: block; }

  /* The sidebar is position:fixed at this width (section 11), so it is
     out of flow and its grid column would otherwise leave a dead strip
     beside the content between --bp-lg and Bootstrap's own lg. */
  .main-content-wrapper { width: 100%; }

  /* Primary and thumb-reachable, so 44px, not the console's 24px
     pointer floor (§8). The token is redefined rather than the value
     written out, so density stays the thing that owns control size. */
  #sidebarToggle {
    --tap-target: 2.75rem;
    min-width: var(--tap-target);
    min-height: var(--tap-target);
  }

  /* ── The stack ── */

  /* No sideways scroll. This is the whole point. */
  .rl-stack { overflow-x: visible; }

  .rl-stack .table,
  .rl-stack .table > tbody { display: block; }

  /* The header row's job is done by the per-cell labels below. */
  .rl-stack .table > thead { display: none; }

  .rl-stack .table > tbody > tr {
    display: block;
    padding: var(--space-12);
    margin-bottom: var(--space-12);
    background-color: var(--color-bg-surface);
    border: 1px solid var(--color-border-subtle);
    border-radius: var(--radius-md);
  }

  .rl-stack .table > tbody > tr:last-child { margin-bottom: 0; }

  /* Striping and hover are both row backgrounds, and a card that is
     already a bordered block does not need a second separation cue
     (§6: spacing first, border third). Handing these two Bootstrap
     variables `transparent` lets its own box-shadow fall through to
     nothing rather than fighting it. */
  .rl-stack .table > tbody > tr > * {
    --bs-table-bg-type: transparent;
    --bs-table-bg-state: transparent;
    display: block;
    padding: var(--space-4) 0;
    font-size: var(--text-body);
    border-bottom-width: 0;
    text-align: start;
  }

  /* The first cell is the record's name. It gets no label — a label on
     a title is noise — and it carries the block's only weight. */
  .rl-stack .table > tbody > tr > *:first-child {
    font-weight: var(--weight-medium);
    padding-top: 0;
    padding-bottom: var(--space-8);
    margin-bottom: var(--space-8);
    border-bottom: 1px solid var(--color-border-subtle);
  }

  .rl-stack .table > tbody > tr > *:last-child { padding-bottom: 0; }

  /* Label left, value right: a spec-sheet row. minmax(0, …) on both
     tracks so a long email wraps instead of widening the card, which
     is what would put the horizontal scroll back. */
  .rl-stack .table > tbody > tr > *[data-label] {
    display: grid;
    grid-template-columns: minmax(0, 8rem) minmax(0, 1fr);
    gap: var(--space-12);
    align-items: baseline;
  }

  .rl-stack .table > tbody > tr > *[data-label]::before {
    content: attr(data-label);
    color: var(--color-fg-muted);
    font-size: var(--text-small);
  }

  /* An empty state or a "no results" row is one cell spanning the
     table. It is not a field, so it takes neither the grid nor the
     title treatment. */
  .rl-stack .table > tbody > tr > [colspan] {
    display: block;
    font-weight: var(--weight-regular);
    border-bottom-width: 0;
    margin-bottom: 0;
  }

  /* NOT HERE: a rule putting .text-end cells back to start alignment.
     Right alignment is a wide-table device (§5) and a card does not
     want it, but Bootstrap ships .text-end as `!important` and no
     selector reaches it. Un-aligning those cells means dropping the
     utility from the markup, which is that page's own migration. */

  /* Row actions are thumb targets once the row is a card. */
  .rl-stack .table > tbody > tr .btn {
    --tap-target: 2.75rem;
    min-width: var(--tap-target);
    min-height: var(--tap-target);
  }

  .rl-stack .table > tbody > tr .btn-group { flex-wrap: wrap; }
}

/* ─────────────────────────────────────────────────────────────
   17. DOMAIN COLOUR — where you are

   Colour coding, restored on the product owner's instruction. It
   overrides DESIGN.md §7 ("don't colour-code beyond the five
   states"); the exception is recorded in the reconciliation doc §8.

   THE RULE THAT KEEPS IT FROM BECOMING FRUIT SALAD:

     a CHIP is a pill with text        and always means STATE
     a WELL is a tinted square + glyph and always means PLACE

   Two systems, two shapes, never the same component. So a paid
   chip beside a Money well cannot be misread as two statuses.

   Four domains, one colour each, used identically on every surface
   they appear: sidebar glyph, section heading, stat tile, page
   header. The point is that the colour TEACHES the section — the
   old scheme gave Properties navy and Units green for no reason a
   user could learn.

   Every value is contrast-checked on all three grounds it can land
   on, in both themes — 24 pairs in tools/check-contrast.php. That
   is the whole reason these hues are what they are.
   ───────────────────────────────────────────────────────────── */

.rl-domain--estate { --rl-domain-fg: var(--color-domain-estate-fg); --rl-domain-bg: var(--color-domain-estate-bg); }
.rl-domain--people { --rl-domain-fg: var(--color-domain-people-fg); --rl-domain-bg: var(--color-domain-people-bg); }
.rl-domain--ledger { --rl-domain-fg: var(--color-domain-ledger-fg); --rl-domain-bg: var(--color-domain-ledger-bg); }
.rl-domain--upkeep { --rl-domain-fg: var(--color-domain-upkeep-fg); --rl-domain-bg: var(--color-domain-upkeep-bg); }

/* The well. Overrides the monochrome .stat-icon from §12 only when a
   domain class is present, so anything not yet assigned stays neutral
   rather than guessing. */
.stat-icon.rl-domain--estate,
.stat-icon.rl-domain--people,
.stat-icon.rl-domain--ledger,
.stat-icon.rl-domain--upkeep,
.stats-icon.rl-domain--estate,
.stats-icon.rl-domain--people,
.stats-icon.rl-domain--ledger,
.stats-icon.rl-domain--upkeep {
  background-color: var(--rl-domain-bg);
  color: var(--rl-domain-fg);
}

/* Section heading — the sidebar group labels and page section titles.
   A 2px rule in the domain colour, not a coloured heading: the text
   stays default ink so it keeps its full contrast against the page. */
.sidebar h6.rl-domain--estate,
.sidebar h6.rl-domain--people,
.sidebar h6.rl-domain--ledger,
.sidebar h6.rl-domain--upkeep {
  border-inline-start: 2px solid var(--rl-domain-fg);
  padding-inline-start: var(--space-8);
}

/* The active nav item takes its own domain rather than the accent, so
   the sidebar says which area you are in at a glance. */
.sidebar .nav-link.active.rl-domain--estate,
.sidebar .nav-link.active.rl-domain--people,
.sidebar .nav-link.active.rl-domain--ledger,
.sidebar .nav-link.active.rl-domain--upkeep {
  background-color: var(--rl-domain-bg);
  color: var(--rl-domain-fg);
}

/* A page header can carry a domain edge. Same reasoning as the section
   heading: the rule is coloured, the text is not. */
.page-header.rl-domain--estate,
.page-header.rl-domain--people,
.page-header.rl-domain--ledger,
.page-header.rl-domain--upkeep {
  border-block-end-color: var(--rl-domain-fg);
}
