/* AgentMetric — CPL journey design 1 (built blind to GetAgent/HomeMove).
   Direction: surveying instruments, not the AI-design house style. Sans display over serif body
   inverts the usual pairing on purpose.

   ⚠ The original signature — a vertical true-line at 1.000 with each agent plotted as a deviation —
   was CUT by James on 2026-08-18. Do not reinvent it in another orientation: no dot plot, no
   deviation bar, no diverging axis, no gauge. The comparison is carried by RANKING and by the SIZE
   of the figures themselves. Good/bad is never encoded by red/green either, so the page still
   survives colourblindness and photocopying. */

:root {
  /* Palette — drawn from survey drawings and drafting paper, not from a brand deck. */
  /* Aligned to the brand suite 2026-08-18 (leadgen/brand/BRAND_GUIDE.md): navy #0D1B3D and blue
     #1E6BFF are the brand's own values. White on #1E6BFF measures 4.55:1 — AA for the bold button
     text it carries. The warning orange stays ours; the brand defines no warning colour. */
  --ink:        #0D1B3D;
  --ink-2:      #3D4A66;
  --ink-3:      #6E7480;
  --paper:      #EDF0F2;
  --surface:    #FFFFFF;
  --rule:       #C9D3DA;
  --rule-soft:  #DFE6EA;
  --accent:     #1E6BFF;
  --accent-ink: #164FD6;
  --accent-wash:#E9F0FF;
  --flag:       #C2410C;   /* ONLY for low-data and warnings. Never decorative. */
  --flag-wash:  #FBEDE6;

  --shadow-sm: 0 1px 2px rgba(18,35,46,.06), 0 1px 3px rgba(18,35,46,.04);
  --shadow-md: 0 2px 4px rgba(18,35,46,.05), 0 8px 24px rgba(18,35,46,.07);
  --shadow-lg: 0 12px 40px rgba(18,35,46,.12);

  --display: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --body:    "Newsreader", ui-serif, Georgia, "Times New Roman", serif;
  --mono:    "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --measure: 68ch;
  --wrap: 1160px;
  --wrap-narrow: 760px;
  --radius: 3px;   /* near-square: this is an instrument, not a toy */
  --paper-2:  #E3E8EB;
  --rule-firm:#AEBCC6;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--body);
  font-size: 1.0625rem;
  line-height: 1.6;
  font-optical-sizing: auto;
}

h1, h2, h3, h4, .display, .eyebrow, button, .btn, label, .label, th, nav, .stat-label {
  font-family: var(--display);
}

h1, h2, h3, h4 { margin: 0 0 .5em; line-height: 1.1; letter-spacing: -0.02em; font-weight: 700; }
h1 { font-size: clamp(2.1rem, 5.2vw, 3.5rem); letter-spacing: -0.035em; }
h2 { font-size: clamp(1.5rem, 3vw, 2.1rem); letter-spacing: -0.028em; }
h3 { font-size: 1.16rem; letter-spacing: -0.015em; }
p  { margin: 0 0 1em; max-width: var(--measure); }

a { color: var(--accent-ink); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { color: var(--ink); }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: 22px; }
.wrap-narrow { max-width: var(--wrap-narrow); }

.eyebrow {
  font-size: .72rem; font-weight: 700; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-3); margin: 0 0 .9rem;
}

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.num  { font-family: var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }

/* ---------------------------------------------------------------- the mark */

.brandmark { display: flex; align-items: center; gap: .6rem; text-decoration: none; color: inherit; }
.brandmark svg { flex: none; display: block; }
.brandmark b {
  font-family: var(--display); font-weight: 700; font-size: 1.08rem;
  letter-spacing: -0.03em; color: var(--ink);
}
.brandmark small {
  font-family: var(--mono); font-size: .58rem; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-3); display: block; margin-top: -2px;
}

/* ---------------------------------------------------------------- chrome */

.masthead {
  position: sticky; top: 0; z-index: 40;
  background: rgba(237,240,242,.92);
  backdrop-filter: saturate(140%) blur(8px);
  border-bottom: 1px solid var(--rule);
}
.masthead .wrap { display: flex; align-items: center; gap: 1.5rem; min-height: 62px; }
.masthead nav { margin-left: auto; display: flex; align-items: center; gap: 1.4rem; }
/* ⚠ :not(.btn) is load-bearing, not tidiness. Without it this rule (0,2,1) outranks .btn (0,1,0)
   and repaints the header call-to-action's label in body ink on a blue field — invisible. It
   shipped that way for exactly one screenshot. */
.masthead nav a:not(.btn) {
  font-size: .84rem; font-weight: 600; letter-spacing: -0.01em;
  color: var(--ink-2); text-decoration: none;
}
.masthead nav a:not(.btn):hover { color: var(--ink); text-decoration: underline; text-underline-offset: 4px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
  font-size: .95rem; font-weight: 700; letter-spacing: -0.01em;
  padding: .82rem 1.4rem; border-radius: var(--radius);
  border: 1px solid transparent; cursor: pointer;
  background: var(--accent); color: #fff; text-decoration: none;
  transition: background .15s ease, transform .15s ease, box-shadow .15s ease;
}
.btn:hover { background: var(--accent-ink); color: #fff; }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn[aria-disabled="true"] {
  background: var(--rule); color: var(--ink-3); cursor: not-allowed; border-color: var(--rule);
}
.btn-lg { font-size: 1.05rem; padding: 1.05rem 1.9rem; }
.btn-ghost { background: transparent; color: var(--ink); border-color: var(--rule); }
.btn-ghost:hover { background: var(--surface); color: var(--ink); border-color: var(--ink-3); }

/* ---------------------------------------------------------------- hero */

.hero { padding: clamp(1.4rem, 6vw, 4.6rem) 0 clamp(2rem, 4vw, 3rem); }

/* ⚠ THREE children, not two, and that is the mobile fix. At 390px the old hero put ~620px of
   headline, paragraph, button and caveat above anything factual. Splitting the text column lets
   the EVIDENCE sit between the standfirst and the call to action on a phone, while staying in the
   right-hand column on a desktop. */
.hero-grid {
  display: grid; gap: clamp(1.25rem, 5vw, 4rem); align-items: start;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.02fr);
  grid-template-areas: "say show" "act show";
}
.hero-say  { grid-area: say; }
.hero-show { grid-area: show; }
.hero-act  { grid-area: act; }
.hero h1 { margin-bottom: .55rem; }
.hero .lede {
  font-size: clamp(1.08rem, 1.7vw, 1.25rem); color: var(--ink-2); line-height: 1.55;
  max-width: 46ch; margin-bottom: 1.5rem;
}
.hero-cta { display: flex; flex-wrap: wrap; gap: .8rem; align-items: center; margin-bottom: 1.3rem; }
.hero-note { font-size: .85rem; color: var(--ink-3); font-family: var(--display); max-width: 44ch; }

/* ---------------------------------------------------------------- sections */

.section { padding: clamp(2.4rem, 5vw, 3.8rem) 0; border-top: 1px solid var(--rule); }
.section-tight { padding: clamp(1.8rem, 3.5vw, 2.6rem) 0; }
.section h2 { max-width: 22ch; }
.section > .wrap > p { color: var(--ink-2); }

/* Editorial split: the claim on the left, the working on the right. Stops a single prose column
   stranding half the page. */
.split { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.25fr); gap: clamp(1.6rem, 5vw, 4rem); align-items: start; }
.split h2 { max-width: 16ch; margin-bottom: 0; }
.split .prose > :first-child { margin-top: 0; }

.explain { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(1.2rem, 3vw, 2.2rem); margin-top: 2rem; }
.explain-item h3 { margin-bottom: .35rem; }
.explain-item p { font-size: .95rem; color: var(--ink-2); margin: 0; }
.explain-fig {
  height: 44px; margin-bottom: .9rem; border-bottom: 1px solid var(--rule);
  position: relative;
}

/* ---------------------------------------------------------------- forms */

.card {
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: clamp(1.4rem, 3vw, 2.1rem);
  box-shadow: var(--shadow-sm);
}
.card + .card { margin-top: 1.2rem; }

.field { margin-bottom: 1.35rem; }
.field:last-child { margin-bottom: 0; }
.field > label, .field > .label {
  display: block; font-size: .9rem; font-weight: 700; letter-spacing: -0.01em;
  margin-bottom: .3rem; color: var(--ink);
}
.field .hint {
  display: block; font-size: .85rem; color: var(--ink-3); margin: 0 0 .55rem; max-width: 56ch;
  font-family: var(--display); font-weight: 400; line-height: 1.45;
}
input[type="text"], input[type="email"], input[type="tel"], select, textarea {
  width: 100%; font-family: var(--display); font-size: 1rem; color: var(--ink);
  padding: .78rem .85rem; background: var(--surface);
  border: 1px solid var(--ink-3); border-radius: var(--radius);
}
input:focus, select:focus, textarea:focus { border-color: var(--accent); }
textarea { min-height: 96px; resize: vertical; font-family: var(--display); line-height: 1.5; }
input::placeholder, textarea::placeholder { color: var(--ink-3); opacity: .75; }

.field-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.1rem; }

.chips { display: flex; flex-wrap: wrap; gap: .5rem; }
.chip {
  position: relative; display: inline-flex;
}
.chip input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.chip span {
  display: inline-flex; align-items: center; padding: .55rem .95rem;
  font-family: var(--display); font-size: .92rem; font-weight: 600;
  border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--surface); color: var(--ink-2); cursor: pointer;
  transition: border-color .12s ease, background .12s ease, color .12s ease;
}
.chip input:hover + span { border-color: var(--ink-3); }
.chip input:checked + span { background: var(--accent); border-color: var(--accent); color: #fff; }
.chip input:focus-visible + span { outline: 3px solid var(--accent); outline-offset: 2px; }

/* consent — deliberately NOT styled as a throwaway line */
.consent {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .85rem;
  padding: 1.05rem; border: 1px solid var(--rule); border-radius: var(--radius);
  background: var(--paper);
}
.consent + .consent { margin-top: .85rem; }
.consent input[type="checkbox"] { width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--accent); cursor: pointer; }
.consent .consent-body { font-size: .93rem; line-height: 1.5; }
.consent .consent-body strong { font-family: var(--display); font-size: .88rem; display: block; margin-bottom: .2rem; }
.consent .consent-meta { font-size: .82rem; color: var(--ink-3); font-family: var(--display); margin-top: .35rem; }

/* ---------------------------------------------------------------- results */

.controls {
  position: sticky; top: 62px; z-index: 30;
  background: var(--paper); border-bottom: 1px solid var(--rule);
  padding: .85rem 0;
}
.controls-inner { display: flex; flex-wrap: wrap; gap: 1rem 1.4rem; align-items: end; }
.control { display: flex; flex-direction: column; gap: .25rem; }
.control label { font-family: var(--display); font-size: .72rem; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--ink-3); }
.control select { padding: .5rem .7rem; font-size: .9rem; min-width: 168px; }

.result-count { font-family: var(--display); font-size: .85rem; color: var(--ink-2); margin-left: auto; align-self: center; }

.tbl-wrap { overflow-x: auto; background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); }
table.agents { width: 100%; border-collapse: collapse; font-size: .92rem; min-width: 900px; }
table.agents th {
  text-align: right; font-size: .68rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-3); padding: .9rem .7rem .6rem; border-bottom: 1px solid var(--rule);
  white-space: nowrap; vertical-align: bottom;
}
table.agents th:first-child, table.agents th.left { text-align: left; }
table.agents td { padding: .85rem .7rem; border-bottom: 1px solid var(--rule-soft); text-align: right; vertical-align: middle; }
table.agents td:first-child, table.agents td.left { text-align: left; }
table.agents tbody tr:hover { background: var(--accent-wash); }
table.agents tbody tr.is-selected { background: var(--accent-wash); }
table.agents tbody tr.is-selected td { border-color: var(--rule); }
table.agents tfoot td {
  border-top: 2px solid var(--ink); border-bottom: none; font-weight: 700;
  font-family: var(--display); font-size: .88rem; color: var(--ink-2); padding-top: .8rem;
}
.agent-name { font-family: var(--display); font-weight: 700; font-size: .98rem; letter-spacing: -0.015em; }
.agent-sub { font-size: .78rem; color: var(--ink-3); font-family: var(--display); }

.pick { display: inline-flex; align-items: center; }
.pick input { width: 20px; height: 20px; accent-color: var(--accent); cursor: pointer; }
.pick input:disabled { cursor: not-allowed; opacity: .35; }


.lowdata {
  display: inline-flex; align-items: center; gap: .3rem;
  font-family: var(--display); font-size: .76rem; font-weight: 600;
  color: var(--flag); background: var(--flag-wash);
  padding: .12rem .45rem; border-radius: 2px; white-space: nowrap;
}

.selbar {
  position: sticky; bottom: 0; z-index: 35;
  background: var(--ink); color: #fff;
  padding: .9rem 0;
  box-shadow: 0 -8px 24px rgba(18,35,46,.16);
}
.selbar .wrap { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.selbar-text { font-family: var(--display); font-size: .95rem; }
.selbar-text b { font-weight: 700; }
.selbar-names { font-size: .82rem; color: #B8C7D3; font-family: var(--display); }
.selbar-copy { min-width: 0; }
.selbar .btn { margin-left: auto; background: #fff; color: var(--ink); }
.selbar .btn:hover { background: var(--accent-wash); color: var(--ink); }
.selbar[hidden] { display: none; }

/* ⭐ THE BUTTON DOES NOT APPEAR, IT WAKES UP. Same box, same position, same size in both states —
   only the weight changes. A control that materialises under the thumb at the moment of tapping a
   checkbox is how a mis-tap happens; a control that has been sitting there, visibly waiting, is
   how someone learns the page has a next step before they have made a single choice.
   ⚠ Not `opacity`, and not grey-on-grey: a ghost button on navy has to stay READABLE while
   reading as not-yet. An outline at reduced contrast does that; a 40%-opacity white does not. */
.selbar-cta[aria-disabled="true"] {
  background: transparent;
  color: #8FA6B8;
  box-shadow: inset 0 0 0 1px #3A5061;
  cursor: default;
}
.selbar-cta[aria-disabled="true"]:hover { background: transparent; color: #8FA6B8; }
.selbar-cta { transition: background-color .18s ease, color .18s ease, box-shadow .18s ease; }
@media (prefers-reduced-motion: reduce) { .selbar-cta { transition: none; } }

/* the empty state is an instruction, so it is set as one: the line that tells you what to do is
   the prominent one, and the count only exists once there is a count */
.selbar[data-empty="true"] .selbar-text { font-weight: 600; }
.selbar-names.is-nudge { color: #FFD9A8; }

/* ---------------------------------------------------------------- prose pages */

.prose { max-width: var(--measure); }
.prose h2 { margin-top: 2.2rem; }
.prose h3 { margin-top: 1.6rem; }
.prose ul { padding-left: 1.15rem; max-width: var(--measure); }
.prose li { margin-bottom: .45rem; }
.prose table { width: 100%; border-collapse: collapse; margin: 1.2rem 0; font-size: .93rem; }
.prose th, .prose td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
.prose th { font-family: var(--display); font-size: .74rem; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3); }

.callout {
  border-left: 3px solid var(--accent); background: var(--surface);
  padding: 1rem 1.2rem; margin: 1.4rem 0; border-radius: 0 var(--radius) var(--radius) 0;
}
.callout p:last-child { margin-bottom: 0; }

/* ---------------------------------------------------------------- steps / confirmation */

.steps { display: flex; align-items: center; gap: .55rem; flex-wrap: wrap; margin-bottom: 1.6rem; }
.step {
  font-family: var(--mono); font-size: .7rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-3); display: inline-flex; align-items: center; gap: .45rem;
}
.step[aria-current="step"] { color: var(--ink); font-weight: 600; }
.step::after { content: ""; width: 18px; height: 1px; background: var(--rule); display: inline-block; }
.step:last-child::after { display: none; }

.tick { width: 46px; height: 46px; border-radius: 50%; background: var(--accent); display: grid; place-items: center; margin-bottom: 1.1rem; }

/* ---------------------------------------------------------------- the reassurance panel
   Sits at the top of the valuation form, where the seller is first asked for a phone number.
   ⚠ It uses --accent-wash and --radius rather than values of its own: the near-square radius is
   the site's identity ("this is an instrument, not a toy") and one rounder panel would read as
   a component borrowed from somewhere else.
   ⭐ The mark is the same idiom as .tick on the confirmation page - a solid accent shape holding
   a white glyph - so it belongs to the set rather than introducing a second icon language. */
.safenote {
  display: grid; grid-template-columns: auto 1fr; gap: 1rem; align-items: start;
  background: var(--accent-wash); border-radius: var(--radius);
  padding: 1.05rem 1.15rem; margin: 0 0 1.7rem;
}
.safenote-mark { display: block; }
.safenote-h {
  font-family: var(--display); font-weight: 700; font-size: 1rem; line-height: 1.3;
  color: var(--ink); margin: .1rem 0 .35rem;
}
.safenote-p { margin: 0; color: var(--ink-2); font-size: .95rem; line-height: 1.5; }
@media (max-width: 560px) {
  .safenote { gap: .8rem; padding: .95rem 1rem; }
  .safenote-mark { width: 31px; height: 36px; }
}

.recap { display: grid; gap: .7rem; margin: 1.3rem 0; }
/* ⚠ The recap moved from the top of the form to sit immediately above the Send button
   (James, 2026-08-20), and it had no styling of its own — it rendered as a stray bulleted
   list. Beside the button it has to read as part of the send action, so: tight, quiet, and
   no bullet furniture competing with the button underneath it. */
/* ⚠ `margin-top: 0` was right when the recap sat ABOVE the button — it hugged the field
   above it. It now sits BELOW the button and was flush against it. James, 2026-08-21. */
.recap:has(.recap-list) { margin: 1.05rem 0 .9rem; gap: .25rem; }
.recap-lede {
  margin: 0; font-family: var(--display); font-size: .88rem; color: var(--ink-3);
}
.recap-list {
  margin: .15rem 0 0; padding: 0; list-style: none;
  font-family: var(--display); font-size: .95rem; font-weight: 700;
  letter-spacing: -0.015em; line-height: 1.45;
}
.recap-item {
  display: flex; align-items: center; gap: .8rem;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--radius); padding: .85rem 1rem;
}
.recap-item b { font-family: var(--display); font-size: .98rem; letter-spacing: -0.015em; }
.recap-item span { margin-left: auto; font-family: var(--mono); font-size: .8rem; color: var(--ink-3); }

/* ---------------------------------------------------------------- footer */

.footer { border-top: 1px solid var(--rule); padding: 2.4rem 0 3rem; margin-top: 2rem; }
.footer-grid { display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr)); gap: 2rem; }
.footer h4 { font-size: .74rem; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); margin-bottom: .7rem; }
.footer ul { list-style: none; margin: 0; padding: 0; }
.footer li { margin-bottom: .4rem; }
.footer a { font-family: var(--display); font-size: .88rem; color: var(--ink-2); text-decoration: none; }
.footer a:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }

/* The cookie-choices control is a BUTTON, because it performs an action rather than navigating —
   but it has to read as one of the footer links beside it, or the withdrawal route looks like a
   different, lesser thing than the links around it. Styled to match exactly, not approximately. */
.linkish {
  font-family: var(--display); font-size: .88rem; color: var(--ink-2);
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  text-align: left; line-height: inherit; letter-spacing: inherit;
}
.linkish:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.footer .small { font-size: .82rem; color: var(--ink-3); font-family: var(--display); max-width: 44ch; line-height: 1.5; }

.proto-flag {
  background: var(--ink); color: #fff; text-align: center;
  font-family: var(--mono); font-size: .7rem; letter-spacing: .06em;
  padding: .4rem 1rem;
  /* ⚠ one line, always. At 360px this wrapped to two and cost ~24px of the fold on a page whose
     whole problem was the fold. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
@media (max-width: 700px) { .proto-flag { font-size: .6rem; letter-spacing: .03em; padding: .32rem .7rem; } }
.proto-flag b { color: #fff; }

/* ---------------------------------------------------------------- cookie banner */

/* ⭐⭐ A CENTRED OVERLAY, NOT A BOTTOM BAR - James, 2026-08-26, after looking at GetAgent's.
   THE MEASUREMENT THAT JUSTIFIES IT: of the 616 sessions that arrived on a Meta ad click that day,
   614 NEVER ANSWERED THE BANNER AT ALL and 2 declined. The failure was not people saying no, it
   was people never registering that there was a question - and a strip docked to the bottom of a
   phone is the easiest thing on the page to miss, most of all inside the Facebook and Instagram
   in-app browsers, which put their OWN toolbar exactly there.
   ⭐ IT ALSO RETIRES A WHOLE CLASS OF BUG. Every rule that used to dock this to the bottom existed
   to keep it clear of the selection bar, which is also bottom-docked: a z-index war, a --selbar-h
   clearance, and a sticky-to-fixed override, all of them fixes for a collision between two things
   anchored to the same edge. Centred, they cannot collide.
   ⚠⚠ AND IT FLIPS WHICH MUTANT IS EQUIVALENT. `compliance_check` recorded that flipping the
   z-index alone was an EQUIVALENT mutant, because the two were separated in the Y axis and never
   overlapped. They overlap by design now, so the stacking order is load-bearing again and there is
   a real mutation case for it. Both C3b cases were rewritten in the same commit. */
.cookie {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: center; justify-content: center;
  padding: 20px;
  background: rgba(13, 27, 61, .55);
}
/* ⚠ max-height + scroll is not decoration: on a phone in landscape the panel is taller than the
   viewport, and a consent panel whose buttons sit off-screen is an unanswerable banner - the exact
   failure C3b exists to catch, arriving by a different route. */
.cookie-panel {
  background: var(--surface); border: 1px solid var(--ink); border-radius: var(--radius);
  box-shadow: var(--shadow-lg); padding: 1.15rem 1.25rem;
  max-width: 560px; width: 100%;
  max-height: calc(100dvh - 40px); overflow-y: auto;
}
/* the page must not scroll behind the overlay. `consent-pending` is on <body> for every route
   whenever the question is outstanding (`_base.html`), so this needs no script either. */
body.consent-pending { overflow: hidden; }
.cookie h4 { font-size: .95rem; margin-bottom: .35rem; letter-spacing: -0.015em; text-transform: none; color: var(--ink); }
/* ⚠ TYPOGRAPHY ONLY in this block — tightening the leading and putting the two buttons on one
   row is where most of the reclaimed chrome comes from.
   ⚠⚠ AND A CORRECTION TO WHAT THIS COMMENT USED TO SAY. It claimed the wording could not be
   changed because doing so "would orphan every stored stamp". That is wrong, and it was wrong in a
   way that discourages a legitimate edit: the fingerprint is DERIVED from the rendered text
   precisely so it moves when the text moves, `consent/record.py` does INSERT OR IGNORE INTO
   wordings so the old text stays on file beside the new one, and an existing record correctly
   goes on saying which wording THAT person agreed to. Nothing is orphaned.
   ⭐ What IS true: rewording is a real event rather than a cosmetic edit, so it belongs in a
   commit that says so. The copy was changed on 2026-08-26 (James picked it from four rendered
   options) and this comment is the one that would have talked the next person out of it. */
.cookie p { font-size: .8rem; color: var(--ink-2); margin-bottom: .6rem; line-height: 1.45; }
@media (max-width: 700px) {
  /* ⚠ TYPOGRAPHY ONLY NOW. This block used to full-bleed the banner to the phone's edges, which
     was the right answer while it was a bottom bar and is the wrong one for a centred panel - a
     card with no gutter reads as a broken layout rather than as a dialog. The tightened type is
     kept; the positioning went with the bar. */
  .cookie { padding: 14px; }
  .cookie-panel { padding: .95rem 1.05rem; }
  .cookie h4 { font-size: .86rem; }
  .cookie p { font-size: .78rem; line-height: 1.4; margin-bottom: .5rem; }
}
/* ⚠⚠ `flex: 1 1 0`, NOT `1 1 auto`, AND THAT IS THE COMPLIANCE PROPERTY RATHER THAN A TIDINESS
   ONE. With `auto` the two buttons are sized from their LABELS, so "equal prominence" holds only
   as long as the two words happen to be a similar length - MEASURED at 390px with Accept/Decline:
   157px against 160px, equal by luck. With `0` they split the row exactly, whatever the labels
   later become, so the property survives the next reword instead of quietly depending on it. */
.cookie-actions .btn { flex: 1 1 0; padding: .55rem .6rem; font-size: .82rem; }
.cookie-current {
  font-weight: 600; color: var(--ink) !important;
  margin-bottom: .5rem !important;
}
.cookie-current[hidden] { display: none; }
.cookie-actions { display: flex; gap: .6rem; flex-wrap: wrap; }
.cookie .btn { padding: .6rem 1.1rem; font-size: .88rem; }
.cookie[hidden] { display: none; }

/* ---------------------------------------------------------------- responsive */

@media (max-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 1fr); grid-template-areas: "say" "show" "act"; gap: 1.4rem; }
  .split { grid-template-columns: minmax(0, 1fr); gap: 1.4rem; }
  .split h2 { max-width: 24ch; }
  .explain { grid-template-columns: minmax(0, 1fr); gap: 1.6rem; }
  .footer-grid { grid-template-columns: minmax(0, 1fr); gap: 1.6rem; }
}

@media (max-width: 700px) {
  body { font-size: 1.02rem; }
  h1 { font-size: clamp(1.85rem, 8.2vw, 2.15rem); letter-spacing: -0.04em; }
  .hero .lede { font-size: 1.02rem; margin-bottom: 0; }
  .hero-note { margin-top: .8rem; }
  .hero-act .btn { width: 100%; }
  .wrap { padding-inline: 18px; }
  .masthead nav { gap: .7rem; }
  .masthead nav a.hide-sm { display: none; }
  /* ⚠ at 360 the header was 145px of a 800px fold: the button wrapped to two lines and the
     working-name label wrapped under the wordmark. Both now hold one line. */
  .masthead .wrap { min-height: 54px; gap: .6rem; }
  .masthead nav .btn { padding: .6rem .85rem; font-size: .88rem; white-space: nowrap; }
  .brandmark small { display: none; }
  .brandmark b { font-size: 1rem; }
  .field-row { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .field-row .field { margin-bottom: 1.35rem; }
  .controls { position: static; }
  .control { flex: 1 1 140px; }
  .control select { min-width: 0; width: 100%; }
  .result-count { margin-left: 0; width: 100%; }
  .selbar .wrap { gap: .6rem; }
  .selbar .btn { margin-left: 0; width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---------------------------------------------------------------- the hero, rebuilt

   ⚠ NO AREA FIGURES. A paid visitor arrives BEFORE telling us where they are, so any local number
   is either wrong for them or unknowable — Leeds figures shown to someone in Bristol are worse
   than no figures. The postcode field is the hero, which is what the category does and what makes
   everything after it true for the reader. It also removes a step from the funnel. */

.hero-centre { max-width: 640px; margin-inline: auto; text-align: center; }
.hero-centre h1 { margin-bottom: .6rem; }
.hero-centre .lede { margin: 0 auto 1.6rem; max-width: 34ch; color: var(--ink-2); }

.eyebrow-chip {
  display: inline-block; margin: 0 0 1.1rem;
  font-family: var(--display); font-size: .76rem; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--accent-ink); background: var(--accent-wash);
  padding: .38rem .8rem; border-radius: 100px;
}

/* ⭐ THE LANDING ARTWORK, added 2026-08-21 when the worked-example table was removed. */
/* ⚠⚠ THESE THREE WERE USED BY index.html AND DEFINED NOWHERE — VERIFIED 2026-08-20 with a
   whole-file search returning zero occurrences of each, against controls (hero-form 7,
   tbl-wrap 5, selbar 28) that proved the search worked. Every band on the landing page therefore
   rendered on the same flat --paper ground, separated only by 1px hairlines. The page was
   AUTHORED to have three tonal grounds and shipped with one. */
.section--tint { background: var(--paper-2); }
.section--invert { background: var(--ink); color: var(--paper); }
.section--invert h2, .section--invert h3 { color: var(--paper); }
.section--invert .eyebrow { color: var(--rule); }
.section--invert a { color: #9DC1FF; }
.split-aside { color: var(--ink-2); }
.section--invert .split-aside { color: var(--rule); }

.hero-art { margin: 1.9rem auto 0; max-width: 760px; }
.hero-art picture, .hero-art img {
  display: block; width: 100%; height: auto;
  border-radius: calc(var(--radius) * 3); background: var(--surface); box-shadow: var(--shadow-md);
}
.hero-pills {
  list-style: none; display: flex; flex-wrap: wrap; gap: .55rem; justify-content: center;
  margin: 1.1rem auto 0; padding: 0; max-width: 760px;
}
.hero-pills li {
  display: inline-flex; align-items: center; gap: .5rem;
  background: var(--surface); border: 1px solid var(--rule);
  border-radius: calc(var(--radius) * 2); padding: .5rem .85rem;
  font-family: var(--display); font-size: .8rem; font-weight: 600; color: var(--ink-2);
}
/* ⚠ Was three different unicode dingbats (◉ ● ◎) at three optical weights, drawn by whatever
   font the device had. One inline tick is identical everywhere and is what "slick" means here. */
.pill-ic { color: var(--accent); flex: none; }
@media (max-width: 560px) {
  .hero-pills { flex-direction: column; align-items: stretch; gap: .4rem; }
  .hero-pills li { justify-content: flex-start; }
}

.hero-form {
  display: flex; gap: .5rem; max-width: 520px; margin: 0 auto;
  background: var(--surface); border: 1px solid var(--rule-firm); border-radius: var(--radius);
  padding: .4rem; box-shadow: 0 1px 2px rgba(18,35,46,.04), 0 10px 26px -16px rgba(18,35,46,.4);
}
.hero-form input {
  flex: 1 1 auto; min-width: 0; border: 0; background: transparent;
  font-size: 1.05rem; padding: .7rem .8rem;
}
.hero-form input:focus { outline: none; }
.hero-form:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
.hero-form .btn { flex: none; white-space: nowrap; }
.hero-error { color: var(--flag); font-family: var(--display); font-size: .9rem; margin: .7rem 0 0; }

.ticks {
  list-style: none; margin: 1.5rem 0 0; padding: 0;
  display: inline-flex; flex-direction: column; gap: .5rem; text-align: left;
}
.ticks li {
  position: relative; padding-left: 1.6rem;
  font-family: var(--display); font-size: .93rem; color: var(--ink-2);
}
.ticks li::before {
  content: ""; position: absolute; left: 0; top: .28em;
  width: 1rem; height: 1rem; border-radius: 50%;
  background: var(--accent-wash) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%230B5FA5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/12px no-repeat;
}

@media (max-width: 700px) {
  .hero-centre { text-align: left; }
  .hero-centre .lede { margin-left: 0; }
  .eyebrow-chip { margin-bottom: .9rem; }
  .hero-form { flex-direction: column; gap: .45rem; padding: .45rem; }
  .hero-form .btn { width: 100%; }
  .ticks { display: flex; margin-top: 1.3rem; }
}

/* ================================================================ RESULTS ON A PHONE — a grid

   ⚠ The previous attempt gave every agent a card of five labelled rows, each carrying a second
   "area" figure. Thirteen agents made that 65 numbers and 65 question marks on one screen, with
   nothing dominant — a data dump, not a comparison. James was right to reject it.

   This is a compact grid instead: one line per agent, four short columns, abbreviated values
   (£204k, 254d). The overall figures appear ONCE, in the top row, rather than beside every figure.
   Same information, a fifth of the ink, and it can be scanned rather than read. */

.th-short, .v-short { display: none; }

@media (max-width: 700px) {
  .th-long, .v { display: none; }
  .th-short, .v-short { display: inline; }

  .tbl-wrap { overflow-x: visible; border: 1px solid var(--rule-firm); border-radius: var(--radius);
              background: var(--surface); }
  table.agents { display: table; width: 100%; min-width: 0; table-layout: fixed; }
  table.agents thead { display: table-header-group; }
  table.agents tbody { display: table-row-group; }
  table.agents tr { display: table-row; background: transparent; border: 0; margin: 0; padding: 0; }
  table.agents th, table.agents td { display: table-cell; }

  /* ⚠ FIVE numeric columns do not fit at 360: the name column was left ~70px and wrapped to four
     lines, colliding with its own figures. The least decision-relevant column is dropped instead
     of squeezing every column past legibility. */
  table.agents th:nth-child(6), table.agents td:nth-child(6) { display: none; }   /* Listings cut */

  table.agents th {
    padding: .55rem .15rem; text-align: right; vertical-align: bottom;
    border-bottom: 1px solid var(--rule-firm); white-space: nowrap; max-width: none;
  }
  table.agents th.left { text-align: left; }
  table.agents th:first-child { padding-left: .55rem; }
  .colsort { font-size: .6rem; letter-spacing: .05em; gap: .15rem; background: none; border: 0;
             padding: 0; box-shadow: none; }

  /* ⚠⚠ THE `?` MARKERS WERE COLLIDING THE HEADER INTO ITSELF. `table-layout: fixed` plus
     `white-space: nowrap` and no overflow rule means an over-wide header does not wrap or clip —
     it BLEEDS INTO ITS NEIGHBOUR, which is why SOLD and AVG were printed on top of each other with
     a stray marker between them. The markers were also 12px: below any usable tap target, so they
     were costing legibility and buying nothing. Removed from the compact header only; the same
     explanations remain on the card view's ranking control and in full on this site's methodology
     page. */
  table.agents thead .tip { display: none; }
  table.agents th > * { max-width: 100%; }

  table.agents td {
    padding: .58rem .15rem; text-align: right; vertical-align: middle;
    border-bottom: 1px solid var(--rule-soft); white-space: nowrap;
    font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: .84rem;
  }
  table.agents td:first-child { padding-left: .55rem; }
  table.agents td.left {
    text-align: left; font-family: var(--display); white-space: normal;
    line-height: 1.18; padding-right: .3rem;
  }

  table.agents th:nth-child(1), table.agents td:nth-child(1) { width: 26px; }
  table.agents th:nth-child(2), table.agents td:nth-child(2) { width: auto; }
  table.agents th:nth-child(3), table.agents td:nth-child(3) { width: 54px; }
  /* ⚠ MEASURED, not guessed: at 34px the SOLD label's ink was 34px wide and its cell was 34px, so
     with 2.4px of padding each side the label bled LEFT and abutted ASK exactly — ink gap 0px at
     360, 390 AND 430. That is the "ASKSOLD" collision, and it is not an overlap, which is why an
     overlap test passed on it. 40px gives the label its own cell plus a real gap; the name column
     is `auto` so it funds the 6px. */
  table.agents th:nth-child(4), table.agents td:nth-child(4) { width: 40px; }
  /* ⚠⚠ THE SAME COLLISION AS THE ONE ABOVE, ONE COLUMN ALONG, AND IT HID FOR MONTHS BECAUSE
     NOBODY COULD SEE THIS TABLE. MEASURED at 390px with scripts off: "VS AVG" inked 46px inside
     a 44px cell — slack −2px — so it bled LEFT and abutted AVG, rendering "AVGVS AVG". Its
     neighbour had 24px going spare, which is where the 12px comes from; the name column is
     unchanged at 120px.
     ⭐ Only reachable with JavaScript DISABLED (the cards replace this table otherwise), so it
     was invisible to every screenshot until the no-JS fallback was restored in `_base.html`.
     ⚠ Not an overlap — the cells never intersect — which is why a bounding-box overlap test
     passes on it. The quantity that catches it is INK slack, cell width minus text width. */
  table.agents th:nth-child(5), table.agents td:nth-child(5) { width: 38px; }
  table.agents th:nth-child(7), table.agents td:nth-child(7) { width: 56px; }

  .agent-name { font-size: .84rem; font-weight: 700; letter-spacing: -.02em; display: block; }
  /* ⚠ Hidden in the compact mobile TABLE, which is a grid of short values and has no room for a
     second line under each name. */
  .agent-sub { display: none; }
  /* ⚠⚠ BUT NOT IN THE CARDS. This rule hid the "from 10 of 13 sales" line on phones — the one
     thing neither competitor shows anywhere, missing on the device most sellers use, while
     desktop carried it on every row. MEASURED before this fix: 7 denominator lines visible on
     desktop, 0 on a phone, though all seven were in the page with display:none.
     ⭐ A card has room for it, and it is the reason to believe the percentage above it. */
  #cards .agent-sub { display: block; margin-top: .1rem; font-size: .72rem; }
  .pick input { width: 16px; height: 16px; }

  td.headline-cell .v-short { font-weight: 700; color: var(--ink); font-size: .9rem; }
  tr.is-benchmark td { background: var(--paper); border-bottom: 2px solid var(--ink); font-weight: 600; }
  tr.is-benchmark .agent-name { font-size: .72rem; font-weight: 700; color: var(--ink-2);
                                text-transform: uppercase; letter-spacing: .05em; }
  tr.is-selected td { background: var(--accent-wash); }
  .lowdata { font-size: .64rem; padding: .06em .26em; }
}

@media (max-width: 380px) {
  table.agents td, table.agents th { padding-left: .12rem; padding-right: .12rem; }
  table.agents td { font-size: .84rem; }
  .agent-name { font-size: .86rem; }
}

/* ⭐ A DIFFERENCE IS TWO THINGS — a magnitude and a direction — and only the magnitude belongs in
   the numeric voice. The figure keeps the mono/tabular treatment every other number on the page
   has; the direction word is set in the display face at reading weight, so the column still scans
   as a column of numbers while saying which way each one points.
   ⚠ Deliberately NO green/red. The page spends its one piece of emphasis on the headline figure,
   and a duration people already misread is the last place to add a value judgement in colour. */
/* ⚠ MEASURED: at 600 this out-weighted every other figure in the row — sold, average price and
   listings cut all render at 400, and 600 is reserved for the headline "% of asking". A second
   bold number in the row competes with the one the whole product is built on. `inherit` lets each
   surface set it: 400 in the table, 500 in a card's metric row, and the card's dominant slot keeps
   its own size. */
.rel-n { font-family: var(--mono); font-variant-numeric: tabular-nums; font-weight: inherit; }
.rel-w { font-family: var(--display); font-style: normal; font-weight: 400;
         color: var(--ink-3); font-size: .86em; }
table.agents td .rel-w { margin-left: .12rem; }
.acard-row .rel-n, .acard-big .rel-n { font-weight: 500; }
/* ⚠⚠ THE PHRASE IS THE CARD'S LABEL, SO IT SITS UNDER THE FIGURE LIKE EVERY OTHER COLUMN'S.
   The speed value nests `<i class="rel-w">` INSIDE `<b class="fig">`, so it laid out inline
   beside the number — and once it grew from "FASTER" to "FASTER THAN AVERAGE" (James, 2026-08-21)
   it took so much of a 390px card that the agent's name was squeezed into five lines.
   ⭐ Nothing else declared here: `.acard-big i` already sets the size, tracking, case and colour
   for a card's label, and this is one. A second set of values would be a second opinion about
   what a card label looks like. */
.acard-big .rel-w { display: block; margin-top: .1rem; }

/* the statement that replaced the agent-consent tick-box: it sits with the button, because the
   act of sending IS the agreement */
.submit-note {
  font-family: var(--display); font-size: .86rem; color: var(--ink-3);
  margin: .9rem 0 0; max-width: 52ch; line-height: 1.5;
}

.permissions { margin-top: 1.6rem; }

/* ⚠ Re-added 2026-08-18: this helper was collateral damage of a regex that removed the old hero
   block — it lived immediately after it in the file. With it gone the header button rendered BOTH
   its labels ("Compare agents" + "Compare") and pushed the masthead 47px past a 360px screen. */
.only-sm { display: none; }
@media (max-width: 700px) { .only-sm { display: inline; } .btn .hide-sm { display: none; } }

/* ================================================================ CARDS / TABLE — the visitor's choice

   Mobile only (James, 2026-08-18): cards by default because a phone card can make ONE number
   dominant; the compact grid stays one tap away for anyone who wants to scan columns. On desktop
   the table is the right form and the toggle does not exist. */

.viewbar { display: none; }
/* ⚠ SAME DEFAULT AS .viewbar, AND FOR THE SAME REASON. Without this the ranking control renders
   as a full-width bare select on desktop, where the table headers already do the sorting. I
   shipped exactly that defect for one build by styling .sortrow only inside the mobile query. */
.sortrow { display: none; }

/* ════════════════════ P1's components, ported 2026-08-21 ════════════════════
   Taken from the approved P1 design rather than reinvented. Class names and values are P1's.
   ⚠ The word that used to be here tripped `served_bytes_check`, correctly: nothing the
   site SERVES may call itself that, source comments included. */

/* ⭐ THE FUNNEL MASTHEAD IS THE MARK AND NOTHING ELSE — James, 2026-08-21: "'Where the data comes
   from' just looks sloppy at the top". Inside the funnel it is the only link there, the longest
   string on a phone header, and it points AWAY from the step the visitor is on. It survives in
   the footer of every page, so nothing is lost. A bare header is what every checkout does.
   ⚠ This supersedes the earlier `hide-sm` half-measure, which only hid it on mobile. */
.masthead--funnel nav { display: none; }

/* ⚠⚠ `.steps`/`.step` WERE ALREADY DEFINED IN THIS STYLESHEET AND RENDERED BY NO PAGE — the same
   class of dead authorship as the three undefined section classes, with the sign flipped: written,
   styled, and never put on screen. `.is-done` was the one piece missing. Used on the two THIN
   funnel pages only, where the complaint is that they look unfinished, and deliberately NOT on the
   comparison, where vertical space is the whole problem. */
.step.is-done { color: var(--ink-3); }

/* ⭐ THE JOURNEY PROGRESS BAR. Four segments of EQUAL width across the full page, directly under
   the masthead — James, 2026-08-21, replacing the worded rail.
   ⚠ `flex: 1 1 0` with `min-width: 0`, not `flex: 1`: the latter distributes FREE space, which
   leaves segments of different widths the moment anything has a different basis. Equal was the
   explicit requirement. */
.prog { display: flex; gap: 3px; height: 3px; width: 100%; background: transparent; }
.prog i { flex: 1 1 0; min-width: 0; background: var(--rule); display: block; }
.prog i.on { background: var(--accent); }

/* ⭐ THE AREA AVERAGE, OPT-IN. James, 2026-08-21: "a slick button that doesn't take up too much
   space". One line where a ~150px card used to be, and it deliberately does NOT sit beside the
   ranking select — he asked that the words in that dropdown keep fitting. */
.avgtoggle {
  display: inline-flex; align-items: center; gap: .45rem; cursor: pointer;
  font-family: var(--display); font-size: .82rem; font-weight: 600; color: var(--accent-ink);
  padding: .3rem 0; margin: 0 0 .7rem; letter-spacing: -0.005em;
}
.avgtoggle:hover { text-decoration: underline; text-underline-offset: 3px; }
.avgtoggle-ic {
  width: 14px; height: 14px; flex: none; border: 1.5px solid currentColor; border-radius: 50%;
  position: relative;
}
/* a chevron that turns, so the control says which way it goes without a second word */
.avgtoggle-ic::after {
  content: ""; position: absolute; left: 3.5px; top: 4px; width: 5px; height: 5px;
  border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg); transition: transform .18s ease;
}
.avgtoggle-on { display: none; }
.avgtoggle-input:checked ~ .avgtoggle .avgtoggle-on { display: inline; }
.avgtoggle-input:checked ~ .avgtoggle .avgtoggle-off { display: none; }
.avgtoggle-input:checked ~ .avgtoggle .avgtoggle-ic::after { transform: rotate(-135deg); top: 5px; }
.avgtoggle-input:focus-visible ~ .avgtoggle { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ---------------------------------------------------------------- the two disclosures
   ⭐ One row, each chevron on the OUTER edge — James's mock, 2026-08-24. On desktop the metrics
   control is absent and a lone flex child sits where the average toggle always did, so this adds
   no desktop change at all. */
/* ⚠ MEASURED: at 360px the pair overflowed the viewport by 18px — "Hide the LS10 average" beside
   "Metrics explained" is wider than the narrowest phone, and the OPEN state is the long one, so
   the overflow appears only after a tap. `min-width: 0` lets a label shrink below its text width
   so the words wrap inside their own column instead of pushing the page sideways; `flex-start`
   keeps the two aligned at the top when one of them takes two lines. */
.disclosures { display: flex; align-items: flex-start; justify-content: space-between; gap: .7rem; }
.disclosures .avgtoggle { margin-bottom: .7rem; min-width: 0; }
.disclosures .avgtoggle > span:not(.avgtoggle-ic) { min-width: 0; }
/* ⚠ The metrics control is MOBILE ONLY: the desktop table explains every metric on its own column
   hover, and two copies of a definition is the drift this project keeps paying for. */
.metricstoggle, .metricspanel { display: none; }
.metricstoggle-input:checked ~ .avgtoggle .avgtoggle-ic::after { transform: rotate(-135deg); top: 5px; }
.metricstoggle-input:focus-visible ~ .avgtoggle { outline: 2px solid var(--accent); outline-offset: 3px; }

@media (max-width: 700px) {
  .metricstoggle { display: inline-flex; }
  /* ⚠⚠ THE PANEL IS A SIBLING OF THE ROW, NOT OF ITS INPUT, so it cannot be revealed by a
     sibling combinator from the checkbox — `:has()` on the zone is how the average row already
     does exactly this, and it is already load-bearing on this page. */
  #reszone:has(.metricstoggle-input:checked) .metricspanel { display: block; }
  .metricspanel {
    background: var(--surface); border: 1px solid var(--rule-soft); border-radius: var(--radius);
    padding: .9rem 1rem; margin: 0 0 .9rem;
  }
  .metricsdl { margin: 0; }
  .metricsdl dt {
    font-family: var(--display); font-weight: 700; font-size: .82rem; color: var(--ink);
    margin: 0 0 .15rem;
  }
  /* ⚠ The asking-price metric is NAMED in lower case, because it reads as a phrase in the
     Rank-by control ("Rank by: of asking price achieved") and under the card's figure. As the
     first word of a definition list entry, beside "Homes sold" and "Average price", the same
     string reads as a typo. Capitalised HERE, in the one place that needs it — a second stored
     label for one metric is how the two versions start disagreeing. */
  .metricsdl dt::first-letter { text-transform: uppercase; }
  /* ⚠ `dd` carries a browser default indent of 40px, which here reads as a broken list. */
  .metricsdl dd { margin: 0 0 .85rem; color: var(--ink-2); font-size: .9rem; line-height: 1.45; }
  .metricsdl dd:last-child { margin-bottom: 0; }
}

/* ⚠ DESKTOP GAP — James, 2026-08-21. The two-up field rows took their height from the TALLEST
   cell, so a field whose note wrapped pushed a dead band under its partner. */
@media (min-width: 701px) {
  .field-row { align-items: start; }
  /* the closing statement is prose, not a form field — it reads across the whole column */
  .submit-note { max-width: none; }
}

/* the benchmark card is hidden until the control is opened */
#cards .acard.is-bench { display: none; }
/* ⚠ MOBILE ONLY — James, 2026-08-21. On desktop the benchmark is the first ROW of the table and
   is always on screen, so a control offering to reveal it would be offering something the reader
   is already looking at. */
@media (min-width: 701px) { .avgtoggle, .avgtoggle-input { display: none; } }
#reszone:has(.avgtoggle-input:checked) #cards .acard.is-bench { display: block; }

/* the property-type and bedroom tile grids — fixed columns so a five-item row cannot orphan one */
.chips--type { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.chips--beds { grid-template-columns: repeat(5, minmax(0, 1fr)); }
@media (max-width: 380px) { .chips--beds { gap: .35rem; } }

/* the comparison's heading row: the count sits beside the title instead of below it */
/* ⭐ THE HEADING BAND on the comparison and the request page. This was an INLINE
   `style="padding:1.8rem 0 1.2rem"` on both, which is why three attempts to tighten it from the
   stylesheet failed — inline wins against any rule without !important, and reaching for
   !important is what made the two rules fight each other. A class can be responsive; an inline
   style cannot. James asked for this gap twice; it was the same cause both times. */
.band-head { padding: 1.5rem 0 1rem; }
.band-results { padding: 1.4rem 0 2rem; }
@media (max-width: 700px) {
  .band-head { padding: .45rem 0 .6rem; }
  .band-results { padding: .5rem 0 2rem; }
}

.reshead { display: flex; align-items: baseline; justify-content: space-between;
           gap: 1rem; flex-wrap: wrap; }
.reshead h1 { font-size: clamp(1.25rem, 2.2vw, 1.6rem); max-width: 28ch; margin: 0;
              letter-spacing: -0.02em; }
.reshead .resmeta { font-family: var(--display); font-size: .82rem; color: var(--ink-3); }
/* ⚠ hidden on a phone ON PURPOSE — every pixel above the first agent card is the thing being
   complained about, and the count is repeated in the benchmark row two lines below. */
@media (max-width: 700px) { .reshead .resmeta { display: none; } }

table.agents th.headline-th { background: var(--paper); }
.btn-block-sm { width: 100%; }
.section--surface { background: var(--surface);
                    border-top: 1px solid var(--rule-soft);
                    border-bottom: 1px solid var(--rule-soft); }
#cards { display: none; }

@media (max-width: 700px) {
  .viewbar {
    display: flex; align-items: center; justify-content: space-between;
    gap: .8rem; margin-bottom: .8rem;
  }
  .viewtoggle {
    display: inline-flex; background: var(--surface);
    border: 1px solid var(--rule-firm); border-radius: 100px; padding: 3px;
  }
  .viewtoggle button {
    font-family: var(--display); font-size: .82rem; font-weight: 700;
    border: 0; background: transparent; color: var(--ink-3);
    padding: .42rem 1rem; border-radius: 100px; cursor: pointer;
  }
  .viewtoggle button.is-active { background: var(--ink); color: #fff; }
  /* ⚠⚠ THE OLD CONTROL TRUNCATED ITS OWN LABEL. A 200px cap at .84rem showed
     "% of asking achie" — James, 2026-08-21: "the sorter isn't working - sloppy". It sorted
     perfectly; it just could not say what it was sorting by. And at 13.44px it sat under the
     16px threshold that makes iOS Safari ZOOM the viewport on focus, so tapping it lurched the
     page. Both are fixed by giving it the width it needs and a 1rem font. */
  /* ⚠⚠ IT WRAPS, BECAUSE THE SAME CONTROL LIVES IN TWO CONTAINERS OF DIFFERENT WIDTHS. The fix
     above gave it "the width it needs" inside journey A's `.wrap`; journey B's ladder is narrower,
     so at 390px the select measured 227px against A's 264px and "of asking price achieved" clipped
     to "of asking price achi..." — the SAME complaint James made on 2026-08-21, in a container
     that did not exist then.
     ⭐ Wrapping fixes it at every width instead of at one: when the select cannot sit beside its
     label it takes the next line whole, and nothing has to know how wide its parent is. */
  .sortrow { display: flex; flex-wrap: wrap; align-items: center; gap: .55rem; margin: .7rem 0 .6rem; }
  .sortrow-label {
    font-family: var(--display); font-size: .84rem; font-weight: 600; color: var(--ink-3);
    white-space: nowrap; margin: 0;
  }
  .sortsel { position: relative; flex: 1 1 15rem; min-width: 0; }
  .sortsel select {
    width: 100%; font-family: var(--display); font-size: 1rem; font-weight: 500;
    color: var(--ink); background: var(--surface);
    border: 1px solid var(--rule-firm); border-radius: var(--radius);
    padding: .55rem .7rem; min-height: 44px; text-overflow: ellipsis;
  }

  /* ⚠⚠⚠ FORCE CARDS ON MOBILE, WHATEVER localStorage SAYS. `app.js` restores `state.view` from a
     previous visit, so a returning visitor who had chosen Table before the toggle was removed
     would land in a table with no control to escape it — the second-visit trap this project has
     already been bitten by. These two rules make the stored value unable to matter here. */
  #reszone[data-view="table"] #cards { display: flex; flex-direction: column; gap: .65rem; }
  #reszone[data-view="table"] .tbl-wrap { display: none; }
  /* ⚠ THE SPACE ABOVE THE FIRST AGENT CARD IS THE COMPLAINT — James, 2026-08-21. The heading
     band and the results band each carried their own generous padding, so ~4.4rem of nothing sat
     between the masthead and the first thing a visitor came for. */
  /* ⚠ James, 2026-08-21: "still too much space here". The heading band carried 1.8rem of
     top padding designed for a page that opened with a masthead and nothing else; the progress
     bar now sits directly above it and supplies its own separation. */
  .reshead { margin-bottom: .2rem; }
  /* ⚠ The heading band's bottom padding and the form band's top padding stacked, so the white
     card sat ~45px below the title on a phone. Neither needed a full band's worth. */
  /* ⚠ The heading band's bottom padding and the form band's top padding stacked under the
     title. Neither needs a full band's worth on a phone — the card's own border already
     separates it. The card keeps its INTERNAL padding; that is not gap, it is the card. */
  .section, .section-tight { padding-top: .5rem; }
  /* ⚠ James, 2026-08-21, twice: the space under the progress bar. The bar now supplies the
     separation the section padding used to, so the padding is nearly all redundant. */
  /* ⚠⚠ THREE COMPETING RULES STOOD HERE AND THE LAST ONE UNDID THE OTHER TWO. It set
     `padding-top: .75rem !important` on BOTH `.reshead` and the section — 12px each, which is
     exactly the 24px gap I was trying to remove. I spent two passes fighting a rule I had
     written myself. One rule now, and no !important to hide behind. */
  #reszone { padding-top: 0; }
  .reshead { padding-top: 0; }
  #reszone[data-view="table"] .sortrow { display: flex; }
  /* the desktop table does not exist on a phone at all — P1 */
  .tbl-wrap--agents { display: none; }
  /* ⚠⚠⚠ UNLESS THE SCRIPT NEVER GOT AS FAR AS SAYING WHICH SURFACE IS ON. Every rule that shows
     something at this width keys off `#reszone[data-view=...]`, which `app.js` sets during init,
     and the cards are BUILT by that same script. So an init that throws leaves the table hidden
     by the rule above and the cards hidden by never matching — a phone showing no agents at all,
     with the funnel dead and nothing on screen to say so. MEASURED on the live site 2026-08-24.
     ⭐ The fallback is the TABLE, not the cards: it is server-rendered and already on the page,
     whereas the cards do not exist until the script that just died creates them. This is the
     same trade the `<noscript>` rule in _base.html already makes for scripts-off, extended to
     the case the noscript branch cannot see — scripts ON and failing. */
  #reszone:not([data-view]) .tbl-wrap--agents { display: block; }

  /* which of the two is on screen is one attribute on the zone */
  #reszone[data-view="cards"] .tbl-wrap { display: none; }
  #reszone[data-view="cards"] #cards { display: flex; flex-direction: column; gap: .65rem; }
  /* ⚠⚠⚠ `#reszone[data-view="table"] #cards { display: none }` STOOD HERE AND CANCELLED THE
     FORCE-CARDS-ON-MOBILE GUARD 30 LINES ABOVE IT — same selector, same specificity, later in the
     file, so it won. That guard's own comment says it exists so a stored `view: "table"` "cannot
     matter here", and this line made it matter: with the table also hidden at this width, a
     returning visitor whose stored view was Table got a comparison page with NO AGENTS ON IT.
     MEASURED 2026-08-24 — zero pickable elements at 430px, while 1440px was fine.
     ⭐ It is a leftover from when the toggle existed on mobile. P1 removed that toggle ("the
     desktop table does not exist on a phone at all"), so nothing here should ever hide the cards:
     they are the only surface a phone has. Deleted rather than reordered — two rules disagreeing
     about one selector is the defect, and winning the argument by position keeps it live. */
  #reszone[data-view="table"] .cardsort-wrap { visibility: hidden; }  /* headers sort the table */

  /* ---------------------------------------------------------------- the agent card */
  .acard {
    position: relative; display: flex; align-items: flex-start; gap: .75rem;
    background: var(--surface); border: 1px solid var(--rule-firm);
    border-radius: 8px; padding: .95rem 1rem;
    box-shadow: 0 1px 2px rgba(13,27,61,.05), 0 6px 16px -12px rgba(13,27,61,.25);
    cursor: pointer;
  }
  .acard input { position: absolute; opacity: 0; pointer-events: none; }
  .acard-tick {
    flex: none; width: 22px; height: 22px; margin-top: .15rem;
    /* ⚠ ink-3, not the hairline grey: an invisible empty checkbox is an invisible affordance,
       and "tap to select" is the card's whole job */
    border: 2px solid var(--ink-3); border-radius: 6px; background: var(--surface);
    display: grid; place-items: center;
  }
  .acard input:checked ~ .acard-tick {
    background: var(--accent); border-color: var(--accent);
  }
  .acard input:checked ~ .acard-tick::before {
    content: ""; width: 12px; height: 12px;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.5 3.5L13 4.5' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat;
  }
  .acard input:disabled ~ .acard-tick { opacity: .35; }
  .acard input:focus-visible ~ .acard-tick { outline: 3px solid var(--accent); outline-offset: 2px; }
  .acard.is-picked { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); background: var(--accent-wash); }

  .acard-body { flex: 1 1 auto; min-width: 0; display: block; }
  .acard-head { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; }
  .acard-name {
    font-family: var(--display); font-weight: 700; font-size: 1rem; letter-spacing: -.02em;
    color: var(--ink); line-height: 1.2;
  }
  /* ⚠⚠ IT MAY SHRINK, AND `flex: none` MEANT IT COULD NOT. MEASURED at 360px the moment the
     metric was renamed "of asking price achieved" (James, 2026-08-24): the caption is set in
     uppercase with positive tracking, could not wrap, and pushed the card 18px past the viewport
     — a page that scrolls sideways on the narrowest phone, caused by four extra characters.
     ⭐ The caption is now a block, so it wraps under its own figure and stays right-aligned; the
     agent name beside it takes the room back. A label long enough to wrap is a wrapping label,
     not a wider page. */
  /* ⚠ AND IT NEEDS A FLOOR AS WELL AS A CEILING. With shrinking allowed and no minimum, a long
     agency name ("BAX Property, Covering North Leeds") squeezed the caption into THREE lines —
     OF ASKING / PRICE / ACHIEVED. The floor is ~two lines of the caption; the NAME wraps first,
     which is what a name is expected to do. */
  .acard-big { flex: 0 1 auto; min-width: 7.6em; text-align: right; display: block; }
  .acard-big i { display: block; text-wrap: balance; }
  .acard-big .fig { font-size: 1.65rem; font-weight: 600; letter-spacing: -.04em; display: block; line-height: 1; }
  .acard-big i {
    font-style: normal; font-family: var(--display); font-size: .64rem; font-weight: 600;
    letter-spacing: .05em; text-transform: uppercase; color: var(--ink-3);
  }
  /* ⚠ FOUR metrics now, not three — the card carries every column the table does. A single flex
     row of four wraps unpredictably at 360 and lands 3+1, which reads as a mistake. A fixed 2x2
     grid is the same information, always the same shape, and each cell gets a whole line so the
     longest label ("listings cut") never truncates. */
  .acard-row {
    display: grid; grid-template-columns: 1fr 1fr; gap: .3rem 1.1rem;
    margin-top: .55rem; padding-top: .55rem;
    border-top: 1px solid var(--rule-soft);
    font-family: var(--display); font-size: .8rem; color: var(--ink-3);
  }
  .acard-row b { color: var(--ink-2); font-family: var(--mono); font-weight: 500; font-size: .84rem; }

  /* the benchmark strip: a reference, visually not a competitor */
  .acard.is-bench {
    background: var(--ink); border-color: var(--ink); color: #C3D2DC;
    display: block; cursor: default;
  }
  .acard-benchlabel {
    margin: 0 0 .45rem; font-family: var(--display); font-size: .68rem; font-weight: 700;
    letter-spacing: .08em; text-transform: uppercase; color: #8FA6B5;
  }
  /* the benchmark takes the cards' own shape, in the cards' own class names, so the two cannot
     drift apart — only the colours differ */
  .acard.is-bench .acard-name { color: #fff; font-size: .74rem; text-transform: uppercase;
                                letter-spacing: .06em; font-weight: 700; }
  .acard.is-bench .acard-big .fig { color: #fff; }
  .acard.is-bench .acard-big i { color: #8FA6B8; }
  .acard.is-bench .acard-row { border-top-color: rgba(255,255,255,.16); color: #8FA6B8; }
  .acard.is-bench .acard-row b { color: #fff; }
  .acard-benchrow {
    display: flex; justify-content: space-between; gap: .6rem; flex-wrap: wrap;
    font-family: var(--display); font-size: .78rem;
  }
  .acard-benchrow b { color: #fff; font-family: var(--mono); font-weight: 500; }
  .acard-benchrow .fig { font-size: 1rem; }
}

/* ================================================================ RESTORED 2026-08-18

   ⚠⚠ A regex block-deletion earlier in this session swallowed three things that live BELOW the
   block it was aiming at: the --rule-firm token (used six times, so those borders were rendering
   as none), the whole .tipbox rule (so every "?" explanation on the site rendered unstyled at the
   bottom of the document — dead), and the desktop .colsort/.tip base styles (so sortable headings
   rendered as native 2px-outset buttons at 1440). I fixed ONE symptom of that deletion (.only-sm)
   and never checked what else went with it. Delete CSS by naming the rules, not by matching a
   range. */

/* the hover note — a body-level element, so no scrolling ancestor can clip it */
.tipbox {
  position: fixed; z-index: 200; display: none;
  max-width: min(300px, calc(100vw - 24px));
  background: var(--ink); color: #fff;
  padding: .7rem .8rem; border-radius: var(--radius);
  font-family: var(--display); font-size: .82rem; font-weight: 400; line-height: 1.45;
  letter-spacing: 0; text-transform: none; text-align: left;
  box-shadow: 0 10px 30px rgba(13,27,61,.35);
  pointer-events: none;
}

/* sortable column headings — flat, not native buttons */
.colsort {
  font-family: var(--display); font-size: .68rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-3);
  background: none; border: 0; padding: 0; cursor: pointer; text-align: inherit;
  appearance: none; -webkit-appearance: none;
  display: inline-flex; align-items: baseline; gap: .3rem;
}
.colsort:hover, .colsort.is-active { color: var(--ink); }
.colsort-arrow { font-style: normal; font-size: .8em; }
th:not(.left) .colsort { flex-direction: row-reverse; }

.tip {
  display: inline-grid; place-items: center; width: 15px; height: 15px; margin-left: .35rem;
  border: 1px solid var(--rule-firm); border-radius: 50%;
  font-family: var(--display); font-size: .62rem; font-weight: 700; color: var(--ink-3);
  cursor: help; vertical-align: middle; background: var(--surface);
}
.tip:hover, .tip:focus-visible { border-color: var(--accent); color: var(--accent); }

/* the sticky masthead was 92% opaque, so page content ghosted through it while scrolling */
.masthead { background: var(--paper); }

/* ⚠⚠ HISTORY, KEPT BECAUSE IT EXPLAINS WHY A STACKING ORDER IS STILL DECLARED HERE. The selection
   bar and the consent banner were once BOTH anchored to the bottom of the viewport, so a z-index
   alone only decided which of them was unusable: at `.cookie { z-index: 60 }` the bar covered
   consent (desktop, "Necessary only" unreachable; mobile 390px, BOTH consent buttons unreachable,
   blocked by `.selbar-text`), and raising the banner instead buried the "Request valuations" CTA.
   It was settled by separating them in the Y axis with a `--selbar-h` clearance.
   ⭐ THE CENTRED OVERLAY RETIRED THAT MECHANISM (see the banner block above). The clearance, its
   deliberately generous fail-safe default, and the app.js measurement that tightened it were all
   fixes for a collision a centred panel cannot have.
   ⚠⚠ WHAT IS NOT RETIRED, AND IS NOW LOAD-BEARING RATHER THAN INERT: the stacking order. The
   overlay covers the whole viewport by design, so it OVERLAPS the bar at every width - which is
   precisely the condition under which z-index decides reachability. It was an equivalent mutant
   while the two were separated; it is a real one now, and `compliance_check` carries the case. */
.selbar { z-index: 70; }

/* ⚠⚠ AND THE BAR MUST ACTUALLY BE AT THE BOTTOM FOR THAT OFFSET TO MEAN ANYTHING. `position:
   sticky; bottom: 0` only pins while there is content below it — on a SHORT page the bar settles
   into the flow partway up, while the banner stays fixed to the viewport and lands on top of the
   CTA. MEASURED on /agents?postcode=M1 at 390x844 after the ranking floor rose to 10 and the page
   got shorter: selbar at 557-685, cookie at 426-696, "Request valuations" unreachable.
   ⭐ Found by the C3b check written this morning for the FIRST version of this collision — it
   fired on a page whose content had changed, which is the point of checking a class rather than
   the three instances someone happened to see.
   ⚠ Only while consent is OUTSTANDING. Once answered the banner is gone and sticky is the right
   behaviour again — the bar settles at the end of the list instead of hovering over it. */
body.has-selbar.consent-pending .selbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}

@media (max-width: 700px) {
  .cardsort-wrap { display: flex; align-items: center; gap: .4rem; }
  .cardsort-wrap select { flex: 1 1 auto; min-width: 0; }
  .cardsort-wrap .tip { flex: none; }
}

/* an invalid field must LOOK invalid — aria-invalid alone is invisible to a sighted user */
input[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--flag); box-shadow: 0 0 0 3px var(--flag-wash);
}

/* ⭐ THE ACCEPT IS THE SITE'S PRIMARY BUTTON; THE REFUSAL IS A FULL-WEIGHT OUTLINED ONE BESIDE
   IT. James, 2026-08-26, after the launch-day measurement: 616 sessions arrived on a Meta click,
   3 answered the banner and ZERO accepted, so the neutral-on-neutral pair went. ⚠ Read that as a
   reason to try, NOT as a diagnosis - 0 of 3 is what a healthy 50% accept rate looks like one time
   in eight, and the volume is what actually binds.

   ⚠⚠ WHAT KEEPS THIS COMPLIANT IS NOT THE COLOUR, IT IS THE COST OF REFUSING: same layer, same
   size, ONE click, ink-on-surface with a solid ink border - never a greyed ghost, never a buried
   link. PECR / checklist C3 asks that refusing be no HARDER than accepting, and `compliance_check`
   C3 and C3b assert precisely that (visible AND reachable, by a hit test at each button's own
   centre). Those are the clauses to keep green if this rule is ever touched again; the fill is
   not what they measure, which is exactly why the fill was safe to change and the reachability
   is not.

   ⚠ #1E6BFF on white measures 4.56:1 - over the 4.5:1 AA floor for this button's 700-weight
   .82rem label, but only just. Do not lighten the accent or thin the label without re-measuring. */
.cookie .btn-ghost {
  background: var(--surface); color: var(--ink); border: 1px solid var(--ink);
}
.cookie .btn-ghost:hover { background: var(--paper); color: var(--ink); }

/* ================================================================ DESKTOP PARITY

   ⚠⚠ Three of the desktop review's findings had ONE cause: the emphasis styles that give the
   table its hierarchy were written INSIDE the <=700px block, so at 1440 the headline figure had
   exactly the same weight as five other numbers and the benchmark row was indistinguishable from
   a fourteenth agent called "All agents near you". I styled the phone and left the desktop
   spreadsheet-flat. These rules are unscoped on purpose. */

/* the number the whole product is built on should dominate at every width */
table.agents td.headline-cell { background: var(--paper); }
table.agents td.headline-cell .v {
  font-size: 1.12rem; font-weight: 600; color: var(--ink);
}
table.agents th:nth-child(3) { background: var(--paper); }

/* the benchmark is a reference, not a competitor: it must never read as a selectable row */
tr.is-benchmark td {
  background: var(--paper-2); border-bottom: 2px solid var(--ink);
  font-family: var(--mono); color: var(--ink-2);
}
tr.is-benchmark td.headline-cell { background: var(--paper-2); }
tr.is-benchmark .agent-name {
  font-family: var(--display); font-size: .74rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--ink-2);
}
table.agents tbody tr.is-benchmark:hover td { background: var(--paper-2); }

/* thirteen agents do not fit in 900px, so more than half the table was read with no column
   labels at all — the sticky budget was being spent on a control bar that has now gone */
@media (min-width: 701px) {
  table.agents thead th {
    position: sticky; top: 62px; z-index: 5;
    background: var(--surface);
  }
  table.agents thead th:nth-child(3) { background: var(--paper); }
}

.pick-hint {
  font-family: var(--display); font-size: .9rem; color: var(--ink-2);
  margin: .5rem 0 0; max-width: 52ch;
}

/* the landing hero was a 640px column stranded in 1440 with ~420px of empty gutter each side */
@media (min-width: 1100px) {
  .hero-centre { max-width: 760px; }
  .hero-centre h1 { font-size: clamp(2.4rem, 3.4vw, 3.4rem); }
  .hero-centre .lede { max-width: 46ch; }
}

/* ⚠ `overflow-x: auto` on the wrapper makes it a scroll container, so `position: sticky` on the
   header sticks to the WRAPPER (which scrolls away with the page) instead of the viewport. The
   same property clipped the tooltip earlier — one declaration, two defects. The table fits at
   desktop since the filter bar and a column were removed, so the wrapper does not need to scroll. */
@media (min-width: 1024px) {
  .tbl-wrap { overflow: visible; }
}

/* ⚠⚠ AND THE BAND WHERE IT DOES NOT FIT. The rule above used to start at 701px on the premise
   that "the table fits at desktop" — true at 1024 and up, false below it. `table.agents` carries
   `min-width: 900px`, so between 701 and 1023 a 959px table sat in a 756px wrapper with overflow
   VISIBLE and pushed the whole page sideways. MEASURED before this fix: the body overflowed by
   262px at 720, 182 at 800, 82 at 900 and 22 at 960 — every width a tablet or a small laptop uses.
   ⭐ This project's own rule is that wide content scrolls inside its own container and the page
   body never scrolls horizontally. Restoring `auto` in that band costs the sticky header its
   viewport anchoring there, which is a smaller defect than a page that slides under the reader. */
@media (min-width: 701px) and (max-width: 1023px) {
  .tbl-wrap { overflow-x: auto; overflow-y: visible; }
}

/* the benchmark counts sales the ranked rows do not, and a reader who adds the column up finds a
   16% gap. Stated plainly, in the flow, rather than as a footnote nobody scrolls to. */
.reconcile {
  margin-top: 1rem; padding: .85rem 1rem;
  background: var(--surface); border-left: 3px solid var(--rule-firm);
  font-size: .88rem; line-height: 1.55; color: var(--ink-2);
}
.reconcile b { color: var(--ink); font-weight: 600; }
.reconcile[hidden] { display: none; }

/* ⚠ THE ONE PIECE OF RED ON THE SITE, and it earns it: this is the only text that tells a visitor
   why the form they just sent came back. It uses --flag, the warning orange already defined for
   low-data marks, rather than a new colour — and it is stated as text, never as colour alone,
   because a red border a colourblind visitor cannot see is the same as no message. */
.field-error {
  margin: .35rem 0 0;
  font-family: var(--display); font-size: .82rem; font-weight: 600;
  color: var(--flag);
}
input[aria-invalid="true"], select[aria-invalid="true"] {
  border-color: var(--flag);
  background: var(--flag-wash);
}

/* ---------------------------------------------------------------- the landing page's worked example

   ⚠ Deliberately QUIETER than the real comparison. It is evidence that the product exists, not the
   product — a full-weight table here would compete with the postcode box, which is the only thing
   this page needs anyone to do. Four rows, three columns, no tick boxes. */
.lede-promise { margin-top: -.4rem; color: var(--ink-2); }
.lede-promise b { color: var(--ink); }

.hero-example { margin: 1.6rem auto 0; max-width: 620px; text-align: left; }
.hero-example-lede,
.hero-example-note {
  font-family: var(--display); font-size: .84rem; color: var(--ink-3); margin: 0 0 .5rem;
}
.hero-example-note { margin: .5rem 0 0; }
/* ⚠⚠ THE EXAMPLE TABLE IS A DIFFERENT TABLE FROM THE COMPARISON AND MUST BE SIZED LIKE ONE.
   `.agents-example` alone loses the cascade to `table.agents` (0-0-1-0 against 0-0-1-1), so the
   900px min-width still applied and the hero rendered FOUR AGENT NAMES AND NO FIGURES, with the
   '% of asking achieved' header cut off at the edge - on the one screen those figures exist to
   prove. Every text check passed throughout, because the numbers were in the DOM the whole time.
   ⭐ It took a screenshot. `table-layout: fixed` then holds the three columns at readable
   proportions instead of letting the denominator line hog 242px of a 322px phone. */
table.agents.agents-example {
  min-width: 0; width: 100%; font-size: .88rem; table-layout: fixed;
}
table.agents.agents-example th:nth-child(1), table.agents.agents-example td:nth-child(1) { width: 46%; }
table.agents.agents-example th:nth-child(2), table.agents.agents-example td:nth-child(2) { width: 32%; }
table.agents.agents-example th:nth-child(3), table.agents.agents-example td:nth-child(3) { width: 22%; }
.agents-example th { padding: .6rem .7rem .45rem; }
/* ⚠ THE BASE HEADERS ARE `white-space: nowrap`, which is right for the full comparison
   inside its own scrolling wrapper and wrong here. This example is three columns on a
   landing page and has to FIT a phone: nowrap made its natural width 344px inside a 322px
   column, so it scrolled sideways inside the hero AND pushed the document 3px past the
   viewport. MEASURED at 360px, caught by `shots` rather than by eye.
   ⚠ The selector needs `table.agents.agents-example` to outrank `table.agents th` and the
   mobile block's own `.agents th` - MEASURED: at plain `.agents-example th` the computed
   value stayed `nowrap` and the fix did nothing. */
table.agents.agents-example th { white-space: normal; }
.agents-example td { padding: .55rem .7rem; }
/* ⚠ The denominator line under each percentage is the whole point of the table and is kept here
   too — it is the one thing neither competitor shows anywhere. */
/* ⚠⚠ AND IT MUST SURVIVE THE PHONE. The mobile block hides `.agent-sub` because the compact
   seven-column table has no room for a second line - but this example has THREE columns and
   exists precisely to show the evidence under each percentage. Without this the landing page
   showed a bare '101.5%' on a phone, which is the competitors' unsourced-average trick, on
   the device most sellers use. Specificity beats the mobile rule outright (0-0-3-1). */
table.agents.agents-example .agent-sub { display: block; font-size: .72rem; margin-top: .1rem; }

/* ⚠ Sits UNDER the label and above the box, so it is read before the field is tapped rather than
   after it has been filled in wrongly. */
.field-note {
  margin: .2rem 0 .4rem;
  font-family: var(--display); font-size: .8rem; color: var(--ink-3);
}
.field-note b { color: var(--ink-2); }

/* ⚠ The list of addresses at the seller's postcode. Full width and the same height as the
   text inputs beside it, so the form reads as one column rather than a control bolted on. */
.addr-select {
  width: 100%; padding: .7rem .75rem; font-family: var(--display); font-size: 1rem;
  color: var(--ink); background: var(--surface); border: 1px solid var(--rule-firm);
  border-radius: var(--radius); margin-bottom: .5rem;
}
.addr-select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ⚠ ADDRESS SUGGESTIONS. Anchored to the field rather than floated in the page, so it cannot
   drift on a phone when the keyboard opens and the viewport shrinks under it. */
.field { position: relative; }
.addr-list {
  position: absolute; z-index: 40; left: 0; right: 0; margin: .2rem 0 0; padding: .25rem;
  list-style: none; background: var(--surface); border: 1px solid var(--rule-firm);
  border-radius: var(--radius); box-shadow: var(--shadow-md);
  max-height: 15rem; overflow-y: auto;
}
.addr-opt {
  padding: .55rem .6rem; border-radius: calc(var(--radius) - 2px); cursor: pointer;
  font-family: var(--display); font-size: .95rem; line-height: 1.35;
}
/* ⚠ Hover AND keyboard share one appearance: two different "this one" styles is how a keyboard
   user loses track of which row Enter will take. */
.addr-opt:hover, .addr-opt.is-active { background: var(--accent-wash); color: var(--ink); }

/* ==============================================================================================
   JOURNEY B — THE LADDER
   Twelve one-question screens. Added 2026-09-04.

   ⚠ SCOPED UNDER `.ladder-page`, IN THIS FILE, NOT A SECOND STYLESHEET. `app.py` content-hashes
   exactly three asset names for cache-busting; a fourth file would ship as `?v=missing` and be
   cached by whatever a visitor already had. The existing pattern for page-scoped styling is a
   `{% block bodyclass %}` and rules in here — `results.html` and `leads.html` both do it.

   ⭐ THE DESIGN BRIEF IN ONE LINE: one question per screen, one thumb, nothing else competing.
   Every measurement below is a phone measurement first; the desktop rules only stop it looking
   stranded on a wide viewport.
   ============================================================================================== */

.ladder-page main { display: block; }

.ladder {
  max-width: 34rem;
  margin: 0 auto;
  padding: 1.25rem 1.15rem 3rem;
}

/* ---- the phase rail. Three named phases rather than "step 9 of 13" — see ladder/steps.py. */
.lad-rail { display: flex; gap: .5rem; margin: .25rem 0 1.6rem; }
.lad-phase { flex: 1 1 0; min-width: 0; }
.lad-phase-name {
  display: block; font-family: var(--display);
  font-size: .68rem; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: .3rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lad-phase.now .lad-phase-name,
.lad-phase.done .lad-phase-name { color: var(--ink); }
.lad-phase-bar { display: block; height: 4px; background: var(--rule-soft); border-radius: 2px; }
.lad-phase-bar i { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
.lad-phase.done .lad-phase-bar i { background: var(--ink); }

/* ---- back. Present on every step but the first; a plain link, so it needs no script. */
.lad-back {
  display: inline-block; font-family: var(--display); font-size: .85rem; font-weight: 600;
  color: var(--ink-2); text-decoration: none; margin-bottom: .9rem;
}
.lad-back:hover { color: var(--accent); text-decoration: underline; }

/* ---- the question. The largest thing on the screen, by a distance. */
.lad-q {
  font-family: var(--display); font-weight: 800; letter-spacing: -0.02em;
  font-size: clamp(1.5rem, 6vw, 2rem); line-height: 1.15;
  color: var(--ink); margin: 0 0 .5rem;
}
.lad-sub { color: var(--ink-2); margin: 0 0 1.4rem; max-width: 30rem; }

.lad-form { display: flex; flex-direction: column; gap: .85rem; margin: 0; }
/* ⚠ Only when the form follows the heading DIRECTLY. Removing the redundant field labels left the
   input sitting hard against the question; a blanket top margin would then double up on the
   screens that do carry a sub-line, which already spaces itself. An adjacent-sibling selector
   spaces exactly the case that needs it and cannot cancel the other. */
.lad-q + .lad-form { margin-top: 1.1rem; }
.lad-label {
  font-family: var(--display); font-size: .8rem; font-weight: 700;
  letter-spacing: .03em; text-transform: uppercase; color: var(--ink-2);
}
.lad-input, .lad-select {
  font: inherit; width: 100%; padding: .9rem 1rem;
  border: 1px solid var(--rule-firm); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
}
.lad-input:focus, .lad-select:focus {
  outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent);
}
.lad-select { padding: .5rem; }
.lad-cta { width: 100%; padding: 1rem 1.4rem; font-size: 1rem; }
.lad-error { color: var(--flag); background: var(--flag-wash); padding: .6rem .8rem;
             border-radius: var(--radius); margin: 0; }
.lad-fine { font-size: .82rem; color: var(--ink-3); margin: .2rem 0 0; }
.lad-trust {
  margin: 2rem 0 0; font-size: .78rem; color: var(--ink-3); text-align: center;
}

/* ---- the front door's own content, on step 1 only: artwork, the three pills, the fee
   disclosure. ⚠ IN THIS BLOCK, NOT AT THE TOP OF THE FILE, and not in a stylesheet of its own:
   `app.py` content-hashes exactly three asset names, so a fourth file ships as `?v=missing` and
   is served from whatever the visitor already had.
   ⚠ `.hero-art` and `.hero-pills` are re-used unscoped from the landing page and need no rules
   here -- `.ladder` already caps the width they were sized against. Only the spacing under the
   ladder's own control is new. */
.ladder .hero-art { margin: 2rem 0 0; }
/* ⚠ STACKED AT EVERY WIDTH INSIDE THE LADDER, and that is the fix rather than the compromise.
   The landing page fits all three on one line because its column is wider; the ladder's 34rem
   column fits two, so the third dropped underneath and centred itself -- which reads as a wrap
   that went wrong rather than as a list. MEASURED at 1440: three pills, two lines, one orphan.
   One per line reads as three claims; it is also exactly what the narrow screen already did, so
   the screen now says the same thing the same way at every width. */
.ladder .hero-pills {
  margin: 1.2rem 0 0; flex-direction: column; align-items: stretch;
}
/* ⚠ The commercial relationship, in the ladder's own type scale rather than `.prose`'s. It is
   the smallest text on the screen and that is deliberate -- it is a disclosure, not a pitch --
   but it is not `.lad-fine` small, because a fee disclosure nobody can read is not a disclosure. */
.lad-disclosure {
  margin: 1.6rem 0 0; font-size: .84rem; line-height: 1.55; color: var(--ink-3);
  max-width: 30rem;
}

/* ---- tap-to-advance options. ⚠ EACH ONE IS A SUBMIT BUTTON, which is what makes "press it and
   it moves on" work with JavaScript blocked. They must therefore look like options, not buttons. */
.lad-choices { gap: .6rem; }
.lad-choice {
  font: inherit; text-align: left; cursor: pointer;
  padding: 1rem 1.1rem; min-height: 3.4rem;
  border: 1px solid var(--rule-firm); border-radius: var(--radius);
  background: var(--surface); color: var(--ink);
  transition: border-color .12s ease, background .12s ease;
}
.lad-choice:hover { border-color: var(--accent); background: var(--accent-wash); }
.lad-choice:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* ⚠ `chosen` marks an answer already given, shown when a visitor comes BACK to a step. It is not
   a default: nothing on this journey is pre-selected except the value slider. */
.lad-choice.chosen { border-color: var(--accent); border-width: 2px; background: var(--accent-wash); }

/* ---- the value slider. The one screen carrying a default. */
.lad-value {
  display: block; text-align: center; font-family: var(--display);
  font-size: clamp(2rem, 9vw, 2.8rem); font-weight: 800; letter-spacing: -0.03em;
  color: var(--ink); margin: .5rem 0 .2rem;
}
.lad-slider { width: 100%; margin: .6rem 0 .2rem; accent-color: var(--accent); height: 2rem; }
.lad-anchor-note { text-align: center; color: var(--ink-3); font-size: .85rem; margin: 0 0 .6rem; }
.lad-skip {
  font: inherit; font-size: .88rem; background: none; border: 0; cursor: pointer;
  color: var(--ink-2); text-decoration: underline; padding: .5rem;
}

/* ---- the processing screen. */
.lad-working { list-style: none; padding: 0; margin: 0 0 1.5rem; }
.lad-working li {
  padding: .7rem 0 .7rem 1.6rem; border-bottom: 1px solid var(--rule-soft);
  color: var(--ink-2); position: relative;
}
.lad-working li::before {
  content: "✓"; position: absolute; left: 0; color: var(--accent); font-weight: 700;
}
.lad-picks { list-style: none; padding: 0; margin: 0 0 1.2rem; }
.lad-picks li {
  padding: .7rem .9rem; border: 1px solid var(--rule-soft); border-radius: var(--radius);
  margin-bottom: .5rem; font-family: var(--display); font-weight: 600;
}

/* ---- the comparison step is wider than a question, so the ladder opens out for it. */
.ladder-page .ladder:has(.agents) { max-width: var(--wrap); }

/* ==============================================================================================
   THE NON-BLOCKING CONSENT BANNER — journey B only. AT THE TOP, IN THE FLOW.
   ⚠⚠ `.cookie` on its own is journey A's full-viewport modal with `overflow: hidden` on the body.
   This variant is a band across the top of the page, before the masthead, in normal flow: it
   pushes the page down and scrolls away with it. Nothing is `fixed`, so nothing needs to know its
   height and nothing else on the page can collide with it. That is the whole design, and it came
   from two failures: docked at the bottom it fought the request button for the same corner, and
   because its visibility is decided by two authorities (server session vs localStorage) a button
   positioned relative to it floated mid-page when they disagreed. James, 2026-09-04.
   ⚠ `consent-open` is the body class that goes WITH it and it deliberately does NOT set
   `overflow: hidden` — that is the entire difference from A, and why it is a separate class.
   ============================================================================================== */
.cookie--top {
  position: static; inset: auto; z-index: auto;
  display: block; padding: 0; background: var(--ink);
}
.cookie--top .cookie-panel {
  max-width: var(--wrap); margin: 0 auto; border: 0; border-radius: 0;
  box-shadow: none; max-height: none; padding: .9rem 1.15rem 1rem;
  background: var(--ink); color: #fff;
}
.cookie--top .cookie-panel h4,
.cookie--top .cookie-panel p { color: #fff; }
/* ⚠⚠ DECLINE WAS A WHITE BOX WITH NO WORDS ON IT. `.btn-ghost` keeps its white `background`
   from the base rule; setting only `color: #fff` for the dark band made the label white-on-white.
   Invisible Decline is a C3 equal-prominence failure, not a cosmetic one — and it was caught by
   looking at a screenshot after every automated check had passed. `background` is set explicitly
   here for exactly that reason. */
.cookie--top .btn-ghost { background: transparent; color: #fff; border-color: rgba(255,255,255,.7); }
.cookie--top .btn-ghost:hover { background: rgba(255,255,255,.12); }

/* ⚠⚠ THE COMPARISON MUST STILL EXIST ON A PHONE, AND ON JOURNEY B IT NEARLY DID NOT.
   `@media (max-width:700px) { .tbl-wrap--agents { display:none } }` hides the table in favour of
   `#cards`, which journey A fills FROM JAVASCRIPT (`app.js` reads `window.PL_DATA`). The ladder is
   deliberately script-free, so it renders no cards — and the first phone screenshot of `/B/agents`
   showed a heading, a consent sentence and a "Request 3 free valuations" button with NO TABLE
   BETWEEN THEM. Every automated check was green: the page was 200, the button counted three
   pre-ticked agents, and the markup contained the whole table.
   ⭐ Found by looking at a picture. It is the exact failure `_base.html`'s `<noscript>` block
   already anticipates for journey A, arriving by a different door.
   ⚠ The table keeps its own horizontal scroll inside `.tbl-wrap`, so the document does not widen. */
/* ⚠⚠ THE OVERRIDE THAT USED TO LIVE HERE FORCED THE LEAGUE TABLE ONTO PHONES, AND IT WAS
   ARGUING WITH A DECISION JAMES HAD ALREADY MADE — "MOBILE: get rid of the table view - cards
   only" (2026-08-21, recorded in results.html). It was added because journey B's comparison did
   not render the `#cards` div app.js fills, so hiding the table left a page with no comparison on
   it at all. ⭐ The real fix was to give B the same markup as A, which it now has; the override
   went with it. Mobile is cards on both journeys. */

/* ⚠ The request button repeated at the TOP of the comparison — James, 2026-09-04. The seller
   arrives with three agents already ticked, so the next action must not be below a long table. */
.lad-cta--top { margin-bottom: 1.2rem; }

/* ⚠⚠ THE DOCKED REQUEST BUTTON. Always on screen while the comparison is scrolled — the whole
   reason it exists — in EVERY state. It owns the bottom edge; the consent banner is at the top. */
.lad-cta--dock {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 70;
  border-radius: 0; width: 100%;
  box-shadow: 0 -6px 20px rgba(13, 27, 61, .16);
}
body.has-dock .ladder { padding-bottom: 5.5rem; }
/* ⚠⚠⚠ NO CONSENT-STATE RULES BELONG HERE, AND TWO HAVE BEEN REMOVED. The first made the button
   `static` while the banner was up (it vanished — James's first screenshot). The second offset it
   by the banner's measured height (it floated mid-page over nothing when localStorage hid the
   banner the server thought was showing — his second). Both coupled the button to the banner; the
   banner now lives at the TOP of the page, in the flow, and this button owns the bottom edge in
   every state with nothing to know about. If a future rule here mentions `consent-open`, it is
   re-creating the coupling that failed twice. */

/* ---- the tick screen. Replaces a fake progress checklist (James, 2026-09-04). One mark, big. */
/* ⚠ The whole screen centres, not just the mark. The heading lives in the ladder SHELL, above
   `{% block ask %}`, so centring only `.lad-done` left the title hard against the left margin with
   a centred tick under it — which looked like a page that had lost its content. */
.ladder--centred { text-align: center; }
.ladder--centred .lad-form { align-items: center; }
/* ⚠ Back stays left. A centred back-arrow reads as decoration rather than a control. */
.ladder--centred .lad-back { display: block; text-align: left; }
.lad-done { text-align: center; padding: .5rem 0 0; }
.lad-tick { color: var(--accent); display: block; margin: 0 auto 1.6rem; }
.lad-done .lad-cta { max-width: 22rem; margin: 0 auto; }

/* ---- the address list. Radios, not a <select>: a native select on a phone opens a modal wheel,
   which is at its worst on a list of forty flats in one building. */
.lad-addrlist {
  border: 1px solid var(--rule-firm); border-radius: var(--radius);
  background: var(--surface); max-height: 22rem; overflow-y: auto;
}
.lad-addr { display: flex; gap: .7rem; align-items: flex-start; cursor: pointer;
            padding: .8rem .9rem; border-bottom: 1px solid var(--rule-soft); }
.lad-addr:last-child { border-bottom: 0; }
.lad-addr:hover { background: var(--accent-wash); }
.lad-addr input { margin-top: .2rem; accent-color: var(--accent); flex: 0 0 auto; }
.lad-addr[hidden] { display: none; }
/* ⚠ The escape hatch is IN the list, not beside it — a seller whose address is genuinely absent
   must not have to work out that the list has failed them. Set apart, not hidden away. */
.lad-addr--manual { border-top: 1px solid var(--rule-firm); font-weight: 600; }
.lad-manual { margin-top: .9rem; }

/* ⚠ `.lad-reveal` USED TO LIVE HERE and is gone with the thing it styled. The phone field was
   once revealed from JavaScript as the seller typed their email; that guessed wrong about when
   they had finished, and the step now asks the two questions on two passes of one URL with no
   script at all. The rules are removed rather than left "in case" — a selector nothing matches is
   the kind of thing a later reader assumes is load-bearing.
   ⭐ THE BUG IT CARRIED IS WORTH KEEPING THOUGH, because the next `display:`-setting class will
   meet it: `display: flex` BEATS the browser's built-in `[hidden] { display: none }`, so an
   element that is meant to start hidden is visible from the first paint. Any rule that sets
   `display` on something ever marked `hidden` needs an explicit `[hidden] { display: none }`
   beside it. */
