/* Font loading moved to <link> tags in each page's <head> (see index.html
   etc.) instead of @import here. An @import inside an external stylesheet
   forces a second render-blocking round trip -- browser must download
   styles.css, parse it, discover the @import, then fetch the font CSS --
   instead of loading fonts in parallel with everything else. This was
   likely the cause of visible font flash/inconsistency on pages that
   render content quickly (e.g. Extensions due, populated via JS right
   after page load, sometimes catching the pre-font-swap moment). */

:root {
  --bg: #faf8f3;
  --bg-zebra-dark: #efe3cf;
  --surface: #ffffff;
  --border: #e4dfd3;
  --text: #1b2420;
  --text-muted: #6b6558;
  --accent: #1b4332;
  --accent-hover: #14352a;
  --gold: #c08a1e;
  --gold-bg: #fcf3dc;
  --success-bg: #e8f0ea;
  --success-text: #1b4332;
  --warning-bg: #fcf3dc;
  --warning-text: #85611b;
  --danger-bg: #f8ded7;
  --danger-bg-dark: #efc9bc;
  --danger-text: #7a2712;
  --positive-bg: #e5f1e7;
  --positive-text: #2d5f3f;
  --positive-border: #7fb894;
  --opportunity-bg: var(--gold-bg);
  --opportunity-text: #8a6414;
  --opportunity-border: var(--gold);
  --radius: 10px;
  --font-display: 'Fraunces', Georgia, serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: 'IBM Plex Mono', 'SF Mono', Menlo, monospace;
  font-family: var(--font-body);
}

* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--font-body); }
h1, h2, h3 { font-family: var(--font-display); }

.topbar {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  grid-template-areas: "brand . user" "nav nav nav";
  row-gap: 10px; column-gap: 16px;
  padding: 16px 32px; background: var(--surface); border-bottom: 1px solid var(--border);
}
.brand {
  grid-area: brand;
  font-family: var(--font-display); font-size: 21px; font-weight: 600;
  color: var(--accent); letter-spacing: -0.01em; position: relative;
  padding-bottom: 2px; border-bottom: 2px solid var(--gold);
  display: inline-flex; align-items: center; gap: 8px;
}
.brand-icon { width: 26px; height: 26px; flex-shrink: 0; }
.user-area { grid-area: user; display: flex; align-items: center; font-size: 13px; }
.badge {
  font-size: 11px; font-weight: 500; color: var(--text-muted);
  background: var(--bg); border: 1px solid var(--border); border-radius: 999px;
  padding: 2px 8px; margin-left: 8px; vertical-align: middle;
}
.tabs { grid-area: nav; display: flex; gap: 4px; flex-wrap: nowrap; overflow-x: auto; flex-shrink: 1; min-width: 0; justify-content: center; }
.tab-btn {
  border: none; background: none; padding: 9px 18px; border-radius: 999px;
  font-size: 14px; font-weight: 600; color: var(--text-muted); cursor: pointer;
  transition: all 0.18s ease; text-decoration: none; display: inline-block;
  white-space: nowrap;
}
.tab-btn:hover { background: var(--gold-bg); color: var(--text); }
.tab-btn.active { background: var(--accent); color: #fff; }

.container { max-width: min(97vw, 2400px); margin: 32px auto; padding: 0 24px; }

.ledger-hero {
  display: flex; gap: 0; margin-bottom: 24px; background: var(--accent);
  border-radius: var(--radius); overflow: hidden; box-shadow: 0 4px 20px rgba(27,67,50,0.15);
}
.ledger-stat {
  flex: 1; padding: 22px 20px; border-right: 1px solid rgba(255,255,255,0.15);
  display: flex; flex-direction: column; gap: 8px;
}
.ledger-stat:last-child { border-right: none; }
.ledger-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  color: rgba(255,255,255,0.7); font-weight: 600;
}
.ledger-number {
  font-family: var(--font-mono); font-size: 32px; font-weight: 600; color: #fff;
  font-variant-numeric: tabular-nums; border-top: 1px solid rgba(255,255,255,0.25); padding-top: 6px;
}

.flag-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 13px;
}
.activity-row {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 13px;
}
.activity-row:last-child { border-bottom: none; }
/* Zebra striping for div-based dashboard lists (Extensions due, Recent
   activity) -- these aren't <table> rows so the .data-table zebra rule
   never reached them. Scoped to .activity-row only (not .flag-detail-row)
   so it doesn't tint the Overdue-docs stalled-notes list, which
   deliberately keeps a plain background inside its already-red
   .flag-group-detail container. */
.activity-row.row-zebra, .flag-detail-row.row-zebra { background: var(--bg); }
/* Compliance state filings render as .client-link-row divs.
   NOTE: .client-link-row's own background is already var(--bg), so an earlier
   attempt at this rule set var(--bg) again -- higher specificity, identical
   colour, zero visible change. The alternating row must use the OTHER palette
   colour to actually read as striped. */
.client-link-row.row-zebra { background: var(--surface); }
/* Color-only urgency modifier for Extensions due -- always paired with the
   base .activity-row class (never used alone), so every row in the list
   keeps identical padding/font-size/border regardless of urgency. Urgency
   is conveyed purely through this red text color, not a size/spacing change. */
.activity-row-urgent { color: var(--danger-text); }

/* Extensions-due rows switch between .activity-row (safe) and
   .flag-detail-row (urgent/overdue) based on days remaining -- but
   .flag-detail-row was built for a single warning line inside an
   already-bordered card elsewhere (Overdue Docs stalled-notes,
   engagement-review/new-engagement banners), so it never had a
   border-bottom. Adding one directly to .flag-detail-row would leak
   into those unrelated uses. This class is added alongside whichever
   urgency class applies, purely to give extension list rows the same
   look as the .data-table rows in My Engagements / Coming Up --
   including a line under the LAST row too (matching how every <td>
   there gets a border-bottom with no last-child exception). */
.dashboard-row-separated { border-bottom: 1px solid var(--border); }
.progress-bar-mini { width: 100px; height: 6px; background: var(--border); border-radius: 999px; overflow: hidden; }
.progress-bar-mini > div { height: 100%; background: var(--accent); }

.flag-group {
  background: var(--danger-bg); border-radius: 8px; margin-bottom: 6px; overflow: hidden;
}
/* Overdue docs alternation: a slightly darker shade of the same danger
   color, not the default grey/white zebra used elsewhere -- keeps the
   urgency signal intact while still giving visual separation between
   groups. Scoped to .flag-group specifically (not the shared .row-zebra
   rule) so it can't affect any other table's striping. */
.flag-group.row-zebra { background: var(--danger-bg-dark); }
.flag-group.row-zebra summary { background: transparent; }
.flag-group summary {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 14px; cursor: pointer; list-style: none; font-size: 13px;
  font-weight: 600; color: var(--danger-text);
}
.flag-group summary::-webkit-details-marker { display: none; }
.flag-group summary::before {
  content: "+";
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface); color: var(--text); font-size: 11px; font-weight: 700;
  margin-right: 8px; flex-shrink: 0;
}
.flag-group[open] summary::before { content: "−"; }
.flag-count-badge {
  background: var(--danger-text); color: #fff; font-size: 11px; font-weight: 700;
  border-radius: 999px; padding: 2px 8px; margin-left: auto;
}
.flag-group-detail { padding: 0 14px 12px 30px; }
.flag-detail-row { font-size: 12px; color: var(--danger-text); padding: 3px 0; }

/* "New client" moments -- a fresh, warm sage-green, distinct from the
   deep ink-green used elsewhere, reading as "growth / new beginning"
   rather than the platform's usual restrained professional tone. */
.positive-banner {
  background: var(--positive-bg); border: 1px solid var(--positive-border);
  color: var(--positive-text); border-radius: 8px; padding: 14px 16px; margin-bottom: 16px;
}
.checklist-block { border-left: 3px solid var(--positive-border); padding-left: 16px; margin-bottom: 24px; }
.checklist-block h3 { color: var(--positive-text); }

/* "New leads" -- reuses the gold already used for AI-extracted badges,
   reading as "opportunity, not yet confirmed" -- exactly the right tone
   for a lead versus a secured client. */
.opportunity-card {
  background: var(--opportunity-bg); border: 1px solid var(--opportunity-border);
  border-radius: var(--radius); padding: 20px 24px; margin-bottom: 20px;
}
.opportunity-card h2 { color: var(--opportunity-text); }
.flag-item-row {
  display: flex; align-items: flex-start; gap: 8px; width: 100%;
  font-size: 12px; color: var(--danger-text); padding: 4px 0; cursor: pointer;
}
.flag-item-row input[type="checkbox"] { flex-shrink: 0; margin-top: 2px; width: 14px; height: 14px; }
.flag-item-row span { flex: 1; }

.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: 24px; margin-bottom: 20px;
  transition: all 0.25s ease;
  overflow-x: auto;
}
.card-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px;
}
.card-header h2 { margin: 0; }
.expand-btn {
  border: 1px solid var(--border); background: var(--surface); border-radius: 6px;
  width: 28px; height: 28px; font-size: 14px; cursor: pointer; color: var(--text-muted);
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.expand-btn:hover { background: var(--bg); color: var(--text); }

.card.expanded {
  position: fixed; inset: 16px; z-index: 100; margin: 0;
  overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25);
  max-width: none;
}
body.card-is-expanded { overflow: hidden; }
.card h2 { margin-top: 0; font-size: 18px; }

.entity-type-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 8px;
}
.entity-tile {
  display: flex; align-items: center; gap: 8px; padding: 10px 12px;
  border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
  font-size: 13px; font-weight: 500; transition: border-color 0.15s, background 0.15s;
}
.entity-tile:hover { border-color: var(--accent); }
.entity-tile input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; margin: 0; flex-shrink: 0; }
.entity-tile:has(input:checked) { border-color: var(--accent); background: var(--success-bg); }
.entity-tile.suggested { border-color: #d4a017; background: #fff8e6; box-shadow: 0 0 0 2px #fff3cd; }
.entity-name-row {
  display: flex; align-items: center; gap: 10px; margin-bottom: 8px;
}
.entity-name-row-label {
  min-width: 190px; font-size: 13px; font-weight: 500; flex-shrink: 0;
}
.entity-name-row input[type="text"] { flex: 1; font-size: 13px; }
.muted { color: var(--text-muted); font-size: 14px; }

.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; flex: 1; min-width: 0; }
.field-row { display: flex; gap: 16px; flex-wrap: wrap; }
label { font-size: 13px; font-weight: 500; color: var(--text-muted); }
/* TEXT-LIKE INPUTS ONLY.
   This was `input, select`, which applied 10px of padding, a border and
   width:100% to checkboxes and radios as well — so a tick box rendered as a
   bordered, padded, full-width panel.

   Three pages had already patched around it with their own overrides, each
   fixing the symptom where somebody happened to notice it. The staff table
   never did, and its persistent-login column looked like a text field. */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]), select {
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
  font-size: 14px; background: var(--surface); color: var(--text);
  width: 100%; min-width: 0;
}

/* And a tick box looks like one, everywhere, without each page saying so. */
input[type="checkbox"], input[type="radio"] {
  width: 16px; height: 16px; min-width: 16px;
  accent-color: var(--accent);
  vertical-align: middle; cursor: pointer;
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled {
  cursor: not-allowed; opacity: 0.45;
}
input:not([type="checkbox"]):not([type="radio"]):focus, select:focus {
  outline: 2px solid var(--accent); outline-offset: -1px;
}
/* A tick box keeps a visible focus ring rather than an inset one, which on a
   16px control covers the tick itself. */
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

.btn-primary {
  background: var(--accent); color: white; border: none; border-radius: 8px;
  padding: 11px 20px; font-size: 14px; font-weight: 600; cursor: pointer;
}
.btn-primary:hover { background: var(--accent-hover); }

.btn-small {
  border: 1px solid var(--border); background: var(--surface); border-radius: 6px;
  padding: 5px 10px; font-size: 12px; cursor: pointer; font-weight: 500;
}
.btn-small:hover { background: var(--bg); }
.btn-reject { color: var(--warning-text); }

.btn-secondary {
  background: none; color: var(--text-muted); border: 1px solid var(--border); border-radius: 8px;
  padding: 8px 14px; font-size: 13px; font-weight: 500; cursor: pointer; margin-top: 4px;
}
.btn-secondary:hover { background: var(--bg); color: var(--text); }

.checklist-block { padding: 16px 0; border-bottom: 1px solid var(--border); }
.checklist-block:last-child { border-bottom: none; }
.checklist-block h3 { font-size: 15px; margin: 0 0 8px; }

.divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.section-divider {
  font-family: var(--font-display); font-size: 15px; font-weight: 600;
  color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em;
  margin: 28px 0 4px; padding-bottom: 6px; border-bottom: 2px solid var(--gold);
}
.section-divider:first-child { margin-top: 0; }

.settings-checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text); font-weight: 400; }
.settings-checkbox-label input[type="checkbox"] { width: 16px; height: 16px; }

.existing-client-banner {
  background: var(--success-bg); border: 1px solid var(--accent); color: var(--accent);
  border-radius: 8px; padding: 10px 14px; font-size: 13px; margin-bottom: 16px;
}
.required-mark { color: var(--warning-text); }
.phone-match-option { display: block; margin: 6px 0; }

.portal-body { background: var(--bg); min-height: 100vh; padding: 24px 16px; }
.portal-container { max-width: min(92vw, 1100px); margin: 0 auto; }
.portal-error {
  margin-top: 16px; padding: 12px; background: var(--warning-bg); color: var(--warning-text);
  border-radius: 8px; font-size: 13px;
}
.portal-checklist { list-style: none; padding: 0; margin: 0; }
.portal-item {
  display: flex; flex-direction: column; gap: 8px;
  padding: 16px 0; border-bottom: 1px solid var(--border);
}
.portal-item:last-child { border-bottom: none; }
.portal-item .item-label { font-size: 15px; font-weight: 600; }
.portal-item .item-meta { font-size: 13px; color: var(--text-muted); line-height: 1.4; }
.portal-item.portal-uploading { opacity: 0.5; pointer-events: none; }
.portal-empty { text-align: center; padding: 24px 0; color: var(--text-muted); font-size: 14px; }
.portal-answer-input { width: 100%; }
.portal-upload-btn {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  background: var(--accent); color: white; padding: 11px 16px; border-radius: 8px;
  font-size: 14px; font-weight: 600; cursor: pointer; width: 100%; max-width: 260px;
}
.portal-upload-btn:hover { background: var(--accent-hover); }
.portal-upload-btn input[type="file"] { display: none; }

.portal-upload-optional {
  display: flex; align-items: center; justify-content: center; gap: 6px; margin-top: 8px;
  background: none; color: var(--accent); border: 1.5px solid var(--accent);
  padding: 9px 16px; border-radius: 8px; font-size: 13px; font-weight: 600;
  cursor: pointer; width: 100%; max-width: 260px;
}
.portal-upload-optional:hover { background: var(--success-bg); }
.portal-upload-optional input[type="file"] { display: none; }

/* Desktop: use the extra width instead of just centering a phone-sized
   column in empty space. Document items go side-by-side (label/description
   left, button right) since there's now room; question items stay stacked
   full-width since a longer answer box is better regardless of screen size. */
@media (min-width: 700px) {
  .portal-item:not(.is-question) {
    flex-direction: row; align-items: center; justify-content: space-between; gap: 20px;
  }
  .portal-item:not(.is-question) .portal-item-text { flex: 1; }
  .portal-item:not(.is-question) .portal-upload-btn { flex-shrink: 0; width: auto; }
}

.client-link-row {
  display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px 10px;
  background: var(--bg); border-radius: 6px; font-size: 12px;
}
.client-link-row input { flex: 1; border: none; background: none; font-size: 12px; color: var(--text-muted); }

.status-submitted { background: var(--success-bg); color: var(--accent); }

.hidden { display: none; }

.checklist { list-style: none; padding: 0; margin: 0; }
.checklist li {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 0; border-bottom: 1px solid var(--border);
}
.checklist li:last-child { border-bottom: none; }
.checklist li.is-question { background: #fafaff; margin: 0 -12px; padding: 12px; border-radius: 8px; border-bottom: none; }
.question-badge {
  font-size: 10px; font-weight: 600; color: #6d5bd0; background: #f0edfd;
  border-radius: 999px; padding: 2px 8px;
}
.item-label { font-weight: 500; font-size: 14px; }
.detail-text { font-weight: 400; color: var(--accent); }
.carried-badge {
  font-size: 10px; font-weight: 600; color: var(--accent); background: var(--success-bg);
  border-radius: 999px; padding: 2px 7px; margin-left: 8px; vertical-align: middle;
}
.ai-badge {
  font-size: 10px; font-weight: 600; color: #9a5b00; background: var(--warning-bg);
  border-radius: 999px; padding: 2px 7px; margin-left: 8px; vertical-align: middle;
}
.detail-input {
  margin-top: 6px; width: 100%; max-width: 320px; padding: 5px 8px;
  font-size: 12px; border: 1px dashed var(--border); border-radius: 6px;
}
.btn-tiny {
  display: block; margin-top: 4px; border: none; background: none;
  color: var(--accent); font-size: 11px; font-weight: 600; cursor: pointer; padding: 2px 0;
}
.btn-tiny:hover { text-decoration: underline; }
.item-meta { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.item-actions { display: flex; gap: 6px; align-items: center; }

.status-pill {
  font-size: 11px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
  text-transform: capitalize;
}
.status-requested { background: var(--warning-bg); color: var(--warning-text); }
.status-received { background: var(--success-bg); color: var(--success-text); }
.status-waived { background: var(--bg); color: var(--text-muted); }

.progress-track {
  width: 100%; height: 8px; background: var(--bg); border-radius: 999px;
  overflow: hidden; margin: 12px 0 4px;
}
.progress-fill {
  height: 100%; background: var(--accent); border-radius: 999px;
  transition: width 0.3s ease; width: 0%;
}

.data-table { width: 100%; border-collapse: collapse; font-size: 14px; table-layout: auto; }
.data-table th { text-align: left; padding: 8px; color: var(--text-muted); font-weight: 500; font-size: 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
.sortable-col { cursor: pointer; user-select: none; }
.sortable-col:hover { color: var(--text); }
/* Cells wrap by default so long client/filing names don't force horizontal
   scroll on the whole table. Columns that should genuinely stay on one line
   (dates, status pills, short codes) opt in with .nowrap-col explicitly. */
.data-table td { padding: 10px 8px; border-bottom: 1px solid var(--border); white-space: normal; word-break: break-word; }
.data-table td.nowrap-col, .data-table th.nowrap-col { white-space: nowrap; }
/* Zebra striping standard for every data table across the app.
   .row-zebra is applied by JS to alternate rows (typically index % 2 === 1) --
   this rule was missing entirely, so every "zebra-striped" table rendered flat. */
.data-table tr.row-zebra td { background: var(--bg); }
.data-table tr.row-zebra.group-member-row td { background: var(--bg); }
/* Group headers (Clients, My Engagements) now use the exact same two
   colors as every other zebra-striped table in the app -- white for the
   base state, var(--bg) for the alternating state -- instead of a
   separate custom shade. This guarantees identical visual consistency,
   not just a similar-looking color. The group header's own always-on
   tint (previously always var(--bg), which made zebra invisible on it)
   is removed; the bold group name + expand icon already signal "this is
   a group" without needing a permanent background tint too. */
.group-header-row { background: var(--surface); }
.group-header-row.row-zebra { background: var(--bg); }
/* ID selectors guarantee this wins over the more specific
   .data-table tr.row-zebra td rule (2 classes + 2 elements beats this
   rule's 2 classes + 0 elements) regardless of source order -- td
   backgrounds always render on top of tr backgrounds visually. */
#clients-table tr.group-header-row.row-zebra td,
#my-engagements-table tr.group-header-row.row-zebra td {
  background: var(--bg);
}
.group-header-row td { font-size: 13px; }
.group-member-row td { background: var(--surface); }
.group-add-row td { padding: 6px 8px 10px 30px; }
.tree-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface); color: var(--text); font-size: 11px; font-weight: 700;
  cursor: pointer; padding: 0; line-height: 1;
}
.tree-toggle:hover { background: var(--gold-bg); }
.sticky-col {
  position: sticky; right: 0; background: var(--surface);
  box-shadow: -2px 0 4px rgba(0,0,0,0.08);
}

/* ── Kebab (⋮) action menu ────────────────────────────────────────────────── */
/* Compact touch-friendly tap target: visually small but the padding brings
   the effective hit area to ~36px, meeting mobile touch-target guidelines
   even though the sticky Actions column itself stays narrow. */
.kebab-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 32px; height: 32px; padding: 0;
  border: 1px solid var(--border); border-radius: 6px;
  background: var(--surface); color: var(--text-muted);
  font-size: 16px; line-height: 1; cursor: pointer;
}
.kebab-btn:hover, .kebab-btn.active { background: var(--gold-bg); color: var(--text); }

/* Appended to document.body with position:fixed (not nested in the table),
   so it always floats above the page regardless of any ancestor's
   overflow-x: auto -- see the comment in common.js's showActionMenu(). */
.action-menu {
  position: fixed; z-index: 100;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  min-width: 170px; padding: 4px; display: flex; flex-direction: column;
}
.action-menu-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  padding: 10px 12px; font-size: 13px; color: var(--text); border-radius: 5px;
  cursor: pointer; text-decoration: none; font-family: var(--font-body);
}
.action-menu-item:hover { background: var(--bg); }
.action-menu-danger { color: var(--danger-text); }
.action-menu-danger:hover { background: var(--danger-bg); }

/* ── Quiet link with hover-reveal icon ───────────────────────────────────────
   Replaces the browser-default blue/underline link look (this app has no
   base `a{}` styling at all, so client-name links were rendering with the
   raw browser default). Name stays in normal text color at rest; a small
   arrow icon fades in on hover as the "this is clickable" cue instead of
   color/underline doing that job. Name + icon are one <a>, one tap target. */
.hover-link {
  color: inherit; text-decoration: none;
  display: inline-flex; align-items: center; gap: 3px;
}
.hover-link:hover { color: var(--accent); }
.hover-link .hover-link-icon {
  opacity: 0; transition: opacity 0.15s ease; font-size: 0.85em;
}
.hover-link:hover .hover-link-icon { opacity: 1; }

/* ── Toast notification ───────────────────────────────────────────────────── */
.toast {
  position: fixed; bottom: 24px; left: 50%;
  transform: translateX(-50%) translateY(12px);
  background: var(--accent); color: #fff;
  padding: 10px 20px; border-radius: 8px; font-size: 13px; font-weight: 500;
  box-shadow: 0 8px 24px rgba(0,0,0,0.18);
  z-index: 200; opacity: 0; pointer-events: none;
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.toast-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast-success { background: var(--accent); }
.toast-error { background: var(--danger-text); }

/* ── Inline-renameable labels ─────────────────────────────────────────────── */
/* The pencil only appears on hover so the list stays visually calm at rest,
   while still signalling the label is editable the moment the mouse is over it. */
.editable-label { cursor: text; border-radius: 3px; padding: 1px 3px; margin-left: -3px; }
.editable-label:hover { background: var(--gold-bg); }
.rename-icon { opacity: 0; font-size: 11px; margin-left: 5px; color: var(--text-muted); transition: opacity 0.15s ease; }
.editable-label:hover .rename-icon { opacity: 1; }
.inline-rename-input { font-size: 14px; font-weight: 600; width: 100%; max-width: 420px; padding: 2px 6px; }
.available-items-row { margin-top: 10px; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }

/* ── Client name typeahead ────────────────────────────────────────────────── */
.typeahead-wrap { position: relative; }
.typeahead-results {
  position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.14);
  max-height: 280px; overflow-y: auto; margin-top: 2px;
}
.typeahead-item {
  padding: 8px 12px; cursor: pointer; display: flex; flex-direction: column; gap: 2px;
  border-bottom: 1px solid var(--border);
}
.typeahead-item:last-of-type { border-bottom: none; }
.typeahead-item:hover { background: var(--gold-bg); }
.typeahead-name { font-size: 13px; font-weight: 500; }
.typeahead-meta { font-size: 12px; color: var(--text-muted); }
.typeahead-footnote {
  padding: 6px 12px; font-size: 11px; color: var(--text-muted);
  border-top: 1px solid var(--border); background: var(--bg);
}

/* ── New-client form layout ───────────────────────────────────────────────── */
/* Six controls previously shared one flex row, so the two name fields (the
   longest values) got squeezed while email took more room than an address
   needs. Split into labelled rows, each field sized to the data it holds. */
.new-client-row {
  display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 10px;
}
.new-client-field {
  display: flex; flex-direction: column; gap: 3px;
}
.new-client-label {
  font-size: 11px; font-weight: 600; color: var(--text-muted);
}
.new-client-field input,
.new-client-field select {
  width: 100%; font-size: 13px;
}
/* Email sits alone on a flexible row, so it would otherwise stretch wider than
   the two name fields that share the row above -- which is the "email box is
   too big" imbalance. Cap it to roughly a name field's share. */
.new-client-field-email { max-width: 320px; }
/* The typeahead dropdown is absolutely positioned against this wrapper, so it
   has to span the full field width rather than the input's intrinsic size. */
.new-client-field .typeahead-wrap { width: 100%; }

/* ── Links whose text IS data ─────────────────────────────────────────────── */
/* .hover-link works when the link text is an action word ("Open") -- the
   pencil/arrow appears on hover and the meaning is obvious. It fails when the
   link text is a VALUE, like a progress figure of "3/13 (2025)", because that
   reads as table data and gives no cue at all until the mouse happens over it.
   Those links need to be visibly links at rest.
   Deliberately the standard browser link styling rather than a palette-matched
   colour: in a dense table the familiar blue-and-underlined convention is
   instantly recognisable as clickable, which matters more here than blending
   with the design. (The app's --accent is also unusable for this -- #1b4332 is
   a deep green almost identical to the body text at #1b2420.) */
.data-link {
  color: #0000EE;              /* standard browser link blue */
  text-decoration: underline;
}
.data-link:visited { color: #551A8B; }   /* standard visited purple */
.data-link:hover   { color: #0000CC; }

/* ── Portal: the file a client has uploaded ──────────────────────────────── */
/* Shown so the client can see WHAT they sent and correct it if it was wrong.
   Sized for a thumb: the remove control sits on its own line on narrow screens
   rather than being squeezed beside the filename, where it would be both hard
   to hit and easy to hit by accident. */
.portal-uploaded-file {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 6px;
  font-size: 13px;
}
.portal-uploaded-name {
  color: var(--muted-text, #5A6472);
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.portal-remove-file,
.portal-remove-row {
  /* Touch target: below ~40px a thumb misses, and a mis-tap here deletes
     something. Never hover-only -- a phone has no hover. */
  min-height: 40px;
  padding: 8px 12px;
}
@media (max-width: 480px) {
  .portal-uploaded-file { flex-direction: column; align-items: flex-start; }
  .portal-remove-file, .portal-remove-row { width: 100%; }
}

/* ── Checklist item provenance ───────────────────────────────────────────── */
/* Where an item came from. Text as well as colour: colour alone fails for
   colour-blind users and vanishes when a checklist is printed. */
.origin-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 2px 7px;
  border-radius: 10px;
  margin-left: 8px;
  vertical-align: middle;
  white-space: nowrap;
}
/* Evidenced by the prior-year return — the strongest signal on the list, so it
   reads as confident rather than cautionary. */
.origin-prior {
  background: #E3EEF9;
  color: #1B3A5C;
  border: 1px solid #BBD3E8;
}
/* No support in the prior return. Deliberately muted: this is a suggestion to
   review, not a warning, and it must not compete with a genuine problem. */
.origin-unevidenced {
  background: #F1F1EE;
  color: #6B6B63;
  border: 1px solid #DDDDD6;
}
.origin-client {
  background: #EDF6EE;
  color: #2C5A32;
  border: 1px solid #C8E2CC;
}
/* The row itself recedes when unevidenced, so a preparer's eye goes to what the
   client actually has. */
li.item-unevidenced { opacity: 0.62; }
li.item-unevidenced:hover { opacity: 1; }

/* The prompt offering to clear items the prior return does not support.
   Informational rather than alarming: it is a suggestion to review, and must
   not look like a problem needing attention. */
.unevidenced-prompt {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 8px;
  padding: 8px 12px;
  background: #F7F7F4;
  border: 1px solid #E4E4DC;
  border-radius: 6px;
  font-size: 13px;
  color: #5A5A52;
}

/* ── Form actions always beat the typeahead panel ─────────────────────────── */
/*
   The suggestions panel is absolutely positioned at z-index 50, so it overlays
   whatever sits below the input -- including Save and Cancel.
   
   That is not merely awkward: it deadlocks. The panel hides on blur, but the
   click cannot reach Save to cause the blur, because the panel is intercepting
   it. A user clicks, nothing happens, and clicking again does nothing either.
   Escape clears it, but only if you know to press Escape.
   
   The action row therefore sits above the panel. Where they overlap the panel
   is obscured, which is the right priority -- the thing the user is trying to
   DO beats the suggestion about what they might have meant. The panel scrolls
   within its 280px, so its entries remain reachable.
*/
.form-actions-row {
  position: relative;
  z-index: 60;   /* above .typeahead-results at 50 */
}

/* Standard items are badged too. Leaving them blank meant a user comparing
   prior-year items against template items saw a majority with no badge at all,
   which reads as "the badge failed" rather than "this is standard". */
.origin-standard {
  background: #EDF3ED;
  color: #3D5A40;
  border: 1px solid #CFE0D1;
}
.origin-custom {
  background: #F3EFE6;
  color: #6B5A38;
  border: 1px solid #E2D7C2;
}
/* The make-up of the checklist at a glance, before reading any of it. */
.provenance-summary {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-muted);
}
.provenance-summary .origin-badge { margin-left: 0; }

/* Income the prior-year return showed that no checklist item covers.
   Previously discarded in silence, which is how a CPA ends up trusting a list
   that quietly omitted six of seven findings. */
.unmatched-sources {
  margin-top: 8px;
  padding: 10px 12px;
  background: #FCF8EE;
  border: 1px solid #E8DDBF;
  border-radius: 6px;
  font-size: 13px;
}
.unmatched-sources ul { margin: 6px 0 6px 18px; padding: 0; }
.unmatched-sources li { margin-bottom: 3px; }

/* ── Checklist sections ──────────────────────────────────────────────────── */
/*
   The checklist groups by provenance ONLY when a prior return was analysed --
   otherwise every item is standard and headings would carry no information.

   Only "Not on last year's return" collapses. The other two stay open on a
   deliberate principle: collapse what you want to dismiss, never what you need
   to act on. An outstanding document behind a chevron is a document some
   preparer will not look at, and in tax work that is how things get missed.
*/
.checklist-section { list-style: none; margin: 14px 0 0; }
.checklist-section:first-child { margin-top: 0; }

.checklist-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  background: var(--gold-bg, #FAF7F0);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
  user-select: none;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);

  /* Sticky, so the heading and its count travel with you down a long list.
     Twelve items in the first section means the second heading is off-screen
     before you reach it, and without this you lose track of which group you
     are looking at -- which is the problem a separate summary line would only
     have papered over.

     z-index sits above rows but below the typeahead panel (50) and the form
     actions (60), so nothing that must be clickable is ever covered. */
  position: sticky;
  top: 0;
  z-index: 20;
  /* Opaque: a translucent heading over scrolling rows is unreadable. */
  background-color: #FAF7F0;
  box-shadow: 0 1px 0 rgba(0,0,0,0.04);
}
/* A collapsed heading has nothing scrolling beneath it, so it need not stick --
   and three stacked sticky headings would eat the viewport. */
.checklist-section.collapsed .checklist-section-head {
  position: static;
}
.checklist-section-head:hover { background: #F3EEE2; }
.section-chevron { font-size: 11px; color: var(--text-muted); width: 10px; }
.section-title { flex: 0 1 auto; }
.section-count {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1px 8px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
}
/* The waive button lives on the heading, so the whole section can be cleared
   without expanding it first. */
.section-action { margin-left: auto; }

.checklist-section-body { list-style: none; margin: 0; padding: 0; }
.checklist-sublist { list-style: none; margin: 0; padding: 0; }

/* A collapsed section that receives an upload or a status change opens itself,
   and the row that changed is marked briefly -- otherwise a preparer acts and
   sees nothing happen. */
.item-just-changed {
  animation: item-flash 2.5s ease-out;
}
@keyframes item-flash {
  0%, 20% { background: #FFF6D6; }
  100%    { background: transparent; }
}

/* ── Extracted figures ───────────────────────────────────────────────────── */
.extracted-figures { display: flex; flex-wrap: wrap; gap: 4px 0; }
.extracted-figure { color: var(--text-muted); font-size: 12px; }
/* A figure the extraction was unsure of. Marked so a reviewer checks four
   numbers rather than forty. */
.figure-low strong { color: #B00000; }
.figure-flag {
  font-size: 10px; font-weight: 600; color: #B00000;
  border: 1px solid #E5C4C4; border-radius: 8px; padding: 0 5px; margin-left: 3px;
}
.extracted-totals { margin-top: 3px; font-size: 12px; }
.meta-tag {
  display: inline-block; font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.03em; background: #EFEFE9; color: #5A5A52;
  border-radius: 3px; padding: 1px 5px; margin-right: 5px;
}
.meta-tag.warn { background: #FBEFD9; color: #7A5A20; }

/* Figures read but not recognised. Prominent on purpose -- a silently dropped
   figure is how income goes missing from a return. */
.extracted-unclassified {
  margin-top: 5px; padding: 6px 9px; font-size: 12px;
  background: #FCF8EE; border: 1px solid #E8DDBF; border-radius: 5px;
}
.extracted-unclassified ul { margin: 4px 0 0 16px; padding: 0; }

/* Notes quote raw document fragments: useful when checking a figure, noise the
   rest of the time. Collapsed by default -- and this is metadata, not
   outstanding work, so hiding it breaks no checklist. */
.extracted-notes { margin-top: 5px; font-size: 12px; }
.extracted-notes summary { cursor: pointer; color: var(--text-muted); }
.extracted-notes ul { margin: 5px 0 0 16px; padding: 0; color: var(--text-muted); }
.extracted-notes li { margin-bottom: 3px; }

/* The document does not match the item it was filed under -- a real filing
   error, previously buried in a wall of assumption text. */
.extraction-mismatch {
  margin-top: 5px; padding: 7px 10px; font-size: 12px;
  background: #FDF0EE; border: 1px solid #E8C4BE; border-radius: 5px; color: #7A3A30;
}

/* Names the document a set of figures was read from.
   Without it the section heading does the talking, and "From the 2024 return"
   reads as though the FIGURES came from the prior-year return -- when the
   heading only explains why the ITEM is on the checklist. Two different facts;
   running them together is how a 1099-INT came to display a W-2's wages. */
.figures-source {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-muted);
}

/* ── Portal: bulk intake ─────────────────────────────────────────────────── */
/* Sits above the checklist because it is what most clients will actually use.
   The checklist remains below for anyone who prefers to work item by item. */
.portal-dropzone {
  border: 2px dashed var(--border);
  border-radius: 8px;
  padding: 22px 16px;
  text-align: center;
  transition: background 0.15s, border-color 0.15s;
}
.portal-dropzone.dropzone-active {
  border-color: var(--accent, #1B3A5C);
  background: var(--gold-bg, #FAF7F0);
}
.portal-bulk-status {
  padding: 9px 12px;
  border-radius: 6px;
  font-size: 14px;
}
.status-working { background: #F2F4F7; color: #44506B; }
.status-done    { background: #EDF6EE; color: #2C5A32; border: 1px solid #C8E2CC; }
.status-error   { background: #FDF0EE; color: #7A3A30; border: 1px solid #E8C4BE; }

/* On a phone the button must be thumb-sized, and the zone should not eat the
   screen before the checklist is even visible. */
@media (max-width: 480px) {
  .portal-dropzone { padding: 16px 12px; }
  .portal-dropzone .btn-primary { width: 100%; min-height: 44px; }
}

/* ── Bulk intake: review queue ───────────────────────────────────────────── */
/* Documents the pipeline would not file on its own.
   Deliberately prominent: this queue IS the safety property. Thirty seconds of
   a preparer's attention here prevents a figure landing on the wrong return. */
.intake-review {
  margin-top: 10px;
  padding: 12px 14px;
  background: #FCF8EE;
  border: 1px solid #E8DDBF;
  border-radius: 7px;
}
.intake-review-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
  font-size: 14px;
}
.intake-doc {
  display: flex;
  gap: 14px;
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 10px 0;
  border-top: 1px solid #EDE4CC;
}
.intake-doc-main { flex: 1 1 320px; min-width: 0; font-size: 13px; }
/* The reason it stopped, in plain language. Without it a reviewer is guessing
   as much as the classifier was. */
.intake-reason { color: #7A5A20; margin-top: 3px; }
.intake-provenance { font-size: 12px; margin-top: 2px; }
.intake-doc-actions {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
}
.intake-target { max-width: 260px; font-size: 13px; }

/* On a phone the controls stack rather than being squeezed, and stay
   thumb-sized -- a mis-tap here files a document against the wrong item. */
@media (max-width: 640px) {
  .intake-doc-actions { width: 100%; }
  .intake-target { max-width: 100%; flex: 1 1 100%; }
  .intake-doc-actions .btn-tiny { min-height: 40px; padding: 8px 12px; }
}

/* ── CPA-side bulk intake ────────────────────────────────────────────────── */
/* A client emails the firm a folder; the CPA drops the pile here for the
   engagement they already have open, so attribution is a deliberate act. */
.engagement-bulk {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.bulk-inline-status { font-size: 12px; }
.bulk-inline-status.status-working { color: var(--text-muted); }
.bulk-inline-status.status-done    { color: #2C5A32; font-weight: 600; }
.bulk-inline-status.status-error   { color: #B00000; font-weight: 600; }

@media (max-width: 640px) {
  .engagement-bulk .btn-tiny { width: 100%; min-height: 40px; }
}

/* ── CPA-side pile upload ────────────────────────────────────────────────── */
/* Collapsed by default: most engagements do not need it, and an always-open
   drop zone would push the checklist down the page for everyone. */
.engagement-bulk { margin-top: 8px; font-size: 13px; }
.engagement-bulk summary {
  cursor: pointer;
  color: var(--accent, #1B3A5C);
  font-weight: 500;
  padding: 4px 0;
}
.engagement-bulk summary:hover { text-decoration: underline; }
.bulk-inline-status {
  padding: 7px 10px;
  border-radius: 5px;
  font-size: 13px;
}

/* ── Feedback panel ──────────────────────────────────────────────────────── */
/* One field and a category. A report that takes five decisions is a report
   nobody writes, and the context that makes it actionable is collected
   automatically rather than asked for. */
.feedback-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(20, 22, 26, 0.45);
  display: flex; align-items: center; justify-content: center;
  padding: 16px;
}
.feedback-panel {
  background: var(--surface, #fff);
  border-radius: 10px;
  padding: 20px;
  width: 100%; max-width: 480px;
  box-shadow: 0 14px 44px rgba(0,0,0,0.28);
}
.feedback-panel textarea {
  width: 100%; font-family: inherit; font-size: 14px;
  padding: 9px 11px; border: 1px solid var(--border); border-radius: 6px;
  resize: vertical; margin-bottom: 10px;
}
.feedback-kinds { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 10px; font-size: 13px; }
.feedback-kind { display: flex; align-items: center; gap: 5px; cursor: pointer; }
.feedback-actions { display: flex; gap: 8px; align-items: center; }

/* Thumb-sized on a phone, and the panel must not exceed the screen. */
@media (max-width: 480px) {
  .feedback-panel { padding: 16px; max-height: 90vh; overflow-y: auto; }
  .feedback-actions .btn-primary { flex: 1; min-height: 44px; }
  .feedback-kinds { flex-direction: column; gap: 8px; }
}

/* Portal footer: keeping the link, and reporting trouble. */
.portal-footer {
  text-align: center;
  margin-top: 18px;
  font-size: 13px;
}
.portal-footer a { color: var(--text-muted); text-decoration: underline; }

/* A client with no engagement. Previously a dead end reading "no engagements
   yet" with nothing to do about it -- and an engagement is genuinely required,
   since §7216 consent attaches to one rather than to a client generally. So
   explain, and offer the way forward. */
.no-engagement-prompt {
  padding: 12px 14px;
  background: var(--gold-bg, #FAF7F0);
  border: 1px solid var(--border);
  border-radius: 6px;
  max-width: 460px;
}

/* Explains why the New Engagement form appeared.
   A CPA who clicked "Upload documents" and landed on a form needs to know the
   upload has not vanished -- it is one step away, and §7216 consent attaches to
   an engagement rather than to a client generally, so the step is real. */
.upload-after-banner {
  padding: 12px 14px;
  margin-bottom: 14px;
  background: #EDF3F9;
  border: 1px solid #C4D8E8;
  border-radius: 7px;
  font-size: 14px;
  line-height: 1.5;
  color: #1B3A5C;
}
/* When upload is the reason they came, make it obvious on arrival rather than
   another quiet control among several. */
.upload-now-highlight {
  padding: 10px 12px;
  background: #EDF6EE;
  border: 1px solid #C8E2CC;
  border-radius: 7px;
}
.upload-now-highlight .btn-tiny {
  background: #2C5A32;
  color: #fff;
  border-color: #2C5A32;
}

/* A document carrying a different taxpayer ID. The one signal that reliably
   means "this belongs to someone else", so it reads as a warning rather than
   another quiet column. */
.intake-mismatch {
  padding: 7px 10px;
  margin-bottom: 6px;
  background: #FDF0EE;
  border: 1px solid #E8C4BE;
  border-radius: 5px;
  font-size: 13px;
  color: #7A3A30;
}
/* Belongs to a different entity in the same group. Informational, not alarming
   -- a business owner's pile legitimately spans several entities, and this is
   the system sorting it rather than something going wrong. */
.intake-routed {
  padding: 7px 10px;
  margin-bottom: 6px;
  background: #EDF3F9;
  border: 1px solid #C4D8E8;
  border-radius: 5px;
  font-size: 13px;
  color: #1B3A5C;
}

/* Correcting a misfiled document after the fact.
   Deliberately quiet -- most documents are on the right entity, and a loud
   control here would suggest doubt where there is none. But it must exist:
   noticing a misfiling a week later and being unable to fix it is what makes
   an otherwise careful pipeline brittle. */
.move-entity-link {
  font-size: 12px;
  color: var(--text-muted);
  text-decoration: underline;
}
.move-entity-link:hover { color: var(--text); }

/* ── Entities named in documents that the firm has no client for ─────────── */
/* Placed ABOVE the review queue on purpose: adding the entity often resolves
   several queued documents at once, so deciding this first saves handling the
   same documents twice. */
.entity-candidates {
  margin-top: 10px;
  padding: 12px 14px;
  background: #EDF3F9;
  border: 1px solid #C4D8E8;
  border-radius: 7px;
}
.entity-candidates-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin-bottom: 10px;
  font-size: 14px;
}
.entity-candidates-head .muted { font-size: 13px; }
.entity-candidate {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 9px 0;
  border-top: 1px solid #D8E5EF;
}
.entity-candidate-main { flex: 1 1 300px; font-size: 13px; }
.entity-candidate-main .muted { display: block; font-size: 12px; margin-top: 2px; }
.entity-candidate-actions { display: flex; gap: 6px; }

@media (max-width: 640px) {
  .entity-candidate-actions { width: 100%; }
  .entity-candidate-actions .btn-tiny { flex: 1; min-height: 40px; }
}

/* ── Table pagination ────────────────────────────────────────────────────── */
/* Shown above and below: a reader who has just scrolled a full table should not
   have to scroll back up to move on. Hidden entirely when everything fits,
   because "Page 1 of 1" is furniture that tells the reader nothing. */
.pager {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 8px 0;
}
.pager .btn-tiny[disabled] {
  opacity: 0.4;
  cursor: default;
}
@media (max-width: 480px) {
  .pager { justify-content: space-between; }
  .pager .btn-tiny { min-height: 40px; padding: 8px 14px; }
}

/* ── Activity log ────────────────────────────────────────────────────────── */
/* An audit log is only useful if you can find one thing in it. Filters sit
   above the table rather than behind a control, because a reviewer arrives
   already knowing what they are looking for. */
.activity-filters {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  align-items: flex-end;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.activity-filters .field { margin: 0; min-width: 140px; }
.activity-filters label {
  display: block;
  font-size: 12px;
  color: var(--text-muted);
  margin-bottom: 3px;
}
.activity-shortcuts {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  padding: 10px 0 0;
}
/* Marks the events a peer reviewer or a client actually asks about -- identity
   reveals and document access. Knowing which action names those are is our job,
   not the reader's. */
.audit-sensitive {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  background: #FDF0EE;
  color: #7A3A30;
  border: 1px solid #E8C4BE;
  border-radius: 3px;
  padding: 1px 5px;
  margin-right: 4px;
}

@media (max-width: 640px) {
  .activity-filters .field { flex: 1 1 100%; }
  .activity-shortcuts .btn-tiny { flex: 1; min-height: 40px; }
}

/* Why a document was sent back.
   Without the reason a client re-sends the same file -- they cannot see the
   problem from their side, and "we need a different one" with no explanation
   is how one exchange becomes three. */
.portal-rejected-note {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 6px;
  padding: 9px 12px;
  background: #FDF6EC;
  border: 1px solid #E8D5B5;
  border-left: 3px solid #C08A2E;
  border-radius: 5px;
  font-size: 14px;
  color: #6B4F1D;
}

/* Documents uploaded and waiting to be placed.
   The row previously read "0/26" whether a client had sent nothing or
   seventeen documents. Those call for opposite actions -- chase them, or go
   and file what arrived -- and the list gave no way to tell them apart. */
.review-waiting {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 7px;
  font-size: 11px;
  font-weight: 600;
  border-radius: 10px;
  background: #FDF6EC;
  color: #6B4F1D;
  border: 1px solid #E8D5B5;
  text-decoration: none;
  white-space: nowrap;
}
.review-waiting:hover { background: #F8EDD9; }

/* ── Feedback, where people look for it ──────────────────────────────────── */
/* The link sat beside "Log out" among four others and was read past. Somebody
   who cannot find how to report a problem does not report it, and a bug we
   never hear about is one we never fix.
   Bottom-right is where people have been trained to look. */
.feedback-fab {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 90;              /* under modals (100+), over page content */
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 9px 16px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  color: #FFFFFF;
  background: var(--accent, #1B3A5C);
  border: none;
  border-radius: 22px;
  cursor: pointer;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.18);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.feedback-fab:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.22);
}
.feedback-fab:active { transform: translateY(0); }

/* On a phone the label costs more room than it earns; the icon carries it. */
@media (max-width: 640px) {
  .feedback-fab {
    right: 14px;
    bottom: 14px;
    padding: 11px 13px;
    font-size: 0;             /* hides the word, keeps the icon */
    gap: 0;
  }
  .feedback-fab span { font-size: 17px; }
}

/* Printing a workbook or a checklist should not print a button. */
@media print { .feedback-fab { display: none; } }

/* ══════════════════════════════════════════════════════════════════════════
   PHONES
   ══════════════════════════════════════════════════════════════════════════

   The staff screens were built for a desk and never adjusted below 640px.
   Nine breakpoints existed, all for individual components; nothing addressed
   the page itself.

   The visible symptom is a page that scrolls sideways: a thirteen-column
   client table cannot fit a 390px screen, so it pushes the whole document
   wider than the viewport and every heading, card and button sits offset from
   the left edge. It reads as "the alignment is broken", and the alignment is
   fine — one element is simply wider than the phone. */

@media (max-width: 640px) {

  /* 24px of padding each side costs 12% of a phone's width and buys nothing. */
  .container {
    margin: 16px auto;
    padding: 0 12px;
    max-width: 100%;
  }

  /* Wide tables scroll WITHIN their own box instead of dragging the page.
     One element being too wide is a scrollbar; the page being too wide is a
     broken layout. */
  .data-table {
    display: block;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    white-space: nowrap;
    /* NO MASK HERE.
       A fade on the right edge showed there was more to scroll -- and the
       actions column is the last one, so the kebab button sat inside the fade:
       greyed out, and unreliable to tap. An affordance that disables the
       control it points at is worse than none.
       The scrollbar and the cut-off column do the same job honestly. */
  }
  .data-table th, .data-table td { padding: 8px 10px; }

  /* Nothing may be wider than the screen. This is the rule that stops one
     stray element offsetting an entire page. */
  body { overflow-x: hidden; }
  img, pre, code { max-width: 100%; }

  /* The nav is nine tabs; it wraps into a wall. Scroll it instead. */
  .topbar {
    flex-wrap: wrap;
    padding: 10px 12px;
    gap: 8px;
  }
  .tabs {
    order: 3;
    width: 100%;
    overflow-x: auto;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    padding-bottom: 4px;
  }
  .tab-btn { white-space: nowrap; flex: 0 0 auto; }

  /* The user area beside the brand, not stacked under it. */
  .user-area { font-size: 12px; margin-left: auto; }

  /* Cards and forms use the full width; a centred column wastes a phone. */
  .card { padding: 14px; margin-bottom: 12px; }
  .field-row { flex-direction: column; gap: 0; }
  .field input, .field select, .field textarea { width: 100%; }

  /* Touch targets. 44px is the size a thumb actually hits. */
  .btn-small, .btn-tiny { min-height: 38px; padding: 8px 12px; }

  /* Headings sized for a desk are too big here. */
  h1 { font-size: 24px; }
  h2 { font-size: 19px; }
  .page-header { flex-direction: column; align-items: flex-start; gap: 8px; }
}

/* Narrow phones. Below this the tab row and table padding need a little more. */
@media (max-width: 400px) {
  .container { padding: 0 10px; }
  .data-table th, .data-table td { padding: 7px 8px; font-size: 13px; }
  .tab-btn { padding: 6px 10px; font-size: 13px; }
}

/* ══════════════════════════════════════════════════════════════════════════
   LANDING PAGE
   ══════════════════════════════════════════════════════════════════════════

   Six full-width cards stacked down a 1100px column: the sides sat empty, the
   page ran long, and the first thing anyone did was scroll past the thing they
   came for. The two routes a visitor actually needs -- find my link, or ask
   about Ledgerly -- now sit side by side. */

.landing-hero {
  padding: 44px 8px 32px;
  text-align: center;
}
.landing-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 18px;
}
.landing-title {
  font-family: "Fraunces", Cambria, Georgia, serif;
  font-size: 38px;
  line-height: 1.2;
  font-weight: 600;
  color: var(--text);
  margin: 0 0 14px;
}
.landing-sub {
  max-width: 620px;
  margin: 0 auto;
  font-size: 16px;
  line-height: 1.55;
  color: var(--text-muted);
}

/* Equal columns, and equal HEIGHT — two cards of different lengths beside each
   other look like one is broken. */
.landing-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  align-items: stretch;
  margin-bottom: 20px;
}
.landing-grid > .card { margin-bottom: 0; }

@media (max-width: 760px) {
  .landing-hero { padding: 28px 4px 22px; }
  .landing-title { font-size: 27px; }
  .landing-sub { font-size: 15px; }
  /* One column. Two 300px cards side by side is worse than one readable one. */
  .landing-grid { grid-template-columns: 1fr; gap: 14px; }
}


/* The actions column, on a phone.
   It is the last column of a table that now scrolls, so on a narrow screen it
   is the furthest thing from the reader -- and it is the column they most often
   want. Pinned to the right edge so it is always in reach. */
@media (max-width: 640px) {
  .data-table td:last-child,
  .data-table th:last-child {
    position: sticky;
    right: 0;
    background: var(--card, #FFFFFF);
    /* A shadow rather than a border, so it reads as "floating above the
       scrolling content" instead of as a column divider. */
    box-shadow: -4px 0 6px -4px rgba(0, 0, 0, 0.18);
  }
  .data-table tr.row-zebra td:last-child { background: var(--bg, #F7F7F2); }

  /* Big enough to hit with a thumb. 32px is a mouse target. */
  .kebab-btn { width: 40px; height: 40px; font-size: 18px; }
}

/* ── What a client's returns show about their structure ──────────────────── */
/* Quiet on purpose. A note, not a queue: a firm importing a hundred clients
   should get something to read, not eighty proposals to dismiss — because a
   wall of proposals gets cleared wholesale and the useful ones go with it. */
.entity-notes {
  margin: 14px 0;
  padding: 12px 14px;
  background: var(--card, #FFFFFF);
  border: 1px solid var(--border, #E5E5DF);
  border-radius: 8px;
}
.entity-notes-head { margin-bottom: 10px; }
.entity-notes-head strong { font-size: 14px; }
.entity-notes-head .muted { display: block; font-size: 12px; margin-top: 3px; }

.entity-note {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-top: 1px solid var(--border, #E5E5DF);
  font-size: 13px;
}
.entity-note .muted { font-size: 12px; }
.entity-note strong { margin-right: 8px; }

/* A disposal changes what goes on this year's return, so it does not look like
   the rest of the list. */
.entity-note-gone {
  background: #FDF6EC;
  margin: 0 -14px;
  padding: 10px 14px;
  border-top: 1px solid #E8D5B5;
}
.entity-note-gone strong { color: #6B4F1D; }
.entity-note-gone .muted { color: #6B4F1D; }

@media (max-width: 640px) {
  .entity-note { flex-direction: column; align-items: flex-start; gap: 6px; }
}

/* ── An access by Ledgerly, in a firm's own log ──────────────────────────── */
/* This is the row that must not look like the firm's own work. A supplier's
   access sitting in the same grey as a colleague's reminder is a row nobody
   notices — and "you can check any time" becomes a claim they cannot act on. */
tr.audit-operator {
  background: #FDF0F0;
  border-left: 3px solid #A03028;
}
tr.audit-operator td { font-weight: 500; }
.audit-operator-tag {
  display: inline-block;
  padding: 1px 6px;
  margin-right: 6px;
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #FFFFFF;
  background: #A03028;
  border-radius: 3px;
}

/* ── Something worth a look ──────────────────────────────────────────────── */
/* Appears only when access to a firm's documents has been unusual. Silent the
   rest of the time, which is what gives it weight — a panel that is always
   there is a panel nobody reads. */
.card.unusual-access {
  border-left: 3px solid #A03028;
  background: #FDF6F5;
}
.card.unusual-access h2 { color: #A03028; }
.unusual-row {
  padding: 8px 0;
  border-top: 1px solid #EFDCDA;
  font-size: 13px;
}
.unusual-row:first-of-type { border-top: none; }
.unusual-row .muted { font-size: 12px; margin-top: 2px; }

/* The quiet line. Deliberately understated — it is read past on 364 days, and
   its job is to make the 365th meaningful by proving something was watching. */
.access-quiet {
  font-size: 12.5px;
  padding: 10px 2px;
  margin: 4px 0 12px;
  border-top: 1px solid var(--border, #E5E5DF);
}

/* ── The download gate, in Firm Settings ─────────────────────────────────── */
.gate-row {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; padding: 11px 0; border-top: 1px solid var(--border, #E5E5DF);
  font-size: 13px;
}
.gate-row:first-of-type { border-top: none; }
.gate-row .muted { display: block; font-size: 12px; margin-top: 2px; max-width: 46em; }
.gate-warn {
  margin-top: 6px; padding: 7px 10px; font-size: 12px;
  background: #FDF6EC; color: #6B4F1D; border-radius: 5px;
}
@media (max-width: 640px) { .gate-row { flex-direction: column; gap: 8px; } }

/* ── A deleted client, still in the list ─────────────────────────────────── */
/* Marked rather than hidden, with the undo beside it. A separate "recently
   deleted" screen is one somebody has to know exists; this puts the undo where
   the mistake happened. */
tr.client-deleted { background: #FAF7F6; color: var(--muted, #5C5C55); }
tr.client-deleted s { opacity: 0.75; }
tr.client-deleted .muted { font-size: 12px; margin-right: 8px; }
.deleted-mark {
  display: inline-block; padding: 1px 6px; margin-right: 7px;
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  color: #FFFFFF; background: #A03028; border-radius: 3px;
}

/* A client who has LEFT is not deleted — their history is intact and the row
   still works. Dimmed, not struck through. */
tr.client-closed { opacity: 0.62; }

/* The authenticator setup code. Shown once, removed as soon as it is used —
   a setup key left on screen is a key the next person past the desk can
   photograph. */
.gate-qr { text-align: center; background: #FFFFFF; }
.gate-qr img { display: block; margin: 8px auto; }
.gate-qr code { font-size: 13px; letter-spacing: 0.06em; }

/* Asking for the gate code, in the page. Not prompt(), which blocks the browser
   before it repaints — that is how the enrolment QR was generated correctly and
   never drawn. */
.gate-ask-backdrop {
  position: fixed; inset: 0; z-index: 9000;
  background: rgba(26,26,24,0.42);
  display: flex; align-items: center; justify-content: center;
}
.gate-ask {
  background: var(--card, #FFFFFF); padding: 22px 26px; border-radius: 10px;
  max-width: 340px; box-shadow: 0 8px 28px rgba(0,0,0,0.18); text-align: center;
}
.gate-ask h3 { margin: 0 0 6px; font-size: 16px; }
.gate-ask .muted { font-size: 12.5px; }
.gate-ask input {
  margin-top: 12px; font-size: 20px; letter-spacing: 0.28em;
  width: 150px; text-align: center; padding: 7px;
}

/* ── The access alert, floating ──────────────────────────────────────────── */
/* The quiet line stays in the page: something permanently on top becomes
   invisible within a week, which is why the old recent-activity list was not
   being read.

   The ALERT is rare, means somebody should look now, and scrolling past it is
   the failure to prevent. So it floats and needs a click to go away. */
.floating-alert {
  position: sticky; top: 12px; z-index: 500;
  box-shadow: 0 6px 20px rgba(160, 48, 40, 0.16);
}
.alert-dismiss {
  position: absolute; top: 8px; right: 10px;
  background: none; border: none; cursor: pointer;
  font-size: 20px; line-height: 1; color: #A03028; padding: 2px 6px;
}
.alert-dismiss:hover { opacity: 0.7; }

/* An intake that did not match its invitation. Not an error — usually a spouse
   filling the form in — so it asks for a look rather than shouting. */
.check-mark {
  display: inline-block; padding: 1px 6px; margin-right: 7px; cursor: pointer;
  font-size: 10.5px; color: #6B4F1D; background: #FDF6EC;
  border: 1px solid #E8D5B5; border-radius: 3px;
}
.check-mark:hover { background: #F8ECD8; }

/* Engagement letter status on the engagement page. */
.letter-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; padding: 9px 0; font-size: 13px;
  border-top: 1px solid var(--border, #E5E5DF);
}
.letter-row:first-child { border-top: none; }

/* ── A firm's public page ─────────────────────────────────────────────────── */
/* Read by three people: a client who has lost their link, somebody checking the
   firm out, and somebody wondering whether the email they just received is
   genuine. Set for reading rather than scanning — this is prose, not data. */

.firm-tagline {
  font-family: var(--font-display, Georgia, serif);
  font-size: 19px; line-height: 1.5; margin: 0 0 28px;
  max-width: 32rem; margin-left: auto; margin-right: auto;
}

.firm-block {
  text-align: left; max-width: 34rem; margin: 0 auto 26px;
  padding-bottom: 22px; border-bottom: 1px solid var(--border, #E5E5DF);
}
.firm-block:last-child { border-bottom: none; }
.firm-block h2 {
  font-family: var(--font-display, Georgia, serif);
  font-size: 16px; margin: 0 0 8px;
}
.firm-block p { margin: 0 0 10px; font-size: 14.5px; line-height: 1.65; }
.firm-block p:last-child { margin-bottom: 0; }

.firm-services { margin: 0; padding-left: 18px; font-size: 14.5px; line-height: 1.9; }

/* The person. A CPA is chosen on trust in a human being, so the photograph and
   the name lead rather than sitting under a heading about the practice. */
.firm-person { display: flex; gap: 18px; align-items: flex-start; }
.firm-person img {
  width: 84px; height: 84px; border-radius: 50%; object-fit: cover; flex: none;
}
.firm-role { color: var(--muted, #5C5C55); font-size: 13px; margin-top: -4px !important; }

/* Facts a stranger can check: a licence number, a year, an address. */
.firm-facts { display: flex; flex-wrap: wrap; gap: 6px 18px; font-size: 13px;
              color: var(--muted, #5C5C55); }

/* The note for somebody wondering whether an email is genuine. Quiet — it is
   reassurance, not a warning, and a red box would create the alarm it exists to
   settle. */
.firm-notice { background: #FAF9F5; border: 1px solid var(--border, #E5E5DF);
               border-radius: 6px; padding: 18px 20px; }
.firm-notice h2 { font-size: 15px; }
.firm-notice p { font-size: 13.5px; }

@media (max-width: 34rem) {
  .firm-person { flex-direction: column; }
  .firm-person img { width: 64px; height: 64px; }
}

/* ── Reports ──────────────────────────────────────────────────────────────── */
/* The report centre uses the ordinary card and data-table classes — an earlier
   version invented its own tab strip, which is how a product starts feeling
   assembled rather than designed. Only these three rules are specific. */
.report-desc { font-size: 12.5px; margin-top: 3px; max-width: 46em; line-height: 1.5; }
.report-action { text-align: right; white-space: nowrap; vertical-align: top; }
.report-unbuilt { opacity: 0.55; }
/* A shovel rather than "Coming". Says somebody is digging, which is the
   difference between a product being built and a product being broken. */
.report-soon {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; color: #B5721C;
  padding: 2px 8px; background: #FDF6EC;
  border: 1px solid #E8D5B5; border-radius: 3px;
}
.report-soon svg { flex: none; opacity: 0.85; }

/* A firm's own tagline as the headline. Set slightly smaller than our own
   marketing line, because a sentence somebody wrote about their practice reads
   differently from a product headline — and at 44px it looks like advertising
   rather than like their firm. */
.landing-title.firm-led {
  font-size: clamp(24px, 4vw, 34px);
  line-height: 1.25;
  max-width: 22em;
  margin-left: auto;
  margin-right: auto;
}

/* ── Sections a firm has finished with ───────────────────────────────────── */
/* The heading is the control — no separate chevron to aim at, which matters
   most on a phone. */

.section-toggle {
  cursor: pointer; user-select: none;
  display: flex; align-items: baseline; gap: 10px;
}
.section-toggle::before {
  content: "▾"; font-size: 11px; color: var(--muted, #5C5C55);
  transition: transform 160ms ease; flex: none;
}
.folded .section-toggle::before { transform: rotate(-90deg); }
.section-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* A folded section still says what is in it. One that shows only its title
   makes somebody open it to find out whether they need to — the same work as
   scrolling past it, plus a click. */
.section-summary {
  font-family: var(--font-body); font-size: 12.5px; font-weight: 400;
  color: var(--muted, #5C5C55);
}
.folded .section-body { display: none; }

/* A panel with nothing in it. Not a preference — a fact about the data. */
.folded-empty { opacity: 0.6; }
.folded-empty .empty-note { font-size: 12.5px; margin: 4px 0 0; }

/* ── The engagement letter, where it matters ─────────────────────────────── */
/* Of tax claims against CPA firms in 2024, roughly 55% had no engagement letter
   for the service being claimed about. A grey line saying "none sent" is the
   same administrative burden that produced that number. */

.letter-prompt {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 14px; margin-top: 10px; padding: 11px 13px;
  background: #FBF9F4; border: 1px solid #E8DFC9; border-radius: 5px;
  font-size: 13px;
}
.letter-prompt .muted { display: block; font-size: 12px; margin-top: 2px; max-width: 44em; }
.letter-prompt code { font-size: 11.5px; word-break: break-all; }

.letter-missing {
  padding: 12px 14px; background: #FDF6EC;
  border-left: 3px solid #B5721C; border-radius: 4px;
}
.letter-missing p { font-size: 12.5px; margin: 4px 0 9px; max-width: 46em; }

/* A client waiting on an answer. Not "has tickets" — the useful signal is that
   somebody has been waiting since Tuesday, so it goes amber after two days. */
.waiting-mark {
  display: inline-block; padding: 1px 6px; margin-right: 7px; cursor: pointer;
  font-size: 10.5px; color: #2C5F2D; background: #F1F6F1;
  border: 1px solid #CFE0CF; border-radius: 3px;
}
.waiting-mark.overdue { color: #6B4F1D; background: #FDF6EC; border-color: #E8D5B5; }
.waiting-mark:hover { filter: brightness(0.97); }

/* New enquiries, on the Clients page. Shown only when there are some — a button
   that is always there and usually means nothing is a button people stop
   seeing. */
.has-enquiries {
  border-color: #2C5F2D !important;
  color: #2C5F2D !important;
}

/* A client waiting on an answer. Not a badge saying "has tickets" — that is
   decoration. The useful signal is that somebody is waiting, and for how long. */
.waiting-mark {
  display: inline-block; padding: 1px 6px; margin-right: 7px; cursor: pointer;
  font-size: 10.5px; color: #1B4332; background: #EEF4EF;
  border: 1px solid #CFE0D4; border-radius: 3px;
}
.waiting-mark:hover { background: #E3EDE6; }
/* Past two days. A question asked this morning is normal; one from Tuesday is
   somebody wondering whether anybody read it.
   The existing mark uses .overdue, not a second class name of mine. */
.waiting-mark.overdue { color: #B5721C; background: #FDF6EC; border-color: #E8D5B5; }

/* ── Marks on a client's row ──────────────────────────────────────────────── */
/* Deleted, Check and "2 waiting" were sitting inline with the name, so a client
   with two of them pushed the name off the visible width — and the name is what
   somebody is scanning for.

   The marks sit above it instead: still noticed, no longer in the way. */
.client-marks { display: block; margin-bottom: 2px; line-height: 1.4; }
.client-marks:empty { display: none; }
.client-marks > span { margin-right: 5px; }

/* ── The engagement letter, in Firm Settings ─────────────────────────────── */
/* A firm's letter already has blanks in it — "Dear ________". Finding those
   means they do not have to learn our placeholder syntax to use their own
   letter. Offered, never applied: it is a legal document. */
.letter-blanks {
  background: #FBF9F4; border: 1px solid #E8DFC9; border-radius: 5px;
  padding: 12px 14px; margin: 10px 0; font-size: 13px;
}
.blank-row { display: flex; gap: 8px; align-items: baseline; margin: 7px 0; }
.blank-row input { width: auto; }
.blank-row code { font-size: 12px; }

.letter-preview-frame {
  margin-top: 14px; padding: 16px 18px;
  background: #FCFBF9; border: 1px solid var(--border, #E5E5DF); border-radius: 5px;
}
.letter-preview-text {
  white-space: pre-wrap; font-size: 13.5px; line-height: 1.7;
  max-height: 40vh; overflow-y: auto;
}

/* ── Reading a letter before sending it ──────────────────────────────────── */
/* Sending was immediate: press a button and a legal document goes to a named
   person. The text is the content of this dialog, not a detail in it. */
.letter-confirm {
  background: var(--card, #FFFFFF); border-radius: 8px;
  padding: 22px 24px; max-width: 560px; width: calc(100% - 32px);
  box-shadow: 0 8px 30px rgba(0,0,0,0.2);
}
.letter-confirm h3 {
  font-family: var(--font-display, Georgia, serif);
  font-size: 17px; margin: 0 0 4px;
}
.letter-confirm .muted { font-size: 12.5px; margin: 0 0 14px; }
.letter-confirm-text {
  white-space: pre-wrap; font-size: 13.5px; line-height: 1.7;
  max-height: 46vh; overflow-y: auto;
  background: var(--paper, #FCFBF9); border: 1px solid var(--border, #E5E5DF);
  border-radius: 4px; padding: 16px 18px;
}
.letter-confirm-actions {
  display: flex; gap: 10px; align-items: center; margin-top: 16px;
}

/* ── The engagement letter box ────────────────────────────────────────────── */
/* A letter is a page of prose. The box was as wide as a form field on a screen
   with room for a document, so somebody editing one scrolled a narrow column
   for no reason. */
#letter-body {
  width: 100%;
  min-height: 22rem;
  font-family: var(--font-body);
  font-size: 13.5px;
  line-height: 1.65;
  resize: vertical;
  /* Full width up to a comfortable measure. Past about 100 characters a line
     is hard to track back from, which is worse than a narrow box. */
  max-width: 62rem;
}
#letter-body:focus { min-height: 32rem; }

.letter-preview-frame { max-width: 62rem; }

/* Said at the top of Reports, not discovered report by report. A firm finding
   half the list greyed out concludes the product is unfinished; a firm told
   first knows what they are looking at. */
.reports-note {
  margin: 12px 0 0; padding: 11px 14px;
  background: #FBF9F4; border: 1px solid #E8DFC9; border-radius: 5px;
  font-size: 13px; line-height: 1.6; max-width: 52em;
}

/* ── The entities a batch of returns named ────────────────────────────────
   Shown once, at the end of an import. Grouped by the client whose return
   named them, because that is how a CPA decides: "do we do Margaret's
   rentals?" rather than "do we do this LLC?".

   Deliberately plain. This is a list to read and tick, not a feature to
   admire, and a firm may be working through forty of them. */

.observed-list {
  margin-top: 12px;
  max-height: 420px;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 6px;
}

.observed-client {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.observed-client:last-child { border-bottom: none; }

.observed-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 0 6px 14px;
  font-size: 13.5px;
  cursor: pointer;
}

/* The tick is a tick. The global input rule gives everything a border and
   full width, which turned checkboxes into text fields across four pages
   before it was scoped — worth being explicit here rather than relying on
   that scoping holding. */
.observed-row input[type="checkbox"] {
  width: auto;
  margin-top: 2px;
  flex: none;
}

.observed-row:hover { background: var(--gold-bg); }

/* A row that cannot be ticked is already a client. Dimmed rather than
   hidden, because "it is already here" is the answer to a question the
   firm would otherwise ask. */
.observed-row input:disabled + span { opacity: 0.62; }

/* ── A chosen file, and the × that takes it back off ──────────────────────
   Every file input was one-way: choose the wrong file and the only escape
   was reloading the page and losing whatever else was typed.

   Deliberately quiet. This sits under an input on eleven screens and should
   read as part of the field rather than as a control competing with it. */

.file-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.file-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 9px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--gold-bg);
  font-size: 12.5px;
  max-width: 100%;
}

/* A long filename truncates rather than pushing the × off the row, because
   the × is the point of the chip. */
.file-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}

.file-chip-x {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0;
  color: var(--muted);
  /* 24px, not the 12px the glyph wants. A dismiss control smaller than a
     fingertip is one somebody stabs at three times on a phone. */
  min-width: 24px;
  min-height: 24px;
  border-radius: 50%;
}

.file-chip-x:hover {
  background: var(--border);
  color: var(--text);
}

/* ── The payer, corrected in place ────────────────────────────────────────
   The label has been renameable for a long time; the detail was not — and
   the detail is the part that comes from reading the return. "Robinhood",
   "TD Ameritrade", a lender's name: exactly what a reader gets slightly
   wrong, and exactly what a client needs to be right before they go looking.

   Styled like the label so it reads as the same gesture, not a new one. */

.editable-detail {
  cursor: pointer;
  border-bottom: 1px dashed transparent;
}

.editable-detail:hover {
  border-bottom-color: var(--muted);
}

/* The pencil stays out of the way until the row is hovered, so a checklist
   of thirty items is not thirty pencils. */
.editable-detail .rename-icon {
  opacity: 0;
  margin-left: 4px;
  font-size: 11px;
  transition: opacity 0.12s;
}

.editable-detail:hover .rename-icon {
  opacity: 0.55;
}

/* ── The notice bar ───────────────────────────────────────────────────────
   Every failure here used to be caught, logged, and hidden — the request
   carried on and returned a cheerful message having quietly done less than
   it said. A database constraint failed for months into a log line nobody
   read, and the first sign was a client created with no entities under it.

   A bar rather than a toast, because the whole problem is silence and a
   message that fades in four seconds is one somebody missed. */

.notice-bar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 14px;
}

.notice-bar:empty {
  margin-bottom: 0;
}

.notice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 6px;
  border-left: 3px solid;
  font-size: 13.5px;
  line-height: 1.45;
}

.notice-text {
  flex: 1;
}

.notice-warning {
  background: var(--gold-bg);
  border-left-color: var(--gold);
  color: var(--text);
}

.notice-error {
  background: var(--danger-bg, #fdf2f2);
  border-left-color: var(--danger-text);
  color: var(--text);
}

.notice-success {
  background: var(--positive-bg, #f2fbf5);
  border-left-color: var(--positive-text);
  color: var(--text);
}

.notice-close {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 17px;
  line-height: 1;
  padding: 0;
  color: var(--muted);
  /* 24px, not the size of the glyph. A dismiss control smaller than a
     fingertip is one somebody stabs at three times on a phone. */
  min-width: 24px;
  min-height: 24px;
  border-radius: 50%;
}

.notice-close:hover {
  background: var(--border);
  color: var(--text);
}

/* ── A chosen file, and the × that takes it back off ──────────────────────
   Every file input was one-way: choose the wrong file and the only escape
   was reloading the page and losing whatever else was typed. */

.file-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }

.file-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 4px 3px 9px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--gold-bg);
  font-size: 12.5px;
  max-width: 100%;
}

/* A long filename truncates rather than pushing the × off the row, because
   the × is the point of the chip. */
.file-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 220px;
}

.file-chip-x {
  border: none;
  background: transparent;
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0;
  color: var(--muted);
  /* 24px, not the size of the glyph — a dismiss control smaller than a
     fingertip is one somebody stabs at three times on a phone. */
  min-width: 24px;
  min-height: 24px;
  border-radius: 50%;
}

.file-chip-x:hover { background: var(--border); color: var(--text); }

/* ── Still being built ────────────────────────────────────────────────────
   Creating a client from a prior return takes half a minute: the record
   appears at the start, their businesses at the end. In between, a client
   with nothing under them looks exactly like one that went wrong — a firm
   refreshed ten times before anybody worked out it was still running.

   Deliberately quiet. This is reassurance, not an alarm. */

.processing-badge {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 10px;
  background: var(--gold-bg);
  border: 1px solid var(--gold);
  color: var(--muted);
  font-size: 11px;
  font-weight: 500;
  vertical-align: middle;
  animation: breathing 1.8s ease-in-out infinite;
}

@keyframes breathing {
  50% { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
  .processing-badge { animation: none; }
}

/* ── Toasts: what is true only now ────────────────────────────────────────
   The bar is for something a person must act on, and stays. A toast is for
   something happening now — reading, saving, saved — and goes.

   Bottom right, above everything, and it never moves the page. */

.toast-tray {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 16px;
  border-radius: 8px;
  background: var(--ink, #1a1a1a);
  color: #fff;
  font-size: 13.5px;
  line-height: 1.4;
  max-width: 380px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  animation: toast-in 0.22s ease-out;
  pointer-events: auto;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
}

.toast-success { background: var(--positive-text, #1e6b3a); }
.toast-working  { background: var(--ink, #1a1a1a); }

/* A quiet spinner. Something must move, or a firm cannot tell a toast that
   is waiting from one that has stopped. */
.toast-spinner {
  width: 13px;
  height: 13px;
  border: 2px solid rgba(255, 255, 255, 0.28);
  border-top-color: #fff;
  border-radius: 50%;
  animation: toast-spin 0.8s linear infinite;
  flex-shrink: 0;
}

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

@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
  .toast-spinner { animation-duration: 2.4s; }
}

@media (max-width: 560px) {
  .toast-tray { left: 14px; right: 14px; bottom: 14px; align-items: stretch; }
  .toast { max-width: none; }
}

/* ── Who prepares this entity's return ────────────────────────────────────
   Reading a return creates the businesses it names, and whether the firm
   prepares each one's return is a question only they can answer. The
   document answers it most of the time — a K-1 means somebody else did —
   and this is for the handful it cannot. */

.prepare-ask {
  border: 1px dashed var(--gold);
  background: var(--gold-bg);
  color: var(--muted);
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 10px;
  cursor: pointer;
  vertical-align: middle;
}

.prepare-ask:hover { background: var(--gold); color: #fff; border-style: solid; }

.prepare-yes {
  display: inline-block;
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 10px;
  background: var(--positive-bg, #f2fbf5);
  border: 1px solid var(--positive-text, #1e6b3a);
  color: var(--positive-text, #1e6b3a);
  vertical-align: middle;
}

/* An entity somebody else prepares. Quiet — it is settled, not a task — but
   present, because a firm takes work over and an answer nobody can see is
   an answer nobody can revise. */
.prepare-no {
  display: inline-block;
  font-size: 11px;
  padding: 1px 7px;
  border-radius: 10px;
  background: var(--border);
  color: var(--muted);
  cursor: pointer;
  vertical-align: middle;
}

.prepare-no:hover { color: var(--text); }
.prepare-yes { cursor: pointer; }

/* A button doing something. Grey, unpressable, and saying what it is doing —
   which is feedback a firm cannot miss and protection against a second
   press at the same time. */
button.is-working {
  opacity: 0.65;
  cursor: wait;
  position: relative;
}

/* ── Is this client already on the list? ──────────────────────────────────
   Three answers that escalate, and must not look alike: a name match is a
   list to recognise or ignore, a phone match is a note, and both together
   is a warning.

   A duplicate client is two engagements, two checklists, documents split
   across both, and a K-1 chased twice — and it surfaces in March. The
   cheapest moment to catch it is while the name is being typed. */

.duplicate-check { margin-top: 5px; }

.dup {
  padding: 7px 10px;
  border-radius: 5px;
  font-size: 12.5px;
  line-height: 1.5;
  border-left: 3px solid;
}

.dup a { color: inherit; text-decoration: underline; }

/* Both matched. The strongest thing said here, and still not a block. */
.dup-both {
  background: var(--gold-bg);
  border-left-color: var(--gold);
}

/* The number is known. Often legitimate. */
.dup-phone {
  background: var(--gold-bg);
  border-left-color: var(--muted);
}

/* Similar names. Not a warning — two Smiths are two Smiths. */
.dup-name {
  background: transparent;
  border-left-color: var(--border);
  padding-left: 9px;
}

.dup-row { margin-top: 2px; }

/* ── Bulk import: the answer sits under the row it is about ────────────────
   The preview used to group rows into separate blocks, so a firm reading
   their own spreadsheet had to find row 47 in a list somewhere else on the
   page. These pair a flagged row with the notice directly beneath it, and
   the pair has to read as one thing - hence no border between them. */
.onb-flagged > td { border-bottom: none; }

/* A row that will not create a client. Dimmed rather than struck through:
   the row is not wrong, it is already done. */
.onb-blocked > td:not(:last-child) { opacity: 0.62; }

.onb-notice > td {
  padding-top: 0;
  padding-bottom: 8px;
  font-size: 12.5px;
  line-height: 1.45;
}

/* Unusable: no name or no phone. Not a match at all, so it takes the plain
   border rather than the gold a real collision gets. */
.dup-bad {
  background: transparent;
  border-left-color: var(--border);
  padding-left: 9px;
}

/* ── One client picker, used everywhere ───────────────────────────────────
   Four screens had their own, and fixing one made another wrong. The create
   forms want people only — an entity cannot be the answer to "is this person
   already here" — and the search boxes want everything, because a CPA
   remembers the property and not always whose it is. */

.typeahead-row {
  padding: 6px 10px;
  cursor: pointer;
  display: flex;
  justify-content: space-between;
  gap: 12px;
  align-items: baseline;
  font-size: 13px;
}

.typeahead-row:hover,
.typeahead-row.is-cursor { background: var(--gold-bg); }

.typeahead-more {
  padding: 5px 10px;
  font-size: 12px;
  border-top: 1px solid var(--border);
}

/* A note about what was typed. Advice — the field still saves. */
.field-note {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted);
  border-left: 2px solid var(--gold);
  padding-left: 8px;
}

/* ── ON A PHONE, A ROW IS A CARD ──────────────────────────────────────────
   A beta user said the phone version was not usable. It was not a styling
   problem: the Clients table has twelve columns, and the mobile approach was
   to keep all twelve and scroll sideways, with the actions column pinned.

   Scrolling sideways through twelve columns to find a client is not usable
   however well it is styled. A phone shows ONE client at a time, with the
   three or four things somebody actually needs, and everything else on the
   client's own page where there is room for it.

   WHAT A CPA NEEDS FROM THIS LIST ON A PHONE
       who it is          the name, big enough to tap
       where they are up to    the progress
       who has it         assigned
       and the actions

   Email, state, created, alias, paused and auto-renew are all real and none
   is why somebody opens this list on a phone. */

@media (max-width: 640px) {

  /* The table stops being a table. */
  #clients-table,
  #clients-table tbody,
  #clients-table tr,
  #clients-table td {
    display: block;
    width: auto;
  }

  #clients-table thead { display: none; }

  #clients-table tr {
    background: var(--card, #fff);
    border: 1px solid var(--border, #e5e5e0);
    border-radius: 10px;
    padding: 12px 14px;
    margin-bottom: 10px;
    position: relative;
  }

  /* Undo the sideways-scroll approach that this replaces. */
  #clients-table td:last-child,
  #clients-table th:last-child {
    position: static;
    box-shadow: none;
  }

  #clients-table td {
    border: none;
    padding: 2px 0;
    text-align: left !important;
    white-space: normal;
  }

  /* ── WHAT SURVIVES ──────────────────────────────────────────────────────
     Name, progress, assigned, actions. The rest is on the client's page.

     Hidden rather than removed from the markup: the same row renders on a
     desktop, and a second template for phones is a second thing to keep
     correct. */
  #clients-table td:nth-child(1),   /* pin      */
  #clients-table td:nth-child(2),   /* alias    */
  #clients-table td:nth-child(4),   /* email    */
  #clients-table td:nth-child(5),   /* phone    */
  #clients-table td:nth-child(6),   /* state    */
  #clients-table td:nth-child(7),   /* created  */
  #clients-table td:nth-child(10),  /* paused   */
  #clients-table td:nth-child(11) { /* renew    */
    display: none;
  }

  /* The name, as the heading it actually is. */
  #clients-table td:nth-child(3) {
    font-size: 16px;
    font-weight: 600;
    line-height: 1.3;
    padding-right: 44px;      /* clear of the actions button */
    margin-bottom: 4px;
  }

  /* Progress and assigned, quiet, on one line each. */
  #clients-table td:nth-child(8),
  #clients-table td:nth-child(9) {
    font-size: 13px;
    color: var(--muted, #6b6b60);
  }

  /* The actions button, top right of the card, and big enough to hit.
     Apple's guidance is 44px and it is the right number: a smaller target on
     a phone is a target somebody misses while holding a coffee. */
  #clients-table td:last-child {
    position: absolute;
    top: 8px;
    right: 8px;
    padding: 0;
  }

  #clients-table .kebab-btn {
    width: 44px;
    height: 44px;
    font-size: 20px;
  }

  /* An entity under a client is indented by the markup itself, which keeps
     the tree legible without this needing to know the class name. A first
     version invented "entity-row"; the rows do not carry it. */

  /* ── EVERY TAP TARGET, EVERYWHERE ──────────────────────────────────────
     Buttons sized for a mouse are the commonest reason a phone feels broken.
     44px is the floor. */
  button, .btn-tiny, .btn-primary, select, input[type="checkbox"] {
    min-height: 44px;
  }

  input[type="checkbox"] { min-width: 22px; min-height: 22px; }

  /* A form field under 16px makes iOS zoom in when it is focused, and then
     the person is looking at half a page and does not know why. */
  input, select, textarea { font-size: 16px; }

  /* Filters stack rather than crowd. */
  /* .filter-row was invented too. .filters is real. */
  .filters { flex-direction: column; align-items: stretch; }
  .filters > * { width: 100%; }
}

/* ── EVERY OTHER LIST, THE SAME WAY ───────────────────────────────────────
   The dashboard has sixteen columns across its widgets, deadlines ten,
   tickets and leads nine each. All were scrolling sideways.

   The general rule rather than a rule per page: any .data-table becomes
   cards, the first cell is the heading, and the last is the actions.

   WHY IT IS GENERAL RATHER THAN PER-PAGE
     Five near-copies of this would drift, and the one that drifts is the one
     nobody notices — usually the page a firm uses least, right up until the
     week they need it. */

@media (max-width: 640px) {

  .data-table:not(#clients-table),
  .data-table:not(#clients-table) tbody,
  .data-table:not(#clients-table) tr,
  .data-table:not(#clients-table) td {
    display: block;
    width: auto;
  }

  .data-table:not(#clients-table) thead { display: none; }

  .data-table:not(#clients-table) tr {
    background: var(--card, #fff);
    border: 1px solid var(--border, #e5e5e0);
    border-radius: 10px;
    padding: 11px 13px;
    margin-bottom: 9px;
  }

  .data-table:not(#clients-table) td {
    border: none;
    padding: 3px 0;
    text-align: left !important;
    white-space: normal;
    font-size: 13.5px;
  }

  /* ── THE HEADING A CARD NEEDS ────────────────────────────────────────────
     Without the column header above it, every line looks the same weight and
     a card is a wall of grey. The first cell is the subject of the row on
     every one of these tables — a client, a deadline, a ticket, a lead — so
     it carries. */
  .data-table:not(#clients-table) td:first-child {
    font-size: 15.5px;
    font-weight: 600;
    color: var(--text, #1a1a1a);
    margin-bottom: 3px;
  }

  /* Anything empty would otherwise leave a blank line in the middle of a
     card, which reads as something missing rather than something absent. */
  .data-table:not(#clients-table) td:empty { display: none; }

  /* A row that says "nothing here" must not become a card. */
  .data-table td[colspan] {
    display: block;
    text-align: center !important;
    font-weight: 400;
    color: var(--muted, #6b6b60);
  }
}

/* ── AND THE THINGS A PHONE ALWAYS GETS WRONG ─────────────────────────────
   None of these is specific to a page. All of them are why a product feels
   broken on a phone rather than merely cramped. */

@media (max-width: 640px) {

  /* A modal that is 600px wide on a 390px screen has its buttons off-screen,
     which looks like the buttons are missing. */
  /* .modal-content does not exist here; .modal and .card do. */
  .modal, .panel, .card {
    max-width: 100%;
    box-sizing: border-box;
  }

  /* A page that scrolls sideways for one wide element makes EVERYTHING feel
     loose, because the whole page moves when somebody swipes a list. */
  body { overflow-x: hidden; }

  /* Long text with nowhere to break — an email, a token, a company name —
     pushes the page wider than the screen. */
  td, .muted, .notice-text { overflow-wrap: anywhere; }

  /* ── SIDE BY SIDE BECOMES STACKED ────────────────────────────────────────
     Two controls side by side on a phone are two controls somebody mis-taps.
     A first version named .button-row and .actions-row, which do not exist —
     the classes here are field-row and form-actions-row. Guessing at a class
     name produces a rule that does nothing and looks like it works. */
  .field-row, .form-actions-row, .new-client-row, .client-link-row {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .field-row > *, .form-actions-row > *,
  .new-client-row > *, .client-link-row > * { width: 100%; }

  /* The notice bar and the toast tray must not sit under a fixed header or
     over the content somebody is reading. */
  .notice-bar { position: static; margin-bottom: 10px; }
}

/* ── THE CLIENT'S OWN SCREEN ──────────────────────────────────────────────
   The portal has no tables, so the card rules above do not apply to it — it
   was already built as cards, which is why a client uploading from a phone
   works better than a CPA reading a client list on one.

   What it still needs is the tap targets, and one honest admission: a
   DROPZONE is a desktop idea. Nobody drags a file on a phone. On a small
   screen it is a large button that opens the picker, and it should look like
   one rather than like an empty box asking for something impossible. */

@media (max-width: 640px) {

  .portal-dropzone {
    padding: 22px 16px;
    border-style: solid;
    cursor: pointer;
    min-height: 88px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
  }

  /* The wording inside it still says "drag" — a sentence a phone cannot act
     on. Changing that needs the markup, not CSS, and inventing a class to
     hide (.drag-hint, which does not exist) would have been a rule doing
     nothing while looking like it worked. Noted rather than faked. */

  /* A checklist item is what a client actually reads. Room to breathe, and a
     tap target that is the whole row rather than a small link inside it. */
  .portal-checklist > * { padding: 12px 0; }

  .portal-container { padding-left: 14px; padding-right: 14px; }
}

/* ── WHAT A PHONE'S BROWSER DOES ON ITS OWN ───────────────────────────────
   Chrome on Android inflates text on pages it judges not to be built for a
   phone. It is trying to help, and the result was "Clients" taking a third of
   the screen while a form label wrapped to four lines beneath it.

   The page IS built for a phone now, so the browser should stop guessing.

   100% rather than none: `none` also stops somebody who has chosen a larger
   text size in their accessibility settings, which is a real person with a
   real reason. */
html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

@media (max-width: 640px) {

  /* ── A ROW OF CONTROLS BECOMES A COLUMN ────────────────────────────────
     The bulk action bar rendered as a vertical column of single letters —
     "R e a s s i g n" down the page. That is what a flex row does when its
     items have no room: each one shrinks to the width of one character and
     the text wraps at every letter.

     Wrapping alone does not fix it. The items have to stop competing for a
     width that is not there. */
  #bulk-action-bar {
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
    padding: 12px;
  }

  #bulk-action-bar > * {
    width: 100%;
    min-width: 0;          /* or a flex item refuses to shrink below its content */
    text-align: center;
  }

  /* The "or" between two choices is a word, not a button. It should not take
     a full row of its own. */
  #bulk-action-bar > span:not([id]) { display: none; }

  /* ── THE CARD HEADER ────────────────────────────────────────────────────
     Title and four actions on one line is a desktop shape. On a phone the
     title goes on its own line and the actions wrap beneath it, each big
     enough to hit. */
  .card-header {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .card-header h2, .card-header h1, .card-header > strong {
    font-size: 20px;
    margin: 0;
  }

  /* The actions sit in a row that wraps, rather than a column of full-width
     buttons — five stacked buttons push the actual content off the screen. */
  /* .header-actions does not exist; the actions sit in a plain div. */
  .card-header > div {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    width: 100%;
  }

  .card-header .btn-tiny {
    flex: 1 1 auto;
    min-width: 44%;
    white-space: nowrap;
  }

  /* ── THE FILTERS ────────────────────────────────────────────────────────
     A label reading "Search by name, alias, or phone" over a field 90px wide
     wraps to four lines. Full width, label above, one per row. */
  .filters label, .field label {
    display: block;
    font-size: 12.5px;
    margin-bottom: 3px;
  }

  .filters select, .filters input, .field select, .field input {
    width: 100%;
    box-sizing: border-box;
  }

  /* Nothing is allowed to be narrower than its own words. */
  .card, .card-header, .filters, #bulk-action-bar { min-width: 0; }
}


/* ── THE CLASS OF PROBLEM, NOT SIXTY-SEVEN INSTANCES ──────────────────────
   The bulk action bar rendered as "R e a s s i g n" down the page. That is
   what a flex row does when its items have no room: each shrinks to the width
   of one character and the text wraps at every letter.

   It was not special. There are sixty-seven flex and grid containers in this
   stylesheet and every one of them does the same thing at 390px. Fixing them
   one at a time — which is what the last three attempts did — finds the next
   one only when somebody photographs it.

   TWO RULES, APPLIED TO ALL OF THEM

     flex-wrap: wrap    a row that cannot fit becomes two rows rather than a
                        row of crushed columns

     min-width: 0       a flex item defaults to min-width:auto, which means it
                        REFUSES to shrink below its content — and when the
                        container has less room than that, the text wraps
                        inside the item instead. This is the actual cause of
                        the vertical letters, and no amount of wrapping fixes
                        it without this.

   Generated from the stylesheet itself rather than typed, so a container added
   later is covered the day somebody adds it. */

@media (max-width: 640px) {

  .action-menu,
  .activity-filters,
  .activity-row,
  .activity-shortcuts,
  .available-items-row,
  .blank-row,
  .card-header,
  .checklist li,
  .checklist-section-head,
  .client-link-row,
  .engagement-bulk,
  .entity-candidate,
  .entity-candidate-actions,
  .entity-candidates-head,
  .entity-name-row,
  .entity-note,
  .entity-tile,
  .expand-btn,
  .extracted-figures,
  .feedback-actions,
  .feedback-kind,
  .feedback-kinds,
  .feedback-overlay,
  .field,
  .field-row,
  .file-chips,
  .firm-facts,
  .firm-person,
  .flag-group summary,
  .flag-item-row,
  .flag-row,
  .gate-ask-backdrop,
  .gate-row,
  .intake-doc,
  .intake-doc-actions,
  .intake-review-head,
  .item-actions,
  .landing-brand,
  .ledger-hero,
  .ledger-stat,
  .letter-confirm-actions,
  .letter-prompt,
  .letter-row,
  .new-client-field,
  .new-client-row,
  .notice,
  .notice-bar,
  .observed-row,
  .pager,
  .portal-item,
  .portal-rejected-note,
  .portal-upload-btn,
  .portal-upload-optional,
  .portal-uploaded-file,
  .provenance-summary,
  .section-toggle,
  .settings-checkbox-label,
  .tabs,
  .toast,
  .toast-tray,
  .typeahead-item,
  .typeahead-row,
  .unevidenced-prompt,
  .user-area {
    flex-wrap: wrap;
    min-width: 0;
  }

  .action-menu > *,
  .activity-filters > *,
  .activity-row > *,
  .activity-shortcuts > *,
  .available-items-row > *,
  .blank-row > *,
  .card-header > *,
  .checklist li > *,
  .checklist-section-head > *,
  .client-link-row > *,
  .engagement-bulk > *,
  .entity-candidate > *,
  .entity-candidate-actions > *,
  .entity-candidates-head > *,
  .entity-name-row > *,
  .entity-note > *,
  .entity-tile > *,
  .expand-btn > *,
  .extracted-figures > *,
  .feedback-actions > *,
  .feedback-kind > *,
  .feedback-kinds > *,
  .feedback-overlay > *,
  .field > *,
  .field-row > *,
  .file-chips > *,
  .firm-facts > *,
  .firm-person > *,
  .flag-group summary > *,
  .flag-item-row > *,
  .flag-row > *,
  .gate-ask-backdrop > *,
  .gate-row > *,
  .intake-doc > *,
  .intake-doc-actions > *,
  .intake-review-head > *,
  .item-actions > *,
  .landing-brand > *,
  .ledger-hero > *,
  .ledger-stat > *,
  .letter-confirm-actions > *,
  .letter-prompt > *,
  .letter-row > *,
  .new-client-field > *,
  .new-client-row > *,
  .notice > *,
  .notice-bar > *,
  .observed-row > *,
  .pager > *,
  .portal-item > *,
  .portal-rejected-note > *,
  .portal-upload-btn > *,
  .portal-upload-optional > *,
  .portal-uploaded-file > *,
  .provenance-summary > *,
  .section-toggle > *,
  .settings-checkbox-label > *,
  .tabs > *,
  .toast > *,
  .toast-tray > *,
  .typeahead-item > *,
  .typeahead-row > *,
  .unevidenced-prompt > *,
  .user-area > * {
    min-width: 0;
    max-width: 100%;
  }

  .entity-type-grid,
  .landing-grid,
  .topbar {
    grid-template-columns: 1fr;
    min-width: 0;
  }

  /* ── NOTHING MAY BE WIDER THAN THE SCREEN ───────────────────────────────
     Horizontal scrolling on a phone is the single thing that makes a web app
     feel broken rather than cramped: the whole page slides when somebody
     swipes a list, and no tap lands where they aimed.

     Every one of these is a real cause of it. */
  * { max-width: 100%; box-sizing: border-box; }

  html, body { overflow-x: hidden; width: 100%; }

  /* An unbroken string — a token, an email, a long company name — is wider
     than the screen and nothing else can push it back. */
  * { overflow-wrap: anywhere; word-break: normal; }

  /* A table that has not been turned into cards still scrolls its own box
     rather than the page. */
  table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }

  img, svg, video, iframe, canvas { height: auto; }

  pre, code { white-space: pre-wrap; }
}

/* ── THE CLIENT TREE ON A PHONE ───────────────────────────────────────────
   A person with twenty-five companies under them is a hierarchy, and a
   hierarchy is the one shape a narrower table cannot express. Turned into
   cards without more thought, the group header and its twenty-five members
   all become identical cards in a flat list, and the relationship — which is
   the whole point of the grouping — disappears.

   WHAT IT BECOMES

     the group header    a heading card: the person's name, how many
                         companies, and a large toggle. Tapping it opens or
                         closes the group.

     each member         indented, attached to the header by a rule down the
                         left, quieter than the header so the eye reads the
                         header first.

   Collapsed by default is already how it works, which is right: twenty-five
   entities expanded on a phone is a scroll nobody finishes. */

@media (max-width: 640px) {

  /* ── THE GROUP HEADER ──────────────────────────────────────────────────
     The thing somebody taps, so it must look tappable and be large enough to
     hit without aiming. */
  #clients-table tr.group-header-row {
    background: var(--gold-bg, #faf6ec);
    border-color: var(--gold, #c9a227);
    padding: 14px;
    margin-bottom: 6px;
  }

  #clients-table tr.group-header-row td {
    display: block;
  }

  /* The name and the count carry; everything else on that row is chrome. */
  #clients-table tr.group-header-row td strong {
    font-size: 16.5px;
    line-height: 1.3;
  }

  /* The toggle: the whole point of the card, so 44px and unmissable. */
  #clients-table .tree-toggle {
    width: 44px;
    height: 44px;
    font-size: 18px;
    position: absolute;
    top: 8px;
    right: 8px;
  }

  /* ── A MEMBER OF A GROUP ───────────────────────────────────────────────
     Indented and joined to the header above by a line, so twenty-five cards
     read as belonging to somebody rather than as twenty-five clients. */
  #clients-table tr.group-header-row ~ tr:not(.group-header-row) {
    margin-left: 14px;
    border-left: 3px solid var(--gold, #c9a227);
    border-top-left-radius: 0;
    border-bottom-left-radius: 0;
  }

  /* ── THE ROW THAT ADDS AN ENTITY ───────────────────────────────────────
     Not a client, so it must not look like one. */
  #clients-table tr.group-add-row {
    background: transparent;
    border: none;
    margin-left: 14px;
    padding: 4px 0 12px;
  }

  #clients-table tr.group-add-row td { text-align: left !important; }

  /* A cell spanning eleven columns is a desktop instruction. On a card it
     just means "this is the whole row". */
  #clients-table td[colspan] { width: 100%; }
}

/* ── MEASURED, NOT GUESSED ────────────────────────────────────────────────
   Rendered at 390px and inspected: the filter row sits in a plain <div> with
   no class at all, so every rule written against ".filters" did nothing. And
   the general `min-width: 0` above — which fixed the vertical letters — let
   each .field collapse to 55px, so the selects showed a single character.

   min-width: 0 is right for a container and wrong for a form field. A field
   has a floor: the width of the thing somebody has to read inside it. */

@media (max-width: 640px) {

  /* A form field takes the full row. Its parent is a flex row with no class,
     so full-width children make it wrap one per line without needing to name
     the parent at all. */
  .field {
    flex: 1 1 100% !important;
    min-width: 100% !important;
    max-width: 100%;
    margin-bottom: 10px;
  }

  .field > select,
  .field > input,
  .field > textarea {
    width: 100%;
    min-width: 0;
  }

  .field > label,
  .field > span {
    white-space: normal;
    font-size: 12.5px;
    margin-bottom: 3px;
  }

  /* ── THE BULK BAR, TIGHTER ──────────────────────────────────────────────
     It stacks correctly now and each item sat in its own tall row with a
     large gap, so five controls filled the screen. They are buttons, not
     sections. */
  #bulk-action-bar {
    gap: 6px;
    padding: 10px;
  }

  #bulk-action-bar > * { margin: 0; }

  #bulk-action-bar .btn-tiny {
    padding: 10px 12px;
    line-height: 1.2;
  }

  /* ── THE HEADER ACTIONS ─────────────────────────────────────────────────
     "+ New client", "Invite a client" and "Export CSV" were spread across
     three rows with a floating button stranded on the right. Two per row,
     even widths, nothing stranded. */
  .card-header > div {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: stretch;
  }

  .card-header > div > * {
    flex: 1 1 calc(50% - 4px);
    min-width: 0;
    margin: 0;
  }

  /* A checkbox and its label belong together on one line, not split across
     the width of the card. */
  label:has(> input[type="checkbox"]) {
    display: flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
  }
}

/* ── THE BULK BAR, MEASURED AGAIN ─────────────────────────────────────────
   Rendered at 390px: the bar stacks, but its select was still 46px — one
   character — and every item sat in a tall row of its own, so five controls
   filled a screen and a half.

   The select is inside a wrapper the generic rules do not reach. And the
   spacing came from the desktop rule, which uses generous gaps because on a
   wide row they read as separation and on a narrow column they read as
   nothing at all. */

@media (max-width: 640px) {

  /* Every control in the bar, however deeply it sits, takes the full width. */
  #bulk-action-bar select,
  #bulk-action-bar input,
  #bulk-action-bar button,
  #bulk-action-bar .btn-tiny {
    width: 100% !important;
    min-width: 0 !important;
    box-sizing: border-box;
    margin: 0;
  }

  #bulk-action-bar > *,
  #bulk-action-bar > * > * {
    width: 100%;
    text-align: center;
  }

  /* Tight, so five controls are five controls rather than a page. */
  #bulk-action-bar {
    gap: 6px !important;
    padding: 10px !important;
    line-height: 1.3;
  }

  #bulk-action-bar .btn-tiny { padding: 11px 10px !important; }

  /* ── THE HEADER'S STRANDED BUTTON ───────────────────────────────────────
     An icon-only button — the expand control — took half a row beside "Export
     CSV" and read as an empty box. It is an icon: it needs the width of an
     icon, not half the header. */
  .card-header > div > button:not(.btn-tiny),
  .card-header button[title*="ull"],
  .card-header button[onclick*="ullscreen"],
  .card-header button[onclick*="xpand"] {
    flex: 0 0 44px;
    width: 44px;
    min-width: 44px;
    padding: 0;
  }

  /* ── THE NAVIGATION ─────────────────────────────────────────────────────
     Eleven tabs wrapped to three lines and took a third of the screen before
     any content. Tighter, and it scrolls sideways in its own strip rather
     than stacking — a nav is the one place horizontal scrolling is right,
     because it is a known row of known things. */
  .tabs, nav {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    gap: 2px;
    padding-bottom: 4px;
    scrollbar-width: none;
  }

  .tabs::-webkit-scrollbar, nav::-webkit-scrollbar { display: none; }

  .tab-btn {
    flex: 0 0 auto;
    white-space: nowrap;
    font-size: 13.5px;
    padding: 10px 11px;
  }
}

/* The version line under a document in the client portal.
 *
 * This value used to be an inline style on the element itself, which no media
 * query can reach. It is here so the phone override below can lift it - and it
 * is 11px, exactly what the inline style said, so the desktop renders
 * identically to before. */
.doc-version-line { font-size: 11px; }


/* ── READABLE WITHOUT A PINCH, ON A PHONE ONLY ──────────────────────────────
 *
 * Below 12px a phone forces a pinch, and a client who has to zoom to read what
 * is being asked of them is a client who gives up. portal-mobile.spec.js has
 * asserted this since it was written and has been failing on the client
 * portal - the surface most clients use, on the device they use it from:
 *
 *   11px: Sent — being reviewed          (x4, the document status pills)
 *   11px: Version 1 · Updated 9/9/2026
 *
 * INSIDE A MEDIA QUERY, because these rules are global and the desktop is what
 * a firm uses for eight hours in February. Strip every @media block from this
 * file and what remains must be exactly what was there before - the rule that
 * makes phone work safe to do at all. 11px is a deliberate density choice at
 * 1400px and stays untouched there.
 *
 * 12px rather than more: it clears the threshold the test names without
 * changing what the badge is - a quiet label beside a document, not a heading.
 */
@media (max-width: 640px) {

  .status-pill,
  .doc-version-line,
  /* "Wrong file? Remove", "+ I have another one of these" - the controls a
   * client uses to correct a mistake, which is exactly when they most need to
   * be able to read them. .btn-tiny is used across the desktop app too and is
   * untouched there; this only applies below 640px. */
  .btn-tiny,
  /* The pencil beside a name. Small by design, but 11px of glyph on a phone is
   * both unreadable and a poor tap target. */
  .rename-icon {
    font-size: 12px;
  }
}


/* ── The client portal: the parts added when a client could not see when ──────
 *
 * Captured at 390px before any of this existed: the home screen was 2,658px of
 * scrolling with no date anywhere on it, two checkbox labels ran off the right
 * edge, and a ticket's status pill broke across two lines on top of its own
 * subject.
 */

/* BUG 1's other half. The layout used to be an inline style, which beat
   .hidden { display: none } and left the form open permanently. As a class it
   loses to .hidden, which is the whole point. */
/* :not(.hidden) is not decoration. .hidden is declared at line 371 and this
   rule is 2,800 lines later -- same specificity, so the LATER one wins and a
   plain `.portal-custom-form { display: flex }` re-opens the form exactly the
   way the inline style did. The first fix moved the bug rather than removing
   it; this one makes .hidden win by never competing with it. */
.portal-custom-form:not(.hidden) {
  margin-top: 8px;
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.portal-custom-form input[type="text"] { flex: 1; min-width: 200px; }

/* When it is due, and whose move it is. */
.portal-status-strip {
  margin: 10px 0 4px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-subtle, #f7f7f5);
  font-size: 14px;
  line-height: 1.45;
}
.portal-status-strip .portal-due {
  font-weight: 600;
  display: block;
}
.portal-status-strip .portal-due-soon { color: var(--danger, #a4301f); }
.portal-status-strip .portal-whose { display: block; margin-top: 2px; }

/* State filings. A list, not a table: a table at 390px is a sideways scroll,
   and this is read on a phone. */
.portal-filing-list { list-style: none; padding: 0; margin: 12px 0 0; }
.portal-filing-list li {
  padding: 10px 0;
  border-top: 1px solid var(--border);
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  align-items: baseline;
}
.portal-filing-list li:first-child { border-top: 0; }
.portal-filing-list .filing-name { font-weight: 600; flex: 1 1 100%; }
.portal-filing-list .filing-where { color: var(--text-muted); font-size: 13px; }
.portal-filing-list .filing-due { font-size: 13px; }

/* BUG 3 · the checkbox labels ran off the right edge -- "documents I still"
   stopped mid-sentence, and the AI opt-out label was cut in half. They were
   laid out as a row that could not wrap. */
/* ── WHY THIS SELECTOR IS SHAPED LIKE THAT ──────────────────────────────────
 *
 * The phone block further down carries:
 *
 *     label:has(> input[type="checkbox"]) { ...; white-space: nowrap; }
 *
 * "A checkbox and its label belong together on one line" -- right for a short
 * toggle, wrong for a sentence. :has() takes the specificity of its argument,
 * so that rule is (0,1,2) and a plain .portal-consent-row (0,1,0) loses to it.
 *
 * The result was only ever visible on a PHONE, which is why it survived: at
 * 390px "It's OK to text me reminders about documents I still" stopped there,
 * and the AI opt-out label was cut in half -- on the one checkbox that decides
 * whether a client's documents are sent to a model.
 *
 * These two labels opt OUT rather than the general rule changing, because the
 * general rule is correct for the toggles it was written for.
 */
label.portal-consent-row:has(> input[type="checkbox"]) {
  display: flex;
  align-items: flex-start;   /* a two-line label must not centre on its box */
  gap: 8px;
  white-space: normal;
}
.portal-consent-row input[type="checkbox"] { flex: 0 0 auto; margin-top: 2px; }
.portal-consent-row span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }

/* BUG 4 · a ticket's status pill wrapped onto two lines and sat on top of the
   subject. The row had no gap and the pill could shrink. */
.portal-ticket-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}
.portal-ticket-row .portal-ticket-subject { flex: 1 1 auto; min-width: 0; }
.portal-ticket-row .status-pill { flex: 0 0 auto; white-space: nowrap; }

/* ── THE FIRM'S SIDE OF THE CLIENT'S BANDS ──────────────────────────────────
 *
 * New classes only. Nothing above this line is altered: the client's page
 * groups by whose document it is, and until now the firm looking at the same
 * client saw a flat list, so the two sides described the same work
 * differently and a phone call had to reconcile them.
 *
 * Nothing collapses, per this file's own rule -- a preparer scanning for what
 * is outstanding must not have to open anything.
 */
.checklist-band {
  list-style: none;
  display: flex; align-items: baseline; gap: 10px;
  margin: 22px 0 4px; padding-bottom: 4px;
  border-bottom: 2px solid var(--accent);
}
.checklist-band:first-child { margin-top: 4px; }
.checklist-band .band-title {
  font-size: 12px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--accent); flex: 1 1 auto;
}
.checklist-band .band-count { font-size: 12.5px; color: var(--text-muted); white-space: nowrap; }
.band-notes {
  font-size: 11px; font-weight: 600; padding: 2px 7px; border-radius: 9px;
  background: var(--warning-bg); color: var(--warning-text); white-space: nowrap;
}
.checklist-subject {
  list-style: none;
  display: flex; align-items: baseline; gap: 8px;
  margin-top: 10px; padding-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.checklist-subject .subject-name { font-weight: 600; flex: 1 1 auto; min-width: 0; }
.checklist-subject .subject-rel {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--text-muted); white-space: nowrap;
}
.checklist-subject .subject-tally {
  font-size: 12.5px; color: var(--text-muted); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* A conversation open on one row, on the row. */
.item-note {
  display: inline-block; margin-left: 8px;
  font-size: 11px; font-weight: 600; padding: 2px 7px; border-radius: 9px;
  background: var(--warning-bg); color: var(--warning-text);
}

/* ── HOW A ROW WAS SATISFIED ────────────────────────────────────────────────
 * Three weights, because they are three different messages.
 *
 * Badging only the photographs would imply every other automatically-filed row
 * had been looked at by somebody. None of them had. So every machine-filed row
 * says so, and the colour carries how much attention it wants.
 */
.evidence-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 9px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .02em;
  white-space: nowrap;
}

/* Filed off a document that carried its own text. A statement of fact, not a
   task -- quiet enough to skim past on a list of forty rows. */
.evidence-auto {
  background: var(--bg-subtle, #F6F5F1);
  color: var(--text-muted, #6B706C);
}

/* Read off a photograph. Amber: not an error and not a task, but a reason to
   glance. Red here would teach people to dismiss it. */
.evidence-photo {
  background: var(--warning-bg, #FAF2DF);
  color: var(--warning-text, #8A6A12);
}

/* Should not occur -- the matcher only files on two agreeing signals. If one
   ever does, it is the most important thing on the row. */
.evidence-check {
  background: var(--danger-bg, #FBEDEC);
  color: var(--danger-text, #8C2F27);
}

/* ── WHICH ROWS ARE WORTH NAMING ────────────────────────────────────────────
 * Shown only on a row competing with siblings for the same document type, and
 * only while no payer is named. A hint on every row is a hint nobody reads.
 *
 * Quiet: it is an offer, not a task. The row works perfectly well without it
 * and a preparer who ignores it loses nothing except the automation.
 */
.naming-hint {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.35;
  color: var(--text-muted, #6B706C);
}

/* ── THE CLIENT NAME IS THE ONE COLUMN THAT MUST NOT BE CLIPPED ──────────────
   Reported 12 September 2026: "In the desktop version - clients tab, client
   name is being truncated on the screen."

   It carried .nowrap-col, which is the class for dates, states and counts --
   short values where a wrap looks like a mistake. On a name it does the
   opposite: "Okonkwo Family Holdings LLC" is forced onto one line, the table
   grows past the card, and the name is the thing that gets cut.

   A name wraps. It also keeps a floor so the column cannot be squeezed to
   nothing when the other eleven columns compete for width, and long unbroken
   strings (an email-shaped alias) break rather than push the table wider. */
.data-table td.client-alias-col,
.data-table th.client-alias-col {
  white-space: normal;
  min-width: 11ch;
  overflow-wrap: anywhere;
}
