/* =========================================================================
   RoomLine — COMPONENTS
   §9 primitives, §11 data states, §12 forms, §13 tables, §15 notifications.

   Loaded after styles.css. Much of this file is corrective: the pre-spec
   sheet gave every button a gradient and a coloured glow, every card a
   shadow and a hover lift, every table header tracked-out capitals, and
   every badge a 20px pill radius. §22 forbids all of it. Rather than hunt
   those declarations through a hundred kilobytes of CSS, they are overruled
   here in one place where the reasoning sits next to the rule.
   ========================================================================= */

/* =========================================================================
   §9.1 BUTTONS
   Four variants. Primary is one per screen region, maximum; secondary is the
   default choice. No arrow glyphs appended to labels — if a button
   navigates, its verb says so.
   ========================================================================= */
.btn, #app .btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 36px; padding: 0 14px;
  border-radius: var(--rl-r-sm);
  border: 1px solid transparent;
  background: var(--rl-i-600); color: #fff;
  font-size: 13.5px; font-weight: 500; line-height: 1;
  cursor: pointer;
  box-shadow: none;              /* no coloured glow under a button */
  filter: none;
  transition: background var(--rl-d-instant) var(--rl-ease-standard),
              border-color var(--rl-d-instant) var(--rl-ease-standard),
              color var(--rl-d-instant) var(--rl-ease-standard);
}
.btn:hover, #app .btn:hover { background: var(--rl-i-700); filter: none; transform: none; box-shadow: none; }
.btn:active, #app .btn:active { transform: scale(0.98); }
.btn:focus-visible { outline: none; box-shadow: var(--rl-focus); }

.btn.ghost, #app .btn.ghost,
.btn.secondary, #app .btn.secondary {
  background: var(--rl-surface);
  color: var(--rl-text);
  border-color: var(--rl-line-control);
  box-shadow: none;
}
.btn.ghost:hover, #app .btn.ghost:hover,
.btn.secondary:hover, #app .btn.secondary:hover {
  background: var(--rl-n-50); border-color: var(--rl-line-strong); transform: none;
}
[data-theme="dark"] .btn.ghost:hover, [data-theme="dark"] #app .btn.ghost:hover { background: var(--rl-surface-raised); }
.btn.ghost:active, #app .btn.ghost:active { transform: scale(0.98); }

.btn.plain, #app .btn.plain {
  background: transparent; border-color: transparent; color: var(--rl-text-2);
}
.btn.plain:hover, #app .btn.plain:hover { background: var(--rl-n-50); color: var(--rl-text); }
[data-theme="dark"] .btn.plain:hover { background: var(--rl-surface-raised); }

/* Destructive, and never filled red until confirmed. */
.btn.danger, #app .btn.danger {
  background: var(--rl-surface); color: var(--rl-text-danger); border-color: var(--rl-line-danger);
}
.btn.danger:hover, #app .btn.danger:hover { background: var(--rl-tint-red-bg); }

.btn.small, #app .btn.small { height: 30px; padding: 0 10px; font-size: 12.5px; }
.btn.large, #app .btn.large { height: 44px; padding: 0 18px; font-size: 15px; }
.btn:disabled, .btn.ghost:disabled { opacity: .55; cursor: not-allowed; transform: none !important; }

/* Loading: the label stays put and the width is locked, so the button does
   not resize under the cursor. */
.btn[aria-busy="true"] { pointer-events: none; }
.btn[aria-busy="true"]::before {
  content: ''; width: 14px; height: 14px; flex: none;
  border: 2px solid currentColor; border-right-color: transparent;
  border-radius: var(--rl-r-full);
  animation: rl-spin .7s linear infinite;
}
@keyframes rl-spin { to { transform: rotate(360deg); } }

/* =========================================================================
   §9.2 INPUTS
   36px, hairline border, 5px radius. Readonly looks like text, because it
   is text.
   ========================================================================= */
/* `input` with no type attribute IS a text input — 132 of them in this
   codebase — and an attribute selector does not match a default. They were
   all missing the control styling: 23px tall, hairline border, none of the
   focus treatment. */
#app input:not([type]),
#app input[type="text"], #app input[type="email"], #app input[type="password"],
#app input[type="number"], #app input[type="date"], #app input[type="time"],
#app input[type="search"], #app input[type="tel"], #app input[type="url"],
#app select, #app textarea,
.form-grid input, .form-grid select, .form-grid textarea,
.field-block input, .field-block select, .field-block textarea {
  min-height: 36px;
  padding: 0 10px;
  border: 1px solid var(--rl-line-control);
  border-radius: var(--rl-r-sm);
  background: var(--rl-surface);
  color: var(--rl-text);
  font-size: 15px; font-family: inherit;
  transition: border-color var(--rl-d-instant) var(--rl-ease-standard);
}
#app textarea, .form-grid textarea, .field-block textarea { padding: 8px 10px; line-height: 1.47; }
#app input:not([type]):focus,
#app input:focus, #app select:focus, #app textarea:focus,
.form-grid input:focus, .form-grid select:focus, .form-grid textarea:focus,
.field-block input:focus, .field-block select:focus, .field-block textarea:focus,
.toolbar-search input:focus {
  outline: none;
  border-color: var(--rl-i-600);
  box-shadow: var(--rl-focus);
}
#app input:disabled, #app select:disabled, #app textarea:disabled {
  background: var(--rl-surface-sunk); color: var(--rl-text-disabled);
}
#app input[readonly] { border-color: transparent; background: transparent; padding-left: 0; }
/* Chrome paints its own background and text into an autofilled field and
   ignores `background`. On the dark theme that lands as near-black text on
   Chrome's pale yellow. See the longer note on the sign-in screen. */
#app input:-webkit-autofill,
#app input:-webkit-autofill:hover,
#app input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--rl-text);
  -webkit-box-shadow: inset 0 0 0 1000px var(--rl-surface);
  box-shadow: inset 0 0 0 1000px var(--rl-surface);
  caret-color: var(--rl-text);
  transition: background-color 100000s ease-in-out 0s;
}
#app input.is-error, #app select.is-error, #app textarea.is-error { border-color: var(--rl-r-500); }

/* §12.2 The error's 20px of space is reserved in the layout, so showing one
   shifts nothing. */
.rl-field { display: block; margin-bottom: var(--rl-s-5); }
.rl-field > input, .rl-field > select, .rl-field > textarea { width: 100%; }
.rl-field > label { display: block; font-size: 12.5px; font-weight: 500; letter-spacing: .005em; margin-bottom: 6px; color: var(--rl-text-2); }
/* The legacy micro-label rules (.form-grid label, uppercase) reach an
   .rl-field nested in a .form-grid wrapper, so one dialog mixed "Guest name"
   with "SOLD THROUGH". The field's own label wins. */
#app .form-grid .rl-field > label, .form-grid .rl-field > label { text-transform: none; font-size: 12.5px; font-weight: 500; letter-spacing: .005em; color: var(--rl-text-2); }
.rl-field-help { font-size: 12.5px; line-height: 16px; color: var(--rl-text-3); margin-top: 4px; }
.rl-field-error {
  min-height: 20px; margin-top: 4px;
  font-size: 12.5px; line-height: 16px; color: var(--rl-text-danger);
  display: flex; align-items: center; gap: 5px;
}
.rl-field-error:empty { min-height: 20px; }

/* The old field label was uppercase and tracked out. §22. */
.field label, .field-block > label, .form-grid > label {
  text-transform: none !important; letter-spacing: .005em !important;
  font-size: 12.5px; font-weight: 500; color: var(--rl-text-2);
}

/* §12.1 Forms are single column; two only for genuinely paired fields. */
.rl-form { max-width: 480px; }
.rl-form-row { display: grid; grid-template-columns: 1fr 1fr; gap: var(--rl-s-3); }

/* =========================================================================
   §9.3 / §13 TABLES — the workhorse
   Sentence-case headers, hairline rules, no zebra striping: stripes fight
   with status tints and the rules already do the job.
   ========================================================================= */
#app table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
#app table th {
  text-align: left;
  text-transform: none;
  letter-spacing: .005em;
  font-size: 12.5px; font-weight: 500;
  color: var(--rl-text-2);
  padding: var(--rl-cell-pad);
  border-bottom: 1px solid var(--rl-line-strong);
  background: var(--rl-surface);
  white-space: nowrap;
}
#app table td {
  padding: var(--rl-cell-pad);
  border-bottom: 1px solid var(--rl-line-soft);
  height: var(--rl-row-h);
}
#app table tr:last-child td { border-bottom: none; }
#app table tbody tr:hover td { background: var(--rl-n-25); transition: background var(--rl-d-instant); }
[data-theme="dark"] #app table tbody tr:hover td { background: var(--rl-surface-raised); }
/* No transition on rest, so a fast mouse across a table leaves no trail of
   slowly fading rows. */
#app table tbody tr td { transition: none; }

/* Numeric columns right-aligned and tabular. Priya trusts numbers that are
   aligned and distrusts numbers that aren't. */
#app table td.num, #app table th.num, #app table .money, .mono {
  text-align: right; font-variant-numeric: tabular-nums lining-nums;
}
#app table th.num { text-align: right; }

/* §9.3 A row's status tint is a 3px left edge, not a full-row background —
   a table of fully tinted rows is unreadable. */
#app table tr[data-sev] td:first-child { box-shadow: inset 3px 0 var(--rl-n-300); }
#app table tr[data-sev="warn"] td:first-child { box-shadow: inset 3px 0 var(--rl-a-500); }
#app table tr[data-sev="fail"] td:first-child { box-shadow: inset 3px 0 var(--rl-r-500); }
#app table tr[data-sev="info"] td:first-child { box-shadow: inset 3px 0 var(--rl-i-500); }
#app table tr.is-selected td { background: var(--rl-tint-accent-bg); }
#app table tr.is-selected td:first-child { box-shadow: inset 2px 0 var(--rl-i-600); }

.list-card, #app .list-card {
  background: var(--rl-surface);
  border: 1px solid var(--rl-line);
  border-radius: var(--rl-r-md);
  box-shadow: none;
  /* `clip`, not `hidden`, and the difference is the whole reason sticky
     table headers did not work.

     Both clip to the border radius, which is all this declaration was ever
     for. But `hidden` also makes the box a SCROLL CONTAINER, and a sticky
     descendant sticks to its nearest scroll container — so every sticky
     `thead th` in the product was pinning itself to a card that never
     scrolls, and rode off the top of the window with the rest of the card.
     `clip` clips without becoming a scrollport, so the header now sticks to
     the one real scroll region (§7.1) and stays clipped to its own card,
     which is exactly the intended behaviour.

     .side-list overrides both axes below and is unaffected. */
  overflow: clip;
}
/* A master list is BOTH .list-card and .side-list, and .list-card's clip
   was winning — so the ticket queue, the contact list, contracts,
   compliance and the parcel log all stopped scrolling and silently hid
   everything past 560px. Four thousand pixels of contacts behind a lid.
   The scroll has to be restored explicitly, because the two rules want
   opposite things from the same element and the clip is only there to keep
   the card's corners. */
.side-list, #app .side-list { overflow-y: auto; overflow-x: hidden; }
/* §7.3 Horizontal scroll gets a gradient scrim on the edge with hidden
   content — the one permitted gradient in the product. */
.list-card .scroll-wrap, .scroll-wrap { overflow-x: auto; overflow-y: hidden; }

/* =========================================================================
   §9 CARDS
   Cards do not lift on hover. No translateY, no shadow growth. The border
   darkens, and that is the entire hover state.
   ========================================================================= */
.card, #app .card, .form-card, .property-card, .listing-card, .feed-card,
.event-card, .automation-card, .aging-card {
  background: var(--rl-surface);
  border: 1px solid var(--rl-line);
  border-radius: var(--rl-r-md);
  box-shadow: none;
  padding: var(--rl-card-pad);
  transition: border-color var(--rl-d-instant) var(--rl-ease-standard);
}
a.card:hover, button.card:hover, .card[onclick]:hover,
.property-card[onclick]:hover, .listing-card[onclick]:hover {
  border-color: var(--rl-line-strong);
  transform: none; box-shadow: none;
}
.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--rl-s-3); margin-bottom: var(--rl-s-3); }
.card-head h3 { font-family: var(--rl-font-sans); font-size: 16px; line-height: 22px; font-weight: 600; letter-spacing: -.005em; margin: 0; }
.card-head-note, .head-note { font-size: 12.5px; color: var(--rl-text-3); }

.section-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--rl-s-3); margin: var(--rl-section-gap) 0 var(--rl-s-3); }
.section-head h3 {
  font-family: var(--rl-font-serif); font-size: 21px; line-height: 28px;
  font-weight: 400; letter-spacing: 0; margin: 0;
}

/* =========================================================================
   §9.4 STATUS CHIPS
   22px, 3px radius, 50-stop fill and 700-stop text. Chips carry no icon
   unless the state is failing.
   ========================================================================= */
.badge, #app .badge, .chip-count, .count-pill {
  display: inline-flex; align-items: center; gap: 5px;
  height: 22px; padding: 0 8px;
  border-radius: var(--rl-r-xs);
  font-size: 12.5px; font-weight: 500; letter-spacing: .005em;
  white-space: nowrap;
  background: var(--rl-n-100); color: var(--rl-n-700);
  box-shadow: none;
}
[data-theme="dark"] .badge, [data-theme="dark"] .count-pill, [data-theme="dark"] .chip-count {
  background: #232937; color: #C3C9D6;
}
/* The old chip carried a decorative 6px dot before every label. It said
   nothing the text did not. */
.badge::before, #app .badge::before { content: none; }
/* paid / signed / overdue are emitted by statusBadgeCls() but were only
   styled in the legacy styles.css, which the #app .badge base above
   outranks — so "Overdue" on Payments rendered as a grey pill while the
   same state on Collections was red. Same tint per meaning, everywhere. */
.badge.ready,   #app .badge.ready,
.badge.paid,    #app .badge.paid,
.badge.signed,  #app .badge.signed   { background: var(--rl-tint-green-bg); color: var(--rl-tint-green-fg); }
.badge.occupied,#app .badge.occupied { background: var(--rl-room-occupied-fill); color: var(--rl-room-occupied-text); }
.badge.inspect, #app .badge.inspect,
.badge.pending, #app .badge.pending  { background: var(--rl-tint-amber-bg); color: var(--rl-tint-amber-fg); }
.badge.issue,   #app .badge.issue,
.badge.overdue, #app .badge.overdue  { background: var(--rl-tint-red-bg); color: var(--rl-tint-red-fg); }
.badge.info,    #app .badge.info     { background: var(--rl-tint-accent-bg); color: var(--rl-tint-accent-fg); }
.badge.issue::before, #app .badge.issue::before,
.badge.overdue::before, #app .badge.overdue::before {
  content: ''; width: 12px; height: 12px; flex: none;
  background: currentColor;
  -webkit-mask: center/12px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M12 8v5M12 17h.01'/%3E%3Ccircle cx='12' cy='12' r='9.2'/%3E%3C/svg%3E");
  mask: center/12px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round'%3E%3Cpath d='M12 8v5M12 17h.01'/%3E%3Ccircle cx='12' cy='12' r='9.2'/%3E%3C/svg%3E");
}
.count-pill, .chip-count { background: var(--rl-tint-accent-bg); color: var(--rl-tint-accent-fg); }

/* Filter chips (§13.1) sit above a table and each one is removable. */
.chip, #app .chip {
  height: 26px; padding: 0 9px;
  border-radius: var(--rl-r-xs);
  border: 1px solid var(--rl-line);
  background: var(--rl-surface); color: var(--rl-text-2);
  font-size: 12.5px; font-weight: 400;
  display: inline-flex; align-items: center; gap: 6px;
  cursor: pointer;
  transition: border-color var(--rl-d-instant), background var(--rl-d-instant), color var(--rl-d-instant);
}
.chip:hover, #app .chip:hover { border-color: var(--rl-line-strong); color: var(--rl-text); }
.chip.active, #app .chip.active {
  background: var(--rl-tint-accent-bg); color: var(--rl-tint-accent-fg);
  border-color: transparent; font-weight: 500;
}

/* =========================================================================
   §9.5 ROOM CELL — the atomic unit of the plan view
   Colour per §3.2. Available is an outline; out of service also carries a
   45° hatch so the state survives greyscale print and colour-blind viewing.
   ========================================================================= */
.rl-room {
  width: var(--rl-room-cell); height: var(--rl-room-cell);
  border-radius: var(--rl-r-xs);
  border: 1px solid var(--rl-room-occupied-line);
  background: var(--rl-room-occupied-fill);
  color: var(--rl-room-occupied-text);
  font-size: 12.5px; font-weight: 500; line-height: 1;
  display: grid; place-items: center;
  cursor: pointer; position: relative; padding: 0;
  font-variant-numeric: tabular-nums;
  transition: transform var(--rl-d-fast) var(--rl-ease-standard),
              border-color var(--rl-d-fast) var(--rl-ease-standard),
              background var(--rl-d-fast) var(--rl-ease-standard),
              opacity var(--rl-d-fast) var(--rl-ease-standard);
}
.rl-room:hover { border-color: var(--rl-line-strong); transform: scale(1.04); z-index: 2; }
.rl-room:active { transform: scale(1); }
.rl-room:focus-visible { outline: none; box-shadow: var(--rl-focus); z-index: 3; }
.rl-room[data-state="reserved"]  { background: var(--rl-room-reserved-fill); border-color: var(--rl-room-reserved-line); color: var(--rl-room-reserved-text); }
.rl-room[data-state="available"] { background: var(--rl-room-available-fill); border: 1px dashed var(--rl-room-available-line); color: var(--rl-room-available-text); }
.rl-room[data-state="turn"]      { background: var(--rl-room-turn-fill); border-color: var(--rl-room-turn-line); color: var(--rl-room-turn-text); }
.rl-room[data-state="oos"] {
  background: var(--rl-room-oos-fill); border-color: var(--rl-room-oos-line); color: var(--rl-room-oos-text);
  background-image: repeating-linear-gradient(45deg,
    var(--rl-room-oos-line) 0, var(--rl-room-oos-line) 1px, transparent 1px, transparent 4px);
}
.rl-room[data-state="notice"]::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 3px;
  background: var(--rl-room-notice-edge); border-radius: 0 0 2px 2px;
}
/* Filtering dims rather than removes, so the spatial layout is preserved. */
.rl-room[data-dim="true"] { opacity: .2; }
.rl-plan-floor { display: flex; align-items: flex-start; gap: var(--rl-s-4); padding: var(--rl-s-3) 0; border-bottom: 1px solid var(--rl-line-soft); }
.rl-plan-label { width: 96px; flex: none; }
.rl-plan-cells { display: flex; flex-wrap: wrap; gap: 5px; }

/* The pre-spec room tile: same palette, no gradient, no 14px radius. */
.room-tile, #app .room-tile {
  border-radius: var(--rl-r-sm);
  border: 1px solid var(--rl-line);
  background: var(--rl-surface);
  padding: 10px;
  box-shadow: none;
}
.room-tile .rn { font-family: var(--rl-font-mono); font-weight: 400; font-size: 13px; }
.room-tile .rb { font-size: 12.5px; color: var(--rl-text-3); }

/* =========================================================================
   §9.6 METRIC BLOCK
   Not a card. A label, a figure, an optional delta, no border and no
   background. Cards for metrics create the identical-rounded-rectangle
   problem this whole design is trying to avoid. The hairline rules between
   columns are the structure; there are no boxes.
   ========================================================================= */
.rl-metrics {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--rl-s-8);
  margin: var(--rl-s-6) 0;
}
/* Rules only while it is one row — see the note on the KPI grids in
   screens.css. Below 1024px this stays a plain gapped grid. */
@media (min-width: 1024px) {
  .rl-metrics { gap: 0; }
  .rl-metrics > * { padding-right: var(--rl-s-8); }
  .rl-metrics > * + * { border-left: 1px solid var(--rl-line-soft); padding-left: var(--rl-s-8); }
}
.rl-metric-label { font-size: 12.5px; letter-spacing: .005em; color: var(--rl-text-2); }
.rl-metric-value {
  font-family: var(--rl-font-serif); font-size: 32px; line-height: 36px;
  font-weight: 400; letter-spacing: -.01em; color: var(--rl-text);
  font-variant-numeric: tabular-nums lining-nums;
  margin: 4px 0 2px; display: block;
}
/* The figure stays --rl-text regardless of direction. Colouring the number
   green because occupancy rose makes every screen a traffic light. */
.rl-metric-delta { font-size: 12.5px; line-height: 16px; color: var(--rl-text-3); display: flex; align-items: center; gap: 4px; }
.rl-metric-delta::before {
  content: ''; width: 0; height: 0; flex: none;
  border-left: 5px solid transparent; border-right: 5px solid transparent;
}
.rl-metric-delta[data-dir="up"]::before   { border-bottom: 8px solid var(--rl-g-500); }
.rl-metric-delta[data-dir="down"]::before { border-top: 8px solid var(--rl-r-500); }
.rl-metric-delta[data-dir="flat"]::before { border: none; width: 8px; height: 2px; background: var(--rl-n-400); }

/* The pre-spec dashboard tiles, re-tuned to the same idea: one big thing per
   screen, everything else small and quiet. */
.dash-hero .hero-metric, .attention-row .att-tile, .today-strip .today-tile {
  background: transparent; border: none; box-shadow: none; border-radius: 0;
}
.dash-hero .hero-value { font-family: var(--rl-font-serif); font-weight: 400; font-variant-numeric: tabular-nums; }
.hero-label, .att-label, .tt-label { text-transform: none !important; letter-spacing: .005em !important; font-size: 12.5px; color: var(--rl-text-2); }
.stat-card .stat-label { text-transform: none !important; letter-spacing: .005em !important; font-weight: 500; font-size: 12.5px; }
.stat-card .stat-value { font-family: var(--rl-font-serif); font-weight: 400; font-variant-numeric: tabular-nums; }

/* =========================================================================
   §9.7 EMPTY STATES
   Written as an invitation, never an apology. Not every empty screen is a
   deficiency — "Everyone's paid up" is a good one and should feel like it.
   ========================================================================= */
.rl-empty, .empty-note, #app .empty-note {
  text-align: center; max-width: 380px; margin: 0 auto;
  padding: var(--rl-s-16) var(--rl-s-4);
  color: var(--rl-text-2);
}
.rl-empty-glyph { color: var(--rl-n-300); margin-bottom: var(--rl-s-3); }
.rl-empty-glyph svg { width: 20px; height: 20px; }
.rl-empty h4 { font-family: var(--rl-font-serif); font-size: 21px; line-height: 28px; font-weight: 400; margin: 0 0 4px; color: var(--rl-text); }
.rl-empty p { font-size: 13.5px; line-height: 19px; margin: 0 0 var(--rl-s-4); color: var(--rl-text-2); }

/* =========================================================================
   §11.1 SKELETONS
   A static block, no shimmer. Shimmer is decoration on a wait. Dimensions
   match the real content within 4px so there is no reflow on arrival, and
   nothing re-runs its enter animation when real data replaces one.
   ========================================================================= */
.rl-skeleton { background: var(--rl-n-100); border-radius: var(--rl-r-xs); height: 14px; }
[data-theme="dark"] .rl-skeleton { background: #232937; }
.rl-skeleton-row { height: var(--rl-row-h); border-bottom: 1px solid var(--rl-line-soft); display: flex; align-items: center; padding: 0 16px; gap: 16px; }
.rl-skeleton-row > .rl-skeleton { flex: 1; }

/* =========================================================================
   §8.5 DIALOGS + §8.4 DRAWERS
   Everything that can be a drawer should be a drawer. Dialogs are reserved
   for destructive confirmations, blocking flows, and the command palette.
   A dialog scales from 0.97, not 0.9: zooming in from small feels like a
   mobile app from 2014.
   ========================================================================= */
/* A CLOSED LAYER IS DISPLAY:NONE. Not transparent, not pointer-events:none —
   gone.

   This rule exists because of a bug it should have prevented. The shortcuts
   dialog was left as `display: grid` at all times and gated only by
   `pointer-events: none` on its container. But pointer-events is not
   inherited in the way that reasoning assumes: a CHILD that sets
   `pointer-events: auto` still receives events through a `none` parent. Its
   scrim did exactly that, so a full-viewport element at z-index 70 sat over
   the entire application, taking every click and tinting every pixel by 32%.
   Nothing in the app was clickable, and the whole product looked slightly
   washed out — which reads as a palette problem rather than a dead overlay,
   which is why it survived a visual review.

   `display: none` on the closed state makes the layer's own children
   irrelevant. That is the invariant: only the open state is laid out. */
.rl-scrim {
  position: fixed; inset: 0; z-index: 70;
  background: var(--rl-overlay);
  opacity: 0; pointer-events: none;
  transition: opacity var(--rl-d-fast) linear;
}
/* A scrim belongs to its layer. It is open when that layer is open — never
   on its own say-so, which is what made the markup's hardcoded attribute
   able to strand it visible. */
.rl-dialog[data-open="true"] > .rl-scrim,
.rl-drawer-host[data-open="true"] > .rl-scrim { opacity: 1; pointer-events: auto; }

.rl-dialog { position: fixed; inset: 0; z-index: 80; display: none; }
.rl-dialog[data-open="true"] { display: grid; }
.rl-dialog-panel {
  background: var(--rl-surface-raised);
  border-radius: var(--rl-r-lg);
  box-shadow: var(--rl-e-3);
  width: 100%; max-width: 560px;
  max-height: 80vh; overflow: hidden;
  display: flex; flex-direction: column;
  opacity: 0; transform: scale(.97) translateY(8px);
  transition: opacity var(--rl-d-instant) var(--rl-ease-exit),
              transform var(--rl-d-fast) var(--rl-ease-exit);
}
.rl-dialog[data-open="true"] .rl-dialog-panel {
  opacity: 1; transform: none;
  transition: opacity var(--rl-d-fast) linear,
              transform var(--rl-d-base) var(--rl-ease-enter);
}
.rl-dialog-head { padding: var(--rl-s-5) var(--rl-s-6) 0; }
.rl-dialog-head h3 { font-family: var(--rl-font-serif); font-size: 21px; line-height: 28px; font-weight: 400; margin: 0; }
.rl-dialog-body { padding: var(--rl-s-4) var(--rl-s-6) var(--rl-s-6); overflow-y: auto; }

/* The pre-spec modal, brought onto the same footing. */
/* z-index 210, not 80. The contract and listing wizards are full-screen
   layers at 200 (styles.css .cw-overlay) and open dialogs of their own —
   the contract preview, the template editor — which rendered BEHIND the
   wizard that opened them: the button did nothing anybody could see.
   Order now: wizard 200 < dialog 210 < palette 220 < confirm 230 < toasts 240. */
#appModal { position: fixed; inset: 0; z-index: 210; display: none; align-items: center; justify-content: center; background: var(--rl-overlay); padding: var(--rl-s-4); }
.modal-box, #app .modal-box {
  background: var(--rl-surface-raised);
  border-radius: var(--rl-r-lg);
  box-shadow: var(--rl-e-3);
  padding: var(--rl-s-6);
  max-width: 560px;
}
.modal-box h3, #app .modal-box h3 {
  font-family: var(--rl-font-serif); font-size: 21px; line-height: 28px;
  font-weight: 400; margin: 0 0 var(--rl-s-4);
}
.modal-scrim, #app .modal-scrim { background: var(--rl-overlay); }

/* §8.4 Drawers slide from the right, full height. The panel arrives with its
   content already composed; drawer content does not stagger in. */
.rl-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 80;
  width: min(520px, 100vw);
  background: var(--rl-surface);
  border-left: 1px solid var(--rl-line);
  display: flex; flex-direction: column;
  transform: translateX(100%); opacity: .6;
  visibility: hidden; pointer-events: none;
  transition: transform var(--rl-d-base) var(--rl-ease-exit),
              opacity var(--rl-d-fast) linear,
              visibility 0s linear var(--rl-d-base);
}
.rl-drawer[data-width="wide"] { width: min(720px, 100vw); }
.rl-drawer[data-open="true"] {
  transform: none; opacity: 1;
  visibility: visible; pointer-events: auto;
  transition: transform var(--rl-d-mid) var(--rl-ease-enter),
              opacity var(--rl-d-fast) linear,
              visibility 0s;
}
.rl-drawer-body { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: none; padding: var(--rl-s-6); }

/* =========================================================================
   §8.10 POPOVERS AND MENUS
   opacity + scale + a 4px rise, origin at the trigger edge.
   ========================================================================= */
.rl-pop {
  position: absolute; z-index: 60; min-width: 200px;
  background: var(--rl-surface-raised);
  border-radius: var(--rl-r-lg);
  box-shadow: var(--rl-e-2);
  padding: 5px;
  opacity: 0; transform: scale(.98) translateY(-4px);
  transform-origin: top left;
  pointer-events: none;
  transition: opacity var(--rl-d-instant) var(--rl-ease-exit),
              transform var(--rl-d-instant) var(--rl-ease-exit);
}
.rl-pop[data-open="true"] {
  opacity: 1; transform: none; pointer-events: auto;
  transition: opacity var(--rl-d-base) var(--rl-ease-enter),
              transform var(--rl-d-base) var(--rl-ease-enter);
}
.rl-menu-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 9px; border: none; background: transparent;
  border-radius: var(--rl-r-sm);
  font-size: 13.5px; color: var(--rl-text); text-align: left; cursor: pointer;
  transition: background var(--rl-d-instant) var(--rl-ease-standard);
}
.rl-menu-item:hover { background: var(--rl-n-50); }
[data-theme="dark"] .rl-menu-item:hover { background: var(--rl-surface); }
.rl-menu-item:focus-visible { outline: none; box-shadow: var(--rl-focus-inset); }
.rl-menu-item.is-current { color: var(--rl-text-accent); font-weight: 500; }
/* The one entry in a New menu that creates the thing everything else on
   the record hangs off. Weighted, and separated from the list below it,
   so it does not read as the eleventh of eleven equal choices. */
.rl-menu-item.is-primary { font-weight: 600; }
.rl-menu-tick { margin-left: auto; }
.rl-menu-label { padding: 8px 9px 4px; font-size: 12.5px; font-weight: 500; color: var(--rl-text-3); }
.rl-menu-sep { height: 1px; background: var(--rl-line-soft); margin: 4px 0; }
.rl-menu-empty { padding: 10px; font-size: 13px; color: var(--rl-text-3); }

/* A segmented control's selected pill slides; the panel underneath does not. */
.rl-seg { display: inline-flex; padding: 2px; gap: 2px; background: var(--rl-surface-sunk); border-radius: var(--rl-r-sm); }
.rl-seg button {
  border: none; background: transparent; border-radius: 3px;
  padding: 5px 11px; font-size: 13px; color: var(--rl-text-2); cursor: pointer;
  transition: background var(--rl-d-fast) var(--rl-ease-standard), color var(--rl-d-fast);
}
.rl-seg button[aria-pressed="true"], .rl-seg button.active {
  background: var(--rl-surface); color: var(--rl-text); font-weight: 500; box-shadow: var(--rl-e-1);
}

/* =========================================================================
   §14 COMMAND PALETTE
   620px, 20vh from the top. The selection highlight snaps: at typing speed
   a sliding highlight lags behind the selection.
   ========================================================================= */
.rl-palette { position: fixed; inset: 0; z-index: 220; display: none; }
.rl-palette[data-open="true"] { display: block; }
.rl-palette-scrim { position: absolute; inset: 0; background: var(--rl-overlay); }
.rl-palette-panel {
  position: relative; margin: 20vh auto 0; width: min(620px, calc(100vw - 32px));
  background: var(--rl-surface-raised);
  border-radius: var(--rl-r-lg);
  box-shadow: var(--rl-e-3);
  overflow: hidden;
  opacity: 0; transform: scale(.97) translateY(8px);
  transition: opacity var(--rl-d-fast) linear, transform var(--rl-d-fast) var(--rl-ease-enter);
}
.rl-palette[data-open="true"] .rl-palette-panel { opacity: 1; transform: none; }
.rl-palette input {
  width: 100%; border: none; outline: none; background: transparent;
  padding: 16px 18px; font-size: 17px; color: var(--rl-text);
  border-bottom: 1px solid var(--rl-line-soft);
}
.rl-palette input::placeholder { color: var(--rl-text-3); }
.rl-pal-list { max-height: min(52vh, 420px); overflow-y: auto; padding: 6px; }
.rl-pal-group { padding: 9px 10px 4px; font-size: 12.5px; font-weight: 500; color: var(--rl-text-3); }
.rl-pal-row {
  display: flex; align-items: baseline; gap: 10px; width: 100%;
  padding: 8px 10px; border: none; background: transparent;
  border-radius: var(--rl-r-sm); cursor: pointer; text-align: left;
}
.rl-pal-row.is-active { background: var(--rl-tint-accent-bg); }
.rl-pal-title { font-size: 13.5px; color: var(--rl-text); }
.rl-pal-sub { font-size: 12.5px; color: var(--rl-text-3); margin-left: auto; }
.rl-pal-mark { background: var(--rl-a-100); color: inherit; border-radius: 2px; }
[data-theme="dark"] .rl-pal-mark { background: var(--rl-a-900); color: var(--rl-a-200); }
.rl-pal-empty { padding: var(--rl-s-8) var(--rl-s-4); text-align: center; }
.rl-keys { margin: 0; }
.rl-key-row { display: flex; align-items: center; gap: var(--rl-s-3); padding: 7px 0; border-bottom: 1px solid var(--rl-line-soft); }
.rl-key-row dt { flex: none; width: 96px; margin: 0; }
.rl-key-row dd { margin: 0; font-size: 13.5px; color: var(--rl-text-2); }

/* =========================================================================
   §15.1 TOASTS
   Bottom-right, stacking upward, three at a time. Do not toast successes
   the user can already see.
   ========================================================================= */
.rl-toasts { position: fixed; right: var(--rl-s-6); bottom: var(--rl-s-6); z-index: 240; display: flex; flex-direction: column-reverse; gap: var(--rl-s-2); }
.rl-toast {
  width: 380px; max-width: calc(100vw - 32px);
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 14px;
  background: var(--rl-surface-raised);
  border-radius: var(--rl-r-md);
  box-shadow: var(--rl-e-2);
  font-size: 13.5px; color: var(--rl-text);
  opacity: 0; transform: translateY(12px);
  transition: opacity var(--rl-d-base) var(--rl-ease-enter), transform var(--rl-d-base) var(--rl-ease-enter);
}
.rl-toast[data-open="true"] { opacity: 1; transform: none; }
.rl-toast[data-leaving="true"] {
  opacity: 0; transform: translateX(12px);
  transition: opacity var(--rl-d-fast) var(--rl-ease-exit), transform var(--rl-d-fast) var(--rl-ease-exit);
}
.rl-toast-glyph { width: 16px; height: 16px; flex: none; margin-top: 2px; }
.rl-toast[data-kind="error"] .rl-toast-glyph { color: var(--rl-text-danger); }
.rl-toast[data-kind="success"] .rl-toast-glyph { color: var(--rl-text-success); }

/* =========================================================================
   §8.8 OPTIMISTIC STATE
   In flight is a state, not a spinner: a 2px left border and 70% opacity.
   ========================================================================= */
[data-inflight="true"] { opacity: .7; box-shadow: inset 2px 0 var(--rl-i-300); }
/* §8.6 / §15.3 A realtime change marks itself with a tint that fades. */
@keyframes rl-touch { from { background-color: var(--rl-tint-accent-bg); } to { background-color: transparent; } }
[data-touched="true"] { animation: rl-touch var(--rl-d-slow) var(--rl-ease-standard) 1.5s forwards; }

/* =========================================================================
   §8.10 MICRO-INTERACTIONS
   Overshoot easing appears on exactly three things: the checkbox tick, the
   toggle knob, and the success tick in a toast. Nowhere else.
   ========================================================================= */
#app input[type="checkbox"], #app input[type="radio"] {
  accent-color: var(--rl-i-600);
  width: 16px; height: 16px; min-height: 0;
  transition: transform var(--rl-d-fast) var(--rl-ease-overshoot);
}
#app input[type="checkbox"]:checked, #app input[type="radio"]:checked { transform: scale(1.05); }

/* =========================================================================
   CORRECTIONS TO THE PRE-SPEC SHEET (§22)
   ========================================================================= */
/* Decorative gradients. The only permitted gradient is the scroll scrim.
   The sign-in screen is no longer patched from here — styles.css owns it
   outright now, in tokens and in both themes, rather than being overruled
   with !important from two files at once. */
.listing-photo, #app .listing-photo, .brand-mark { background-image: none !important; }
.listing-photo { background: var(--rl-surface-sunk); color: var(--rl-text-3); }
#app .listing-photo { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; font-family: inherit; font-size: 12px; letter-spacing: 0; overflow: hidden; }
#app .listing-photo.has-img { padding: 0; }
#app .listing-photo img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Kanban cards do not lift. */
.kanban-card, #app .kanban-card {
  background: var(--rl-surface); border: 1px solid var(--rl-line);
  border-radius: var(--rl-r-sm); box-shadow: none;
  transition: border-color var(--rl-d-instant) var(--rl-ease-standard);
}
.kanban-card:hover, #app .kanban-card:hover { transform: none; box-shadow: none; border-color: var(--rl-line-strong); }
.kanban-col, #app .kanban-col {
  background: var(--rl-surface-sunk); border: 1px solid var(--rl-line);
  border-radius: var(--rl-r-md); box-shadow: none; padding: var(--rl-s-3);
}
.kanban-card[data-drag="true"] { box-shadow: var(--rl-e-2); transform: scale(1.02) rotate(1deg); }
.kanban-col[data-over="true"] { background: var(--rl-tint-accent-bg); }

/* Serif and mono are named by token, so the two families the old sheet
   hardcoded by name resolve to the new ones everywhere at once. */
.display, .stat-value, .view-title, h1, h2 { font-family: var(--rl-font-serif); }
[style*="Fraunces"] { font-family: var(--rl-font-serif) !important; }
[style*="JetBrains"] { font-family: var(--rl-font-mono) !important; }

/* Mono is for reference codes only, never decoration on a label. */
.mono, #app .mono { font-family: var(--rl-font-mono); font-size: 13px; }

/* Uppercase tracked-out eyebrows, wherever the old sheet set them. */
.brand-tag, .hero-label, .att-label, .tt-label, .stat-label, .cal-legend-item,
.acct-head-role, .kpi-label, .filter-label {
  text-transform: none !important;
  letter-spacing: .005em !important;
}

/* §7.3 Vertical scroll containers never get a shadow; they get a border. */
.side-list, .scroll-wrap { box-shadow: none; }

/* The Netlify attribution badge does not belong in production. */
#netlify-badge, .netlify-badge, [class*="netlify-badge"] { display: none !important; }

/* =========================================================================
   §20 SENTENCE CASE, EVERYWHERE
   Buttons, headings, labels, menu items, table headers. Title Case only for
   proper nouns; ALL-CAPS nowhere at all (§22).

   The pre-spec sheet set text-transform: uppercase in thirty-five places,
   almost always paired with 0.04–0.09em of tracking — the eyebrow-label
   habit. Each of those selectors is named here rather than swept up by a
   blanket rule, so the tracking can come back down to the scale's own
   0.005em at the same time: a sentence-case label carrying caps tracking
   reads as spaced-out rather than as a label.
   ========================================================================= */
#app .aging-card span, #app .al-now, #app .automation-stat span, #app .brand-tag,
#app .cal-day-cell .dow, #app .cal-label-col, #app .cd-meta-grid .m-label,
#app .cd-section-title, #app .chat-day, #app .cl-checkin th, #app .cl-deadline .m-label,
#app .contact-info-grid .m-label, #app .cs-sched th, #app .ct-lp-b, #app .cw-util-head,
#app .field label, #app .form-grid label, #app .field-block label, #app .gov-duties li b,
#app .gov-fields label, #app .gov-stat span, #app .hazard-cat, #app .hazard-tag,
#app .hero-label, #app .hz-flag, #app .hz-label, #app .kanban-col-head h4,
#app .line-items-table th, #app .lw-media-first, #app .pace-controls label,
#app .pc-stat span, #app .rc-stage-when, #app .stat-card .stat-label,
#app .task-group-head, #app table th,
#loginScreen .field label {
  text-transform: none;
  letter-spacing: .005em;
  font-weight: 500;
}
/* A last line of defence for anything that sets it inline or in a rule this
   list has not caught. Nothing in RoomLine is set in capitals. */
#app [style*="uppercase"] { text-transform: none !important; }

/* Reference codes stay mono but are not shouted either. */
#app .mono, #app .rl-mono { text-transform: none; letter-spacing: 0; }

/* =========================================================================
   §9.3 ROW ACTIONS
   "Row actions appear on hover and on focus-within, as ghost icon buttons."
   The pre-spec sheet's default .btn is the filled accent, so a table of
   fifty rows rendered fifty filled indigo buttons — the loudest objects on
   a screen whose actual subject is the money in the leftmost columns. A
   button inside a row is a row action; it is never the primary action of
   the screen.
   ========================================================================= */
#app td .btn, #app td .btn.small,
#app .side-item .btn, #app .kanban-card .btn {
  background: var(--rl-surface);
  color: var(--rl-text);
  border-color: var(--rl-line-control);
}
#app td .btn:hover, #app td .btn.small:hover,
#app .side-item .btn:hover, #app .kanban-card .btn:hover {
  background: var(--rl-n-50); color: var(--rl-text);
}
[data-theme="dark"] #app td .btn:hover { background: var(--rl-surface-raised); }
#app td .btn.danger, #app td .btn.small.danger { color: var(--rl-text-danger); border-color: var(--rl-line-danger); }
#app td .btn.danger:hover { background: var(--rl-tint-red-bg); }

/* §10.6 AGEING BUCKETS
   A segmented filter with counts, not four floating cards. Severity is the
   left edge; the figure is the content. Four identical rounded rectangles
   with coloured top rules is the pattern §9.6 exists to avoid. */
.aging-strip, #app .aging-strip {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 0; margin-bottom: var(--rl-s-6);
  border: 1px solid var(--rl-line); border-radius: var(--rl-r-md);
  overflow: hidden; background: var(--rl-surface);
}
.aging-card, #app .aging-card {
  background: transparent; border: none; border-radius: 0; box-shadow: none;
  border-left: 1px solid var(--rl-line-soft);
  padding: var(--rl-s-3) var(--rl-s-4);
  text-align: left;
  transition: background var(--rl-d-instant) var(--rl-ease-standard);
}
.aging-strip > .aging-card:first-child { border-left: none; }
.aging-card:hover, #app .aging-card:hover { background: var(--rl-n-25); border-color: var(--rl-line-soft); }
[data-theme="dark"] .aging-card:hover { background: var(--rl-surface-raised); }
.aging-card b, #app .aging-card b {
  font-family: var(--rl-font-serif); font-weight: 400; font-size: 32px; line-height: 36px;
  font-variant-numeric: tabular-nums; display: block;
}
.aging-card span, #app .aging-card span { font-size: 12.5px; color: var(--rl-text-2); }
/* Severity as a 3px left edge, the same encoding tables use (§9.3). */
.aging-card.b0,  #app .aging-card.b0  { border-top: none; box-shadow: inset 3px 0 var(--rl-g-500); }
.aging-card.b30, #app .aging-card.b30 { border-top: none; box-shadow: inset 3px 0 var(--rl-a-500); }
.aging-card.b60, #app .aging-card.b60 { border-top: none; box-shadow: inset 3px 0 var(--rl-a-600); }
.aging-card.b90, #app .aging-card.b90 { border-top: none; box-shadow: inset 3px 0 var(--rl-r-500); }
@media (max-width: 700px) { .aging-strip, #app .aging-strip { grid-template-columns: repeat(2, 1fr); } }

/* §9.7's 64px of vertical padding is right for a table or a panel that owns
   the screen. In a 340px side list it is most of the list. */
.side-list .empty-note, .rl-pop .empty-note, .notif-list .empty-note,
.chat-body .empty-note, td .empty-note ~ .empty-note {
  padding: var(--rl-s-5) var(--rl-s-3);
  font-size: 13px;
}
/* The dashed well (styles.css) is for an empty state standing on the page.
   Inside something that already has an edge — a card, a detail panel, a
   table cell — it drew a box inside a box (Refunds, Tickets' activity,
   Stripe, Listings). There the text is enough. */
#app .card .empty-note, #app .listing-card .empty-note, #app td .empty-note {
  border: 0; background: none;
}
#app .contracts-list .empty-note { padding: var(--rl-s-8) var(--rl-s-4); font-size: 13px; }

/* =========================================================================
   §13 TABLE BEHAVIOUR
   The visual half of js/table.js.
   ========================================================================= */

/* Sortable headers. The chevron only appears on the sorted column — a column
   of grey arrows on every header is noise on a screen whose subject is the
   figures underneath them. */
#app table thead th[role="columnheader"] {
  cursor: pointer;
  user-select: none;
  position: relative;
  padding-right: 22px;
}
#app table thead th[role="columnheader"]:hover { color: var(--rl-text); }
#app table thead th[role="columnheader"]:focus-visible { outline: none; box-shadow: var(--rl-focus-inset); }
#app table thead th[data-rl-sorted]:not([data-rl-sorted=""]) { color: var(--rl-text); font-weight: 500; }
#app table thead th[data-rl-sorted]:not([data-rl-sorted=""])::after {
  content: '';
  position: absolute; right: 8px; top: 50%;
  width: 0; height: 0;
  border-left: 4px solid transparent; border-right: 4px solid transparent;
}
#app table thead th[data-rl-sorted="asc"]::after  { border-bottom: 5px solid var(--rl-text-2); margin-top: -3px; }
#app table thead th[data-rl-sorted="desc"]::after { border-top: 5px solid var(--rl-text-2); margin-top: -2px; }
#app table th.num, #app table td.num {
  text-align: right;
  font-variant-numeric: tabular-nums lining-nums;
}
#app table th.rl-col-actions, #app table td.rl-col-actions { text-align: right; white-space: nowrap; }

/* §7.2 The header row sticks under whatever is above it inside the scroll
   region — the tab bar, or nothing. rlSyncStickyTop() measures it into
   --rl-sticky-top. This only engages while the wrapper is NOT a horizontal
   scroll container — see the note in js/table.js on why the two are
   mutually exclusive.

   THE `#app` IS LOAD-BEARING. Without it this selector is (0,2,3) and loses
   `position` to `#app table thead th[role="columnheader"]` at (1,1,3) —
   which sets `position: relative` a hundred lines above, to anchor the sort
   arrow. `top` is not a contested property, so it applied from here while
   `position` came from there: every SORTABLE header in a sticky table was
   relatively positioned and shoved `top: ~101px` DOWN, landing in the
   middle of its own tbody. It has been that way since the sticky rule was
   added, hidden only because the old 171px page header pushed the
   displaced row off-screen. `position: sticky` establishes a containing
   block just as `relative` does, so the sort arrow is unaffected. */
.scroll-wrap[data-hscroll="false"] { overflow: visible; }
#app .scroll-wrap[data-sticky="true"] table thead th {
  position: sticky;
  top: var(--rl-sticky-top, 0px);
  z-index: 20;
  box-shadow: 0 1px 0 var(--rl-line-strong);
}
.scroll-wrap[data-hscroll="true"] { overflow-x: auto; overflow-y: hidden; }

/* §7.3 A horizontal scroll container gets a 24px gradient scrim on whichever
   edge has hidden content — the one permitted gradient in the product. */
.scroll-wrap[data-hscroll="true"] { position: relative; }
.scroll-wrap[data-edge="right"]::after,
.scroll-wrap[data-edge="both"]::after,
.scroll-wrap[data-edge="left"]::before,
.scroll-wrap[data-edge="both"]::before {
  content: ''; position: sticky; top: 0; z-index: 21;
  display: block; width: 24px; height: 100%;
  pointer-events: none;
}
.scroll-wrap[data-edge="left"]::before, .scroll-wrap[data-edge="both"]::before {
  float: left; margin-right: -24px;
  background: linear-gradient(to right, rgba(20,23,31,.06), transparent);
}
.scroll-wrap[data-edge="right"]::after, .scroll-wrap[data-edge="both"]::after {
  float: right; margin-left: -24px;
  background: linear-gradient(to left, rgba(20,23,31,.06), transparent);
}
[data-theme="dark"] .scroll-wrap[data-edge="left"]::before,
[data-theme="dark"] .scroll-wrap[data-edge="both"]::before {
  background: linear-gradient(to right, rgba(0,0,0,.35), transparent);
}
[data-theme="dark"] .scroll-wrap[data-edge="right"]::after,
[data-theme="dark"] .scroll-wrap[data-edge="both"]::after {
  background: linear-gradient(to left, rgba(0,0,0,.35), transparent);
}

/* Keyboard row focus (§13.1). */
#app table tbody tr:focus-visible {
  outline: none;
  box-shadow: var(--rl-focus-inset);
}

/* The pager. Sentence case, no arrows, and the size picker only shows up
   when there is more than one page worth of rows to size (§7.5). */
.pager, #app .pager { display: flex; align-items: center; gap: var(--rl-s-2); justify-content: flex-end; padding: var(--rl-s-3) var(--rl-s-4); }
.pager-info { font-size: 12.5px; color: var(--rl-text-3); margin-right: auto; font-variant-numeric: tabular-nums; }
.pager-size { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--rl-text-3); }
.pager-size select { min-height: 30px; height: 30px; padding: 0 6px; font-size: 12.5px; }

/* =========================================================================
   §16.2 TABLES BECOME CARD LISTS
   "Never horizontal-scroll a table on a phone." Each row becomes a card: the
   first column is the primary line, everything else is a labelled line under
   it, and the actions column sits at the foot. The labels come from the
   column headers, stamped onto each cell by js/table.js — so this works on
   every table in the product without any of them being rewritten.
   ========================================================================= */
@media (max-width: 719px) {
  #app .scroll-wrap { overflow: visible !important; }
  #app .scroll-wrap[data-edge]::before, #app .scroll-wrap[data-edge]::after { display: none; }
  #app table, #app table tbody, #app table tr, #app table td { display: block; width: auto; }
  #app table thead { display: none; }
  #app table tr {
    border: 1px solid var(--rl-line);
    border-radius: var(--rl-r-md);
    background: var(--rl-surface);
    padding: var(--rl-s-3);
    margin-bottom: var(--rl-s-2);
  }
  #app table td {
    border: none; height: auto; padding: 2px 0;
    display: flex; align-items: baseline; gap: var(--rl-s-3);
    text-align: left !important;
    min-height: 0;
  }
  #app table td::before {
    content: attr(data-label);
    flex: none; width: 40%; max-width: 140px;
    font-size: 12.5px; color: var(--rl-text-3);
  }
  /* The first cell is the card's title, so it loses its label and gains
     weight — a card headed "Resident  Yara Sorensen" reads worse than one
     headed "Yara Sorensen". */
  #app table td:first-child {
    font-size: 15px; font-weight: 500; color: var(--rl-text);
    margin-bottom: 4px;
  }
  #app table td:first-child::before { display: none; }
  #app table td.rl-col-actions {
    justify-content: flex-end; gap: var(--rl-s-2);
    margin-top: var(--rl-s-2); padding-top: var(--rl-s-2);
    border-top: 1px solid var(--rl-line-soft);
  }
  #app table td.rl-col-actions::before { display: none; }
  #app table td:empty { display: none; }
  /* §16.2 tap targets. */
  #app table td .btn { min-height: 44px; }
}

/* =========================================================================
   §8.9 / §10.3 THE PIPELINE BOARD
   ========================================================================= */
.kanban-col { min-width: 220px; transition: background var(--rl-d-instant) var(--rl-ease-standard); }
.kanban-col-head { display: flex; align-items: center; justify-content: space-between; gap: var(--rl-s-2); margin-bottom: var(--rl-s-2); }
.kanban-col-head h4 { font-size: 13.5px; font-weight: 500; margin: 0; color: var(--rl-text); }

/* Over a valid target the column tints and an insertion line appears at the
   foot of it. Over an invalid one, nothing tints and the cursor says so —
   §8.9 is explicit that an illegal move is blocked, not silently reverted. */
.kanban-col[data-over="true"] { background: var(--rl-tint-accent-bg); }
.kanban-col[data-over="true"]::after {
  content: ''; display: block; height: 2px; margin-top: 6px;
  background: var(--rl-i-300); border-radius: 1px;
}
.kanban-col[data-invalid="true"] { background: var(--rl-surface-sunk); }
body.rl-dragging { cursor: grabbing; user-select: none; }
body.rl-drag-invalid { cursor: not-allowed; }

.kanban-card { cursor: grab; position: relative; }
.kanban-card:focus-visible { outline: none; box-shadow: var(--rl-focus); }
/* §10.3: a card past the days-in-stage threshold gains a 3px amber edge.
   It used to say "no contact 14d+" in red inside the meta line, which is a
   sentence where a mark would do. */
/* #app-scoped because the corrective rule further up sets box-shadow:none
   at that specificity, and a severity edge that loses to a reset is a
   severity edge nobody ever sees. */
#app .kanban-card.is-stale { box-shadow: inset 3px 0 var(--rl-a-500); }

/* Lifted by pointer: e-2, 1.02 and a degree of rotation, per §8.9. */
#app .kanban-card.is-dragging {
  position: fixed; z-index: 90; margin: 0; pointer-events: none;
  box-shadow: var(--rl-e-2) !important;
  transform: scale(1.02) rotate(1deg);
  transition: box-shadow var(--rl-d-fast) var(--rl-ease-standard),
              transform var(--rl-d-fast) var(--rl-ease-standard);
}
/* Lifted by keyboard: the same state, said differently — it has not left the
   column, so it marks itself in place rather than following a pointer. */
#app .kanban-card[data-lifted="true"] {
  box-shadow: var(--rl-e-2), inset 3px 0 var(--rl-i-600);
  border-color: var(--rl-line-accent);
}
.kanban-placeholder {
  border: 1px dashed var(--rl-line-strong);
  border-radius: var(--rl-r-sm);
  margin-bottom: 8px;
}

.kanban-card .kc-name { display: flex; align-items: baseline; gap: var(--rl-s-2); font-size: 13.5px; font-weight: 500; margin-bottom: 3px; }
.kanban-card .kc-name a { flex: 1; min-width: 0; color: inherit; text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.kanban-card .kc-name a:hover { text-decoration: underline; text-underline-offset: 2px; }
.kanban-card .kc-meta { font-size: 12.5px; color: var(--rl-text-2); margin-bottom: 4px; }
.kanban-card .kc-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--rl-s-2); font-size: 12.5px; color: var(--rl-text-3); }
.kanban-card .kc-age { font-variant-numeric: tabular-nums; }
.kanban-card .kc-age.is-stale { color: var(--rl-text-warning); font-weight: 500; }
/* The explicit path stays available but stops being the loudest thing on the
   card: eighty-two full-width dropdowns was the board's dominant texture. */
.kanban-card .kc-move { display: block; margin-top: 8px; }
.kanban-card .kc-move select {
  width: 100%; min-height: 28px; height: 28px;
  font-size: 12.5px; color: var(--rl-text-3);
  background: transparent; border-color: var(--rl-line);
}
.kanban-card .kc-move select:hover { color: var(--rl-text); border-color: var(--rl-line-control); }
.kanban-empty { font-size: 12.5px; color: var(--rl-text-3); padding: var(--rl-s-3) 2px; }

/* Visually hidden, still announced. */
.rl-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .kanban-card.is-dragging { transform: none; }
}

/* =========================================================================
   §10.5 / §16.3 THE PARCEL DESK
   Marcus is standing up with one hand free and somebody waiting in front of
   him. Every control here is sized for a thumb.
   ========================================================================= */

/* The resident picker: type a surname or a room number, tap a result. */
.rl-pick { margin-top: 6px; border: 1px solid var(--rl-line); border-radius: var(--rl-r-sm); overflow: hidden; max-height: 216px; overflow-y: auto; }
.rl-pick-row {
  display: flex; align-items: center; gap: var(--rl-s-3); width: 100%;
  min-height: 44px; padding: 8px 12px;
  border: none; border-bottom: 1px solid var(--rl-line-soft);
  background: var(--rl-surface); cursor: pointer; text-align: left;
  transition: background var(--rl-d-instant) var(--rl-ease-standard);
}
.rl-pick-row:last-child { border-bottom: none; }
.rl-pick-row:hover { background: var(--rl-n-25); }
[data-theme="dark"] .rl-pick-row:hover { background: var(--rl-surface-raised); }
.rl-pick-row:focus-visible { outline: none; box-shadow: var(--rl-focus-inset); }
.rl-pick-row.is-picked { background: var(--rl-tint-accent-bg); }
.rl-pick-name { flex: 1; font-size: 15px; color: var(--rl-text); }
.rl-pick-room { font-size: 13px; color: var(--rl-text-3); font-variant-numeric: tabular-nums; }
.rl-pick-empty { padding: 14px 12px; font-size: 13.5px; color: var(--rl-text-3); }

/* Carrier chips instead of a free-text box, which is how the same carrier
   ended up in the data as DPD, dpd, D.P.D and "dpd local". */
.rl-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.rl-chips .chip { min-height: 36px; padding: 0 12px; font-size: 13.5px; }
.rl-hidden { display: none !important; }
.rl-more { margin: var(--rl-s-4) 0; }
.rl-more summary {
  cursor: pointer; font-size: 13.5px; color: var(--rl-text-2);
  padding: 8px 0; list-style: none;
}
.rl-more summary::-webkit-details-marker { display: none; }
.rl-more summary::before { content: '+ '; color: var(--rl-text-3); }
.rl-more[open] summary::before { content: '– '; }
.rl-more summary:focus-visible { outline: none; box-shadow: var(--rl-focus); border-radius: var(--rl-r-xs); }

/* One-tap collect from the list. */
.parcel-row { display: flex; align-items: stretch; gap: 0; padding: 0 !important; }
.parcel-open {
  flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 2px;
  border: none; background: transparent; cursor: pointer; text-align: left;
  padding: 10px 12px;
}
.parcel-open:focus-visible { outline: none; box-shadow: var(--rl-focus-inset); }
.parcel-collect {
  flex: none; width: 48px; min-height: 44px;
  border: none; border-left: 1px solid var(--rl-line-soft);
  background: transparent; color: var(--rl-text-3);
  cursor: pointer; display: grid; place-items: center;
  transition: background var(--rl-d-instant) var(--rl-ease-standard), color var(--rl-d-instant);
}
.parcel-collect svg { width: 18px; height: 18px; }
.parcel-collect:hover { background: var(--rl-tint-green-bg); color: var(--rl-text-success); }
.parcel-collect:focus-visible { outline: none; box-shadow: var(--rl-focus-inset); }

/* §8.8 in-flight, rollback and the held exit. */
.parcel-row[data-inflight="true"] { opacity: .7; box-shadow: inset 2px 0 var(--rl-i-300); }
.parcel-row[data-rolledback="true"] { animation: rl-rollback var(--rl-d-slow) var(--rl-ease-standard) forwards; }
@keyframes rl-rollback { from { background: var(--rl-tint-red-bg); } to { background: transparent; } }
.parcel-row[data-leaving="true"] {
  opacity: 0; transform: translateX(12px);
  transition: opacity var(--rl-d-base) var(--rl-ease-exit), transform var(--rl-d-base) var(--rl-ease-exit);
}
@media (prefers-reduced-motion: reduce) {
  .parcel-row[data-leaving="true"] { transform: none; }
}

/* §10.5 Ticket views as a segmented control. */
.rl-seg-views { margin-bottom: var(--rl-s-3); flex-wrap: wrap; }
.rl-seg-count {
  margin-left: 6px; font-size: 11.5px; color: var(--rl-text-3);
  font-variant-numeric: tabular-nums;
}
.rl-seg button[aria-pressed="true"] .rl-seg-count { color: var(--rl-text-2); }

/* An inline control that reads as the value it holds until you touch it —
   the assignee line is a field, but it is also the answer to "who has this". */
.rl-inline-select {
  min-height: 28px; height: 28px; padding: 0 6px 0 4px;
  font-size: 13.5px; border-color: transparent; background: transparent;
  max-width: 100%;
}
.rl-inline-select:hover { border-color: var(--rl-line-control); background: var(--rl-surface); }

/* §8.8 in-flight and rollback on a list row, wherever it appears. */
.side-item[data-inflight="true"] { opacity: .7; box-shadow: inset 2px 0 var(--rl-i-300); }
.side-item[data-rolledback="true"] { animation: rl-rollback var(--rl-d-slow) var(--rl-ease-standard) forwards; }
/* --rl-text-3 clears AA on the canvas and on a flat surface, and not on a
   RAISED one or on a tint — which is exactly where these three meta lines
   sit: a selected row, a room tile, a kanban card. One step up clears it
   everywhere, in both themes. */
.side-item .si-ref { font-size: 12px; color: var(--rl-text-2); margin-top: 1px; }
#app .room-tile .rb { color: var(--rl-text-2); }
.kanban-card .kc-foot, .kanban-card .kc-age { color: var(--rl-text-2); }

/* =========================================================================
   §18 TOUCH TARGETS
   "44px minimum on mobile, 32px minimum on desktop with 8px separation."
   Chips at 26px and segmented buttons at 27px were both under it — small
   enough that a filter chip takes two goes with a trackpad, which is the
   kind of thing that reads as the app being unresponsive rather than as the
   target being small.
   ========================================================================= */
.chip, #app .chip { min-height: 32px; padding: 0 11px; }
.rl-seg button { min-height: 32px; }
.rl-seg { padding: 3px; }
#app .btn.small { height: 32px; }
/* Anything that toggles is at least a finger wide, including the box itself. */
#app input[type="checkbox"], #app input[type="radio"] { width: 18px; height: 18px; }
.check-row, .choice-row { min-height: 32px; display: flex; align-items: center; gap: var(--rl-s-2); }

@media (max-width: 719px) {
  .chip, #app .chip, .rl-seg button, #app .btn.small { min-height: 44px; }
  #app input[type="checkbox"], #app input[type="radio"] { width: 22px; height: 22px; }
}

/* Role editor: the "Can access" section picker (js/settings.js). No #app
   prefix anywhere here — #appModal is a direct child of <body>, outside
   #app — and its own container class so the checkboxes escape
   `.field-block input{width:100%}` in styles.css, which was stretching each
   one to 172x36px. */
.perm-picker {
  border: 1px solid var(--rl-line); border-radius: var(--rl-r-sm);
  padding: 10px 12px; margin-top: 4px;
}
.perm-picker-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding-bottom: 8px; margin-bottom: 8px; border-bottom: 1px solid var(--rl-line);
  font-size: 12px; color: var(--rl-text-2);
}
.perm-picker-head span:last-child { display: flex; gap: 4px; }
.perm-picker .perm-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 2px 16px;
  max-height: 44vh; overflow-y: auto;
}
.perm-picker .perm-opt {
  display: flex; align-items: center; gap: 8px; min-height: 30px;
  font-size: 13px; font-weight: 400; text-transform: none; letter-spacing: normal;
  color: var(--rl-text); cursor: pointer;
}
/* .modal-box in the selector purely for specificity — it has to beat
   styles.css `.field-block input{width:100%;padding:9px 10px;border:...}`,
   every part of which is wrong on a checkbox. */
.modal-box .perm-picker .perm-opt input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; flex: 0 0 auto; margin: 0; padding: 0;
  border-radius: 3px; background: none;
}
@media (max-width: 719px) {
  .perm-picker .perm-grid { grid-template-columns: 1fr; }
  .modal-box .perm-picker .perm-opt input[type="checkbox"] { width: 20px; height: 20px; }
}

/* Settings → Properties: each site's contract address under its name. */
.prop-row > span:first-child { min-width: 0; }
.prop-addr { display: block; font-size: 12px; color: var(--rl-text-3); margin-top: 2px; }
.prop-addr.missing { color: var(--rl-text-warning); }
.prop-row-end { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.prop-preview {
  display: flex; flex-direction: column; gap: 3px; margin: 4px 0 12px; padding: 10px 12px;
  border-radius: var(--rl-r-sm); background: var(--rl-surface-sunk); font-size: 13px; color: var(--rl-text);
}
.prop-preview span { font-size: 12px; color: var(--rl-text-3); }
.prop-preview b { font-weight: 600; }

/* Contract template editor: where an agreement is used (js/contract-wizard.js
   cwTemplateListingPickerHtml). Deliberately NOT inside a .field-block —
   `.field-block label` (11px bold caps) and `.field-block input{width:100%}`
   turned every listing row into a shouted caption beside a 172px "checkbox",
   which is what this replaced. #appModal sits outside #app, so no #app prefix. */
.ctp-sec { margin: var(--rl-s-4) 0 var(--rl-s-2); padding-top: var(--rl-s-4); border-top: 1px solid var(--rl-line); }
.ctp-sec-title { font-size: 14px; font-weight: 600; color: var(--rl-text); margin-bottom: var(--rl-s-2); }
.ctp-sec-sub { font-size: 12.5px; font-weight: 600; color: var(--rl-text-2); margin: var(--rl-s-4) 0 var(--rl-s-2); }
.ctp-dim { color: var(--rl-text-3); font-weight: 400; }
.ctp-sec label.ctp-default {
  display: flex; gap: 10px; align-items: flex-start; cursor: pointer;
  padding: 10px 12px; border: 1px solid var(--rl-line); border-radius: var(--rl-r-sm);
  font-size: 13px; color: var(--rl-text);
}
.ctp-default b { display: block; font-size: 13.5px; font-weight: 600; }
.ctp-default .ctp-dim { display: block; font-size: 12.5px; margin-top: 2px; }
.ctp { border: 1px solid var(--rl-line); border-radius: var(--rl-r-sm); }
.ctp-bar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px;
  padding: 10px 12px; border-bottom: 1px solid var(--rl-line); background: var(--rl-surface-sunk);
  border-radius: var(--rl-r-sm) var(--rl-r-sm) 0 0; font-size: 12.5px;
}
.ctp-count { font-weight: 600; color: var(--rl-text); min-width: 120px; }
.ctp-terms { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; flex: 1 1 auto; }
.ctp-chip {
  font: inherit; font-size: 12px; padding: 3px 9px; border-radius: var(--rl-r-full);
  border: 1px solid var(--rl-line-control); background: var(--rl-surface); color: var(--rl-text); cursor: pointer;
}
.ctp-chip:hover { border-color: var(--rl-line-accent); color: var(--rl-text-accent); }
.ctp-bar-end { display: flex; gap: 4px; margin-left: auto; }
.ctp-list { max-height: 46vh; overflow-y: auto; padding: 4px 0; }
.ctp-b { border-bottom: 1px solid var(--rl-line-soft); }
.ctp-b:last-child { border-bottom: 0; }
.ctp-b > summary {
  display: flex; align-items: center; gap: 10px; padding: 9px 12px; cursor: pointer;
  font-size: 13.5px; color: var(--rl-text); list-style: none;
}
.ctp-b > summary::-webkit-details-marker { display: none; }
.ctp-b > summary::after { content: '›'; margin-left: auto; color: var(--rl-text-3); transition: transform var(--rl-d-fast) var(--rl-ease-standard); }
.ctp-b[open] > summary::after { transform: rotate(90deg); }
.ctp-gcount { font-size: 12px; }
.ctp-rt { padding: 2px 12px 8px 38px; }
/* label.… for specificity: a global `label:has(> input[type=checkbox])`
   rule makes every ticked label inline-flex, which ran each room type and
   its listing together on one line. */
.ctp label.ctp-rt-head, .ctp label.ctp-row {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  text-transform: none; letter-spacing: normal; color: var(--rl-text);
}
.ctp-rt-head { font-size: 13px; font-weight: 600; padding: 5px 0 3px; }
.ctp-row { font-size: 13px; font-weight: 400; min-height: 30px; padding-left: 26px; border-radius: var(--rl-r-xs); }
.ctp-row:hover { background: var(--rl-surface-sunk); }
.ctp-row-main { flex: 1 1 auto; }
.ctp-now { font-size: 12px; color: var(--rl-text-warning); white-space: nowrap; padding-right: 6px; }
/* .modal-box for specificity over every generic input rule. */
.modal-box .ctp input[type="checkbox"], .modal-box .ctp-default input[type="checkbox"] {
  width: 16px; height: 16px; min-height: 0; flex: 0 0 auto; margin: 0; padding: 0;
  border-radius: 3px; background: none; accent-color: var(--rl-i-600);
}
.modal-box .ctp-default input[type="checkbox"] { margin-top: 2px; }
.ctp-moves {
  margin: 0; padding: 9px 12px; border-top: 1px solid var(--rl-line);
  font-size: 12.5px; background: var(--rl-tint-amber-bg); color: var(--rl-tint-amber-fg);
  border-radius: 0 0 var(--rl-r-sm) var(--rl-r-sm);
}
.ctp-used { display: block; font-size: 12px; color: var(--rl-text-3); margin-top: 2px; }
.ctp-warn {
  margin-bottom: var(--rl-s-3); padding: 10px 12px; border-radius: var(--rl-r-sm);
  background: var(--rl-tint-amber-bg); color: var(--rl-tint-amber-fg); font-size: 13px;
}
@media (max-width: 719px) {
  .ctp-rt { padding-left: 20px; }
  .ctp-row { padding-left: 8px; min-height: 44px; flex-wrap: wrap; }
  .ctp-now { flex-basis: 100%; padding-left: 30px; margin-top: -8px; }
  .modal-box .ctp input[type="checkbox"], .modal-box .ctp-default input[type="checkbox"] { width: 20px; height: 20px; }
}

/* A checkbox that sits alone in a table cell has no label beside it to
   extend its hit area, so the cell becomes the target (§18). */
.rl-toggle-cell {
  display: inline-flex; align-items: center; justify-content: flex-end;
  min-width: 44px; min-height: 32px; cursor: pointer;
}

/* A label wrapping a checkbox IS the target, so it has to be one (§18). */
label:has(> input[type="checkbox"]), label:has(> input[type="radio"]),
#app label:has(> input[type="checkbox"]), #app label:has(> input[type="radio"]) {
  min-height: 32px; display: inline-flex; align-items: center; gap: 8px;
}
@media (max-width: 719px) {
  label:has(> input[type="checkbox"]), label:has(> input[type="radio"]) { min-height: 44px; }
}
.rl-rename { width: 180px; font-size: 13px; }

/* §16.2 / §18: "Tap targets ≥ 44px. Non-negotiable." Controls are 36px on a
   desktop, which is right there and wrong on a phone — a 36px date field is
   a miss as often as a hit with a thumb. */
@media (max-width: 719px) {
  #app input:not([type]), #app input[type="text"], #app input[type="email"],
  #app input[type="password"], #app input[type="number"], #app input[type="date"],
  #app input[type="time"], #app input[type="search"], #app input[type="tel"],
  #app input[type="url"], #app select, #app textarea,
  #app .btn, #app .btn-select,
  .form-grid input, .form-grid select, .form-grid textarea,
  .field-block input, .field-block select, .field-block textarea {
    min-height: 44px;
  }
  #app textarea { min-height: 88px; }
  .rl-need { min-height: 44px; }
}


/* THE min-width: auto TRAP.

   A flex or grid item's default min-width is `auto`, which means it refuses
   to shrink below its content — so one long unbroken title, or a row of
   buttons that will not wrap, pushes its whole container wider than the
   screen and takes the page's horizontal scroll with it. It is the single
   most common cause of a layout that works on a laptop and breaks on a
   phone, and it accounted for most of what the audit found at 420px.

   Setting it to 0 lets the truncation and wrapping that is already declared
   on these elements actually happen. */
#app .split-layout > *,
#app .grid > *,
#app .side-item > *,
#app .cd-meta-grid > *,
#app .form-grid > * { min-width: 0; }
.side-item .si-title, .side-item .si-sub, .side-item .si-ref {
  overflow: hidden; text-overflow: ellipsis;
}
.side-item .si-sub, .side-item .si-ref { white-space: nowrap; }
/* Two lines is enough for a title in a 340px master list; past that it is
   pushing the status chip off the bottom of the row. */
.side-item .si-title {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
}

/* Toolbars are a single flex row on a desktop and have to become several on
   a phone. Without the wrap they simply run off the side of the screen and
   take the page's horizontal scroll with them. */
@media (max-width: 719px) {
  .view-toolbar, .cal-toolbar, .cal-nav, .cal-filters, .form-actions,
  .toolbar-row, .rl-plan-controls, .card-head, .section-head {
    flex-wrap: wrap;
  }
  .view-toolbar > *, .cal-toolbar > * { max-width: 100%; }
  .toolbar-search { flex: 1 1 100%; }
  /* A control that wrapped onto its own line should use it. */
  .view-toolbar .btn-select, .cal-toolbar .btn-select { flex: 1 1 auto; }

  /* Every sub-navigation pill is a tap target (§18). */
  .ops-sub-btn, .reports-sub-btn, .governance-sub-btn, .qf-chip,
  #app .ops-sub-btn, #app .reports-sub-btn, #app .governance-sub-btn, #app .qf-chip {
    min-height: 44px;
  }
}

@media (max-width: 719px) {
  /* In card mode a cell is a label plus a value; when the value is a row of
     controls it needs its own line rather than the label's leftovers. */
  #app table td { flex-wrap: wrap; }
  #app table td.rl-col-actions .btn { flex: 0 1 auto; }
  /* The two remaining sub-44px targets: a bare toggle and a disclosure. */
  #app label:has(> input[type="checkbox"]), #app label:has(> input[type="radio"]),
  .rl-toggle-cell { min-height: 44px; }
  .gov-duties summary, #app .gov-duties summary { min-height: 44px; }
}

/* =========================================================================
   AN ALERT BAND
   Not an empty state. The distinction matters: a dashed border and centred
   padding is the visual language of "nothing here yet", and the compliance
   banner was wearing it while saying six statutory tests were overdue.

   Same severity language as the Needs you rows on Today — a 3px left edge,
   a tint, and the text at the size it would be in a sentence.
   ========================================================================= */
.rl-alert {
  display: flex; align-items: center; gap: var(--rl-s-3); flex-wrap: wrap;
  padding: 12px var(--rl-s-4);
  margin-bottom: var(--rl-s-4);
  border: 1px solid var(--rl-line);
  border-radius: var(--rl-r-md);
  box-shadow: inset 3px 0 var(--rl-n-400);
  background: var(--rl-surface);
  font-size: 13.5px; line-height: 19px; color: var(--rl-text);
  text-align: left;
}
.rl-alert strong { font-weight: 500; }
.rl-alert > .btn { margin-left: auto; }
.rl-alert[data-sev="fail"] { background: var(--rl-tint-red-bg); border-color: var(--rl-line-danger); box-shadow: inset 3px 0 var(--rl-r-500); }
.rl-alert[data-sev="warn"] { background: var(--rl-tint-amber-bg); border-color: var(--rl-a-200); box-shadow: inset 3px 0 var(--rl-a-500); }
.rl-alert[data-sev="info"] { background: var(--rl-tint-accent-bg); border-color: var(--rl-line-accent); box-shadow: inset 3px 0 var(--rl-i-500); }
.rl-alert[data-sev="ok"]   { background: var(--rl-tint-green-bg); border-color: var(--rl-g-200); box-shadow: inset 3px 0 var(--rl-g-500); }
[data-theme="dark"] .rl-alert[data-sev="warn"] { border-color: #6B4A12; }
[data-theme="dark"] .rl-alert[data-sev="ok"]   { border-color: #1B5334; }
@media (max-width: 719px) {
  .rl-alert > .btn { margin-left: 0; width: 100%; }
}

/* A button styled as a link is still a control, and §18's floor applies to
   it. These sit in the Files list where they are the only way to open a
   signed agreement — a 23px target for the most consequential row on the
   screen. */
#app .link-btn { min-height: 32px; display: inline-flex; align-items: center; background: none; border: 0; padding: 0 4px; font: inherit; color: var(--rl-text-link, var(--rl-i-600)); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
#app .link-btn:hover { color: var(--rl-i-700); }
@media (max-width: 719px) { #app .link-btn { min-height: 44px; } }

/* The waitlist state sits under its own control rather than trailing it, so
   the line does not depend on the select happening to fit beside it. */
.lo-waitlist-state { display: block; margin-top: 4px; }

/* =========================================================================
   §7.1 THE MASTER PANE IN A SPLIT LAYOUT

   Eleven master lists carried a hardcoded inline `max-height` in app.html —
   460px, 560px, 600px, 640px, picked at four different moments and related
   to nothing. What that produced, on every one of them:

     * a short fixed box with FOUR THOUSAND pixels of rows behind it,
       reachable only by putting the pointer over the list and scrolling it
       rather than the page;
     * two scrollbars whose relationship to each other was not visible;
     * and, because the box was in normal flow, a master list that scrolled
       up out of the window while you read the detail it was the index for.

   The fix is the standard master/detail arrangement, and it is what the one
   inline number was standing in for: the master column PINS under the tab
   bar and takes the height the window actually has, so the page scroll
   belongs to the detail and the list is always where you left it.

   The first grid child is targeted rather than the list itself because most
   of these views wrap the list together with its pager — pinning the list
   alone would slide its own pager away from it. Views whose list IS the
   first child (Refunds, Move-outs) match the same selector.
   ========================================================================= */
@media (min-width: 901px) {
  .split-layout > :first-child,
  .contracts-layout > :first-child {
    position: sticky;
    top: calc(var(--rl-sticky-top, 0px) + var(--rl-s-3));
    max-height: calc(100vh - var(--rl-topbar-h) - var(--rl-sticky-top, 0px) - var(--rl-s-10));
    display: flex; flex-direction: column; min-height: 0;
  }
  /* The list takes what is left after the pager, and is the part that
     scrolls. min-height:0 because a flex item will not shrink below its
     content otherwise, which would push the pager out of the pinned box. */
  .split-layout > :first-child > .side-list,
  .contracts-layout > :first-child > .contracts-list,
  .split-layout > .side-list,
  .contracts-layout > .contracts-list {
    flex: 1 1 auto; min-height: 0; max-height: none;
  }
  .split-layout > :first-child > .pager-bar,
  .contracts-layout > :first-child > .pager-bar { flex: none; }
}

/* Stacked. Pinning the list here would park it on top of the detail it
   exists to open, so it stays in flow — but it keeps a cap, because a
   4,000px list above the detail is its own kind of unusable. */
@media (max-width: 900px) {
  .split-layout > :first-child > .side-list,
  .contracts-layout > :first-child > .contracts-list,
  .split-layout > .side-list,
  .contracts-layout > .contracts-list { max-height: 60vh; }
}

/* =========================================================================
   MOTION PASS (2026-09-15) — the transitions the product was missing.

   The rules above already move the drawer, popovers, the palette, toasts
   and the route change. What still cut hard:

   - #appModal — every form in the product — faded in and vanished out.
   - The detail pane beside every list redrew in place when a different
     row was picked, so it was hard to see that anything had changed.
   - Presses: .btn:active scaled, but transform was not in the transition
     list, so the scale snapped.
   - Progress bars jumped to their width; list selection snapped.

   All from the §3.5 tokens, none of it load-bearing, and every one of
   them is flattened by the reduced-motion block in tokens.css.
   ========================================================================= */

/* The dialog: scrim fades, panel rises and settles; the exit is shorter
   than the entrance (§8.1 — leaving should never make you wait). */
@keyframes rl-scrim-in  { from { opacity: 0; } to { opacity: 1; } }
@keyframes rl-scrim-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes rl-panel-in  { from { opacity: 0; transform: translateY(10px) scale(.975); } to { opacity: 1; transform: none; } }
@keyframes rl-panel-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(6px) scale(.985); } }
@keyframes rl-panel-swap { from { opacity: .35; transform: translateY(4px); } to { opacity: 1; transform: none; } }

#appModal { animation: rl-scrim-in var(--rl-d-fast) var(--rl-ease-standard); }
#appModal .modal-box { animation: rl-panel-in var(--rl-d-mid) var(--rl-ease-enter); will-change: transform, opacity; }
#appModal[data-swap="true"] { animation: none; }
#appModal[data-swap="true"] .modal-box { animation: rl-panel-swap var(--rl-d-base) var(--rl-ease-enter); }
#appModal[data-closing="true"] {
  pointer-events: none;
  animation: rl-scrim-out var(--rl-d-fast) var(--rl-ease-exit) forwards;
}
#appModal[data-closing="true"] .modal-box { animation: rl-panel-out var(--rl-d-fast) var(--rl-ease-exit) forwards; }

/* Presses and hovers ease instead of snapping. */
.btn, #app .btn {
  transition: background var(--rl-d-instant) var(--rl-ease-standard),
              border-color var(--rl-d-instant) var(--rl-ease-standard),
              color var(--rl-d-instant) var(--rl-ease-standard),
              box-shadow var(--rl-d-fast) var(--rl-ease-standard),
              opacity var(--rl-d-fast) var(--rl-ease-standard),
              transform var(--rl-d-instant) var(--rl-ease-standard);
}
.btn:active, #app .btn:active, .btn.ghost:active, #app .btn.ghost:active { transform: scale(0.97); }

/* The detail pane beside a list, re-entering when the selection changes.
   rlSwapIn() (shell.js) sets the class only when the SELECTION changed, so
   a re-render of the same record — a status dropdown, a realtime row —
   does not replay it. */
@keyframes rl-swap-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.rl-swap { animation: rl-swap-in var(--rl-d-base) var(--rl-ease-enter); }

/* Selecting a row in a master list. */
#app .side-item, #app .contract-item, #app .inbox-row {
  transition: background var(--rl-d-fast) var(--rl-ease-standard),
              box-shadow var(--rl-d-fast) var(--rl-ease-standard),
              transform var(--rl-d-fast) var(--rl-ease-standard);
}

/* Bars grow to their value instead of appearing at it. */
#app [id^="bar"], #app .bar-fill, #app .progress-fill, #app .tsp-fill, #app .rl-meter > span {
  transition: width var(--rl-d-long) var(--rl-ease-enter);
}

/* Disclosure panels open with the same small rise as a popover. */
@keyframes rl-reveal { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
#app details[open] > :not(summary) { animation: rl-reveal var(--rl-d-base) var(--rl-ease-enter); }

/* Record tabs: the chosen tab's rule draws in from its centre. */
@keyframes rl-rule-in { from { transform: scaleX(.2); opacity: 0; } to { transform: scaleX(1); opacity: 1; } }

/* Table pages and filters: the rows settle in rather than blinking. The
   class is applied by rlSwapIn on the table wrapper, so sorting and
   re-renders of the same page stay still. */
#app .rl-swap-rows tbody { animation: rl-panel-swap var(--rl-d-base) var(--rl-ease-enter); }

/* Chips and badges that change state cross-fade their colours. */
#app .badge, #app .rl-flag, #app .p360-chip, #app .qf-chip, #app .ops-sub-btn {
  transition: background-color var(--rl-d-fast) var(--rl-ease-standard),
              color var(--rl-d-fast) var(--rl-ease-standard),
              border-color var(--rl-d-fast) var(--rl-ease-standard);
}

/* Form controls: focus ring and border ease in. */
#app input, #app select, #app textarea {
  transition: border-color var(--rl-d-fast) var(--rl-ease-standard),
              box-shadow var(--rl-d-fast) var(--rl-ease-standard),
              background-color var(--rl-d-fast) var(--rl-ease-standard);
}

/* The person record's tab rule (rlP360TabInk, shell.js). The selected
   tab's own border steps aside while the ink is there, so there is one
   rule, and it is the one that moves. */
#app .p360-tabs { position: relative; }
#app .p360-tabs.has-ink .p360-tab[aria-selected="true"] { border-bottom-color: transparent; }
#app .p360-tab-ink {
  position: absolute; bottom: -1px; height: 2px;
  background: var(--rl-i-600); border-radius: 2px 2px 0 0;
  transform-origin: left center; pointer-events: none;
}

/* rlConfirm (chrome.js) — its own layer above #appModal (z-index 210), so a
   question asked from inside a form never replaces the form. */
.rl-confirm { position: fixed; inset: 0; z-index: 230; display: none; place-items: center; padding: var(--rl-s-4); }
.rl-confirm[data-open="true"] { display: grid; }
.rl-confirm-scrim { position: absolute; inset: 0; background: var(--rl-overlay); animation: rl-scrim-in var(--rl-d-fast) var(--rl-ease-standard); }
.rl-confirm-panel {
  position: relative; width: min(440px, 100%);
  background: var(--rl-surface-raised); border-radius: var(--rl-r-lg); box-shadow: var(--rl-e-3);
  padding: var(--rl-s-6);
  animation: rl-panel-in var(--rl-d-mid) var(--rl-ease-enter);
}
.rl-confirm-panel h3 {
  margin: 0; font-family: var(--rl-font-serif); font-size: 19px; line-height: 26px; font-weight: 400; color: var(--rl-text);
}
.rl-confirm-panel p { margin: var(--rl-s-3) 0 0; font-size: 13.5px; line-height: 20px; color: var(--rl-text-2); }
.rl-confirm-actions { display: flex; justify-content: flex-end; gap: var(--rl-s-2); margin-top: var(--rl-s-5); }
.rl-confirm .btn.danger.is-solid { background: var(--rl-r-600); border-color: var(--rl-r-600); color: #fff; }
.rl-confirm .btn.danger.is-solid:hover { background: var(--rl-r-700); border-color: var(--rl-r-700); }
.rl-confirm[data-closing="true"] { pointer-events: none; }
.rl-confirm[data-closing="true"] .rl-confirm-scrim { animation: rl-scrim-out var(--rl-d-fast) var(--rl-ease-exit) forwards; }
.rl-confirm[data-closing="true"] .rl-confirm-panel { animation: rl-panel-out var(--rl-d-fast) var(--rl-ease-exit) forwards; }
@media (max-width: 719px) {
  .rl-confirm { place-items: end stretch; padding: 0; }
  .rl-confirm-panel { width: 100%; border-radius: var(--rl-r-lg) var(--rl-r-lg) 0 0; padding-bottom: calc(var(--rl-s-6) + env(safe-area-inset-bottom)); }
  .rl-confirm-actions .btn { flex: 1; height: 44px; }
}

/* Record tabs never wrap a label ("Front desk" was breaking into "Fro /
   de" on a phone); the strip scrolls sideways instead. The ink sits on the
   bottom edge rather than 1px below it, because a scrolling strip clips
   anything outside its box. */
#app .p360-tab { white-space: nowrap; flex: none; }
@media (max-width: 719px) {
  #app .p360-tabs { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
  #app .p360-tabs::-webkit-scrollbar { display: none; }
  #app .p360-tab-ink { bottom: 0; }
}
