/* ===========================================================================
 * home.css — the public home page at '/'.
 *
 * Visual language: cinematic editorial. A deep, atmospheric canvas (an aerial
 * city at dusk), warm amber as the single interactive accent, teal reserved for
 * "this actually happened" proof, an editorial serif for display type and
 * tabular mono for every number on the page.
 *
 * Theming: the page defines its OWN token set rather than borrowing the app's
 * trust-blue palette, but it flips on the same [data-theme="dark"] attribute
 * styles.css uses, so the existing theme toggle keeps working across the whole
 * site. Defaults below are the LIGHT values; the dark block overrides them.
 *
 * Nothing here leaks into the map app — every rule is scoped under .hp or an
 * .hp-* class.
 * ======================================================================== */

.hp {
  /* -- surfaces ---------------------------------------------------------- */
  --hp-bg: #f4f1ec;            /* warm paper, not the app's cool slate */
  --hp-bg-deep: #e9e4db;       /* footer / banded sections */
  --hp-card: #ffffff;
  --hp-card-veil: rgba(255, 255, 255, 0.72); /* frosted panels over the backdrop */
  --hp-line: rgba(24, 32, 47, 0.12);
  --hp-line-soft: rgba(24, 32, 47, 0.07);

  /* -- type -------------------------------------------------------------- */
  --hp-ink: #161d2b;
  --hp-ink-soft: rgba(22, 29, 43, 0.74);
  --hp-muted: #626d7e;

  /* -- accents ----------------------------------------------------------- */
  --hp-primary: #b0651b;       /* amber, darkened for contrast on paper */
  --hp-primary-bright: #d78a35;/* the amber used for fills/glows */
  --hp-primary-ink: #ffffff;   /* text on a primary-filled button */
  --hp-primary-wash: rgba(215, 138, 53, 0.12);
  --hp-signal: #0d7a70;        /* teal = verified / actually happened */
  --hp-signal-wash: rgba(13, 122, 112, 0.1);

  /* -- depth ------------------------------------------------------------- */
  --hp-shadow: 0 1px 2px rgba(16, 24, 40, 0.06), 0 18px 44px -26px rgba(16, 24, 40, 0.4);
  --hp-shadow-lg: 0 32px 80px -36px rgba(16, 24, 40, 0.5);
  --hp-backdrop-scrim: linear-gradient(
    to bottom,
    rgba(244, 241, 236, 0.86),
    rgba(244, 241, 236, 0.8) 42%,
    rgba(244, 241, 236, 0.95)
  );

  /* -- fonts ------------------------------------------------------------- */
  --hp-display: "Fraunces", "Archivo Narrow", Georgia, serif;
  --hp-sans: var(--sans);
  --hp-mono: var(--mono);

  /* -- rhythm ------------------------------------------------------------ */
  --hp-max: 1180px;
  --hp-gutter: clamp(18px, 5vw, 44px);

  margin: 0;
  /* styles.css pins body to height:100% for the fixed-chrome map app; this page
     scrolls, so relax it back to a minimum. */
  height: auto;
  min-height: 100%;
  font-family: var(--hp-sans);
  color: var(--hp-ink);
  background: var(--hp-bg);
  -webkit-font-smoothing: antialiased;
}

/* ---------- Dark: the reference look ------------------------------------ */
[data-theme="dark"] .hp {
  --hp-bg: #12161f;
  --hp-bg-deep: #0d1017;
  --hp-card: #1a202c;
  --hp-card-veil: rgba(26, 32, 44, 0.66);
  --hp-line: rgba(255, 255, 255, 0.1);
  --hp-line-soft: rgba(255, 255, 255, 0.055);

  --hp-ink: #f3f5f8;
  --hp-ink-soft: rgba(243, 245, 248, 0.74);
  --hp-muted: #939db0;

  --hp-primary: #f0a860;
  --hp-primary-bright: #f0a860;
  --hp-primary-ink: #2b1a06;
  --hp-primary-wash: rgba(240, 168, 96, 0.14);
  --hp-signal: #4fc9bd;
  --hp-signal-wash: rgba(79, 201, 189, 0.12);

  --hp-shadow: 0 1px 0 rgba(0, 0, 0, 0.3), 0 20px 46px -26px rgba(0, 0, 0, 0.8);
  --hp-shadow-lg: 0 36px 90px -40px rgba(0, 0, 0, 0.9);
  --hp-backdrop-scrim: linear-gradient(
    to bottom,
    rgba(18, 22, 31, 0.72),
    rgba(18, 22, 31, 0.7) 40%,
    rgba(18, 22, 31, 0.94)
  );
}

/* Ambient wash behind everything: a warm glow top-right (the amber highlight
   in the reference art) fading into cool depth. Fixed so it doesn't scroll. */
.hp::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(58rem 34rem at 82% -10%, var(--hp-primary-wash), transparent 62%),
    radial-gradient(46rem 32rem at 6% 2%, rgba(99, 102, 241, 0.09), transparent 58%),
    radial-gradient(70rem 46rem at 50% 118%, rgba(45, 178, 170, 0.07), transparent 60%);
}

/* ---------- Fixed city backdrop ----------------------------------------- */
/* The aerial photograph stays put while the page scrolls over it, so the whole
   home page reads as "scanning a real place from above". Purely decorative. */
.hp-backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* 190 KB OF WEBP, NOT 2.4 MB OF PNG.
     This is the largest asset on the page a paid click lands on — decorative,
     full-bleed, and covered by the scrim below — and on a phone on cell data it
     was several seconds of the hero rendering over an empty plate. Same image,
     roughly 8% of the bytes.
     The JPEG is the fallback: image-set picks the first format the browser can
     decode, so anything without WebP support still gets a 260 KB file rather
     than the 2.4 MB one. Declared twice because the unprefixed `image-set` is
     the standard and `-webkit-image-set` is what older Safari understands; a
     browser that knows neither falls through to the plain url() below. */
  background-image: url("/images/city-backdrop.jpg");
  background-image: -webkit-image-set(
    url("/images/city-backdrop.webp") type("image/webp"),
    url("/images/city-backdrop.jpg") type("image/jpeg"));
  background-image: image-set(
    url("/images/city-backdrop.webp") type("image/webp"),
    url("/images/city-backdrop.jpg") type("image/jpeg"));
  background-size: cover;
  background-position: top center;
  opacity: 1;
}
[data-theme="dark"] .hp-backdrop { opacity: 1; }
.hp-backdrop::after {
  /* Scrim so text stays readable over the photo in either theme. */
  content: "";
  position: absolute;
  inset: 0;
  background: var(--hp-backdrop-scrim);
}

/* Everything real sits above the backdrop + wash. */
.hp-shell { position: relative; z-index: 1; }

/* ---------- Shared primitives ------------------------------------------- */
.hp-wrap {
  max-width: var(--hp-max);
  margin: 0 auto;
  padding-inline: var(--hp-gutter);
}

/* Section eyebrow: mono, tracked-out, amber — the "instrument label" voice. */
.hp-eyebrow {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-family: var(--hp-mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--hp-primary);
}
.hp-eyebrow svg { fill: currentColor; }

.hp-h2 {
  margin: 12px 0 0;
  font-family: var(--hp-display);
  font-weight: 600;
  font-size: clamp(26px, 3.4vw, 38px);
  line-height: 1.12;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.hp-lede {
  margin: 14px 0 0;
  max-width: 62ch;
  font-size: 16px;
  line-height: 1.65;
  color: var(--hp-ink-soft);
}

/* ===========================================================================
 * LONG-FORM DOCUMENTS — /privacy and /methodology
 * ===========================================================================
 * The site's other sections are marketing: short blocks, wide measure, a figure
 * or a card carrying most of the weight. These two pages are the opposite — a
 * few thousand words that someone actually has to READ, in order, possibly
 * while deciding whether to trust the product with a card number.
 *
 * So the rules here are the ones long-form prose needs and the marketing
 * sections don't: a measure capped near 70 characters, numbered headings that
 * can be linked to and cited, a definition list for the "term — what it means"
 * shape both documents lean on constantly, and enough space between sections
 * that the structure is visible while scrolling past at speed.
 * ======================================================================== */

/* The masthead block: eyebrow, title, dates, summary. Sits on the page's own
   background rather than in a card — this is a document, not a widget. */
.hp-legal-head { padding-bottom: clamp(24px, 3vw, 36px); }
.hp-legal-dates {
  margin: 14px 0 0;
  font-family: var(--hp-mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--hp-muted);
}

/* THE SUMMARY BOX. Not decoration: the handful of facts a reader wants are
   otherwise spread across a dozen numbered sections, and a policy that makes
   someone hunt for "do you sell my data" reads as though it would rather they
   didn't find out. It says explicitly that the sections below govern, so it
   cannot be mistaken for the operative text. */
.hp-legal-summary {
  margin: 30px 0 0;
  max-width: 74ch;
  padding: 22px 24px;
  border: 1px solid var(--hp-line);
  border-left: 3px solid var(--hp-primary);
  border-radius: 14px;
  background: var(--hp-card);
}
.hp-legal-summary-head {
  margin: 0 0 12px;
  font-family: var(--hp-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hp-primary);
}
.hp-legal-summary ul { margin: 0; padding-left: 20px; display: grid; gap: 9px; }
.hp-legal-summary li {
  font-size: 15px;
  line-height: 1.6;
  color: var(--hp-ink-soft);
}
.hp-legal-summary li b { color: var(--hp-ink); }
.hp-legal-summary-foot {
  margin: 14px 0 0;
  font-size: 12.5px;
  font-style: italic;
  color: var(--hp-muted);
}

/* The document body. */
.hp-legal { padding: 0 0 clamp(56px, 8vw, 96px); }
.hp-legal .hp-wrap > * { max-width: 74ch; }

.hp-legal h2 {
  margin: 44px 0 0;
  padding-top: 26px;
  border-top: 1px solid var(--hp-line-soft);
  font-family: var(--hp-display);
  font-weight: 600;
  font-size: clamp(20px, 2.2vw, 25px);
  line-height: 1.22;
  letter-spacing: -0.01em;
  /* Anchored headings: every h2 and h3 carries an id, so a support reply can
     link someone to §5.2 rather than telling them to scroll. */
  /* Clears the sticky nav, so a link to #rights lands on the heading rather
     than behind the header. */
  scroll-margin-top: 88px;
}
.hp-legal h2:first-child { margin-top: 8px; border-top: 0; padding-top: 0; }
.hp-legal h3 {
  margin: 30px 0 0;
  font-size: 16.5px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--hp-ink);
  /* Clears the sticky nav, so a link to #rights lands on the heading rather
     than behind the header. */
  scroll-margin-top: 88px;
}
.hp-legal p {
  margin: 13px 0 0;
  font-size: 15.5px;
  line-height: 1.72;
  color: var(--hp-ink-soft);
}
.hp-legal p b, .hp-legal li b, .hp-legal dd b { color: var(--hp-ink); font-weight: 600; }
.hp-legal a { color: var(--hp-primary); }
.hp-legal a:hover { text-decoration: underline; }
.hp-legal ul { margin: 13px 0 0; padding-left: 22px; display: grid; gap: 10px; }
.hp-legal li {
  font-size: 15.5px;
  line-height: 1.7;
  color: var(--hp-ink-soft);
}
.hp-legal code {
  font-family: var(--hp-mono);
  font-size: 0.88em;
  padding: 1px 5px;
  border-radius: 4px;
  background: var(--hp-card-veil);
  color: var(--hp-ink);
  /* A long host name must not push the measure wider than the prose. */
  word-break: break-word;
}

/* The "term — what it means" list both documents use for enumerating the
   categories of data, of retention, of hit type. A <dl> rather than a bulleted
   list because that is what these pairs actually are, and because the term
   needs to be findable on its own when someone is looking for one row. */
.hp-legal-dl { margin: 16px 0 0; }
.hp-legal-dl dt {
  margin-top: 16px;
  font-size: 14px;
  font-weight: 600;
  color: var(--hp-ink);
}
.hp-legal-dl dt:first-child { margin-top: 0; }
.hp-legal-dl dd {
  margin: 5px 0 0;
  padding-left: 16px;
  border-left: 2px solid var(--hp-line-soft);
  font-size: 15px;
  line-height: 1.68;
  color: var(--hp-ink-soft);
}

/* Every figure on the page uses tabular mono so columns of numbers line up and
   digits don't jitter when they update from the API. */
.hp-num {
  font-family: var(--hp-mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

/* Buttons ----------------------------------------------------------------- */
.hp-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  border: 1px solid transparent;
  border-radius: 11px;
  padding: 0 18px;
  height: 44px;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  /* A button label is one line, always. Without this a button that flexbox has
     squeezed below its text width breaks mid-label instead — "Sign / in",
     "Start / free" — which is what the nav bar was doing. Every rule below is
     sized so a button never HAS to be squeezed; this is the guarantee that a
     future label change can't quietly reintroduce the two-line look. */
  white-space: nowrap;
  transition: background-color 0.16s var(--ease), border-color 0.16s var(--ease),
    transform 0.16s var(--ease), box-shadow 0.16s var(--ease);
}
.hp-btn-primary {
  background: var(--hp-primary-bright);
  color: var(--hp-primary-ink);
  box-shadow: 0 10px 26px -14px var(--hp-primary-bright);
}
.hp-btn-primary:hover { transform: translateY(-1px); filter: brightness(1.06); }
.hp-btn-ghost {
  background: var(--hp-card-veil);
  border-color: var(--hp-line);
  color: var(--hp-ink);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.hp-btn-ghost:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
.hp-btn-sm { height: 36px; padding: 0 14px; font-size: 14px; border-radius: 9px; }

/* ===========================================================================
 * NAV
 * ======================================================================== */
.hp-nav {
  position: sticky;
  top: 0;
  z-index: 40;
  border-bottom: 1px solid var(--hp-line-soft);
  background: color-mix(in srgb, var(--hp-bg) 82%, transparent);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
.hp-nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  height: 64px;
  max-width: var(--hp-max);
  margin: 0 auto;
  padding-inline: var(--hp-gutter);
}
.hp-brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
  color: var(--hp-ink);
}
.hp-brand-mark { display: grid; place-items: center; color: var(--hp-primary); }
.hp-brand-name {
  font-family: var(--hp-display);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.hp-brand-name b { font-weight: 700; color: var(--hp-primary); }

/* Brand lockup: the logo plus the tagline, held together as one unit.
   .hp-nav-inner is a three-way space-between, so wrapping these two in a flex
   item keeps the tagline anchored to the mark instead of drifting toward the
   nav links as the viewport widens. */
.hp-brand-lockup {
  display: flex;
  align-items: center;
  gap: 12px;
  /* The nav is a fixed 64px tall and space-between will happily crush the first
     item to fit; this stops the tagline wrapping to two lines in the tight band
     between 860px (where the nav links disappear) and the breakpoint below. */
  min-width: 0;
}
/* The tagline. Sized to sit UNDER the nav links rather than compete with them:
   this is the promise, they are the navigation, and a tagline as loud as the
   menu turns the header into two things shouting. The leading rule keeps it on
   the logo's optical centre line. */
.hp-tagline {
  margin: 0;
  padding-left: 12px;
  border-left: 1px solid var(--hp-line);
  font-size: 13.5px;
  line-height: 1.2;
  color: var(--hp-muted);
  white-space: nowrap;
}

/* The tagline is the first thing to give when the bar runs out of room: it is a
   restatement of the headline sitting a few hundred pixels below it, so losing
   it costs the visitor nothing, whereas losing a CTA costs them the product.
   The divider is the tagline's own left border rather than a separate element,
   so it disappears with it instead of being left hanging beside the mark.

   The breakpoint is 1180px, not the 1080px it used to be, because 1080 was
   far too generous. Measured with the real Inter faces, the signed-out bar
   needs roughly:

       logo + tagline   350px
       four nav links   394px
       toggle + 2 CTAs  226px
       two 16px gaps     32px
       ---------------------
                       1002px

   against a content column of (viewport − 2 × 44px gutter). Flexbox does not
   warn you when that does not fit — it shrinks the items and lets their labels
   wrap, which is exactly what the bar was doing.

   1180 rather than a tighter number because --hp-max is 1180: past that point
   the column stops growing with the viewport and sits at 1092px whatever the
   screen. So this is the first width at which the tagline has real room —
   ~90px spare in Inter, ~45px in the wider fallback face the browser paints
   with on the frames before the webfont lands. Anything lower puts the tagline
   in a column that has already run out. */
@media (max-width: 1180px) {
  .hp-tagline { display: none; }
}

/* NOTE: there is no .hp-nav-link-cta rule any more. That class styled a second
   "Open the map" link inside .hp-nav-actions on the home page only — the 105px
   that pushed the signed-out bar past its column. The link now lives once, in
   the nav list, as .hp-nav-drawer-extra (see below), which is where the other
   pages have always kept it. */

/* The hero mark. Sizing in a real class rather than in invalid HTML attributes
   that only worked by accident — see the note on it in home.html. The width is
   capped so the SVG scales down on a phone instead of forcing a sideways
   scroll. */
.hp-hero-mark {
  filter: drop-shadow(5px 5px 10px rgba(0, 0, 0, 0.3));
  margin-bottom: 6px;
}
.brand-logo-img.brand-logo-hero {
  height: clamp(84px, 16vw, 170px);
  width: auto;
  max-width: 100%;
}

.hp-nav-links { display: flex; align-items: center; gap: 26px; }
.hp-nav-links a {
  font-size: 14.5px;
  color: var(--hp-muted);
  text-decoration: none;
  transition: color 0.15s var(--ease);
  /* Same reason as .hp-btn: a section link is a label, not a paragraph. */
  white-space: nowrap;
}
.hp-nav-links a:hover { color: var(--hp-ink); }
/* The actions never shrink. They are the smallest, most load-bearing items in
   the bar — a 36px square toggle and two CTAs — so if anything has to give up
   width it should be the links row, which the breakpoints below fold into a
   drawer wholesale rather than crushing. */
.hp-nav-actions { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }

/* Theme toggle — same two-icon pattern as the app's topbar button. */
.hp-theme {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--hp-line);
  border-radius: 9px;
  background: transparent;
  color: var(--hp-muted);
  cursor: pointer;
}
.hp-theme:hover { color: var(--hp-primary); border-color: var(--hp-primary); }
.hp-theme .icon-sun { display: none; }
[data-theme="dark"] .hp-theme .icon-sun { display: block; }
[data-theme="dark"] .hp-theme .icon-moon { display: none; }

/* Account button — the signed-in replacement for the "Start free" CTA.
   Deliberately shaped like .hp-theme rather than like a button: it is a
   control, not a call to action, and putting it in the same square next to the
   theme toggle makes that read at a glance. The initials are mono for the same
   reason the map's .profile-btn does it — two letters in a proportional face at
   this size look like a typo. */
.hp-account {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1px solid var(--hp-line);
  border-radius: 9px;
  background: var(--hp-primary-wash);
  color: var(--hp-primary);
  font-family: var(--hp-mono);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  cursor: pointer;
  transition: border-color 0.14s, background 0.14s, color 0.14s;
}
.hp-account:hover {
  border-color: var(--hp-primary);
  background: var(--hp-primary);
  color: var(--hp-primary-ink);
}
.hp-account[hidden] { display: none; }

/* ---------------------------------------------------------------------------
 * THE NAV ON SMALL SCREENS
 * ---------------------------------------------------------------------------
 * At ≤860px the four section links used to be set to display:none with nothing
 * put in their place — no menu button, no drawer, no replacement of any kind.
 * "The map", "How it works", "Track record" and "What's free" simply became
 * unreachable on every phone, on every page of the site.
 *
 * What survived was worse: .hp-nav-actions is a no-wrap row holding a 36px
 * theme toggle plus three buttons — roughly 340px of content before the logo
 * and the gutters. On a 375px viewport that overflows, so "Start free" was
 * pushed off the edge or the whole page scrolled sideways.
 *
 * The steps, each matching a width at which the row stops fitting. The numbers
 * come from measuring the signed-out bar with the real Inter faces, and each
 * one has 20px or more of headroom so a fallback face on the first frames
 * cannot tip it back into wrapping:
 *
 *   ≤ 1180px  The tagline goes (rule further up).
 *   ≤ 940px   The links move into a drawer behind a menu button. Nothing is
 *             lost, it just moves. This was 860px, which was optimistic: at
 *             861px the four links plus the CTAs need ~793px and the column is
 *             773px, so the labels wrapped in the band just above the old
 *             breakpoint.
 *   ≤ 480px   The bar tightens — smaller gaps, tighter buttons, smaller mark,
 *             narrower gutters — and "Sign in" drops its ghost-button chrome
 *             for plain link styling. That is what buys both CTAs a single
 *             line each down to a 320px screen without taking anything away:
 *             the toggle stays, both CTAs stay, the menu stays.
 * ------------------------------------------------------------------------- */

/* The drawer's own copy of "Open the map". Declared BEFORE the media query that
   reveals it — base first, override second, or the base wins at equal
   specificity and the entry never appears. */
.hp-nav-drawer-extra { display: none; }

/* The menu button. In the markup at every width and revealed only below 940px,
   so no script decides whether it exists. */
.hp-nav-toggle {
  display: none;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--hp-line);
  border-radius: 10px;
  background: var(--hp-card-veil);
  color: var(--hp-ink);
  cursor: pointer;
}
.hp-nav-toggle:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
.hp-nav-toggle .icon-close { display: none; }
.hp-nav-toggle[aria-expanded="true"] .icon-open { display: none; }
.hp-nav-toggle[aria-expanded="true"] .icon-close { display: block; }

@media (max-width: 940px) {
  .hp-nav-toggle { display: grid; }

  /* The drawer. Not a full-screen overlay: it is four links, and covering the
     whole page to show four links means also solving focus trapping, scroll
     locking and a close affordance for something that fits in 180px. */
  .hp-nav-links {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    right: var(--hp-gutter);
    left: auto;
    z-index: 60;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    min-width: 208px;
    padding: 8px;
    border: 1px solid var(--hp-line);
    border-radius: 14px;
    background: var(--hp-card);
    box-shadow: var(--hp-shadow);
  }
  .hp-nav-inner { position: relative; }
  .hp-nav-links.is-open { display: flex; }
  .hp-nav-links a {
    padding: 9px 10px;
    border-radius: 8px;
    font-size: 15px;
    color: var(--hp-ink);
  }
  .hp-nav-links a:hover { background: var(--hp-card-veil); }

  /* "Open the map" appears as soon as there IS a drawer to put it in, rather
     than waiting for 560px as it used to. It is a destination, not a funnel
     step, so the drawer is where it belongs the moment the drawer exists —
     and below 940px it is the only route to /map in the header. */
  .hp-nav-drawer-extra { display: block; }
}

/* Phones. The bar here is the mark, the menu button, the theme toggle and the
   two CTAs: about 387px of content on a 375px screen, so every item gives a
   little rather than one item being crushed — and nothing is taken away. */
@media (max-width: 480px) {
  .hp-nav-inner { gap: 10px; padding-inline: 14px; }
  .hp-nav-actions { gap: 6px; }
  /* Tighter than .hp-btn-sm, and only here: the labels are two short words and
     the padding is what is expensive, not the type. */
  .hp-nav-actions .hp-btn-sm { padding: 0 10px; font-size: 13.5px; }
  /* The drawer is positioned off --hp-gutter, which the line above no longer
     matches; without this the panel hangs 5px outside the bar's right edge. */
  .hp-nav-links { right: 14px; }
  /* styles.css already drops the mark to 30px below 600px for the map topbar.
     24px is the home bar's own step — small, but it is 20px of the 50px this
     block has to find. */
  .hp-brand-lockup .brand-logo-img { height: 24px; }
  /* "Sign in" gives up its ghost chrome here and reads as a plain link. The
     border, the veil and the 20px of side padding are worth ~28px on a bar that
     is ~50px short at 375px, and they are the part of the control nobody needs:
     beside a filled orange "Start free" the hierarchy is already obvious. The
     tap target is untouched — it keeps the full 36px height. This is what lets
     the theme toggle, both CTAs and the menu all stay on a 320px screen. */
  .hp-nav-actions .hp-btn-ghost {
    padding: 0 4px;
    border-color: transparent;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* Below 340px — an original iPhone SE, or a 375px phone at heavy browser zoom —
   even the trimmed bar is ~25px too wide, and with the labels held to one line
   that surplus would push the page into sideways scrolling instead of wrapping.
   The theme toggle is the one control here that is a preference rather than a
   route: the page already follows the OS colour scheme on its own (see the
   inline script in the <head>), and the toggle returns the moment the viewport
   does. */
@media (max-width: 340px) {
  .hp-theme { display: none; }
}

/* ===========================================================================
 * HERO
 *
 * THE FOLD IS ONE GRID. The lockup, the headline and the zip teaser on the
 * left; the four coverage figures, as separate widgets, on the right. They used
 * to be a full-width band two sections further down (.hp-proof, now gone), so a
 * visitor never saw the claim and the proof of it at the same time — see the
 * comment on the section in home.html.
 *
 * The top padding is smaller than it was for the same reason: the hero now
 * carries the figures too, and every pixel above the mark is a pixel of the
 * thing that has to fit on one screen.
 * ======================================================================== */
.hp-hero { padding: clamp(20px, 3vh, 44px) 0 clamp(32px, 6vh, 64px); }

.hp-hero-grid {
  display: grid;
  gap: clamp(26px, 3.2vw, 44px);
  /* TOP-ALIGNED, NOT CENTRED. The left column grows by ~200px the moment a zip
     lookup renders its result card, and centring would slide the figures down
     the page every time someone used the form. Aligned to the top they simply
     stay where the reader last saw them. */
  align-items: start;
}
/* Both columns are grid items holding long words and a scroll container, so
   both need the min-width:0 that grid items do not get by default — without it
   a long metro name in the accuracy ticker can push the column wider than its
   track and force the whole page sideways. */
.hp-hero-lead,
.hp-hero-stats { min-width: 0; }

/* The right column: two labelled groups of figures, stacked. The scorecard is
   `hidden` until its payload lands, and `display: grid` here is what makes that
   free — a hidden grid item occupies nothing, so the coverage figures sit at
   the top of the column until the scorecard arrives and then move down once,
   rather than the column reserving a hole for it. */
.hp-hero-stats {
  display: grid;
  gap: clamp(14px, 1.4vw, 18px);
  align-content: start;
}

/* THE TEASER ROW, IN A COLUMN INSTEAD OF ACROSS THE PAGE.
   Its three items are a 220px zip field, a trade select that takes whatever is
   left, and a ~215px submit button. Across the full width that left the select
   ~300px; in the hero's left column it left it SIXTY, which rendered as
   "Roofir" with the caret sitting on top of the text.

   The zip field is what gives the width back. Its 220px basis was sized for a
   full-page row and is far more than five digits and an icon need — 160px still
   shows "Your zip code" in full — so the select gets 60px back and the button
   another 8 from tighter padding. Scoped to the hero, because the closing CTA's
   copy of this form still spans the page and its proportions were never wrong.

   The basis is not touched below 720px: .hp-field-zip is already overridden to
   `flex: 1 1 auto` there, where the row is a COLUMN and a basis means height
   rather than width. See the note on that rule. */
.hp-hero-lead .hp-field-zip { flex: 0 1 160px; }
.hp-hero-lead .hp-teaser .hp-btn { padding: 0 18px; }
/* The label over a group. Each group answers a different question — how good
   are the predictions, and what is behind them — and eight unlabelled cards in
   one column would read as one long list of unrelated numbers. */
.hp-hero-stats-group > .hp-eyebrow { display: block; margin: 0 0 10px; }

/* ---------- The widgets, in the hero's right column -----------------------
   SEPARATE CARDS HERE, not the joined slabs the two original bands used. Run
   the full width of the page, hairline dividers on one shared surface read as
   an instrument panel; in a 2x2 block beside a headline the same treatment
   reads as a table someone forgot to finish. Distinct cards say "independent
   facts", which is what these are.

   BOTH GRIDS GET THE SAME FRAME so the eight cards read as one set of figures
   in two labelled groups rather than as two designs that happened to land in
   the same column. Only the CONTENTS differ — a scorecard cell pairs a figure
   with a comparison, a coverage cell is a figure on its own — which is exactly
   the difference the reader should see. */
.hp-hero-stats .hp-proof-grid,
.hp-hero-stats .hp-scorecard-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  /* Undo the joined-slab frame: the cells carry their own now. */
  border: 0;
  border-radius: 0;
  overflow: visible;
  background: none;
}
.hp-hero-stats .hp-proof-cell,
.hp-hero-stats .hp-score-cell {
  padding: 13px 16px;
  border: 1px solid var(--hp-line);
  border-radius: 16px;
  /* The same veiled, blurred surface as the teaser form directly opposite it,
     so the two columns read as one composition over the aerial backdrop rather
     than as a form beside a stack of opaque boxes. */
  background: var(--hp-card-veil);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: var(--hp-shadow);
}

/* The coverage figures are the SUPPORTING group, and their type says so.
   On their own in this column they carried a 40px figure; sharing it with the
   scorecard, that made the thing explaining the claim louder than the claim.
   Sized just under the scorecard's 33px ceiling instead.

   Everything the accuracy ticker measures itself from — --proof-value-size,
   --proof-label-size, --proof-label-leading — is still declared on
   .hp-proof-grid and merely overridden here, so the row height it derives
   follows these numbers automatically. See .hp-acc-window. */
.hp-hero-stats .hp-proof-grid {
  /* FIXED, not a vw clamp like the band's. Above 1180px the page stops growing
     (--hp-max), so the column these sit in is a constant width from there up —
     but a vw-scaled figure keeps growing past it, which is why the block was
     ~10px taller at 1440 than at 1280 for no visible gain and pushed its last
     row through the fold on the wider screen. A column of fixed width gets type
     of fixed size. */
  --proof-value-size: 26px;
  --proof-value-gap: 10px;
  /* A notch down from the band's 13px. It buys back roughly a line on the
     accuracy cell, whose four-line label is the tallest thing in this group and
     therefore sets the height of the whole bottom row. */
  --proof-label-size: 12.5px;
}
.hp-hero-stats .hp-proof-label { margin-top: 8px; }
/* HOW MANY LINES THE ACCURACY LABEL GETS, which is also what fixes the ticker's
   row height: the label is clipped to exactly the number of lines that height
   is computed from, so an allowance that is too small slices the last line in
   half and one that is too large leaves dead space under every figure. See
   .hp-acc-window.

   STACKED, three — one more than the longest metro name needs, since the cards
   are then half the page wide and "median accuracy across San Bernardino, CA's
   642 top calls." is two lines. The two-column case raises it below.

   Declared here rather than relying on .hp-proof-cell-accuracy's own value:
   that three was sized for the full-width band this replaced, and inheriting a
   number that happens to match today is how it goes wrong the next time the
   band's rule is touched. */
.hp-hero-stats .hp-proof-cell-accuracy { --acc-label-lines: 3; }

/* ---------- The scorecard, in the same column ----------------------------
   It used to be a full-width banner with a centred heading and a page-level
   <h2>. In a 470px column that heading was three lines of 40px display type
   competing with the actual headline six inches to its left, so the head is
   left-aligned and sized down to a group label — the figures are what the
   block is for, and the heading's job here is only to say whose figures they
   are. The text itself is untouched: paintScorecard() writes it, and it has to
   read correctly in both the nationwide and the single-zip state. */
.hp-hero-stats .hp-scorecard { padding: 0; }
.hp-hero-stats .hp-scorecard-head {
  max-width: none;
  margin: 0 0 10px;
  text-align: left;
}
.hp-hero-stats .hp-scorecard-title {
  margin: 6px 0 0;
  /* Fixed for the same reason as the coverage figures below — see that note. */
  font-size: 18px;
  line-height: 1.26;
  letter-spacing: -0.012em;
}
.hp-hero-stats .hp-scorecard-sub {
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.48;
}
.hp-hero-stats .hp-scorecard-sub[hidden] { display: none; }
/* The scorecard's figures are the LOUD group — see the note on the coverage
   figures above. Held a little under the value size they have at full width so
   two of them side by side in this column still fit on one line. */
.hp-hero-stats .hp-score-value { font-size: 27px; }
.hp-hero-stats .hp-score-unit { font-size: 12px; }
/* The cells are flex columns with a 6px gap between every line; at this size
   that reads as four loose paragraphs rather than one figure with its caption. */
.hp-hero-stats .hp-score-cell { gap: 4px; }

/* THE PAIR IN THE MIDDLE ROW, KEPT IN STEP. Their units are different lengths
   — "homes we said would pull a permit" wraps to two lines, "homes that did"
   does not — so the dollar line underneath sat at a different height in each,
   and the dashed rule that separates it became a visible step between two cards
   whose whole purpose is to be read against each other. Two lines' worth of
   space in both, which costs nothing: the row is already as tall as the wrapped
   one. Scoped to the pair — the cells that are neither the wide live figure
   above nor the accuracy cell below. */
.hp-hero-stats .hp-score-cell:not(:first-child):not(:has(.hp-score-axes)) .hp-score-unit {
  min-height: 2.4em;
}
/* The kicker names the cell AND its month — "Actually happened · June 2026" —
   which is 29 tracked-out mono characters against ~190px of cell. At the band's
   tracking it wrapped to two lines in that one cell and nowhere else, which
   dropped its figure half a line below the figure it is meant to be compared
   with. Tightened just enough to hold one line; still the widest-tracked type
   on the card, which is what makes it read as a label. */
.hp-hero-stats .hp-score-kicker { font-size: 9px; letter-spacing: 0.09em; }

/* ---- WHICH CELL GOES WHERE, and why it is not a plain 2x2 ----------------
   renderNationalScorecard emits its cells in one fixed order:

     1  what we're forecasting NOW
     2  what we forecast for the last settled month
     3  what actually happened in it
     4  how close that was

   and (2) and (3) are a PAIR — each is close to meaningless without the other,
   which is why the builder's own comment puts them next to each other. Dropped
   into a 2x2 they land diagonally, top-right and bottom-left, and the one
   comparison the block exists to make becomes the one thing the eye has to work
   for.

   So the two singles span the full width and the pair gets the middle row to
   itself:

     [  forecasting now             ]
     [ we forecast ][ actually ... ]
     [  how close we were           ]

   which also suits them individually — the live figure is the headline and
   reads better wide, and the accuracy cell's four bars go two-up instead of
   stacking four deep (see .hp-score-axes below), which is most of what pays for
   the extra row.

   MATCHED BY STRUCTURE, NOT BY COUNT. The accuracy cell is the only one that
   contains axes, and the live forecast is the only one the builders ever emit
   first — so this holds for the zip-scope version of the block too, and for the
   partial states where the payload yields only one or two cells (each then
   simply spans, which is the right answer for a lone cell anyway). */
.hp-hero-stats .hp-score-cell:first-child,
.hp-hero-stats .hp-score-cell:has(.hp-score-axes) { grid-column: 1 / -1; }

/* The live figure, laid out ACROSS its full-width card rather than down it.
   Stacked, a wide card spends 130px saying four short things in a 470px column
   and the unit line ends two-thirds empty. Set on one baseline, the figure and
   what it counts read as the single sentence they are — "41,287 homes we expect
   to pull a permit in September 2026" — and the card gives ~30px back to the
   rows below, which is most of what the extra row costs.

   Only this cell. The pair beneath it is half as wide and has no room for it,
   and they have to stay identical to each other in any case: two cards a reader
   is meant to compare cannot be laid out differently. */
.hp-hero-stats .hp-score-cell:first-child {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: 14px;
  row-gap: 4px;
}
.hp-hero-stats .hp-score-cell:first-child .hp-score-kicker,
.hp-hero-stats .hp-score-cell:first-child .hp-score-sub { grid-column: 1 / -1; }

/* Two-up rather than four-deep, now that the cell has the full column width.
   Each axis is a label, a percentage and a bar under them; stacked four high
   they were the tallest thing in the whole right-hand column. */
.hp-hero-stats .hp-score-axes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px 18px;
}
.hp-hero-stats .hp-score-sub { margin-top: 5px; padding-top: 7px; font-size: 12.5px; }

/* THE TWO-COLUMN FOLD. Below this width the columns stack, which is the layout
   the hero had before the figures joined it.

   1120px, and the number comes from the LEFT column rather than from the right.
   The teaser row (a 220px zip field, a trade select and a 'See the free counts'
   button) has to sit on one line, and it needs ~530px of track before the
   select starts getting crushed between the two fixed-width items. At 1120px
   the left track is ~545px. Every width below that is better served by the
   stacked layout, where the teaser has the whole page and the widgets go 2-up
   at twice the width.

   AFTER the rules it overrides, not before. A media query adds no specificity,
   so between two equally specific rules it is source order that decides — the
   same trap .hp-field-zip documents further up this file, and the reason the
   four-line allowance below has to be written down here rather than beside the
   three-line one. */
@media (min-width: 1120px) {
  .hp-hero-grid {
    grid-template-columns: minmax(0, 1.1fr) minmax(360px, 0.9fr);
  }
  /* FOUR, because a coverage widget in this column is only ~190px of content
     wide and that sentence runs to four lines in it. */
  .hp-hero-stats .hp-proof-cell-accuracy { --acc-label-lines: 4; }
}

@media (max-width: 480px) {
  /* One column on a phone. Two figures side by side in a ~140px cell is where
     the numbers start wrapping, and a wrapped headline figure is worse than a
     taller column nobody has to read all of. */
  .hp-hero-stats .hp-proof-grid,
  .hp-hero-stats .hp-scorecard-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- Below the fold -----------------------------------------------
   The description and the two footnotes, on a hairline that says the pitch
   above it is over. Each carries its own top margin from the rules that style
   it elsewhere; those are replaced here so the gaps are set in one place. */
.hp-hero-below {
  margin-top: clamp(30px, 4vw, 52px);
  padding-top: clamp(22px, 3vw, 34px);
  border-top: 1px solid var(--hp-line-soft);
}
.hp-hero-below .hp-sub { margin-top: 0; }
.hp-hero-below .hp-scorecard-foot,
.hp-hero-below .hp-proof-foot { margin-top: 16px; }
/* The scorecard's caveat paragraph is written by paintScorecard() and is empty
   until a payload lands. Inside the old banner that cost nothing, because the
   whole banner was hidden; out here on its own it would be an empty <p> holding
   16px of margin open above the other footnote. */
.hp-scorecard-foot:empty { display: none; }

/* Live status chip: pulsing dot + real coverage figures from /api/coverage. */
.hp-live {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 7px 14px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: var(--hp-card-veil);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  font-size: 13px;
  color: var(--hp-ink-soft);
}
.hp-live-dot {
  position: relative;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--hp-primary-bright);
  flex: none;
}
.hp-live-dot::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: inherit;
  animation: hp-ping 2s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes hp-ping {
  0% { transform: scale(1); opacity: 0.75; }
  75%, 100% { transform: scale(2.6); opacity: 0; }
}
.hp-live-sep { color: var(--hp-line); }

.hp-headline {
  margin: 26px 0 0;
  max-width: 17ch;
  font-family: var(--hp-display);
  font-weight: 600;
  font-size: clamp(38px, 6.6vw, 68px);
  line-height: 1.02;
  letter-spacing: -0.028em;
  text-wrap: balance;
}
.hp-headline em {
  font-style: italic;
  color: var(--hp-primary);
}
.hp-sub {
  margin: 26px 0 0;
  max-width: 64ch;
  font-size: clamp(16px, 1.4vw, 18px);
  line-height: 1.66;
  color: var(--hp-ink-soft);
}
.hp-sub b { color: var(--hp-ink); font-weight: 600; }

/* ---------- Teaser form (zip + trade -> /api/teaser) --------------------- */
.hp-teaser {
  margin-top: 36px;
  max-width: 760px;
  padding: 13px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  background: var(--hp-card-veil);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: var(--hp-shadow-lg);
}
.hp-teaser-row { display: flex; gap: 10px; align-items: stretch; }
@media (max-width: 720px) {
  .hp-teaser-row { flex-direction: column; }
}
.hp-field {
  display: flex;
  align-items: center;
  gap: 9px;
  flex: 1 1 0;
  min-width: 0;
  padding: 0 13px;
  border: 1px solid var(--hp-line);
  border-radius: 12px;
  background: var(--hp-card);
}
.hp-field:focus-within {
  border-color: var(--hp-primary);
  box-shadow: 0 0 0 3px var(--hp-primary-wash);
}
.hp-field svg { flex: none; color: var(--hp-primary); }
.hp-field input,
.hp-field select {
  flex: 1;
  min-width: 0;
  height: 50px;
  border: 0;
  background: transparent;
  color: var(--hp-ink);
  font: inherit;
  font-size: 15.5px;
  appearance: none;
  outline: none;
}
.hp-field select { cursor: pointer; padding-right: 18px; }
.hp-field option { background: var(--hp-card); color: var(--hp-ink); }
.hp-field-zip { flex: 0 1 220px; }
/* FLEX-BASIS FOLLOWS THE MAIN AXIS, AND AT 720px THE MAIN AXIS TURNS.
   The 220px basis above keeps the zip box narrow beside the trade select while
   the row is horizontal. Once .hp-teaser-row becomes `flex-direction: column`
   that same 220px stops meaning "220px wide" and starts meaning "220px TALL",
   which is why the zip box rendered as a huge slab on a phone while the select
   and the button stacked underneath it stayed a normal 52px.

   Placed HERE, after the rule it overrides, rather than up in the
   flex-direction block: a media query adds no specificity, so between two
   equally specific rules it is source order that decides, and an override
   written earlier in the file would simply lose. */
@media (max-width: 720px) {
  /* Sized by its own 50px input, like every other field in the row. */
  .hp-field-zip { flex: 1 1 auto; }
}
.hp-field-select { position: relative; }
.hp-field-select::after {
  /* Custom caret — the native one can't be themed on all platforms. */
  content: "";
  position: absolute;
  right: 14px;
  width: 8px;
  height: 8px;
  border-right: 1.6px solid var(--hp-muted);
  border-bottom: 1.6px solid var(--hp-muted);
  transform: translateY(-2px) rotate(45deg);
  pointer-events: none;
}
.hp-teaser .hp-btn { height: 52px; padding: 0 22px; font-size: 15.5px; }

/* ---------- Signed-in hero CTA -------------------------------------------
   Revealed by auth.js for a returning customer. Sized to match the teaser's
   submit button — 52px — because it stands in the same place in the hero and
   answers the same "what do I do here" question, just for someone who is past
   the funnel. */
.hp-signed-in { margin: 4px 0 26px; }
.hp-signed-in[hidden] { display: none; }
.hp-signed-in-cta {
  height: 52px;
  padding: 0 26px;
  font-size: 16px;
}
.hp-signed-in-note {
  margin: 10px 4px 0;
  font-size: 12.5px;
  color: var(--hp-muted);
}
.hp-teaser-note {
  margin: 11px 4px 2px;
  font-size: 12.5px;
  color: var(--hp-muted);
}

/* ---------- Teaser result card ------------------------------------------ */
.hp-result {
  margin-top: 18px;
  max-width: 760px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  background: var(--hp-card);
  box-shadow: var(--hp-shadow);
  overflow: hidden;
}
.hp-result-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px;
  border-bottom: 1px solid var(--hp-line-soft);
}
.hp-result-zip { font-size: 14px; color: var(--hp-muted); }
.hp-result-zip b { color: var(--hp-ink); font-family: var(--hp-mono); }
.hp-tag {
  font-family: var(--hp-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 4px 9px;
  border-radius: 999px;
  background: var(--hp-signal-wash);
  color: var(--hp-signal);
}
.hp-tag-warn { background: var(--hp-primary-wash); color: var(--hp-primary); }

.hp-result-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1px;
  background: var(--hp-line-soft);
}
.hp-result-stat { background: var(--hp-card); padding: 18px; }
.hp-result-stat .hp-num {
  display: block;
  font-size: 30px;
  font-weight: 700;
  line-height: 1.1;
}
.hp-result-stat .accent { color: var(--hp-primary); }
.hp-result-stat span.hp-stat-label {
  display: block;
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--hp-muted);
}
.hp-result-body { padding: 16px 18px; font-size: 13.5px; line-height: 1.6; color: var(--hp-ink-soft); }
.hp-result-body b { color: var(--hp-ink); }
.hp-result-proof {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin: 0 18px 16px;
  padding: 12px 14px;
  border: 1px solid var(--hp-signal-wash);
  border-radius: 12px;
  background: var(--hp-signal-wash);
  font-size: 13px;
  line-height: 1.55;
  color: var(--hp-ink-soft);
}
.hp-result-proof b { color: var(--hp-ink); }
.hp-result-proof .dot {
  flex: none;
  margin-top: 6px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--hp-signal);
}
.hp-result-cta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 16px 18px;
  border-top: 1px solid var(--hp-line-soft);
  background: var(--hp-primary-wash);
}
.hp-result-cta p { margin: 0; font-size: 12.5px; color: var(--hp-muted); }

.hp-note { padding: 20px; }
.hp-note-title { font-family: var(--hp-display); font-size: 19px; font-weight: 600; }
.hp-note-body { margin-top: 8px; font-size: 14px; line-height: 1.6; color: var(--hp-ink-soft); }
.hp-note-body b { color: var(--hp-ink); font-family: var(--hp-mono); }
.hp-note .hp-btn { margin-top: 14px; }

/* ===========================================================================
 * THE COVERAGE FIGURES — four real numbers pulled from the API
 *
 * These rules are the SHAPE OF A FIGURE — kicker, value, label — and they are
 * deliberately independent of where the figures sit. They currently sit in the
 * hero's right column, which restyles the grid around them into four separate
 * widgets (see .hp-hero-stats above); the band wrapper they used to live in
 * (.hp-proof, a full-width strip between the scorecard and the map) is gone
 * along with its section.
 *
 * .hp-proof-foot outlives the band too: it is the definition of the accuracy
 * figure, so it travelled with the figures into .hp-hero-below.
 * ======================================================================== */
.hp-proof-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: 1px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  overflow: hidden;
  background: var(--hp-line);
}
.hp-proof-cell { background: var(--hp-card); padding: 24px; }
.hp-proof-kicker {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-family: var(--hp-mono);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--hp-primary);
}
.hp-proof-kicker::before {
  content: "";
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: currentColor;
}
/* The three type metrics of a proof cell, named once so the accuracy cell's
   ticker can size its rows from the same numbers instead of hard-coding a
   height that then goes stale. See .hp-acc-window. */
.hp-proof-grid {
  --proof-value-size: clamp(30px, 3.6vw, 42px);
  --proof-value-gap: 14px;    /* kicker -> figure */
  --proof-label-size: 13px;
  --proof-label-leading: 1.55;
}
.hp-proof-value {
  margin: var(--proof-value-gap) 0 0;
  font-size: var(--proof-value-size);
  font-weight: 700;
  line-height: 1;
}
.hp-proof-label {
  margin: 12px 0 0;
  font-size: var(--proof-label-size);
  line-height: var(--proof-label-leading);
  color: var(--hp-muted);
}
.hp-proof-foot {
  margin: 26px 0 0;
  max-width: 78ch;
  font-size: 12px;
  line-height: 1.65;
  color: var(--hp-muted);
}

/* ===========================================================================
 * MAP SECTION — a live Leaflet choropleth on real tract data
 * ======================================================================== */
.hp-map-section { position: relative; padding: clamp(56px, 8vw, 100px) 0; }
.hp-map-head { max-width: 640px; margin: 0 auto clamp(26px, 4vw, 40px); text-align: center; }
.hp-map-head .hp-lede { margin-inline: auto; }

.hp-map-panel {
  border: 1px solid var(--hp-line);
  border-radius: 22px;
  overflow: hidden;
  background: var(--hp-card);
  box-shadow: var(--hp-shadow-lg);
}
.hp-map-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 13px;
  border-bottom: 1px solid var(--hp-line-soft);
}
.hp-control {
  display: flex;
  align-items: center;
  gap: 8px;
  position: relative;
  padding: 0 12px;
  border: 1px solid var(--hp-line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--hp-bg) 60%, transparent);
}
.hp-control > label {
  font-family: var(--hp-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
  white-space: nowrap;
}
.hp-control select {
  height: 40px;
  max-width: 210px;
  border: 0;
  background: transparent;
  color: var(--hp-ink);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
  appearance: none;
  padding-right: 16px;
  outline: none;
}
.hp-control select option { background: var(--hp-card); color: var(--hp-ink); }
.hp-control::after {
  content: "";
  position: absolute;
  right: 12px;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--hp-muted);
  border-bottom: 1.5px solid var(--hp-muted);
  transform: translateY(-2px) rotate(45deg);
  pointer-events: none;
}
/* ...but only on a control that actually holds a picker.
   The caret is drawn on .hp-control itself rather than on the <select> inside
   it, so every control wearing that class got one — including the map's zip
   field, which is a text input with a Go button and has nothing to drop down.
   A chevron on a box that does not open is a promise the control cannot keep,
   and it sat directly under the Go arrow, which made the pair read as two
   different affordances for the same field.

   Written as "no select, no caret" rather than as an exception naming
   .hp-control-zip, so a future control that is not a picker cannot inherit the
   arrow the same way this one did. */
.hp-control:not(:has(select))::after { content: none; }
.hp-map-controls .hp-btn { margin-left: auto; }
@media (max-width: 720px) {
  .hp-map-controls .hp-btn { margin-left: 0; width: 100%; }
}

.hp-map-stage { position: relative; }
.hp-map-canvas {
  height: clamp(340px, 52vh, 520px);
  width: 100%;
  background: var(--hp-bg-deep);
}
/* Leaflet paints its own panes; keep them under our overlays. */
.hp-map-canvas .leaflet-pane { z-index: 1; }
.hp-map-canvas .leaflet-top,
.hp-map-canvas .leaflet-bottom { z-index: 5; }

/* Floating readout: what the pointer is over, or the top area by default. */
.hp-map-readout {
  position: absolute;
  top: 14px;
  left: 14px;
  z-index: 6;
  min-width: 190px;
  max-width: 260px;
  padding: 12px 14px;
  border: 1px solid var(--hp-line);
  border-radius: 13px;
  background: color-mix(in srgb, var(--hp-card) 88%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: var(--hp-shadow);
  pointer-events: none;
}
.hp-readout-kicker {
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
.hp-readout-name {
  margin-top: 5px;
  font-family: var(--hp-display);
  font-size: 16px;
  font-weight: 600;
  line-height: 1.2;
}
.hp-readout-value {
  margin-top: 8px;
  font-size: 24px;
  font-weight: 700;
  color: var(--hp-primary);
}
.hp-readout-unit { margin-top: 3px; font-size: 11.5px; color: var(--hp-muted); }
/* The gate, restated on the readout: how much detail sits inside this area
   that the visitor can't see yet. */
.hp-readout-lock {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--hp-line-soft);
  font-size: 11.5px;
  color: var(--hp-muted);
}
.hp-readout-lock svg { flex: none; color: var(--hp-primary); }
/* The icon is a WRAPPER SPAN around the svg rather than the svg itself, because
   the padlock and its signed-in replacement (an arrow) are toggled with the
   `hidden` attribute — and setting .hidden on an SVG element does nothing, so
   there has to be an HTML element in between. That wrapper is now the flex
   item, so it, not the svg, carries the sizing and colour. */
.hp-readout-lock-icon {
  display: grid;
  place-items: center;
  flex: none;
  color: var(--hp-primary);
}

/* ---------- Under-map: state totals + the trial gate -------------------- */
.hp-map-foot { border-top: 1px solid var(--hp-line-soft); }
.hp-map-totals {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 1px;
  background: var(--hp-line-soft);
}
.hp-map-total { background: var(--hp-card); padding: 16px 18px; }
.hp-map-total .k {
  display: block;
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
.hp-map-total .v {
  display: block;
  margin-top: 6px;
  font-size: 22px;
  font-weight: 700;
}
.hp-map-total.accent .v { color: var(--hp-primary); }

.hp-map-gate {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-top: 1px solid var(--hp-line-soft);
  background: var(--hp-primary-wash);
}
.hp-gate-icon { display: grid; place-items: center; color: var(--hp-primary); flex: none; }
.hp-map-gate p {
  margin: 0;
  flex: 1 1 260px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--hp-ink-soft);
}
.hp-map-gate p b { color: var(--hp-ink); }

/* Legend: the same 5-class sequential ramp the map app uses. */
.hp-map-legend {
  position: absolute;
  bottom: 14px;
  left: 14px;
  z-index: 6;
  padding: 10px 12px;
  border: 1px solid var(--hp-line);
  border-radius: 11px;
  background: color-mix(in srgb, var(--hp-card) 88%, transparent);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  font-size: 11px;
  color: var(--hp-muted);
}
.hp-legend-bar { display: flex; height: 8px; width: 148px; border-radius: 4px; overflow: hidden; margin: 6px 0 5px; }
.hp-legend-bar span { flex: 1; }
.hp-legend-axis { display: flex; justify-content: space-between; font-family: var(--hp-mono); font-size: 10px; }

/* Overlay states: loading spinner and "no data here" message. */
.hp-map-overlay {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: grid;
  place-items: center;
  gap: 10px;
  text-align: center;
  padding: 24px;
  background: color-mix(in srgb, var(--hp-bg) 72%, transparent);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
  font-size: 14px;
  color: var(--hp-muted);
}

/* THE NOTE VARIANT — a remark about a map that HAS content.
 *
 * The blocking version above is right when the map is empty (loading, or a
 * failed request): there is nothing underneath worth seeing. It is wrong when
 * there is. "Outlines aren't published for this state" used to use it, so a
 * visitor who looked up their zip got their own outline drawn underneath a
 * full-bleed scrim — dimmed, blurred, and, because the base rule sets no
 * pointer-events, impossible to click, pan or zoom.
 *
 * As a note it pins itself to the bottom of the map, keeps its hands off the
 * pointer, and lets the map through. */
.hp-map-overlay.is-note {
  inset: auto 0 0 0;
  display: flex;
  justify-content: center;
  padding: 0 14px 14px;
  background: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  /* The map underneath is the point; this is a caption on it. */
  pointer-events: none;
}
.hp-map-overlay.is-note #hpMapOverlayText {
  max-width: min(520px, 100%);
  padding: 8px 14px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--hp-card) 92%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  font-size: 12.5px;
  line-height: 1.35;
  color: var(--hp-muted);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14);
}
/* The legend and the zoom chip are pinned bottom-left, so the note sits above
   them rather than across them. :has() on the stage rather than a sibling
   combinator because both of those elements come BEFORE the overlay in the
   markup, and `~` only looks forward. */
.hp-map-stage:has(.hp-map-overlay.is-note:not([hidden])) .hp-map-zoom-hint { display: none; }
.hp-map-overlay.is-note { padding-bottom: 14px; }
.hp-map-stage:has(.hp-map-legend:not([hidden])) .hp-map-overlay.is-note { padding-bottom: 60px; }

/* The retry offered only when the overlay is reporting OUR failure. */
.hp-map-retry {
  padding: 7px 16px;
  border: 1px solid var(--hp-line);
  border-radius: 9px;
  background: var(--hp-card);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  color: var(--hp-ink);
  cursor: pointer;
}
.hp-map-retry:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
.hp-map-retry[hidden] { display: none; }

/* "Click the map to zoom". Bottom-left, clear of the readout at the top-left,
   the legend at the bottom-right and the zip tile at the top-right. */
.hp-map-zoom-hint {
  position: absolute;
  left: 14px;
  bottom: 14px;
  z-index: 6;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--hp-card) 88%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--hp-muted);
  pointer-events: none;
}
.hp-map-zoom-hint[hidden] { display: none; }
.hp-map-zoom-hint svg { color: var(--hp-primary); }
.hp-spinner {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 2px solid var(--hp-line);
  border-top-color: var(--hp-primary);
  animation: hp-spin 0.8s linear infinite;
}
@keyframes hp-spin { to { transform: rotate(360deg); } }

/* Ranked strip under the map — the top areas in the current view. */
.hp-map-rank {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding: 13px;
  border-top: 1px solid var(--hp-line-soft);
  scrollbar-width: thin;
}
/* THE AREA NAMES WERE UNREADABLE IN DARK MODE, and the reason is worth
   stating because it is a trap this file can fall into again: a <button> does
   NOT inherit `color`. The UA stylesheet gives it `color: buttontext`, which
   is black. Every other colour on this chip was themed, so in light mode the
   omission was invisible — black text on a pale chip is what you wanted
   anyway — while in dark mode the ranked cities rendered near-black on a dark
   chip and could not be read at all.

   Set on the CHIP rather than on the `b`, so the value line and anything
   added later inherit it too. Every other <button> introduced since —
   .hp-map-back, .hp-zip-near-chip — carries the same explicit declaration for
   the same reason. */
.hp-rank-chip {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 9px 13px;
  border: 1px solid var(--hp-line);
  border-radius: 11px;
  background: color-mix(in srgb, var(--hp-bg) 55%, transparent);
  color: var(--hp-ink);
  font-size: 12.5px;
  cursor: pointer;
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.hp-rank-chip:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
/* The area the visitor has clicked, kept in sync with the map's selection. */
.hp-rank-chip.is-active {
  border-color: var(--hp-primary);
  background: var(--hp-primary-wash);
  color: var(--hp-primary);
}
.hp-rank-chip b { font-weight: 600; white-space: nowrap; }
.hp-rank-chip .hp-num { color: var(--hp-primary); font-size: 13px; }

/* ===========================================================================
 * HOW IT WORKS
 * ======================================================================== */
.hp-how { padding: clamp(56px, 8vw, 96px) 0; }
.hp-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin-top: 40px;
}
.hp-step {
  padding: 26px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  background: var(--hp-card-veil);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.hp-step-n {
  font-family: var(--hp-mono);
  font-size: 12px;
  color: var(--hp-muted);
}
.hp-step-icon { margin-top: 18px; color: var(--hp-primary); }
.hp-step h3 {
  margin: 14px 0 0;
  font-family: var(--hp-display);
  font-size: 20px;
  font-weight: 600;
}
.hp-step p { margin: 9px 0 0; font-size: 14px; line-height: 1.62; color: var(--hp-muted); }

/* ===========================================================================
 * TRACK RECORD — real rows from /api/predictions/best
 * ======================================================================== */
.hp-record { padding: clamp(20px, 4vw, 40px) 0 clamp(56px, 8vw, 96px); }
.hp-record-panel {
  margin-top: 30px;
  border: 1px solid var(--hp-line);
  border-radius: 22px;
  overflow: hidden;
  background: var(--hp-card);
  box-shadow: var(--hp-shadow);
}
.hp-record-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  padding: 13px;
  border-bottom: 1px solid var(--hp-line-soft);
}
.hp-record-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  margin-left: auto;
  font-size: 13px;
  color: var(--hp-muted);
}
.hp-record-summary .hp-num { font-weight: 600; color: var(--hp-ink); }
.hp-record-summary .signal { color: var(--hp-signal); }
@media (max-width: 720px) {
  .hp-record-summary { margin-left: 0; }
}

.hp-record-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(272px, 1fr));
  gap: 1px;
  background: var(--hp-line-soft);
}
.hp-record-card { background: var(--hp-card); }
/* An open card grows IN PLACE rather than spanning the grid. Spanning gave the
   map a wider frame and cost a hole: a full-width card cannot sit beside its
   neighbour, so it drops to its own row and leaves the rest of the previous one
   as a bare grey rectangle of grid background. A 272px column is a perfectly
   good frame for one tract outline, and the pairs and facts below it are
   auto-fit grids that simply fold to one column at that width. */
.hp-record-card.is-open { background: var(--hp-card-veil); }

/* THE WHOLE COLLAPSED CARD IS THE DISCLOSURE CONTROL. A <button> rather than a
   click handler on the <article>, so the keyboard and a screen reader get the
   same affordance a mouse does. The padding moved off .hp-record-card and onto
   this so the hit area is the whole card rather than an inset rectangle. */
.hp-record-toggle {
  display: block;
  width: 100%;
  padding: 18px;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.hp-record-toggle:hover { background: var(--hp-card-veil); }
/* Open, the summary pair and the verdict line are repeated in full below — and
   the same row twice inside 250px reads as a rendering bug rather than as
   emphasis. The head stays: it is what says which card is open. */
.hp-record-card.is-open .hp-record-toggle .hp-vs,
.hp-record-card.is-open .hp-record-toggle .hp-record-foot { display: none; }
.hp-record-toggle:focus-visible { outline: 2px solid var(--hp-primary); outline-offset: -2px; }

/* The affordance line. Quiet: the figures are the subject of the card and this
   is an invitation, not a call to action. */
.hp-record-more {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-top: 13px;
  font-size: 12px;
  font-weight: 600;
  color: var(--hp-primary);
}
.hp-record-more svg { transition: transform 0.18s var(--ease); }
.hp-record-card.is-open .hp-record-more svg { transform: rotate(180deg); }

.hp-record-detail { padding: 0 18px 18px; }
.hp-record-detail[hidden] { display: none; }
/* Map first, then the figures: the question the expanded card answers that the
   collapsed one couldn't is "where is this". */
.hp-record-map-wrap { position: relative; margin-bottom: 16px; }
.hp-record-map {
  height: 180px;
  border-radius: 12px;
  overflow: hidden;
  background: var(--hp-card-veil);
}
.hp-record-map-note {
  margin: 0;
  padding: 14px 0;
  font-size: 12.5px;
  color: var(--hp-muted);
}
/* Leaflet paints its own white ground; the tiles are themed, the container
   should not fight them. */
.hp-record-map .leaflet-container { background: var(--hp-card-veil); }
/* Open, the pairs get the card's full width — three across instead of stacked. */
.hp-vs-full {
  margin-top: 0;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.hp-record-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 12px 18px;
  margin: 16px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--hp-line-soft);
}
.hp-record-facts dt {
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
.hp-record-facts dd {
  margin: 3px 0 0;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--hp-ink);
}

.hp-record-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.hp-record-tract {
  font-family: var(--hp-mono);
  font-size: 13px;
  font-weight: 600;
}
/* Named tract: the card is headed by a neighborhood ("Sugar House"), not a
   geoid, so it takes the page's own face. Mono stays on the geoid fallback,
   where a run of digits genuinely reads better fixed-width. */
.hp-record-tract.is-name {
  font-family: inherit;
  font-size: 14px;
}
.hp-record-cat { margin-top: 3px; font-size: 12px; color: var(--hp-muted); }
.hp-hit {
  flex: none;
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: 999px;
  background: var(--hp-signal-wash);
  color: var(--hp-signal);
}
.hp-hit-rank { background: var(--hp-primary-wash); color: var(--hp-primary); }

/* Predicted-vs-actual pairs, the core of the proof. */
.hp-vs { margin-top: 14px; display: grid; gap: 10px; }
.hp-vs-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 8px;
  padding: 9px 11px;
  border: 1px solid var(--hp-line-soft);
  border-radius: 11px;
}
.hp-vs-cell .k {
  display: block;
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
.hp-vs-cell .v { display: block; margin-top: 3px; font-size: 15px; font-weight: 600; }
.hp-vs-cell.actual .v { color: var(--hp-signal); }
.hp-vs-arrow { color: var(--hp-muted); font-size: 13px; }
.hp-record-foot { margin-top: 13px; font-size: 12px; line-height: 1.55; color: var(--hp-muted); }
.hp-record-foot b { color: var(--hp-ink); }

.hp-record-empty { padding: 40px 20px; text-align: center; font-size: 14px; color: var(--hp-muted); }

/* ===========================================================================
 * TRADES STRIP
 * ======================================================================== */
.hp-trades { padding: 0 0 clamp(56px, 8vw, 96px); }
.hp-trade-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin-top: 38px;
}
.hp-trade-card {
  position: relative;
  aspect-ratio: 4 / 5;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  overflow: hidden;
  isolation: isolate;
}
.hp-trade-card img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.5s var(--ease-out);
}
.hp-trade-card:hover img { transform: scale(1.05); }
.hp-trade-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    var(--hp-bg) 4%,
    color-mix(in srgb, var(--hp-bg) 55%, transparent) 42%,
    transparent 78%
  );
}
.hp-trade-copy { position: absolute; inset: auto 0 0 0; z-index: 1; padding: 22px; }
.hp-trade-copy h3 {
  margin: 0;
  font-family: var(--hp-display);
  font-size: 24px;
  font-weight: 700;
}
.hp-trade-copy p { margin: 8px 0 0; font-size: 13.5px; line-height: 1.55; color: var(--hp-ink-soft); }

/* ===========================================================================
 * ACCESS TIERS
 * ======================================================================== */
.hp-tiers {
  border-block: 1px solid var(--hp-line-soft);
  background: color-mix(in srgb, var(--hp-bg-deep) 78%, transparent);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  padding: clamp(56px, 8vw, 96px) 0;
}
.hp-tier-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
  gap: 18px;
  margin-top: 40px;
}
.hp-tier {
  padding: 24px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  background: var(--hp-card);
}
.hp-tier-signal { border-color: color-mix(in srgb, var(--hp-signal) 34%, transparent); background: var(--hp-signal-wash); }
.hp-tier-primary { border-color: color-mix(in srgb, var(--hp-primary) 34%, transparent); background: var(--hp-primary-wash); }
.hp-tier-badge {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--hp-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
.hp-tier-signal .hp-tier-badge { color: var(--hp-signal); }
.hp-tier-primary .hp-tier-badge { color: var(--hp-primary); }
.hp-tier h3 { margin: 16px 0 0; font-family: var(--hp-display); font-size: 20px; font-weight: 600; }
.hp-tier ul { margin: 16px 0 0; padding: 0; list-style: none; display: grid; gap: 10px; }
.hp-tier li {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: 9px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--hp-ink-soft);
}
.hp-tier li svg { margin-top: 2px; color: var(--hp-muted); }
.hp-tier-signal li svg { color: var(--hp-signal); }
.hp-tier-primary li svg { color: var(--hp-primary); }
.hp-tiers .hp-btn { margin-top: 34px; }

/* ===========================================================================
 * FINAL CTA
 * ======================================================================== */
.hp-final { padding: clamp(56px, 8vw, 96px) 0; }
.hp-final-card {
  position: relative;
  overflow: hidden;
  padding: clamp(28px, 5vw, 56px);
  border: 1px solid var(--hp-line);
  border-radius: 26px;
  background: linear-gradient(135deg, var(--hp-card), color-mix(in srgb, var(--hp-bg-deep) 88%, transparent));
  box-shadow: var(--hp-shadow-lg);
}
.hp-final-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 82% 18%, var(--hp-primary-wash), transparent 46%);
  pointer-events: none;
}
.hp-final-card > * { position: relative; }
.hp-final-card h2 {
  margin: 0;
  max-width: 20ch;
  font-family: var(--hp-display);
  font-size: clamp(26px, 3.6vw, 38px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.hp-final-card > p { margin: 14px 0 0; max-width: 52ch; color: var(--hp-muted); font-size: 15px; line-height: 1.6; }
.hp-final .hp-teaser { margin-top: 28px; }

/* ===========================================================================
 * FOOTER
 * ======================================================================== */
.hp-footer {
  border-top: 1px solid var(--hp-line-soft);
  background: color-mix(in srgb, var(--hp-bg-deep) 88%, transparent);
  padding: clamp(40px, 6vw, 64px) 0 32px;
}
.hp-foot-segments { margin-bottom: 40px; }
.hp-foot-segments .hp-eyebrow { color: var(--hp-muted); }
.hp-seg-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
.hp-seg-row a {
  padding: 8px 15px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: var(--hp-card);
  font-size: 13.5px;
  color: var(--hp-ink);
  text-decoration: none;
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.hp-seg-row a:hover { border-color: var(--hp-primary); color: var(--hp-primary); }

.hp-foot-main {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 34px;
  padding-top: 34px;
  border-top: 1px solid var(--hp-line-soft);
}
.hp-foot-about { max-width: 34ch; }
.hp-foot-about p { margin: 14px 0 0; font-size: 13.5px; line-height: 1.6; color: var(--hp-muted); }
.hp-foot-cols { display: flex; flex-wrap: wrap; gap: 40px; }
.hp-foot-col p { margin: 0 0 12px; font-size: 13.5px; font-weight: 600; }
.hp-foot-col ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 9px; }
.hp-foot-col li, .hp-foot-col a {
  font-size: 13.5px;
  color: var(--hp-muted);
  text-decoration: none;
}
.hp-foot-col a:hover { color: var(--hp-ink); }
.hp-foot-legal {
  margin: 34px 0 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--hp-muted);
  opacity: 0.85;
}

/* ---------- Leaflet tooltip skin (scoped to the home map) ---------------- */
.hp-tract-tooltip.leaflet-tooltip {
  border: 1px solid var(--hp-line);
  border-radius: 9px;
  background: var(--hp-card);
  color: var(--hp-ink);
  font-family: var(--hp-sans);
  font-size: 12.5px;
  box-shadow: var(--hp-shadow);
  padding: 7px 10px;
}
.hp-tract-tooltip.leaflet-tooltip::before { display: none; }

/* ---------- Reduced motion ---------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .hp *, .hp *::before, .hp *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* ==========================================================================
   Trades carousel
   ==========================================================================
   Replaces the three static cards. One card per work type, in a horizontally
   scrollable track with snap points, so the full list is reachable by touch,
   trackpad, arrow buttons and keyboard alike. */
.hp-carousel {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
}
.hp-trade-track {
  display: flex;
  gap: 18px;
  flex: 1 1 auto;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Room for the cards' hover lift and focus ring, which would otherwise be
     clipped by the scroll container. */
  padding: 6px 2px 14px;
  scrollbar-width: thin;
}
.hp-trade-track::-webkit-scrollbar { height: 6px; }
.hp-trade-track::-webkit-scrollbar-thumb {
  background: var(--line);
  border-radius: 999px;
}

.hp-trade-track .hp-trade-card {
  position: relative;
  flex: 0 0 258px;
  scroll-snap-align: start;
  overflow: hidden;
  border-radius: 14px;
  border: 1px solid var(--line);
  background: var(--surface);
  /* The WHOLE card is the target, not just the button that fades in over the
     photo — see the click handler in initTradeCarousel(). The cursor has to say
     so, otherwise the card lifts and glows on hover while still looking like
     something you cannot click. */
  cursor: pointer;
  transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}
.hp-trade-track .hp-trade-card:hover,
.hp-trade-track .hp-trade-card:focus-within,
.hp-trade-track .hp-trade-card:focus-visible {
  transform: translateY(-3px);
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.16);
  border-color: var(--accent, #f5a623);
  outline: none;
}

.hp-trade-media {
  position: relative;
  height: 132px;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.hp-trade-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The fallback for a trade with no photograph: its initials, in the brand
   palette. Better than a stock photo of the wrong job. */
.hp-trade-mono {
  font-family: var(--font-display, inherit);
  font-size: 40px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--accent, #f5a623);
  opacity: 0.55;
}

/* The per-card CTA. Hidden until the card is hovered or focused, so the grid
   reads as a set of trades rather than as a wall of buttons — but it is the
   whole point of the card, so it appears over the image where the eye already
   is, not below the copy. */
.hp-trade-cta {
  position: absolute;
  left: 50%;
  top: 66px;
  transform: translate(-50%, 6px);
  opacity: 0;
  pointer-events: none;
  white-space: nowrap;
  transition: opacity 0.16s ease, transform 0.16s ease;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
}
.hp-trade-track .hp-trade-card:hover .hp-trade-cta,
.hp-trade-track .hp-trade-card:focus-within .hp-trade-cta,
.hp-trade-track .hp-trade-card:focus-visible .hp-trade-cta {
  opacity: 1;
  pointer-events: auto;
  transform: translate(-50%, 0);
}
/* Darken the photo behind the revealed CTA so the button stays legible over
   whatever the image happens to be. */
.hp-trade-track .hp-trade-card:hover .hp-trade-media::after,
.hp-trade-track .hp-trade-card:focus-within .hp-trade-media::after,
.hp-trade-track .hp-trade-card:focus-visible .hp-trade-media::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.32);
}

.hp-carousel-nav {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: opacity 0.14s, border-color 0.14s;
}
.hp-carousel-nav:hover:not(:disabled) { border-color: var(--accent, #f5a623); }
.hp-carousel-nav:disabled { opacity: 0.3; cursor: default; }

@media (max-width: 640px) {
  /* On a phone the track is already swipeable; the arrows are dead weight. */
  .hp-carousel-nav { display: none; }
  .hp-trade-track .hp-trade-card { flex-basis: 78%; }
  /* No hover on touch, so the CTA is simply always visible there. */
  .hp-trade-cta { opacity: 1; pointer-events: auto; transform: translate(-50%, 0); }
  .hp-trade-media::after { content: ''; position: absolute; inset: 0; background: rgba(0, 0, 0, 0.3); }
}

/* ==========================================================================
   Your-zip tile, inside the coverage map
   ========================================================================== */
.hp-zip-tile {
  position: absolute;
  /* 8, not 700. .hp-map-stage is position:relative with z-index:auto, so it
     establishes NO stacking context — a value here competes directly with the
     sticky nav (z-index 40) rather than being scoped to the map, and 700 meant
     the tile slid over the header on scroll.
     8 puts it one above .hp-map-overlay (7), whose full-bleed scrim would
     otherwise wash the tile out whenever the chosen state has no coverage
     loaded — and still far below the nav. */
  z-index: 8;
  top: 14px;
  /* THE RIGHT-HAND CORNER, not the left one.
     This was pinned at left:14px — the same coordinates as .hp-map-readout, the
     hover tile that names the area under the pointer. The tile is z-index 8 and
     the readout is 6, so looking up a ZIP silently covered the readout rather
     than moving it aside, and hover-to-inspect stopped working with no
     explanation. They are two different answers to two different questions and
     they can both be on screen. */
  right: 14px;
  left: auto;
  width: min(300px, calc(100% - 28px));
  /* A CEILING AND ITS OWN SCROLL. Populated, this tile carries a head, two or
     three stats, a scope note, three tract rows, the property-preview list and
     a CTA — inside a canvas that floors at clamp(340px, 52vh, 520px). On a
     short viewport it ran past the bottom of the map and over the ranked-areas
     strip below it. */
  max-height: calc(100% - 28px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 13px 14px 14px;
  border-radius: 13px;
  border: 1px solid var(--line);
  background: var(--surface);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
/* Below the width where the two tiles could sit side by side, the zip tile is
   the one the visitor asked for, so it takes the corner and the readout gives
   way rather than being covered by it. */
@media (max-width: 760px) {
  .hp-zip-tile:not([hidden]) ~ .hp-map-readout,
  .hp-map-stage:has(.hp-zip-tile:not([hidden])) .hp-map-readout { display: none; }
}
.hp-zip-tile[hidden] { display: none; }

.hp-zip-close {
  position: absolute;
  top: 6px;
  right: 8px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.hp-zip-close:hover { color: var(--ink); }

.hp-zip-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 10px;
}
.hp-zip-badge {
  padding: 2px 8px;
  border-radius: 6px;
  background: var(--accent, #f5a623);
  color: #1b1300;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.5px;
}
.hp-zip-place {
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hp-zip-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 9px 12px;
}
.hp-zip-stat { display: flex; flex-direction: column; }
.hp-zip-stat-value { font-size: 17px; font-weight: 700; color: var(--ink); }
.hp-zip-stat-label {
  font-size: 10px;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  color: var(--muted);
}

.hp-zip-tracts { margin-top: 12px; }
.hp-zip-tracts-head {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 5px;
}
.hp-zip-scope { text-transform: none; letter-spacing: 0; font-weight: 500; }
.hp-zip-tract {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: 12px;
  border-top: 1px solid var(--line);
}
.hp-zip-tract-rank {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent, #f5a623);
  color: #1b1300;
  font-size: 10px;
  font-weight: 700;
}
.hp-zip-tract-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--ink);
}
.hp-zip-tract-value { flex: 0 0 auto; font-weight: 700; color: var(--ink); }

/* FULL WIDTH, STILL FLEX. `display: block` was what this had — which is how it
   got its own line, and also how it lost the `align-items: center` that .hp-btn
   uses to centre its label inside its fixed height. The text sat on the button's
   text baseline near the top of a 36px box. `display: flex` gives the same
   block-level, full-width behaviour and keeps the centring. */
.hp-zip-cta { display: flex; margin-top: 12px; }

/* Numbered pins for the three tract predictions, matching the tile's list. */
.hp-zip-pin span {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent, #f5a623);
  color: #1b1300;
  border: 2px solid var(--surface, #fff);
  font-size: 11px;
  font-weight: 700;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}

/* The one free property preview, inside the zip tile. */
.hp-zip-preview-btn {
  display: block;
  width: 100%;
  margin-top: 12px;
  padding: 8px 10px;
  border: 1px dashed var(--accent, #f5a623);
  border-radius: 9px;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.14s;
}
.hp-zip-preview-btn:hover:not(:disabled) {
  background: color-mix(in srgb, var(--accent, #f5a623) 12%, transparent);
}
.hp-zip-preview-btn:disabled { opacity: 0.7; cursor: default; }

.hp-zip-preview { margin-top: 11px; }
.hp-zip-preview[hidden] { display: none; }
.hp-zip-preview-head {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 5px;
}
.hp-zip-preview-row {
  padding: 5px 0;
  border-top: 1px solid var(--line);
}
.hp-zip-preview-addr { font-size: 12px; font-weight: 600; color: var(--ink); }
.hp-zip-preview-why { font-size: 11px; color: var(--muted); }
.hp-zip-preview-note {
  margin-top: 7px;
  font-size: 11px;
  color: var(--muted);
  font-style: italic;
}

/* Caption under the zip tile's figures: what they're summed over. */
.hp-zip-scope {
  margin: 8px 0 0;
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--muted);
}
.hp-zip-scope[hidden] { display: none; }

/* ===========================================================================
 * Rotating accuracy figure
 * ========================================================================
 * A one-row window with a taller track sliding through it. The window's height
 * IS the row height — every row is sized to fill it exactly — so exactly one
 * row is readable at a time.
 *
 * --acc-row-h IS THE CONTRACT WITH THE JS. initAccuracyRotation() in home.js
 * advances the track with `translateY(calc(var(--acc-row-h) * -N))` rather than
 * measuring the window in pixels, because this height is fractional and
 * clientHeight is not: rounding it once per step is what used to walk the
 * ticker slowly out of alignment down the list, only for the wrap back to the
 * first city to snap it clean again. Keep the row height and the window height
 * expressed as this one variable — the moment they are allowed to disagree the
 * slide stops landing on a row boundary. */
/* The rows still have to be a FIXED, uniform height — the ticker slides by
   exactly one row — but the figure was picked by eye and came up short:

     figure  clamp(30px, 3.6vw, 42px), line-height 1   -> up to 42px
     gap     2px
     label   2 lines x 13px x 1.55                     -> 40.3px
                                                        = 84.3px, in 76px

   ...so the second line of the label was sliced through the middle, which is
   what the accuracy cell was doing on wide screens. The height is now DERIVED
   from those three numbers, all of which live on .hp-proof-grid next to the
   rules that use them, so changing the type cannot silently re-break this. */
.hp-proof-cell-accuracy {
  overflow: hidden;
  /* THREE, not two. The grid is auto-fit minmax(210px, 1fr), so the column is
     ~190px of content at four-across on a 1024px screen — and "median accuracy
     across San Bernardino, CA's 642 top calls." is three lines there even
     though it is two on a 1440px screen. Two lines was sized for the widest
     case and clipped every narrower one.

     A cell that needs only two lines carries ~20px of slack at the bottom
     instead. That costs nothing visible: the proof cells are top-aligned inside
     a grid that already stretches every cell to the tallest in its row. */
  --acc-label-lines: 3;
  --acc-figure-gap: 4px;
  --acc-row-h: calc(
    var(--proof-value-size)
    + var(--acc-figure-gap)
    + var(--acc-label-lines) * var(--proof-label-size) * var(--proof-label-leading)
  );
}

.hp-acc-window {
  position: relative;
  overflow: hidden;
  height: var(--acc-row-h);
  /* The other three cells put --proof-value-gap between the kicker and the
     figure via .hp-proof-value's own margin. Inside the ticker that margin is
     zeroed (rows must butt together for the slide to be seamless), so the gap
     moves out here — otherwise the accuracy figure floats 14px above the three
     numbers it is meant to sit in line with. */
  margin-top: var(--proof-value-gap);
}
.hp-acc-track {
  will-change: transform;
}
.hp-acc-row {
  height: var(--acc-row-h);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}
/* Inside the ticker the label has a fixed two lines to work with, so a long
   metro name clips rather than pushing the next row out of alignment. */
.hp-acc-row .hp-proof-label {
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  /* The SAME line count the row height is computed from, so the clamp and the
     box it lives in can never disagree — which is the disagreement that was
     cutting the last line in half. */
  -webkit-line-clamp: var(--acc-label-lines);
  -webkit-box-orient: vertical;
  line-clamp: var(--acc-label-lines);
}
.hp-acc-row .hp-proof-value { margin: 0 0 var(--acc-figure-gap); }

@media (max-width: 720px) {
  /* Below this the grid drops to two columns, which are WIDER than the
     four-across columns above it — but the value clamp is also at its 3.6vw
     floor here, so a fourth line is cheap insurance for a long metro name.
     The height follows from the line count rather than being a second
     hard-coded figure that has to be kept in step with the first. */
  .hp-proof-cell-accuracy { --acc-label-lines: 4; }
}

/* ===========================================================================
 * SCORECARD BANNER — "what we forecast, and how the last one landed"
 *
 * One block that carries the NATIONWIDE figures until a visitor looks up a
 * zip, then the same block carries that zip's. See the markup's comment for
 * why it is one block rather than two.
 *
 * IT LIVES IN THE HERO'S RIGHT COLUMN NOW, above the coverage figures, and the
 * rules that put it there are with the rest of the hero (.hp-hero-stats). What
 * is left here is the block's own anatomy — the cells, their type, the accuracy
 * bars — which is independent of where the block sits, and the full-width
 * defaults it still falls back on if it is ever placed anywhere else.
 *
 * It deliberately does NOT reuse .hp-proof-grid's cell structure. The coverage
 * figures are single standalone numbers; these are PAIRS a reader is meant to
 * compare — forecast against actual — and they need a cell that holds a figure,
 * its unit and a comparison line. Borrowing the coverage cell would have made
 * "165 homes" and "183 homes" look like two unrelated statistics sitting next
 * to each other. (The two grids do share a CARD FRAME in the hero column, so
 * the eight read as one set; that is a surface, not a structure.)
 * ======================================================================== */
.hp-scorecard {
  position: relative;
  z-index: 1;
}
.hp-scorecard[hidden] { display: none; }

.hp-scorecard-head { max-width: 720px; margin: 0 auto clamp(20px, 3vw, 30px); text-align: center; }
.hp-scorecard-title { margin: 10px 0 0; }
.hp-scorecard-sub { margin: 12px auto 0; }

.hp-scorecard-grid {
  display: grid;
  /* Wider minimum than the proof band's 210px: every cell here carries a
     figure AND a comparison line under it, and at 210px the comparison
     wrapped to three lines and stopped reading as a caption. */
  grid-template-columns: repeat(auto-fit, minmax(238px, 1fr));
  gap: 1px;
  border: 1px solid var(--hp-line);
  border-radius: 18px;
  overflow: hidden;
  background: var(--hp-line);
}
.hp-score-cell {
  background: var(--hp-card);
  padding: 20px 22px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* The cell that reports what ACTUALLY happened, as opposed to what we said
   would happen. Teal is the page's existing "verified / really occurred"
   signal (--hp-signal, used by the track-record cards), so the colour is
   already carrying that meaning for anyone who has scrolled the page. */
.hp-score-cell.is-actual .hp-score-value { color: var(--hp-signal); }
.hp-score-cell.is-actual .hp-score-kicker { color: var(--hp-signal); }

.hp-score-kicker {
  margin: 0;
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hp-primary);
}
.hp-score-value {
  margin: 2px 0 0;
  font-size: clamp(25px, 2.9vw, 33px);
  font-weight: 700;
  line-height: 1.05;
  color: var(--hp-ink);
}
/* The unit, spelled out. The whole point of requirement "say what each number
   is": "1,240" alone is not a fact, "1,240 homes forecast" is. */
.hp-score-unit {
  margin: 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--hp-ink-soft);
}
.hp-score-note {
  margin: 2px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--hp-muted);
}
/* A second figure inside one cell — the dollar line under a homes figure.
   Kept visually subordinate because dollars are only reported for the metros
   that publish valuations, and it must not read as the headline. */
.hp-score-sub {
  margin: 6px 0 0;
  padding-top: 8px;
  border-top: 1px dashed var(--hp-line);
  font-size: 13px;
  color: var(--hp-ink-soft);
}
.hp-score-sub b { color: var(--hp-ink); font-weight: 700; }

/* The accuracy cell's three axes, as a compact row of labelled bars. */
.hp-score-axes { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
.hp-score-axis { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; align-items: center; }
.hp-score-axis-label { font-size: 11.5px; color: var(--hp-muted); }
.hp-score-axis-value { font-size: 13px; font-weight: 700; color: var(--hp-ink); }
.hp-score-axis-bar {
  grid-column: 1 / -1;
  height: 4px;
  border-radius: 2px;
  background: var(--hp-line);
  overflow: hidden;
}
.hp-score-axis-bar > span {
  display: block;
  height: 100%;
  border-radius: 2px;
  background: var(--hp-primary-bright);
}

/* ---- THE INFO AFFORDANCE, and the popover it opens -----------------------

   The scorecard's caveats used to be three paragraphs under the hero. They now
   hang off the figures they qualify, behind one of these. See the block comment
   on infoBtn() in home.js for why they moved.

   The button is deliberately quiet: it sits on a kicker or an axis label, both
   of which are already the smallest type on the card, and an affordance that
   out-shouted the figure it annotates would be worse than the footnote it
   replaced. It earns its contrast on hover, focus and while open.

   THE HIT TARGET IS NOT THE CIRCLE. 13px is a fine size to look at and a poor
   one to hit with a thumb, so ::after throws an invisible ~41px pad around it.
   That pad is the reason for `position: relative` here. */
.hp-info {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  margin: 0 0 0 5px;
  padding: 0;
  width: 13px;
  height: 13px;
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  border: 1px solid currentColor;
  border-radius: 50%;
  background: none;
  color: inherit;
  opacity: 0.5;
  cursor: pointer;
  /* The label this sits on may be uppercase, tracked-out mono. None of that
     should reach the glyph, which is a lowercase serif-ish 'i' at 9px. */
  font: 700 9px/1 var(--hp-mono);
  letter-spacing: 0;
  text-transform: none;
  transition: opacity 120ms ease, background-color 120ms ease,
              border-color 120ms ease, color 120ms ease;
}
.hp-info::after {
  content: '';
  position: absolute;
  inset: -14px;
}
.hp-info:hover { opacity: 1; }
.hp-info:focus-visible {
  opacity: 1;
  outline: 2px solid var(--hp-primary);
  outline-offset: 3px;
}
/* Open: filled, so a reader scanning a card with four of them can see which
   one the visible text belongs to. */
.hp-info[aria-expanded="true"] {
  opacity: 1;
  background: var(--hp-primary);
  border-color: var(--hp-primary);
  color: var(--hp-card);
}

/* WHY THE ICON HANGS OFF THE UNIT LINE AND NOT THE KICKER.

   The kicker was the obvious place and it is the one place it cannot go. On the
   middle pair it reads "Actually happened · July 2026", which the rule above
   (.hp-hero-stats .hp-score-kicker) tightened specifically to hold ONE line —
   and measured in the hero column it clears that line by 16px. An icon needs
   about 16px. Inline, it wrapped the kicker and dropped that cell's figure 12px
   below the figure it is meant to be compared against, which is exactly the
   misalignment the tightening exists to prevent. Absolutely positioning it in
   the corner does not help either: the text already runs to within 16px of the
   cell's edge, so the icon would sit on top of "2026".

   The unit line has the room the kicker does not. .hp-score-unit already
   reserves two lines on the paired cells (min-height: 2.4em, see above) so the
   two cards stay in step, and neither unit string fills the second line — so
   the icon lands in space that is already paid for and CANNOT change a cell's
   height. It also reads better there: "homes that did (i)" annotates the thing
   the figure counts, which is what the caveat is usually about.

   The accuracy cell keeps its icon on the kicker, where it has ~330px of slack,
   and the axis labels keep theirs inline. */

/* The popover is a child of <body> (see initInfoPopovers) and is positioned in
   viewport coordinates by JS, so it inherits nothing from the card it belongs
   to — every property it needs is set here rather than left to cascade. */
.hp-info-pop {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1200;                 /* the page's highest is 60, the sticky header */
  width: min(304px, calc(100vw - 24px));
  margin: 0;
  padding: 12px 14px;
  border: 1px solid var(--hp-line);
  border-radius: 12px;
  /* Opaque, NOT the frosted --hp-card-veil the cells use: this lands on top of
     other text as often as not, and a translucent panel over type is unreadable
     in exactly the case a caveat most needs to be read. */
  background: var(--hp-card);
  box-shadow: var(--hp-shadow);
  color: var(--hp-muted);
  font-family: var(--hp-sans);
  font-size: 12.5px;
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: normal;
  text-transform: none;
  text-align: left;
}
.hp-info-pop[hidden] { display: none; }
/* The copy bolds the part that carries the qualification — a metro count, a
   coverage percentage, "weaker number" — and that emphasis is the whole reason
   the sentence is worth reading, so it gets full-contrast ink. */
.hp-info-pop b { color: var(--hp-ink); font-weight: 700; }
/* One <p> per caveat — see infoBtn(). The gap is what separates "here is what
   this figure measures" from "and here is what qualifies it", which is the
   distinction a single run-on paragraph loses. */
.hp-info-pop p { margin: 0 0 9px; }
.hp-info-pop p:last-child { margin-bottom: 0; }

@media (prefers-reduced-motion: reduce) {
  .hp-info { transition: none; }
}

.hp-scorecard-foot {
  margin: 16px 0 0;
  max-width: 82ch;
  font-size: 12px;
  line-height: 1.65;
  color: var(--hp-muted);
}

/* ===========================================================================
 * MAP: the nationwide/state view controls
 * ======================================================================== */

/* "All states". Shown only in the state view — see the markup's comment. */
.hp-map-back {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 13px;
  border: 1px solid var(--hp-line);
  border-radius: 10px;
  background: color-mix(in srgb, var(--hp-bg) 55%, transparent);
  /* EXPLICIT, like every other <button> on this page. A button does not
     inherit `color` — see the note on .hp-rank-chip below, which is the same
     bug this line pre-empts. */
  color: var(--hp-ink);
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.hp-map-back:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
.hp-map-back[hidden] { display: none; }

/* The zip field that lives in the map's own control bar. Shares .hp-control's
   pill shape so it reads as one of the map's controls rather than as a form
   that wandered down from the hero. */
.hp-control-zip { gap: 6px; }
.hp-control-zip input {
  width: 84px;
  border: 0;
  background: transparent;
  color: var(--hp-ink);
  font: inherit;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  padding: 8px 0;
  outline: none;
}
.hp-control-zip input::placeholder { color: var(--hp-muted); }
.hp-zip-go {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 7px;
  background: var(--hp-primary);
  color: var(--hp-primary-ink);
  cursor: pointer;
}
.hp-zip-go:hover { background: var(--hp-primary-bright); }

/* A state polygon's label in the nationwide view. A Leaflet divIcon, so it has
   no background of its own — the halo is what keeps it readable over both the
   light and dark basemaps. */
.hp-state-label {
  font-family: var(--hp-sans);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--hp-ink);
  text-shadow:
    0 0 3px var(--hp-card), 0 0 3px var(--hp-card),
    0 0 6px var(--hp-card), 0 0 6px var(--hp-card);
  white-space: nowrap;
  pointer-events: none;
}

/* ===========================================================================
 * MAP: last month's figures for the clicked area
 * ======================================================================== */
.hp-area-detail {
  padding: 15px 13px 0;
  border-top: 1px solid var(--hp-line-soft);
}
.hp-area-detail[hidden] { display: none; }
.hp-area-detail-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 12px;
}
.hp-area-detail-name { font-size: 15px; font-weight: 700; color: var(--hp-ink); }
.hp-area-detail-period {
  font-family: var(--hp-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
}
/* The reliability flag the scorer sets. A city-month scored over a handful of
   active tracts gets its figures shown WITH this caveat rather than hidden —
   the counts are real — but a reader must not quote it as a track record. */
.hp-area-thin {
  padding: 2px 8px;
  border: 1px dashed var(--hp-line);
  border-radius: 999px;
  font-size: 10.5px;
  color: var(--hp-muted);
}
.hp-area-rows {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
}
.hp-area-row {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 11px 13px;
  border: 1px solid var(--hp-line-soft);
  border-radius: 11px;
  background: color-mix(in srgb, var(--hp-bg) 45%, transparent);
}
.hp-area-row-label {
  font-size: 11px;
  letter-spacing: 0.02em;
  color: var(--hp-muted);
}
.hp-area-row-pair { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.hp-area-pred { font-size: 16px; font-weight: 700; color: var(--hp-ink); }
.hp-area-vs { font-size: 11px; color: var(--hp-muted); }
/* Teal again: this is the figure that actually occurred. */
.hp-area-actual { font-size: 16px; font-weight: 700; color: var(--hp-signal); }
.hp-area-detail-foot {
  margin: 12px 0 0;
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--hp-muted);
}

/* ===========================================================================
 * ZIP TILE: last month, and the neighbouring zips
 * ======================================================================== */
.hp-zip-last {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--hp-line-soft);
}
.hp-zip-last[hidden] { display: none; }
.hp-zip-last-head {
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
  margin-bottom: 7px;
}
.hp-zip-last-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
  padding: 3px 0;
}
.hp-zip-last-k { color: var(--hp-muted); }
.hp-zip-last-v { font-weight: 700; color: var(--hp-ink); font-variant-numeric: tabular-nums; }
.hp-zip-last-v.is-actual { color: var(--hp-signal); }
.hp-zip-last-note { margin: 7px 0 0; font-size: 11px; line-height: 1.5; color: var(--hp-muted); }

.hp-zip-nearby { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--hp-line-soft); }
.hp-zip-nearby[hidden] { display: none; }
.hp-zip-nearby-head {
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-muted);
  margin-bottom: 7px;
}
.hp-zip-nearby-list { display: flex; flex-wrap: wrap; gap: 6px; }
.hp-zip-near-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 5px 9px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: color-mix(in srgb, var(--hp-bg) 55%, transparent);
  /* Explicit, for the same reason as .hp-rank-chip below. */
  color: var(--hp-ink);
  font: inherit;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: border-color 0.15s var(--ease), color 0.15s var(--ease);
}
.hp-zip-near-chip:hover { border-color: var(--hp-primary); color: var(--hp-primary); }
.hp-zip-near-chip .dist { font-size: 10.5px; color: var(--hp-muted); }
.hp-zip-near-chip:hover .dist { color: inherit; }

/* A neighbouring zip's pin on the map. Smaller and quieter than the numbered
   tract pins — these are navigation, not findings. */
.hp-zip-near-pin {
  display: flex;
  align-items: center;
  justify-content: center;
  transform: translate(-50%, -50%);
  padding: 2px 7px;
  border: 1px solid var(--hp-line);
  border-radius: 999px;
  background: var(--hp-card);
  color: var(--hp-ink);
  font-family: var(--hp-sans);
  font-size: 10.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  cursor: pointer;
  box-shadow: var(--hp-shadow);
}
.hp-zip-near-pin:hover { border-color: var(--hp-primary); color: var(--hp-primary); }

/* ===========================================================================
 * ZIP TILE: the property preview, which now opens on ONE property
 *
 * The old flow dropped five addresses at once, which is a list to skim rather
 * than a claim to evaluate. Leading with the single best property and the
 * plain-English reasons it won gives a visitor one thing to judge — and the
 * "show me the other N" button then makes the size of what is being withheld
 * concrete, which a paragraph about the trial cannot.
 * ======================================================================== */
.hp-zip-top-prop {
  margin-top: 8px;
  padding: 12px 13px;
  border: 1px solid var(--hp-primary);
  border-radius: 12px;
  background: var(--hp-primary-wash);
}
.hp-zip-top-kicker {
  font-family: var(--hp-mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--hp-primary);
  margin-bottom: 5px;
}
.hp-zip-top-addr { font-size: 14px; font-weight: 700; color: var(--hp-ink); }
.hp-zip-top-why { margin: 7px 0 0; font-size: 12px; line-height: 1.6; color: var(--hp-ink-soft); }
.hp-zip-top-reasons { margin: 7px 0 0; padding: 0; list-style: none; }
.hp-zip-top-reasons li {
  position: relative;
  padding-left: 15px;
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--hp-ink-soft);
}
.hp-zip-top-reasons li::before {
  content: "";
  position: absolute;
  left: 3px;
  top: 7px;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--hp-primary);
}
/* "Show me the other 47". Same visual weight as the preview button it
   replaces, because it is the same decision at the next step. */
.hp-zip-more-btn {
  display: block;
  width: 100%;
  margin-top: 9px;
  padding: 9px 13px;
  border: 1px solid var(--hp-primary);
  border-radius: 10px;
  background: transparent;
  color: var(--hp-primary);
  font: inherit;
  font-size: 12.5px;
  font-weight: 700;
  cursor: pointer;
}
.hp-zip-more-btn:hover { background: var(--hp-primary-wash); }
.hp-zip-more-btn[hidden] { display: none; }
