/*
 * Houso — clean, modern product chrome. One hand-written sheet, mobile-first.
 *
 * The platform's own face throughout — San Francisco where there is one —
 * and a light monospace for numbers and dates.
 * Greyscale on a dark ground, and deliberately no hue: status, verdict and
 * rating are told apart by lightness and by shape — a solid pill, an
 * outlined one, a dashed one, a filled meter segment against a hollow one.
 * Nothing on the page means anything by its colour alone, so the sheet
 * needs one palette rather than a light and a dark one.
 */

:root {
  color-scheme: dark;

  --bg: #0b0c0d;
  --surface: #141617;
  --surface-2: #1e2122;
  --border: #2c3031;
  --text: #f5f6f6;
  --text-muted: #9ba1a3;
  --text-dim: #6b7173;

  /* The one place a brand colour would go in. Everything tinted or filled
     reads off this, so the page takes a hue by changing this line. */
  --accent: #f5f6f6;

  --rung-3: #f5f6f6;
  --rung-2: #b9bec0;
  --rung-1: #7e8486;
  --rung-0: #3a3d3f;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.3);

  --sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable Text",
          "Segoe UI", system-ui, ui-sans-serif, Roboto, sans-serif;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, "Cascadia Mono", Consolas, monospace;
}

* { box-sizing: border-box; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--sans);
  font-size: 0.95rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* ONE MEASURE, named once and inherited. The flash is a sibling of <main>
   rather than a child, so both read the token off the column they share
   instead of each carrying its own copy of the number. Default is 42rem —
   what a report is written to be read at; a page asks for the wide one
   with `content_for :measure, "wide"`. */
.content-col { --measure: 42rem; }
.content-col.wide { --measure: 68rem; }
/* The ops tables want it. 42rem is the wrong measure for a seven-column
   crawls table, which at 1280px wrapped every timestamp over four lines
   with a third of the screen unused beside it. */
.wrap { max-width: var(--measure, 42rem); margin: 0 auto; padding: 1.5rem 1.15rem 5rem; }
/* Wide is for the tables, not for the sentences above them: prose set
   across 68rem is a measure nobody reads twice. The fact strip is a <p>
   that is really a row of chips, so it opts back out.

   Direct children only, which is the limit worth knowing: wrap a section
   in a div and its paragraphs go wide again with no rule to point at. The
   deeper shape is the reverse — a narrow column with the tables breaking
   out of it — and it is a bigger change than this file wanted. */
.content-col.wide .wrap > p, .content-col.wide .wrap > ul, .content-col.wide .wrap > dl { max-width: 46rem; }
.content-col.wide .wrap > .fact-strip { max-width: none; }

/* --- nav: a top bar on narrow screens, a sidebar from 64rem up ---
   One element, one landmark — CSS repositions it rather than duplicating
   the brand/nav markup per breakpoint. */
.sidebar {
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: 10;
}
.sidebar .head-row {
  max-width: 42rem; margin: 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; padding: 0.75rem 1.15rem;
}
.sidebar .brand { display: flex; align-items: center; text-decoration: none; }
.sidebar .wordmark { font-weight: 600; font-size: 0.98rem; color: var(--text); letter-spacing: -0.01em; }

.nav-area { display: flex; align-items: center; gap: 0.8rem; }
/* The group headings ("Ops", the household's name) are the sidebar's
   structure and are only structure once the bar is a column. In the
   narrow top bar the two groups sit side by side on one line, where a
   heading each would cost more room than it explains. */
.side-heading { display: none; }

.side-nav { display: flex; flex-wrap: wrap; align-items: center; gap: 0.2rem; font-size: 0.85rem; }
.side-nav .nav-link {
  display: flex; align-items: center; gap: 0.4rem;
  color: var(--text-muted); text-decoration: none; font-weight: 500;
  padding: 0.4rem 0.65rem; border-radius: var(--radius-sm);
  transition: background-color 0.12s ease, color 0.12s ease;
}
.side-nav .nav-link:hover { background: var(--surface-2); color: var(--text); }
.side-nav .nav-link.active { background: color-mix(in srgb, var(--accent) 14%, var(--surface)); color: var(--accent); }

/* How many runs are not finished, on the nav item that opens them. The
   count is the console's one unprompted report: a stuck run has to be
   visible from the page you happen to be on. */
.nav-badge {
  margin-left: auto; flex: none;
  min-width: 1.25rem; padding: 0 0.35rem; border-radius: 999px;
  background: color-mix(in srgb, var(--text-muted) 18%, var(--surface)); color: var(--text-muted);
  font-family: var(--mono); font-size: 0.7rem; font-weight: 600; line-height: 1.35rem;
  text-align: center;
}

@media (min-width: 64rem) {
  .shell { display: flex; align-items: flex-start; }
  .sidebar {
    position: sticky; top: 0; height: 100vh; flex: none; width: 15rem;
    border-bottom: none; border-right: 1px solid var(--border);
    backdrop-filter: none; background: var(--surface);
    padding: 1.5rem 1rem; overflow-y: auto;
  }
  .sidebar .head-row {
    max-width: none; margin: 0; padding: 0;
    flex-direction: column; align-items: flex-start; gap: 1.75rem;
  }
  .sidebar .brand { padding: 0 0.65rem; }
  .nav-area { display: block; width: 100%; }
  /* nowrap: as a column inside a 100vh pane, wrapping would break a long
     nav into a second column rather than into a second line. */
  .side-nav { flex-direction: column; flex-wrap: nowrap; align-items: stretch; width: 100%; gap: 0.1rem; }
  .side-nav .nav-link { width: 100%; }
  .side-heading {
    display: block; color: var(--text-muted); font-size: 0.7rem; font-weight: 600;
    text-transform: uppercase; letter-spacing: 0.07em;
    padding: 0 0.65rem; margin: 0 0 0.4rem;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* The household's own name is a name, not a section label, so it keeps
     its sentence case where "OPS" above it does not. */
  .side-heading.side-user { text-transform: none; font-size: 0.78rem; letter-spacing: 0; }
  /* The sidebar is a column of groups: brand, then each nav group, then
     sign-out at the foot of the pane rather than immediately under the
     last link, so the two never read as one list. */
  .sidebar .head-row { gap: 1.4rem; flex: 1; }
  .nav-foot { margin-top: auto; }
  .content-col { flex: 1; min-width: 0; }
}

a { color: var(--text); text-decoration-color: var(--text-dim); text-underline-offset: 2px; }

h1, h2, h3 { font-family: var(--sans); font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
h1 { font-size: 1.4rem; line-height: 1.3; margin: 1.1rem 0 0.5rem; }
h2 {
  font-size: 0.95rem; margin: 1.8rem 0 0.6rem; padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--border);
}

.flash {
  max-width: var(--measure, 42rem); margin: 0.85rem auto 0; padding: 0.6rem 1rem;
  border-radius: var(--radius-sm); font-size: 0.88rem;
  border: 1px solid transparent;
}
.flash-notice { background: var(--surface-2); color: var(--text); border-color: var(--border); }
.flash-alert { background: var(--surface-2); color: var(--text); border-color: var(--border); border-left: 3px solid var(--text); font-weight: 500; }

table { border-collapse: collapse; width: 100%; font-size: 0.86rem; }
/* Below the sidebar breakpoint a six-column table cannot fit any phone,
   and the choice is between scrolling the table and scrolling the page.
   `display: block` turns the table itself into the scroller, so the page
   body never moves sideways under the reader. */
@media (max-width: 48rem) {
  .wrap table { display: block; overflow-x: auto; }
}
th, td { text-align: left; padding: 0.65rem 0.7rem; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--text-muted); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
tbody tr { transition: background-color 0.1s ease; }
tbody tr:hover { background: var(--surface-2); }

/* Numbers align right and set in the mono face, so a column of them can be
   compared down rather than read across. */
th.numeric, td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
td.numeric { font-family: var(--mono); font-size: 0.82rem; }
td.numeric .meta, td.numeric .chip { font-family: var(--sans); }
/* Timestamps and week numbers are short and read as one token; letting
   them wrap turned "about 18 hours ago" into four lines. */
.nowrap { white-space: nowrap; }

/* The foot totals the body above it, so it has to read as a different kind
   of row: no hover, a rule that closes the table, and no bottom border
   under the last line. */
tfoot th, tfoot td {
  border-top: 2px solid var(--border); border-bottom: 0;
  font-weight: 600; color: var(--text);
}
tfoot th { font-size: 0.72rem; }
tfoot tr:hover { background: transparent; }

.row-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; }

/* The stock page's filter row, and the pager under its table. Both wrap
   rather than scroll: five controls do not fit a phone in one line, and a
   filter bar that overflows sideways hides the control on its right end —
   which on this page is the one that submits it. */
.filter-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin: 0 0 1rem; }
/* Grows into the row's spare width, because an address is the longest
   thing typed here and the selects beside it are fixed by their options.
   The basis keeps it from collapsing to nothing when they all wrap. */
.filter-bar input[type="search"] { flex: 1 1 14rem; }
.pager { display: flex; flex-wrap: wrap; align-items: center; gap: 0.7rem; margin: 0.9rem 0 0; }

/* Every cell in the stock table is a fact and a qualifier — an address and
   its postcode, a price and what it was, a date and why it matters — and
   inline they run together into one paragraph the eye cannot find the
   address in. A line each. Not a change to .meta itself: everywhere else
   it trails the sentence it belongs to, on purpose. */
.stock td .meta { display: block; }
/* The chip stays on the date's line: it labels that date, and the sentence
   under it is what explains the label. */
.stock td .chip { display: inline-block; }
/* Enough for the longest address any agent writes before it wraps mid-name.
   The table is its own scroller below 48rem, so this costs the page
   nothing there. */
.stock td:first-child { min-width: 12rem; }

button, .button {
  display: inline-block; border: 1px solid var(--accent); border-radius: var(--radius-sm); cursor: pointer;
  background: var(--accent); color: var(--bg); font-family: var(--sans); font-weight: 500; font-size: 0.88rem;
  padding: 0.5rem 0.9rem; text-decoration: none;
  transition: background-color 0.12s ease, border-color 0.12s ease, transform 0.05s ease;
}
button:hover, .button:hover { background: color-mix(in srgb, var(--accent) 85%, black); }
button:active, .button:active { transform: translateY(1px); }
button.secondary, .button.secondary { background: var(--surface); color: var(--text); border-color: var(--border); }
button.secondary:hover, .button.secondary:hover { border-color: var(--text-muted); background: var(--surface-2); }
button.danger, .button.danger { background: var(--surface); color: var(--text); border-color: var(--border); }
button.danger:hover, .button.danger:hover { background: color-mix(in srgb, var(--text) 12%, var(--surface)); border-color: var(--text); }
button.small, .button.small { padding: 0.3rem 0.6rem; font-size: 0.78rem; }
button:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* Every type this app actually renders, named one by one rather than a
   bare `input`, so a checkbox never inherits width:100%. The cost of that
   is that a type left off the list gets NO styling at all and renders as a
   bare browser default — which is what the review page's address field did
   the day it was added as `email`. Add the type here when you add the
   field. */
/* EVERY TEXTUAL CONTROL, by excluding the ones that must not inherit
   width:100% rather than by listing the ones that should. The list was
   enumerated type by type, which meant a type left off it rendered as a
   bare browser default with no border, font or padding — and one was:
   the ops sign-in page's input[type="password"]. Adding "email" for the
   review page would have left that still broken and asked the next
   person to remember a ritual. input[type="submit"] stays out
   deliberately: `button, .button` above does not match it, so f.submit
   is styled as a button by the rule below, not as a field. */
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"]):not([type="hidden"]), textarea, select {
  width: 100%; font-family: var(--sans); font-size: 0.92rem; color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.5rem 0.65rem; background: var(--surface);
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="file"]):not([type="hidden"]):focus, textarea:focus, select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
select { width: auto; padding: 0.45rem; }
textarea { min-height: 6rem; resize: vertical; line-height: 1.5; }
label { display: block; font-weight: 600; font-size: 0.8rem; margin: 0.9rem 0 0.3rem; }

/* A CHECKBOX AND ITS OWN WORDS ON ONE LINE. The block rule above is right
   for every other field — a label above its input — and wrong for a
   checkbox, where it puts the words on the line below the box and so
   beside the box belonging to the flag above. Overridden inside the row
   only. The gap between rows is larger than the gap inside one, because
   that spacing is the only thing telling a reader which words go with
   which box. */
.exclusion { display: flex; align-items: baseline; gap: 0.5rem; margin: 0.55rem 0; }
.exclusion label { display: inline; margin: 0; font-weight: 500; }

/* A hint that is telling the buyer something they may not want, rather
   than explaining a field. Warm rather than red: nothing is wrong, and a
   brief with no address is a supported way to use the product. */
.hint.warn { color: #8a5a1c; }
.hint { color: var(--text-muted); font-size: 0.8rem; margin: 0.2rem 0 0; }

/* ONE CONTROL AND THE WORDS IT APPLIES TO, ON ONE LINE. The rule above
   is display:block, which is right for a stacked form and wrong for a
   checkbox: it put every label on the line below its own box, so on the
   brief review page each box sat 8px under the PREVIOUS label and 22px
   above its own and read as belonging to the wrong one. Measured, not
   guessed — 2.75 times nearer the words it is not for.

   That is a real fault on that form specifically. Brief::EXCLUSIONS are
   enforced before a penny is spent, on every listing, every week, and
   the report prints the rejection back as the buyer's own stated rule —
   so a box ticked by mistake removes a whole category of house and never
   surfaces anywhere. The gap inside a row is smaller than the gap
   between rows, which is the whole point.

   The label keeps a normal weight here: it is a sentence the buyer reads,
   not a heading over a field. */
.field-row { display: flex; align-items: center; gap: 0.55rem; margin: 0.5rem 0; }
.field-row + .field-row { margin-top: 0.65rem; }
.field-row label { display: inline; margin: 0; font-weight: 400; font-size: 0.9rem; }
.field-row input[type="checkbox"] { margin: 0; flex: 0 0 auto; }
/* The priority number. Width:100% is right for every other input on the
   page and absurd for a box holding "1", so it is overridden here rather
   than the shared rule being weakened. */
.field-row .rank-input { width: 3.5rem; flex: 0 0 auto; text-align: center; }

code { font-family: var(--mono); font-size: 0.9em; }

/* --- interview chat --- */
.topics {
  border: 1px solid var(--border); background: var(--surface-2); border-radius: var(--radius);
  padding: 0.85rem 1rem; margin: 1rem 0; font-size: 0.85rem;
}
.topics > strong { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); font-weight: 600; }
.topics ol { margin: 0.5rem 0 0; padding-left: 1.2rem; }
.topics li { margin: 0.25rem 0; color: var(--text); }
.topics li::marker { color: var(--text-muted); font-size: 0.85em; }

.chat { display: flex; flex-direction: column; gap: 0.55rem; margin: 1.1rem 0; }
.bubble {
  max-width: 88%; padding: 0.6rem 0.85rem; border-radius: 12px;
  white-space: pre-wrap; font-size: 0.92rem; line-height: 1.5;
  animation: rise 0.2s ease both;
}
.bubble.assistant { background: var(--surface-2); align-self: flex-start; }
.bubble.user { background: var(--accent); color: var(--bg); align-self: flex-end; }
/* Houso mid-turn, or the reason a turn failed. Muted and italic so it
   reads as the app talking about the conversation rather than as a thing
   Houso said — it is the one bubble that is not part of the transcript. */
.bubble.pending { color: var(--text-muted); font-style: italic; }
.chat-form { display: flex; align-items: flex-end; gap: 0.5rem; margin-top: 0.85rem; }
.chat-form textarea {
  min-height: 2.75rem; max-height: 12rem; flex: 1;
  resize: none; overflow-y: auto;
}

/* --- listing imagery and sale history --- */
.card-disclosure { margin: 0.35rem 0; }
.card-disclosure summary { color: var(--text-muted); font-size: 0.85rem; cursor: pointer; }
.card-disclosure ul { margin: 0.25rem 0 0; padding-left: 1.1rem; }
.card-disclosure li { color: var(--text-muted); font-size: 0.85rem; line-height: 1.5; }

/* The house, first and large. A buyer scanning a report decides from the
   picture before they read a word, so it gets the width. */
.listing-photo {
  width: 100%; height: auto; display: block; margin: 0 0 0.75rem;
  border: 1px solid var(--border); border-radius: var(--radius);
}
.imagery { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.5rem; margin: 0.6rem 0 0.2rem; }
.imagery figure { margin: 0; }
.imagery img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--border); border-radius: var(--radius);
}
.imagery figcaption { color: var(--text-muted); font-size: 0.78rem; margin-top: 0.2rem; }

@keyframes rise {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* --- cards --- */
.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-card);
  padding: 1rem 1.1rem; margin: 0.9rem 0;
  animation: rise 0.22s ease both;
}
.card h2 { font-size: 1.05rem; margin: 0 0 0.3rem; padding-bottom: 0; border-bottom: 0; }

.fact-strip {
  font-family: var(--mono); font-size: 0.76rem; color: var(--text-muted);
  letter-spacing: 0; margin: 0 0 0.65rem;
}
.fact-strip a { color: var(--accent); }

.eyebrow {
  font-size: 0.72rem; text-transform: uppercase; font-weight: 600;
  letter-spacing: 0.06em; color: var(--text-muted); margin: 0 0 0.5rem;
}
/* The runs page is the one place an eyebrow labels the card after it in a
   repeating stack, so each needs air above it. Everywhere else it is the
   first child of its own block and the block supplies the gap — which is
   why this is two selectors rather than a wider default. */
.progress + .eyebrow, .hint + .eyebrow { margin-top: 1.2rem; }
.score-row { margin: 0.4rem 0; display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }

.week-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 1.2rem 0 0.4rem; }
.week-head h1 { margin: 0; }

/* --- chips: the signature element --- */
.chip {
  display: inline-flex; align-items: center; gap: 0.4em;
  font-family: var(--sans); font-size: 0.76rem; font-weight: 500;
  padding: 0.2rem 0.6rem 0.2rem 0.5rem; border-radius: 999px;
  background: var(--surface-2); color: var(--text-muted);
  white-space: nowrap;
}
.chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
/* Solid for a thing met, an outline for a thing that falls short, a ring
   on a dim ground for a thing we could not establish, and flat for
   unremarkable. Shape, not hue: "weak" and "unknown" are different kinds
   of answer and must not read as two amounts of one. */
.chip.strong { background: var(--accent); color: var(--bg); }
.chip.strong::before { background: var(--bg); }
.chip.weak {
  background: transparent; color: var(--text);
  box-shadow: inset 0 0 0 1px var(--text-muted);
}
.chip.weak::before { background: transparent; box-shadow: inset 0 0 0 1.5px var(--text); }
.chip.unknown {
  background: var(--surface-2); color: var(--text-muted);
  box-shadow: inset 0 0 0 1px var(--border);
}
.chip.unknown::before { background: transparent; box-shadow: inset 0 0 0 1.5px var(--text-muted); }
.chip.fine { background: var(--surface-2); color: var(--text-muted); }

.meta { color: var(--text-muted); font-size: 0.84rem; }

/* WHAT THE PHOTOGRAPHS SAID AND THE WORDS DID NOT. Set apart from the
   other .meta lines around it because it is the only one anywhere on the
   report that is our own reading rather than something the agent or the
   source stated, and a buyer deciding whether to trust it needs to see
   that it is a different kind of claim. On the card as well as on the
   two lists, where it contradicts the verdict directly above it. */
.meta.revealed {
  border-left: 2px solid var(--border); padding-left: 0.55rem;
  margin-top: 0.35rem; font-style: italic;
}

.verdict-line { margin: 0.75rem 0; font-size: 0.92rem; }

.memo {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 1rem 1.1rem; white-space: pre-wrap;
  line-height: 1.6;
}

.overview { font-size: 0.95rem; line-height: 1.65; margin: 1rem 0 1.4rem; }

.actions { display: flex; gap: 0.55rem; flex-wrap: wrap; margin: 1.1rem 0; }

/* --- shortlist zone --- */
.shortlist-zone { border-top: 1px solid var(--border); margin-top: 0.9rem; padding-top: 0.65rem; }
.shortlist-zone .actions { margin: 0.4rem 0; }
/* The agent's own facts about the house, as a scannable list rather than
   a paragraph — ten cards get skimmed, not read. */
.listing-facts { margin: 0.5rem 0; padding-left: 1.1rem; }
.listing-facts li { margin: 0.15rem 0; }

/* Facts off the property's own page. A two-column list rather than
   bullets, because these are answers to questions the buyer asked ("what
   is the garden?") and a label makes that legible at a glance. The value
   column wraps; some of these run to a sentence, and truncating the
   garden on a brief that ranks the garden second would be absurd. */
.detail-facts {
  display: grid; grid-template-columns: minmax(7rem, auto) 1fr;
  gap: 0.15rem 0.7rem; margin: 0.35rem 0 0.6rem; font-size: 0.9rem;
}
.detail-facts dt { color: var(--text-muted); }
.detail-facts dd { margin: 0; }
@media (max-width: 30rem) {
  .detail-facts { grid-template-columns: 1fr; gap: 0 0; }
  .detail-facts dt { margin-top: 0.35rem; font-size: 0.82rem; }
}

/* --- the property card --- */

/* Icons ride the text they label: sized in em so they scale with whatever
   they sit in, and stroked in currentColor so a cell's status colours the
   mark as well as the word. */
.icon-sprite { position: absolute; width: 0; height: 0; }
svg.i {
  width: 1em; height: 1em; flex: none; vertical-align: -0.12em;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}

.card.property { padding: 0; overflow: hidden; }
.property .shot { position: relative; }
.property .listing-photo { margin: 0; border: 0; border-radius: 0; }
.rank {
  flex: none; font-family: var(--mono); font-size: 0.72rem; font-weight: 600;
  font-variant-numeric: tabular-nums; color: var(--text-muted);
  background: var(--surface-2); border-radius: var(--radius-sm);
  padding: 0.1rem 0.42rem; align-self: center;
}
.property .credit {
  position: absolute; right: 0.55rem; bottom: 0.55rem;
  font-size: 0.7rem; color: var(--text-muted);
  background: var(--surface); border-radius: var(--radius-sm); padding: 0.1rem 0.4rem;
}
.card-body { padding: 0.95rem 1.05rem 1.05rem; }

.card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 0.8rem; flex-wrap: wrap; }
.card-head h2 { margin: 0; font-size: 1.02rem; line-height: 1.35; flex: 1 1 14rem; padding-bottom: 0; border-bottom: 0; }

.price {
  font-size: 1.2rem; font-weight: 600; letter-spacing: -0.015em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}

.specs { display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; margin: 0.5rem 0 0; font-size: 0.82rem; color: var(--text-muted); }
.specs span { display: inline-flex; align-items: center; gap: 0.3rem; }
.specs b { font-weight: 500; color: var(--text); font-variant-numeric: tabular-nums; }
.placing { margin: 0.45rem 0 0; }

.conflict {
  display: flex; gap: 0.5rem; align-items: flex-start; margin: 0.8rem 0 0;
  padding: 0.55rem 0.7rem; border-radius: var(--radius-sm);
  background: var(--surface-2); box-shadow: inset 2px 0 0 var(--text);
  color: var(--text); font-size: 0.85rem; font-weight: 500;
}
.conflict svg.i { margin-top: 0.22rem; }

.match { display: flex; align-items: center; gap: 0.5rem; margin: 0.85rem 0 0; font-size: 0.84rem; flex-wrap: wrap; }
.match b { font-weight: 600; font-variant-numeric: tabular-nums; }
.meter { display: flex; gap: 2px; flex: none; }
.meter i { width: 13px; height: 5px; border-radius: 1px; background: var(--border); }
.meter i.met { background: var(--accent); }
.meter i.short { background: transparent; box-shadow: inset 0 0 0 1px var(--text); }

/* THE BUYER'S PRIORITIES, IN THEIR ORDER, IN THE SAME CELLS ON EVERY
   CARD. Fixed positions are the point: a reader comparing ten houses
   scans one column rather than reading ten paragraphs, which only works
   while the fourth cell is garden on every card in the report. */
.priority-grid {
  display: flex; flex-direction: column;
  gap: 1px; margin: 0.9rem -1.05rem 0; padding-top: 1px;
  background: var(--border); border-top: 1px solid var(--border);
}
.priority-grid .cell {
  background: var(--surface); padding: 0.65rem 1.05rem 0.7rem;
  display: grid; grid-template-columns: 11.5rem minmax(0, 1fr);
  gap: 0.2rem 1rem; align-items: baseline;
}
.cell-label { display: flex; align-items: baseline; gap: 0.32rem; margin: 0; }
.cell.met .cell-label { color: var(--text); }
.cell.short { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--text); }
.cell.short .cell-label { color: var(--text); }
.priority-grid .cell p { margin: 0; font-size: 0.86rem; line-height: 1.5; text-wrap: pretty; }

.notes { margin: 0.95rem 0 0; }
.notes ul { list-style: none; margin: 0.3rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.notes li { display: flex; gap: 0.45rem; align-items: flex-start; font-size: 0.855rem; }
.notes li svg.i { margin-top: 0.28rem; color: var(--text-muted); }

.disclosure-body { padding: 0.3rem 0 0.4rem; }

.listing-links { margin: 0.55rem 0 0; }
.evidence-row { margin: 0.35rem 0; }

.priorities { margin: 0.9rem 0 1rem; padding: 0.7rem 0.85rem; background: var(--surface-2); border-radius: var(--radius-sm); }
.priorities ol { list-style: none; margin: 0.35rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.3rem 0.45rem; counter-reset: priority; }
.priorities li {
  counter-increment: priority; display: inline-flex; align-items: center; gap: 0.3rem;
  background: var(--surface); border: 1px solid var(--border); border-radius: 999px;
  padding: 0.1rem 0.55rem 0.1rem 0.4rem; font-size: 0.78rem;
}
.priorities li::before {
  content: counter(priority); font-family: var(--mono); font-size: 0.68rem;
  color: var(--text-muted); font-variant-numeric: tabular-nums;
}

/* --- the placement split: where a house is, above how well it matches --- */
.placement { margin: 2.4rem 0 0.6rem; }
.placement h2 { font-size: 1.1rem; margin: 0; }
.placement p { margin: 0.45rem 0 0; font-size: 0.84rem; color: var(--text-muted); }
.placement + .tier { margin-top: 1.1rem; }

.tier { margin: 2rem 0 0.8rem; display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.tier h2.eyebrow { margin: 0; padding-bottom: 0; border-bottom: 0; }
.tier .rule { flex: 1 1 3rem; height: 1px; background: var(--border); }
.tier p { margin: 0; font-size: 0.8rem; color: var(--text-muted); flex-basis: 100%; }

.notice {
  margin: 0.9rem 0; padding: 0.65rem 0.85rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border); border-left: 3px solid var(--text);
  font-size: 0.84rem; color: var(--text-muted);
}
.notice b { color: var(--text); font-weight: 600; }

.ruled-out-line { margin: 1.4rem 0 0; }

@media (max-width: 34rem) {
  .priority-grid .cell { grid-template-columns: minmax(0, 1fr); }
  .card-head h2 { flex-basis: 100%; }
}

/* The evidence, present and out of the way. */
.evidence { margin-top: 0.6rem; }
.evidence .card-disclosure { margin-left: 0.6rem; }

/* --- empty / quiet states --- */
.quiet {
  color: var(--text-muted); padding: 2rem 0.5rem; text-align: center;
  border-top: 1px solid var(--border); margin-top: 1rem; font-size: 0.9rem;
}

/* --- run progress ---
   A run is ten minutes of model calls. The card is a live readout of it:
   which step, how far through the properties, and the log of what it has
   already done. Timestamps and counts are mono, like every other number
   in the app. */
.progress .progress-stage {
  font-size: 1.05rem; margin: 0 0 0.2rem; padding-bottom: 0; border-bottom: 0;
}
.progress-detail { margin: 0 0 0.7rem; color: var(--text-muted); font-size: 0.9rem; }

.bar {
  height: 6px; border-radius: 999px; background: var(--surface-2);
  overflow: hidden; margin: 0.45rem 0 0.8rem;
}
.bar-fill {
  height: 100%; border-radius: 999px; background: var(--accent);
  transition: width 0.3s ease;
}

/* The clock the Stimulus controller writes into: quiet while a run is
   moving, amber once it has stopped saying anything. */
.run-clock { font-family: var(--mono); font-size: 0.76rem; margin: 0 0 0.5rem; }
.run-clock.stalled { color: var(--text); font-weight: 500; }

.steps {
  list-style: none; margin: 0.6rem 0 0; padding: 0.6rem 0 0;
  border-top: 1px solid var(--border);
  max-height: 19rem; overflow-y: auto;
}
.step {
  display: flex; gap: 0.55rem; align-items: baseline; flex-wrap: wrap;
  padding: 0.18rem 0; font-size: 0.82rem; color: var(--text-muted);
}
.step-time { font-family: var(--mono); font-size: 0.72rem; flex: none; }
.step-label { color: var(--text); }
.step-detail { flex: 1 1 12rem; min-width: 0; }
.step-took { font-family: var(--mono); font-size: 0.72rem; flex: none; }
.step.current .step-label { color: var(--accent); font-weight: 500; }
.step.current .step-time::before {
  content: ""; display: inline-block; width: 5px; height: 5px;
  border-radius: 50%; background: var(--accent); margin-right: 0.4rem;
  animation: pulse 1.4s ease-in-out infinite;
}

@keyframes pulse { 50% { opacity: 0.25; } }

/* Ops: the same run, one line, inside a table cell. */
.progress-row { display: inline-flex; gap: 0.4rem; align-items: baseline; flex-wrap: wrap; }

.run-log summary { cursor: pointer; }
.run-log .steps { border-top: 0; max-height: none; }

/* --- the funnel ---
   A run's listings reduced step by step. The bar is the count against the
   feed it came from, so the shape of the page is the shape of the funnel:
   one wide bar at the top and a row of narrow ones under it. Steps that
   split the feed sit flush left; the exits they split into are indented
   under them, which is the only hierarchy the list needs. */
.funnel { list-style: none; margin: 0.6rem 0 1.4rem; padding: 0; }
.funnel-step { margin: 0 0 0.55rem; }
.funnel-step.exit, .funnel-step.arrived,
.funnel-step.discarded, .funnel-step.unreached {
  margin-left: 1.1rem; padding-left: 0.85rem; border-left: 1px solid var(--border);
}

.funnel-head {
  display: flex; gap: 0.55rem; align-items: baseline;
  flex-wrap: wrap; margin: 0; font-size: 0.88rem;
}
.funnel-count {
  font-family: var(--mono); font-weight: 600; min-width: 2.6rem;
  text-align: right; flex: none;
}
.funnel-label { flex: 1 1 12rem; min-width: 0; }
.funnel-share { font-family: var(--mono); font-size: 0.74rem; color: var(--text-muted); }

.funnel-step .bar { margin: 0.3rem 0 0; }
/* Both sit under the label, so they clear the count column: .funnel-count's
   min-width plus the head's gap. */
.funnel-step .hint,
.funnel-step .card-disclosure { margin-left: 3.15rem; }

/* A through-step is the whole feed at that point, so its bar is the ruler
   the exits below are measured against — Step#width, not Step#share. */
.funnel-step.through .funnel-head { font-weight: 500; font-size: 0.95rem; }
.funnel-step.through .bar-fill { background: var(--text-muted); }
.funnel-step.exit .bar-fill,
.funnel-step.discarded .bar-fill { background: var(--text-dim); }
.funnel-step.arrived .bar-fill { background: var(--accent); }
.funnel-step.unreached .bar-fill { background: var(--border); }
.funnel-step.arrived .funnel-count { color: var(--accent); }
.funnel-step.unreached .funnel-count { color: var(--text-muted); }

/* Which of the buyer's areas Houso reached this week. Quiet where the
   week was covered; the gap is not, because "nobody looked there" is a
   different thing from "nothing came up there" and the page has to keep
   them apart at a glance. */
.coverage-line { color: var(--text-muted); font-size: 0.84rem; margin: 0.3rem 0 1rem; }

/* The standing qualifications, once for the whole report. Quiet and at
   the foot: each one is worth reading once and none is worth reading six
   times, which is what they cost when they sat on every card. */
.caveats { margin: 1.6rem 0 1rem; }
.caveats ul { margin: 0; padding-left: 1.1rem; }
.caveats li { color: var(--text-muted); font-size: 0.84rem; margin: 0.3rem 0; }

/* The houses the week had no room for. Deliberately not a card: a card
   says "I have read this house and here is what I think", and the whole
   point of these is that nobody has. A row and a thumbnail. */
.held-back { margin: 1.6rem 0 1rem; }
.held-list { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.held-item {
  display: flex;
  gap: 0.9rem;
  align-items: center;
  padding: 0.7rem 0;
  border-top: 1px solid var(--border);
}
.held-photo {
  width: 84px;
  height: 62px;
  object-fit: cover;
  border-radius: var(--radius-sm);
  flex: 0 0 auto;
}
.held-body { flex: 1 1 auto; min-width: 0; }
.held-address { margin: 0 0 0.2rem; font-weight: 600; }
/* Margins only: .meta and .fact-strip keep the sizes they have
   everywhere else, so a held-back line reads as the same kind of thing
   as the facts on a card rather than as a louder one. */
.held-body .fact-strip, .held-body .meta { margin: 0.15rem 0; }

/* Scored in full and held to the bar. Borrows the held-back row's
   proportions on purpose — these are the same kind of object to a
   reader, a line rather than a card — and differs in the one way that
   matters: no photograph. A thumbnail would make it look like something
   on offer, and we looked at this one and would not recommend it. */
.considered { margin: 1.6rem 0 1rem; }
.considered-list { list-style: none; margin: 0.6rem 0 0; padding: 0; }
.considered-item { padding: 0.7rem 0; border-top: 1px solid var(--border); }
.considered-body { min-width: 0; }
.considered-body .fact-strip, .considered-body .meta { margin: 0.15rem 0; }
/* The sentence that says why, in the pipeline's own words. Given the
   weight of a verdict line, because that is what it is. */
.considered-reason { margin: 0.3rem 0 0.2rem; color: var(--text-muted); }

/* A crawl older than its own weekly clock. Not an error — a fortnight-old
   reading still says what that agent had — but a crawl that has quietly
   stopped running is the thing this page exists to catch, so the row is
   dimmed rather than left looking current. */
.stale td { color: var(--text-muted); }

/* --- ops: the column carrying the answer --- */
/* On a crawl table the prose column is the news — which agents turned, which
   listings were new. Left to size itself inside the scrolling table a phone
   gives it, it wraps to roughly one name a line and stretches its row past a
   screenful of empty cells beside it. */
.wide-cell { min-width: 20rem; }

/* --- ops: which areas were searched --- */
.area-coverage { margin: 0.6rem 0 1.4rem; padding-left: 1.1rem; font-size: 0.88rem; }
.area-coverage li { margin: 0.25rem 0; }
.area-coverage .unsearched { color: var(--text); font-weight: 500; }

/* A run that does not add up has lost a house between two steps, which is
   the one thing this page exists to catch — so it is not a hint. */
.funnel-warning {
  border: 1px solid var(--text); border-radius: var(--radius);
  padding: 0.7rem 0.9rem; margin: 0.8rem 0; font-size: 0.88rem; color: var(--text);
}

@media (min-width: 40rem) {
  .sidebar .head-row { padding: 0.85rem 1.15rem; }
}

/* ---- Dashboard: stat tiles and ordinal bars ---------------------------- */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0.75rem;
  margin: 1rem 0 1.5rem;
}

.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-card);
  padding: 0.85rem 1rem;
}

.tile .figure {
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.tile .figure.blank { color: var(--text-muted); font-weight: 400; }
.tile .label { font-size: 0.8rem; font-weight: 600; margin-top: 0.3rem; }
.tile .sub { font-size: 0.78rem; color: var(--text-muted); margin-top: 0.15rem; }

.bar {
  display: flex;
  gap: 2px;
  width: 100%;
  height: 22px;
  margin: 0.15rem 0 0.5rem;
  border-radius: 3px;
  overflow: hidden;
}

.bar > span { min-width: 2px; }

.bar-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem 1.1rem;
  font-size: 0.82rem;
  margin-bottom: 0.75rem;
}

.bar-legend .key { display: inline-flex; align-items: baseline; gap: 0.4rem; }
.bar-legend .swatch {
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 2px;
  flex: none;
  transform: translateY(1px);
}
.bar-legend .count { font-weight: 600; }
.bar-legend .of { color: var(--text-muted); }

.rows { display: grid; gap: 0.3rem; margin-bottom: 0.75rem; }
.rows .row { display: grid; grid-template-columns: minmax(6rem, 12rem) 1fr; gap: 0.75rem; align-items: center; }
.rows .row .name { font-size: 0.84rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rows .row .name .of { color: var(--text-muted); font-variant-numeric: tabular-nums; }
.rows .row .bar { margin: 0; height: 16px; }

details.detail { margin: 0 0 1rem; }
details.detail > summary {
  cursor: pointer;
  font-size: 0.82rem;
  color: var(--text-muted);
  padding: 0.2rem 0;
  list-style-position: outside;
}
details.detail > summary:hover { color: var(--text); }
details.detail[open] > summary { margin-bottom: 0.4rem; }
