/* ============================================================================
   CivilHive Design System — 04 · Components
   ----------------------------------------------------------------------------
   Every visible component in the product. Loaded after Bootstrap, and each
   block here also claims the Bootstrap class it replaces (`.btn`, `.card`,
   `.badge`, `.table`, `.modal-content`, `.alert`…) so legacy templates that
   have not been rebuilt yet still land inside the system rather than showing
   Bootstrap's own look next to it.
   ========================================================================== */

/* ============================================================================
   Buttons
   ============================================================================
   Pills, sentence case, set at reading size. The old layer used 12px uppercase
   labels, which read as chrome rather than as something you press.

   Hierarchy: charcoal solid is the primary action; amber is reserved for the
   ONE brand moment per view (the marketing CTA, "Post a project"). A page with
   two amber buttons has no primary action.
   ========================================================================== */
.cv-btn,
.btn {
  --_bg: transparent;
  --_ink: var(--cv-ink);
  --_line: transparent;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cv-sp-2);
  padding: 0.5rem 1.125rem;
  min-height: 2.5rem;
  border: 1px solid var(--_line);
  border-radius: var(--cv-r-pill);
  background-color: var(--_bg);
  background-image: none;
  color: var(--_ink);
  font-family: inherit;
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-semibold);
  line-height: 1.4;
  letter-spacing: var(--cv-track-body);
  text-align: center;
  text-transform: none;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  box-shadow: none;
  transition: background-color var(--cv-t-fast) var(--cv-ease),
              border-color var(--cv-t-fast) var(--cv-ease),
              color var(--cv-t-fast) var(--cv-ease),
              opacity var(--cv-t-fast) var(--cv-ease);
}
.cv-btn:hover, .btn:hover { color: var(--_ink); }
.cv-btn:focus-visible, .btn:focus-visible { outline: none; box-shadow: var(--cv-ring); }
.cv-btn:active:not(:disabled), .btn:active:not(:disabled) { transform: translateY(1px); }
.cv-btn:disabled, .btn:disabled, .cv-btn.disabled, .btn.disabled {
  opacity: 0.5; cursor: not-allowed; transform: none;
}
.cv-btn .bi, .btn .bi { font-size: 1em; line-height: 1; }

/* Primary — charcoal */
.cv-btn--primary, .btn-primary, .btn-dark {
  --_bg: var(--cv-action); --_ink: var(--cv-action-ink); --_line: var(--cv-action);
}
.cv-btn--primary:hover, .btn-primary:hover, .btn-dark:hover {
  --_bg: var(--cv-action-hover); --_line: var(--cv-action-hover); --_ink: var(--cv-action-ink);
}

/* Accent — amber. Dark ink, always: white on amber fails contrast. */
.cv-btn--accent, .btn-warning {
  --_bg: var(--cv-accent); --_ink: var(--cv-ink-on-accent); --_line: var(--cv-accent);
}
.cv-btn--accent:hover, .btn-warning:hover {
  --_bg: var(--cv-accent-hover); --_line: var(--cv-accent-hover); --_ink: var(--cv-ink-on-accent);
}

/* Secondary — white on a hairline. The default for row-level actions. */
.cv-btn--secondary, .btn-secondary, .btn-light, .btn-outline-secondary, .btn-outline-dark {
  --_bg: var(--cv-surface); --_ink: var(--cv-ink); --_line: var(--cv-line-strong);
}
.cv-btn--secondary:hover, .btn-secondary:hover, .btn-light:hover,
.btn-outline-secondary:hover, .btn-outline-dark:hover {
  --_bg: var(--cv-surface-sunk); --_ink: var(--cv-ink); --_line: var(--cv-line-strong);
}

/* Ghost — no chrome until hovered. Toolbars, card headers. */
.cv-btn--ghost, .btn-link {
  --_bg: transparent; --_ink: var(--cv-ink-2); --_line: transparent;
  text-decoration: none;
}
.cv-btn--ghost:hover, .btn-link:hover { --_bg: var(--cv-surface-sunk); --_ink: var(--cv-ink); }

/* Status buttons */
.cv-btn--danger, .btn-danger  { --_bg: var(--cv-danger);  --_ink: #fff; --_line: var(--cv-danger); }
.cv-btn--danger:hover, .btn-danger:hover { --_bg: var(--cv-danger-text); --_line: var(--cv-danger-text); --_ink: #fff; }
.cv-btn--success, .btn-success { --_bg: var(--cv-success); --_ink: #fff; --_line: var(--cv-success); }
.cv-btn--success:hover, .btn-success:hover { --_bg: var(--cv-success-text); --_line: var(--cv-success-text); --_ink: #fff; }
.cv-btn--info, .btn-info { --_bg: var(--cv-info); --_ink: #fff; --_line: var(--cv-info); }
.cv-btn--info:hover, .btn-info:hover { --_bg: var(--cv-info-text); --_line: var(--cv-info-text); --_ink: #fff; }
.btn-outline-primary { --_bg: transparent; --_ink: var(--cv-ink); --_line: var(--cv-ink); }
.btn-outline-primary:hover { --_bg: var(--cv-action); --_ink: var(--cv-action-ink); --_line: var(--cv-action); }
/* The considered form of a destructive action: it has to be findable and
   unmistakably red, but a solid red block in a side panel outshouts the thing
   the page is actually for. `--danger` stays for a confirmation screen, where
   destroying something IS the primary action. */
.cv-btn--danger-outline,
.btn-outline-danger { --_bg: transparent; --_ink: var(--cv-danger-text); --_line: var(--cv-danger); }
.cv-btn--danger-outline:hover,
.btn-outline-danger:hover { --_bg: var(--cv-danger); --_ink: #fff; --_line: var(--cv-danger); }
.btn-outline-success { --_bg: transparent; --_ink: var(--cv-success-text); --_line: var(--cv-success); }
.btn-outline-success:hover { --_bg: var(--cv-success); --_ink: #fff; --_line: var(--cv-success); }
/* Sits on a photo or a dark band. */
.cv-btn--onDark { --_bg: var(--cv-n-0); --_ink: var(--cv-n-900); --_line: var(--cv-n-0); }
.cv-btn--onDark:hover { --_bg: var(--cv-n-100); --_line: var(--cv-n-100); --_ink: var(--cv-n-900); }

/* Sizes */
.cv-btn--sm, .btn-sm { min-height: 2.125rem; padding: 0.3125rem 0.875rem; font-size: var(--cv-text-xs); }
.cv-btn--lg, .btn-lg { min-height: 2.875rem; padding: 0.6875rem 1.5rem; font-size: var(--cv-text-md); }
.cv-btn--block, .btn-block { width: 100%; }
.cv-btn--icon { width: 2.5rem; padding: 0; }
.cv-btn--icon.cv-btn--sm { width: 2.125rem; }

/* ============================================================================
   Cards & panels
   ============================================================================ */
.cv-card, .card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  box-shadow: none;
  transition: border-color var(--cv-t-fast) var(--cv-ease), box-shadow var(--cv-t-fast) var(--cv-ease);
}
.cv-card__body, .card-body { padding: var(--cv-sp-5); }
.cv-card__head, .card-header {
  display: flex; align-items: center; justify-content: space-between; gap: var(--cv-sp-3);
  padding: var(--cv-sp-4) var(--cv-sp-5);
  background-color: transparent;
  border-bottom: 1px solid var(--cv-line-soft);
  border-radius: var(--cv-r-lg) var(--cv-r-lg) 0 0;
}
.cv-card__head h1, .cv-card__head h2, .cv-card__head h3, .cv-card__head h4,
.card-header h1, .card-header h2, .card-header h3, .card-header h4,
.cv-card__title, .card-title { margin: 0; font-size: var(--cv-text-h4); }
.cv-card__foot, .card-footer {
  padding: var(--cv-sp-4) var(--cv-sp-5);
  background-color: transparent;
  border-top: 1px solid var(--cv-line-soft);
  border-radius: 0 0 var(--cv-r-lg) var(--cv-r-lg);
}
/* Interactive cards get a border cue, never a lift — most cards here are list
   rows, and rows that jump under the cursor make a long list feel unstable. */
.cv-card--link:hover, .cv-card--hover:hover { border-color: var(--cv-line-strong); box-shadow: var(--cv-shadow-sm); }
.cv-card--flush { border: 0; border-radius: 0; background: transparent; }
.cv-card--sunk { background-color: var(--cv-surface-sunk); border-color: transparent; }

/* ─── Result row — the list-page card from the reference ──────────────────
   Media | content | action rail. Collapses to a single column on phones. */
.cv-result {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--cv-sp-4);
  padding: var(--cv-sp-5);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  transition: border-color var(--cv-t-fast) var(--cv-ease), box-shadow var(--cv-t-fast) var(--cv-ease);
}
.cv-result:hover { border-color: var(--cv-line-strong); box-shadow: var(--cv-shadow-sm); }
@media (min-width: 768px) {
  .cv-result--media { grid-template-columns: 12rem minmax(0, 1fr); }
  .cv-result--rail  { grid-template-columns: minmax(0, 1fr) 15rem; }
  .cv-result--media.cv-result--rail { grid-template-columns: 12rem minmax(0, 1fr) 15rem; }
}
.cv-result__media {
  aspect-ratio: 4 / 3;
  border-radius: var(--cv-r-md);
  overflow: hidden;
  background-color: var(--cv-surface-sunk);
}
.cv-result__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cv-result__body { min-width: 0; }
.cv-result__title {
  margin: 0 0 var(--cv-sp-2);
  font-size: var(--cv-text-h4);
  overflow-wrap: anywhere;
}
.cv-result__title a { color: var(--cv-ink); }
.cv-result__title a:hover { color: var(--cv-accent-text); }
.cv-result__meta {
  margin: 0 0 var(--cv-sp-3);
  font-size: var(--cv-text-xs);
  color: var(--cv-ink-3);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--cv-sp-1) var(--cv-sp-2);
}
.cv-result__meta > * + *::before { content: "·"; margin-right: var(--cv-sp-2); color: var(--cv-ink-4); }
.cv-result__desc {
  margin: 0 0 var(--cv-sp-3);
  font-size: var(--cv-text-sm);
  color: var(--cv-ink-2);
  overflow-wrap: anywhere;
}
.cv-result__foot { display: flex; align-items: center; gap: var(--cv-sp-2); flex-wrap: wrap; margin-top: var(--cv-sp-4); }
.cv-result__side {
  display: flex; flex-direction: column; gap: var(--cv-sp-2);
  padding-top: var(--cv-sp-1);
}
@media (min-width: 768px) {
  .cv-result--rail .cv-result__side {
    padding-left: var(--cv-sp-5);
    border-left: 1px solid var(--cv-line-soft);
    padding-top: 0;
  }
}
.cv-result__side .cv-btn, .cv-result__side .btn { width: 100%; }

/* In grid mode there is no side rail — the same block stacks under the body,
   where a column of uppercase labels is heavier than the content above it.
   Lay the key/value pairs out inline instead. */
.cv-results--grid .cv-result__side {
  border-left: 0;
  padding-left: 0;
  padding-top: var(--cv-sp-3);
  border-top: 1px solid var(--cv-line-soft);
}
.cv-results--grid .cv-result__side > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--cv-sp-3);
}
.cv-results--grid .cv-result__side .cv-deflist__k { margin-bottom: 0; }

/* The deadline line. Red is reserved for exactly this kind of time pressure. */
.cv-deadline { font-size: var(--cv-text-xs); font-weight: var(--cv-weight-semibold); color: var(--cv-ink-2); }
.cv-deadline--urgent { color: var(--cv-urgent-text); }

/* ─── KPI tile ────────────────────────────────────────────────────────────*/
/* A grid, not a flex row. Every call site passes bare spans with no wrapper
   element — as a flex row that put the label and the number side by side,
   pushed to opposite ends of the tile, which is not what a KPI tile is. The
   grid gives the text its own column, one item per row, and lets the optional
   icon span the whole height on the right. row-gap stays 0 so the empty rows
   the icon's span creates cost nothing; the label and delta carry margins. */
.cv-kpi {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  column-gap: var(--cv-sp-4);
  row-gap: 0;
  padding: var(--cv-sp-5);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
}
.cv-kpi > * { grid-column: 1; }
.cv-kpi__note {
  display: block; margin-top: var(--cv-sp-2);
  font-size: var(--cv-text-2xs); color: var(--cv-ink-3);
}
.cv-kpi__label {
  display: block; margin-bottom: var(--cv-sp-1);
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--cv-ink-3);
}
.cv-kpi__value {
  display: block;
  font-size: 1.75rem; font-weight: var(--cv-weight-bold); line-height: 1.1;
  letter-spacing: var(--cv-track-h2); color: var(--cv-ink);
  font-variant-numeric: tabular-nums;
}
.cv-kpi__delta { display: inline-flex; align-items: center; gap: 0.2rem; margin-top: var(--cv-sp-2); font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium); }
.cv-kpi__delta--up { color: var(--cv-success-text); }
.cv-kpi__delta--down { color: var(--cv-danger-text); }
.cv-kpi__value--urgent { color: var(--cv-danger-text); }
/* Icon tile: a soft amber square, not a saturated gradient block. */
.cv-kpi__icon {
  grid-column: 2; grid-row: 1 / span 6; align-self: center;
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; width: 2.75rem; height: 2.75rem;
  border-radius: var(--cv-r-md);
  background-color: var(--cv-accent-soft);
  color: var(--cv-accent-text);
  font-size: 1.15rem;
}
.cv-kpi__icon--success { background-color: var(--cv-success-soft); color: var(--cv-success-text); }
.cv-kpi__icon--danger  { background-color: var(--cv-danger-soft);  color: var(--cv-danger-text); }
.cv-kpi__icon--info    { background-color: var(--cv-info-soft);    color: var(--cv-info-text); }
.cv-kpi__icon--neutral { background-color: var(--cv-surface-sunk); color: var(--cv-ink-2); }

.cv-kpi-row { display: grid; gap: var(--cv-sp-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }

/* ============================================================================
   Chips, badges, tags
   ============================================================================ */
.cv-chip, .badge {
  display: inline-flex; align-items: center; gap: var(--cv-sp-1);
  padding: 0.2rem 0.625rem;
  border: 1px solid transparent;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-medium);
  line-height: 1.5;
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  /* A chip must never be wider than what contains it. `nowrap` above is right
     for a status label ("Published", "Located"), but a chip holding user data
     -- a place line, a category name -- has no length limit, and without this
     it simply ran out past the edge of its card. */
  max-width: 100%;
  min-width: 0;
}

/* For a chip whose text comes from the database rather than a fixed
   vocabulary: it wraps onto a second line instead of escaping the card, and an
   unbroken token (a PIN run together with a street, say) breaks rather than
   pushing the whole chip out. */
.cv-chip--wrap {
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: left;
  /* Two lines of text against a centred icon reads as misaligned; pin the
     icon to the first line instead. */
  align-items: flex-start;
  border-radius: var(--cv-r-md);
}
.cv-chip--wrap > i { line-height: 1.5; }
.cv-chip--accent,  .badge.bg-warning, .badge.text-bg-warning { background-color: var(--cv-accent-soft);  color: var(--cv-accent-text); }
.cv-chip--success, .badge.bg-success, .badge.text-bg-success { background-color: var(--cv-success-soft); color: var(--cv-success-text); }
.cv-chip--danger,  .badge.bg-danger,  .badge.text-bg-danger  { background-color: var(--cv-danger-soft);  color: var(--cv-danger-text); }
.cv-chip--info,    .badge.bg-info,    .badge.text-bg-info    { background-color: var(--cv-info-soft);    color: var(--cv-info-text); }
.cv-chip--neutral, .badge.bg-secondary, .badge.bg-light      { background-color: var(--cv-surface-sunk); color: var(--cv-ink-2); }
.cv-chip--solid,   .badge.bg-primary,  .badge.bg-dark        { background-color: var(--cv-ink); color: var(--cv-n-0); }
.cv-chip--outline { background-color: transparent; border-color: var(--cv-line-strong); color: var(--cv-ink-2); }
/* A leading dot for status chips reads faster than colour alone. */
.cv-chip__dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background-color: currentColor; flex: 0 0 auto; }

/* ─── Filter chip — the pill row from the reference ───────────────────────*/
.cv-filterchip {
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  padding: 0.375rem 0.875rem;
  border: 1px solid var(--cv-line-strong);
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-medium);
  cursor: pointer;
  transition: background-color var(--cv-t-fast) var(--cv-ease),
              border-color var(--cv-t-fast) var(--cv-ease),
              color var(--cv-t-fast) var(--cv-ease);
}
.cv-filterchip:hover { border-color: var(--cv-n-400); color: var(--cv-ink); }
.cv-filterchip.is-active, .cv-filterchip[aria-pressed="true"] {
  background-color: var(--cv-accent-soft);
  border-color: var(--cv-accent-line);
  color: var(--cv-accent-text);
  font-weight: var(--cv-weight-semibold);
}
.cv-filterchip__x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1rem; height: 1rem; margin-right: -0.25rem;
  border: 0; border-radius: 50%;
  background-color: transparent; color: inherit;
  font-size: 0.7rem; cursor: pointer; opacity: 0.7;
}
.cv-filterchip__x:hover { opacity: 1; background-color: rgb(0 0 0 / 0.08); }

/* ─── Rating ──────────────────────────────────────────────────────────────*/
.cv-rating { display: inline-flex; align-items: center; gap: var(--cv-sp-1); font-size: var(--cv-text-xs); }
.cv-rating__stars { display: inline-flex; gap: 1px; color: var(--cv-accent); font-size: 0.8rem; line-height: 1; }
.cv-rating__stars .is-empty { color: var(--cv-line-strong); }
.cv-rating__value { font-weight: var(--cv-weight-semibold); color: var(--cv-ink); font-variant-numeric: tabular-nums; }
.cv-rating__count { color: var(--cv-ink-3); }

/* A review row. Two columns from 48rem up — who wrote it and what it was about
   on a narrow rail, the rating and the body beside it — because a 60-character
   comment alone in a 1400px card reads as a layout mistake. The body keeps a
   measure for the same reason an article body does; the card itself stays
   fluid, so the page still fills the viewport. */
.cv-review { display: grid; gap: var(--cv-sp-3); grid-template-columns: minmax(0, 1fr); }
.cv-review__who { display: flex; gap: var(--cv-sp-3); align-items: flex-start; min-width: 0; }
.cv-review__name {
  margin: 0; font-size: var(--cv-text-md);
  font-weight: var(--cv-weight-semibold); color: var(--cv-ink);
}
.cv-review__meta {
  margin: var(--cv-sp-1) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-3);
  font-variant-numeric: tabular-nums;
}
.cv-review__body { min-width: 0; }
.cv-review__text { margin: var(--cv-sp-3) 0 0; max-width: 72ch; }
.cv-review__tags {
  display: flex; flex-wrap: wrap; gap: var(--cv-sp-2) var(--cv-sp-4);
  margin: var(--cv-sp-4) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-3);
}
@media (min-width: 48rem) {
  .cv-review { grid-template-columns: 16rem minmax(0, 1fr); gap: var(--cv-sp-6); }
}

/* A star rating INPUT that works with JavaScript switched off. The radios are
   rendered in descending order and the row is reversed, so `input:checked ~
   label` — which reaches the labels that follow in the DOM, i.e. the lower
   values — fills exactly the stars to the left of the choice. The previous
   widget hid its radios in `.d-none` and set them from script, so a visitor
   without JS could not pick a rating at all and the form could never be
   submitted. */
.cv-starpick { border: 0; padding: 0; margin: 0; min-width: 0; }
.cv-starpick__set { display: inline-flex; flex-direction: row-reverse; gap: var(--cv-sp-1); }
.cv-starpick__set input {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.cv-starpick__set label {
  cursor: pointer; font-size: 1.75rem; line-height: 1;
  color: var(--cv-line-strong); transition: color var(--cv-t-fast) var(--cv-ease);
}
.cv-starpick__set input:checked ~ label { color: var(--cv-accent); }
.cv-starpick__set:hover label { color: var(--cv-line-strong); }
.cv-starpick__set label:hover,
.cv-starpick__set label:hover ~ label { color: var(--cv-accent); }
/* Focus has to be visible on the LABEL, since the radio itself is off-screen. */
.cv-starpick__set input:focus-visible + label {
  box-shadow: var(--cv-ring); border-radius: var(--cv-r-sm);
}

/* ─── Avatar ──────────────────────────────────────────────────────────────*/
.cv-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 2.5rem; height: 2.5rem;
  border-radius: 50%;
  overflow: hidden;
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-semibold);
  text-transform: uppercase;
}
.cv-avatar img { width: 100%; height: 100%; object-fit: cover; }
.cv-avatar--sm { width: 2rem; height: 2rem; font-size: var(--cv-text-2xs); }
.cv-avatar--lg { width: 3.5rem; height: 3.5rem; font-size: var(--cv-text-md); }
.cv-avatar--xl { width: 5rem; height: 5rem; font-size: var(--cv-text-h3); }
.cv-avatar--square { border-radius: var(--cv-r-md); }
/* A verified tick, anchored to the avatar. */
.cv-avatar-wrap { position: relative; display: inline-flex; }
.cv-avatar-wrap__tick {
  position: absolute; right: -2px; bottom: -2px;
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.1rem; height: 1.1rem;
  border-radius: 50%;
  border: 2px solid var(--cv-surface);
  background-color: var(--cv-success);
  color: #fff; font-size: 0.55rem;
}

/* ============================================================================
   Field — the labelled control unit
   ============================================================================
   Rendered by templates/ui/_field.html. Label above, control, then help or
   error. One structure everywhere, at two densities: default for real forms,
   `--compact` for the filter rail and inline search bars.
   ========================================================================== */
.cv-field { display: block; margin-bottom: var(--cv-sp-4); }
.cv-field:last-child { margin-bottom: 0; }
/* A checkbox field carries both classes (see ui/_field.html), and `.cv-field`
   here would otherwise beat `.form-check` in base.css on source order alone —
   both are one class, and this sheet loads later. The box then sat ABOVE its
   label instead of beside it, everywhere {% cv_field %} rendered a checkbox. */
.cv-field.form-check {
  display: flex;
  align-items: flex-start;
  gap: var(--cv-sp-2);
}
/* The text half is a flex item, and a flex item's default `min-width: auto` is
   its own content width — so a checkbox with a sentence for a label refused to
   wrap and pushed the form past the viewport at 390px. Same trap as the
   `min-height: auto` that made the console nav's `overflow-y` inert. */
.cv-field.form-check > div { min-width: 0; }
.cv-field__label {
  display: block;
  margin-bottom: var(--cv-sp-2);
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-medium);
  color: var(--cv-ink);
}
.cv-field--compact { margin-bottom: var(--cv-sp-3); }
.cv-field--compact .cv-field__label { margin-bottom: var(--cv-sp-1); font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }
.cv-field--compact input, .cv-field--compact select, .cv-field--compact textarea { min-height: 2.25rem; font-size: var(--cv-text-xs); }
.cv-field--invalid input, .cv-field--invalid select, .cv-field--invalid textarea { border-color: var(--cv-danger); }

/* A prefix/suffix affordance inside the control, e.g. ₹ on a bid amount. */
.cv-field__wrap { position: relative; display: block; }
/* The control's own padding comes from a selector list in base.css whose
   `.form-control` branch is a class, so a plain descendant selector here does
   not reliably outrank it once Bootstrap's `.form-control` is in the mix.
   Naming the class the widgets actually carry settles it, and covers a widget
   rendered without that class too. */
.cv-field__wrap--prefix > input,
.cv-field__wrap--prefix > input.form-control { padding-inline-start: 2rem; }
.cv-field__wrap--suffix > input,
.cv-field__wrap--suffix > input.form-control { padding-inline-end: 2.25rem; }
.cv-field__wrap--suffix > .cv-field__affix { left: auto; right: 0.75rem; }
.cv-field__affix {
  position: absolute; top: 50%; left: 0.75rem;
  transform: translateY(-50%);
  color: var(--cv-ink-3);
  font-size: var(--cv-text-sm);
  pointer-events: none;
}

/* Field grids: forms lay out on a 12-track grid so two- and three-up rows are
   declarative rather than a nest of Bootstrap columns. */
.cv-fieldgrid { display: grid; gap: var(--cv-sp-4); grid-template-columns: repeat(12, minmax(0, 1fr)); }
.cv-fieldgrid > * { grid-column: span 12; }
/* All twelve spans, not a subset. The partial set this started as meant a
   {% cv_field col="2" %} silently fell back to span 12 and broke the row it
   was part of — a missing rule that looks like a layout bug. */
@media (min-width: 640px) {
  .cv-fieldgrid > .cv-col-1  { grid-column: span 1; }
  .cv-fieldgrid > .cv-col-2  { grid-column: span 2; }
  .cv-fieldgrid > .cv-col-3  { grid-column: span 3; }
  .cv-fieldgrid > .cv-col-4  { grid-column: span 4; }
  .cv-fieldgrid > .cv-col-5  { grid-column: span 5; }
  .cv-fieldgrid > .cv-col-6  { grid-column: span 6; }
  .cv-fieldgrid > .cv-col-7  { grid-column: span 7; }
  .cv-fieldgrid > .cv-col-8  { grid-column: span 8; }
  .cv-fieldgrid > .cv-col-9  { grid-column: span 9; }
  .cv-fieldgrid > .cv-col-10 { grid-column: span 10; }
  .cv-fieldgrid > .cv-col-11 { grid-column: span 11; }
  /* Redundant against the `> *` default above, but `col="12"` is the obvious
     thing to write for a full-width field and every other span exists — so
     without this rule it was a class that resolved to nothing. */
  .cv-fieldgrid > .cv-col-12 { grid-column: span 12; }
}

/* A titled block of fields inside a long form. */
.cv-formsection { padding-block: var(--cv-sp-6); border-top: 1px solid var(--cv-line-soft); }
.cv-formsection:first-of-type { padding-top: 0; border-top: 0; }
.cv-formsection__title { margin: 0 0 var(--cv-sp-1); font-size: var(--cv-text-h4); }
.cv-formsection__hint { margin: 0 0 var(--cv-sp-5); font-size: var(--cv-text-xs); color: var(--cv-ink-3); }

/* Sticky form actions — a long form's submit must not require a scroll hunt. */
.cv-formbar {
  position: sticky;
  bottom: 0;
  display: flex; align-items: center; justify-content: flex-end; gap: var(--cv-sp-3);
  flex-wrap: wrap;
  margin-top: var(--cv-sp-6);
  padding: var(--cv-sp-4) var(--cv-sp-5);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  box-shadow: var(--cv-shadow-md);
}
.cv-formbar__note { margin-right: auto; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }

/* ─── Repeatable row set ──────────────────────────────────────────────────
   Rows of small fields standing in for a JSON list. Each row is its own grid
   so the columns line up down the set; the widths come from --cv-rowset-cols,
   which the page sets to match its own row shape. */
.cv-rowset {
  display: flex; flex-direction: column; gap: var(--cv-sp-2);
  padding: var(--cv-sp-3);
  background-color: var(--cv-surface-sunk);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-md);
}
.cv-rowset__row {
  display: grid;
  grid-template-columns: var(--cv-rowset-cols, minmax(0, 1fr) minmax(0, 1fr)) 2.25rem;
  align-items: center;
  gap: var(--cv-sp-2);
}
@media (max-width: 640px) {
  /* One field per line below 640px: four inputs and a bin in a 320px row is
     four unusable inputs. The bin keeps its own line, right-aligned. */
  .cv-rowset__row { grid-template-columns: minmax(0, 1fr); }
  .cv-rowset__row > [data-cv-row-remove] { justify-self: end; }
  .cv-rowset__row + .cv-rowset__row { padding-top: var(--cv-sp-3); border-top: 1px solid var(--cv-line); }
}
.cv-rowset__add { align-self: flex-start; }

/* ─── Stepper ─────────────────────────────────────────────────────────────*/
.cv-steps { display: flex; align-items: center; gap: var(--cv-sp-2); flex-wrap: wrap; margin-bottom: var(--cv-sp-6); padding: 0; list-style: none; }
.cv-steps li { display: inline-flex; align-items: center; gap: var(--cv-sp-2); margin: 0; }
.cv-steps li + li::before { content: ""; width: 1.5rem; height: 1px; background-color: var(--cv-line-strong); }
.cv-step {
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium); color: var(--cv-ink-3);
}
.cv-step__num {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5rem; height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--cv-line-strong);
  background-color: var(--cv-surface);
  font-size: var(--cv-text-2xs); font-variant-numeric: tabular-nums;
}
.cv-step.is-active { color: var(--cv-ink); font-weight: var(--cv-weight-semibold); }
.cv-step.is-active .cv-step__num { background-color: var(--cv-accent); border-color: var(--cv-accent); color: var(--cv-ink-on-accent); }
.cv-step.is-done .cv-step__num { background-color: var(--cv-success); border-color: var(--cv-success); color: #fff; }
.cv-step.is-done { color: var(--cv-ink-2); }
/* A step may carry an icon instead of a number (the onboarding band does), so
   the state colours have to reach the icon too. */
.cv-step.is-active > .bi { color: var(--cv-accent-text); }
.cv-step.is-done > .bi { color: var(--cv-success-text); }

/* ============================================================================
   Tables
   ============================================================================
   The admin console is 29 pages of these, so the table carries a lot of weight.
   ========================================================================== */
.cv-tablewrap {
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  overflow: hidden;
}
.cv-tablescroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }

.cv-table, .table {
  width: 100%;
  margin: 0;
  border-collapse: collapse;
  color: var(--cv-ink-2);
  font-size: var(--cv-text-sm);
}
.cv-table thead th, .table thead th {
  position: sticky; top: 0; z-index: 1;
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-surface-sunk);
  border-bottom: 1px solid var(--cv-line);
  color: var(--cv-ink-3);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  text-align: left;
  white-space: nowrap;
}
.cv-table tbody td, .table tbody td {
  padding: var(--cv-sp-3) var(--cv-sp-4);
  border-bottom: 1px solid var(--cv-line-soft);
  vertical-align: middle;
}
.cv-table tbody tr:last-child td, .table tbody tr:last-child td { border-bottom: 0; }
.cv-table tbody tr:hover, .table tbody tr:hover { background-color: var(--cv-surface-inset); }
.cv-table--dense tbody td, .cv-table--dense thead th { padding: var(--cv-sp-2) var(--cv-sp-3); }
.cv-table__num { text-align: right; font-variant-numeric: tabular-nums; }
/* A cell that carries a primary line and a quiet second line under it. */
.cv-table__link { display: block; font-weight: var(--cv-weight-semibold); color: var(--cv-ink); }
.cv-table__link:hover { color: var(--cv-accent-text); }
.cv-table__strong { display: block; font-weight: var(--cv-weight-medium); color: var(--cv-ink); }
.cv-table__sub { display: block; margin-top: 0.1rem; font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }
.cv-table__sub:empty { display: none; }
/* The actions cell. Every state change in this product is a POST, so each
   action is its own <form> — and a form is block-level, so three actions
   stacked into three lines and tripled the row height of the whole table.
   `display: contents` makes the buttons the flex items instead, which is why
   the rule is here rather than a `style="display:inline"` on each form.
   Harmless on a <th>, which is the other place this class is used. */
/* The column carrying the record's name. Without a floor on it the browser
   hands the width to whichever column has the least shrinkable content, and
   the name — the thing you scan the table by — is what collapses: measured at
   90px on the ad list, breaking every title to one word per line while 200px
   sat unused at the right. The table is inside `.cv-tablescroll`, so a floor
   here scrolls rather than pushing the page sideways. */
.cv-table__primary { min-width: 13rem; }
/* A cell whose value reads as one token — a date range, an id, a duration.
   Letting "16 Jun 2026 → 15 Aug 2026" break turned it into three lines and
   made the row taller than the name beside it. */
.cv-table__nowrap { white-space: nowrap; }

.cv-table__actions {
  display: flex; align-items: center; justify-content: flex-end;
  gap: var(--cv-sp-2); white-space: nowrap;
}
.cv-table__actions form { display: contents; }
/* A sortable header carries a hit target and a direction cue. */
.cv-table th a { display: inline-flex; align-items: center; gap: var(--cv-sp-1); color: inherit; }
.cv-table th a:hover { color: var(--cv-ink); }
.cv-table th[aria-sort] a { color: var(--cv-ink); }

/* ============================================================================
   Tabs
   ============================================================================ */
.cv-tabs, .nav-tabs {
  display: flex; align-items: center; gap: var(--cv-sp-1);
  margin: 0 0 var(--cv-sp-5);
  padding: 0;
  list-style: none;
  border-bottom: 1px solid var(--cv-line);
  overflow-x: auto;
}
.cv-tabs li, .nav-tabs .nav-item { margin: 0; }
.cv-tab, .nav-tabs .nav-link {
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--cv-ink-3);
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--cv-t-fast) var(--cv-ease), border-color var(--cv-t-fast) var(--cv-ease);
}
.cv-tab:hover, .nav-tabs .nav-link:hover { color: var(--cv-ink); }
.cv-tab.is-active, .nav-tabs .nav-link.active {
  color: var(--cv-ink);
  font-weight: var(--cv-weight-semibold);
  border-bottom-color: var(--cv-accent);
  background: transparent;
}
.cv-tab__count {
  padding: 0.05rem 0.4rem; border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk); color: var(--cv-ink-3);
  font-size: var(--cv-text-2xs); font-variant-numeric: tabular-nums;
}
.cv-tab.is-active .cv-tab__count { background-color: var(--cv-accent-soft); color: var(--cv-accent-text); }

/* Segmented control — for 2-4 mutually exclusive view modes. */
.cv-segment { display: inline-flex; padding: 3px; background-color: var(--cv-surface-sunk); border-radius: var(--cv-r-pill); }
.cv-segment a, .cv-segment button {
  padding: 0.3125rem 0.875rem;
  border: 0; border-radius: var(--cv-r-pill);
  background: transparent; color: var(--cv-ink-2);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium);
  cursor: pointer;
  transition: background-color var(--cv-t-fast) var(--cv-ease), color var(--cv-t-fast) var(--cv-ease);
}
.cv-segment .is-active { background-color: var(--cv-surface); color: var(--cv-ink); font-weight: var(--cv-weight-semibold); box-shadow: var(--cv-shadow-xs); }

/* Radio-driven segment. A segmented control inside a filter form has to carry
   a real form value, not a link — the Opportunities page needs `type=` to
   travel with the other filters instead of resetting them, which the anchor
   variant above cannot do. The radio stays in the accessibility tree and only
   its box is hidden, so keyboard and screen-reader behaviour is the native
   radiogroup one. */
.cv-segment--form label {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.3125rem 0.875rem;
  border-radius: var(--cv-r-pill);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium);
  cursor: pointer; white-space: nowrap;
  transition: background-color var(--cv-t-fast) var(--cv-ease), color var(--cv-t-fast) var(--cv-ease);
}
.cv-segment--form input {
  position: absolute; inset: 0;
  width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
}
.cv-segment--form label:hover { color: var(--cv-ink); }
.cv-segment--form label:has(input:checked) {
  background-color: var(--cv-surface); color: var(--cv-ink);
  font-weight: var(--cv-weight-semibold); box-shadow: var(--cv-shadow-xs);
}
.cv-segment--form label:has(input:focus-visible) { box-shadow: var(--cv-focus-ring); }
.cv-segment--block { display: flex; width: 100%; }
.cv-segment--block > label, .cv-segment--block > a, .cv-segment--block > button { flex: 1 1 0; }

/* ============================================================================
   Dropdown menus (Bootstrap JS drives these)
   ============================================================================ */
.dropdown-menu {
  min-width: 12rem;
  padding: var(--cv-sp-2);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  box-shadow: var(--cv-shadow-lg);
  font-size: var(--cv-text-sm);
}
.dropdown-item {
  display: flex; align-items: center; gap: var(--cv-sp-3);
  padding: var(--cv-sp-2) var(--cv-sp-3);
  border-radius: var(--cv-r-md);
  color: var(--cv-ink-2);
  font-weight: var(--cv-weight-regular);
}
.dropdown-item:hover, .dropdown-item:focus { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.dropdown-item.active, .dropdown-item:active { background-color: var(--cv-accent-soft); color: var(--cv-ink); font-weight: var(--cv-weight-medium); }
.dropdown-item .bi { width: 1.1rem; text-align: center; color: var(--cv-ink-3); }
.dropdown-divider { margin: var(--cv-sp-2) 0; border-top: 1px solid var(--cv-line-soft); }
.dropdown-header {
  padding: var(--cv-sp-2) var(--cv-sp-3);
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold);
  /* `--cv-ink-4` is the placeholder/disabled step, not a text step: it
     measures 2.39:1 in light and 3.32 in dark. Real text belongs on
     `--cv-ink-3`. Same correction on `.cv-feed__when` and
     `.cv-console__heading`. */
  letter-spacing: 0.06em; text-transform: uppercase; color: var(--cv-ink-3);
}

/* ============================================================================
   Alerts & notices
   ============================================================================ */
.cv-notice, .alert {
  display: flex; align-items: flex-start; gap: var(--cv-sp-3);
  padding: var(--cv-sp-4);
  border: 1px solid var(--cv-line);
  border-left: 3px solid var(--cv-ink-3);
  border-radius: var(--cv-r-md);
  background-color: var(--cv-surface);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-sm);
}
.cv-notice__icon { flex: 0 0 auto; font-size: 1.05rem; line-height: 1.4; color: var(--cv-ink-3); }
.cv-notice__title { display: block; margin-bottom: var(--cv-sp-1); color: var(--cv-ink); font-weight: var(--cv-weight-semibold); }
.cv-notice--accent,  .alert-warning { border-left-color: var(--cv-accent);  background-color: var(--cv-accent-soft); }
.cv-notice--success, .alert-success { border-left-color: var(--cv-success); background-color: var(--cv-success-soft); }
.cv-notice--danger,  .alert-danger  { border-left-color: var(--cv-danger);  background-color: var(--cv-danger-soft); }
.cv-notice--info,    .alert-info, .alert-primary { border-left-color: var(--cv-info); background-color: var(--cv-info-soft); }
.cv-notice--accent .cv-notice__icon  { color: var(--cv-accent-text); }
.cv-notice--success .cv-notice__icon { color: var(--cv-success-text); }
.cv-notice--danger .cv-notice__icon  { color: var(--cv-danger-text); }
.cv-notice--info .cv-notice__icon    { color: var(--cv-info-text); }

/* ============================================================================
   Empty state
   ============================================================================ */
.cv-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--cv-sp-3);
  padding: var(--cv-sp-16) var(--cv-sp-6);
  text-align: center;
  background-color: var(--cv-surface);
  border: 1px dashed var(--cv-line-strong);
  border-radius: var(--cv-r-lg);
}
.cv-empty__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 3.5rem; height: 3.5rem;
  border-radius: 50%;
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-3);
  font-size: 1.5rem;
}
.cv-empty__title { margin: 0; font-size: var(--cv-text-h4); color: var(--cv-ink); }
.cv-empty__text { margin: 0; max-width: 42ch; font-size: var(--cv-text-sm); color: var(--cv-ink-3); }

/* ============================================================================
   Pagination
   ============================================================================ */
.cv-pagination, .pagination {
  display: flex; align-items: center; justify-content: center; gap: var(--cv-sp-1);
  flex-wrap: wrap;
  margin: var(--cv-sp-8) 0 0;
  padding: 0;
  list-style: none;
}
.cv-pagination li, .pagination li { margin: 0; }
.cv-pagination a, .cv-pagination span, .pagination .page-link {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 2.25rem; height: 2.25rem;
  padding: 0 var(--cv-sp-2);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-md);
  background-color: var(--cv-surface);
  color: var(--cv-ink-2);
  font-size: var(--cv-text-xs);
  font-variant-numeric: tabular-nums;
  transition: background-color var(--cv-t-fast) var(--cv-ease), border-color var(--cv-t-fast) var(--cv-ease);
}
.cv-pagination a:hover, .pagination .page-link:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
/* The current page. This paired an INVERTING role token with a FIXED literal:
   `--cv-ink` is near-black in light and near-white in dark, while `--cv-n-0`
   is white in both -- so in dark mode the active page number was white on
   white, 1.09:1, completely invisible. The action pair inverts together
   (charcoal + white in light, amber + near-black in dark, since dark promotes
   amber to the action role), giving 17.4:1 and 7.91:1. */
.cv-pagination .is-active, .pagination .active .page-link {
  background-color: var(--cv-action); border-color: var(--cv-action);
  color: var(--cv-action-ink); font-weight: var(--cv-weight-semibold);
}
.cv-pagination .is-disabled, .pagination .disabled .page-link { opacity: 0.45; pointer-events: none; }

/* ============================================================================
   Modals & offcanvas (Bootstrap JS)
   ============================================================================ */
.modal-content {
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-xl);
  box-shadow: var(--cv-shadow-xl);
}
.modal-header { padding: var(--cv-sp-5); border-bottom: 1px solid var(--cv-line-soft); }
.modal-title { font-size: var(--cv-text-h3); margin: 0; }
.modal-body { padding: var(--cv-sp-5); }
.modal-footer { padding: var(--cv-sp-4) var(--cv-sp-5); border-top: 1px solid var(--cv-line-soft); gap: var(--cv-sp-2); }
.modal-backdrop.show { background-color: var(--cv-overlay); opacity: 1; }
.offcanvas { background-color: var(--cv-surface); border-color: var(--cv-line); }
.btn-close { opacity: 0.5; }
.btn-close:hover { opacity: 1; }

/* ============================================================================
   Progress
   ============================================================================ */
.cv-progress, .progress {
  height: 0.5rem;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk);
  overflow: hidden;
}
.cv-progress__bar, .progress-bar {
  /* display:block matters: a <span> bar is inline by default, and width has
     no effect on an inline box — the bar renders empty at every percentage. */
  display: block;
  height: 100%;
  background-color: var(--cv-accent);
  border-radius: var(--cv-r-pill);
  transition: width var(--cv-t-slow) var(--cv-ease);
}
/* Near the end of an allowance. Red here means "you are about to run out",
   which is the one thing red is for in this system. */
.cv-progress__bar--urgent { background-color: var(--cv-urgent); }
.cv-progress__bar--done { background-color: var(--cv-success); }
.cv-progress--lg { height: 0.75rem; }

/* ─── Long-form legal document ────────────────────────────────────────────
   The privacy policy and the terms. These were the last two templates in the
   repo carrying an inline <style> block -- 65 and 55 lines -- and between them
   40 `var(--ch-*)` references that no loaded stylesheet defines, so the intro
   panel, every section rule, all three callout types and the footer were
   drawing with those declarations dropped: a warning box with no warning
   colour, a section heading with no rule beneath it.

   Written as element selectors under one scope rather than a class per
   paragraph, because the content is prose an editor will extend and no-one
   maintaining a policy should have to remember a class name to add a
   paragraph to it. */
.cv-legal { color: var(--cv-ink-2); }
.cv-legal__brand { text-align: center; margin-bottom: var(--cv-sp-6); }
.cv-legal__brand h2 {
  margin: 0 0 var(--cv-sp-1);
  font-size: var(--cv-text-h3); font-weight: var(--cv-weight-bold); color: var(--cv-ink);
}
.cv-legal__brand p { margin: 0; font-size: var(--cv-text-sm); color: var(--cv-ink-3); }

.cv-legal__lead {
  padding: var(--cv-sp-4) var(--cv-sp-5);
  margin-bottom: var(--cv-sp-7);
  background-color: var(--cv-accent-soft);
  border-left: 3px solid var(--cv-accent);
  border-radius: var(--cv-r-md);
}
.cv-legal__lead p { margin: 0 0 var(--cv-sp-2); }
.cv-legal__lead p:last-child { margin-bottom: 0; }
.cv-legal__date { font-weight: var(--cv-weight-semibold); color: var(--cv-accent-text); }

.cv-legal__section { margin-bottom: var(--cv-sp-7); }
.cv-legal__section h2 {
  margin: 0 0 var(--cv-sp-4);
  padding-bottom: var(--cv-sp-2);
  border-bottom: 1px solid var(--cv-line);
  font-size: var(--cv-text-h3); font-weight: var(--cv-weight-semibold); color: var(--cv-ink);
}
.cv-legal__section h3 {
  margin: var(--cv-sp-5) 0 var(--cv-sp-2);
  font-size: var(--cv-text-lg); font-weight: var(--cv-weight-semibold); color: var(--cv-ink);
}
.cv-legal__section h4 {
  margin: 0 0 var(--cv-sp-2);
  font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); color: var(--cv-ink);
}
.cv-legal__section p { margin: 0 0 var(--cv-sp-3); }
.cv-legal__section ul, .cv-legal__section ol { margin: 0 0 var(--cv-sp-4); padding-left: 1.35rem; }
.cv-legal__section li { margin-bottom: var(--cv-sp-2); }
.cv-legal__section li:last-child { margin-bottom: 0; }
.cv-legal strong { color: var(--cv-ink); font-weight: var(--cv-weight-semibold); }

/* A set-aside block inside a section: a category of data, something to note,
   something to be careful about. The left rule carries the meaning, which is
   why the neutral case still has one. */
.cv-legal__block {
  padding: var(--cv-sp-4) var(--cv-sp-5);
  margin: var(--cv-sp-4) 0;
  background-color: var(--cv-surface-sunk);
  border-left: 3px solid var(--cv-line-strong);
  border-radius: var(--cv-r-md);
}
.cv-legal__block > :last-child { margin-bottom: 0; }
.cv-legal__block--info {
  background-color: var(--cv-info-soft); border-left-color: var(--cv-info);
}
.cv-legal__block--warn {
  background-color: var(--cv-danger-soft); border-left-color: var(--cv-danger);
}
.cv-legal__block--warn > strong:first-child {
  display: block; margin-bottom: var(--cv-sp-2); color: var(--cv-danger-text);
}

.cv-legal__foot {
  margin-top: var(--cv-sp-8);
  padding-top: var(--cv-sp-5);
  border-top: 1px solid var(--cv-line);
  text-align: center;
  font-size: var(--cv-text-xs); color: var(--cv-ink-3);
}

/* ─── Chart panel ─────────────────────────────────────────────────────────
   A canvas needs a sized box: Chart.js with `maintainAspectRatio: false`
   measures its parent, and a parent with no height collapses to nothing.
   The height is on the wrapper rather than the <canvas> so the library can
   own the element's own width and height attributes, which is how it keeps a
   chart crisp on a high-DPI screen. */
.cv-chart { position: relative; height: 18rem; }
.cv-chart--short { height: 13rem; }
.cv-chart--tall { height: 22rem; }
.cv-chartgrid {
  display: grid; gap: var(--cv-sp-5);
  grid-template-columns: 1fr;
  margin-bottom: var(--cv-sp-5);
}
@media (min-width: 1024px) {
  .cv-chartgrid--2 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
  .cv-chartgrid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ============================================================================
   Media & misc
   ============================================================================ */
/* `display` is load-bearing here for the same reason it is on `.cv-presence`:
   neither `aspect-ratio` nor `width` applies to an inline box, so this drew at
   nothing the moment it was put on a <span> rather than a <div>. Stated once,
   so the element it is written on stops mattering. */
.cv-media {
  display: block; border-radius: var(--cv-r-lg);
  overflow: hidden; background-color: var(--cv-surface-sunk);
  max-width: 100%;
}
.cv-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A thumbnail cell whose record has no image. Centring the placeholder icon is
   what stops the row height jumping between the two cases. */
.cv-media__fallback {
  display: grid; place-items: center; width: 100%; height: 100%;
  color: var(--cv-ink-4); font-size: 1.1rem;
}
.cv-media--16x9 { aspect-ratio: 16 / 9; }
.cv-media--4x3  { aspect-ratio: 4 / 3; }
.cv-media--1x1  { aspect-ratio: 1 / 1; }

/* Definition rows — "Owner: … / Location: …" summary blocks on detail pages. */
.cv-deflist { display: grid; gap: var(--cv-sp-3) var(--cv-sp-5); grid-template-columns: 1fr; margin: 0; }
@media (min-width: 640px) { .cv-deflist--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.cv-deflist__k { display: block; font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold); letter-spacing: 0.05em; text-transform: uppercase; color: var(--cv-ink-3); margin-bottom: 0.15rem; }
.cv-deflist__v { display: block; font-size: var(--cv-text-sm); color: var(--cv-ink); }
/* As many columns as fit, for a list whose length depends on which optional
   fields the record happens to carry. */
.cv-deflist--cols { grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr)); }

/* ─── Checklist ───────────────────────────────────────────────────────────
   A set of related checkboxes — permissions, notification toggles. Two columns
   where there is room, one below 30rem, because a permission label wrapping
   onto a second line beside its box reads as two options. */
.cv-checklist {
  display: grid;
  gap: var(--cv-sp-2) var(--cv-sp-4);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
.cv-checklist .form-check { display: flex; align-items: center; gap: var(--cv-sp-2); margin: 0; padding: 0; }
.cv-checklist .form-check-label { margin: 0; font-size: var(--cv-text-sm); color: var(--cv-ink-2); }

/* ─── File list ───────────────────────────────────────────────────────────
   Attached files as rows: icon, name over its metadata, and one action. Rows
   are separated by a rule rather than boxed, because a panel of eight boxed
   cards reads as eight things to decide about rather than one list. */
.cv-filelist { list-style: none; margin: 0; padding: 0; }
.cv-filelist__item {
  display: flex; align-items: center; gap: var(--cv-sp-3);
  margin: 0;
  padding: var(--cv-sp-3) var(--cv-sp-5);
  border-top: 1px solid var(--cv-line-soft);
}
.cv-filelist__icon {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem;
  border-radius: var(--cv-r-sm);
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-3);
}
.cv-filelist__text { min-width: 0; flex: 1 1 auto; }
.cv-filelist__name {
  display: block;
  font-size: var(--cv-text-sm); font-weight: var(--cv-weight-medium);
  color: var(--cv-ink);
  /* A file called sanctioned-plan-revision-4-final.pdf must not push the
     delete button off the panel. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.cv-filelist__name:hover { color: var(--cv-accent-text); }
.cv-filelist__meta { display: block; font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }
.cv-filelist__item form { margin: 0; flex: 0 0 auto; }

/* ─── Photo grid ──────────────────────────────────────────────────────────
   Site photographs. A fixed row height keeps a page of mixed portrait and
   landscape phone shots from turning into a ragged wall. */
.cv-photogrid { display: grid; gap: var(--cv-sp-3); grid-template-columns: repeat(auto-fill, minmax(min(100%, 11rem), 1fr)); }
.cv-photogrid__item { margin: 0; }
.cv-photogrid__item img {
  display: block; width: 100%; height: 9rem;
  object-fit: cover;
  border-radius: var(--cv-r-md);
  border: 1px solid var(--cv-line);
  background-color: var(--cv-surface-sunk);
}
.cv-photogrid__item a:focus-visible img { box-shadow: var(--cv-focus-ring); }
.cv-photogrid__item figcaption { margin-top: var(--cv-sp-1); font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }

/* ─── Portfolio grid ──────────────────────────────────────────────────────
   The provider's work gallery in edit mode. Wider tracks than .cv-photogrid
   because each tile carries a title, a description and its own actions rather
   than being a thumbnail; the image keeps a fixed ratio so a row of mixed
   phone shots still lines up. */
.cv-portfoliogrid {
  display: grid; gap: var(--cv-sp-4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
}
.cv-portfoliogrid__item { margin: 0; overflow: hidden; display: flex; flex-direction: column; }
.cv-portfoliogrid__item img {
  display: block; width: 100%; aspect-ratio: 4 / 3;
  object-fit: cover;
  background-color: var(--cv-surface-sunk);
}
.cv-portfoliogrid__item > .cv-card__body {
  display: flex; flex-direction: column; gap: var(--cv-sp-2);
  flex: 1 1 auto;
}
.cv-portfoliogrid__actions { margin-top: auto; padding-top: var(--cv-sp-2); }
.cv-portfoliogrid__actions form { margin: 0; }

/* ─── Task card ───────────────────────────────────────────────────────────
   A card on the project schedule. Full height so a row of them lines up, with
   the actions pinned to the bottom regardless of how much a task describes. */
.cv-task { height: 100%; }
.cv-task > .cv-card__body { height: 100%; }
.cv-task__files { padding-top: var(--cv-sp-3); border-top: 1px solid var(--cv-line-soft); }

/* ─── Quick actions ───────────────────────────────────────────────────────
   The dashboard's launcher. Twelve destinations need to be scannable, so each
   is a tile with one icon and one label and nothing else; the primary action
   for the role takes the amber accent and everything else stays quiet. */
.cv-actiongrid { display: grid; gap: var(--cv-sp-3); grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 640px)  { .cv-actiongrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .cv-actiongrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
.cv-action {
  display: flex; align-items: center; gap: var(--cv-sp-3);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-md);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium); color: var(--cv-ink);
  transition: border-color var(--cv-t-fast) var(--cv-ease), box-shadow var(--cv-t-fast) var(--cv-ease);
}
.cv-action:hover { border-color: var(--cv-line-strong); box-shadow: var(--cv-shadow-sm); color: var(--cv-ink); }
.cv-action .bi {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 1.75rem; height: 1.75rem;
  border-radius: var(--cv-r-sm);
  background-color: var(--cv-surface-sunk); color: var(--cv-ink-2);
  font-size: 0.875rem;
}
/* A queue depth on the tile that leads to the queue. Red, because "14 people
   are waiting on you" is the one thing on the console that is time-critical —
   and it sits at the end of the row rather than floating in a corner, which
   is how the old dashboard did it with four hardcoded declarations and an
   absolute position. */
.cv-action__count {
  flex: 0 0 auto; margin-left: auto;
  padding: 0.0625rem 0.4rem; border-radius: var(--cv-r-pill);
  background-color: var(--cv-urgent); color: var(--cv-ink-on-dark);
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-bold);
  font-variant-numeric: tabular-nums;
}

/* The one action the role is here to do. Dark ink on amber, never white. */
.cv-action--primary { background-color: var(--cv-accent-soft); border-color: var(--cv-accent); }
.cv-action--primary .bi { background-color: var(--cv-accent); color: var(--cv-n-900); }

/* ─── Onboarding band ─────────────────────────────────────────────────────
   A notice that also carries a step index and an action, so it needs the
   three-column shape the plain notice does not have. */
.cv-onboard { align-items: center; gap: var(--cv-sp-4); }
.cv-onboard__body { flex: 1 1 auto; min-width: 0; }
.cv-onboard__detail { margin: var(--cv-sp-1) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-2); }
.cv-onboard__action { flex: 0 0 auto; }
@media (max-width: 640px) {
  .cv-onboard { flex-wrap: wrap; }
  .cv-onboard__action { width: 100%; justify-content: center; }
}

/* Account-state bands wrapper (ui/_status_bands.html). Each band renders only
   when it has something to prompt, so the vertical rhythm lives on the bands
   themselves — an empty wrapper then takes no space, and there's never a double
   gap. Gives the cards room above, below, and between so they stop touching. */
.cv-bands > .cv-notice { margin-top: var(--cv-sp-5); }
.cv-bands > .cv-notice:last-child { margin-bottom: var(--cv-sp-5); }

/* ─── Rates strip ─────────────────────────────────────────────────────────
   Scrolls horizontally when it does not fit rather than animating: the old
   version marqueed forever, which makes the one number you want unreadable. */
.cv-rates {
  display: flex; align-items: center; gap: var(--cv-sp-4);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
}
.cv-rates__label {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold);
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--cv-ink-3);
}
.cv-rates__dot { width: 6px; height: 6px; border-radius: 50%; background-color: var(--cv-success); }
.cv-rates__list {
  display: flex; gap: var(--cv-sp-5);
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
  margin: 0; padding: 0; list-style: none;
}
.cv-rates__list::-webkit-scrollbar { display: none; }
.cv-rates__list > li { display: inline-flex; align-items: baseline; gap: var(--cv-sp-2); white-space: nowrap; }
.cv-rates__name { font-size: var(--cv-text-xs); color: var(--cv-ink-2); }
.cv-rates__value { font-size: var(--cv-text-xs); font-weight: var(--cv-weight-semibold); color: var(--cv-ink); }
.cv-rates__value small { font-weight: var(--cv-weight-regular); color: var(--cv-ink-3); }
.cv-rates__all { flex: 0 0 auto; font-size: var(--cv-text-xs); color: var(--cv-accent-text); }
@media (max-width: 640px) { .cv-rates__label, .cv-rates__all { display: none; } }

/* A KPI that is also a link to the list it counts. */
a.cv-kpi { transition: border-color var(--cv-t-fast) var(--cv-ease), box-shadow var(--cv-t-fast) var(--cv-ease); }
a.cv-kpi:hover { border-color: var(--cv-line-strong); box-shadow: var(--cv-shadow-sm); }

/* ─── Activity feed ──────────────────────────────────────────────────────*/
.cv-feed { display: flex; flex-direction: column; padding: 0; margin: 0; list-style: none; }
.cv-feed > li { display: flex; gap: var(--cv-sp-3); padding-block: var(--cv-sp-3); }
.cv-feed > li + li { border-top: 1px solid var(--cv-line-soft); }
.cv-feed__icon {
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 1.75rem; height: 1.75rem;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk); color: var(--cv-ink-2);
  font-size: 0.8125rem;
}
.cv-feed__body { min-width: 0; flex: 1 1 auto; }
.cv-feed__title { display: block; font-size: var(--cv-text-xs); font-weight: var(--cv-weight-semibold); color: var(--cv-ink); }
.cv-feed__text { margin: 2px 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }
.cv-feed__when { margin: 2px 0 0; font-size: var(--cv-text-2xs); color: var(--cv-ink-3); font-variant-numeric: tabular-nums; }
/* Unread carries the accent as a marker, not as decoration. */
.cv-feed > li.is-unread .cv-feed__icon { background-color: var(--cv-accent-soft); color: var(--cv-accent-text); }

/* ─── Section index ───────────────────────────────────────────────────────
   The wayfinding for a long form. A real multi-step wizard would split the
   requirement form's single POST across several requests and several places
   for validation to go wrong; this gives the same sense of where you are and
   how much is left, while the form stays one submission. */
.cv-stepnav { display: flex; flex-direction: column; gap: 2px; padding: 0; margin: 0; list-style: none; counter-reset: cv-step; }
.cv-stepnav a {
  display: flex; align-items: center; gap: var(--cv-sp-3);
  padding: var(--cv-sp-2) var(--cv-sp-3);
  border-radius: var(--cv-r-md);
  font-size: var(--cv-text-xs); color: var(--cv-ink-2);
}
.cv-stepnav a::before {
  counter-increment: cv-step;
  content: counter(cv-step);
  flex: 0 0 auto;
  display: grid; place-items: center;
  width: 1.375rem; height: 1.375rem;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-surface-sunk);
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold);
  font-variant-numeric: tabular-nums;
  color: var(--cv-ink-3);
}
.cv-stepnav a:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.cv-stepnav a.is-active { background-color: var(--cv-accent-soft); color: var(--cv-ink); font-weight: var(--cv-weight-medium); }
.cv-stepnav a.is-active::before { background-color: var(--cv-accent); color: var(--cv-n-900); }

/* ─── Chat panel ──────────────────────────────────────────────────────────
   A conversation thread inside a side panel (requirement detail) and, later,
   as a full page. Both use these classes so there is one chat appearance in
   the app; the inline <script> that appends bubbles builds the same markup.

   Own messages take the charcoal action colour rather than the amber accent:
   amber is the signal colour, and a thread where every second bubble shouts
   is unreadable. */
.cv-chat { display: flex; flex-direction: column; min-height: 0; }
.cv-chat__head {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-4) var(--cv-sp-5);
  border-bottom: 1px solid var(--cv-line-soft);
  font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold);
}
.cv-chat__head .bi { color: var(--cv-ink-3); }
.cv-chat__body {
  display: flex; flex-direction: column; gap: var(--cv-sp-2);
  height: 17rem; overflow-y: auto; overscroll-behavior: contain;
  padding: var(--cv-sp-4) var(--cv-sp-5);
  background-color: var(--cv-surface-sunk);
}
.cv-chat__msg {
  max-width: 85%;
  padding: var(--cv-sp-2) var(--cv-sp-3);
  border-radius: var(--cv-r-lg);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  font-size: var(--cv-text-xs); line-height: var(--cv-leading);
  overflow-wrap: anywhere;
}
.cv-chat__msg--sent {
  align-self: flex-end;
  background-color: var(--cv-action); border-color: var(--cv-action);
  color: var(--cv-action-ink);
}
.cv-chat__msg--received { align-self: flex-start; }
.cv-chat__time { display: block; margin-top: 2px; font-size: var(--cv-text-2xs); opacity: 0.7; font-variant-numeric: tabular-nums; }
.cv-chat__empty {
  margin: auto; text-align: center;
  font-size: var(--cv-text-xs); color: var(--cv-ink-3);
}
.cv-chat__empty .bi { display: block; margin-bottom: var(--cv-sp-2); font-size: 1.5rem; color: var(--cv-ink-4); }
.cv-chat__form {
  display: flex; gap: var(--cv-sp-2);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  border-top: 1px solid var(--cv-line-soft);
}
.cv-chat__form input { flex: 1 1 auto; min-width: 0; }

/* The full-page conversation. Same block, a `--page` modifier: one bubble
   design, one composer, two densities — rather than a second chat vocabulary
   for the page that had its own 265 lines of inline CSS. */
.cv-chat--page {
  height: clamp(28rem, calc(100dvh - 16rem), 46rem);
  overflow: hidden;
}
.cv-chat--page .cv-chat__body { height: auto; flex: 1 1 auto; gap: var(--cv-sp-3); }
.cv-chat--page .cv-chat__msg { max-width: min(38rem, 78%); font-size: var(--cv-text-sm); }

.cv-chat__head--page {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--cv-sp-3); align-items: start;
}
.cv-chat__who { min-width: 0; }
.cv-chat__title {
  margin: 0; font-size: var(--cv-text-md);
  font-weight: var(--cv-weight-semibold); color: var(--cv-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cv-chat__sub {
  margin: var(--cv-sp-1) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-3);
  font-weight: var(--cv-weight-regular);
  display: flex; align-items: center; gap: var(--cv-sp-2); flex-wrap: wrap;
}
.cv-chat__tools { display: flex; align-items: center; gap: var(--cv-sp-1); }

/* Presence. Amber for idle rather than a third hue, and a ring so the dot is
   distinguishable without relying on colour alone. */
.cv-presence {
  /* `display` is load-bearing: a bare <span> is inline, and width/height do
     not apply to an inline box — the dot drew at zero size with a background
     nobody could see. Same defect as the progress bars in Phase B, and a
     computed-style check for the colour passes right through it, so the suite
     asserts the rendered SIZE as well. */
  display: inline-block; vertical-align: middle;
  width: 0.5rem; height: 0.5rem; border-radius: 50%; flex: 0 0 auto;
  background-color: var(--cv-ink-4); box-shadow: 0 0 0 2px var(--cv-surface);
}
/* `_chat_row.html` renders `cv-presence--{{ room.other_presence|default:'offline' }}`,
   so `--offline` is a real state this component is asked for and had no rule
   for. The base dot is already the offline grey, so the declaration is the
   same — but a class that resolves to nothing is a class nobody can tell is
   working, and the sweep now treats one as a defect. */
.cv-presence--offline { background-color: var(--cv-ink-4); }
.cv-presence--active { background-color: var(--cv-success); }
.cv-presence--idle   { background-color: var(--cv-accent); }

.cv-chat__sender {
  display: block; margin-bottom: 2px;
  font-size: var(--cv-text-2xs); font-weight: var(--cv-weight-semibold);
  color: var(--cv-ink-3);
}
.cv-chat__tick { margin-left: var(--cv-sp-2); letter-spacing: -0.08em; }
.cv-chat__tick.is-read { color: var(--cv-info-text); }
.cv-chat__msg--sent .cv-chat__tick.is-read { color: #fff; opacity: 1; }

.cv-chat__attach { display: block; margin-bottom: var(--cv-sp-2); }
.cv-chat__attach img, .cv-chat__attach video {
  display: block; max-width: 100%; max-height: 14rem;
  border-radius: var(--cv-r-md);
}
.cv-chat__file {
  display: inline-flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-2) var(--cv-sp-3);
  border-radius: var(--cv-r-md); background-color: var(--cv-surface-sunk);
  color: inherit; text-decoration: none; font-size: var(--cv-text-xs);
  overflow-wrap: anywhere;
}
.cv-chat__msg--sent .cv-chat__file { background-color: rgb(255 255 255 / .16); }

.cv-chat__banner {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-3) var(--cv-sp-5);
  font-size: var(--cv-text-xs);
  border-bottom: 1px solid var(--cv-line-soft);
}
.cv-chat__banner--blocked { background-color: var(--cv-danger-soft); color: var(--cv-danger-text); }
.cv-chat__banner--warn    { background-color: var(--cv-accent-soft); color: var(--cv-accent-text); }

.cv-chat__composer { border-top: 1px solid var(--cv-line-soft); }
.cv-chat__composer .cv-chat__form { border-top: 0; }
.cv-chat__pending {
  display: flex; align-items: center; gap: var(--cv-sp-2);
  padding: var(--cv-sp-2) var(--cv-sp-4) 0;
  font-size: var(--cv-text-xs); color: var(--cv-ink-2);
}
.cv-chat__hint {
  margin: 0; padding: 0 var(--cv-sp-4) var(--cv-sp-3);
  font-size: var(--cv-text-2xs); color: var(--cv-ink-3);
}
.cv-chat__emoji {
  display: flex; flex-wrap: wrap; gap: var(--cv-sp-1);
  max-height: 9rem; overflow-y: auto;
  padding: var(--cv-sp-3) var(--cv-sp-4);
  border-top: 1px solid var(--cv-line-soft);
}
.cv-chat__emoji button {
  border: 0; background: none; cursor: pointer; font-size: 1.35rem;
  line-height: 1; padding: var(--cv-sp-1); border-radius: var(--cv-r-sm);
}
.cv-chat__emoji button:hover { background-color: var(--cv-surface-sunk); }
.cv-chat__typing { font-style: italic; color: var(--cv-accent-text); }

@media (max-width: 47.99rem) {
  .cv-chat--page { height: clamp(24rem, calc(100dvh - 11rem), 40rem); }
  .cv-chat--page .cv-chat__msg { max-width: 88%; }
}

/* A native <dialog>. The browser gives us the modal semantics for free — focus
   trapping, Escape to close, inert background — which is the whole reason the
   report flow stopped being three chained prompt() calls. */
.cv-dialog {
  border: 0; padding: 0; max-width: min(30rem, calc(100vw - 2rem));
  width: 100%; border-radius: var(--cv-r-lg);
  background-color: var(--cv-surface); color: var(--cv-ink);
  box-shadow: var(--cv-shadow-lg);
}
.cv-dialog::backdrop { background-color: rgb(0 0 0 / .45); }
.cv-dialog > form { padding: var(--cv-sp-6); }

/* ─── Log viewer ──────────────────────────────────────────────────────────
   The application log. The previous version drew a fake macOS window —
   three coloured dots and a title bar — from `.log-terminal`, `.dot-red`
   and friends, none of which any loaded sheet defined, so it rendered as a
   column of undifferentiated text. What a log actually needs is a monospace
   grid with the level legible at a glance. */
.cv-logview {
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  background-color: var(--cv-surface);
  overflow: hidden;
}
.cv-logview__head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--cv-sp-3);
  padding: var(--cv-sp-3) var(--cv-sp-4);
  border-bottom: 1px solid var(--cv-line);
  background-color: var(--cv-surface-sunk);
  font-size: var(--cv-text-xs); color: var(--cv-ink-2);
  font-family: var(--cv-font-mono, ui-monospace, monospace);
}
.cv-logview__lines { max-height: 40rem; overflow: auto; }
.cv-logline {
  display: grid; gap: var(--cv-sp-3);
  /* FOUR columns, for the four children: level, timestamp, module, message.
     Written with three, the message fell into an implicit fourth track sized
     by its content while the module name took the 1fr — so every log line
     wrapped its message into a four-character strip beside half a screen of
     nothing. Caught in a screenshot, not by the class check. */
  grid-template-columns: 5.5rem 11.5rem 9rem minmax(0, 1fr);
  padding: var(--cv-sp-2) var(--cv-sp-4);
  border-bottom: 1px solid var(--cv-line-soft);
  font-family: var(--cv-font-mono, ui-monospace, monospace);
  font-size: var(--cv-text-2xs); line-height: 1.6;
}
.cv-logline:last-child { border-bottom: 0; }
.cv-logline__ts { color: var(--cv-ink-3); white-space: nowrap; }
.cv-logline__msg { overflow-wrap: anywhere; color: var(--cv-ink); }
.cv-logline__mod { color: var(--cv-ink-3); overflow: hidden; text-overflow: ellipsis; }
.cv-loglevel {
  align-self: start; justify-self: start;
  padding: 0 var(--cv-sp-2); border-radius: var(--cv-r-sm);
  font-weight: var(--cv-weight-semibold); letter-spacing: 0.02em;
  background-color: var(--cv-surface-sunk); color: var(--cv-ink-2);
}
.cv-loglevel--INFO     { background-color: var(--cv-info-soft);    color: var(--cv-info-text); }
.cv-loglevel--DEBUG    { background-color: var(--cv-surface-sunk); color: var(--cv-ink-3); }
.cv-loglevel--WARNING  { background-color: var(--cv-accent-soft);  color: var(--cv-accent-text); }
.cv-loglevel--ERROR,
.cv-loglevel--CRITICAL { background-color: var(--cv-danger-soft);  color: var(--cv-danger-text); }

@media (max-width: 71.99rem) {
  /* Below a wide desktop the four columns stop fitting: the message gets the
     full width and the metadata sits above it. */
  .cv-logline { grid-template-columns: 5.5rem minmax(0, 1fr); gap: var(--cv-sp-1) var(--cv-sp-3); }
  .cv-logline__ts { grid-column: 2; }
  .cv-logline__mod { grid-column: 2; }
  .cv-logline__msg { grid-column: 2; }
}
@media (max-width: 35.99rem) {
  .cv-logline { grid-template-columns: minmax(0, 1fr); }
  .cv-logline__ts, .cv-logline__mod, .cv-logline__msg { grid-column: 1; }
}

/* ─── Map picker ──────────────────────────────────────────────────────────
   The Leaflet location picker (templates/civilhive/_map_picker.html). Its
   styles used to be an inline <style> in the include, which meant they were
   re-emitted on every page that showed a map and could not reference the
   design tokens. */
.cv-map { display: block; }
.cv-map__toolbar { display: flex; gap: var(--cv-sp-2); margin-bottom: var(--cv-sp-3); flex-wrap: wrap; }
.cv-map__search { flex: 1 1 14rem; min-width: 0; }
.cv-map__canvas {
  height: 20rem; width: 100%;
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  background-color: var(--cv-surface-sunk);
  /* Leaflet's own panes carry z-index up to 700; keeping the map at 0 stops a
     tile layer painting over the sticky form bar below it. */
  z-index: 0;
}
.cv-map__coords { margin: var(--cv-sp-2) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink); }
.cv-map__coords .cv-num { font-weight: var(--cv-weight-medium); }
.cv-map__hint { margin: var(--cv-sp-1) 0 0; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }
/* Leaflet's own chrome, brought into the palette. */
.leaflet-container { font: inherit; border-radius: var(--cv-r-lg); }
.leaflet-bar a, .leaflet-control-attribution {
  color: var(--cv-ink-2); background-color: var(--cv-surface);
}
.leaflet-bar a:hover { background-color: var(--cv-surface-sunk); color: var(--cv-ink); }
.leaflet-control-attribution { font-size: var(--cv-text-2xs); }

/* Skeleton shimmer for async panels. */
.cv-skeleton {
  border-radius: var(--cv-r-md);
  background: linear-gradient(90deg, var(--cv-surface-sunk) 25%, var(--cv-line-soft) 37%, var(--cv-surface-sunk) 63%);
  background-size: 400% 100%;
  animation: cv-shimmer 1.4s ease-in-out infinite;
}
@keyframes cv-shimmer { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }

/* Toasts (SweetAlert2 is themed where it is used). */
.cv-toast {
  padding: var(--cv-sp-3) var(--cv-sp-4);
  background-color: var(--cv-n-900);
  color: var(--cv-n-0);
  border-radius: var(--cv-r-md);
  box-shadow: var(--cv-shadow-lg);
  font-size: var(--cv-text-sm);
}

/* ============================================================================
   Sponsored slot
   ----------------------------------------------------------------------------
   Rendered by the `ad_slot` inclusion tag. It styled itself with `--a-*`
   variables that live only in argon.css, which the new shells do not load, so
   every ad in the app was drawing with those declarations dropped — a
   borderless white block with default type. Same root cause as the `--ch-*`
   properties on the property pages.

   An ad must be visibly an ad: the label is not optional decoration, and the
   slot is deliberately quieter than a real result card so it cannot be
   mistaken for one.
   ============================================================================ */
.cv-ads { display: flex; flex-direction: column; gap: var(--cv-sp-3); margin-block: var(--cv-sp-4); }
.cv-ad {
  overflow: hidden;
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
}
.cv-ad__link { display: block; color: var(--cv-ink); }
.cv-ad__link:hover { color: var(--cv-ink); }
.cv-ad__media { display: block; width: 100%; max-height: 220px; object-fit: cover; }
.cv-ad__video { position: relative; width: 100%; padding-top: 56.25%; background-color: var(--cv-n-900); }
.cv-ad__video iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.cv-ad__body { padding: var(--cv-sp-4); }
.cv-ad__label {
  display: inline-block;
  padding: 0.1rem var(--cv-sp-2);
  border-radius: var(--cv-r-sm);
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-3);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-bold);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.cv-ad__title { margin: var(--cv-sp-2) 0 0; font-size: var(--cv-text-md); font-weight: var(--cv-weight-semibold); }
.cv-ad__desc { margin: var(--cv-sp-1) 0 0; font-size: var(--cv-text-sm); color: var(--cv-ink-3); }

/* ============================================================================
   Dropzone
   ----------------------------------------------------------------------------
   A click-or-drag image target with a live preview, used by the provider
   profile's business photo. It replaces a `.bp-*` block whose every colour was
   a `var(--ch-x, var(--a-x))` pair — both namespaces are gone from the new
   shells, so the border, background, hover and drag ring all resolved to
   nothing and the control rendered as an invisible box.
   ============================================================================ */
.cv-dropzone {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 4 / 3;
  max-width: 100%;
  overflow: hidden;
  padding: var(--cv-sp-4);
  text-align: center;
  background-color: var(--cv-surface-sunk);
  border: 1px dashed var(--cv-line-strong);
  border-radius: var(--cv-r-lg);
  color: var(--cv-ink-3);
  cursor: pointer;
  transition: border-color var(--cv-t-fast) var(--cv-ease),
              background-color var(--cv-t-fast) var(--cv-ease);
}
.cv-dropzone:hover { border-color: var(--cv-ink-3); }
.cv-dropzone:focus-visible { outline: none; box-shadow: var(--cv-ring); }
.cv-dropzone.is-over { border-color: var(--cv-accent); background-color: var(--cv-accent-soft); }
.cv-dropzone__preview { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.cv-dropzone__empty { display: flex; flex-direction: column; align-items: center; gap: var(--cv-sp-1); }
.cv-dropzone__empty .bi { font-size: 1.5rem; margin-bottom: var(--cv-sp-1); }
.cv-dropzone__title { font-size: var(--cv-text-sm); font-weight: var(--cv-weight-medium); color: var(--cv-ink-2); }
.cv-dropzone__hint  { font-size: var(--cv-text-2xs); color: var(--cv-ink-3); }
.cv-dropzone__busy {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background-color: rgb(255 255 255 / 0.82);
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-medium);
  color: var(--cv-ink);
}

/* ============================================================================
   Switch, and the preference rows it sits in
   ----------------------------------------------------------------------------
   The settings page drew its switches entirely from `--ch-*` / `--a-*`
   variables that no longer exist, so the track had no background and the
   checked state had no fill: the toggles were invisible and there was no way
   to tell an enabled preference from a disabled one.

   Built on a real checkbox so it keeps the keyboard behaviour, the focus ring
   and — the point on this page — the form semantics: the view reads these with
   `'key' in request.POST`, so the control must be a genuine checkbox that
   posts when checked and nothing when not.
   ============================================================================ */
.cv-switch { position: relative; display: inline-block; flex: 0 0 auto; width: 2.75rem; height: 1.5rem; }
.cv-switch input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer;
}
.cv-switch__track {
  position: absolute; inset: 0;
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-n-300);
  transition: background-color var(--cv-t-fast) var(--cv-ease);
  pointer-events: none;
}
.cv-switch__track::after {
  content: "";
  position: absolute;
  top: 0.1875rem; left: 0.1875rem;
  width: 1.125rem; height: 1.125rem;
  border-radius: 50%;
  background-color: var(--cv-n-0);
  box-shadow: var(--cv-shadow-sm);
  transition: transform var(--cv-t-fast) var(--cv-ease);
}
.cv-switch input:checked + .cv-switch__track { background-color: var(--cv-ink); }
.cv-switch input:checked + .cv-switch__track::after { transform: translateX(1.25rem); }
.cv-switch input:focus-visible + .cv-switch__track { box-shadow: var(--cv-ring); }
.cv-switch input:disabled + .cv-switch__track { opacity: 0.5; cursor: not-allowed; }

/* One preference: icon, label over its explanation, control on the right. */
.cv-pref {
  display: flex;
  align-items: center;
  gap: var(--cv-sp-4);
  padding: var(--cv-sp-4) var(--cv-sp-5);
  border-top: 1px solid var(--cv-line-soft);
}
.cv-pref:first-child { border-top: 0; }
.cv-pref__icon {
  display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
  width: 2.25rem; height: 2.25rem;
  border-radius: var(--cv-r-md);
  background-color: var(--cv-surface-sunk);
  color: var(--cv-ink-2);
}
.cv-pref__text { min-width: 0; flex: 1 1 auto; }
.cv-pref__name { display: block; font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); color: var(--cv-ink); }
.cv-pref__desc { display: block; margin-top: 0.1rem; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }
.cv-pref--nested { padding-left: var(--cv-sp-10); }
/* Dimmed, NOT disabled — see the note in settings.html. */
.cv-pref--muted .cv-pref__icon,
.cv-pref--muted .cv-pref__text { opacity: 0.55; }

/* A choice made of whole cards rather than a bare radio row. */
.cv-choice {
  display: grid;
  gap: var(--cv-sp-3);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 640px) { .cv-choice { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.cv-choice__opt {
  display: flex;
  align-items: flex-start;
  gap: var(--cv-sp-3);
  padding: var(--cv-sp-4);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
  cursor: pointer;
  transition: border-color var(--cv-t-fast) var(--cv-ease),
              background-color var(--cv-t-fast) var(--cv-ease);
}
.cv-choice__opt:hover { border-color: var(--cv-line-strong); }
.cv-choice__opt:has(input:checked) { border-color: var(--cv-ink); background-color: var(--cv-surface-sunk); }
.cv-choice__opt:has(input:focus-visible) { box-shadow: var(--cv-ring); }
.cv-choice__opt input { flex: 0 0 auto; margin-top: 0.15rem; }
.cv-choice__name { display: block; font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); }
.cv-choice__desc { display: block; margin-top: 0.15rem; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }

/* ============================================================================
   Subscription plans
   ----------------------------------------------------------------------------
   Three price cards that must line up whatever their feature lists weigh, so
   the action sits on an auto-margin rather than at the natural end of the
   content. The recommended plan is marked with a border and a ribbon, not with
   a different colour alone.
   ============================================================================ */
.cv-plans {
  display: grid;
  gap: var(--cv-sp-5);
  grid-template-columns: minmax(0, 1fr);
  align-items: stretch;
}
@media (min-width: 720px)  { .cv-plans { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 1100px) { .cv-plans { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.cv-plan {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--cv-sp-6);
  background-color: var(--cv-surface);
  border: 1px solid var(--cv-line);
  border-radius: var(--cv-r-lg);
}
.cv-plan--featured { border-color: var(--cv-accent); border-width: 2px; }
.cv-plan__ribbon {
  position: absolute;
  top: 0; right: var(--cv-sp-5);
  transform: translateY(-50%);
  padding: 0.15rem var(--cv-sp-3);
  border-radius: var(--cv-r-pill);
  background-color: var(--cv-accent);
  color: var(--cv-n-900);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-bold);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.cv-plan__name { margin: 0; font-size: var(--cv-text-h4); }
.cv-plan__desc { margin: var(--cv-sp-2) 0 0; font-size: var(--cv-text-sm); color: var(--cv-ink-3); }
.cv-plan__price { margin: var(--cv-sp-5) 0; }
.cv-plan__amount {
  display: block;
  font-size: var(--cv-text-h1);
  font-weight: var(--cv-weight-bold);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.cv-plan__period { display: block; font-size: var(--cv-text-xs); color: var(--cv-ink-3); }
.cv-plan__features {
  list-style: none;
  margin: 0;
  padding: var(--cv-sp-5) 0 0;
  border-top: 1px solid var(--cv-line-soft);
  display: flex;
  flex-direction: column;
  gap: var(--cv-sp-2);
  font-size: var(--cv-text-sm);
  color: var(--cv-ink-2);
}
.cv-plan__features li { display: flex; align-items: flex-start; gap: var(--cv-sp-2); }
.cv-plan__features .bi { flex: 0 0 auto; color: var(--cv-success-text); line-height: 1.5; }
.cv-plan__features--empty .bi { color: var(--cv-ink-3); }
/* Auto margin, so a plan with two features and one with eight still align. */
.cv-plan__action { margin-top: auto; padding-top: var(--cv-sp-6); }

/* ============================================================================
   Accordion
   ----------------------------------------------------------------------------
   Native <details>/<summary>, so the answers stay in the DOM (and findable with
   the browser's own in-page search) and the control needs no JavaScript. The
   default disclosure triangle is removed in favour of a chevron that rotates,
   because the triangle sits at a different baseline in every browser.
   ============================================================================ */
.cv-accordion { display: flex; flex-direction: column; }
.cv-accordion__item + .cv-accordion__item { border-top: 1px solid var(--cv-line-soft); }
.cv-accordion__q {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--cv-sp-4);
  padding: var(--cv-sp-4) var(--cv-sp-5);
  font-size: var(--cv-text-sm);
  font-weight: var(--cv-weight-semibold);
  color: var(--cv-ink);
  cursor: pointer;
  list-style: none;
}
.cv-accordion__q::-webkit-details-marker { display: none; }
.cv-accordion__q:hover { background-color: var(--cv-surface-sunk); }
.cv-accordion__q:focus-visible { outline: none; box-shadow: var(--cv-ring); }
.cv-accordion__chevron {
  flex: 0 0 auto;
  color: var(--cv-ink-3);
  transition: transform var(--cv-t-fast) var(--cv-ease);
}
.cv-accordion__item[open] .cv-accordion__chevron { transform: rotate(180deg); }
.cv-accordion__a {
  padding: 0 var(--cv-sp-5) var(--cv-sp-5);
  font-size: var(--cv-text-sm);
  color: var(--cv-ink-2);
  max-width: 72ch;
}
.cv-accordion__a > p:last-child { margin-bottom: 0; }
