/* Rentwarden components — the app's shared vocabulary.
 *
 * One file, used by every page. It replaces `app.css` and `console.css`, which
 * had grown into per-page stylesheets with one-off class names: the same
 * card, the same field, the same table styled two or three times slightly
 * differently, plus inline `style=` attributes doing the rest.
 *
 * Rules for anything added here:
 *   1. Style the *kind of thing*, not the page it appears on. `.card`, not
 *      `
:root[data-theme='dark'] {
  --ok: #5fc794;
  --ok-bg: rgb(95 199 148 / .13);
  --color-subtle: #262b38;
  --color-text-2: #c3c8d8;
  --faint: #7c8298;
  --surface: #212530;
  --line2: rgb(255 255 255 / 18%);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --ok: #5fc794;
    --ok-bg: rgb(95 199 148 / .13);
    --color-subtle: #262b38;
    --color-text-2: #c3c8d8;
    --faint: #7c8298;
    --surface: #212530;
    --line2: rgb(255 255 255 / 18%);
  }
}

/* ══════════════════════════════════════════════════════════════════ faces
 *
 * Public Sans for everything a person reads, JetBrains Mono for machine
 * strings. The second face is not decoration: `RC (2)` and `SD1mo (160)` are
 * Rent Manager strings, not words, and setting them in mono tells a
 * bookkeeper at a glance which text came from the system and which came from
 * us. It also stops `HUD (87)` being read as prose.
 */
body,
input, select, textarea, button {
  font-family: "Public Sans", Inter, system-ui, sans-serif;
}

/* Text for whoever is not looking at the screen.
 *
 * The theme requires severity to travel in three channels, and in the
 * worklist two of them are colour: the row's left edge and the type's own
 * colour. The third was a `title`, which screen readers announce
 * inconsistently or not at all, so to a reader who cannot see colour the
 * severity was not there. This carries the word without taking a pixel.
 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Numbers a person compares down a column get tabular figures and nothing
   else (TYPOGRAPHY.md, "Numbers"). `.num` was pulling in the mono face as
   well, which set whole sentences containing a figure in JetBrains Mono. */
.num { font-variant-numeric: tabular-nums; letter-spacing: 0; }

/* The second face is for machine strings only: ids, codes, timestamps. It is
   what tells a bookkeeper that `HUD (87)` came from Rent Manager and is not
   prose. */
/* Ids, codes and timestamps are machine strings wherever they appear
   (TYPOGRAPHY.md: "Id, code, timestamp"). Named here by the component that
   holds them rather than by a class on every template, so a new page that
   uses `.col-id` gets it without anybody remembering. */
.mono, code, pre,
.col-id, .status-when, .fact .value.num,
.dropdown-label.num, .dropdown-option.num {
  font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

/* ═══════════════════════════════════════════════════════════════ controls
 *
 * Styled by element, not by class. The theme styles `.input`, but Django
 * renders a bare `<input type="text">` with no class — so every form field in
 * the app was falling through to browser-default chrome (a 2px inset bevel)
 * while everything around it was themed. A control should look right because
 * of what it *is*, not because somebody remembered a class name.
 *
 * `:where()` keeps specificity at zero, so a class can still override any of
 * this without an arms race.
 */
:where(.input),
:where(select),
:where(textarea),
:where(input:not([type])),
:where(input[type="text"]),
:where(input[type="url"]),
:where(input[type="password"]),
:where(input[type="number"]),
:where(input[type="date"]),
:where(input[type="datetime-local"]),
:where(input[type="search"]),
:where(input[type="email"]),
:where(input[type="tel"]) {
  width: 100%;
  box-sizing: border-box;
  min-height: 38px;
  padding: 8px 11px;
  font: 400 13.5px Inter, system-ui, sans-serif;
  color: var(--color-text);
  caret-color: var(--color-accent);
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  /* Kills the platform bevel that survives a border declaration on some
     controls, notably date and search on WebKit. */
  appearance: none;
  -webkit-appearance: none;
}

/* The chevron has to be drawn back on once `appearance: none` removes the
   native one. Declared at real specificity, not inside `:where()`: the
   theme sets `background` on `select`, which would otherwise wipe it. */
select {
  padding-right: 30px;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 16px) center, calc(100% - 11px) center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  background-color: var(--color-surface);
}

select:hover { background-color: var(--color-surface); }

/* A multi-select is a list, not a dropdown; it has no arrow to draw. */
select[multiple] {
  background-image: none;
  padding: 4px;
  min-height: 0;
}

input:hover, select:hover, textarea:hover {
  border-color: color-mix(in srgb, var(--color-text) 40%, transparent);
}

input:focus-visible, select:focus-visible, textarea:focus-visible {
  border-color: var(--color-accent);
  outline: 2px solid color-mix(in srgb, var(--color-accent) 35%, transparent);
  outline-offset: 0;
}

input::placeholder, textarea::placeholder { color: var(--muted); }

textarea { min-height: 68px; resize: vertical; }

/* Checkboxes and radios are not text boxes and must not inherit the box. */
:where(input[type="checkbox"]), :where(input[type="radio"]) {
  width: auto;
  min-height: 0;
  padding: 0;
  border: 0;
  background: none;
  appearance: auto;
  -webkit-appearance: auto;
  accent-color: var(--color-accent);
}

/* Date fields keep their picker button, which appearance:none hides. */
input[type="date"]::-webkit-calendar-picker-indicator { opacity: .55; cursor: pointer; }
input[type="date"]::-webkit-calendar-picker-indicator:hover { opacity: 1; }

/* A field label, used wherever a control needs naming. `.field` from the
   theme handles the block version; this is the inline one. */
.label {
  display: block;
  font: 500 12px Inter, system-ui, sans-serif;
  color: var(--muted);
  margin-bottom: 5px;
}

.label-sm { font-size: 10.5px; letter-spacing: .04em; }

.help { font-size: 11.5px; color: var(--muted); margin-top: 4px; line-height: 1.45; }

.err {
  font-size: 11.5px;
  color: var(--crit);
  font-weight: 500;
  margin-top: 4px;
}

/* The second channel: colour alone would put colourblind users on the wrong
   side of a form they cannot submit. */
.err::before { content: "▪ "; }

.hint { font-size: 11.5px; color: var(--muted); }

/* ═════════════════════════════════════════════════════════════════ layout */

.stack { display: flex; flex-direction: column; gap: var(--space-3); }
.stack-lg { display: flex; flex-direction: column; gap: var(--space-6); }
.row { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.row-end { display: flex; align-items: flex-end; gap: var(--space-2); flex-wrap: wrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.baseline { align-items: baseline; }
.spacer { flex: 1; }

/* Field grids. `auto` fills what it can; the numbered ones hold their shape
   until they cannot. */
.grid { display: grid; gap: 12px 14px; }
.grid-3 { grid-template-columns: repeat(3, 1fr); }
.grid-2 { grid-template-columns: repeat(2, 1fr); }
.span-2 { grid-column: span 2; }

@media (max-width: 780px) {
  .grid-3, .grid-2 { grid-template-columns: 1fr; }
  .span-2 { grid-column: auto; }
}

/* ══════════════════════════════════════════════════════════════════ cards */

.card {
  background: var(--panel);
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line);
  padding: 18px 20px;
}

/* `overflow: hidden` clips a table to the card's rounded corners — and also
   clips anything that has to escape the card, which a dropdown panel does by
   definition. A card holding one lets it out. */
.card-flush { padding: 0; overflow: hidden; }
.card-flush:has(.dropdown) { overflow: visible; }

/* A flush card drops its padding so a table can rule edge to edge — but its
   own heading is not the table, and it still needs the card's inset. */
.card-flush > .card-head { padding: 18px 20px 0; }

/* The first and last columns get the card's inset too, so the content lines
   up with the heading above it. Only the outer edges: the padding between
   columns is the table's business. */
.card-flush .table th:first-child,
.card-flush .table td:first-child { padding-left: 20px; }
.card-flush .table th:last-child,
.card-flush .table td:last-child { padding-right: 20px; }

.card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  margin-bottom: 14px;
}

.card-title { font: 600 15px Inter, system-ui, sans-serif; margin: 0; color: var(--color-text); }
.card-foot { display: flex; align-items: center; gap: 10px; margin-top: 16px; flex-wrap: wrap; }

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

.eyebrow {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--faint);
}

/* An explanatory note that must not be missed — the kind that reverses the
   reader's assumption. */
.note {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--color-text-2);
  text-wrap: pretty;
  box-shadow: inset 2px 0 0 var(--warn);
  padding-left: 11px;
  max-width: 68ch;
  margin: 0 0 14px;
}

.note-crit { box-shadow: inset 2px 0 0 var(--crit); }

/* ═══════════════════════════════════════════════════════════════ switches
 *
 * A checkbox reads as "tick to agree". On or off is a state, and across a
 * list of ten checks the difference is what you can see at a glance.
 */
.switch { position: relative; display: inline-block; width: 38px; height: 22px; flex: none; }
.switch input { position: absolute; inset: 0; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }

.switch span {
  position: absolute;
  inset: 0;
  background: var(--line);
  border-radius: 999px;
  transition: background .15s ease;
  pointer-events: none;
}

.switch span::before {
  content: "";
  position: absolute;
  width: 16px; height: 16px; left: 3px; top: 3px;
  background: var(--panel);
  border-radius: 50%;
  transition: transform .15s ease;
  box-shadow: 0 1px 2px rgb(0 0 0 / 25%);
}

.switch input:checked + span { background: var(--ok); }
.switch input:checked + span::before { transform: translateX(16px); }
.switch input:focus-visible + span { outline: 2px solid var(--color-accent); outline-offset: 2px; }

.switch-sm { width: 32px; height: 19px; }
.switch-sm span::before { width: 13px; height: 13px; left: 3px; top: 3px; }
.switch-sm input:checked + span::before { transform: translateX(13px); }


/* ══════════════════════════════════════════════════════════════ dropdowns
 *
 * The standard multi-select, from the design handoff
 * (design_handoff_settings_redesign/handoff_dropdown). Pasted as designed;
 * the only edits are this header and the notes marked ADOPTED below.
 *
 * A native `<select multiple>` is a scrolling list box, not a dropdown: it
 * takes several rows of height whether or not anyone is using it, and it
 * selects by ctrl-click, which people get wrong and cannot discover.
 */

.dropdown {
  position: relative;
  display: inline-block;
  width: 100%;
}

/* ---- closed control: must line up with a native <select> ---- */
.dropdown > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  box-sizing: border-box;
  width: 100%;
  height: 38px;
  padding: 0 var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.dropdown > summary::-webkit-details-marker { display: none; }

.dropdown-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.dropdown-label:empty::before,
.dropdown-label[data-empty]::before {
  content: attr(data-placeholder);
  color: var(--muted);
}

.chev {
  flex: none;
  width: 10px;
  height: 10px;
  color: var(--muted);
  transition: transform .14s ease;
}
.dropdown[open] > summary .chev { transform: rotate(180deg); }

.dropdown > summary:hover { border-color: var(--color-text); }
.dropdown > summary:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}
.dropdown[open] > summary { border-color: var(--color-accent); }

.dropdown[aria-disabled="true"] > summary,
.dropdown:has(> summary[aria-disabled="true"]) > summary {
  background: var(--color-bg);
  color: var(--muted);
  cursor: not-allowed;
}


/* ---- popup ---- */
.dropdown-panel {
  position: absolute;
  z-index: 40;
  top: calc(100% + var(--space-1));
  left: 0;
  min-width: 100%;
  width: max-content;
  max-width: 340px;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--color-divider);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 28px rgb(0 0 0 / .22);   /* only non-token value; no shadow token exists */
  overflow: hidden;
}

/* ADOPTED, with one change: the handoff's `@supports (position-area:)` block
 * is removed. It set `anchor-name: --dd` on every `.dropdown > summary`, so
 * all ten dropdowns on the settings page shared one anchor name and their
 * panels attached to whichever the browser resolved first — the panel opened
 * detached from the control that opened it. CSS cannot mint a unique name per
 * instance, and the alternative is an inline style per control.
 *
 * The block existed to escape a clipping ancestor. There is none: the panel
 * is checked against its ancestors in the dropdown tests, so plain absolute
 * positioning inside the `position: relative` wrapper is correct and anchors
 * per instance for free. If a scrolling ancestor is ever introduced, give the
 * dropdown a unique anchor name from the widget rather than reinstating this.
 */

/* ---- search ---- */
.dropdown-search {
  position: sticky;
  top: 0;
  z-index: 1;
  box-sizing: border-box;
  width: 100%;
  height: 34px;
  padding: 0 var(--space-3);
  background: var(--color-surface);
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  color: var(--color-text);
  font: inherit;
  font-size: 13px;
}
.dropdown-search::placeholder { color: var(--muted); }
.dropdown-search:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

/* ---- option list ---- */
.dropdown-list {
  max-height: 264px;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-gutter: stable;
  padding: var(--space-1);
  margin: 0;
  list-style: none;
}

.dropdown-option {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-2);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  font-size: 13px;
  line-height: 1.35;
  cursor: pointer;
}
.dropdown-option input[type="checkbox"] {
  flex: none;
  margin: 0;
  accent-color: var(--color-accent);
  cursor: pointer;
}
.dropdown-option:hover { background: var(--info-bg); }
.dropdown-option:has(:focus-visible) {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}
.dropdown-option:has(:checked) { color: var(--color-text); font-weight: 500; }

/* A filtered-out option disappears — unless it is chosen. */
.dropdown-option.is-filtered-out:not(:has(:checked)) { display: none; }
/* Chosen rows kept only because of the rule above read as carried over. */
.dropdown-option.is-filtered-out:has(:checked) { opacity: .72; }

/* ── what is chosen, outside the dropdown ─────────────────────────────────
 *
 * theme/DROPDOWN_CHOSEN_VALUES.md. The only record of a choice used to be
 * the text inside the closed control, and the only way to undo one was to
 * open a 165-row panel and find it. Undoing was harder than doing, which is
 * backwards for a setting somebody makes once and revises for years.
 *
 * The chips carry no form state. A chip's × unchecks its checkbox, which is
 * what redraws the row, so the two can never disagree about a count.
 */
.dropdown-field { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }

.dropdown-chosen {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 6px;
  align-items: center;
  margin: 0;
  padding: 0;
  list-style: none;
}

.dropdown-chosen:empty { display: none; }

/* Capped rather than scrolled: forty rent charge types is a configuration
   problem, and a scrolling box would hide it. "+30 more" says it out loud. */
.dropdown-chosen [data-overflow] { display: none; }
.dropdown-chosen.is-expanded [data-overflow] { display: inline-flex; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 4px 3px 4px 7px;
  border-radius: var(--radius-sm);
  background: var(--info-bg);
  color: var(--info);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}

.chip.num { font-variant-numeric: tabular-nums; }

.chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}

/* Red arrives at the moment of the destructive act, not before: a row of red
   chips reads as a row of errors. */
.chip-x:hover { background: var(--crit-bg); color: var(--crit); }
.chip-x:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

.dropdown-clear, .dropdown-more {
  padding: 4px 3px;
  border: 0;
  background: none;
  font-size: 11.5px;
  font-weight: 500;
  cursor: pointer;
}

.dropdown-clear { color: var(--muted); }
.dropdown-clear:hover { color: var(--crit); }
.dropdown-more { color: var(--color-accent); }

/* ── chosen options pinned inside the panel ───────────────────────────────
 *
 * So the panel also answers "what did I pick?" without scrolling. Grouped
 * rather than sorted: sorting the whole list by chosen state moves options
 * around under people as they click.
 */
.dropdown-group-label {
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 0;
  padding: 3px 8px;
  background: var(--panel);
  color: var(--faint);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
}

.dropdown-group + .dropdown-group { box-shadow: inset 0 1px 0 var(--line); }
.dropdown-group--chosen .dropdown-option { background: var(--info-bg); }

/* In this group the click removes, so it hovers the colour of removing. */
.dropdown-group--chosen .dropdown-option:hover { background: var(--crit-bg); }

.dropdown-empty {
  padding: var(--space-3);
  color: var(--muted);
  font-size: 13px;
}

/* ---- narrow context: table cell ---- */
.dropdown-sm { width: 170px; }
.dropdown-sm > summary {
  height: 30px;
  padding: 0 var(--space-2);
  font-size: 12.5px;
}
.dropdown-sm .dropdown-panel { min-width: 260px; }
.dropdown-sm .dropdown-list { max-height: 220px; }

/* ══════════════════════════════════════════════════════════ reference rows
 *
 * Lists you scan to confirm something is on them. Thin, quiet, scrollable,
 * never dominant.
 */
.rowlist { box-shadow: inset 0 1px 0 var(--line), inset 0 -1px 0 var(--line); }
/* A fixed height, not a maximum: two of these sit side by side, and a pair
   that resizes as rows are added makes the page jump under the cursor and
   leaves the two columns ending at different places. */
.rowlist-tall { height: 160px; overflow-y: auto; }
.rowlist table { width: 100%; table-layout: fixed; border-collapse: collapse; }

.rowlist td {
  font-size: 11.5px;
  line-height: 1.8;
  color: var(--color-text-2);
  padding: 0 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rowlist td.col-id { width: 52px; text-align: right; color: var(--faint); }
.rowlist td.col-remove { width: 28px; text-align: right; }
.rowlist tr.is-empty td { white-space: normal; font-style: italic; }

/* ═══════════════════════════════════════════════════════════ data tables */

.table-compact th, .table-compact td { padding-top: 4px; padding-bottom: 4px; font-size: 12.5px; }


/* ══════════════════════════════════════════════════════════ the worklist
 *
 * theme/ISSUES_TABLE.md, direction 1b. Not a table: a table asks you to read
 * across cells, and the question here is only "what should I do first".
 *
 * One line per issue. The wrapping that turned a screen holding twenty
 * issues into one holding nine came from columns narrower than their
 * content, so nothing wraps and nothing shrinks: it truncates, and the whole
 * string stays in `title`.
 */
.triage { display: flex; flex-direction: column; gap: 3px; }

/* Metrics taken from the design rather than approximated: 6px radius, 3px
   severity edge, 32px of row. A row two pixels off reads as a different
   component sitting in the same list. */
.triage-row {
  background: var(--panel);
  border-radius: 6px;
  box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--muted);
}

/* Severity as the left edge. Never the only signal: the type name beside it
   carries the same meaning, and the edge has a `title`. */
.triage-row[data-sev="crit"] { box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--crit); }
.triage-row[data-sev="warn"] { box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--warn); }
.triage-row[data-sev="info"] { box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--info); }
.triage-row[data-sev="res"]  { box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--res); }

.triage-row > summary {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 32px;
  padding: 7px 11px 7px 14px;
  cursor: pointer;
  list-style: none;
  font-size: 12.5px;
  color: var(--color-text-2);
}

.triage-row > summary::-webkit-details-marker { display: none; }

/* ── the list's own toolbar ───────────────────────────────────────────────
 *
 * On the same left edge as the rows, one line above the thing it acts on.
 * The chips at the top of the page are filters and a page-level refresh; a
 * control that changes the list belongs to the list, not among them.
 */
.list-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  /* Enough room that a button in here does not crowd the first row. */
  padding: 0 2px 10px;
  font-size: 12px;
  color: var(--muted);
}

.select-actions { display: inline-flex; align-items: center; gap: 12px; }

/* ── selecting rows ───────────────────────────────────────────────────────
 *
 * A mode, not a permanent column. Closing thirty-nine identical issues one
 * at a time is the worst chore on this page, but the furniture for that
 * chore should not sit in front of everybody who is only reading.
 */
.row-select { display: none; align-items: center; flex: none; margin: 0; cursor: pointer; }
.is-selecting .row-select { display: inline-flex; }

/* One action model at a time: per-row buttons go while the mode is on, or a
   row offers two different ways to act on it at once. */
.is-selecting .row-actions { display: none; }

/* The rows that cannot be selected keep their alignment, so turning the mode
   on does not shuffle every line sideways. */
.is-selecting .triage-row > summary { padding-left: 8px; }
.is-selecting .triage-row:not(:has(.row-select)) > summary { padding-left: 33px; }
/* The hover colour goes on the row, not on the summary inside it. An inset
   box-shadow paints above its own element's background but below any child's,
   so a background on the summary covered the 3px severity edge and read as a
   pale block landing on top of the row rather than the row changing colour. */
.triage-row:hover { background: var(--surface); }
.triage-row > summary:focus-visible { outline: 2px solid var(--color-accent); outline-offset: -2px; }

/* The type is the one part that takes a fixed width, so the eye can run down
   the column of them without reading each line. */
.t-type {
  width: 158px;
  flex: none;
  font-weight: 500;
  font-size: 12px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--muted);
}

.triage-row[data-sev="crit"] .t-type { color: var(--crit); }
.triage-row[data-sev="warn"] .t-type { color: var(--warn); }
.triage-row[data-sev="info"] .t-type { color: var(--info); }
.triage-row[data-sev="res"]  .t-type { color: var(--res); }

/* One line of plain English: the property is a quiet prefix, the subject
   carries the weight, and the fact is the only new information. */
.t-what { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.t-prop { color: var(--faint); }
.t-sep { color: var(--faint); }
.t-who { color: var(--color-text); font-weight: 500; }

.t-amount {
  width: 88px;
  flex: none;
  text-align: right;
  font: 500 12.5px "JetBrains Mono", ui-monospace, monospace;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

.t-age {
  width: 74px;
  flex: none;
  text-align: right;
  font: 400 11.5px "JetBrains Mono", ui-monospace, monospace;
  color: var(--faint);
}

/* Nothing at stake. An en dash in the faintest grey: a blank reads as a bug,
   and a zero would be a claim the rule never made. */
.nil { color: var(--faint); }

@media (max-width: 900px) {
  .t-type { width: 130px; }
  .t-amount { width: 78px; }
  .t-age { display: none; }
}

/* ── the row, opened ──────────────────────────────────────────────────────
 *
 * In place, so somebody keeps their position in the list. Navigating away to
 * read four facts and come back is how people lose track of which ones they
 * have already looked at.
 */
.t-body {
  padding: 12px 14px 14px;
  box-shadow: inset 0 1px 0 var(--line);
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.facts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px 16px; }

@media (max-width: 900px) { .facts { grid-template-columns: repeat(2, 1fr); } }

/* Evidence that is a list of records needs the width of the row. */
.fact-wide { grid-column: 1 / -1; }

.fact .label {
  font: 600 10px Inter, system-ui, sans-serif;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0;
}

.fact .value { margin-top: 3px; font-size: 12.5px; color: var(--color-text-2); }


/* ── actions that stay out of the way ─────────────────────────────────────
 *
 * Hidden until the row is hovered or holds focus, so a list of 45 issues is
 * not also a list of 45 buttons. Opacity rather than display, so the row
 * does not reflow, and `:focus-within` keeps every one keyboard reachable.
 */
.reveal { opacity: 0; transition: opacity .12s ease; }
summary:hover .reveal,
tr:hover .reveal,
tr:focus-within .reveal,
.reveal:focus-within { opacity: 1; }

/* Once the row is open, the real button is inside it. Two buttons saying
   "Close issue" within an inch of each other is one decision presented as a
   choice between two. */
details[open] > summary .reveal { display: none; }

@media (prefers-reduced-motion: reduce) { .reveal { transition: none; } }

/* ── just added ───────────────────────────────────────────────────────────
 *
 * Something that was not on the page a moment ago, flashed once so the eye
 * finds it. An insert that does not go through the save bar has no other
 * confirmation, and a new row appended to a list of ten is otherwise
 * indistinguishable from the ten.
 *
 * Colour only: nothing moves, nothing resizes, so it cannot push the rest of
 * the page around while somebody is reading it. Generic on purpose - any row
 * or card can carry the class, not just People.
 */
@keyframes just-added {
  from { background: var(--ok-bg); }
  to   { background: transparent; }
}
.just-added,
.just-added > td { animation: just-added 1.2s ease-out; }

/* The flash is a courtesy, and for anyone who has asked for less motion it
   is the kind that gets skipped. The row is still there either way. */
@media (prefers-reduced-motion: reduce) {
  .just-added,
  .just-added > td { animation: none; }
}

/* ── the bar under a list ─────────────────────────────────────────────────
 *
 * Where you are, how much there is, and how to reach the rest. Numbered
 * pages rather than an inner scrollbar: a card that scrolls inside a page
 * that scrolls clips rows and gives the window two scrollbars. And rather
 * than an endless "show more", which grows the document without bound and
 * loses your place every time you come back.
 */
.pager {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 2px 0;
  font-size: 12px;
  color: var(--muted);
  flex-wrap: wrap;
}

.pager-pages, .pager-sizes { display: flex; align-items: center; gap: 2px; }
.pager-sizes { gap: 4px; margin-right: 8px; }

/* Page numbers are links first and buttons never: middle-clicking page 3
   should open page 3. */
.pager a.num, .pager span.num.is-on {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 26px;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  color: var(--muted);
}

.pager a.num:hover { background: var(--surface); color: var(--color-text); text-decoration: none; }

/* The page you are on is stated, not merely tinted: it is not a link, and it
   says so by being the only one with a ring and full-strength text. */
.pager .num.is-on {
  color: var(--color-text);
  font-weight: 600;
  box-shadow: inset 0 0 0 1px var(--line2, var(--line));
}

.pager .gap { color: var(--faint); padding: 0 2px; }

/* A dead end is shown, not hidden: a control that disappears at the edges
   makes the row of them jump sideways on the last page. */
.pager .is-off { opacity: .4; cursor: default; }

/* ── the filter row ───────────────────────────────────────────────────────
 *
 * One box and two selects, on one line, nothing wrapping.
 */
.filter-row { margin: 14px 0; flex-wrap: nowrap; }

/* The box people type in gets the leftover space; the selects take only what
   their longest option needs. Form controls default to full width, which is
   right in a form and wrong on a toolbar. */
.filter-row .grow { flex: 1; min-width: 200px; }
.filter-row select { width: auto; flex: none; }

@media (max-width: 700px) { .filter-row { flex-wrap: wrap; } }

/* ── nothing to show ──────────────────────────────────────────────────────
 *
 * An empty list reads as broken. This says what was looked at and when.
 */
.empty-state {
  padding: 26px;
  text-align: center;
  background: var(--panel);
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px var(--line);
}

.empty-state .headline { font: 600 13.5px Inter, system-ui, sans-serif; color: var(--color-text); }
.empty-state p { margin: 5px 0 0; font-size: 12.5px; color: var(--muted); }

/* ══════════════════════════════════════════════════════════════ side rail */

.rail-layout { display: grid; grid-template-columns: 232px 1fr; align-items: start; }

@media (max-width: 900px) {
  .rail-layout { grid-template-columns: 1fr; }
  /* Stacked, the rail is above the content rather than beside it, so pinning
     it would cover the thing it navigates to. */
  .rail {
    position: static;
    max-height: none;
    overflow: visible;
    box-shadow: inset 0 -1px 0 var(--line);
    padding: 0 0 20px;
  }
  .rail-body { padding-left: 0; }
}

/* The rail follows you down a long page. Sticky belongs on the rail itself,
   not on a wrapper inside it: a sticky element can only move within its own
   containing block, and the inner wrapper's block was the rail, which is
   exactly as tall as the wrapper. It never moved a pixel. */
.rail {
  position: sticky;
  top: 20px;
  align-self: start;
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  scrollbar-width: thin;
  box-shadow: inset -1px 0 0 var(--line);
  padding: 4px 22px 24px 0;
}
.rail-body { padding: 4px 0 40px 28px; min-width: 0; }

.rail-title { font: 600 17px Inter, system-ui, sans-serif; color: var(--color-text); }
.rail-sub { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.rail-links { display: flex; flex-direction: column; gap: 1px; margin-top: 16px; }

.rail-links a {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--muted);
  text-decoration: none;
}

.rail-links a:hover, .rail-links a:focus-visible { background: var(--panel); color: var(--color-text); }
.rail-rule { height: 1px; background: var(--line); margin: 16px 0 12px; }

/* A status pill: a dot, a word, and a timestamp underneath. */
.status {
  margin-top: 16px;
  padding: 11px 12px;
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line);
}

/* Green, not the slate `--res`. A resolved *finding* is deliberately slate,
   to keep red and green out of the severity set for colourblind
   separability — but this is not a severity. It is a live on/off state, the
   same vocabulary as the switches, which is what `--ok` exists for. Slate
   here read as "nothing to report" rather than "working". */
.status-head { display: flex; align-items: center; gap: 7px; font: 600 12.5px Inter, sans-serif; color: var(--ok); }
.status .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: none; }
.status-bad .status-head { color: var(--crit); }
.status-bad .dot { background: var(--crit); }
.status-when { font-size: 11.5px; color: var(--muted); margin-top: 3px; }

/* ═══════════════════════════════════════════════════════════════ save bar */

.savebar {
  position: sticky;
  bottom: 14px;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 13px;
  border-radius: var(--radius-md);
  background: var(--panel);
  box-shadow: inset 0 0 0 1px var(--line), 0 6px 18px rgb(0 0 0 / 14%);
}

.savebar .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--line); flex: none; }
.savebar.is-dirty .dot { background: var(--warn); }
.savebar .status-text { font-size: 12.5px; color: var(--muted); }

.btn-text { background: none; border: 0; cursor: pointer; color: var(--muted); font-size: 12.5px; }

/* ═══════════════════════════════════════════════════════════════ the nav */

.nav-user {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding-left: 18px;
  margin-left: -8px;
  box-shadow: inset 1px 0 0 var(--line);
}

.nav-user .muted { font-size: 13px; }
.theme-toggle .theme-light { display: none; }
:root[data-theme='dark'] .theme-toggle .theme-dark { display: none; }
:root[data-theme='dark'] .theme-toggle .theme-light { display: inline; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) .theme-toggle .theme-dark { display: none; }
  :root:not([data-theme='light']) .theme-toggle .theme-light { display: inline; }
  :root[data-theme='light'] .theme-toggle .theme-dark { display: inline; }
}

/* The theme styles error, warning and success; `info` is used for no-op
   outcomes and would otherwise fall through unstyled. */
.msg.info { background: var(--info-bg); border-left-color: var(--info); color: var(--info); }


/* ═══════════════════════════════════════════════════════════ checks table
 *
 * A row per rule: state, what it is, how it is tuned. Generic — a new rule
 * appears here without anybody touching a template or this file.
 */
table.checks th { padding-bottom: 8px; }
table.checks td { padding: 12px 16px 12px 0; vertical-align: top; }
table.checks td:first-child { padding-left: 0; width: 56px; }

.check-name { width: 280px; }
.check-title { font: 600 13px Inter, system-ui, sans-serif; color: var(--color-text); }

/* Off keeps its settings visible, just receded — hiding them makes the switch
   feel destructive, and you cannot see what you are turning back on. */
/* ── one row of fields and the button that submits them ──────────────────
 *
 * The button aligns with the *inputs*, not with the help text under them, so
 * its top edge matches the field tops instead of floating against a column
 * of explanations.
 */
.add-row { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
.add-field { min-width: 200px; }
.add-field.grow { flex: 1; }

/* One label height, so the control tops line up across the row. */
.add-action { margin-top: 22px; }

/* ── the people table ─────────────────────────────────────────────────────
 *
 * One line of information per person, so one line of height. The row was
 * 87px of mostly nothing, which made three people fill a screen.
 */
.people-table tbody tr { height: 52px; }
.people-table tbody tr:hover { background: var(--info-bg); }

/* Marked for removal, not yet removed. Visible and struck through until
   Save, so Discard is meaningful and a mis-click costs nothing. */
.people-table tr.is-removing td { opacity: .5; text-decoration: line-through; }
.people-table tr.is-removing .icon-btn { opacity: 1; text-decoration: none; }

/* The DELETE box is the state; the × is the control that toggles it. */
.people-table input[name$="-DELETE"] { display: none; }

.people-table .person {
  font: 600 14px Inter, system-ui, sans-serif;
  color: var(--color-text);
  max-width: 260px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── a control that exists but cannot be used ─────────────────────────────
 *
 * Shown at 35% with its normal colours and a `title` that says why. Never
 * hidden and never greyed into the background: a missing control reads as a
 * bug, and the reason is the whole message.
 */
.is-disabled,
button.icon-btn:disabled {
  opacity: .35;
  cursor: not-allowed;
}

.is-disabled:hover { background: none; }

/* ── a reference matrix ───────────────────────────────────────────────────
 *
 * A grid of yes/no facts is read as a shape rather than as words, so it
 * carries none. Yes is a check in `--ok`; no is a faint en dash. No colour
 * behind a cell and no red for "no": nothing here is a failure, it is the
 * boundary of a role.
 *
 * The one place a bare tick is allowed to mean something. On/off *controls*
 * are still switches.
 */
.matrix tbody tr { height: 44px; }
.matrix tbody tr:hover { background: none; }
.matrix td { vertical-align: middle; }
.matrix-role { font: 600 13px Inter, system-ui, sans-serif; color: var(--color-text); }
.matrix .glyph-check { color: var(--ok); width: 8px; height: 13px; border-width: 2px; }

/* ── a table of fields, not of targets ────────────────────────────────────
 *
 * Row hover means "this row is a thing you can act on". Here every row is a
 * set of inputs you edit in place: highlighting one on the way past points at
 * nothing, and it moves under the cursor while you are aiming at a checkbox.
 */
.table-form tbody tr:hover {
  background: linear-gradient(to right, transparent, var(--soft) 40px,
              var(--soft) calc(100% - 40px), transparent) no-repeat bottom / 100% 1px;
}

/* A checkbox with its label beside it, which is the only way round a
   checkbox reads. Labels sit above every other field in the row so the
   columns line up; a box under its own label reads as unlabelled. */
.check-inline {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 38px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
}

.check-inline:hover { color: var(--color-text); }

/* A list of mutually exclusive choices. Django renders a radio group as a
   bare div of inputs and labels, which stacks with no spacing and no hit
   area worth the name. Same shape as `.check-inline`, one per line. */
/* Django wraps a radio group in its own div and each option in another, so
   the stacking has to be set on the wrapper it actually renders, not on the
   element the template names. */
.choice-list { margin-top: 4px; }
.choice-list > div { display: flex; flex-direction: column; gap: 2px; }

.choice-list label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  margin: 0;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--color-text-2);
  cursor: pointer;
}

.choice-list label:hover { background: var(--info-bg); color: var(--color-text); }
.choice-list input { margin: 0; flex: none; }
.choice-list label:has(:checked) { color: var(--color-text); font-weight: 500; }

table.checks tr.is-off td { opacity: .55; }
table.checks tr.is-bad td { box-shadow: inset 3px 0 0 var(--crit), inset 0 -1px 0 var(--line); }

/* Rule settings sit inline and wrap; each is narrow because each is one
   number, one date or one switch. */
/* Widths by type, not by content, so two rules sharing their first settings
   line up down the table (theme/RULE_SETTING_FIELD_ORDER.md). Five fit one
   row; a sixth wraps rather than everything shrinking. */
table.checks .row-end > div { min-width: 0; }
table.checks [data-width="int"] { width: 88px; }
table.checks [data-width="decimal"] { width: 96px; }
table.checks [data-width="date"] { width: 140px; }
table.checks [data-width="text"] { width: 110px; }
table.checks textarea[data-width] { width: 170px; min-height: 38px; }


/* ═══════════════════════════════════════════════════════════════ page head */

h1 { margin: 0; }

/* The page header is a block, and what follows it is the page. Without this
   the first card sits directly against the subtitle. */
.row-between + .card,
.row-between + .stack-lg,
.row-between + form { margin-top: var(--space-8); }
.sub { color: var(--muted); font-size: 13px; margin: 6px 0 22px; }
.row-between > div > .sub { margin-bottom: 0; }

/* A form field that should take the space left over on its row. */
.grow { flex: 1; min-width: 240px; }

/* A field and the button that submits it are one thing, so they stay on one
   line: `.row-end` wraps by default, and a 240px minimum on the field was
   enough to push the button underneath it in any narrow container. The field
   gives up its minimum instead, because a slightly short box still reads as a
   box, while a button on its own line reads as a separate action. */
.row-tight { flex-wrap: nowrap; }
.row-tight .grow { min-width: 0; }

/* A control inside a table cell sizes to its content. Full width is right
   in a form and wrong in a row, where it pushes the columns that carry the
   information off to the side. Applies wherever the control sits in the
   cell, not only inside a `.row`: the people table puts a bare select in a
   cell and it stretched the whole column. */
.table td select,
.table td input:not([type="checkbox"]):not([type="hidden"]) { width: auto; flex: none; }
.table td .row { flex-wrap: nowrap; }

/* Table columns that recur across pages. */
.col-actions { text-align: right; white-space: nowrap; }

/* ═════════════════════════════════════════════════════════════ narrow page
 *
 * Sign-in and anything else that is one short column.
 */
.narrow { max-width: 380px; margin: 8vh auto 0; }
.narrow .btn { width: 100%; }


/* ═════════════════════════════════════════════════════════════ icon buttons
 *
 * Row actions in a dense table. A row of word-buttons reads as a wall and
 * pushes the columns that carry the actual information off the screen; an
 * icon with a `title` and an `aria-label` says the same thing in 28px.
 *
 * Only for actions that recur on every row. Anything a person does once per
 * page keeps its words.
 */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.icon-btn svg { width: 15px; height: 15px; }
.icon-btn:hover { background: var(--info-bg); color: var(--color-text); }
.icon-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 1px; }

/* Removing is destructive-looking but reversible, so it is red at rest in a
   reference list where it must stay findable, not only on hover. */
.icon-btn-crit { color: var(--crit); }
.icon-btn-crit:hover { background: var(--crit-bg); color: var(--crit); }

/* Dense rows: the button must not set the row height. */
.rowlist .icon-btn { width: 20px; height: 20px; }
.rowlist .icon-btn svg { width: 12px; height: 12px; }

.row-actions { display: inline-flex; gap: 2px; justify-content: flex-end; }

/* == the public page ======================================================
 *
 * From Claude Design's "Rentwarden Landing.dc.html". The prototype is inline
 * styles throughout, which is that format rather than a proposal: nothing in
 * this app carries a style attribute and nothing here starts.
 *
 * Named after the kind of thing rather than after the landing page - a hero,
 * a band, a step - because the next public page will want the same furniture
 * and `.band {
  padding: 60px 26px;
  background: var(--panel);
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.band-title {
  font-size: 27px;
  font-weight: 600;
  line-height: 1.18;
  letter-spacing: -.02em;
  max-width: 700px;
  text-wrap: pretty;
}
.band-lede {
  margin-top: 14px;
  font-size: 14.5px;
  line-height: 1.6;
  color: var(--color-text-2);
  max-width: 620px;
  text-wrap: pretty;
}

/* Two columns that become one when there is not room, which is most phones.
 * `auto-fit` rather than a breakpoint, so the column count follows the
 * content width instead of a guess about the device. */
.split {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 32px;
  align-items: start;
}
.split-figure { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }

/* -- the top of the page ------------------------------------------------- */
.hero { max-width: 1080px; margin: 0 auto; padding: 76px 0 60px; }
.hero-title {
  margin-top: 16px;
  /* Fluid rather than stepped: this is the one line on the site that has to
   * hold together from a phone to a desktop without a breakpoint each. */
  font-size: clamp(31px, 5.4vw, 50px);
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: -.028em;
  max-width: 800px;
  text-wrap: pretty;
}
.hero-lede {
  margin-top: 22px;
  font-size: 16.5px;
  line-height: 1.55;
  color: var(--color-text-2);
  max-width: 640px;
  text-wrap: pretty;
}
.cta-row {
  margin-top: 32px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px 18px;
}

/* The product stated as a number, before any claim about it. */
.stat-strip {
  margin-top: 52px;
  padding: 16px 18px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 22px;
}
.stat-live { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
.stat-live .bullet { margin-top: 0; }
.stat-figure { font-size: 15px; font-weight: 500; color: var(--color-text); }
.stat-note { font-size: 12px; flex: 1; min-width: 180px; }

/* -- the arithmetic ------------------------------------------------------- */
.prose { display: flex; flex-direction: column; gap: 16px; max-width: 620px; }
.prose p { font-size: 15px; line-height: 1.65; color: var(--color-text-2); text-wrap: pretty; }
.prose-lead { color: var(--color-text); font-weight: 500; }

.ledger {
  background: var(--color-surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 18px 8px;
}
.ledger-row {
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
}
.ledger-label { font-size: 13px; color: var(--color-text-2); }
.ledger-value { font-size: 14px; font-weight: 500; color: var(--color-text); white-space: nowrap; }
.ledger-note { padding: 14px 0 8px; font-size: 12px; line-height: 1.5; color: var(--muted); text-wrap: pretty; }

/* Money going the wrong way, in the same red the app uses for a critical
 * finding, because it is the same fact. */
.is-crit { color: var(--crit); }
.is-warn { color: var(--warn); }

/* -- three steps ---------------------------------------------------------- */
.step-grid {
  margin-top: 30px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  /* The gap is the rule: one border around the group and a 1px gutter letting
   * the ground through, rather than a border per cell doubling up. */
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
}
.step { background: var(--panel); padding: 22px 20px 24px; }
.step-n { font-size: 11px; font-weight: 500; letter-spacing: .06em; color: var(--faint); }
.step-title { margin-top: 12px; font-size: 15px; font-weight: 600; line-height: 1.3; }
.step-body { margin-top: 9px; font-size: 13px; line-height: 1.6; color: var(--color-text-2); text-wrap: pretty; }

/* -- what it catches ------------------------------------------------------ */
.tile-grid {
  margin-top: 28px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
}
.tile {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 16px 18px;
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.tile-title { font-size: 14px; font-weight: 600; line-height: 1.3; }
.tile-body { margin-top: 5px; font-size: 13px; line-height: 1.55; color: var(--color-text-2); text-wrap: pretty; }

/* Severity as colour alone, which the app never does: inside the product a
 * state carries colour, glyph and word together. Here the dot sits beside a
 * sentence that already says which bucket it is, so it carries nothing on its
 * own and there is nothing to fail to perceive. */
.bullet { margin-top: 6px; width: 7px; height: 7px; flex: 0 0 7px; border-radius: 99px; }
.bullet-ok { background: var(--ok); }
.bullet-warn { background: var(--warn); }
.bullet-crit { background: var(--crit); }

.aside {
  margin-top: 18px;
  font-size: 13px;
  line-height: 1.6;
  color: var(--color-text-2);
  border-left: 2px solid var(--warn);
  padding-left: 10px;
  max-width: 660px;
  text-wrap: pretty;
}

/* -- a screenshot, or the space one is owed ------------------------------- */
.figure {
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel);
  min-height: 260px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.figure img { display: block; width: 100%; height: auto; }
.figure-wide { min-height: 620px; }
/* Unfilled on purpose, and saying so. A finished-looking page with a blank
 * rectangle in it reads as broken; one that names the missing screenshot
 * reads as unfinished, which is the true state. */
.figure-pending { border-style: dashed; }
.figure-wanted {
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 420px;
  text-align: center;
  text-wrap: pretty;
}
.figure-caption {
  margin-top: 14px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 720px;
  text-wrap: pretty;
}

/* -- what it can and cannot touch ----------------------------------------- */
.spec-list {
  margin-top: 26px;
  background: var(--color-surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 20px 8px;
  list-style: none;
}
.spec { padding: 18px 0; border-bottom: 1px solid var(--line); display: flex; gap: 12px; align-items: flex-start; }
.spec-title { font-size: 14px; font-weight: 600; line-height: 1.35; color: var(--color-text); }
.spec-body {
  margin-top: 6px;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--color-text-2);
  max-width: 660px;
  text-wrap: pretty;
}
.fine { margin-top: 14px; font-size: 12px; line-height: 1.5; color: var(--muted); max-width: 660px; text-wrap: pretty; }

/* -- pricing -------------------------------------------------------------- */
.price-card {
  background: var(--panel);
  border: 1px solid var(--color-divider);
  border-radius: 8px;
  padding: 24px;
}
.price {
  margin-top: 14px;
  font-size: 30px;
  font-weight: 500;
  letter-spacing: -.02em;
  color: var(--color-text);
}
.price-note { margin-top: 6px; font-size: 12.5px; color: var(--muted); }
.tick-list { margin-top: 20px; padding-top: 4px; border-top: 1px solid var(--line); list-style: none; }
.tick {
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  display: flex;
  gap: 10px;
  align-items: baseline;
  font-size: 13.5px;
  color: var(--color-text-2);
}
.tick-mark { color: var(--ok); font-size: 13px; }
.price-card .btn { margin-top: 22px; }

/* -- questions ------------------------------------------------------------ */
.faq {
  margin-top: 26px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 24px;
}
.faq-item { border-top: 1px solid var(--color-divider); padding-top: 14px; }
.faq-q { font-size: 14px; font-weight: 600; line-height: 1.4; text-wrap: pretty; }
.faq-a { margin: 8px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--color-text-2); text-wrap: pretty; }

/* -- the last ask --------------------------------------------------------- */
.closer { padding: 76px 0 84px; }
.closer-title {
  font-size: clamp(26px, 4vw, 36px);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -.025em;
  max-width: 640px;
  text-wrap: pretty;
}
.closer-lede { margin-top: 16px; font-size: 15.5px; line-height: 1.6; color: var(--color-text-2); }

/* -- public chrome -------------------------------------------------------- */
.site-head { border-bottom: 1px solid var(--line); background: var(--panel); }
.site-links { display: flex; align-items: center; gap: 18px; flex: 1; }
.site-links a { font-size: 13px; color: var(--muted); text-decoration: none; }
.site-links a:hover { color: var(--color-text); }

.site-foot { border-top: 1px solid var(--line); background: var(--panel); margin-top: 40px; }
.site-foot .wrap {
  padding: 24px 26px 34px;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px 24px;
}
.foot-brand { font-size: 13px; font-weight: 600; }
.foot-line { font-size: 12.5px; color: var(--muted); flex: 1; }
.site-foot a { font-size: 12.5px; color: var(--muted); text-decoration: none; }
.site-foot a:hover { color: var(--color-text); }
.foot-signin { font-weight: 600; }

/* The one button size the app had no use for: a public call to action is the
 * biggest thing on its row, and inside the product nothing should be. */
.btn-lg { height: 46px; padding: 0 24px; font-size: 15px; }

@media (max-width: 640px) {
  .site-links { display: none; }
  .hero { padding: 48px 0 40px; }
  .band { padding-left: 18px; padding-right: 18px; }
  .figure-wide { min-height: 320px; }
}

/* Waiting on a person, rather than on somebody getting round to it.
 *
 * The row takes the `info` colour instead of its severity colour, rather than
 * a tint behind it. Red and amber mean "this is costing you"; a row somebody
 * has already asked to close is not costing you anything while it waits, and
 * leaving it red puts it in the same visual bucket as the ones that are.
 *
 * Blue because it already means "a state, not a problem" everywhere else
 * here. The severity itself is not lost: the word is still in the row, which
 * is the channel that survives when colour does not.
 *
 * After the `data-sev` rules, and at higher specificity, so it wins whatever
 * severity the finding happens to carry.
 */
.triage-row.is-waiting[data-sev] {
  box-shadow: inset 0 0 0 1px var(--line), inset 3px 0 0 var(--info);
}
.triage-row.is-waiting[data-sev] .t-type { color: var(--info); }

/* ── confirmations, out of the way ────────────────────────────────────────
 *
 * These used to sit in the document flow, so one appearing pushed the page
 * down under the reader's eyes, and it stayed there until the next
 * navigation. Both are wrong for the same reason: a confirmation is an event,
 * not content.
 *
 * Fixed, so nothing reflows. Bottom right, away from the nav and the save
 * bar, which is the other thing that floats.
 */
.toasts {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: min(420px, calc(100vw - 40px));
  pointer-events: none;
}
.toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin: 0;
  box-shadow: 0 6px 24px rgb(0 0 0 / .16);
  animation: toast-in .18s ease-out;
}
.toast-text { flex: 1; }
.toast-close {
  flex: none;
  background: none;
  border: 0;
  padding: 0 2px;
  font-size: 18px;
  line-height: 1;
  color: currentColor;
  opacity: .6;
  cursor: pointer;
}
.toast-close:hover { opacity: 1; }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.toast.is-leaving { opacity: 0; transition: opacity .2s ease; }

/* Somebody who asked for less motion still gets the toast, just without it
   sliding in. It is information either way. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast.is-leaving { transition: none; }
}

@media (max-width: 640px) {
  .toasts { right: 12px; left: 12px; bottom: 12px; max-width: none; }
}

/* ── which company you are looking at ─────────────────────────────────────
 *
 * The dropdown component, in the nav, with the options as submit buttons
 * rather than links: switching changes what every page shows next, which is
 * a POST, and membership is re-checked server side rather than trusted from
 * whatever slug a browser submits.
 */
.company-switch { margin-left: 4px; }
/* Its own element rather than `.dropdown-label`, which picker.js owns and
   fills from the checked inputs. This control has none. */
.switch-current { font: inherit; color: inherit; }
.company-switch .dropdown-list form { margin: 0; }
.switch-option {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  cursor: pointer;
  color: inherit;
}
.switch-option[aria-current="true"] { font-weight: 600; }

/* ── the lockup ───────────────────────────────────────────────────────────
 *
 * The mark is inline SVG so it can take `var(--color-accent)` and
 * `currentColor` and be right in both themes from one copy. The wordmark is
 * real text, in the real webfont, which an SVG `<text>` inside an `<img>`
 * could never be.
 *
 * The files design supplied are in `static/audit/brand/` and are the right
 * thing outside the app, where there is no stylesheet to inherit from.
 */
/* The logo's own blue, which is not the app's accent.
 *
 * theme.css sets `--color-accent: #5d5294`; design's mark, lockups and
 * favicon all use `#3f4bc4`. Following the token made the nav mark a muted
 * purple while the favicon in the same browser tab stayed indigo: one logo,
 * two colours, side by side.
 *
 * A brand colour belongs to the brand rather than to the palette, so it is
 * stated here. Light and dark are design's own pair, from mark.svg and
 * mark-dark.svg, so the inline mark and the shipped files cannot drift.
 */
:root, :root[data-theme='light'] { --brand-accent: #3f4bc4; }
:root[data-theme='dark'] { --brand-accent: #98a2ff; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) { --brand-accent: #98a2ff; }
}

/* `.nav a` paints every nav link in `--muted`, which greyed the rule under
   the mark. The brand is the one link in there that is not navigation. */
.nav .brand,
.nav .brand:hover {
  color: var(--color-text);
  text-decoration: none;
}
.brand {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-right: auto;
  color: var(--color-text);
  text-decoration: none;
}
.brand-mark { width: 24px; height: 24px; flex: none; display: block; }
.brand-name {
  font: 500 16px "Public Sans", Inter, system-ui, sans-serif;
  letter-spacing: -.02em;
}

/* The mark alone below the width where the name would crowd the nav. It is
   the recognisable half, and the page title says the rest. */
@media (max-width: 480px) {
  .brand-name { display: none; }
}

/* ── something is happening ───────────────────────────────────────────────
 *
 * A run takes about forty seconds and used to be invisible: the "Run started"
 * toast leaves after five, and nothing else on the page changed until
 * somebody reloaded. Thirty-four seconds of a page that looks like the button
 * did nothing is how people press it twice.
 *
 * No bar. What is honestly knowable is that it started, that it is alive, and
 * how long it has been; a bar would have to invent the rest, and this is an
 * application whose whole claim is that it does not tell you things it does
 * not know.
 */
.working {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 12px;
  padding: 10px 14px;
  background: var(--info-bg);
  border: 1px solid color-mix(in srgb, var(--info) 30%, transparent);
  border-radius: var(--radius-md);
  font-size: 13px;
  color: var(--color-text);
}
.working-text { flex: 1; }

/* Alive rather than decorative: it is the one thing on screen saying the
   process has not gone away, which this app has watched happen. */
.working-pulse {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 50%;
  background: var(--info);
  animation: working-pulse 1.4s ease-in-out infinite;
}
@keyframes working-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: .35; transform: scale(.78); }
}

/* Still says a run is happening, without the movement. The elapsed seconds
   carry it for anyone who asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .working-pulse { animation: none; }
}
