/* Design tokens — DESIGN.md §4. Minimal shell styling for now; expanded
   per-screen as each phase's UI is built. */
:root {
  color-scheme: light;
  --page: #f9f9f7;
  --surface: #fcfcfb;
  --surface-sunken: #f0efec;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  --text-muted: #898781;
  --border: rgba(11, 11, 11, 0.10);
  --border-strong: #c3c2b7;
  --accent: #2a78d6;
  --accent-hover: #256abf;
  --accent-wash: #cde2fb;
  --good: #0ca30c;
  --warning: #fab219;
  --serious: #ec835a;
  --critical: #d03b3b;
  --gridline: #e1e0d9;      /* chart gridlines only */
  --success-text: #006300;  /* success text needs a darker step than --good */

  /* Text-on-tint steps for badges. Each is the darkened partner of a status
     colour, so the word stays >=4.5:1 on its own 12%-opacity tint (§13).
     Named tokens rather than hex literals in the badge rules, because §4
     asks for palette values to be re-validated rather than eyeballed — and
     an inline hex is invisible to that review. */
  --warning-text: #8a5600;
  --serious-text: #8c3d17;
  --critical-text: #a01f1f;

  --font: system-ui, -apple-system, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }

/* Present to assistive technology, absent visually — §13. Used for column
   headings that are obvious sighted but need a name announced. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* DESIGN.md §4.3 — never removed, never outline:none without a
   replacement. :focus-visible so a mouse click on a button doesn't ring it,
   but every keyboard path does. The two-layer shadow keeps a gap between
   the control and the ring so it reads on any surface. */
:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--surface),
    0 0 0 4px var(--accent);
  border-radius: 6px;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-primary);
  background: var(--page);
}

.app-shell {
  display: flex;
  min-height: 100vh;
}

/* Phone chrome. Present in the markup at every width and shown only below
   768px, so there is one DOM rather than two to keep in step. */
.mobile-bar,
.nav-scrim {
  display: none;
}

.mobile-menu {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-left: -10px;
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--text-secondary);
  cursor: pointer;
}

.mobile-menu:hover {
  background: var(--accent-wash);
  color: var(--text-primary);
}

.mobile-title {
  font-weight: 600;
  letter-spacing: 0.05em;
}

.sidebar {
  width: 240px;
  flex-shrink: 0;
  background: var(--surface);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 16px 0;

  /* The rail is its own column and does not grow with the page.
     As a plain flex child it stretched to the height of the *document*, so
     on a long screen — the walkthrough, a full bill list — the profile and
     Sign out sat at the bottom of the content rather than at the bottom of
     the window, and reaching them meant scrolling past everything. Now it
     is one viewport tall and stays put while the content scrolls, with its
     own scrollbar for the case where the nav itself does not fit.

     `align-self: flex-start` is what stops the flex container stretching it
     back; `height`, not `min-height`, is what makes the internal scroll
     work. Below 768px the drawer rules take over and this no longer
     applies. */
  position: sticky;
  top: 0;
  align-self: flex-start;
  height: 100vh;
  overflow-y: auto;
}

.sidebar-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 0 16px 16px;
  /* Separates the identity from the destinations, the same hairline the
     footer and the Settings link already use. Left in the base rule rather
     than the two collapsed blocks, which do not touch it — one place to
     change, and nothing for those two to disagree about. */
  border-bottom: 1px solid var(--border);
  margin-bottom: 8px;
}

.nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Inset pills rather than full-bleed rows: the tint then reads as a
     marker on the link, not as a stripe across the rail. */
  margin: 0 12px;
  padding: 8px 12px;
  border-radius: 6px;
  color: var(--text-secondary);
  text-decoration: none;
}

.nav-link svg {
  flex-shrink: 0;
}

/* DESIGN.md §7's rule above Settings. Configuration is a different kind of
   destination from the objects above it, and a decorative line is the whole
   of the difference — so no <hr>, which some readers announce and others
   ignore. The margin does more work than the border: the footer carries its
   own border-top a little below, and without the gap the two read as a pair
   of stripes. */
.nav-link-apart {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.nav-link .nav-badge {
  margin-left: auto;
}

/* DESIGN.md §7 — the sidebar collapses on demand, not only at a breakpoint.
   Shaped like a nav link so it reads as part of the rail in both states;
   muted, because it is chrome rather than a destination.

   Hidden until sidebar.js reveals it: a control that cannot work should not
   be on screen (§3.5). */
.sidebar-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  /* Pushed to the right end of the brand row. `margin-left: auto` rather
     than justify-content on the parent, so it moves without disturbing the
     mark and wordmark beside it. */
  margin-left: auto;
  flex: none;
  width: 28px;
  height: 28px;
  padding: 0;
  background: none;
  /* An outlined box rather than a bare glyph: the rail has no other control
     in it, so nothing else says this one is pressable. */
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-secondary);
  font: inherit;
  cursor: pointer;
}

html.sidebar-toggle-ready .sidebar-toggle {
  display: flex;
  /* Repeated from the rule above so the alignment travels with the rule
     that makes the button visible — one less way for a later edit to leave
     it stranded on the left. */
  margin-left: auto;
}

/* The arrow always points the way the edge will move, so it is the action
   rather than the state. Deliberately outside the marked collapsed blocks:
   the toggle is hidden at the ≤1279px breakpoint, so this is not part of
   the appearance those two blocks have to agree on. */
.toggle-icon {
  display: inline-flex;
}

.toggle-icon-expand {
  display: none;
}

html.sidebar-collapsed .toggle-icon-collapse {
  display: none;
}

html.sidebar-collapsed .toggle-icon-expand {
  display: inline-flex;
}

.sidebar-toggle:hover {
  background: var(--accent-wash);
  color: var(--text-primary);
}

/* ---- collapsed sidebar: manual ---------------------------------------
   Declaration-for-declaration the same as the ≤1279px block further down,
   and held that way by test_the_two_collapsed_blocks_agree. Two blocks
   because CSS cannot apply a class from a media query, and the breakpoint
   has to work with no JS at all. Marked so the test can find them. */
/* Above the phone breakpoint only. Below 768px the sidebar is a drawer
   and neither collapse applies, which is what keeps the drawer rules
   free of undo-rules for a state that no longer exists. */
@media (min-width: 769px) {
/* collapsed-block: manual */
html.sidebar-collapsed .sidebar {
  width: 72px;
}

html.sidebar-collapsed .sidebar .nav-label,
html.sidebar-collapsed .sidebar .sidebar-brand-text,
html.sidebar-collapsed .sidebar-footer .user-name {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

html.sidebar-collapsed .nav-link {
  display: flex;
  justify-content: center;
  padding: 10px 0;
}

/* The toggle keeps its 28px box — it is a bordered button, so repadding it
   the way a nav link is repadded would crush the glyph inside the border. */
html.sidebar-collapsed .sidebar-toggle {
  margin-left: 0;
}

html.sidebar-collapsed .nav-link-apart {
  margin-top: 12px;
  padding-top: 12px;
}

html.sidebar-collapsed .sidebar-brand {
  flex-direction: column;
  gap: 8px;
  justify-content: center;
  padding: 0 0 16px;
}

/* A dot, not a number: at 72px the count would sit on top of the glyph it
   is meant to annotate. `font-size: 0` hides the digits without removing
   them from the accessible name. */
html.sidebar-collapsed .nav-link .nav-badge {
  margin-left: 0;
  position: absolute;
  transform: translate(12px, -10px);
  min-width: 0;
  width: 7px;
  height: 7px;
  padding: 0;
  border: 1.5px solid var(--surface);
  border-radius: 5px;
  font-size: 0;
}

html.sidebar-collapsed .sidebar-footer {
  padding: 16px 8px;
  text-align: center;
}

html.sidebar-collapsed .account-menu > summary {
  justify-content: center;
  gap: 0;
  padding: 6px 0;
}

html.sidebar-collapsed .account-caret {
  display: none;
}
/* end collapsed-block */
}

/* Brand guidelines §1 — the mark is present at every width; collapsing the
   sidebar hides the wordmark beside it, not the mark itself. It used to be
   a literal letter "B" shown only when collapsed. */
.sidebar-brand-mark {
  flex: none;
  border-radius: 5px;
}

.auth-mark {
  display: block;
  margin: 0 auto 12px;
  border-radius: 10px;
}

/* A neutral tint, deliberately not the accent wash the current destination
   uses — sharing one made every hovered link look selected. */
.nav-link:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

/* The current destination. §13 — state is never carried by colour alone, so
   the weight goes 600 as well as the tint, and the markup sets
   aria-current="page".

   The 3px rail marker this used to carry went with the move to inset pills
   on 2026-08-27: a left edge marker cuts the rounded corner, and the accent
   text is a second signal the full-bleed row did not have.

   Nothing here touches the box model, or the rail would jump on every
   navigation. */
.nav-link.is-active {
  background: var(--accent-wash);
  color: var(--accent-hover);
  font-weight: 600;
}

.nav-badge {
  display: inline-block;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--warning);
  color: var(--warning-text);
  font-size: 11px;
  font-weight: 600;
  text-align: center;
}

/* Who is signed in. Collapsed, the name is visually hidden and the icon
   carries it alone — with the name still in the DOM and on the title, per
   §15. */
.sidebar-footer {
  margin-top: auto;
  padding: 12px;
  border-top: 1px solid var(--border);
}

/* DESIGN.md §7's `KB  David ▾`. A native <details>, so it opens with no
   JavaScript; account.js only adds Escape and outside-click. */
.account-menu {
  position: relative;
}

.account-menu > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 6px;
  cursor: pointer;
  list-style: none;
  color: var(--text-secondary);
  min-height: 44px;
}

.account-menu > summary::-webkit-details-marker {
  display: none;
}

.account-menu > summary:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

.account-menu > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.avatar {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--accent-wash);
  color: var(--accent-hover);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.account-menu .user-name {
  flex: 1;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-caret {
  flex: 0 0 auto;
  display: flex;
  color: var(--text-muted);
  transition: transform 120ms ease;
}

.account-menu[open] .account-caret {
  transform: rotate(180deg);
}

/* Opens upward: the footer is the last thing in the rail, so a downward
   sheet would fall off the bottom of the window. */
.account-sheet {
  position: absolute;
  bottom: calc(100% + 6px);
  left: 0;
  right: 0;
  min-width: 200px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(11, 11, 11, 0.12);
  z-index: 30;
}

.account-who {
  margin: 0;
  padding: 6px 8px 8px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  line-height: 1.35;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

.account-role {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
}

.account-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: 40px;
  padding: 8px;
  margin-top: 2px;
  border: none;
  border-radius: 6px;
  background: none;
  font: inherit;
  font-size: 13px;
  color: var(--text-primary);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.account-item:hover {
  background: var(--surface-sunken);
}

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

/* Signing out is not destructive — it is reversible by signing in again —
   so it takes the plain step, never the critical one (§5.5). */
.account-signout {
  color: var(--text-secondary);
}

.link-button {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  cursor: pointer;
  font: inherit;
}

.content {
  flex: 1;
  padding: 24px;
  max-width: 1440px;
}

.messages {
  list-style: none;
  padding: 0;
  margin: 0 0 16px;
}

.message {
  padding: 8px 12px;
  border-radius: 6px;
  margin-bottom: 8px;
  background: var(--surface-sunken);
}

.message.error { color: var(--critical); }
.message.success { color: var(--good); }
.message.warning { color: var(--warning-text); }

/* A link inside a wrapping lede moves down whole rather than splitting
   "View on" from "Zillow" across two lines. */
.lede-link {
  white-space: nowrap;
}

/* A field with a button beside it — the listing link and its Fill address. */
.field-with-action {
  display: flex;
  gap: 8px;
}

.field-with-action input {
  flex: 1 1 auto;
  min-width: 0;
}

.field-with-action .button-secondary {
  flex: 0 0 auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

th {
  text-align: left;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--text-secondary);
  background: var(--surface-sunken);
  padding: 8px 12px;
}

/* §4.4 — 44px comfortable row height. Set on the cell rather than the row
   so the border-collapse table still honours it. */
td {
  height: 44px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
}

/* §5.3/§5.4 — amounts right-aligned with tabular figures, and the header
   aligned over them so the column reads as one block. */
.col-amount,
td.col-amount {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* DESIGN.md §6 — dot or icon + word, never colour alone. The icon is a
   child <svg> inheriting currentColor, so each tone colours its own glyph.
   §13 and §15 both make this non-optional: the interface has to read
   correctly in greyscale and for a colourblind user. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 500;
  line-height: 1.4;
  background: var(--surface-sunken);
  color: var(--text-secondary);
}

.badge svg {
  flex-shrink: 0;
}

/* §6 asks for "a tinted background at ~12% opacity". Mixed from the status
   token rather than hand-picked, so the tint is provably that token and
   there is no eyeballed hex to drift from the palette (§4). The foreground
   is the darkened text partner, which is what carries the contrast. */
.badge-muted { color: var(--text-muted); }
.badge-warning {
  color: var(--warning-text);
  background: color-mix(in oklab, var(--warning) 18%, var(--surface));
}
.badge-accent { color: var(--accent-hover); background: var(--accent-wash); }
.badge-good {
  color: var(--success-text);
  background: color-mix(in oklab, var(--good) 12%, var(--surface));
}
.badge-serious {
  color: var(--serious-text);
  background: color-mix(in oklab, var(--serious) 18%, var(--surface));
}
/* Rejected, and anything overdue. Previously undefined, which silently fell
   back to the neutral base rule and made a rejected bill look like a
   draft — the exact confusion §6 exists to prevent. */
.badge-critical {
  color: var(--critical-text);
  background: color-mix(in oklab, var(--critical) 12%, var(--surface));
}

/* A category label — contact type, payment method. Deliberately NOT a
   status badge: it carries no lifecycle meaning, so it gets no icon and no
   status colour, and cannot be confused for one at a glance (§6). */
.chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 6px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text-secondary);
  font-size: 11px;
  font-weight: 500;
}

/* A chip that filters. Same shape as the category chip so the two read as
   one family, with an active state that isn't carried by colour alone
   (§13 — the border weight and background both change). */
.chip-link {
  text-decoration: none;
  padding: 6px 12px;
  font-size: 13px;
}

.chip-link:hover {
  border-color: var(--accent);
  color: var(--text-primary);
}

.chip-link.is-active {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-hover);
  font-weight: 600;
}

/* DESIGN.md §7 — the filter row. Persistent, its state in the URL, sitting
   between the page header and the table. */
.filter-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 12px;
}

.filter-tab {
  padding: 8px 12px;
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 14px;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.filter-tab:hover {
  color: var(--text-primary);
}

/* §8 tabs are underline-style; the active one also goes 600 so the state
   is not carried by colour alone (§13). */
.filter-tab.is-active {
  color: var(--text-primary);
  font-weight: 600;
  border-bottom-color: var(--accent);
}

.filter-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
}

.filter-search,
.filter-select {
  height: 36px;
  /* 12px, from the design canvas. Shared with .filter-select so the box and
     the dropdowns beside it are inset by the same amount. */
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  font: inherit;
  color: var(--text-primary);
}

/* Grows to fill the row, but stops at 320px — the width the design canvas
   draws on all four list artboards. Without the cap, `flex: 1` lets the box
   swallow every pixel the dropdowns leave behind, so on a wide screen the
   search field is several times the size of the controls beside it and the
   row stops reading as a set. */
.filter-search {
  flex: 1;
  min-width: 200px;
  max-width: 320px;
}

/* DESIGN.md §12 — hx-indicator on the region, not the page. htmx adds
   .htmx-request for the life of the request; hidden the rest of the time
   so the row doesn't reflow when it appears. */
.filter-working {
  font-size: 13px;
  color: var(--text-muted);
  opacity: 0;
  transition: opacity 120ms ease-in;
}

.filter-working.htmx-request {
  opacity: 1;
}

/* Apply is a fallback for a filter row htmx is not driving, so it is hidden
   from the first paint and revealed only when needed — by filters.js if htmx
   did not load, or by the <noscript> block in base.html if scripting is off.
   Hiding it *after* load instead made it flash on every page load, since the
   script is deferred and runs after the page is drawn.
   Two classes deep to beat .button-secondary's own `display`. */
.filter-row .filter-apply {
  display: none;
}

html.no-htmx .filter-row .filter-apply {
  display: inline-flex;
}

/* The results region dims rather than disappearing while a search lands —
   swapping to a blank and back reads as a flicker, and on a slow
   connection an empty table looks like "no results". */
#property-results.htmx-swapping,
#dashboard-body.htmx-swapping {
  opacity: 0.5;
}

/* DESIGN.md §5 Dates — a deadline within 7 days is --warning, an overdue
   one --critical, "both with an icon". The icon and, for overdue, the word
   itself carry the meaning so it survives greyscale (§13). */
.due-flag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: 500;
  white-space: nowrap;
}

.due-flag.is-soon { color: var(--warning-text); }
.due-flag.is-overdue { color: var(--critical-text); }

.due-word {
  font-weight: 400;
  font-size: 12px;
}

.result-count {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0 0 8px;
}

/* Context for a list that arrived pre-filtered from somewhere else — which
   subset, and the way back. Sits above the count rather than inside it: the
   count states how many, this states of what, and merging them produced a
   sentence that did both jobs badly. */
.filter-note {
  font-size: 13px;
  color: var(--text-muted);
  margin: 0 0 8px;
}

/* A hub-page section showing its most recent few, with the way to the rest. */
.section-more {
  margin: 12px 0 0;
  font-size: 13px;
  color: var(--text-secondary);
}

/* A possible duplicate bill. Uses the serious tint rather than critical:
   nothing has gone wrong yet, and this is a question rather than an error
   (DESIGN.md §5.5 — reserve critical for overdue, failed, over-budget). */
.duplicate-warning {
  border: 1px solid var(--serious);
  background: color-mix(in oklab, var(--serious) 10%, var(--surface));
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  max-width: 640px;
}

.duplicate-warning h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 15px;
  margin: 0 0 8px;
  color: var(--serious-text);
}

.duplicate-warning p {
  margin: 0 0 12px;
  color: var(--text-secondary);
}

.duplicate-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 13px;
}

.duplicate-list li {
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
}

.empty-state {
  text-align: center;
  padding: 64px 24px;
  color: var(--text-secondary);
}

.empty-state h2 {
  font-size: 15px;
  color: var(--text-primary);
}

.empty-state .button-primary {
  display: inline-block;
  margin-top: 16px;
}

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 24px;
}

.page-header h1 {
  margin: 4px 0;
}

.button-primary,
.button-secondary {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 12px;
  border-radius: 6px;
  font-weight: 500;
  font-size: 14px;
  text-decoration: none;
  cursor: pointer;
  border: none;
  font-family: inherit;
}

.button-primary {
  background: var(--accent);
  color: white;
}

.button-primary:hover {
  background: var(--accent-hover);
}

.button-secondary {
  background: var(--surface);
  color: var(--text-secondary);
  border: 1px solid var(--border-strong);
}

/* A disabled button has to look disabled. Neither primary nor secondary had
   a `:disabled` rule, so a locked Send stayed full blue with a pointer and
   its hover colour — unclickable but indistinguishable from live. The same
   grey the pager and the walkthrough's closed Next already use. After the
   hover rules on purpose: equal specificity, so source order decides. */
.button-primary:disabled,
.button-secondary:disabled {
  background: var(--surface-sunken);
  color: var(--text-muted);
  cursor: not-allowed;
}

/* Only the secondary carries a border, so only it fades one — giving the
   primary a border on disable would shift it two pixels. */
.button-secondary:disabled {
  border-color: var(--border);
}

/* DESIGN.md §11.3 — a button that moves money does not look like a button
   that saves a form. Wider, taller, with an icon, so approving is never a
   thing you do by muscle memory on the way to somewhere else. */
.button-financial {
  height: 44px;
  min-width: 200px;
  padding: 0 20px;
  gap: 8px;
  justify-content: center;
  font-size: 15px;
  font-weight: 600;
}

/* §11 destructive actions live behind a disclosure, never as a bare button
   beside Edit. <details> gives us that with no JavaScript. */
.actions-menu {
  position: relative;
  margin-top: 24px;
}

.actions-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font-size: 14px;
  cursor: pointer;
  list-style: none;
  width: max-content;
}

.actions-menu > summary::-webkit-details-marker { display: none; }

.actions-menu[open] > summary svg { transform: rotate(180deg); }

.actions-menu-panel {
  margin-top: 8px;
  padding: 16px;
  max-width: 480px;
  border: 1px solid var(--critical);
  border-radius: 8px;
  background: var(--surface);
}

.actions-menu-panel h2 {
  font-size: 15px;
  margin: 0 0 8px;
}

.actions-menu-panel p {
  color: var(--text-secondary);
  margin: 0 0 12px;
}

/* Type-to-confirm: §11 requires typing the invoice number for anything
   financial, so the button stays disabled until the field matches. */
.confirm-phrase {
  height: 36px;
  width: 100%;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  font: inherit;
  font-variant-numeric: tabular-nums;
  margin-bottom: 12px;
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  margin-top: 16px;
}

.card h2 {
  font-size: 15px;
  margin: 0 0 12px;
}

/* DESIGN.md §10.2 — the property photo gallery. Fixed-height tiles so a
   mix of portrait and landscape shots still reads as a grid; object-fit
   crops rather than distorting. */
.card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.card-header h2 {
  margin: 0;
}

.card-header .count {
  color: var(--text-muted);
  font-weight: 400;
}

/* §10.2 expenses — three figures side by side, each labelled with what it
   actually means. Never merged into one total (§5.8). */
.expense-totals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 16px;
  padding: 12px 0 16px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 12px;
}

.expense-figure {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.expense-label {
  font-size: 13px;
  color: var(--text-secondary);
}

.expense-value {
  font-size: 20px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.expense-note {
  font-size: 12px;
  color: var(--text-muted);
}

/* §10.2 — a photo opens in place rather than a new tab. Dark ground on
   purpose: the frame should recede so the photograph is the only lit thing,
   which is the one place in Baxter that does not follow the page palette. */
.photo-viewer {
  border: 0;
  padding: 0;
  /* Fixed, not max-: sized to the image, the box jumped between a wide
     landscape shot and a narrow portrait one, and the controls moved under
     the cursor between photos. The frame stays put and the photograph is
     letterboxed inside it.
     900 rather than 1100 because the gallery is mostly 4:3 photographs a few
     hundred pixels wide — a bigger frame only upscaled them further.
     `88vh` is the third term because the frame below is 4:3 of this width:
     capping the *height* instead would clamp the frame and leave the box
     wider than 4:3, which put an identical band beside every photo. */
  width: min(900px, 92vw, 88vh);
  background: #111;
  color: #f4f3ef;
  border-radius: 8px;
  overflow: hidden;
}

.photo-viewer::backdrop {
  background: rgba(0, 0, 0, 0.78);
}

.photo-viewer-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  background: #000;
  /* The photo's own shape, bounded — set by photo-viewer.js, 4:3 until it is.
     A fixed ratio banded every screenshot; a free one made the box lurch
     between photos. Bounding it keeps the width constant and moves only the
     height, and only between 4:3 and 2.4:1.
     The lower bound is 4:3 on purpose: it is the tallest shape allowed, so
     the 88vh cap on the width above is enough to keep the box on screen. */
  --photo-ratio: 4 / 3;
  aspect-ratio: var(--photo-ratio);
}

.photo-viewer-frame img {
  /* Fills the frame. `contain` and never `cover`: scaling a photograph up to
     fit is fine, cropping one is not — the part cut off could be the crack
     the photo was taken to record. */
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.photo-viewer-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 14px;
  padding: 12px 16px;
  border-top: 1px solid rgba(244, 243, 239, 0.14);
  /* A photo with no recorded date has one line of meta where another has two;
     without this the frame above appears to shift as you page through. */
  min-height: 68px;
}

/* The dates travel with the photo — cloned from the tile, so they cannot
   disagree with the gallery. */
.photo-viewer-meta {
  flex: 1 1 220px;
  min-width: 0;
}

.photo-viewer-meta .photo-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 12px;
  color: rgba(244, 243, 239, 0.72);
}

.photo-viewer-meta .photo-taken {
  color: #f4f3ef;
  font-weight: 600;
}

.photo-viewer-meta .photo-taken.is-unknown {
  color: rgba(244, 243, 239, 0.6);
  font-weight: 400;
  font-style: italic;
}

.photo-viewer-position {
  font-size: 12px;
  color: rgba(244, 243, 239, 0.6);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.photo-viewer a.link-button {
  color: #9cc4f0;
}

.photo-viewer .button-secondary {
  min-width: 44px;
  background: transparent;
  color: #f4f3ef;
  border-color: rgba(244, 243, 239, 0.28);
}

.photo-viewer .button-secondary[hidden] {
  display: none;
}

.photo-tile a:focus-visible img {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (max-width: 620px) {
  .photo-viewer { width: min(100vw, 72vh); border-radius: 0; }
}

/* A strip, not a grid. A single walkthrough can produce sixty photos, and
   sixty tiles in a wrapping grid is ~2,500px of vertical scroll in the middle
   of a nine-section page — it buries every section beneath it. A strip is one
   row whatever it holds, which is what a hub-page section has to be. */
.photo-strip-wrap {
  position: relative;
}

.photo-strip {
  display: flex;
  gap: 16px;
  list-style: none;
  padding: 0;
  margin: 0;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  /* Reachable and scrollable by keyboard — a scroll container needs the
     tabindex in the markup and this to show where focus is. */
  scroll-padding: 4px;
}

.photo-strip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: 8px;
}

.photo-strip > .photo-tile {
  flex: 0 0 200px;
  scroll-snap-align: start;
}

/* The affordance. `overflow-x: auto` on its own is a trap on a desktop:
   plenty of mice have no horizontal wheel, so everything past the edge is
   simply invisible. photo-strip.js sets data-scroll, and both the fades and
   the buttons key off it — so with no script there is no dead control and
   the strip still scrolls by touch, trackpad and keyboard. */
.photo-strip-wrap::before,
.photo-strip-wrap::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 48px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
}

.photo-strip-wrap::before {
  left: 0;
  background: linear-gradient(to right, var(--surface), transparent);
}

.photo-strip-wrap::after {
  right: 0;
  background: linear-gradient(to left, var(--surface), transparent);
}

.photo-strip-wrap[data-scroll="middle"]::before,
.photo-strip-wrap[data-scroll="end"]::before,
.photo-strip-wrap[data-scroll="middle"]::after,
.photo-strip-wrap[data-scroll="start"]::after {
  opacity: 1;
}

.photo-strip-nav {
  position: absolute;
  top: 72px;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  color: var(--text-primary);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  cursor: pointer;
}

.photo-strip-nav:hover:not(:disabled) {
  background: var(--surface-sunken);
}

.photo-strip-nav:disabled {
  opacity: 0;
  pointer-events: none;
}

.photo-strip-nav.is-prev {
  left: -12px;
}

.photo-strip-nav.is-next {
  right: -12px;
}

@media (prefers-reduced-motion: reduce) {
  .photo-strip-wrap::before,
  .photo-strip-wrap::after {
    transition: none;
  }
}

.photo-tile {
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--surface-sunken);
}

.photo-tile img {
  display: block;
  width: 100%;
  height: 160px;
  object-fit: cover;
  background: var(--surface-sunken);
}

.photo-meta {
  padding: 8px;
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.photo-taken {
  color: var(--text-primary);
  font-weight: 500;
}

/* An unrecorded date reads as absent, not as a value — §3.7. */
.photo-taken.is-unknown {
  color: var(--text-muted);
  font-weight: 400;
  font-style: italic;
}

.photo-uploaded {
  color: var(--text-muted);
}

.fact-list {
  display: grid;
  grid-template-columns: 160px 1fr;
  row-gap: 8px;
  margin: 0;
}

.fact-list dt {
  color: var(--text-secondary);
  font-size: 13px;
}

.fact-list dd {
  margin: 0;
}

/* §3.7 — a figure's date and source sit with it, never in a separate
   column, so a value can never be read apart from how current it is. */
.fact-provenance {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
}

.stacked-form {
  max-width: 480px;
}

.form-actions {
  display: flex;
  gap: 8px;
  margin-top: 16px;
}

.stat-row {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin-bottom: 8px;
}

.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
}

.stat-label {
  font-size: 13px;
  color: var(--text-secondary);
  margin-bottom: 4px;
}

.stat-value {
  font-size: 28px;
  font-weight: 600;
}

.agenda-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}

.agenda-row:first-of-type {
  border-top: none;
}

.agenda-date {
  /* Wide enough for the longest form it renders — "04 Aug 2026" — and
     `nowrap` so a narrow viewport folds the description instead. It was
     56px, which wrapped the date onto a second line and made every overdue
     row two lines tall. */
  width: 96px;
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 13px;
  color: var(--text-secondary);
}

.agenda-kind {
  width: 128px;
  flex-shrink: 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
}

.agenda-what {
  flex: 1;
}

.agenda-sub {
  color: var(--text-muted);
  font-size: 12px;
}

/* DESIGN.md §5.5 — money in is not a negative number and never red. The
   success step plus the word "In", so direction is never colour alone
   (§13). Money out keeps primary ink: an outgoing payment is not an error. */
.agenda-amount.is-money-in {
  color: var(--success-text);
}

/* The sale lifecycle actions on the property page. A row of fields rather
   than a stacked form: each action is two or three values, and a full-page
   form for "record the contract" would be a screen for a sentence. */
.sale-action {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.sale-action .field {
  margin: 0;
}

/* A read that failed. Serious rather than critical: nothing has gone wrong
   with the money, and the document can still be entered by hand — §5.5
   reserves critical for overdue and failed payments. */
.read-failed {
  display: block;
  margin-top: 4px;
  font-size: 12px;
  color: var(--serious-text);
}

.read-failed-why {
  display: block;
  color: var(--text-muted);
}

.direction-label {
  margin-left: 4px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.01em;
}

.agenda-amount {
  font-variant-numeric: tabular-nums;
  flex-shrink: 0;
  text-align: right;
  min-width: 120px;
}

/* DESIGN.md §9 — the dashboard splits below the figures: the agenda takes
   the width it needs and "Needs your attention" sits beside it.

   The panel is FIRST in source and second on screen (grid-column places it),
   which is deliberate. It holds stalled states that no filter touches, and
   the kind chips live inside .dash-main — announcing a control ahead of
   something it does not change is the same mistake in reading order that
   printing it above would be visually. */
.dash-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 24px;
  align-items: start;
}

.dash-split > .dash-main {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
}

.dash-split > .attention-panel {
  grid-column: 2;
  grid-row: 1;
}

/* §14 — the sidebar collapses at 1280px and the width for a second column
   goes with it. Stacked, the panel lands above the list, which is where it
   sat before the split and is the honest fallback: source order already
   puts it first, so nothing needs reordering. */
@media (max-width: 1280px) {
  .dash-split {
    grid-template-columns: minmax(0, 1fr);
  }

  .dash-split > .dash-main,
  .dash-split > .attention-panel {
    grid-column: 1;
    grid-row: auto;
  }
}

/* DESIGN.md §9 — "Needs your attention". Rows rather than a list, so an
   item reads as one line at a glance; 44px minimum per §4.3's touch target
   since these are the page's most-clicked links. The panel is warning-toned
   throughout rather than per-row: everything in it is one severity, and
   colouring individual rows would imply a ranking that isn't there. */
.attention-panel {
  border-color: var(--warning);
}

.attention-panel h2 {
  color: var(--warning-text);
}

.attention-row {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-height: 44px;
  padding: 10px 0;
  border-top: 1px solid var(--border);
}

.attention-row:first-of-type {
  border-top: none;
}

.attention-icon {
  flex-shrink: 0;
  color: var(--text-muted);
  line-height: 1;
  padding-top: 2px;
}

.attention-what {
  flex: 1;
}

.attention-hint {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
}

.stat-note {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-muted);
}

/* Two figures rather than the dashboard's four, so they are capped instead
   of stretched — a pair of half-width tiles reads as a banner. */
.stat-row-pair {
  grid-template-columns: repeat(2, minmax(0, 280px));
  margin-bottom: 24px;
}

/* Expected, Received and Cost. Capped so two figures do not stretch into a
   banner, and wrapping rather than shrinking below three columns. */
.stat-row-money {
  grid-template-columns: repeat(auto-fit, minmax(240px, 280px));
  margin-bottom: 24px;
}

/* Money in takes the success step, never the plain one: §5.5 reserves red
   for wrong rather than negative, and green here says "arrived". */
/* A loss. §5.5 reserves the critical step for money that has gone wrong, and
   a property that returned less than it cost qualifies. Never colour alone —
   the minus sign is in the figure itself. */
.stat-value-critical {
  color: var(--critical-text);
}

.stat-value-in {
  color: var(--success-text);
}

/* A withdrawn listing's asking price is history, not money owed or
   expected. Stepped back rather than hidden — it is still the record of
   what the property was offered at. */
.amount-ended {
  color: var(--text-muted);
}

/* DESIGN.md §11 — destructive actions are visually separated from the
   primary flow, never sitting adjacent to ordinary edit controls. */
.danger-zone {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.button-danger {
  display: inline-flex;
  align-items: center;
  height: 36px;
  padding: 0 12px;
  border-radius: 6px;
  font-weight: 500;
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  background: var(--surface);
  color: var(--critical);
  border: 1px solid var(--critical);
}

.button-danger:hover {
  background: var(--critical);
  color: white;
}

/* Something a person still owes, like removing a voided bill from the books:
   filled in the warning colour so it cannot be read as optional. Near-black
   text, not --warning-text, which is under 4.5:1 on full-strength --warning. */
.button-warning {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 36px;
  padding: 0 12px;
  border-radius: 6px;
  font-weight: 600;
  font-size: 14px;
  font-family: inherit;
  cursor: pointer;
  background: var(--warning);
  color: var(--text-primary);
  border: 1px solid color-mix(in oklab, var(--warning) 70%, black);
}

.button-warning:hover {
  background: color-mix(in oklab, var(--warning) 85%, black);
}

.button-warning:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.form-section {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
}

.form-section legend {
  font-size: 13px;
  font-weight: 600;
  padding: 0 6px;
  color: var(--text-secondary);
}

/* Payment details — visually marked so it's obvious this block is the one
   place full numbers appear. CLAUDE.md §5.8. */
.form-section.sensitive {
  border-color: var(--border-strong);
  background: var(--surface-sunken);
}

.field-help {
  font-size: 12px;
  color: var(--text-muted);
  margin: 4px 0 0;
}

/* Errors also render under each field, where they are fixed. This is the
   summary that says a save was refused at all — a nine-field form can put
   its only failure below the fold. */
.form-error-summary {
  margin: 0 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--critical);
  border-left-width: 3px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--critical) 8%, var(--surface));
  color: var(--critical-text);
  font-size: 13px;
}

.form-error-lead {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.form-error-summary ul {
  margin: 6px 0 0;
  padding-left: 26px;
}

.form-error-summary li {
  margin-top: 2px;
}

.form-error-summary a {
  color: inherit;
}

/* A value the page you came from already knows — shown as a fact, not a
   control, so it cannot be changed into the wrong property by mistake. */
.field-fixed-label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-secondary);
  margin-bottom: 4px;
}

.field-fixed {
  margin: 0;
  padding: 10px 12px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-primary);
}

.field-inline {
  display: flex;
  align-items: center;
  gap: 8px;
}

.field-inline input[type="checkbox"],
.field-inline input[type="radio"] {
  flex: none;
  width: auto;
  height: auto;
  margin: 0;
}

.field-inline label {
  margin-bottom: 0;
}

/* Document confirmation — the source document beside the editable fields,
   so a person can check every figure against the page it came from
   (DESIGN.md §10.6). */
.confirm-panes {
  display: grid;
  grid-template-columns: 1fr 480px;
  gap: 20px;
  align-items: start;
}

.confirm-preview {
  position: sticky;
  top: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

/* The document, on its own page. Same sandboxed route the confirmation
   screen uses; not sticky, because there is no second column here for it to
   stay level with. */
.doc-preview {
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}

.doc-preview iframe,
.doc-preview img {
  display: block;
  width: 100%;
  height: 720px;
  border: 0;
  background: var(--surface-sunken);
}

.doc-preview img {
  height: auto;
  max-height: 720px;
  object-fit: contain;
}

.confirm-preview iframe,
.confirm-preview img {
  display: block;
  width: 100%;
  height: 720px;
  border: 0;
  background: var(--surface-sunken);
}

.confirm-preview img {
  height: auto;
  max-height: 720px;
  object-fit: contain;
}

.confirm-form {
  max-width: none;
}

/* Confidence sits next to the value it describes, never on its own —
   CLAUDE.md §3.7 wants source and certainty visible wherever a figure
   informs money. */
.confidence-note {
  font-size: 12px;
  color: var(--text-muted);
  margin: 4px 0 0;
}

.confidence-note.is-low {
  color: var(--warning-text);
  font-weight: 500;
}

.provenance {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 12px;
}

/* ---------------------------------------------------------------------
   DESIGN.md §14 — responsive behaviour.

   The phone case the spec names is "checking a status and approving a bill
   on the way to a closing", so the approval flow has to survive all the
   way down while bulk entry does not.
   --------------------------------------------------------------------- */

/* 1024-1279px — sidebar collapses to icons, detail panels stack. §15 bars
   icon-only nav *without labels*, so the label is kept for screen readers
   and surfaced as a tooltip rather than deleted. */
@media (min-width: 769px) and (max-width: 1279px) {
  /* collapsed-block: breakpoint */
  .sidebar {
    width: 72px;
  }

  .sidebar .nav-label,
  .sidebar .sidebar-brand-text,
  .sidebar-footer .user-name {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .nav-link {
    display: flex;
    justify-content: center;
    padding: 10px 0;
  }

  /* The toggle keeps its 28px box — it is a bordered button, so repadding it
     the way a nav link is repadded would crush the glyph inside the border. */
  .sidebar-toggle {
    margin-left: 0;
  }

  .nav-link-apart {
    margin-top: 12px;
    padding-top: 12px;
  }

  .sidebar-brand {
    flex-direction: column;
    gap: 8px;
    justify-content: center;
    padding: 0 0 16px;
  }

  /* A dot, not a number: at 72px the count would sit on top of the glyph it
     is meant to annotate. `font-size: 0` hides the digits without removing
     them from the accessible name. */
  .nav-link .nav-badge {
    margin-left: 0;
    position: absolute;
    transform: translate(12px, -10px);
    min-width: 0;
    width: 7px;
    height: 7px;
    padding: 0;
    border: 1.5px solid var(--surface);
    border-radius: 5px;
    font-size: 0;
  }

  .sidebar-footer {
    padding: 16px 8px;
    text-align: center;
  }

  .account-menu > summary {
    justify-content: center;
    gap: 0;
    padding: 6px 0;
  }

  .account-caret {
    display: none;
  }
  /* end collapsed-block */

  /* The sidebar is already icons-only here, so the toggle has nothing to
     do. A control that appears inert is worse than no control. */
  html.sidebar-toggle-ready .sidebar-toggle {
    display: none;
  }

  .fact-list {
    grid-template-columns: 1fr;
    row-gap: 4px;
  }

  .fact-list dt {
    margin-top: 8px;
  }
}

@media (max-width: 1100px) {
  .confirm-panes {
    grid-template-columns: 1fr;
  }

  .confirm-preview {
    position: static;
  }
}

/* DESIGN.md §14, the 768-1023px band: "tables drop to priority columns via
   the column picker's defaults". The picker was judged scaffolding for a
   table that does not exist yet, so this implements the *defaults* without
   it — the columns a table can lose and still answer its own question are
   marked in the markup and dropped here.

   Only in this band. Below 768 the table stacks into cards, where vertical
   space is free and every field should show. */
@media (min-width: 769px) and (max-width: 1023px) {
  .col-optional {
    display: none;
  }
}

/* Below 768px — tables become stacked cards, label above value. The label
   comes from each cell's data-label, so there is exactly one source of
   truth for a column heading. Dashboard drops to one column. */
@media (max-width: 768px) {
  /* The sidebar becomes an off-canvas drawer. Neither collapse applies at
     this width (both are floored at 769px), so these are plain rules rather
     than undoing a state that no longer exists — and the labels come back
     in full, which is how §15 is satisfied on a device with no hover to
     surface a title tooltip. */
  .app-shell {
    flex-direction: column;
  }

  .mobile-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    position: sticky;
    top: 0;
    z-index: 30;
    padding: 8px 16px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
  }

  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 50;
    width: 240px;
    transform: translateX(-100%);
    /* Off the screen *and* out of the tab order — a translated drawer is
       still focusable, so a keyboard user would tab into an invisible
       menu. */
    visibility: hidden;
    transition: transform 150ms ease;
  }

  html.nav-open .sidebar {
    transform: none;
    visibility: visible;
  }

  html.nav-open .nav-scrim {
    display: block;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 40;
    background: rgba(11, 11, 11, 0.4);
  }

  /* §4.3 — 44px touch targets. The rail's 36px rows were fine for a mouse. */
  .nav-link {
    min-height: 44px;
  }

  /* The collapse control belongs to a rail that does not exist here. */
  html.sidebar-toggle-ready .sidebar-toggle {
    display: none;
  }

  .content {
    padding: 16px;
  }

  .stat-row {
    grid-template-columns: 1fr;
  }

  table.stacked,
  table.stacked thead,
  table.stacked tbody,
  table.stacked tr,
  table.stacked td {
    display: block;
    width: 100%;
  }

  table.stacked {
    border: 0;
    background: none;
  }

  table.stacked thead {
    /* The column headings move into each cell via data-label. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  table.stacked tr {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 8px 12px;
    margin-bottom: 12px;
  }

  table.stacked td {
    height: auto;
    padding: 6px 0;
    border-top: 0;
    text-align: left;
  }

  table.stacked td + td {
    border-top: 1px solid var(--border);
  }

  table.stacked td::before {
    content: attr(data-label);
    display: block;
    font-size: 12px;
    font-weight: 500;
    letter-spacing: 0.01em;
    color: var(--text-secondary);
    margin-bottom: 2px;
  }

  /* An amount stays right-aligned in its own cell even stacked — it reads
     as a figure, not a sentence. */
  table.stacked td.col-amount {
    text-align: right;
  }

  .page-header {
    flex-direction: column;
    gap: 12px;
  }

  /* The search takes a line, the selects share the next one. Stacking every
     control full width made a four-line tower out of a filter row. */
  .filter-row {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }

  /* The default: a line of its own. A row with two or three selects cannot
     fit the search beside them on a phone without leaving it unusably
     narrow. `.single-select` below is the exception, not this. */
  .filter-search {
    flex: 1 0 100%;
    max-width: none;
    min-width: 0;
    /* `input[type="search"]` gets `-webkit-appearance: searchfield`, which
       lets Safari impose its own box and ignore the height below — which is
       why it sat shorter than the select beside it. Deliberately not on the
       select: the same reset strips its dropdown arrow. */
    appearance: none;
    -webkit-appearance: none;
  }

  /* Sized to its own content and never grown: the search is the only item
     that takes up slack, so it gets every spare pixel. A select that grows
     ends up wider than its longest option, which shows as dead space
     between the label and the arrow. It may still shrink on a narrow
     phone. */
  .filter-select {
    flex: 1 1 140px;
    width: auto;
    min-width: 0;
  }

  /* The search shares its line with the first select instead of claiming one
     of its own. Opted into per template, never inferred: on a row with three
     selects it saves a line (search+select, then two) and on sales' single
     select it saves the only extra one.
     The select is capped because it sizes to its *longest option*, not the
     selected one: sales' "Property address" is ~184px of a 328px row, so
     uncapped it took more than half whatever was showing and left dead space
     beside a short selection. Under half, the search keeps the majority; the
     longest option clips and tapping still shows it in full. */
  .filter-row.inline-search .filter-search {
    flex: 1 1 160px;
  }

  .filter-row.inline-search .filter-select {
    flex: 0 1 auto;
    max-width: 40%;
  }

  .filter-search,
  .filter-select {
    /* 36px at every width until now, which is under §13's 44px touch
       minimum on the one device where it is a touch target. */
    height: 44px;
    /* Under 16px, iOS Safari zooms the page when the field takes focus.
       Everything else inherits the 14px body font. */
    font-size: 16px;
  }

  /* The other two things in a filter strip, for the same reason — and so a
     36px tab does not sit beside a 44px search box. */
  .filter-tab,
  .chip-link {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  /* Nowrap lets a flex item shrink, which squashes a label to its longest
     word. A strip that scrolls must not also compress. */
  .filter-tab,
  .chip-row .chip-link {
    flex-shrink: 0;
  }

  /* §14 — the tabs scroll rather than wrapping into four rows on a phone. */
  .filter-tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
  }

  .button-financial {
    width: 100%;
  }
}

/* Privacy and terms: public, readable text, the same signed in or not. */
.legal-shell {
  max-width: 720px;
  margin: 0 auto;
  padding: 32px 16px 64px;
  display: grid;
  gap: 24px;
}

.legal-brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  color: var(--text-primary);
  text-decoration: none;
}

.legal-page {
  max-width: 68ch;
  display: grid;
  gap: 28px;
  line-height: 1.6;
}

.legal-page h1 {
  margin: 0;
  font-size: 28px;
}

.legal-page h2 {
  margin: 0 0 8px;
  font-size: 18px;
}

.legal-page p,
.legal-page ul {
  margin: 0 0 8px;
}

.legal-page ul {
  padding-left: 20px;
  display: grid;
  gap: 6px;
}

.legal-head {
  display: grid;
  gap: 6px;
}

.legal-updated {
  color: var(--text-muted);
  font-size: 13px;
}

.legal-lede {
  color: var(--text-secondary);
}

.legal-foot {
  color: var(--text-secondary);
  border-top: 1px solid var(--border);
  padding-top: 16px;
}

.auth-page {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  min-height: 100vh;
}

.auth-legal {
  margin: 0;
  display: flex;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
}

.auth-legal a {
  color: var(--text-secondary);
}

.auth-card {
  width: 400px;
  padding: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.auth-card h1 {
  font-size: 18px;
  margin: 0 0 16px;
}

.auth-lede {
  margin: 0 0 16px;
  color: var(--text-secondary);
  line-height: 1.5;
}

.auth-links {
  margin: 16px 0 0;
  font-size: 13px;
}

.field {
  margin-bottom: 12px;
}

.field label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  margin-bottom: 4px;
}

.field input {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  font: inherit;
}

/* Textareas had no rule at all, so every one in the app rendered as a raw
   browser default — monospace, its own border — beside inputs that were
   styled. Same class of thing as the five screens that grew their own
   `{{ form.as_p }}`. `textarea.checklist-textarea` keeps its deliberate
   monospace by matching this selector's specificity and coming later. */
.field textarea {
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  font: inherit;
  line-height: 1.5;
  resize: vertical;
}

/* Selects had the same gap as textareas — every dropdown in every form was
   a browser default beside a 36px styled input. No `appearance: none`: that
   strips the native arrow, and the filter row already learned that. */
.field select {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  font: inherit;
  background: var(--surface);
  color: var(--text-primary);
}

/* The auth cards' button used to be its own `button.primary` rule — a second
   primary button, without the `:disabled` look the house one has. */
.button-block {
  width: 100%;
  justify-content: center;
}

/* A disabled destructive button reads as unavailable, not as an error
   waiting to happen — §11's type-to-confirm gate. */
.button-danger:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.actions-menu-panel label {
  display: block;
  font-size: 13px;
  font-weight: 500;
  margin-bottom: 4px;
}

/* §10.6's action row — Skip sits left, the save action right, so the
   destructive-adjacent choice and the commit are not neighbours. */
.confirm-actions {
  align-items: center;
  flex-wrap: wrap;
}

.confirm-actions-spacer {
  flex: 1;
}

/* ---------------------------------------------------------------------
   DESIGN.md §10.8 — the inspection walkthrough.

   The one phone-first screen in Baxter, and §14's explicit exception:
   everywhere else the phone is for looking things up, not entering data.
   This one is performed standing in a house with one hand free, so it is
   written mobile-first and *widens* on a desktop rather than collapsing on
   a phone. That is the reverse of every other screen here.

   Nothing in this block encodes a verdict. The progress bar measures how
   much of the walk is done — never how ready the property is (CLAUDE.md
   §2.6).
   --------------------------------------------------------------------- */

.walkthrough {
  /* Deliberately NOT centred. `.content` is left-aligned like every other
     screen, so a centred column here floated in the middle of a wide page
     while everything else started at the left edge — it read as a mistake
     rather than as a choice. The measure that matters is the item text,
     which is capped on `.finding-text`, not the page. */
  max-width: 1100px;
}

/* §10.8 — "on desktop the same screen widens to two columns". The walk
   itself is linear, so the second column is not a detail pane for a
   selected item: it is the section nav and the finish action, which are
   the two things a walker reaches for out of order. On a phone they
   stack, and the finish action stays *after* the list where it belongs —
   grid areas rather than source order, so the reading order is right at
   both widths. */
.walkthrough-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "jump"
    "main"
    "finish";
  gap: 0 24px;
}

.walkthrough-header {
  position: sticky;
  top: 0;
  z-index: 5;
  /* Opaque, not translucent: item text scrolling under a see-through
     header is exactly the sort of misreading §2.1 warns about. */
  background: var(--page);
  padding: 12px 0 16px;
  border-bottom: 1px solid var(--border);
}

.walkthrough-back {
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
}

.walkthrough-header h1 {
  font-size: 20px;
  margin: 4px 0 2px;
}

.walkthrough-provenance {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-secondary);
}

.walkthrough-closed {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: 6px;
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: 13px;
}

/* --- counts ------------------------------------------------------- */

.inspection-progress-text {
  margin: 0 0 6px;
  font-size: 14px;
  color: var(--text-secondary);
}

.count-attention {
  color: var(--serious);
  font-weight: 500;
}

.inspection-progress {
  height: 8px;
  border-radius: 4px;
  background: var(--surface-sunken);
  overflow: hidden;
}

.inspection-progress > span {
  display: block;
  height: 100%;
  /* Accent, deliberately not --good. A full bar means every item was
     answered, not that every answer was a pass. */
  background: var(--accent);
  border-radius: 4px;
}

.inspection-tallies {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  font-size: 12px;
  color: var(--text-muted);
}

.tally-figure {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: var(--text-primary);
}

.tally-figure.tally-attention {
  color: var(--serious);
}

/* --- jump list ---------------------------------------------------- */

.walkthrough-jump {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 12px 0;
  -webkit-overflow-scrolling: touch;
}

.walkthrough-jump a {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font-size: 13px;
  text-decoration: none;
  white-space: nowrap;
}

.jump-attention {
  display: inline-block;
  padding: 1px 6px;
  border-radius: 10px;
  background: color-mix(in oklab, var(--serious) 16%, var(--surface));
  color: var(--serious);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* --- sections ----------------------------------------------------- */

.walkthrough-section {
  margin-bottom: 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}

.walkthrough-section > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* 44px minimum touch target — DESIGN.md §13. */
  min-height: 44px;
  padding: 10px 14px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 600;
  list-style: none;
}

.walkthrough-section > summary::-webkit-details-marker {
  display: none;
}

.walkthrough-section > summary::after {
  content: "";
  width: 8px;
  height: 8px;
  border-right: 2px solid var(--text-muted);
  border-bottom: 2px solid var(--text-muted);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}

.walkthrough-section[open] > summary::after {
  transform: rotate(-135deg);
}

.section-meta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  font-size: 12px;
  font-weight: 400;
  color: var(--text-muted);
}

.section-count {
  font-variant-numeric: tabular-nums;
}

/* --- one item ----------------------------------------------------- */

.finding {
  padding: 14px;
  border-top: 1px solid var(--border);
}

.finding-text {
  /* The one thing that needs a measure. Sixty items of prose running the
     full width of a desktop is harder to walk, not easier. */
  max-width: 68ch;
  margin: 0 0 10px;
  font-size: 15px;
  line-height: 1.45;
  color: var(--text-primary);
}

.finding-states {
  display: grid;
  /* Two columns on a phone, four when there is room. Four 44px targets
     side by side do not fit a 360px screen without shrinking them below
     §13's minimum, and shrinking the target is the wrong trade on the one
     screen operated one-handed. */
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.finding-state {
  min-height: 44px;
  padding: 0 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
}

.finding-state:hover {
  border-color: var(--text-muted);
}

/* The chosen state is never colour alone — it also gains weight and a
   ring, per §7 and §13. */
.finding-state.is-chosen {
  font-weight: 600;
  color: var(--text-primary);
  border-color: currentColor;
  box-shadow: inset 0 0 0 1px currentColor;
}

.finding-state[data-state="ok"].is-chosen {
  background: color-mix(in oklab, var(--good) 14%, var(--surface));
  color: var(--success-text);
}

.finding-state[data-state="needs_attention"].is-chosen {
  background: color-mix(in oklab, var(--serious) 16%, var(--surface));
  color: var(--serious);
}

.finding-state[data-state="not_applicable"].is-chosen,
.finding-state[data-state="not_checked"].is-chosen {
  background: var(--surface-sunken);
  color: var(--text-secondary);
}

/* "Not checked" must never look like a pass, and must not look like an
   empty slot either — an unanswered item is a real state with a real
   consequence (CLAUDE.md §3.3). A dashed edge reads as deliberately
   open. */
.finding-state[data-state="not_checked"] {
  border-style: dashed;
}

.finding-note-label {
  display: block;
  margin-top: 10px;
}

.finding-note {
  width: 100%;
  min-height: 44px;
  padding: 10px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-primary);
  font: inherit;
  font-size: 14px;
  resize: vertical;
}

.finding-note-read {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  background: var(--surface-sunken);
  font-size: 14px;
  color: var(--text-secondary);
}

.finding-status {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0;
  font-size: 12px;
}

.finding-meta {
  color: var(--text-muted);
}

/* §10.8 — "each answer posts on tap with its own indicator; an unsaved
   item is visibly unsaved". The indicator belongs to the item, not the
   page: sixty items each saving separately must not share one spinner. */
.finding-saving {
  opacity: 0;
  color: var(--text-muted);
}

.finding.htmx-request .finding-saving {
  opacity: 1;
}

.finding.htmx-request {
  /* Visibly in flight, without moving anything — the walker's thumb is
     already where the next item will be. */
  background: var(--surface-sunken);
}

.finding[data-dirty="true"] .finding-note {
  border-color: var(--warning);
}

.walkthrough-finish {
  margin: 24px 0 48px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}

.walkthrough-finish .field-help {
  margin: 8px 0 12px;
}

.seed-command {
  padding: 10px 12px;
  border-radius: 6px;
  background: var(--surface-sunken);
  font-size: 13px;
  overflow-x: auto;
}

.walkthrough-jump {
  grid-area: jump;
}

.walkthrough-main {
  grid-area: main;
  min-width: 0;
}

.walkthrough-finish {
  grid-area: finish;
}

/* Four 44px targets side by side do not fit a 360px screen without going
   under §13's minimum, so they are 2x2 on a phone and a single row from
   768px. Shrinking the target is the wrong trade on the one screen
   operated one-handed. */
@media (min-width: 768px) {
  .finding-states {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

@media (min-width: 1024px) {
  .walkthrough-layout {
    grid-template-columns: minmax(0, 1fr) 260px;
    grid-template-areas:
      "main jump"
      "main finish";
    grid-template-rows: auto 1fr;
  }

  /* A vertical section nav rather than a horizontal scroller — there is
     room for it, and a list of eight sections with their attention counts
     is easier to aim at than a strip that scrolls sideways. */
  /* Deliberately not sticky. The header above it is sticky and its height
     changes when the provenance line wraps, so any `top` offset here is a
     guess that is wrong at some widths — and a nav that overlaps the
     header is worse than one that scrolls. The progress figures stay on
     screen regardless, which is what §10.8 actually requires. */
  .walkthrough-jump {
    flex-direction: column;
    align-items: stretch;
    overflow-x: visible;
    align-self: start;
  }

  .walkthrough-jump a {
    justify-content: space-between;
  }

  .walkthrough-finish {
    align-self: start;
  }
}

/* --- photos against an item — DESIGN.md §10.8 -------------------------- */

.finding-photos {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
}

.finding-photos img {
  width: 72px;
  height: 72px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid var(--border);
  display: block;
}

.finding-photo-form {
  margin-top: 10px;
}

/* The input is visually hidden inside its own label, so the label is the
   control. Not `display: none` — that would take it out of the
   accessibility tree and off the keyboard, which is the same mistake the
   collapsed sidebar avoids with its labels. */
.finding-photo-add {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px;
  border: 1px dashed var(--border-strong);
  border-radius: 6px;
  color: var(--text-secondary);
  font-size: 14px;
  cursor: pointer;
}

.finding-photo-add:hover {
  border-color: var(--text-muted);
  color: var(--text-primary);
}

.finding-photo-add:focus-within {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
}

.finding-photo-add input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.finding-error {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 6px;
  background: color-mix(in oklab, var(--critical) 12%, var(--surface));
  color: var(--critical);
  font-size: 13px;
}

/* --- the walk as steps — DESIGN.md §10.8 ------------------------------- */

.step-header {
  margin-bottom: 12px;
}

.step-position {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
  text-transform: none;
}

.step-header h2 {
  margin: 2px 0 4px;
  font-size: 18px;
}

.step-progress {
  margin: 0;
  font-size: 13px;
  color: var(--text-secondary);
}

.step-items {
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
}

/* The first item has no rule above it — the card edge is already there. */
.step-items > .finding:first-child {
  border-top: 0;
}

/* --- the step list ----------------------------------------------------- */

.step-link {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font-size: 13px;
  text-decoration: none;
  white-space: nowrap;
}

.step-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  border-radius: 50%;
  background: var(--surface-sunken);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.step-tally {
  margin-left: auto;
  padding-left: 8px;
  color: var(--text-muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

/* Finished: a tick, and the tick is the signal — not the colour. */
.step-link.is-complete .step-mark {
  background: color-mix(in oklab, var(--good) 18%, var(--surface));
  color: var(--success-text);
}

/* Current: rail, weight and tint together, matching the sidebar's
   current-destination treatment. State is never carried by colour alone
   (§7, §13). */
.step-link.is-current {
  color: var(--text-primary);
  font-weight: 600;
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
  border-color: color-mix(in oklab, var(--accent) 40%, var(--border-strong));
  /* An inset rail rather than a border, so nothing shifts by 3px when the
     step changes — the same reason the sidebar uses one. */
  box-shadow: inset 3px 0 0 var(--accent);
}

.step-link.is-current .step-mark {
  background: var(--accent);
  color: #fff;
}

/* Locked: not a link, and says so to a screen reader as well as to the eye. */
.step-link.is-locked {
  border-style: dashed;
  color: var(--text-muted);
  cursor: not-allowed;
  opacity: 0.7;
}

/* --- moving between steps ---------------------------------------------- */

.step-footer {
  margin: 16px 0 48px;
}

.step-move {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.step-move .button-primary,
.step-move .button-secondary {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* A closed Next that explains itself. A disabled control saying nothing is
   a dead end; this one names what is outstanding, and the sentence beneath
   says how to clear it honestly. */
.button-primary.is-disabled {
  background: var(--surface-sunken);
  color: var(--text-muted);
  border: 1px dashed var(--border-strong);
  cursor: not-allowed;
  padding: 0 16px;
}

.step-blocked {
  margin: 12px 0 0;
  padding: 10px 12px;
  border-radius: 6px;
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1.5;
}

@media (min-width: 1024px) {
  .step-link {
    width: 100%;
  }
}

.step-abandon {
  margin-top: 16px;
  text-align: center;
}

/* --- the checklist editor --------------------------------------------- */

textarea.checklist-textarea {
  width: 100%;
  /* The list is prose, and prose is read in a proportional face — but the
     format is line-based, and a monospace column makes a stray blank line
     or a trailing space visible, which is exactly the mistake that turns
     an item into a section heading. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.6;
  padding: 12px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-primary);
  resize: vertical;
}

.section-summary {
  margin: 0;
  padding: 0;
  list-style: none;
}

.section-summary li {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
}

.section-summary li:last-child {
  border-bottom: 0;
}

.checklist-items {
  margin: 0;
  padding-left: 20px;
  font-size: 14px;
  line-height: 1.6;
}

.checklist-items li {
  padding: 4px 0;
}

.checklist-item-section {
  display: inline-block;
  min-width: 150px;
  color: var(--text-muted);
  font-size: 12px;
}

/* --- the work plan ----------------------------------------------------- */

/* §10.2 — closing a round of work. The actions sit under the list they
   govern, and earlier plans stay on the page so closing one is not a dead
   end for its jobs. */
.work-plan-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.work-earlier {
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.work-earlier h3 {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
  margin: 0 0 8px;
}

.work-earlier-plan {
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}

.work-earlier-plan:last-of-type {
  border-bottom: 0;
}

.work-earlier-plan > summary {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  min-height: 32px;
}

.work-earlier-meta {
  font-size: 12px;
  color: var(--text-muted);
}

.work-earlier-items {
  margin: 8px 0 0;
  padding-left: 20px;
  font-size: 13px;
  color: var(--text-secondary);
}

.work-earlier-items li {
  margin-bottom: 4px;
}

.work-summary {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--text-secondary);
}

.work-summary strong {
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.work-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* A row, not a table cell. A job's scope is a sentence, and the columns
   beside it are mostly empty when the job is first raised. */
.work-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

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

.work-main {
  min-width: 0;
}

.work-scope {
  margin: 0 0 2px;
  font-size: 14px;
  line-height: 1.45;
  color: var(--text-primary);
}

.work-meta {
  margin: 0;
  font-size: 12px;
  color: var(--text-muted);
}

/* "Nobody assigned" and "No estimate" are facts, not faults. Muted and
   italic so they read as an absence rather than as a warning — a job
   nobody has priced yet is the normal state of a job just raised. */
.work-unset {
  font-style: italic;
}

.work-origin {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}

.work-side {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.work-done {
  min-height: 32px;
  padding: 0 10px;
  font-size: 13px;
}

/* A finished job stays legible — it is the record of what was done, not
   noise to fade out. Only the scope loses emphasis. */
.work-row.is-closed .work-scope {
  color: var(--text-secondary);
}

.work-awaiting {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.work-awaiting h3 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: 15px;
}

.work-awaiting .field-help {
  margin: 2px 0 8px;
}

@media (max-width: 767px) {
  .work-row {
    flex-direction: column;
    gap: 8px;
  }

  .work-side {
    width: 100%;
    justify-content: space-between;
  }
}

/* Shared page furniture — these were inline styles repeated on every new
   screen, which is how a "style" becomes five slightly different styles. */
.back-link {
  color: var(--text-muted);
  font-size: 13px;
  text-decoration: none;
}

.page-lede {
  margin: 4px 0 0;
  color: var(--text-secondary);
  /* 70ch, matching the other prose measures in this file. At 60ch a single
     sentence of subtitle wrapped, which reads as a paragraph rather than a
     caption. */
  max-width: 70ch;
}

/* Where a job came from. A quiet well, not a warning — provenance, not a
   problem. */
.from-finding {
  margin: 0 0 20px;
  padding: 12px 14px;
  border-radius: 6px;
  background: var(--surface-sunken);
}

.from-finding-label {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 500;
  color: var(--text-muted);
}

.from-finding-item {
  margin: 0;
  font-size: 14px;
  color: var(--text-primary);
}

.from-finding-note {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--text-secondary);
}

/* --- the confirmation dialog — DESIGN.md §11 ---------------------------- */

.confirm-dialog {
  width: min(460px, calc(100vw - 32px));
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  /* §4.3 allows exactly one shadow, for overlays. This is the overlay. */
  box-shadow: 0 8px 24px rgba(11, 11, 11, 0.12);
}

.confirm-dialog::backdrop {
  background: rgba(11, 11, 11, 0.45);
}

/* Opens without a slide or a bounce. This is a pause before something
   irreversible; motion would make it feel incidental, and §2's "boring is a
   feature" applies hardest here. A short fade only, and none at all for
   anyone who has asked for less motion. */
.confirm-dialog[open] {
  animation: confirm-appear 120ms ease-out;
}

@keyframes confirm-appear {
  from {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .confirm-dialog[open] {
    animation: none;
  }
}

.confirm-dialog-form {
  margin: 0;
}

.confirm-dialog-head {
  display: flex;
  gap: 14px;
  padding: 24px 24px 20px;
}

/* The tone glyph. One of the three is shown; the others are not rendered,
   rather than hidden with opacity, so a screen reader never meets the pair
   that do not apply. */
.confirm-dialog-icon > [data-icon-tone] {
  display: none;
}

.confirm-dialog-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 36px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
}

.confirm-dialog[data-tone="primary"] .confirm-dialog-icon {
  background: color-mix(in oklab, var(--accent) 14%, var(--surface));
  color: var(--accent);
}

.confirm-dialog[data-tone="primary"] [data-icon-tone="primary"],
.confirm-dialog[data-tone="warning"] [data-icon-tone="warning"],
.confirm-dialog[data-tone="danger"] [data-icon-tone="danger"] {
  display: block;
}

.confirm-dialog[data-tone="warning"] .confirm-dialog-icon {
  background: color-mix(in oklab, var(--warning) 20%, var(--surface));
  color: var(--warning-text, #8a5600);
}

.confirm-dialog[data-tone="danger"] .confirm-dialog-icon {
  background: color-mix(in oklab, var(--critical) 14%, var(--surface));
  color: var(--critical);
}

.confirm-dialog h2 {
  margin: 6px 0 6px;
  font-size: 17px;
  line-height: 1.3;
}

.confirm-dialog [data-confirm-body] {
  margin: 0;
  color: var(--text-secondary);
  font-size: 14px;
  line-height: 1.55;
}

/* A tinted footer rather than a bare row: it separates the decision from
   the explanation, so the buttons do not read as part of the sentence
   above them. */
.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 16px 24px;
  border-top: 1px solid var(--border);
  background: var(--surface-sunken);
  border-radius: 0 0 7px 7px;
}

.confirm-dialog-actions button {
  min-height: 40px;
  padding: 0 18px;
}

@media (max-width: 480px) {
  .confirm-dialog-actions {
    flex-direction: column-reverse;
  }

  .confirm-dialog-actions button {
    width: 100%;
  }
}

/* --- changing a work item's status ------------------------------------- */

/* Badge above, control below — the badge is what you read, the dropdown is
   what you change. Same shape as the deal contract status. */
.work-side {
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}

.status-form {
  display: flex;
  align-items: center;
  gap: 6px;
}

.status-form select {
  min-height: 34px;
  padding: 0 8px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-secondary);
  font: inherit;
  font-size: 13px;
}

.status-form .button-secondary,
.status-form .button-primary {
  min-height: 34px;
  padding: 0 12px;
  font-size: 13px;
}

@media (max-width: 767px) {
  .work-side {
    align-items: stretch;
  }

  .status-form select {
    flex: 1;
  }
}

/* --- the audit trail ---------------------------------------------------- */

.audit-id {
  color: var(--text-muted);
  font-size: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

/* --- the pager (DESIGN.md §8) -------------------------------------------
   One pager for every list, from templates/_pagination.html.

   `space-between` rather than a centred group on purpose: the position
   label changes width as the page number and total do, and a centred group
   would slide Previous and Next sideways under the cursor between pages.
   Fixed ends mean the button you just pressed is still where you left it. */

.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.pager .button-secondary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

/* An end of the range is shown, never hidden. A control that disappears at
   the boundary makes the other one jump across the row; one that stays put
   and greys out keeps the pager still. */
.pager .button-secondary.is-disabled {
  color: var(--text-muted);
  border-color: var(--border);
  background: var(--surface-sunken);
  cursor: not-allowed;
}

.pager-nav {
  display: flex;
  align-items: center;
  gap: 12px;
}

.pager-position {
  margin: 0;
  text-align: center;
  font-size: 13px;
  color: var(--text-secondary);
  /* Stable width, so Previous and Next do not creep sideways as "Page 9 of
     12" becomes "Page 10 of 12". Same reason the ends are fixed. */
  min-width: 116px;
}

/* Rows per page. Plain links, not a select with a change handler: a filter
   still has to work with htmx absent (§3.5), and this is the same shape the
   dashboard horizon tabs already use. */
.pager-size {
  display: flex;
  align-items: center;
  gap: 2px;
  font-size: 13px;
}

.pager-size-label {
  margin-right: 6px;
  color: var(--text-muted);
}

.pager-size-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 32px;
  min-height: 32px;
  padding: 0 8px;
  border-radius: 6px;
  color: var(--text-secondary);
  text-decoration: none;
}

a.pager-size-option:hover {
  background: var(--surface-sunken);
  color: var(--text-primary);
}

/* The chosen size is not a link, so state is carried by weight and tint
   rather than by colour alone (§13). */
.pager-size-option.is-current {
  background: var(--surface-sunken);
  color: var(--text-primary);
  font-weight: 600;
}

.pager-note {
  margin: 8px 0 0;
  font-size: 12px;
  color: var(--text-muted);
}

.pager-range {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
}

@media (max-width: 767px) {
  /* The range folds away before the buttons do — on a phone the two
     controls are what the pager is for, and "showing 51-100 of 320" is the
     part you can do without. */
  .pager-range {
    display: none;
  }

  /* Two groups will not sit on one phone line, so the size control wraps
     above rather than squeezing Previous and Next below the 44px target
     §13 requires. */
  .pager {
    flex-wrap: wrap;
    justify-content: center;
  }

  .pager-nav {
    width: 100%;
    justify-content: space-between;
  }

  .pager-position {
    min-width: 0;
  }
}

/* --- the settings landing page ------------------------------------------ */

.settings-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 16px;
}

.settings-card {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 20px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  text-decoration: none;
}

.settings-card:hover {
  border-color: var(--border-strong);
}

.settings-card strong {
  display: block;
  font-size: 15px;
}

.settings-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: color-mix(in oklab, var(--accent) 12%, var(--surface));
  color: var(--accent);
}

.settings-note {
  display: block;
  margin-top: 2px;
  color: var(--text-secondary);
  font-size: 13px;
  line-height: 1.5;
}

/* Two-line list rows share this sub-line treatment. It replaces the inline
   `style="color: var(--text-muted); font-size: 12px;"` that was being
   copy-pasted onto every new list. */
.row-sub {
  color: var(--text-muted);
  font-size: 12px;
}

/* A figure meant to be read against the ones above and below it — masked
   account numbers, tax IDs, money in a definition list. Tabular figures keep
   the digits on a common grid so a column of them lines up. */
.num {
  font-variant-numeric: tabular-nums;
}

/* A value nobody has recorded: "Not on file", "not recorded", an em dash.
   Muted so it reads as an absence rather than as content — and a class
   rather than an inline colour, so §4's palette review can see it. */
.value-unset {
  color: var(--text-muted);
}

/* --- layout helpers, replacing 105 inline style attributes --------------
   Every one of these was written inline on at least one screen, which put
   colour and spacing where §4's palette review cannot see them. */

/* A form card: one column, read at a comfortable measure. */
.card-narrow {
  max-width: 560px;
}

/* Narrower still — the auth screens, which hold one or two fields. */
.card-slim {
  max-width: 480px;
}

/* Wider — a card holding a full-width textarea, like the checklist editor. */
.card-wide {
  max-width: 760px;
}

/* A muted paragraph standing on its own, usually an empty or inert state. */
.muted-note {
  color: var(--text-muted);
  margin: 0;
}

/* A heading row inside a page rather than at the top of one. */
.section-header {
  margin-bottom: 12px;
}

.section-header h2 {
  margin: 0;
}

/* Buttons sitting side by side. */
.button-row {
  display: flex;
  gap: 8px;
  align-items: center;
}

/* A one-button POST form that should not break the line it sits on. */
.inline-form {
  display: inline;
}

.flush {
  margin: 0;
}

.flush-top {
  margin-top: 0;
}

.spaced {
  margin-top: 12px;
}

.spaced-lg {
  margin-top: 24px;
}

/* --- what changed since the last walk (Phase 8) -------------------------
   Two recordings side by side, and deliberately nothing that reads as a
   verdict: no progress bar, no green-vs-red framing of the pair, no
   summary tile. CLAUDE.md §2.6 — Baxter reports what an inspector found
   and never whether an appraiser will accept it, and a comparison screen
   is the easiest place in the app to break that by accident. */

.walkthrough-compare {
  margin: 12px 0 0;
  font-size: 13px;
}

.walkthrough-compare a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.change-section {
  border-top: 1px solid var(--border);
  padding: 12px 0;
}

.change-section > summary {
  cursor: pointer;
  font-weight: 600;
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.change-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
}

.change-row {
  padding: 10px 0;
  border-top: 1px solid var(--gridline);
}

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

/* An item recorded the same on both days is still listed — leaving it out
   would make the page a list of problems rather than a record of the walk —
   but it steps back so the differences read first. */
.change-row[data-kind="unchanged"] .change-item {
  color: var(--text-secondary);
}

.change-item {
  margin: 0 0 6px;
  max-width: 70ch;
}

.change-states {
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  font-size: 13px;
}

.change-arrow {
  color: var(--text-muted);
}

.change-note {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--text-secondary);
  max-width: 70ch;
}

/* A property that was never acquired, or has been sold. Not an error — the
   pursuit simply ended — so it gets the quiet well rather than a warning
   step (§5.5 reserves those for overdue and failed payments). */
.property-closed {
  margin: 0 0 20px;
  padding: 12px 14px;
  border-radius: 6px;
  background: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: 14px;
  line-height: 1.55;
}

/* --- the dashboard agenda and overdue — DESIGN.md §9 -------------------- */

/* Was two inline `style="color: var(--critical-text)"` attributes. Classes
   now, for the same reason `.back-link` and `.page-lede` exist: an inline
   style is invisible to the palette review §4 asks for. */
.overdue-header {
  align-items: baseline;
  margin-bottom: 4px;
}

/* The money already late, beside the count — the figure and the rows it
   covers sit in the same card so they cannot disagree. */
.overdue-total {
  font-size: 13px;
  font-weight: 600;
  color: var(--critical-text);
  margin-left: 6px;
}

.overdue-heading {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  color: var(--critical-text);
}

/* Says out loud that this list ignores the controls above it. Without the
   note a list that does not respond to a filter reads as broken. */
.overdue-note {
  font-size: 12px;
  color: var(--text-muted);
}

.agenda-date.is-overdue {
  color: var(--critical-text);
  font-weight: 500;
}

/* One card for the whole agenda with a month band per group, rather than a
   card per month: twelve cards down a page read as twelve unrelated lists,
   one list with headings reads as a timeline. */
.agenda-card .result-count {
  margin: 0 0 4px;
}

.agenda-month {
  margin: 12px -16px 0;
  padding: 6px 16px;
  background: var(--surface-sunken);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-secondary);
}

.agenda-month:first-of-type {
  margin-top: 8px;
}

/* The whole row is the link (developer's call, 2026-08-22) — the "View"
   button it replaces added a fourth column to a row that already had three,
   and a target you have to aim for is worse on a phone than one the width
   of the screen.

   Because an anchor carries link colour and underline by default, both are
   reset here: an agenda row is a row, not a paragraph of links. What marks
   it as clickable is the hover tint and a real focus ring, so the keyboard
   is not left guessing. */
/* `.card` carries `margin-top` and no bottom, and `.filter-row` carries
   `margin-bottom` and no top — so the overdue card and the kind chips below
   it sat flush against each other with no gap at all. The card owns the
   problem, so the card fixes it; scoping it here rather than adding a top
   margin to `.filter-row` avoids shifting every other filter row in the
   app. */
.overdue-card {
  margin-bottom: 16px;
}

a.agenda-row {
  color: inherit;
  text-decoration: none;
}

a.agenda-row:hover {
  background: var(--surface-sunken);
}

a.agenda-row:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
  border-radius: 4px;
}

.attention-panel .panel-note {
  margin: 2px 0 12px;
  font-size: 12px;
  color: var(--text-muted);
}

.attention-panel .panel-empty {
  margin: 0;
  font-size: 13px;
  color: var(--text-muted);
}

/* Each item is its own well rather than a bare row: they are unrelated
   stalled things, and a run of undifferentiated lines reads as one list of
   steps to work through in order, which they are not. */
.attention-row {
  display: block;
  padding: 10px 12px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-sunken);
}

.attention-row:last-child {
  margin-bottom: 0;
}

.attention-what a {
  font-size: 14px;
  font-weight: 500;
}

.attention-hint {
  display: block;
  margin-top: 2px;
  font-size: 12px;
  color: var(--text-muted);
}

/* --- clickable table rows ----------------------------------------------- */

/* A `<tr>` cannot be an anchor, so the row's primary link stretches its own
   hit area over the whole row with a pseudo-element. The real anchor stays
   the accessible target — keyboard focus, the screen reader's link list and
   the status-bar URL all come from it — and only the *mouse* target grows.
   A JS click handler on the row would give none of that back.

   Any other link in the row is lifted above the overlay so it stays
   clickable; without that, the row link would swallow it. */
tr.row-link {
  position: relative;
  cursor: pointer;
}

tr.row-link:hover {
  background: var(--surface-sunken);
}

tr.row-link .row-link-target::after {
  content: "";
  position: absolute;
  inset: 0;
}

tr.row-link a:not(.row-link-target) {
  position: relative;
  z-index: 1;
}

/* Below 768px `table.stacked` becomes cards, where the row is already a
   block — the overlay still covers it, so nothing extra is needed. */

/* --- Document detail: the read-in-progress state (DESIGN.md §10.13) ------
   A queued read and a read nobody asked for looked identical before this,
   which is what made a stalled worker read as a broken feature. */
.read-working-line {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 4px;
}

.read-working {
  border-left: 3px solid var(--accent);
}

.read-failed-card {
  border-left: 3px solid var(--serious);
}

.spinner {
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

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

/* A spinner that never stops is worse than none for anyone who finds motion
   difficult; the words above it already say what is happening. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: var(--border-strong); }
}

/* An action is being worked on: the page leaves when the server answers.
   `submit-once.js` and `confirm-dialog.js` mark the form `aria-busy` and the
   pressed button `data-busy`. A locked page otherwise showed "not-allowed"
   over its disabled buttons, which reads as broken, not busy. `!important`
   because the disabled rules name a cursor at higher specificity. */
html:has(form[aria-busy="true"]),
html:has(form[aria-busy="true"]) * {
  cursor: progress !important;
}

/* The pressed button keeps its own colour and gains a spinner — phones have
   no cursor to change. After every `:disabled` rule on purpose. */
.button-primary[data-busy]:disabled {
  background: var(--accent);
  color: white;
}

.button-secondary[data-busy]:disabled {
  background: var(--surface);
  color: var(--text-secondary);
  border-color: var(--border-strong);
}

.button-danger[data-busy]:disabled,
.button-warning[data-busy]:disabled {
  opacity: 1;
}

button[data-busy]::after {
  content: "";
  flex: none;
  width: 12px;
  height: 12px;
  margin-left: 8px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  button[data-busy]::after { animation: none; border-right-color: currentColor; opacity: 0.6; }
}

/* The dashboard on a phone. It was rebuilt after the mobile pass and never
   got one: every class below had a desktop rule and nothing else, so the one
   screen the client looks at most was the least adapted. */
@media (max-width: 768px) {
  /* Date and amount on one line, description on the next. In a single row
     the 96px date plus the amount's 120px minimum leaves about 68px of a
     360px screen for the description — eight characters, then a ragged
     column of wrapped words. */
  .agenda-row {
    flex-wrap: wrap;
    align-items: center;
  }

  .agenda-date {
    order: 1;
  }

  .agenda-amount {
    order: 2;
    margin-left: auto;
    min-width: 0;
  }

  .agenda-what {
    order: 3;
    flex: 1 0 100%;
  }

  /* Every other .filter-row is a form of search boxes and selects, which
     should stack full width. These are chips — six of them, and stacking
     turns a single line into a wall. */
  .filter-row.chip-row {
    flex-direction: row;
    flex-wrap: nowrap;
    overflow-x: auto;
    align-items: center;
  }

  /* A heading beside a total, which do not fit side by side once the total
     carries a currency code. */
  .card-header {
    flex-wrap: wrap;
  }

  .overdue-header {
    align-items: flex-start;
  }
}

/* --- the insurance submission — compose and preview (DESIGN.md §10.14) -- */

/* Adding a property: the purchase is asked for only when it is already
   owned. CSS rather than script, so without :has() the fields just stay. */
.stacked-form:has(input[name="starting_stage"][value="lead"]:checked) .owned-purchase {
  display: none;
}

/* A fieldset of recipient checkboxes, dressed as one field. */
.stacked-form fieldset {
  border: 0;
  padding: 0;
  margin: 0 0 16px;
  min-width: 0;
}

.stacked-form legend {
  display: block;
  font-size: 13px;
  font-weight: 500;
  margin-bottom: 4px;
  padding: 0;
}

/* The message as it will land: header lines, then the body verbatim. */
.mail-preview {
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  max-width: 640px;
}

.mail-headers {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 12px;
  margin: 0;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}

.mail-headers dt {
  color: var(--text-secondary);
}

.mail-headers dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.mail-body {
  margin: 0;
  padding: 16px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  line-height: 1.6;
  white-space: pre;
  overflow-x: auto;
}

/* The HTML part carries its own inline styles, as it will in a mail client;
   the box only gives it room and a light ground so it reads as a letter. */
.mail-body.mail-html {
  font-family: inherit;
  white-space: normal;
  padding: 0;
  background: none;
  color: #111;
}

/* The app's table rules must not reach it — a mail client has none of them,
   and the preview's job is to show what the broker sees. */
.mail-html table { background: none; }
.mail-html td { border: 0; height: auto; background: none; vertical-align: top; }

.mail-gaps {
  margin: 12px 0 0;
  color: var(--warning-text);
  font-size: 13px;
}

/* One sent submission on the property page: a disclosure whose body is the
   message verbatim. */
.mail-record {
  border-top: 1px solid var(--border);
}

.mail-record summary {
  cursor: pointer;
  padding: 10px 0;
  font-weight: 500;
}

.mail-record .mail-body {
  margin: 0 0 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface-sunken);
}
