/* Dashboard. Layout only - tokens, type, buttons and focus come from
   /styles.css, the same brand sheet the marketing pages use. */

body {
  background: var(--cream);
}
main {
  min-height: calc(100svh - 96px);
}

/* ── The shell ──────────────────────────────────────────────────────────────
   The header and the three status bars are cards floating on the cream, not
   bands welded to the edges of the window. One inset drives all of them, so
   the gap above the header, beside it, between it and the banner underneath,
   and around the bars at the foot are the same measurement - the thing that
   makes detached furniture read as deliberate rather than as a header that
   came loose. Kept small enough that the content underneath does not lose
   width worth having on a laptop.

   Nothing here is lifted with a shadow. A card is separated by its own edge -
   a 1px rule and the cream showing all the way round it - which is the same
   thing a shadow was being asked to say, said once instead of twice. */
:root {
  --shell: clamp(10px, 1.5vw, 18px);
  --shell-r: 18px;
}

/* The white and the rule belong to the card now, not to the full-width strip,
   so both are taken off .header-light (which the legal pages still use as it
   was). Two classes beat the one in /styles.css without !important. */
.site-header.header-light {
  background: transparent;
  border-bottom: 0;
  padding: var(--shell) var(--shell) 0;
}
.site-header.header-light .nav-inner {
  background: #fff;
  border: 1px solid var(--border);
  border-radius: var(--shell-r);
  /* Tighter than the marketing header's --pad: inside a card that already has
     an inset around it, a 56px gutter reads as a hole rather than as room. */
  padding: 9px var(--s5);
  min-height: 62px;
}

/* Any explicit `display` on a class beats the UA's `[hidden] { display:none }`.
   Without this, `.app-nav` (display:flex), `.gate` (display:flex) and every
   .btn (display:inline-flex) ignore the attribute entirely - the signed-out
   page showed the app nav and a Sign out button, and the sign-in card would
   have stayed on screen after signing in. The extension stylesheets carry the
   same rule for the same reason. */
[hidden] {
  display: none !important;
}

.app-nav {
  display: flex;
  gap: var(--s1);
  /* Centred between the brand and the actions rather than tucked against the
     brand: three destinations is a primary navigation, not an afterthought
     hanging off the wordmark. */
  flex: 1;
  justify-content: center;
  min-width: 0;
}
.app-tab {
  font-family: var(--karla);
  font-size: 15px;
  font-weight: 600;
  border: 1px solid transparent;
  background: transparent;
  color: var(--faint);
  padding: 9px 16px;
  min-height: 44px;
  border-radius: var(--r-pill);
  cursor: pointer;
  transition:
    background 0.22s var(--ease),
    color 0.22s var(--ease),
    border-color 0.22s var(--ease);
}
.app-tab:hover {
  background: var(--cream);
  color: var(--ink);
}
.app-tab.is-active {
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-color: var(--accent-line);
}
.app-actions {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: var(--s3);
}

/* ── Mode, beside the brand ───────────────────────────────────────────────── */
/* Its own group so .nav-inner keeps three children and its space-between
   layout is untouched. min-width:0 lets the brand shrink before the pill does -
   the mode is one short word and always worth keeping legible. */
.brand-group {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
/* Sentence case, not uppercase: the label reads "Mode: Review" as a statement,
   and uppercase plus letter-spacing over that many characters gets wide enough
   to crowd the nav on a laptop. */
.mode-pill {
  font-size: 12.5px;
  font-weight: 600;
  border-radius: var(--r-pill);
  padding: 5px 12px;
  white-space: nowrap;
  flex: none;
  cursor: default;
}
/* Review is the default and the quiet state: nothing leaves without a press. */
.mode-pill.is-review {
  color: var(--accent-ink);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
}
/* Auto sends on its own, so it is the one state that should catch the eye.
   Same amber the OAuth consent-incomplete page uses for "read this". */
.mode-pill.is-auto {
  color: var(--warn);
  background: var(--warn-soft);
  border: 1px solid rgba(203, 118, 44, 0.34);
}
/* Paused outranks both: with it on, neither channel sends, so showing "AUTO"
   here would be false. */
.mode-pill.is-paused {
  color: var(--faint);
  background: var(--cream);
  border: 1px solid var(--border);
}

/* The countdown to the next scan, left of Sign out. Quiet on purpose: it is
   reassurance, not a control, and it must never compete with the mode pill -
   which states whether this account sends by itself and is the one thing in
   the header worth reading twice. Monospaced digits so a number ticking from
   10m to 9m does not shuffle the button beside it. */
.scan-pill {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--faint);
  background: var(--cream);
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 5px 12px;
  white-space: nowrap;
  flex: none;
  cursor: default;
  font-variant-numeric: tabular-nums;
}
/* Due now, or overdue. The scan is a cron tick away rather than pending on
   anything the user can influence, so this stays quiet too - it says "any
   moment", not "something is wrong". */
.scan-pill.is-due {
  color: var(--accent-ink);
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
@media (max-width: 640px) {
  /* First thing to go when the header runs out of room. The mode pill states
     whether applications send themselves; this one is a clock. */
  .scan-pill {
    display: none;
  }
}

/* Reports what is actually running. Never advertises the extension as a
   locked feature - a user with no extension sees a complete product. */
.src-pill {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--accent-ink);
  background: var(--accent-soft);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  padding: 6px 13px;
  white-space: nowrap;
}

/* ── Signed out ──────────────────────────────────────────────────────────── */
.gate {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s7) var(--pad);
}
.gate-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  padding: clamp(28px, 5vw, 56px);
  max-width: 720px;
  width: 100%;
}
.gate-card .h2 {
  margin: var(--s3) 0 var(--s4);
}
.gate-card .btn {
  margin-top: var(--s6);
}
.gate-note {
  font-size: 13.5px;
  color: var(--faint);
  margin: var(--s4) 0 0;
}
.gate-note a {
  color: var(--accent-ink);
}
.gate-modes {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s5);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--border);
}
.gate-modes h3 {
  font-family: var(--work);
  font-weight: 500;
  font-size: 16px;
  color: var(--ink);
  margin: 0 0 6px;
}
.gate-modes p {
  font-size: 14px;
  color: var(--body);
  margin: 0;
  line-height: 1.55;
}

/* ── Stats ───────────────────────────────────────────────────────────────── */
.stat-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s3);
  padding: var(--s5) var(--pad) 0;
}
.stat {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s4);
}
.stat b {
  display: block;
  font-family: var(--work);
  font-weight: 500;
  font-size: 26px;
  letter-spacing: -0.04em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.stat span {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ── Work area ───────────────────────────────────────────────────────────── */
.work {
  display: grid;
  grid-template-columns: 380px minmax(0, 1fr);
  gap: var(--s5);
  padding: var(--s5) var(--pad) var(--s8);
  align-items: start;
}
.col-list {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}

/* ── The match list is the height of the screen, and stays put ───────────────

   It was a fixed box - `max-height: min(70vh, 760px)` on an inner scroller,
   inside a page that scrolls too - so a user reading down the detail pane took
   the whole list off screen with it and got it back by scrolling up. Reported
   on 2026-08-20 as "in the matches section I need lot of jobs to display but it
   here left after scrolling only i can able to see".

   Sticky rather than taller, because taller alone does not fix it: the list is
   the navigation for the pane beside it, and navigation that scrolls away is
   the thing being complained about. Pinned to the viewport it holds roughly
   twice the rows on a laptop AND is still there after reading a draft.

   `align-items: start` on .work is what makes it work - a stretched grid item
   is already as tall as its row and has nothing to stick within - and it was
   already there.

   ── Scoped to #view-matches, and that is not cosmetic ─────────────────────

   `.col-list` is shared by FOUR views: matches, referral, assistant and
   settings. Written on the bare class this block capped and un-stretched all
   of them, and it broke the assistant, whose columns are deliberately
   `align-items: stretch` so the two end in the same place - the exact layout
   frontend.md#the-assistant-fills-the-window exists to hold, and which caught
   it. Only Matches has a detail pane to scroll beside, so only Matches gets
   this. Same rule the block at the bottom of this file already states for the
   assistant's own overrides. */
#view-matches .col-list {
  position: sticky;
  top: var(--s4);
  /* The column, not the rows, owns the height: the head, the search box and
     the filter chips are fixed furniture and the rows take what is left. */
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - var(--s5) * 2);
}
.col-head {
  padding: var(--s3);
  border-bottom: 1px solid var(--border);
  display: flex;
  gap: var(--s2);
  align-items: center;
}
.list {
  max-height: min(70vh, 760px);
  overflow-y: auto;
}
/* Matches only - see the note on #view-matches .col-list. The cap above is what
   the referral and settings columns still want; here the COLUMN owns the height
   and the rows take what the furniture above them leaves.

   `min-height: 0` is load-bearing on a flex child that scrolls: without it the
   rows insist on their full content height and the column grows past the
   viewport instead of the list scrolling inside it. */
#view-matches .list {
  flex: 1 1 auto;
  min-height: 0;
  max-height: none;
}
.row {
  display: block;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
  background: transparent;
  font: inherit;
  padding: var(--s4);
  transition: background 0.22s var(--ease);
}
.row:hover {
  background: var(--accent-soft);
}
.row.is-active {
  background: var(--cream);
  border-left-color: var(--accent);
}
.row-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 14.5px;
  color: var(--ink);
  line-height: 1.3;
}
.row-co {
  font-size: 13px;
  color: var(--faint);
  margin-top: 2px;
}
.row-foot {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: var(--s2);
}
.tag {
  font-size: 10.5px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  background: var(--cream);
  color: var(--faint);
  border: 1px solid var(--border);
}
.tag-score {
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-color: var(--accent-line);
}
/* ── Search, inside the list it filters ─────────────────────────────────────
   Sits under .col-head and above .list, on the same 1px rule the header
   already draws, so the list still reads as one card rather than as a card
   with a widget stuck on top. No fill and no icon: the placeholder says what
   it does, and this palette spends colour on one number per screen. */
.list-search {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s3);
  border-bottom: 1px solid var(--border);
}
.list-search-input {
  flex: 1;
  /* Grid and flex children refuse to shrink past their intrinsic width, and a
     text input's is wide enough to push this 380px column open. */
  min-width: 0;
  font-family: var(--karla);
  /* 16px: anything smaller and iOS Safari zooms the page on focus, which on a
     phone leaves the user looking at a magnified fragment of the list. */
  font-size: 16px;
  line-height: 1.4;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 9px var(--s3);
  min-height: 40px;
}
.list-search-input::placeholder {
  color: var(--faint);
}
/* The browser's own clear affordance, removed: it is a different shape in
   every engine, it is invisible in Firefox, and there is a real button beside
   it that also resets the list. */
.list-search-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}
.list-search-clear {
  font: inherit;
  font-size: 12.5px;
  color: var(--faint);
  background: transparent;
  border: 0;
  padding: 6px var(--s2);
  /* 40px rather than 44: it sits inside a row that is already 40 tall, and a
     taller target would make the row taller for a control most people never
     press. Its hit area spans the full row height. */
  min-height: 40px;
  cursor: pointer;
  border-radius: var(--r-sm);
}
.list-search-clear:hover {
  color: var(--ink);
  background: var(--cream);
}
.list-search-clear:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
/* "3 results" / "nothing here, but 4 in Skipped". Same class as the other
   under-header notes so it inherits their spacing and colour. */
.list-search-note {
  margin: 0;
}
.list-search-note button {
  font: inherit;
  color: var(--accent-ink);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

.empty {
  padding: var(--s7) var(--s5);
  text-align: center;
  color: var(--faint);
  font-size: 14px;
}

/* ── Composer ────────────────────────────────────────────────────────────── */
.col-detail {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  min-height: 420px;
}
.blank {
  padding: var(--s8) var(--s5);
  text-align: center;
}
.compose {
  padding: var(--s6);
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.compose-head {
  display: flex;
  justify-content: space-between;
  gap: var(--s4);
  align-items: flex-start;
}
.compose-head h1 {
  font-family: var(--work);
  font-weight: 500;
  font-size: 24px;
  letter-spacing: -0.035em;
  color: var(--ink);
  margin: 0;
  line-height: 1.2;
}
/* The other listing for the same job. Quiet, because it is a footnote to the
   posting above it rather than a second posting - see lib/openings.ts. */
.j-also {
  margin: 6px 0 0;
  font-size: 13px;
  color: var(--faint);
}
.j-also a {
  color: var(--accent-ink);
}
.j-meta {
  font-size: 13.5px;
  color: var(--faint);
  margin: 6px 0 0;
}
.j-meta .sep {
  margin: 0 7px;
  opacity: 0.45;
}
.j-meta a {
  color: var(--accent-ink);
  font-weight: 600;
  text-decoration: none;
}
.j-meta a:hover {
  text-decoration: underline;
}
.j-badges {
  display: flex;
  gap: 6px;
  flex: none;
}
.badge {
  font-size: 11px;
  font-weight: 700;
  padding: 4px 11px;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: #fff;
  white-space: nowrap;
}
.badge-soft {
  background: var(--cream);
  color: var(--faint);
  border: 1px solid var(--border);
}

.jd {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--cream);
}
.jd summary {
  padding: 11px 14px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: var(--faint);
}
.jd pre {
  margin: 0;
  padding: 0 14px 14px;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--karla);
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--body);
  max-height: 260px;
  overflow-y: auto;
}
.notice {
  padding: 11px 14px;
  border-radius: var(--r-sm);
  font-size: 13.5px;
  border: 1px solid;
  line-height: 1.5;
}
.notice-warn {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn-line);
}
.compose-actions {
  display: flex;
  gap: var(--s2);
  align-items: center;
  flex-wrap: wrap;
}

/* ── Fields ──────────────────────────────────────────────────────────────── */
.fld {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: var(--s4);
}
.fld:last-child {
  margin-bottom: 0;
}
.fld > span:not(.hint) {
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
.fld input,
.fld select,
.fld textarea {
  font-family: var(--karla);
  font-size: 14px;
  padding: 11px 13px;
  width: 100%;
  min-height: 44px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--bg);
  color: var(--ink);
  transition: border-color 0.18s var(--ease);
}
.fld textarea {
  min-height: 80px;
  resize: vertical;
  line-height: 1.6;
}
/* A focused field is styled by /styles.css, which is to say not at all - no
   ring, no glow, no border change. The border-darken that used to live here
   went with it; see that file for what the removal costs and how to undo it.
   The transition on .fld input above is left alone: it still carries the
   hover and invalid states, which are not focus. */
.fld-grow textarea {
  min-height: 280px;
}
.hint {
  font-size: 12.5px;
  color: var(--faint);
  line-height: 1.5;
}
.hint code {
  font-family: var(--mono);
  font-size: 12px;
  background: var(--cream);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
}
.mb {
  margin-bottom: var(--s4);
}
.mt {
  margin-top: var(--s4);
}
.status {
  font-size: 13.5px;
  color: var(--faint);
}
/* The palette's own greens and reds, not two hexes that predate them.
   --success is 6.43:1 on white and --danger 5.9:1, both verified in
   /styles.css; the literals here were close to those and answerable to
   nothing, so a palette change would have left them behind. */
.status.ok {
  color: var(--success);
}
.status.err {
  color: var(--danger);
}
.muted {
  color: var(--faint);
  font-size: 14px;
}

/* ── Settings ────────────────────────────────────────────────────────────── */
/* ── Settings: the same shell as Matches ──────────────────────────────────── */
/* It was a 900px column centred in the window, so on a laptop it read as a
   different product from the page one tab away and spent most of the screen on
   margin. It now reuses .work / .col-list / .col-detail unchanged - pick a
   section on the left, see it on the right, exactly as a match behaves. */

/* The fixed savebar this reserved room for is gone - settings save
   themselves - so the page no longer ends in 140px of nothing. */
/* The right column is a container for cards, not a card itself - fieldsets
   already carry their own border, and nesting the two looked like a mistake. */
.settings .col-detail {
  background: transparent;
  border: 0;
  min-height: 0;
}
.set-head {
  display: block;
}
.set-title {
  font-family: var(--work);
  font-weight: 600;
  font-size: 17px;
  letter-spacing: -0.02em;
  margin: 0;
}
.set-head .muted {
  margin: 2px 0 0;
  font-size: 12.5px;
}

/* Mirrors .list / .row so the two pages feel like one product - but NOT its
   scroll cap. .list is capped because it can hold a hundred matches; this nav
   holds exactly eight items, so a max-height only ever clips the last one, and
   the one it clipped was Account. */
.set-nav {
  overflow: visible;
}
/* The card already draws the bottom edge. */
.set-tab:last-child {
  border-bottom: 0;
}
.set-tab {
  display: block;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-left: 3px solid transparent;
  background: transparent;
  font: inherit;
  padding: var(--s4);
  transition: background 0.22s var(--ease);
}
.set-tab:hover {
  background: var(--accent-soft);
}
.set-tab.is-active {
  background: var(--cream);
  border-left-color: var(--accent);
}
.set-tab b {
  display: block;
  font-family: var(--work);
  font-weight: 500;
  font-size: 14.5px;
  color: var(--ink);
}
.set-tab small {
  display: block;
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--faint);
}
/* Deleting your account should not look like changing a preference. */
.set-tab-danger b {
  color: var(--danger);
}
.set-tab-danger.is-active {
  border-left-color: var(--danger);
}
/* The first fieldset in a panel has the panel's own top edge above it, so its
   default top margin would read as a stray gap. */
.set-panel > fieldset:first-child {
  margin-top: 0;
}

.settings fieldset,
.resume-view fieldset {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg);
  padding: var(--s5);
  margin: var(--s5) 0 0;
}
.settings legend,
.resume-view legend {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 0 var(--s2);
  display: flex;
  align-items: center;
  gap: 9px;
}
.settings legend::before,
.resume-view legend::before {
  content: "";
  width: 18px;
  height: 2px;
  background: var(--accent);
  border-radius: 2px;
}
.grid2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s4);
}
.grid3 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--s4);
}

.modes {
  display: flex;
  gap: var(--s3);
}
.modes label {
  flex: 1;
  display: flex;
  gap: 10px;
  align-items: flex-start;
  cursor: pointer;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: var(--s4);
  transition:
    border-color 0.22s var(--ease),
    background 0.22s var(--ease);
}
.modes label:hover {
  border-color: var(--ink);
}
.modes label:has(input:checked) {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
}
.modes input {
  width: auto;
  min-height: 0;
  margin-top: 3px;
  accent-color: var(--accent-ink);
  flex: none;
}
.modes b {
  display: block;
  font-family: var(--work);
  font-weight: 500;
  font-size: 15px;
  color: var(--ink);
  margin-bottom: 2px;
}
.modes small {
  display: block;
  font-size: 12.5px;
  color: var(--faint);
  line-height: 1.5;
}
/* Four options rather than two, so they wrap instead of squeezing.
   .modes is a flex row with `flex: 1` labels, which is right for the two-way
   Review/Autonomous choice it was written for and wrong for the four-way one
   the teams composer asks: at four across, each label is narrow enough that its
   own sub-line breaks after every word. A grid with a sensible minimum lets
   them sit two-up and fall to one-up on a phone.

   LAYOUT ONLY, and no palette - the labels keep every border, fill and checked
   state .modes already defines. shared/DESIGN.md: a product stylesheet adds
   layout, never a palette. */
.modes-wrap {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}

/* The recommendation, carried as a word rather than as a preselected radio.
   Flat and bordered like everything else in the estate - no fill, no shadow. */
.ob-rec {
  font-family: var(--karla);
  font-style: normal;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent-ink);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-pill);
  padding: 1px 7px;
  margin-left: 7px;
  vertical-align: 1px;
}
/* Continue is revealed disabled and stays that way until a mode is chosen. */
#btn-ob-continue:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.sources {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--s2);
}
.src {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  padding: 11px 13px;
  min-height: 44px;
  background: var(--cream);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font-size: 13.5px;
  color: var(--body);
}
.src:has(input:checked) {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--ink);
}
.src input {
  width: 16px;
  height: 16px;
  min-height: 0;
  flex: none;
  accent-color: var(--accent-ink);
}
.src em {
  font-style: normal;
  color: var(--faint);
  font-size: 11.5px;
  margin-left: auto;
}

.drop {
  border: 2px dashed var(--border-strong);
  border-radius: var(--r-md);
  padding: var(--s6);
  text-align: center;
  cursor: pointer;
  background: var(--cream);
  transition:
    border-color 0.22s var(--ease),
    background 0.22s var(--ease);
}
.drop:hover,
.drop:focus-visible,
.drop.over {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
  outline: none;
}
.drop b {
  display: block;
  font-family: var(--work);
  font-weight: 500;
  font-size: 15px;
  color: var(--ink);
}
.drop small {
  display: block;
  font-size: 12px;
  color: var(--faint);
  margin-top: 3px;
}
.file-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  text-align: left;
  justify-content: center;
}
.file-icon {
  background: var(--ink);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  padding: 8px 10px;
  border-radius: var(--r-sm);
}
.file-row b {
  display: block;
  font-size: 14px;
  color: var(--ink);
}
.file-row small {
  display: block;
  font-size: 12px;
  color: var(--faint);
}
.btn-row {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-top: var(--s3);
}

/* ── Responsive ──────────────────────────────────────────────────────────── */
@media (max-width: 1080px) {
  .work {
    grid-template-columns: 320px minmax(0, 1fr);
  }
}
@media (max-width: 900px) {
  .work {
    grid-template-columns: 1fr;
  }
  .list {
    max-height: 42vh;
  }
  /* Stacked, the match list is a screen of its own - .work.is-detail hides it
     while a match is open - so it neither sticks nor gets capped. Pinning it
     here would pin it to a viewport it already fills, and the 42vh cap above
     was showing four rows on a phone. */
  #view-matches .col-list {
    position: static;
    max-height: none;
  }
  #view-matches .list {
    max-height: none;
  }
  .app-nav {
    margin-left: var(--s4);
  }
  .gate-modes {
    grid-template-columns: 1fr;
  }
  .grid3 {
    grid-template-columns: 1fr;
  }
}

/* Signed in, the header carries four things: brand, tab bar, mode pill and
   Sign out. Below ~720px they stop fitting on one line, so the tabs drop to
   their own row and the pill is allowed to ellipsise rather than push the row
   wider than the viewport. The pill is not hidden - which mode you are in is
   the whole point of the feature, so it degrades instead of disappearing. */
@media (max-width: 720px) {
  /* Matched to the selector that sets the card's padding above, or that one
     wins on specificity and the wrapped rows have no room to breathe.

     The gutter comes back in here. A phone is where the shell's inset costs
     the most - it is taken off both edges of a 390px screen and then the
     card's own padding again - and at 24px the four tabs ran past the card's
     right edge with Sign out sitting on top of the first one. */
  .site-header.header-light .nav-inner {
    flex-wrap: wrap;
    row-gap: var(--s2);
    padding: var(--s3);
  }
  /* `flex: none` is the load-bearing word. The desktop rule is `flex: 1`,
     i.e. a flex-basis of 0, so the tab bar's own width counted for nothing
     when the header decided where to break: it never dropped to the row
     `order: 3` and `width: 100%` were written for, it stayed on the first
     line as a 5px box, and the four tabs simply painted out of it - over
     Sign out and past the edge of the header. With a real basis it takes the
     line it was always meant to have. */
  .app-nav {
    order: 3;
    flex: none;
    width: 100%;
    margin-left: 0;
    /* Four destinations. Wrapping to a second line is the honest outcome on a
       narrow phone; overflowing is not, and neither is a row you have to
       scroll to discover Settings in. */
    flex-wrap: wrap;
  }
  .app-tab {
    padding: 9px 12px;
  }
  .app-actions {
    margin-left: auto;
    min-width: 0;
  }
  .src-pill {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
@media (max-width: 640px) {
  .grid2 {
    grid-template-columns: 1fr;
  }
  .modes {
    flex-direction: column;
  }
  .compose {
    padding: var(--s4);
  }
  .compose-head {
    flex-direction: column;
  }
  .compose-head h1 {
    font-size: 20px;
  }
}
@media (pointer: coarse) {
  .stat-tab {
    min-height: 44px;
  }
}

/* ── Pause ─────────────────────────────────────────────────────────────────
   Sits first in Settings. Styled as a card rather than one more checkbox in a
   row of them, because it is the control someone hunts for when they want the
   sending to stop now - and a control that is hard to find is one people route
   around by deleting their account. */
.pause-card {
  border-color: var(--border-strong);
}
.pause-row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  cursor: pointer;
}
.pause-row input {
  margin-top: 3px;
  flex: none;
}
/* The row's own heading, and ONLY that one. Unscoped, this also caught the
   <b>not</b> inside the referrals explanation and put a single word on a line
   of its own in the middle of a sentence. */
.pause-row > span > b {
  display: block;
  color: var(--ink);
}
.pause-row small {
  display: block;
  margin-top: 4px;
  color: var(--body);
}
/* Amber, not red: paused is a state the user chose, not a fault. */
.pause-card:has(#s-paused:checked) {
  border-color: #d98324;
  background: var(--warn-soft);
}
.pause-state {
  margin: var(--s3) 0 0;
  font: 500 13px/1.5 var(--karla);
  color: #8a5210;
}

/* ── Delete account ────────────────────────────────────────────────────────
   Red, and last on the page. The only destructive control in the product, so
   it is the only thing allowed to use this colour - if anything else turns
   red later, this stops reading as "this one is different". */
.danger-card {
  border-color: var(--danger-line);
  background: var(--danger-soft);
}
.danger-card legend {
  color: var(--danger);
}
.danger-lede {
  margin: 0 0 var(--s3);
  font: 400 14px/1.6 var(--karla);
  color: var(--body);
}
.btn-danger {
  background: var(--danger);
  color: #fff;
  border: 1px solid var(--danger);
}
.btn-danger:hover:not(:disabled) {
  background: var(--danger-strong);
  border-color: var(--danger-strong);
}
.btn-danger:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* Said before the user reaches Google's consent screen. "Send email on your
   behalf" is an optional checkbox there, unticked by default, and Continue
   works without it - so this has to be prominent enough to survive skimming. */
.gate-warn {
  margin: 14px 0 0;
  padding: 10px 14px;
  border-left: 3px solid #cb762c;
  background: var(--warn-soft);
  border-radius: 0 8px 8px 0;
  color: var(--warn);
  font-size: 13.5px;
  line-height: 1.5;
}
.gate-warn b {
  color: var(--warn-ink);
}

/* A jump to the tab that actually has matches, inside the empty state. */
.link-btn {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: var(--accent-ink);
  text-decoration: underline;
  cursor: pointer;
}

/* The Resume tab, when there is no resume and nothing typed either.
   Red rather than the amber used elsewhere for "read this": amber says the
   product is working and you should know something, and this one says it can
   do nothing at all for you until you act. Nothing will ever match, nothing
   can ever send.

   The dot carries the signal as much as the colour does. Colour alone fails
   for the ~1 in 12 men with a red-green deficiency, and it fails again for
   anyone who never saw the tab in its normal state to compare against - which
   is everybody here, since this shows on first run. renderResumeState() also
   sets a title, so the reason is reachable rather than merely indicated. */
.app-tab.needs-resume {
  color: var(--danger);
}
.app-tab.needs-resume::after {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: 7px;
  vertical-align: 1.5px;
  border-radius: 50%;
  background: var(--danger);
}
/* Still the selected tab first. Without this the active pill and the warning
   fight, and the user loses track of which view they are actually in. */
.app-tab.needs-resume.is-active {
  color: var(--ink);
}

/* An inline note inside a view.
   It carried the unreadable-résumé card until that moved into the notification
   panel; what is left is the generic block, still used where a pane has one
   thing to say that is not an error - the roles a founding team could not fill,
   for instance. It has no colour of its own: .notice-warn adds the amber, and a
   .notice on its own draws a border in the ink already in use. */
.notice {
  margin: 0 0 20px;
}
.notice[hidden] {
  display: none;
}

/* ── Notifications ─────────────────────────────────────────────────────────

   The bell and its panel, which is where all six banners went. Flat and
   bordered like everything else: no shadow on the panel, because separation
   here comes from a border and a change of surface, and a dropdown is exactly
   the element that tempts one. See shared/DESIGN.md rule 1. */
.notif {
  position: relative;
}
.notif-bell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  background: var(--bg);
  color: var(--faint);
  cursor: pointer;
  transition:
    color 0.22s var(--ease),
    border-color 0.22s var(--ease);
}
.notif-bell svg {
  width: 19px;
  height: 19px;
}
.notif-bell:hover {
  color: var(--ink);
  border-color: var(--ink);
}
.notif-bell:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* The count, and the one place this surface spends colour.

   A number is neutral - it says "three things happened" and nothing about
   whether they matter. A BLOCKING state is a different claim, so it renders as
   a --danger dot with no number at all: the product cannot do its job, and a
   count would invite reading it as "one of several". That is the whole of the
   colour budget here. See shared/DESIGN.md rule 3. */
.notif-count {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: var(--r-pill);
  background: var(--ink);
  color: var(--bg);
  font-family: var(--work);
  font-size: 10.5px;
  font-weight: 600;
  line-height: 17px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.notif-count.is-blocking {
  min-width: 9px;
  width: 9px;
  height: 9px;
  padding: 0;
  top: 0;
  right: 0;
  background: var(--danger);
}

.notif-panel {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  z-index: 40;
  width: min(380px, calc(100vw - 32px));
  max-height: min(70vh, 560px);
  display: flex;
  flex-direction: column;
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  overflow: hidden;
}
.notif-panel[hidden] {
  display: none;
}
.notif-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border);
}
.notif-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 14px;
  color: var(--ink);
}
.notif-body {
  overflow-y: auto;
}
.notif-group {
  display: flex;
  flex-direction: column;
}

/* One row. A button when it navigates, a div when it does not - so the two
   share every rule but the cursor, and a row with nowhere to go does not
   pretend to be pressable. */
.notif-item {
  width: 100%;
  text-align: left;
  display: block;
  padding: var(--s3) var(--s4);
  border: 0;
  border-bottom: 1px solid var(--border);
  background: transparent;
  font: inherit;
}
.notif-item-row {
  cursor: pointer;
  transition: background 0.22s var(--ease);
}
.notif-item-row:hover {
  background: var(--accent-soft);
}

/* Unread and blocking are marked on the LEFT EDGE rather than with a fill.
   A tinted row reads as a status, and neither of these is one - unread is about
   the reader, and blocking is about the product. A rule is the quietest thing
   that still separates them at a glance, and it is the device .row already uses
   for the active item. */
.notif-item.is-unread {
  border-left: 3px solid var(--ink);
}
.notif-item.is-blocking {
  border-left: 3px solid var(--danger);
}
.notif-item-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 14px;
  color: var(--ink);
  line-height: 1.35;
}
.notif-item-body {
  font-size: 12.5px;
  color: var(--faint);
  line-height: 1.5;
  margin-top: 3px;
}
.notif-item .btn {
  margin-top: var(--s2);
}
.notif-when {
  font-size: 11px;
  color: var(--faint);
  margin-top: 5px;
}
.notif-sep {
  padding: var(--s2) var(--s4);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  background: var(--cream);
  border-bottom: 1px solid var(--border);
}

/* An empty panel says so. "Nothing to report" and a blank box are different
   answers, and only one of them tells somebody the surface loaded. */
.notif-empty {
  padding: var(--s5) var(--s4);
  font-size: 13px;
  color: var(--faint);
  line-height: 1.6;
  margin: 0;
}

/* ── The mark, in the app header ──────────────────────────────────────────
   One ink for the whole lockup here. The cyan is the mark's colour on the
   marketing pages, where it is a bright drawing on a navy field and the only
   colour in the header; on this white card it was the one saturated thing
   beside a navy word, and the two read as separate logos sitting next to each
   other rather than as one. The `stroke` presentation attribute in the SVG is
   overridden by any CSS rule, so the markup stays shared. */
.brand-mark {
  left: 5px;
  position: relative;
  bottom: 0px;
  width: 26px;
  height: 26px;
  flex: none;
  transition: transform 0.22s var(--ease);
  /* Nudged right, off the card's inner edge and up against the word it belongs
     to: /styles.css leaves 5px between them, which was enough for the mark to
     float. */
  margin-left: 4px;
  margin-right: 1px;
}
.brand-mark path {
  stroke: var(--ink);
}
/* Same reason: the attribution is set in the wordmark's ink, not held back at
   60%. Size and weight already put it below the product name. */
.header-light .brand-by {
  color: var(--ink);
  opacity: 1;
}
.brand:hover .brand-mark {
  transform: rotate(-6deg) scale(1.06);
}

/* ── Press feedback ───────────────────────────────────────────────────────
   Every control on this page now answers the pointer within a frame, whether
   or not the work behind it is instant. Several buttons here fire a request
   and repaint only when it returns, so on a slow connection the honest read of
   a click was "nothing happened" - and the second click was the one that
   double-sent. The pressed state is CSS so it cannot be starved by JS, and
   .is-busy is applied for as long as the request is actually in flight. */
.btn,
.stat-tab,
.set-tab,
.app-tab,
.row {
  -webkit-tap-highlight-color: transparent;
}
.btn:active:not([disabled]),
.stat-tab:active,
.set-tab:active,
.app-tab:active {
  transform: translateY(1px) scale(0.985);
}
.row:active {
  transform: scale(0.995);
}
.btn:focus-visible,
.stat-tab:focus-visible,
.set-tab:focus-visible,
.app-tab:focus-visible,
.row:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
/* The button keeps its label and its width - a spinner that replaces the text
   makes the row jump, and a button that changes size under the cursor is how
   you mis-click the one beside it. */
.btn.is-busy {
  position: relative;
  color: transparent !important;
  pointer-events: none;
}
.btn.is-busy::after {
  content: "";
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 2px solid currentcolor;
  border-top-color: transparent;
  color: inherit;
  animation: spin 0.6s linear infinite;
}
.btn-primary.is-busy::after {
  border-color: #fff;
  border-top-color: transparent;
}
.btn.is-busy {
  color: transparent;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}
/* A control that just did something says so, briefly, without a status line. */
@keyframes did {
  0% {
    box-shadow: 0 0 0 0 var(--accent-soft);
  }
  100% {
    box-shadow: 0 0 0 12px transparent;
  }
}
.btn.did,
.stat-tab.did,
.set-tab.did {
  animation: did 0.5s var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .btn:active:not([disabled]),
  .stat-tab:active,
  .set-tab:active,
  .app-tab:active,
  .row:active {
    transform: none;
  }
  .btn.did,
  .stat-tab.did,
  .set-tab.did,
  .brand:hover .brand-mark {
    animation: none;
    transform: none;
  }
}

/* ── Upload progress ──────────────────────────────────────────────────────── */
.prog {
  margin-top: var(--s4);
}
.prog[hidden] {
  display: none;
}
.prog-track {
  height: 8px;
  border-radius: var(--r-pill);
  background: var(--accent-line);
  overflow: hidden;
}
.prog-bar {
  height: 100%;
  width: 0%;
  border-radius: var(--r-pill);
  background: var(--accent-ink);
  transition: width 0.25s var(--ease);
}
/* The parse phase has no byte count to report, so it crawls rather than
   claiming a percentage it cannot know. */
.prog.is-parsing .prog-bar {
  background-image: linear-gradient(
    90deg,
    transparent 0%,
    rgba(255, 255, 255, 0.45) 50%,
    transparent 100%
  );
  background-size: 40% 100%;
  animation: sheen 1.1s linear infinite;
}
@keyframes sheen {
  from {
    background-position: -60% 0;
  }
  to {
    background-position: 160% 0;
  }
}
.prog.is-error .prog-bar {
  background: var(--danger);
}
.prog-label {
  margin-top: 8px;
  font-size: 13px;
  color: var(--faint);
}

/* ── First run ────────────────────────────────────────────────────────────── */
.onboard {
  display: flex;
  justify-content: center;
  padding: var(--s7) var(--s4);
}
/* The eyebrow is the one thing in this card that inherits a left-aligned rule
   from the marketing sheet, and it read as a hanging indent above centred
   wording. */
.ob-card .eyebrow {
  justify-content: center;
}
.ob-card {
  width: min(640px, 100%);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--s7) var(--s6);
  text-align: center;
}
.ob-card .lede {
  margin: var(--s3) auto var(--s5);
  max-width: 46ch;
}
.drop-lg {
  padding: var(--s7) var(--s5);
}
.ob-actions {
  display: flex;
  gap: var(--s3);
  justify-content: center;
  align-items: center;
  margin-top: var(--s5);
  flex-wrap: wrap;
}
.ob-note {
  margin-top: var(--s4);
  font-size: 12.5px;
  color: var(--faint);
}
/* The one line on this screen that has to be read before the drop zone is
   used, so it gets the accent surface rather than the muted note colour the
   rest of the small print shares. */
.ob-required {
  margin: 0 auto var(--s5);
  max-width: 46ch;
  padding: 10px 16px;
  border: 1px solid rgba(31, 116, 136, 0.28);
  background: #eef7f9;
  color: var(--accent-ink);
  border-radius: var(--r-sm);
  font-size: 13.5px;
  line-height: 1.5;
}
/* The one decision this screen asks for, revealed once the resume has parsed.
   Reuses .modes rather than inventing a second radio treatment: the same
   control appears under Settings > How it runs, and somebody who sets it here
   and later goes looking for it should recognise what they are looking at.

   The card is centred and these are prose, so the alignment is reset here
   rather than in .modes - the Settings copy of the component sits in a
   left-aligned panel and must not inherit a fix it does not need. */
.ob-choice {
  margin-top: var(--s6);
  padding-top: var(--s6);
  border-top: 1px solid var(--border);
  text-align: left;
}
.ob-choice fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0; /* fieldsets refuse to shrink below their content otherwise */
}
.ob-choice-legend {
  display: block;
  width: 100%;
  padding: 0;
  margin-bottom: var(--s4);
  font-family: var(--work);
  font-weight: 500;
  font-size: 15px;
  color: var(--ink);
  text-align: center;
}
.ob-choice-note {
  margin-top: var(--s3);
  font-size: 12.5px;
  color: var(--faint);
  text-align: center;
}

/* Folded away until asked for. It is the way out of a screen with no way past
   it, not an option being offered alongside the upload. */
.ob-danger {
  margin-top: var(--s4);
  padding-top: var(--s4);
  border-top: 1px solid var(--border);
  text-align: left;
}
.ob-danger .fld {
  max-width: 22rem;
  margin-inline: auto;
}
.ob-danger .btn,
.ob-danger .status {
  display: block;
  margin: var(--s3) auto 0;
  text-align: center;
}

/* ── Waiting for the first match ──────────────────────────────────────────── */
.blank-pulse {
  display: flex;
  gap: 6px;
  justify-content: center;
  margin-bottom: var(--s4);
}
.blank-pulse span {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent-ink);
  opacity: 0.35;
  animation: bp 1.4s var(--ease) infinite;
}
.blank-pulse span:nth-child(2) {
  animation-delay: 0.18s;
}
.blank-pulse span:nth-child(3) {
  animation-delay: 0.36s;
}
@keyframes bp {
  0%,
  100% {
    opacity: 0.28;
    transform: translateY(0);
  }
  40% {
    opacity: 1;
    transform: translateY(-4px);
  }
}
@media (prefers-reduced-motion: reduce) {
  .blank-pulse span,
  .prog.is-parsing .prog-bar {
    animation: none;
  }
}
/* When the scan is done and this account is simply empty, the waiting
   animation would be a lie. renderBlank() removes it. */
.blank.is-settled .blank-pulse {
  display: none;
}
/* The row is a caption now, not navigation: the state tabs that used to fill
   it moved into the strip above. */
.col-head {
  flex-wrap: wrap;
}
.col-head-note {
  margin-left: auto;
  font-size: 12px;
  color: var(--faint);
  white-space: nowrap;
}
.col-head-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 14px;
  color: var(--ink);
}
.col-head-note:empty {
  display: none;
}

/* ── Matches that exist, when the current tab has none ────────────────────── */
/* An empty Pending list is the most common thing a returning user sees, and it
   used to hide the queued, sent and open-to-apply work behind three unvisited
   tabs - so a working account looked like an idle one. */
.spill {
  border-top: 1px solid var(--border);
  margin-top: var(--s2);
}
.spill-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s4) var(--s4) var(--s2);
}
.spill-head b {
  font-family: var(--work);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}
.spill-head span {
  font-size: 12px;
  color: var(--faint);
}

/* ── The strip is the state filter ────────────────────────────────────────── */
/* A tile that filters has to look pressable and has to say which one is on -
   the counts alone were mistaken for a read-only summary, which is why the
   list carried a duplicate set of tabs in the first place. */
.stat-tab {
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition:
    border-color 0.22s var(--ease),
    background 0.22s var(--ease),
    transform 0.22s var(--ease);
}
.stat-tab:hover {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}
.stat-tab.is-active {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
}
.stat-tab.is-active span {
  color: var(--accent-ink);
}
/* The cap is a number you cannot filter by, so it must not invite the press. */
.stat-meta {
  cursor: default;
}

/* ── Resume, as its own view ──────────────────────────────────────────────── */
/* A single card in the middle of the page. It has one job and no list beside
   it, so it does not use the two-column .work shell that Matches and Settings
   share. */
.work-single {
  max-width: 720px;
  margin: 0 auto;
  padding: var(--s6) var(--pad) var(--s8);
}
.resume-view .drop {
  padding: var(--s7) var(--s5);
}
/* Centred, like the first-run screen it mirrors - the same upload, in the same
   arrangement, so the second time you meet it you already know it. */
.resume-view .btn-row {
  justify-content: center;
}
.resume-view .hint,
.resume-view .prog-label {
  text-align: center;
}
.rv-title {
  text-align: center;
  margin: 0;
}
.rv-lede {
  text-align: center;
  max-width: 46ch;
  margin: var(--s3) auto var(--s2);
}

/* ── Brand, in the app header ─────────────────────────────────────────────── */
/* The stacked wordmark, the plate-less mark and their spacing all live in
   /styles.css now, on .brand itself, so the app header and the marketing pages
   cannot drift apart. Only the hover belongs here - the marketing header has
   its own motion language. */

/* The two notes under the list heading - what can be sent, and why the tab
   looks the way it does. Muted rather than a warning: "you open these
   yourself" is a normal outcome for more than half the pool, not an error.

   They are one block, not two rows. Each used to carry its own full-width
   rule and `padding: 0 var(--pad)`, and --pad is the page gutter - up to 56px
   - so inside a 380px column the text started a seventh of the way across,
   well right of the heading it explains, with no space above it and a hairline
   under each. Two banded strips, neither aligned to anything. Now: the
   heading's own gutter, air above and below, one surface, and a single rule
   where the block meets the first row. */
.col-head-sendable {
  margin: 0;
  padding: 10px var(--s3);
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
  background: var(--cream);
  text-wrap: pretty;
}
/* Only when the note above it is actually on screen - either can be hidden,
   and whichever comes first has to keep its own top padding. */
.col-head-sendable:not([hidden]) + .col-head-sendable {
  padding-top: 0;
}

/* ── The list's own filters ──────────────────────────────────────────────────

   Three groups on one wrapping row, sitting between the search box and the
   rows they narrow. Deliberately not in the header with the tabs: they are
   properties of THIS list, they reset when it does, and a control that changes
   what you are looking at belongs beside it.

   The chip is the tab bar's shape at the list's scale - pill, cream on hover,
   accent when on - so a user who has learnt what an active tab looks like has
   already learnt this. `aria-pressed` carries the state for anyone not seeing
   the colour, which is why tone is never the only carrier here. */
.list-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2) var(--s4);
  padding: 10px var(--s3);
  border-bottom: 1px solid var(--border);
}
.filter-set {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
  /* A group may wrap internally, but its label must never be orphaned from the
     first chip under it. */
  min-width: 0;
}
.filter-label {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  margin-right: 2px;
}
.chip-filter {
  font-family: var(--karla);
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
  color: var(--faint);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--r-pill);
  padding: 6px 11px;
  /* Below the 44px the tab bar uses, and on purpose: these are secondary
     controls inside a scrolling column, and a 44px row of them would push the
     first match off the fold on a phone - which is the complaint two of these
     chips exist to answer. 30px is still a comfortable touch target for a
     control this width, and they are never the only route to anything. */
  min-height: 30px;
  cursor: pointer;
  transition:
    background 0.22s var(--ease),
    color 0.22s var(--ease),
    border-color 0.22s var(--ease);
}
.chip-filter:hover {
  background: var(--cream);
  color: var(--ink);
}
.chip-filter.is-on {
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-color: var(--accent-line);
}
.chip-filter:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
/* The rule belongs to the block, so it appears once and only when there is a
   block to close. [hidden] is display:none, so a hidden note draws nothing. */
.col-head-sendable:not([hidden]) ~ .list {
  border-top: 1px solid var(--border);
}

/* The figures are the message, so they are set as figures: Work Sans over the
   Karla the sentence is in, ink over faint, and lining numerals so a column of
   them lines up. Everything around them stays quiet - promoting the count is
   only worth anything if the prose does not compete with it. */
.col-head-sendable b {
  font-family: var(--work);
  font-weight: 600;
  font-size: 13px;
  color: var(--ink);
  font-variant-numeric: tabular-nums lining-nums;
}

/* The open-to-apply breakdown: a label, one reason per line, and a closing
   aside. Three facts of the same kind, so they are set as three rows of the
   same shape rather than one sentence carrying all of them. */
.note-breakdown {
  display: grid;
  gap: 7px;
}
.note-eyebrow {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
/* The counts share a column and are right-aligned in it, so 7 and 85 end at
   the same place and the reasons start at one. */
.note-reasons {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  gap: 4px var(--s2);
}
.note-reasons b {
  text-align: right;
  line-height: 1.35;
}
.note-reasons > span {
  color: var(--body);
  line-height: 1.35;
}
/* Quieter than the reasons it follows: it is the way out of the list, not a
   fourth item in it. */
.note-tail {
  font-size: 11.5px;
  color: var(--faint);
}

/* Two columns so the will and the will-not are read together. They are the
   same size on purpose - the limits are not fine print. */
.tailor-rules {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
  margin: 12px 0 4px;
}
.tailor-rules ul {
  margin: 0;
  padding-left: 18px;
}
.tailor-rules li {
  margin: 0 0 5px;
  font-size: 0.88rem;
  color: #4a4a4a;
  line-height: 1.45;
}
.tailor-head {
  margin: 0 0 6px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.tailor-will .tailor-head {
  color: var(--accent-ink);
}
.tailor-wont .tailor-head {
  color: #a3521c;
}
.tailor-note {
  margin: 10px 0 0;
  font-size: 0.86rem;
  color: var(--body);
  line-height: 1.5;
}
.tailor-note[hidden] {
  display: none;
}

/* ── Match readiness ─────────────────────────────────────────────────────────

   The card in the Resume view that answers "why do I have no matches".

   Two tokens are added here rather than to styles.css because nothing on the
   marketing site shows a measured gain: --mr-gain is the one colour on the card,
   and it is spent on the single number the user is being asked to act on. The
   rest of the card is deliberately monochrome so that number has nothing to
   compete with. Light-only, like the whole dashboard - there is no data-theme
   attribute and no prefers-color-scheme block anywhere in this product, and a
   dark variant here would be styling a UI that does not exist. */
.mr-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.mr-verdict {
  font-family: var(--work);
  font-weight: 500;
  font-size: 17px;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.mr-basis {
  font-size: 12px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}

.mr-list {
  display: flex;
  flex-direction: column;
}
.mr-list:empty {
  display: none;
}
.mr-item {
  display: flex;
  align-items: flex-start;
  gap: var(--s4);
  padding: var(--s4) 0;
  border-top: 1px solid var(--border);
}
.mr-item:first-child {
  border-top: 0;
  padding-top: 0;
}
.mr-item:last-child {
  padding-bottom: 0;
}
.mr-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.mr-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 15px;
  color: var(--ink);
}
.mr-why {
  font-size: 13.5px;
  color: var(--body);
  line-height: 1.55;
}
.mr-act {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--s2);
  flex-shrink: 0;
}

/* Reads as data, not decoration: tabular figures, and the only colour here. */
.mr-gain {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-family: var(--work);
  font-variant-numeric: tabular-nums;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  padding: 3px 10px;
  border-radius: var(--r-pill);
  color: var(--success);
  background: var(--success-soft);
  border: 1px solid var(--success-line);
}
.mr-gain b {
  font-size: 14px;
  font-weight: 600;
}

.mr-ok {
  display: flex;
  align-items: center;
  gap: var(--s3);
}
.mr-ok-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--success);
  flex-shrink: 0;
}

/* The refusal, kept visible. The seniority lever is the strongest one there is
   and it is not offered; saying so in the card is what makes that a decision
   the user can see rather than an omission only the source code knows about. */
.mr-refuse {
  margin: var(--s4) 0 0;
  padding-top: var(--s4);
  border-top: 1px dashed var(--border-strong);
  font-size: 12.5px;
  color: var(--faint);
  line-height: 1.55;
}
.mr-refuse b {
  color: var(--body);
  font-weight: 600;
}
.mr-refuse[hidden],
.mr-card[hidden] {
  display: none;
}

@media (max-width: 560px) {
  .mr-item {
    flex-direction: column;
    gap: var(--s3);
  }
  .mr-act {
    flex-direction: row;
    align-items: center;
    align-self: stretch;
    justify-content: space-between;
  }
}

/* The same readiness card, mounted in the empty Matches list.

   Separated from the funnel text above it by a rule rather than a border box:
   the empty state is already a bordered column, and a card inside a card reads
   as two unrelated things when it is meant to read as the answer to the
   sentence directly above. */
.mr-inline {
  margin-top: var(--s5);
  padding-top: var(--s5);
  border-top: 1px solid var(--border);
  text-align: left;
}
.mr-inline .mr-head {
  margin-bottom: var(--s3);
}
/* The empty state centres its prose; the card is a list and must not inherit
   that, or every suggestion sits mid-line with its button adrift. */
.mr-inline .mr-why,
.mr-inline .mr-title,
.mr-inline .mr-refuse,
.mr-inline .notice {
  text-align: left;
}

/* ── Why this matched ────────────────────────────────────────────────────────

   The scoring breakdown, which the server computes for every match and the
   dashboard used to read only to check for "out_of_region".

   Tone is never the only carrier: every chip says its meaning in words, so
   "outside the regions you set" reads the same to someone who cannot separate
   amber from grey. */
.why {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin: var(--s4) 0 0;
}
.why-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
.why-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.chip-why {
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  padding: 4px 10px;
  border-radius: var(--r-pill);
  background: var(--cream);
  color: var(--body);
  border: 1px solid var(--border);
}
.chip-good {
  background: var(--success-soft);
  color: var(--success);
  border-color: var(--success-line);
}
.chip-warn {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: var(--warn-line);
}
.chip-accent {
  background: var(--accent-soft);
  color: var(--accent-ink);
  border-color: var(--accent-line);
}
.why[hidden] {
  display: none;
}

/* The two lines the sender appends to every application.

   Shown in the compose pane, and deliberately not editable: there is no setting
   that removes them, so presenting them as a field somebody could clear would
   be a lie about what the product does. Muted, because it is not the
   applicant's writing and should not read as though it were - but present,
   because discovering it in a recruiter's reply would be worse. */
.f-footer {
  margin-top: 10px;
  padding: 10px 12px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.02);
}
.f-footer[hidden] {
  display: none;
}
.f-footer-tag {
  display: block;
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 6px;
}
.f-footer pre {
  margin: 0;
  font-family: inherit;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--faint);
  white-space: pre-wrap;
  word-break: break-word;
}

/* ── The Extension page ─────────────────────────────────────────────────────

   Built from the shared palette in /styles.css and nothing else. The first
   version of this block invented three tokens that do not exist anywhere in
   ULYX - `--line`, `--surface` and `--muted` - and gave each a hex fallback, so
   every one silently painted an off-palette colour beside the real ones:
   #d9dcd3 next to --border #e7e7e2, and a green-grey #6a716a next to --body
   #535353. The fallback is what made that invisible, which is why there are
   none here: a token that stops existing must break loudly rather than quietly
   render a colour nobody chose.

   Light-only, like the rest of the dashboard - there is no data-theme attribute
   and no prefers-color-scheme block in this product, and a dark variant here
   would be styling a UI that does not exist. */
/* Every view in this dashboard is full-bleed, and the inset comes from a
   wrapper inside it: .work is `var(--s5) var(--pad) var(--s8)` and .work-single
   is the centred variant. This page had NEITHER, so the pill, the heading, the
   lede and all four cards sat flush against the left edge of the window while
   the header and the banners above them were inset by --shell. Measured in a
   browser: #view-extension ran 0 -> 1728 on a 1728px viewport.
   
   The padding is taken rather than the class, because .work is a
   380px + 1fr grid built for the settings aside and this page is a single
   column with a card grid in it. Same measurements, right layout. */
.ext-view {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  padding: var(--s5) var(--pad) var(--s8);
}
/* ── The intro: the pitch on the left, the number on the right ──────────────
   Two columns rather than two stacked blocks. The lede is capped at 62ch for
   readability, which in a 1240px shell left roughly half the row empty and put
   the stat card - the one thing on this page that is about THIS user - alone
   underneath with its 44px number stranded at the far left.

   The text column is the wider of the two and takes the fraction, so the aside
   stays a readable card rather than growing to half a widescreen. */
.ext-intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(300px, 380px);
  gap: var(--s6);
  align-items: start;
}
.ext-head {
  display: flex;
  flex-direction: column;
  /* The pill, the heading and the lede are one block, so they sit closer to
     each other than the block does to anything else. h1 carries its own
     margins from the brand sheet and they were doing the spacing here by
     accident - stated, so the rhythm is the same whatever that file does. */
  gap: var(--s3);
}
.ext-head h1 {
  margin: 0;
}
.ext-lede {
  max-width: 62ch;
  color: var(--body);
  margin: 0;
}

/* Below the two-column breakpoint the aside goes under the pitch, where it
   still reads in the right order: what it does, then whether it is worth it. */
@media (max-width: 900px) {
  .ext-intro {
    grid-template-columns: 1fr;
    gap: var(--s4);
  }
}

.pill-testing {
  align-self: flex-start;
  font-family: var(--karla);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: var(--r-pill);
  border: 1px solid var(--warn-line);
  background: var(--warn-soft);
  color: var(--warn);
}

/* The number is the point of the page, so it is allowed to be large - and it is
   the only colour on the view, so it has nothing to compete with. The same
   reasoning .mr-head is built on. */
.ext-stat {
  display: flex;
  /* Stacked, not on one baseline. In a full-width row the number and its
     sentence sat on the same line with the number pinned left; in a 380px
     card the number reads as the headline it is and the sentence explains it
     underneath, which is the relationship between them anyway. */
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s1);
  padding: var(--s5);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg);
}
.ext-stat-n {
  font-family: var(--work);
  font-size: 52px;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--accent-ink);
}
.ext-stat-of {
  color: var(--body);
}
.ext-stat-of b {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.ext-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  /* 12px between 285px cards read as a seam rather than a gap, and it was half
     the gap the intro above uses. --s4 keeps four cards on one row at this
     width while letting them breathe. */
  gap: var(--s4);
  /* Equal heights. These four were 495, 241, 522 and 331 tall - a row of cards
     with four different bottoms reads as a layout that broke rather than as
     four cards with different amounts to say. Content still sits at the top of
     each; it is the boxes that line up. */
  align-items: stretch;
}
/* Deliberately the same shape as a settings fieldset, so the two read as one
   product rather than as a page bolted on. */
.panel-card {
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg);
  padding: var(--s5);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  margin: 10px;
}
.panel-card h2 {
  margin: 0;
  font-family: var(--work);
  font-weight: 500;
  font-size: 17px;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.ext-steps {
  margin: 0;
  padding-left: 1.2em;
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}
.ext-steps li {
  color: var(--body);
  font-size: 13px;
}

/* The pairing code. Monospaced and widely tracked because it is read off this
   screen and typed into another one. */
.pair {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.pair-code {
  font-family: var(--mono);
  font-size: 24px;
  letter-spacing: 0.22em;
  color: var(--ink);
  padding: 6px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--cream);
  user-select: all;
}

/* ── The two install paths ───────────────────────────────────────────────────
   Only one primary button shows at a time (see renderInstallPath), so this is
   layout for whichever one it is, plus the disclosure the other hides behind. */
.ext-install-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}
.ext-fallback {
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}
.ext-fallback > summary {
  cursor: pointer;
  color: var(--ink);
  font-size: 14px;
}
.ext-fallback > summary:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* ── Admin ───────────────────────────────────────────────────────────────────
   The operator's view. Drawn by admin.js in React; everything here is layout
   and every colour is a token from /styles.css, because the argument for
   hand-drawing the charts instead of taking a library was that they had to
   read as the same product as the rest of this file. If a hex appears below,
   that argument has been lost.

   It reuses .stat and .panel-card as they are rather than restating them. The
   strip on the Matches view and the tiles here are the same object doing the
   same job, and a second definition that merely resembled the first is exactly
   the drift scripts/sync-shared-assets.mjs exists to prevent one level up. */
.adm-shell {
  padding: var(--s5) var(--pad) var(--s8);
}
.adm-boot,
.adm-state {
  padding: var(--s7) 0;
  text-align: center;
}
.adm-failed {
  color: var(--danger);
  font-size: 14px;
}
.adm-error {
  color: var(--danger);
  font-size: 14px;
  margin: 0 0 var(--s4);
}

.adm-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s5);
  flex-wrap: wrap;
  margin-bottom: var(--s5);
}
.adm-title {
  font-family: var(--work);
  font-weight: 500;
  font-size: 24px;
  letter-spacing: -0.03em;
  color: var(--ink);
  margin: 0;
}
.adm-sub {
  margin: 6px 0 0;
  max-width: 68ch;
  font-size: 13px;
  line-height: 1.55;
}

/* Same grid as .stat-strip, one row down the page. Six tiles at a comfortable
   width, folding to three and then two rather than to one - a single column of
   headline numbers is a list, and the point of a strip is comparison. */
.adm-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s3);
  margin-bottom: var(--s5);
}
.adm-tile-sub {
  display: block;
  margin-top: 6px;
  font-size: 12px;
  font-style: normal;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--faint);
  line-height: 1.4;
}

/* Two columns, with the ones that need the room spanning both. A chart of 30
   points in a 380px column is a smudge, and the funnel is a list of sentences
   - both of them lose more to a narrow column than the density gains. */
.adm-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s4);
  align-items: start;
}
.adm-card-wide {
  grid-column: 1 / -1;
}
@media (max-width: 900px) {
  .adm-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
.adm-card {
  gap: var(--s4);
}
.adm-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}
.adm-note {
  font-size: 12px;
  color: var(--faint);
}

/* ── Charts ───────────────────────────────────────────────────────────────── */
.adm-chart svg {
  display: block;
  width: 100%;
  overflow: visible;
}
/* Axis and tick type. 11px is the floor at which tabular numerals stay
   legible against --faint at this contrast; below it the labels are decoration
   pretending to be a scale. */
.adm-axis {
  font-family: var(--karla);
  font-size: 11px;
  fill: var(--faint);
  font-variant-numeric: tabular-nums;
}
.adm-donut-total {
  font-family: var(--work);
  font-size: 22px;
  font-weight: 500;
  letter-spacing: -0.03em;
  fill: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* The value under a series, which doubles as the hover readout. It has a fixed
   line so the card does not resize as the cursor crosses the chart - a panel
   that grows a pixel on hover is the sort of thing that reads as a bug. */
.adm-readout {
  margin: var(--s2) 0 0;
  min-height: 18px;
  font-size: 12.5px;
  color: var(--faint);
}
.adm-readout b {
  font-family: var(--work);
  font-weight: 500;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.adm-empty {
  margin: 0;
  padding: var(--s5) 0;
  text-align: center;
  font-size: 13px;
  color: var(--faint);
}

/* ── Funnel ───────────────────────────────────────────────────────────────── */
.adm-funnel {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.adm-funnel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
}
.adm-funnel-stage {
  font-family: var(--work);
  font-size: 14px;
  color: var(--ink);
}
.adm-funnel-head b {
  font-family: var(--work);
  font-weight: 500;
  font-size: 18px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}
.adm-funnel-track {
  margin-top: 6px;
  height: 10px;
  border-radius: var(--r-pill);
  background: var(--cream);
  overflow: hidden;
}
.adm-funnel-fill {
  display: block;
  height: 100%;
  border-radius: var(--r-pill);
  transition: width 0.4s var(--ease);
}
.adm-funnel-note {
  margin: 6px 0 0;
  font-size: 12px;
  color: var(--faint);
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  flex-wrap: wrap;
}
/* The loss, not the survival. "83% lost here" is the number an operator is
   looking for, and printing the complement instead makes them do the
   subtraction on every row. */
.adm-funnel-drop {
  color: var(--warn);
  font-variant-numeric: tabular-nums;
}

/* ── Donut and legend ─────────────────────────────────────────────────────── */
.adm-donut {
  display: flex;
  align-items: center;
  gap: var(--s5);
  flex-wrap: wrap;
}
.adm-legend {
  list-style: none;
  margin: 0;
  padding: 0;
  flex: 1 1 200px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}
.adm-legend li {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 13px;
  color: var(--body);
}
.adm-legend i {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: 0 0 auto;
}
.adm-legend span {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.adm-legend b {
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  font-weight: 500;
}
.adm-legend em {
  font-style: normal;
  color: var(--faint);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  min-width: 34px;
  text-align: right;
}
.adm-legend-inline {
  flex-direction: row;
  gap: var(--s4);
  margin-bottom: var(--s3);
}

/* ── Horizontal bars ──────────────────────────────────────────────────────── */
.adm-bars,
.adm-compare-rows {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.adm-bars li,
.adm-compare-rows li {
  display: grid;
  grid-template-columns: minmax(0, 8.5rem) minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
}
.adm-bar-label {
  font-size: 12.5px;
  color: var(--body);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The qualifier under a bar's name - "41% contactable". On its own line
   because as part of the label it was the half the ellipsis ate, and it is the
   number the card exists to show. */
.adm-bar-label em {
  display: block;
  font-style: normal;
  font-size: 11px;
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
}
.adm-bar-track {
  position: relative;
  height: 14px;
  border-radius: var(--r-pill);
  background: var(--cream);
  overflow: hidden;
}
.adm-bar-fill,
.adm-bar-sub {
  position: absolute;
  inset: 0 auto 0 0;
  border-radius: var(--r-pill);
  transition: width 0.4s var(--ease);
}
/* Drawn INSIDE the outer bar rather than beside it, because it is a subset of
   it: "of the 202 postings from this source, 57 carry an address". Two bars
   side by side would invite reading them as independent quantities. */
.adm-bar-sub {
  height: 100%;
}
.adm-bar-value {
  font-family: var(--work);
  font-weight: 500;
  font-size: 13px;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.adm-bar-sub-value {
  font-style: normal;
  font-weight: 400;
  color: var(--success);
  margin-left: 6px;
}
.adm-compare-pair {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.adm-compare-pair .adm-bar-track {
  height: 10px;
}
.adm-compare .adm-bar-sub-value {
  color: var(--faint);
}

/* ── Tables ───────────────────────────────────────────────────────────────── */
.adm-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.adm-table th {
  text-align: left;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 0 0 6px;
  border-bottom: 1px solid var(--border);
}
.adm-table td {
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
  color: var(--body);
  white-space: nowrap;
}
.adm-table tr:last-child td {
  border-bottom: 0;
}
/* Only the FIRST column is prose, and only it may be truncated. Every column
   after it is a short value that has to stay whole - `width: 1%` on all of
   them made two of them share the leftovers, which is how "review" rendered as
   "revie…" beside a header reading "MODESENT". */
.adm-table th:first-child,
.adm-table td:first-child {
  width: 100%;
  max-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.adm-table th:not(:first-child),
.adm-table td:not(:first-child) {
  width: 1%;
  text-align: right;
  padding-left: var(--s4);
}
.adm-num {
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ── Three standing figures, side by side ─────────────────────────────────── */
.adm-pairs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--s4);
}
.adm-pairs b {
  display: block;
  font-family: var(--work);
  font-weight: 500;
  font-size: 26px;
  letter-spacing: -0.04em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}
.adm-pairs span {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
.adm-pairs p {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.5;
}

/* ── Regions ─────────────────────────────────────────────────────────────────
   A checkbox per region, laid out like the source list because it is the same
   kind of control: a closed set the server owns, rendered from what GET
   /api/me serves. It replaced a free-text box over five accepted values, where
   every plausible thing a person typed - "London", "England", "Europe" - was
   discarded in silence by canonicalRegions(), and regions is a hard filter, so
   what the user actually saw was an empty dashboard. */
.regions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s2);
  margin-top: 6px;
}
/* The consequence of the current selection, under the control that sets it.
   An empty set of checkboxes reads as unfinished, and it is a legitimate state
   - canonicalRegions treats it as "no filter", which fails open to more
   matches rather than to none. */
.fld-hint {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--faint);
}

/* ── Master and detail, on a screen that only fits one ────────────────────────
   Above 900px both columns are on screen and this control has no job. Below it
   they stack, and selecting a match used to render a pane 243px below the fold
   with no scroll to it - so on the device carrying most of the traffic, the
   core interaction of the product produced no visible change at all.

   Everything here is inside the media query on purpose. The class is set by
   app.js without checking the width a second time, so a resize is resolved by
   the browser rather than by a listener that has to agree with this file. */
.detail-back {
  display: none;
}
@media (max-width: 900px) {
  .work.is-detail .col-list {
    display: none;
  }
  /* The strip is the list's filter, and the list is not on this screen. It is
     313px on a 390px-wide phone - more than a third of the viewport spent on
     controls for something that is not visible.

     `:has()` and not a sibling combinator: the strip comes BEFORE .work in the
     document, and `~` only reaches forwards. */
  #view-matches:has(.work.is-detail) .stat-strip {
    display: none;
  }
  .work.is-detail .detail-back {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    /* Full width and 44px tall: it is the only way back at this size, and it
       is pressed with a thumb. */
    width: 100%;
    min-height: 44px;
    margin-bottom: var(--s3);
    padding: 10px 14px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--r-sm);
    font: inherit;
    font-size: 14px;
    color: var(--ink);
    cursor: pointer;
  }
  .work.is-detail .detail-back:hover {
    border-color: var(--accent-line);
    background: var(--accent-soft);
  }
  .work.is-detail .detail-back:focus-visible {
    outline: 2px solid var(--accent-ink);
    outline-offset: 2px;
  }
  /* The heading is focused when the pane opens so a screen reader lands on the
     job rather than on a row that is now hidden. It is a heading, not a
     control, so it must not draw a focus ring for a mouse user. */
  #j-title:focus {
    outline: none;
  }
  #j-title:focus-visible {
    outline: 2px solid var(--accent-ink);
    outline-offset: 4px;
    border-radius: 4px;
  }
}

/* ── Closing the loop ────────────────────────────────────────────────────────
   Everything below belongs to the half of the product that used to end at
   "sent": recording that the user applied themselves, what came of it, and
   whether any of it is working. */

/* The user's own last thirty days. One line, sitting where the stat strip's
   counters of the present instant leave off. Not a card: the header and the
   announcement bar already take 320px of a 390px phone. */
.activity {
  margin: 0;
  padding: var(--s3) var(--pad) 0;
  font-size: 13px;
  line-height: 1.55;
  color: var(--faint);
}
.activity b {
  font-family: var(--work);
  font-weight: 500;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* ── What came of it ───────────────────────────────────────────────────────── */
.outcome {
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--cream);
  padding: var(--s3) var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.outcome-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  flex-wrap: wrap;
}
.outcome-label {
  font-family: var(--work);
  font-weight: 500;
  font-size: 14px;
  color: var(--ink);
}
/* The follow-up nudge, and only once it is earned. Amber rather than red: a
   week of silence is normal, not a failure. */
.outcome-note {
  font-size: 12.5px;
  color: var(--warn);
}
.outcome-note:empty {
  display: none;
}
.outcome-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}
.outcome-choice {
  font: inherit;
  font-size: 13px;
  min-height: 34px;
  padding: 5px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-pill);
  background: var(--bg);
  color: var(--body);
  cursor: pointer;
  transition:
    border-color 0.2s var(--ease),
    background 0.2s var(--ease);
}
.outcome-choice:hover {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
}
.outcome-choice:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
/* Pressing the one already chosen clears it, so the on state has to read as
   something you can press again rather than as a completed step. */
.outcome-choice.is-on {
  border-color: var(--accent-ink);
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 600;
}

/* ── Copying a draft you cannot send ───────────────────────────────────────── */
.copybar {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  padding: var(--s3);
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm);
  background: var(--surface, var(--bg));
}
.copybar-label {
  font-size: 13px;
  color: var(--faint);
  margin-right: auto;
}
/* ── Follow-ups waiting for a yes ──────────────────────────────────────────
   The only thing on this page that is an INBOX rather than a description, so it
   is the one block here allowed to look like it wants something. Bordered
   rather than tinted: the estate is nearly monochrome and a coloured panel
   would read as an alert, which this is not. */
.followups {
  margin: var(--s3) 0 0;
  padding: var(--s2) var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
}
.followups-h {
  margin: 0;
  padding: var(--s2) 0;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}
.followups[open] > .followups-h {
  margin-bottom: var(--s3);
}
.followups-h:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
.followup-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s3);
}
.followup + .followup {
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
}
.followup-head {
  font-size: 13px;
  font-weight: 600;
}
/* Verbatim and in full. A truncated preview would be asking somebody to
   approve a message they have not read, which is the one thing this control
   must not do. `pre-wrap` keeps the paragraph breaks the drafter wrote. */
.followup-body {
  margin: var(--s2) 0;
  white-space: pre-wrap;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ink);
  /* Bounded, and scrolls rather than truncates.
     The first version showed the whole body inline on the grounds that
     approving a message you have not read is the one thing this control must
     not ask for. That is still true - but at 1320x900 the panel took 450px and
     pushed the match list, which is what this page is FOR, entirely below the
     fold. Scrolling keeps every word reachable and bounds what it costs the
     page; a fade would hide text, which is the thing being avoided. */
  max-height: 180px;
  overflow-y: auto;
}
.followup-to {
  margin: 0 0 var(--s2);
  font-size: 12px;
  color: var(--faint);
}
.followup-actions {
  display: flex;
  gap: var(--s2);
  flex-wrap: wrap;
}

/* ── How far applications actually got ─────────────────────────────────────
   A <details>, not a card. Most accounts open this to a row of zeros - outcomes
   are reported and almost nobody has reported one - and a permanently-expanded
   panel of zeros reads as a broken product. Collapsed, the summary line is one
   sentence that says which kind of zero it is. */
.pipeline {
  margin: var(--s3) 0 0;
  /* The same side padding as .activity and .stat-strip above it. Without it
     the summary sat flush against the viewport edge, outside the page. */
  padding: 0 var(--pad);
  font-size: 13px;
  color: var(--faint);
}
.pipeline > summary {
  cursor: pointer;
  padding: var(--s2) 0;
}
.pipeline > summary:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
.pipeline-body {
  padding: var(--s2) 0 0;
}
.funnel {
  list-style: none;
  margin: 0 0 var(--s2);
  padding: 0;
  display: grid;
  gap: 4px;
}
/* The bar sits BEHIND its own label rather than beside it. A separate bar
   column would either crush the labels on a 390px phone or need a second
   breakpoint; this reads at any width and needs neither. */
.funnel-step {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  padding: 3px var(--s2);
  isolation: isolate;
}
.funnel-bar {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: -1;
  /* --border, not --cream. The first version used --cream (#f7f6f3) against a
     near-white page and the bars were invisible - the funnel rendered as a
     plain list of numbers, which is exactly the thing it was built to stop
     being. Caught by looking at it. */
  background: var(--border);
  border-radius: var(--r-sm);
  /* A stage with nothing in it still renders its row; the bar just has no
     width. An absent row would read as "not applicable" rather than "none". */
  min-width: 0;
}
.funnel-name {
  flex: 1;
}
.funnel-n {
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}
.pipeline-note {
  margin: var(--s2) 0;
  font-size: 12px;
  line-height: 1.5;
}
.srctable {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  margin-top: var(--s3);
}
.srctable caption {
  text-align: left;
  padding-bottom: var(--s2);
  color: var(--faint);
  font-size: 12px;
  line-height: 1.5;
}
.srctable th,
.srctable td {
  text-align: right;
  padding: 4px var(--s2);
  border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
.srctable thead th,
.srctable tbody th {
  text-align: left;
  font-weight: 600;
}
.srctable tbody th {
  color: var(--ink);
}

/* ── Row tags ─────────────────────────────────────────────────────────────── */
/* A posting old enough that it is probably filled. Quiet, not alarming: the
   applicant is the one who would otherwise spend an application finding out. */
.tag-stale {
  color: var(--warn);
  border-color: var(--warn-line);
  background: var(--warn-soft);
}
.tag-outcome {
  border-color: var(--border-strong);
}
.tag-replied,
.tag-interview {
  color: var(--success);
  border-color: var(--success-line);
  background: var(--success-soft);
}
.tag-rejected,
.tag-no_reply,
.tag-withdrawn {
  color: var(--faint);
}
/* The two endings worth seeing across a list. Same treatment as replied and
   interview above, because reaching one of them is the same kind of fact. */
.tag-offer,
.tag-hired {
  color: var(--success);
  border-color: var(--success-line);
  background: var(--success-soft);
  font-weight: 700;
}

/* ── What lines up, and what does not ──────────────────────────────────────
   Written by cron/insights.ts over the top of each user's queue. Absent on
   most rows by design, so this whole strip has to be invisible when empty
   rather than leaving a gap where a card looks like it failed to load. */
.row-fit {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: var(--s2);
}
.fit {
  font-size: 11px;
  line-height: 1.35;
  padding: 2px 0;
  color: var(--faint);
}
/* Marked with a glyph rather than with colour alone. The estate is nearly
   monochrome on purpose, and a red/green pair carrying the whole meaning is
   both off-language here and unreadable to the people most likely to be
   scanning a long list quickly. */
.fit::before {
  font-weight: 700;
  margin-right: 4px;
}
.fit-yes::before {
  content: "+";
  color: var(--success);
}
.fit-no::before {
  content: "\2212";
  color: var(--warn);
}
.fit + .fit {
  padding-left: var(--s2);
  border-left: 1px solid var(--border);
}

@media (max-width: 900px) {
  /* The activity line belongs to the list, like the strip above it. */
  #view-matches:has(.work.is-detail) .activity {
    display: none;
  }
}

/* ── Referrals ──────────────────────────────────────────────────────────────
   Almost nothing new. The view reuses .stat-strip for the two halves, .work
   for the shell, .row for the list and .panel-card for a candidate - the same
   furniture as Matches and the Extension page, so the tab reads as part of the
   product rather than as a second one bolted on.

   What is here is only what those classes cannot do: a detail pane that stacks
   cards rather than holding one form, and a code input that should not be as
   wide as the column it sits in. */
#ref-detail .compose {
  gap: var(--s4);
}
#ref-detail .panel-card {
  gap: var(--s3);
}
/* The six-digit code, and nothing else, is a short field. A full-width box for
   six characters reads as the wrong input. */
#ref-detail input[inputmode="numeric"] {
  max-width: 160px;
  font-family: var(--mono);
  letter-spacing: 0.3em;
}
/* A tile that names a mode rather than counting one - "Refer someone" - has no
   number to be large about. */
.stat-tab b {
  overflow-wrap: anywhere;
}

/* ── The assistant ──────────────────────────────────────────────────────────
   No new visual language. Every colour, radius, spacing step and easing curve
   below is a token from /styles.css, and the layout is the .work shell Matches
   and Settings already use - so the only genuinely new things here are the
   message list and the composer.

   The decision worth writing down is what this DOES NOT do: no chat bubbles.
   Opposing tinted bubbles are the convention everywhere else, and in this
   palette - flat, near-monochrome, surfaces separated by a 1px rule and the
   cream showing through - two coloured fills per exchange would be the loudest
   thing in the product, and would be saying what the speaker labels already
   say. So a turn is a block with a hairline under it, exactly like a row in a
   list, and the only colour spent is on the tool trace. */

/* .sr-only now lives in /styles.css: the landing page loads that sheet and
   nothing else, and one definition beats two that can drift. */

/* ── Both panels fill the window ────────────────────────────────────────────
   A conversation list with two entries in it was a 217px card ending in
   mid-air next to a 597px one, with a band of cream under both. Two panels
   that disagree about where the page ends read as a layout that broke rather
   than as one that is empty.

   The height is taken from the FLEX CHAIN rather than from a `100svh - Npx`
   calculation, because there is no correct N: the announcement bar above this
   is dismissible, so the work area starts at 159px with it and about 100px
   without, and any constant is wrong in one of those two states. Making main a
   column when this view is the visible one lets the bar take its natural
   height and this fill whatever is left, in both.

   Scoped with :has() so no other view's layout changes - Matches and Settings
   size to their content on purpose, and a Settings panel stretched to the
   window would be a fieldset with 400px of nothing under it. The codebase
   already uses :has() for the same kind of "style the parent from the state of
   a child" problem in the mobile match strip. */
@media (min-width: 901px) {
  /* ── The chain needs a ceiling, not just a floor ──────────────────────────
     The block below gave `main` a flex column and every descendant `flex: 1`,
     and that only ever produced a full-height layout because `main` carries
     `min-height: calc(100svh - 96px)`. A floor. Nothing above it was BOUNDED,
     so once the conversation list grew past that floor `main` sized to its
     content instead, `flex: 1` resolved against the content height, and both
     columns grew together - 2,555px of panel in a 906px window, with the page
     scrolling and the composer pushed off the bottom. The list's own
     `overflow-y: auto` could not help: an element that sizes to its content
     never has anything to scroll.

     Reported against an account with a dozen conversations. It cannot happen
     with one, which is exactly why the guard on this layout never saw it - it
     asserted the panels FILL the window, and filling was the half that already
     worked.

     So the ceiling is put where there is a real one: the window. `body`
     becomes the flex column and `main` the item that takes what the header
     leaves, which is still no constant anywhere - the reason the note below
     gives for refusing `100svh - Npx` holds exactly as it did, and this is
     that argument carried one level further up rather than abandoned. `main`'s
     floor is dropped here because a floor and a ceiling on the same box means
     the taller of the two wins, which is the bug.

     Scoped to this view like everything else in this block: Matches and
     Settings size to their content and scroll the page, and a body that could
     not scroll would strand them. */
  body:has(#view-assistant:not([hidden])) {
    height: 100svh;
    display: flex;
    flex-direction: column;
    /* The panels scroll inside themselves; the page behind them must not also
       scroll, or a trackpad flick moves whichever the pointer is not over. */
    overflow: hidden;
  }
  main:has(#view-assistant:not([hidden])) {
    display: flex;
    flex-direction: column;
    /* Takes what the header leaves, and no more. `main`'s own
       `min-height: calc(100svh - 96px)` is overridden rather than left to
       fight this: a floor and a ceiling on one box means the taller wins. */
    flex: 1 1 auto;
    min-height: 0;
  }
  main:has(#view-assistant:not([hidden])) #view-assistant {
    flex: 1;
    display: flex;
    flex-direction: column;
    /* Without this a flex item refuses to shrink below its content, and the
       internal scrollers below never get a bounded height to scroll inside. */
    min-height: 0;
  }
  #chat-work {
    flex: 1;
    min-height: 0;
    /* .work sets `start`, which is what makes the two columns disagree. */
    align-items: stretch;
  }
  /* The list becomes a column so its scroller can take the leftover space
     rather than the card ending wherever the last row happens to. */
  #chat-work .col-list {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  #chat-work .list {
    flex: 1;
    /* Load-bearing on a flex child that scrolls, and the same line
       `#view-matches .list` already carries for the same reason: without it
       the rows insist on their full content height and the column grows past
       the window instead of the list scrolling inside it. Matches learned this
       first; this copy was written without it. */
    min-height: 0;
    /* .list caps itself at min(70vh, 760px) for the referral and settings
       columns, which here would reintroduce the short card this whole block
       removes. Matches lifts the same cap its own way - see
       `#view-matches .list` - and the two are scoped apart on purpose: a rule
       written on the bare class reaches all four views that use it, and on
       2026-08-20 one written that way broke exactly this layout. */
    max-height: none;
  }
}

/* ── Letting the columns shrink ─────────────────────────────────────────────
   A grid item defaults to `min-width: auto`, which means it refuses to become
   narrower than its own min-content - so at 375px both columns computed to
   384px inside a 335px track and the whole page scrolled sideways. Matches
   does not hit this because nothing in it has a wide min-content floor; the
   composer here does, being a textarea with an intrinsic width next to a
   fixed-width button.

   This is load-bearing twice. The second time is the tables the model writes:
   `overflow-x: auto` on .chat-tablewrap cannot contain anything while its
   ancestors are free to stretch instead, so without this the scroller would
   silently do nothing and a five-column table would push the page sideways
   exactly the same way. Scoped to this view rather than added to .col-list and
   .col-detail, which are shared with Matches and are not broken. */
#chat-work > * {
  min-width: 0;
}

/* The right column is a column: log grows, composer stays put at the bottom.
   Height is capped rather than left to the content so the composer does not
   walk down the page as a conversation gets longer - the field you type into
   should be in the same place for the ninth question as for the first. */
.chat-detail {
  display: flex;
  flex-direction: column;
  /* A floor for the case the grid row cannot fill - a very short window, and
     mobile, where the stretch rules above do not apply. The ceiling that used
     to be here is gone: the row height is the ceiling now, and keeping both
     meant the shorter of the two always won. */
  min-height: 480px;
  padding: 0;
}
.chat-log {
  flex: 1;
  overflow-y: auto;
  padding: var(--s5);
}

/* ── One exchange ─────────────────────────────────────────────────────────── */
.chat-turn {
  padding-bottom: var(--s5);
  margin-bottom: var(--s5);
  border-bottom: 1px solid var(--border);
}
.chat-turn:last-child {
  border-bottom: 0;
  margin-bottom: 0;
}
/* The question, in the display face at the weight .row-title uses. It is the
   thing being answered, so it reads as the heading of its own block. */
.chat-q {
  font-family: var(--work);
  font-weight: 500;
  font-size: 15px;
  line-height: 1.4;
  color: var(--ink);
  margin: 0 0 var(--s3);
}
.chat-a {
  font-size: 14.5px;
  /* 1.65 rather than the 1.5 body text uses elsewhere: these are paragraphs
     read once, in a narrow column, next to tables and lists. */
  line-height: 1.65;
  color: var(--body);
}
.chat-a > *:first-child {
  margin-top: 0;
}
.chat-a > *:last-child {
  margin-bottom: 0;
}
.chat-a p {
  margin: 0 0 var(--s3);
}
.chat-a ul,
.chat-a ol {
  margin: 0 0 var(--s3);
  padding-left: var(--s5);
}
.chat-a li {
  margin-bottom: var(--s1);
}
.chat-a strong {
  color: var(--ink);
  font-weight: 600;
}
.chat-a code {
  font-family: var(--mono);
  font-size: 12.5px;
  background: var(--cream);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 1px 5px;
  color: var(--body);
}
.chat-a a {
  color: var(--accent-ink);
}

/* A table the model wrote can be wider than the column on a phone. It scrolls
   inside its own box rather than pushing the page sideways - the one thing a
   dashboard must never do. */
.chat-a .chat-tablewrap {
  overflow-x: auto;
  margin: 0 0 var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}
.chat-a table {
  border-collapse: collapse;
  width: 100%;
  font-size: 13px;
}
.chat-a th,
.chat-a td {
  text-align: left;
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.chat-a th {
  font-family: var(--work);
  font-weight: 500;
  color: var(--ink);
  background: var(--cream);
}
.chat-a tr:last-child td {
  border-bottom: 0;
}
/* Counts and scores line up under each other rather than wandering by glyph
   width. The figures are the reason the table exists. */
.chat-a td {
  font-variant-numeric: tabular-nums;
}

/* ── What it looked at ────────────────────────────────────────────────────
   The tool trace, under the answer. It is the receipt: every figure above came
   out of one of these, and this is what makes "where did that number come
   from" answerable without opening the transcript. Small and faint - it is
   provenance, not content. */
.chat-used {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  margin-top: var(--s3);
}
.chat-used-label {
  font-size: 11.5px;
  color: var(--faint);
}

/* ── Waiting ──────────────────────────────────────────────────────────────
   A turn takes six to ten seconds because it really is doing several lookups,
   and a panel that shows nothing for ten seconds is indistinguishable from one
   that has hung. This names the lookup actually in flight, so the wait is
   legible rather than merely occupied. */
.chat-working {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 13px;
  color: var(--faint);
}
.chat-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: chat-pulse 1.4s var(--ease) infinite;
}
@keyframes chat-pulse {
  0%,
  100% {
    opacity: 0.35;
  }
  50% {
    opacity: 1;
  }
}

/* ── Composer ─────────────────────────────────────────────────────────────── */
.chat-composer {
  display: flex;
  gap: var(--s2);
  align-items: flex-end;
  padding: var(--s3) var(--s5);
  border-top: 1px solid var(--border);
  background: var(--bg);
  /* Sits inside the card's own bottom corners. */
  border-radius: 0 0 var(--r-md) var(--r-md);
}
.chat-input {
  flex: 1;
  /* Same reason as the grid items above: a flex item will not shrink past its
     intrinsic width, and a textarea's is about twenty characters. */
  min-width: 0;
  font: inherit;
  /* 16px exactly. Anything smaller and iOS Safari zooms the page on focus,
     which on a phone leaves the user looking at a magnified fragment of the
     dashboard with no obvious way back. */
  font-size: 16px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
  padding: 10px var(--s3);
  resize: none;
  /* Grows with the text up to a ceiling, then scrolls. Set in JS; the max
     lives here so the two cannot disagree about it. */
  max-height: 132px;
  overflow-y: auto;
}
.chat-input::placeholder {
  color: var(--faint);
}
.chat-send {
  /* 44px: the minimum a thumb can hit reliably, and the height the field
     starts at, so the two sit on the same baseline. */
  min-height: 44px;
  min-width: 76px;
}
.chat-foot {
  padding: 0 var(--s5) var(--s4);
  margin: 0;
}

/* The composer when the assistant cannot be used - no resume yet, or the API
   is unreachable. Same treatment as #btn-ob-continue and .btn-danger, because
   "unavailable" should look the same everywhere in this product rather than
   once per feature. A control that looks pressable and is not is the thing
   being avoided; the explanation is in the panel above it. */
.chat-input:disabled,
.chat-send:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
.chat-input:disabled {
  background: var(--cream);
}

/* ── Nothing asked yet ────────────────────────────────────────────────────
   An empty log is the one screen every user sees first, and "ask me anything"
   is the least useful thing it could say - this assistant can answer a narrow,
   specific set of questions well and everything else badly. The starters are
   the four questions the tools were built for, so the empty state doubles as
   the feature's documentation. */
.chat-empty {
  padding: var(--s6) var(--s2);
  text-align: center;
}
.chat-empty h2 {
  font-family: var(--work);
  font-weight: 500;
  font-size: 17px;
  color: var(--ink);
  margin: 0 0 var(--s2);
}
.chat-empty p {
  font-size: 14px;
  color: var(--faint);
  margin: 0 auto var(--s5);
  max-width: 42ch;
  line-height: 1.6;
}
.chat-starters {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  max-width: 420px;
  margin: 0 auto;
}
.chat-starter {
  font: inherit;
  font-size: 13.5px;
  text-align: left;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  padding: 11px var(--s4);
  min-height: 44px;
  cursor: pointer;
  transition:
    background 0.18s var(--ease),
    border-color 0.18s var(--ease);
}
.chat-starter:hover {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.chat-starter:active {
  transform: translateY(1px) scale(0.99);
}
.chat-starter:focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* ── The allowance, and refusals ──────────────────────────────────────────── */
.chat-note {
  font-size: 13px;
  line-height: 1.55;
  border-radius: var(--r-sm);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
}
/* Amber, not red: running out of free credit is "read this, the product is
   working" - there is a way on, and it is in the sentence. */
.chat-note-warn {
  background: var(--warn-soft);
  border: 1px solid var(--warn-line);
  color: var(--warn-ink);
}
.chat-note-err {
  background: var(--danger-soft);
  border: 1px solid var(--danger-line);
  color: var(--danger-strong);
}
.chat-note a {
  color: inherit;
}

@media (prefers-reduced-motion: reduce) {
  .chat-dot {
    animation: none;
    opacity: 0.8;
  }
  .chat-starter:active {
    transform: none;
  }
  .chat-starter {
    transition: none;
  }
}

@media (max-width: 900px) {
  .chat-detail {
    max-height: none;
  }
  .chat-log {
    padding: var(--s4);
  }
  .chat-composer {
    padding: var(--s3) var(--s4);
  }
  .chat-foot {
    padding: 0 var(--s4) var(--s4);
  }
}
