/*
 * One stylesheet, because there is one page.
 *
 * Dark and light both come from the same tokens, switched on the viewer's own
 * setting — there is no theme control here and no preference to store, since
 * the page holds nothing else a person would want to configure.
 */

:root {
  color-scheme: light dark;

  --bg: #f6f7f9;
  --panel: #ffffff;
  --ink: #16191d;
  --muted: #61696f;
  --line: #dfe3e8;
  --accent: #335c81;
  --accent-ink: #ffffff;
  --ok-bg: #e4f3e8;
  --ok-ink: #1d6b33;
  --error: #b3261e;
  --radius: 8px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14171a;
    --panel: #1c2024;
    --ink: #e8eaed;
    --muted: #9aa2a9;
    --line: #2e343a;
    --accent: #6f9dc7;
    --accent-ink: #10151a;
    --ok-bg: #1c3a25;
    --ok-ink: #7fc894;
    --error: #f2b8b5;
  }
}

* {
  box-sizing: border-box;
}

/*
 * The parts of a page nobody draws still belong to it. Left alone, the
 * selection highlight is the platform's blue and the caret is pure black —
 * two colours from outside the palette, on every screen.
 */
::selection {
  background: color-mix(in srgb, var(--accent) 28%, transparent);
  color: var(--ink);
}

input,
textarea {
  caret-color: var(--accent);
}

/*
 * `hidden` is how this page switches screens, and any `display` rule beats it.
 *
 * The UA stylesheet's `[hidden] { display: none }` is a plain attribute
 * selector, so `.signin { display: grid }` — one class, higher specificity —
 * wins and the element stays on screen with the attribute set. Signing in
 * looked like it did nothing: the shell was rendering underneath the sign-in
 * card, which is the worst shape for this to fail in, because the attribute is
 * set correctly and every check of it says the page is right.
 */
[hidden] {
  display: none !important;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
}

h1 {
  margin: 0;
  font-size: 1.35rem;
}

h2 {
  margin: 0;
  font-size: 1.1rem;
}

.muted {
  color: var(--muted);
}

.hint {
  margin: 0.25rem 0 0;
  color: var(--muted);
  font-size: 0.85rem;
}

.error {
  margin: 0.75rem 0 0;
  color: var(--error);
  font-size: 0.9rem;
}

code {
  font-size: 0.85em;
}

/* --------------------------------------------------------------- sign-in */

.signin {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  padding: 1rem;
}

.card {
  width: min(24rem, 100%);
  padding: 1.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.card p {
  margin: 0.35rem 0 1.25rem;
}

/*
 * "Forgot your password?" under the sign-in button.
 *
 * Quiet on purpose. It is the second thing anybody wants from this screen, and
 * a link competing with the button would be a link some people press first.
 */
.forgot {
  display: block;
  margin-block-start: 1rem;
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
}

.forgot:hover,
.forgot:focus-visible {
  color: var(--ink);
}

/*
 * An edit that lives inside a table cell.
 *
 * A button that reads as a link, because it belongs to the name beside it
 * rather than to the row's action column — putting "rename" among Research and
 * Remove would give an edit the same weight as releasing somebody's lead.
 */
.linky {
  padding: 0;
  border: 0;
  margin-inline-start: 0.4rem;
  background: none;
  color: var(--muted);
  font-size: 0.75rem;
  text-decoration: underline;
  cursor: pointer;
}

.linky:hover,
.linky:focus-visible {
  color: var(--ink);
}

/* ----------------------------------------------------------------- shell */

.topbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
  /*
   * Wraps rather than overflowing. With two tabs the bar fitted a phone by a
   * few pixels; the third pushed it to 477px against a 375px screen, and the
   * symptom was the whole PAGE scrolling sideways — which reads as a broken
   * layout somewhere in the view rather than as a full header.
   */
  flex-wrap: wrap;
}

.brand {
  font-weight: 600;
}

/* Pushes the sign-out button to the far end without a second rule for it. */
.topbar #who {
  margin-inline-start: auto;
}

/*
 * The tab strip is the part that grows, so it is the part allowed to take a
 * line of its own when there is not room for everything.
 */
.topbar .tabs {
  flex-wrap: wrap;
}

/*
 * Below 40rem the six tabs wrapping onto their own lines is the header rule
 * above doing its job — it stops the page scrolling sideways — but it also
 * makes the header itself tall enough to push real content below the fold on
 * a phone. Collapsed behind a hamburger instead, same breakpoint the search
 * form's own mobile rule already uses.
 */
.nav-toggle {
  display: none;
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.35rem 0.6rem;
}

@media (max-width: 40rem) {
  .nav-toggle {
    display: inline-flex;
  }

  /* Collapsed by default; JS toggles `.open` rather than `hidden`, so the
     menu can be closed with a CSS transition instead of popping shut. */
  .topbar .tabs {
    display: none;
    flex-basis: 100%;
    flex-direction: column;
    gap: 0;
    margin-top: 0.5rem;
    order: 10;
  }

  .topbar .tabs.open {
    display: flex;
  }

  .topbar .tabs a {
    padding: 0.6rem 0.25rem;
    border-bottom: 1px solid var(--line);
  }

  .topbar .tabs a:last-child {
    border-bottom: 0;
  }
}

.view {
  max-width: 64rem;
  margin: 0 auto;
  padding: 1.5rem 1.25rem;
}

.row-between {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1rem;
}

.row-end {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-top: 1.25rem;
}

/* ---------------------------------------------------------------- fields */

label {
  display: block;
  margin-bottom: 0.3rem;
  font-size: 0.85rem;
  font-weight: 500;
}

label:not(:first-of-type) {
  margin-top: 0.9rem;
}

input,
select {
  width: 100%;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: inherit;
  font: inherit;
}

input:focus-visible,
select:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

button {
  padding: 0.55rem 1rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-ink);
  font: inherit;
  font-weight: 500;
  /* A two-word label breaks across two lines in the narrow topbar otherwise. */
  white-space: nowrap;
  cursor: pointer;
}

button:disabled {
  opacity: 0.6;
  cursor: default;
}

.card button {
  width: 100%;
  margin-top: 1.25rem;
}

button.ghost {
  border-color: var(--line);
  background: transparent;
  color: var(--ink);
}

/* ----------------------------------------------------------------- table */

/*
 * The company's name stays put while the rest of the row scrolls.
 *
 * The companies table is about 1200px of content — six columns and five
 * buttons — so on any normal window it scrolls sideways inside its own box.
 * Reaching the buttons therefore scrolls the FIRST column off the left, and
 * the first column is the only thing that says which company the row is about.
 * Reported on 2026-08-25 as "there is no company name on the list", which is
 * exactly what it looks like: no clipped edge, no hint, just rows of dates and
 * scores belonging to nobody.
 *
 * `.bleed` was added to stop the BUTTONS being cut off the right end. It did,
 * and moved the loss to the other end. A wide table needs an anchor, not more
 * width.
 *
 * Scoped to this table: the roster is narrow and needs none of it, and a
 * sticky cell that is never scrolled is a background painted for nothing.
 */
#companies th:first-child,
#companies td:first-child {
  position: sticky;
  /* Negative by exactly `.bleed`'s gutter, so the pinned cell reaches the
     scrollport's edge. At `0` it stops at the PADDING edge and the scrolling
     row shows through the 20px strip beside it, which reads as a rendering
     fault rather than as a pinned column. The padding below puts the text back
     where it was. */
  inset-inline-start: -1.25rem;
  padding-inline-start: 2.1rem;
  z-index: 1;
  /* Cells are transparent by default and would let the scrolling row show
     through. This is the same surface `.table-wrap` paints. */
  background: var(--panel);
  /* A shadow rather than a border: under `border-collapse: collapse` a border
     on a sticky cell is not reliably painted, and the seam is what tells a
     reader the column is pinned rather than misaligned. */
  box-shadow: 1px 0 0 var(--line);
}

.table-wrap {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  /* Six columns on a phone is wider than the screen; the page must not be. */
  overflow-x: auto;
}

.table-wrap > p {
  margin: 0;
  padding: 1rem;
}

table {
  width: 100%;
  border-collapse: collapse;
  white-space: nowrap;
}

th,
td {
  padding: 0.6rem 0.85rem;
  text-align: start;
  border-bottom: 1px solid var(--line);
}

th {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--muted);
}

/*
 * A removed seller, when the roster is asked to show them. Dimmed rather than
 * struck through: the row is still readable and still actionable — the button
 * in it puts them back — and a strike-through reads as "deleted", which is the
 * one thing a removed seller is not.
 */
tbody tr.dim td,
.tile.dim {
  color: var(--muted);
}

tbody tr:last-child td {
  border-bottom: none;
}

.badge {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  background: var(--line);
  font-size: 0.78rem;
}

.badge.ok {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

/* ------------------------------------------------------------------ tabs */

.tabs {
  display: flex;
  gap: 0.25rem;
}

.tabs a {
  padding: 0.35rem 0.7rem;
  border-radius: var(--radius);
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 500;
}

.tabs a:hover {
  color: var(--ink);
}

.tabs a.active {
  background: var(--line);
  color: var(--ink);
}

/* ------------------------------------------------------------------ note */

.note {
  margin-bottom: 1.25rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
  font-size: 0.92rem;
}

/*
 * The caveat block. Its own colour because caveats are the unvalidated
 * identifiers, and an unvalidated identifier is exactly the fact that looks
 * authoritative in a generated site and is wrong.
 */
.note.warn {
  border-inline-start-color: var(--error);
}

.note ul {
  margin: 0.5rem 0 0;
  padding-inline-start: 1.1rem;
}

.note button {
  display: block;
  margin-top: 0.75rem;
}

/* ---------------------------------------------------------- table extras */

.small {
  font-size: 0.85rem;
}

td .small {
  margin-top: 0.15rem;
}

.right {
  text-align: end;
}

.nowrap {
  white-space: nowrap;
}

button.small {
  padding: 0.3rem 0.6rem;
  font-size: 0.85rem;
}

.badge.bad {
  background: color-mix(in srgb, var(--error) 18%, transparent);
  color: var(--error);
}

/* --------------------------------------------------------------- dossier */

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

.swatch {
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--line);
  border-radius: 4px;
}

#dossier-body h4 {
  margin: 1.25rem 0 0.35rem;
  font-size: 0.9rem;
}

#dossier-body ul {
  margin: 0;
  padding-inline-start: 1.1rem;
  font-size: 0.9rem;
}

#dossier-body p {
  margin: 0.25rem 0;
}

#dossier-body pre {
  max-height: 24rem;
  padding: 0.75rem;
  border-radius: var(--radius);
  background: var(--bg);
  font-size: 0.78rem;
  /* A crawled document has lines of any length; the dialog must not grow to fit. */
  overflow: auto;
}

#dossier-body details {
  margin-top: 1.5rem;
}

#dossier-body summary {
  cursor: pointer;
  font-size: 0.9rem;
  color: var(--muted);
}

/* ---------------------------------------------------------------- dialog */

dialog {
  width: min(26rem, calc(100vw - 2rem));
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
}

/* The dossier is the one dialog whose job is reading, not filling in. */
dialog.wide {
  width: min(46rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
}

dialog::backdrop {
  background: rgb(0 0 0 / 0.45);
}

dialog .row-between {
  margin-bottom: 0.5rem;
}

dialog .muted {
  margin: 0 0 1rem;
  font-size: 0.9rem;
}

/* The close X is chrome, not a field, so it keeps its own tight box. */
#invite-close {
  padding: 0.2rem 0.5rem;
  line-height: 1;
}

/* The register's answer, as a small facts table inside its note. */
.facts {
  margin: 0.6rem 0 0.4rem;
  border-collapse: collapse;
  white-space: normal;
}

.facts th,
.facts td {
  padding: 0.15rem 0.75rem 0.15rem 0;
  border: none;
  text-align: start;
  font-size: 0.9rem;
}

.facts th {
  width: 5.5rem;
  color: var(--muted);
  font-weight: 500;
}

/* --------------------------------------------------------- analysis panel */

/*
 * Eight cards rather than eight table rows. A row would have to fit the label,
 * the purpose, the dependency it is waiting on, the model that produced it and
 * whether the reading is stale — and every one of those is a sentence, not a
 * cell.
 *
 * `auto-fill` with a minimum rather than a fixed column count, so the panel is
 * two columns on a laptop and one on a phone without a media query deciding
 * which.
 */
.analyses {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}

.analysis {
  padding: 0.8rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

/* The ones that have produced something, marked the same way a note is. */
.analysis.has-run {
  border-inline-start: 3px solid var(--accent);
}

.analysis h4 {
  margin: 0;
  font-size: 0.95rem;
}

.analysis p {
  margin: 0.35rem 0 0;
}

/*
 * Inline warnings inside an otherwise muted line — a caveat count, or a reading
 * taken from a crawl that has since been repeated. Not `.error`, which is for a
 * thing that failed; these are things that worked and need a second look.
 */
.warn-text {
  color: var(--error);
}

/*
 * A chatbot's system prompt is the deliverable, not a field. It is meant to be
 * read as written and copied out whole, so it keeps its own line breaks and
 * scrolls rather than reflowing.
 */
pre.prompt {
  max-height: 24rem;
  overflow: auto;
  padding: 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  white-space: pre-wrap;
  font-size: 0.88rem;
  line-height: 1.5;
}

/* ------------------------------------------------------- prospecting form */

/*
 * Two tiers, because there are two questions and only one of them is asked
 * every time. The top row IS the search — say where, press the button. What
 * sits under the rule narrows a search that has already been described.
 *
 * The old form was one flex row of five equal boxes, bottom-aligned. Only the
 * "where" field carried a hint, so that field was a line taller than the rest,
 * and bottom-aligning dragged every other label half a line down: five controls
 * on one row with five different label baselines. The hint moving out of the
 * field is the fix — not a nudge, the reason it happened.
 */
.nearby-form {
  /* One height for every control on the row, so the button cannot sit a
     pixel proud of the input beside it. */
  --control-h: 2.35rem;

  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  margin-bottom: 1rem;
}

.nearby-form .ask {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.6rem;
}

.nearby-form .field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
}

/*
 * Margins off, spacing by `gap`.
 *
 * The global label rule is written for the stacked dialog forms: it blocks,
 * adds a bottom margin, and gives `label:not(:first-of-type)` a 0.9rem top
 * margin so fields breathe down a column. In a ROW that top margin lands on
 * the second of two sibling labels and pushes it down 14.4px — which is why
 * "Include chains" has always sat a few pixels below "Has an address" and read
 * as a rendering fault. It is not one, and it is not new; it just had nowhere
 * to show until two labels shared a line.
 */
.nearby-form label {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--muted);
}

.nearby-form .field input,
.nearby-form .field select,
.nearby-form .go {
  height: var(--control-h);
  border-radius: var(--radius);
  font: inherit;
}

.nearby-form .field input,
.nearby-form .field select {
  padding: 0 0.55rem;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--ink);
}

.nearby-form .field input:disabled,
.nearby-form .field select:disabled {
  /* Disabled because a drawn area already answered this, not because the
     control is broken — so it dims rather than greys out. */
  opacity: 0.5;
}

.nearby-form .mode select {
  min-width: 10rem;
}

/* The longest value on the row takes whatever is left. */
.nearby-form .grow {
  flex: 1 1 18rem;
}

/*
 * Two letters, sized for two letters. Centred and upper-cased on the way in,
 * so "pl" and "PL" look like the same answer while it is being typed.
 */
.nearby-form .country input {
  width: 4rem;
  text-align: center;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.nearby-form .go {
  padding-inline: 1.25rem;
  font-weight: 600;
}

.nearby-form .go:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent) 86%, var(--ink));
}

.nearby-form .go:active:not(:disabled) {
  transform: translateY(1px);
}

/*
 * The line under the search: what this mode expects, and the two ways of
 * saying where by pointing instead of typing. They belong here rather than in
 * a button bar of their own — pointing at a place is an alternative to naming
 * one, not a separate feature.
 */
.nearby-form .ask-aside {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0.55rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.nearby-form .sep {
  color: var(--muted);
  opacity: 0.6;
}

/*
 * A text button, underlined like the link it behaves as. The underline is
 * offset and half-toned rather than sitting on the baseline at full strength,
 * which is the browser default and belongs to no design.
 */
.nearby-form .link {
  padding: 0;
  height: auto;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent);
  cursor: pointer;
}

.nearby-form .link:hover {
  text-decoration-color: currentColor;
}

/* The drawn area, shown as something you can put back down. */
.chip.area {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 500;
}

.chip.area::after {
  content: "✕";
  font-size: 0.75em;
  color: var(--muted);
}

/*
 * Below the rule: the narrowing. Separated rather than boxed — proximity does
 * the grouping, and a second panel inside the first would be a container
 * standing in for a relationship.
 */
.nearby-form .narrow {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 0.6rem 1.1rem;
  margin-top: 0.9rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--line);
}

/*
 * The two switches are one unit and wrap as one.
 *
 * `flex-wrap: wrap` here put them on two lines at the exact width they
 * occupied — the pair measured 259.4px inside a 259.4px box, so sub-pixel
 * rounding decided it, and the result was two labels a few pixels out of line
 * with each other rather than an obvious second row. It read as a rendering
 * fault, which is precisely what it was.
 */
.nearby-form .switches {
  display: flex;
  flex: 0 0 auto;
  flex-wrap: nowrap;
  align-items: center;
  gap: 1rem;
  /* Sits on the controls' middle rather than the top of the row, which is
     where a shorter child lands by default. */
  padding-block-end: 0.45rem;
}

@media (max-width: 30rem) {
  /* Only where there is genuinely no room for both. */
  .nearby-form .switches {
    flex-wrap: wrap;
    gap: 0.25rem 1rem;
  }
}

.nearby-form .switches label {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--ink);
  font-size: 0.9rem;
  font-weight: 400;
  /* Without this the two-word labels wrap and drag their checkboxes into the
     middle of a two-line block, which reads as a rendering fault. */
  white-space: nowrap;
  cursor: pointer;
}

.nearby-form .switches input {
  width: 1rem;
  height: 1rem;
  margin: 0;
  /* Both states get the page's own colour. Left to the browser, "on" is the
     platform blue and "off" is an unstyled grey square, and the pair looks
     like two different controls. */
  accent-color: var(--accent);
}

@media (max-width: 40rem) {
  /* The value and the action each take the full width; the two short fields
     stay side by side, because they are short. */
  .nearby-form .grow,
  .nearby-form .go {
    flex: 1 1 100%;
  }
}

/*
 * Claim state on a search row. "Taken" never names who — a refused claim gives
 * a date and never a colleague, and the roster is not a leaderboard of who is
 * chasing what.
 */
.pill {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: 0.8rem;
}

.pill.ours {
  background: var(--ok-bg);
  color: var(--ok-ink);
}

.pill.taken {
  border: 1px solid var(--line);
  color: var(--muted);
}

/* An anchor that has to sit in a row of buttons without pretending to be one. */
.button-like {
  display: inline-block;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--ink);
  text-decoration: none;
}

/* ------------------------------------------------------------- sources */

/*
 * A list of things a person added, each with its own remove. Not a table: the
 * rows are different shapes — a URL, a tax id, a sentence — and only some carry
 * a note.
 */
.sources {
  margin: 0.5rem 0 1.25rem;
  padding: 0;
  list-style: none;
}

.sources li {
  position: relative;
  padding: 0.6rem 5rem 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-bottom: 0.5rem;
  word-break: break-word;
}

/*
 * The remove button sits in the padding reserved for it on the right, so a long
 * URL wrapping to three lines never pushes it off the row.
 */
.sources li button {
  position: absolute;
  inset-block-start: 0.6rem;
  inset-inline-end: 0.6rem;
}

/* --------------------------------------------------------------- the map */

/*
 * Pin colours are the same four states the table shows, so a row and its pin
 * agree without a legend. `--pin-free` is the loudest of them on purpose: a
 * business with no website is what this whole screen is looking for.
 */
:root {
  --pin-free: #c2410c;
  --pin-site: #6b7280;
  --pin-taken: #9ca3af;
  --map-ink: #1d4ed8;
}

@media (prefers-color-scheme: dark) {
  :root {
    --pin-free: #fb923c;
    --pin-site: #9aa2a9;
    --pin-taken: #6b7280;
    --map-ink: #93b4f5;
  }
}

.map-host {
  margin-bottom: 1rem;
}

.map {
  position: relative;
  height: clamp(18rem, 45vh, 30rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
  /* Without this the browser claims the drag for scrolling and the map never
     sees a pointermove — which on a phone is the whole interaction. */
  touch-action: none;
  cursor: grab;
}

.map.picking {
  cursor: crosshair;
}

.map-layer {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
}

.map-tiles img {
  position: absolute;
  /* Tiles butt against each other; a fractional gap would show as a grid of
     hairlines across the map. */
  display: block;
  user-select: none;
}

/*
 * No filter any more.
 *
 * A `brightness(0.82)` was painted over OpenStreetMap's light tiles because
 * they were the only ones available and a white rectangle in a dark interface
 * is genuinely uncomfortable. The tile service renders a real dark style now —
 * dark ground, dark water, light labels — which is what a map at night is
 * supposed to look like, and dimming it would only make it muddy.
 */

.map-shapes,
.map-pins {
  position: absolute;
  inset: 0;
  /* The shapes and the empty space between pins must not eat the drag. */
  pointer-events: none;
}

/*
 * Every shape is drawn twice: a broad stroke in the page's own background
 * underneath, then the real one on top. A map is a surface already covered in
 * thin coloured lines, and a single 1.5px dashed circle across the middle of it
 * is genuinely invisible — measured, not guessed.
 */
.map-casing {
  fill: none;
  stroke: var(--panel);
  stroke-width: 6;
  stroke-linejoin: round;
  opacity: 0.65;
}

.map-circle {
  fill: color-mix(in srgb, var(--map-ink) 14%, transparent);
  stroke: var(--map-ink);
  stroke-width: 2.5;
  stroke-dasharray: 7 5;
}

.map-centre {
  fill: var(--map-ink);
}

.map-ring {
  fill: color-mix(in srgb, var(--map-ink) 16%, transparent);
  stroke: var(--map-ink);
  stroke-width: 2.5;
  stroke-linejoin: round;
}

.map-ring.drawing {
  fill: none;
  stroke-dasharray: 6 4;
}

.map-corner {
  fill: var(--panel);
  stroke: var(--map-ink);
  stroke-width: 2;
}

.map-pin {
  position: absolute;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  padding: 0;
  border: 2px solid var(--panel);
  border-radius: 999px;
  background: var(--pin-site);
  pointer-events: auto;
  cursor: pointer;
}

.map-pin.free {
  background: var(--pin-free);
}

.map-pin.taken {
  background: var(--pin-taken);
  opacity: 0.65;
}

.map-pin.ours {
  background: var(--ok-ink);
}

.map-pin.selected {
  /* Grown rather than recoloured: the colour is already saying something else. */
  width: 20px;
  height: 20px;
  margin: -10px 0 0 -10px;
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--map-ink) 45%, transparent);
  z-index: 2;
}

/*
 * The label beside a pin, opt-in via `place.sticker` -- see map.js's own
 * comment. Anchored on the pin's own point and lifted up-and-right of it with
 * a transform rather than a margin, so the offset survives the pin's `left`/
 * `top` being rewritten on every pan without this rule having to know the
 * pin's size.
 */
.map-sticker {
  position: absolute;
  transform: translate(8px, -22px);
  max-width: 11rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 0.1rem 0.4rem;
  border-radius: var(--radius);
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 0.7rem;
  line-height: 1.3;
  pointer-events: none;
  box-shadow: 0 1px 2px color-mix(in srgb, var(--map-ink) 20%, transparent);
}

/*
 * The embedded map on Today and Companies -- a `<details>`, same shape as
 * `.scout-settings`, so a seller who never uses it can close it and a seller
 * who does keeps it open across a poll-driven redraw of the table beside it
 * (the map itself is redrawn separately, from `companymap.js`, and is not
 * touched by that redraw).
 */
/*
 * `.bleed`, not just `width: 100%` -- `.view` itself caps at 64rem (see its
 * own rule), so anything narrower than that was already full width of a
 * column that is not full width of the screen. `.bleed` is this codebase's
 * own answer to exactly that, already proven on the Companies table (rule
 * 27's sticky-column note), and it is a full VIEWPORT width, so the map is
 * actually as wide as the browser window rather than as wide as a reading
 * column that only looks narrow on anything wider than a laptop.
 */
.map-embed {
  margin-bottom: 0.75rem;
  padding: 0.6rem 1.25rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.map-embed summary {
  cursor: pointer;
  font-weight: 600;
}

/* Edge-to-edge of the (now full-width) card, not its padded interior. */
.map-embed .map-host {
  width: 100%;
  margin: 0.6rem -1.25rem -0.6rem;
}

.map-embed .map-host .map {
  border-radius: 0;
  border-inline: 0;
}

/* A pin's click scrolls its row into view and flashes it, so the two views
   of the same list stay legibly connected. */
@keyframes map-flash {
  from {
    background: color-mix(in srgb, var(--accent) 35%, transparent);
  }
  to {
    background: transparent;
  }
}

.map-flash {
  animation: map-flash 1.2s ease-out;
}

.map-tools {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-end: 0.5rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.map-draw {
  position: absolute;
  inset-block-end: 2rem;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  display: flex;
  gap: 0.35rem;
  flex-wrap: wrap;
  justify-content: center;
}

.map-btn {
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.map-btn:disabled {
  opacity: 0.5;
  cursor: default;
}

.map-hint {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-start: 0.5rem;
  max-width: min(22rem, calc(100% - 5rem));
  padding: 0.3rem 0.6rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  color: var(--ink);
  font-size: 0.85rem;
}

/*
 * Attribution is a condition of using both the tiles and the data, not a
 * courtesy — ODbL requires the credit and the OSM tile policy requires it to be
 * visible on the map itself.
 */
.map-credit {
  position: absolute;
  inset-block-end: 0;
  inset-inline-end: 0;
  margin: 0;
  padding: 0.1rem 0.4rem;
  border-start-start-radius: var(--radius);
  background: color-mix(in srgb, var(--panel) 85%, transparent);
  font-size: 0.72rem;
  color: var(--muted);
}

.map-credit a {
  color: inherit;
}

/* -------------------------------------------------------------- narrowing */

.facets {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.5rem 0 0.85rem;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

/* Switched off, not removed — the count is still the honest denominator. */
.chip.off {
  opacity: 0.45;
  text-decoration: line-through;
}

.chip.on {
  border-color: var(--accent);
  color: var(--accent);
}

.chip-count {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.layout-switch {
  margin-bottom: 0.6rem;
}

.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.segmented button {
  padding: 0.3rem 0.8rem;
  border: none;
  border-radius: 0;
  background: var(--panel);
  color: var(--muted);
  font-size: 0.85rem;
}

.segmented button.on {
  background: var(--accent);
  color: var(--accent-ink);
}

/* ------------------------------------------------------------- by street */

.street {
  margin-bottom: 1.25rem;
}

.street h4 {
  margin: 0 0 0.4rem;
  font-size: 1rem;
}

.street .row-between {
  align-items: baseline;
}

/*
 * The house number leads the row and is the width of three digits, so the
 * numbers line up into a column somebody can run their eye down while walking.
 */
.street table {
  table-layout: fixed;
}

td.house {
  width: 3.5rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/*
 * Fixed widths, counted from the tick box. They are positional and therefore
 * fragile — adding a column to `streetRow` without moving these silently
 * squeezes the business name — so the count is written down: 1 tick, 2 number,
 * 3 name, 4 site, 5 visit, 6 action.
 */
.street td:nth-child(1) {
  width: 2.5rem;
}

.street td:nth-child(3) {
  /* The remainder, and the only column allowed to wrap — a business name is
     the one thing here that must never be cut off. */
  white-space: normal;
}

.street td:nth-child(4) {
  width: 8rem;
}

.street td:nth-child(5) {
  width: 10.5rem;
}

.street td:nth-child(6) {
  width: 6.5rem;
}

/* The row a pin points at, and the other way round. */
tbody tr.selected {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

tbody tr[data-ref] {
  cursor: pointer;
}


/* ---------------------------------------------------- walked, and swept */

/*
 * The tick column. Narrow, and the box itself is bigger than it looks: the
 * whole cell is the target, because this is used standing up on a street.
 */
th.pick,
td.pick {
  width: 2.5rem;
  padding-inline-end: 0;
}

th.pick input,
td.pick input {
  width: 1rem;
  height: 1rem;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* A row that is about to be acted on, distinct from the one the map points at. */
tbody tr.picked {
  background: color-mix(in srgb, var(--accent) 7%, transparent);
}

tbody tr.selected.picked {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}

/*
 * A door somebody has already been to.
 *
 * Quietened rather than hidden. It is still a lead — a shop that was shut on
 * Tuesday is open on Wednesday — and hiding it would make the list disagree
 * with the street. The point is only that the eye skips it first.
 *
 * Done with COLOUR and not with `opacity`, which was the first attempt and was
 * wrong for a reason worth keeping: opacity applies to a whole subtree and
 * cannot be undone by a descendant, so it faded the note box on the very row
 * somebody had just walked to and was about to type in.
 */
tbody tr.walked td:not(.pick) {
  color: var(--muted);
}

tbody tr.walked strong {
  font-weight: 500;
}

.visit-col {
  min-width: 9.5rem;
}

select.visit {
  width: 100%;
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
}

/* What somebody else found here. Never a name — see visits.js. */
.walked-by {
  margin-top: 0.2rem;
  white-space: normal;
}

/*
 * The note and the come-back date, on the rows that have been walked.
 *
 * Under the name rather than in a column of their own: a column would be empty
 * on nearly every row, and these appear on the few that are no longer just a
 * name on a map.
 */
.visit-detail {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.35rem;
}

.visit-detail input {
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: 0.82rem;
}

.visit-detail input[type="text"] {
  flex: 1 1 12rem;
  min-width: 8rem;
}

/*
 * The bar that appears when something is ticked.
 *
 * Sticky, because the rows it acts on are a screenful long and a seller who
 * has ticked twelve of them should not have to scroll back to find the button.
 */
.sweep {
  position: sticky;
  inset-block-start: 0;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.75rem;
  margin-bottom: 0.6rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--panel);
  font-size: 0.9rem;
}

.sweep-opt {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  font-size: 0.9rem;
  font-weight: 400;
  color: var(--ink);
  white-space: nowrap;
}

.sweep-opt input[type="checkbox"] {
  width: 1rem;
  height: 1rem;
  margin: 0;
  accent-color: var(--accent);
}

.sweep-opt select {
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
  font-size: 0.85rem;
}

.sweep .link {
  padding: 0;
  border: none;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 0.9rem;
  text-decoration: underline;
  text-underline-offset: 0.2em;
  cursor: pointer;
}

/* What the last sweep did. Stays until the next search replaces it. */
.note.sweep-result {
  margin: 0.5rem 0 0.75rem;
}

/*
 * A pin for a door already walked.
 *
 * A ring rather than a different fill: the fill already says whether the
 * business has a website, which is a different question and the more important
 * one. Two facts, two channels.
 */
.map-pin.walked {
  outline: 2px solid var(--muted);
  outline-offset: 1px;
}

/* ------------------------------------------------- where to go, not who */

/*
 * The density ramp: one hue, five steps, light→dark, anchored on the
 * documented orange and stepped by LIGHTNESS in OKLCH rather than by eye. Both
 * modes are chosen for their own surface — the dark column is the same hue
 * stepped for a dark ground, not an automatic flip of the light one.
 *
 * Orange rather than the default sequential blue for a measurable reason: this
 * ramp is drawn over a map. `--map-ink` is already blue and so is every body of
 * water OpenStreetMap draws, and Sopot — the town this was proven on — is half
 * sea. Blue squares over blue water is not a taste objection.
 *
 * It is also the colour `--pin-free` already carries, which is the same fact
 * one zoom level down: a business with no website. The heat IS those pins,
 * counted.
 */
:root {
  --heat-1: #fae2d9;
  --heat-2: #f4c5b4;
  --heat-3: #eba48a;
  --heat-4: #e0825f;
  --heat-5: #d35e30;
}

@media (prefers-color-scheme: dark) {
  :root {
    --heat-1: #6c2000;
    --heat-2: #903309;
    --heat-3: #b64618;
    --heat-4: #d85825;
    --heat-5: #f57242;
  }
}

/*
 * Translucent, because what is underneath is the point — a square means
 * nothing without the streets it covers.
 */
.heat {
  stroke: var(--panel);
  stroke-width: 1;
  fill-opacity: 0.62;
}

.heat.b0 { fill: var(--heat-1); }
.heat.b1 { fill: var(--heat-2); }
.heat.b2 { fill: var(--heat-3); }
.heat.b3 { fill: var(--heat-4); }
.heat.b4 { fill: var(--heat-5); }

.heat.hot {
  stroke: var(--ink);
  stroke-width: 2;
  fill-opacity: 0.78;
}

/*
 * The count, on every square big enough to hold it.
 *
 * Colour is never the only encoding here: a reader who cannot separate two
 * oranges must still be able to use the map, and the ranked list beside it is
 * the same data again as a table.
 */
.heat-label {
  fill: var(--ink);
  font-size: 11px;
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: central;
  paint-order: stroke;
  stroke: var(--panel);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.legend-row {
  margin: 0.5rem 0 0.75rem;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.7rem;
}

.legend-band {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  font-size: 0.82rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.heat-swatch {
  display: inline-block;
  width: 0.85rem;
  height: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  vertical-align: -0.1em;
}

.heat-swatch.b0 { background: var(--heat-1); }
.heat-swatch.b1 { background: var(--heat-2); }
.heat-swatch.b2 { background: var(--heat-3); }
.heat-swatch.b3 { background: var(--heat-4); }
.heat-swatch.b4 { background: var(--heat-5); }

tbody tr[data-square] {
  cursor: pointer;
}

tbody tr[data-square].selected {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}


/*
 * The one banner that is not information but a way out.
 *
 * An owner with no seller row is refused by every view in this dashboard, and
 * this is the only control that fixes it — so it does not sit quietly at the
 * top looking like the enrolment note it is. Somebody reading "You are not
 * enrolled in the partner programme" in red under a form needs to find this.
 */
.note.enrol {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}

.note.enrol button {
  margin-inline-start: 0.5rem;
}

/*
 * The map with no picture behind it.
 *
 * Still a map: the search circle, the drawn ring, the pins and the density
 * squares are this application's own geometry and are all still in the right
 * places. Only the streets are missing. A flat, deliberate ground says that,
 * where a third of a basemap and a lot of empty space says "broken".
 */
.map.no-tiles {
  background:
    repeating-linear-gradient(
      0deg,
      transparent 0 39px,
      color-mix(in srgb, var(--line) 55%, transparent) 39px 40px
    ),
    repeating-linear-gradient(
      90deg,
      transparent 0 39px,
      color-mix(in srgb, var(--line) 55%, transparent) 39px 40px
    ),
    var(--bg);
}

.map-offline {
  position: absolute;
  inset-block-end: 1.6rem;
  inset-inline-start: 50%;
  transform: translateX(-50%);
  max-width: min(26rem, calc(100% - 2rem));
  margin: 0;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--panel) 92%, transparent);
  color: var(--muted);
  font-size: 0.85rem;
  text-align: center;
}

/* ------------------------------------------------------------ full bleed */

/*
 * A block that leaves the reading column and takes the whole window.
 *
 * Prose wants 64rem and a table of seven columns does not — it wants every
 * pixel there is, and a horizontal scrollbar inside a narrow box is a worse
 * answer than the room that was there all along.
 *
 * `--page` IS MEASURED IN JAVASCRIPT, NOT `100vw`. A viewport unit includes the
 * vertical scrollbar, so the classic `width:100vw; margin-inline:calc(50% -
 * 50vw)` full-bleed overflows by exactly the scrollbar's width on every desktop
 * browser that reserves one — producing a horizontal scrollbar on the page for
 * a rule whose entire purpose was removing one. `ui.js` sets `--page` from
 * `documentElement.clientWidth`, which is the number that is actually true.
 *
 * The fallback matters: with `--page` unset this is an ordinary full-width
 * block rather than a broken one, so the page is correct before the first
 * measurement and stays correct if the script never runs.
 */
.bleed {
  width: var(--page, 100%);
  /* Negative, and exactly half the difference — which is what re-centres a
     wider block inside a narrower one without knowing either number. */
  margin-inline: calc((100% - var(--page, 100%)) / 2);
  /* The same gutter `.view` gives everything else, so the edge lines up. */
  padding-inline: 1.25rem;
}

/*
 * "Find their web presence", inside the sources dialog.
 *
 * It lives here rather than as a seventh button on the companies row: that row
 * is already wider than a window, which is how the company's own name ended up
 * scrolled off the screen.
 */
.findweb {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-block: 0.75rem 0.5rem;
}

.found {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-block-end: 0.5rem;
}

/* The address is the thing being judged, so it is allowed to be long and must
   not push its button off the row. */
.found > div:first-child {
  min-width: 0;
  overflow-wrap: anywhere;
}

.notes {
  margin: 0.5rem 0 0;
  padding-inline-start: 1.1rem;
}

/* ------------------------------------------------------------------ site */

/*
 * The website panel: a score, two captures and a list of faults.
 *
 * Laid out to be TURNED AROUND. A seller holds this out to the person whose
 * website it is, which is why the picture is large and first, and why the
 * findings are sentences rather than a specification.
 */
.site-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.25rem;
}

.site-score {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: 0.15rem;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.site-score strong {
  font-size: 1.7rem;
  line-height: 1;
}

.site-score span {
  color: var(--muted);
  font-size: 0.85rem;
}

.site-verdict p {
  margin: 0 0 0.25rem;
}

/*
 * Four bands, shared by the panel's score block and the list's small badge.
 *
 * Colour carries the reading, so the number is always there beside it — a band
 * alone would be a claim nobody can check, which is the one thing this whole
 * feature is built not to be.
 */
.band-good {
  border-color: color-mix(in srgb, var(--ok-ink) 45%, var(--line));
  color: var(--ok-ink);
}

.band-fair {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  color: var(--accent);
}

.band-poor,
.band-bad {
  border-color: color-mix(in srgb, var(--error) 45%, var(--line));
  color: var(--error);
}

.band-bad {
  background: color-mix(in srgb, var(--error) 12%, transparent);
}

/* The same reading, small enough for a table cell. */
.score {
  display: inline-block;
  min-width: 2.2rem;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 0.85rem;
  font-weight: 600;
  text-align: center;
}

.site-shots {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.site-shots figure {
  margin: 0;
}

/* The laptop capture takes what is left; the phone one stays phone-shaped. */
.shot.desktop {
  flex: 1 1 auto;
  min-width: 0;
}

.shot.mobile {
  flex: 0 0 clamp(9rem, 22%, 13rem);
}

.shot-frame {
  display: grid;
  place-items: center;
  min-height: 6rem;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
}

.shot-frame img {
  display: block;
  width: 100%;
  height: auto;
}

.shot figcaption {
  margin-top: 0.35rem;
}

.site-group {
  margin: 1.25rem 0 0.5rem;
}

.site-group.bad {
  color: var(--error);
}

.site-group.good {
  color: var(--ok-ink);
}

/*
 * Each finding is a claim and the measurement behind it, on two lines.
 *
 * The measurement is never dropped on a narrow screen. It is the half that
 * makes the claim checkable, and a sentence that says a site is slow without
 * saying how slow is the kind of thing this panel exists to replace.
 */
.site-findings {
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-findings li {
  padding: 0.55rem 0.75rem;
  border: 1px solid var(--line);
  border-inline-start-width: 3px;
  border-radius: var(--radius);
  background: var(--panel);
}

.site-findings li + li {
  margin-top: 0.4rem;
}

.site-findings.bad li {
  border-inline-start-color: var(--error);
}

.site-findings.weak li {
  border-inline-start-color: var(--accent);
}

.site-findings.good li {
  border-inline-start-color: var(--ok-ink);
}

.site-findings strong {
  display: block;
}

.site-findings span {
  font-size: 0.9rem;
}

.site-raw {
  margin-top: 1.5rem;
}

/*
 * On a phone the two captures stack, because side by side each would be too
 * small to be evidence of anything — which is the only reason either is here.
 */
@media (max-width: 40rem) {
  .site-shots {
    flex-direction: column;
  }

  .shot.mobile {
    flex: 1 1 auto;
    max-width: 13rem;
  }

  .site-head {
    flex-wrap: wrap;
  }
}

/*
 * The measurements table carries sentences for labels — "Wider than a phone
 * by", "Smallest type on a phone" — where the dossier's `.facts` was built for
 * "NIP" and "KRS". Its 5.5rem label column wrapped every one of these onto
 * three lines.
 */
.site-raw .facts {
  width: 100%;
}

.site-raw .facts th {
  width: 14rem;
  vertical-align: top;
}

/* ------------------------------------------------------------ build spec */

/*
 * The JSON somebody is about to paste into a generator.
 *
 * Behind a `<details>` on purpose — the decision this screen supports is
 * "is this worth handing over", which the summary table answers, and the
 * document itself is for the person who wants to check before pasting. Capped
 * and scrolled for the same reason the dossier's is: a brief with twelve pages
 * of copy would otherwise grow the dialog past the window.
 */
#buildspec-body pre {
  max-height: 26rem;
  padding: 0.75rem;
  border-radius: var(--radius);
  background: var(--bg);
  font-size: 0.78rem;
  line-height: 1.5;
  overflow: auto;
}

#buildspec-body details {
  margin-top: 1.25rem;
}

#buildspec-body summary {
  cursor: pointer;
  font-size: 0.9rem;
}

/*
 * `.facts` was built for the dossier, whose labels are "NIP" and "KRS", so its
 * label column is 5.5rem. THIS IS THE THIRD PANEL TO NEED WIDENING — the
 * website panel's sentences wrapped onto three lines, and both of these do the
 * same: "Language of the copy" and "Everything, summarised" are phrases, not
 * identifiers. Measured in a browser, at 1280px, before the rule below existed.
 */
#buildspec-body .facts th {
  width: 12rem;
  vertical-align: top;
}

#runall-body .facts th {
  width: 13rem;
  vertical-align: top;
  font-weight: 500;
}

#runall-body .facts td {
  vertical-align: top;
}

/*
 * Wider than a form dialog, narrower than a reading one.
 *
 * The default 26rem is right for three fields and a button. This is eleven rows
 * whose second column carries a SENTENCE — the upstream's own reason a lead
 * cannot run an analysis, which is the most useful thing on the panel. At 26rem
 * that reason wrapped to six lines and the checklist ran off the bottom of the
 * screen; measured in a browser at 1280px before this rule.
 */
#runall {
  width: min(34rem, calc(100vw - 2rem));
  max-height: calc(100vh - 4rem);
}

/* ------------------------------------------------------ overnight scout */

/*
 * A `<details>` rather than always-open: this is a once-a-session setting,
 * not a control anybody watches, and the manual "Scout ten" bar above it is
 * the one that deserves the attention on an ordinary visit.
 */
.scout-settings {
  margin-top: 0.75rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  font-size: 0.9rem;
}

.scout-settings summary {
  cursor: pointer;
  font-weight: 600;
}

.scout-settings-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 0.9rem;
  margin-top: 0.5rem;
}

.scout-settings-row label {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-weight: 400;
}

.scout-settings-row .field {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.scout-settings-row input[type="number"] {
  width: 4rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg);
  color: var(--ink);
  font: inherit;
}

/* --------------------------------------------------- search, filter, page */

/*
 * Shared by every table that grew a search box this session: Queue,
 * Companies, Sellers. `input`/`select` default to `width: 100%` (see the
 * fields block above), which is right for a form laid out one field per row
 * and wrong here — a search box and a dropdown sitting side by side both
 * trying to be full width is two elements fighting for one row.
 */
.table-filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
  margin-bottom: 1rem;
}

.table-filters input[type="search"] {
  flex: 1 1 16rem;
  width: auto;
}

.table-filters select {
  flex: 0 0 auto;
  width: auto;
}

.pager {
  margin-top: 0.75rem;
}

/*
 * AI Studio. One card per analysis, closed by default -- eleven open
 * textareas of prompt text on first load is a wall nobody reads, and the ones
 * already customised (open by default) are the ones worth seeing at a glance.
 */
.prompt-card {
  margin-bottom: 0.75rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.prompt-card summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-weight: 600;
}

.prompt-card textarea {
  width: 100%;
  margin-top: 0.6rem;
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
}

.prompt-card .row-between {
  margin-top: 0.5rem;
}

/*
 * The tiles/list wrapper. `#view` already sits inside `.view`'s own padding,
 * so the max-width lives on the wrapper one level up rather than fighting
 * `.view`'s own layout -- and `.expanded` simply lifts the cap rather than
 * setting a second, competing width, so a table's own horizontal scroller
 * (`.bleed`) is untouched either way.
 */
.view-wrap {
  max-width: 1400px;
  margin-inline: auto;
  width: 100%;
}

.view-wrap.expanded {
  max-width: none;
}

.view-controls {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}

.view-toggle {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.view-toggle button {
  border: 0;
  border-radius: 0;
}

.view-toggle button.active {
  background: var(--accent);
  color: var(--accent-ink, #fff);
}

/*
 * One tile per row a table would have drawn -- same data, same buttons, same
 * event wiring, just a card instead of a `<tr>`. `minmax(280px, 1fr)` is
 * chosen against the widest thing a tile carries (Companies' action row),
 * so a tile never gets so narrow its buttons wrap onto four lines.
 */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 0.8rem;
}

.tile {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
}

.tile-title {
  font-weight: 600;
}

.tile-field {
  display: flex;
  justify-content: space-between;
  gap: 0.6rem;
  font-size: 0.85rem;
}

.tile-field .label {
  color: var(--muted);
  flex-shrink: 0;
}

.tile-field .value {
  text-align: right;
}

.tile-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.4rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--line);
}

/*
 * The website screenshot on a Companies tile -- the one glance a seller
 * standing in front of a business actually wants, so it leads the card. A
 * fixed aspect box rather than the capture's own dimensions: the desktop
 * capture's real size would make one tile's picture several times another's,
 * which is the clutter this was added to reduce, not add to.
 */
.tile-thumb {
  aspect-ratio: 16 / 10;
  margin: -0.8rem -0.8rem 0.4rem;
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--surface, color-mix(in srgb, var(--panel) 92%, var(--ink) 8%));
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tile-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top;
}

/* ---------------------------------------------------------------------------
 * The stage, and the people behind it.
 *
 * The stage is the one field on a company row a person decides rather than a
 * crawl measures, so it reads as a control on the seller plane and as a flat
 * chip on the owner's — where there is no route behind it.
 * ------------------------------------------------------------------------- */

.stage-select,
.stage-chip {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.15rem 0.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  max-width: 9.5rem;
}

.stage-chip {
  display: inline-block;
  white-space: nowrap;
}

/*
 * A customer looks settled, not urgent. The three stages that protect the claim
 * share the "this is fine" colour the rest of the dashboard already uses for a
 * finished thing, so a glance down the column separates the pipeline from the
 * book of business without anybody reading a word.
 */
.stage-won,
.stage-delivered,
.stage-live {
  background: var(--ok-bg);
  color: var(--ok-ink);
  border-color: color-mix(in srgb, var(--ok-ink) 40%, var(--line));
}

/* Ended, either way: present but receded, so it stops competing with live work. */
.stage-lost,
.stage-dormant {
  color: var(--muted);
}

.contact-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 0.9rem;
  margin-block-end: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/*
 * A suppressed contact is bordered in the error colour rather than merely
 * carrying a line of text. This panel is scanned for a phone number, and the
 * one thing that must not happen is finding the number without seeing the
 * refusal above it.
 */
.contact-stopped {
  border-color: var(--error);
}

.contact-stop-notice {
  margin: 0;
}

.contact-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.contact-reach {
  margin: 0;
}

.contact-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
  margin-block-start: 0.15rem;
}

.contact-form {
  border-top: 1px solid var(--line);
  padding-block-start: 1rem;
  margin-block-start: 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.contact-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.6rem 0.9rem;
}

.contact-grid label {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.contact-grid input,
.contact-grid select {
  font: inherit;
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.35rem 0.45rem;
}

.contact-wide {
  grid-column: 1 / -1;
}

.checkbox {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/*
 * The box itself, which the form rules would otherwise stretch.
 *
 * Inputs in this dashboard are full-width by default, which is right for every
 * field that holds a value and wrong for the one that is a glyph: a 200px
 * checkbox pushes its own label into a narrow column beside it and reads as a
 * layout fault. `flex: none` as well as a width, because the flex container
 * would happily shrink it below both.
 */
.checkbox input[type="checkbox"] {
  flex: none;
  width: 1rem;
  height: 1rem;
  margin: 0;
  accent-color: var(--accent);
}

/* ---------------------------------------------------------------------------
 * My profile.
 *
 * The one screen a seller fills in about themselves. Everything here is small:
 * a face, three fields and a line of prose.
 * ------------------------------------------------------------------------- */

.profile-head {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  margin-bottom: 1rem;
  flex-wrap: wrap;
}

.avatar {
  flex: none;
  width: 5rem;
  height: 5rem;
  border-radius: 50%;
  object-fit: cover;
  background: var(--line);
}

.avatar-empty {
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: 1.75rem;
}

.row-start {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}

/*
 * A file input dressed as a button.
 *
 * The native control renders as a button plus a filename the browser writes
 * itself, which reads as junk beside a real one and cannot be styled. Wrapping
 * a hidden input in a label keeps the whole thing keyboard-reachable and
 * clickable without any script.
 */
.file-button {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 0.85rem;
  color: var(--ink);
}

.file-button:hover {
  border-color: var(--accent);
}

/*
 * A list that is a list of things rather than prose.
 *
 * The half-finished enrolments on the Sellers tab: each entry is an address and
 * a button, so the marker and the indent are noise around a row.
 */
.plain {
  list-style: none;
  margin: 0.75rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

/*
 * A field that is off because another field says so.
 *
 * The seller dialog disables the branch allocation until somebody may recruit.
 * Without this it looks exactly like an empty field that simply refuses to
 * accept typing, which reads as a broken input rather than as a consequence.
 */
#seller-edit input:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
 * The Pipeline tab.
 *
 * A board that is scanned rather than read: the count is the thing the eye
 * lands on, the stage names it, and the aging line qualifies it. Empty stages
 * stay in place and recede — a board that dropped them would rearrange itself
 * as work moved, and the gap where a stage should be is the information.
 * ------------------------------------------------------------------------- */

.board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
  gap: 0.6rem;
  margin-block-start: 0.75rem;
}

/* The two exits, below the pipeline and quieter: an archive, not a queue. */
.board-closed {
  grid-template-columns: repeat(auto-fit, minmax(7.5rem, 12rem));
  margin-block-start: 0.5rem;
  opacity: 0.72;
}

.board-col {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem 0.7rem;
  background: var(--panel);
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.board-col-empty {
  opacity: 0.55;
}

/* Settled, not urgent — the same colour the dashboard already uses for a
 * finished thing, so the book of business separates from the pipeline at a
 * glance without anybody reading a word. */
.board-col-customer {
  background: var(--ok-bg);
  border-color: color-mix(in srgb, var(--ok-ink) 35%, var(--line));
}

.board-stage {
  font-size: 0.78rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.board-count {
  font-size: 1.7rem;
  font-weight: 600;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.health {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0.6rem;
  margin-block-start: 0.75rem;
}

.health-item {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.7rem 0.8rem;
  background: var(--panel);
}

/* Only what needs attention is coloured. A dashboard where every tile is red
 * is one nobody reads, so a healthy figure stays the ordinary ink. */
.health-bad {
  border-color: var(--error);
}

.health-bad .health-value {
  color: var(--error);
}

.health-value {
  font-size: 1.45rem;
  font-weight: 600;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.health-label {
  font-size: 0.88rem;
  margin-block: 0.1rem 0.25rem;
}

/* ---------------------------------------------------------------------------
 * The history column.
 * ------------------------------------------------------------------------- */

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.tl-item {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: 0.6rem;
  padding-block: 0.5rem;
  /* The rule runs between entries rather than around them, so the column reads
   * as one sequence instead of a stack of cards. */
  border-block-start: 1px solid var(--line);
}

.tl-item:first-child {
  border-block-start: 0;
}

.tl-mark {
  color: var(--muted);
  text-align: center;
  line-height: 1.5;
}

/* Somebody else's contact: present, and visibly not ours to read. */
.tl-contact_other .tl-body {
  opacity: 0.75;
}

.tl-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.tl-when {
  white-space: nowrap;
}

.tl-detail {
  margin: 0.2rem 0 0;
  white-space: pre-wrap;
}

/* ---------------------------------------------------------------------------
 * Money.
 *
 * Amounts are tabular everywhere they appear, because these are read in a
 * column and compared to each other. Overdue is the only thing coloured — a
 * panel where every figure is red is one nobody reads.
 * ------------------------------------------------------------------------- */

.money-amount,
.money-ref,
.money-invoices li {
  font-variant-numeric: tabular-nums;
}

.money-unit {
  font-size: 0.65em;
  font-weight: 400;
  color: var(--muted);
}

.money-card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem 0.9rem;
  margin-block-end: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

/* A cancelled sale is a thing that happened, so it stays on screen — receded,
 * and struck through where the number is, so it cannot be read as live. */
.money-cancelled {
  opacity: 0.6;
}

.money-cancelled .money-amount {
  text-decoration: line-through;
}

.money-amount {
  font-weight: 600;
  white-space: nowrap;
}

.money-invoices,
.money-changes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}

.money-invoices li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding-block: 0.2rem;
  border-block-start: 1px solid var(--line);
}

.money-invoices li:first-child {
  border-block-start: 0;
}

.money-ref {
  color: var(--muted);
  font-size: 0.85rem;
}

.money-overdue .money-ref {
  color: var(--error);
}

.money-invoice-actions,
.money-actions {
  display: flex;
  gap: 0.35rem;
  flex-wrap: wrap;
}

.money-invoice-actions {
  margin-inline-start: auto;
}

.money-change {
  border-block-start: 1px solid var(--line);
  padding-block: 0.45rem;
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.money-change:first-child {
  border-block-start: 0;
}

/*
 * Removing a company is the one row action that takes a lead out of the pool
 * and puts somebody's name on the decision. It reads as consequential without
 * shouting: the error colour on the text and the border, not a filled red
 * button sitting among eight ghosts.
 */
.ghost.danger {
  color: var(--error);
  border-color: color-mix(in srgb, var(--error) 40%, var(--line));
}

.ghost.danger:hover {
  border-color: var(--error);
}

/* ---------------------------------------------------------------------------
 * Selecting many companies, and the batch that runs over them.
 *
 * The bar only exists while something is ticked — see `drawSelection` — so it
 * is styled to be noticed rather than to sit quietly: it carries the one
 * control on this page that spends money on two hundred companies at once.
 * ------------------------------------------------------------------------ */
.selection-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 1rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 8%, var(--panel));
}

/*
 * The tick column, kept narrow. `width` on a table cell is a suggestion the
 * browser honours when nothing else in the column is wider, which here is
 * exactly the case.
 */
.pick-cell {
  width: 2rem;
  vertical-align: middle;
}

/* The fields block sets `width: 100%` on inputs; a checkbox is not a field. */
.pick-cell input[type="checkbox"],
.automation-step input[type="checkbox"] {
  width: auto;
  margin: 0;
}

.tile-title .pick-cell {
  display: inline-block;
  margin-inline-end: 0.4rem;
}

/*
 * The batch's progress bar.
 *
 * The width is set from JavaScript through the CSSOM, never as a `style`
 * attribute — `style-src 'self'` blocks the attribute and permits the
 * property, which is the lesson the dossier's brand-colour chips already
 * record. So the fill starts at zero here and is moved by the script.
 */
.batch-bar {
  height: 0.5rem;
  margin: 0.8rem 0;
  border-radius: 999px;
  background: var(--line);
  overflow: hidden;
}

.batch-bar-fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  transition: width 0.4s ease;
}

.batch-failed {
  margin: 0.6rem 0;
  font-size: 0.85rem;
}

.batch-failed ul {
  margin: 0.4rem 0 0;
  padding-inline-start: 1.2rem;
  color: var(--muted);
}

/* ---------------------------------------------------------------------------
 * Automation: the steps, in the order they happen.
 * ------------------------------------------------------------------------ */
.automation-steps {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0.8rem 0 1.2rem;
}

.automation-step {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* The label block sets a bottom margin for stacked form fields. */
  margin: 0;
  font-weight: 400;
}

.automation-step > span {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

/*
 * The send step. Dimmed rather than absent: a switch that is missing reads as
 * one nobody has built yet, and this one is a decision (rule 31).
 */
.automation-step.off {
  border-style: dashed;
  opacity: 0.75;
}
