← back to Homesonspec

apps/web/src/app/search/SearchClient.tsx

1079 lines

"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { fmtPrice, VerificationBadge } from "@homesonspec/shared-ui";

// Which URL params seed the filter state on load (href-drill: a link like
// /search?city=Austin&status=MOVE_IN_READY deep-links into a pre-filtered grid).
// Multi-value params (repeatable) vs single-value params are split so hydration
// rebuilds the exact filter shape the rail expects.
const MULTI_FILTER_KEYS = ["st", "builder", "status", "homeType"] as const;
const SINGLE_FILTER_KEYS = [
  "priceMin", "priceMax", "bedsMin", "bathsMin", "sqftMin", "sqftMax",
  "stories", "garageMin", "city", "incentives", "ageRestricted", "moveInByMonths",
] as const;

function hydrateFilters(qs: string): Record<string, string | string[]> {
  const sp = new URLSearchParams(qs);
  const out: Record<string, string | string[]> = {};
  for (const k of MULTI_FILTER_KEYS) {
    const vals = sp.getAll(k);
    if (vals.length) out[k] = vals;
  }
  for (const k of SINGLE_FILTER_KEYS) {
    const v = sp.get(k);
    if (v) out[k] = v;
  }
  return out;
}

// Leaflet needs window — client-only load, direct file import (not the barrel).
const MapView = dynamic(() => import("@homesonspec/shared-ui/src/MapView").then((m) => m.MapView), { ssr: false });

interface Home {
  id: string;
  street: string | null;
  city: string;
  state: string;
  zip: string;
  price: string | number | null;
  images: string[];
  beds: number | null;
  bathsTotal: string | number | null;
  sqft: number | null;
  stories: number | null;
  garageSpaces: number | null;
  homeType: string | null;
  lat: string | number | null;
  lon: string | number | null;
  constructionStatus: string;
  estCompletionDate: string | null;
  verificationLabel: string;
  lastVerifiedAt: string | null;
  publishedAt: string;
  community: { slug: string; name: string; schoolDistrict: string | null };
  builder: { slug: string; name: string };
}

interface SearchResponse {
  homes: Home[];
  total: number;
  page: number;
  pageSize: number;
  location: { label: string; lat: number; lon: number } | null;
}

interface Band {
  label: string;
  min: number | null;
  max: number | null;
  count: number;
}

interface Facets {
  constructionStatus: { value: string; count: number }[];
  builder: { value: string; label: string; count: number }[];
  beds: { value: number; count: number }[];
  baths: { value: number; count: number }[];
  homeType: { value: string; count: number }[];
  state: { value: string; count: number }[];
  stories: { value: number; count: number }[];
  garage: { value: number; count: number }[];
  city: { value: string; count: number }[];
  priceBands: Band[];
  sqftBands: Band[];
}

const STATE_NAMES: Record<string, string> = {
  AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas", CA: "California", CO: "Colorado",
  CT: "Connecticut", DE: "Delaware", FL: "Florida", GA: "Georgia", HI: "Hawaii", ID: "Idaho",
  IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas", KY: "Kentucky", LA: "Louisiana",
  ME: "Maine", MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi",
  MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey",
  NM: "New Mexico", NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio", OK: "Oklahoma",
  OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island", SC: "South Carolina", SD: "South Dakota",
  TN: "Tennessee", TX: "Texas", UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington",
  WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming", DC: "Washington DC",
};

// Broadened sort set. Every option maps 1:1 to a whitelisted key in packages/search
// (SORT_KEYS) and drives the server-side orderBy over the full result set. The Table
// view's click-to-sort column headers set the SAME `sort` param.
const SORTS = [
  { value: "newest", label: "Newest" },
  { value: "price_asc", label: "Price ↑" },
  { value: "price_desc", label: "Price ↓" },
  { value: "sqft_asc", label: "Sq Ft ↑" },
  { value: "sqft_desc", label: "Sq Ft ↓" },
  { value: "beds_desc", label: "Beds (most)" },
  { value: "baths_desc", label: "Baths (most)" },
  { value: "completion_asc", label: "Completion (soonest)" },
  { value: "completion_desc", label: "Completion (latest)" },
  { value: "city_asc", label: "City A→Z" },
  { value: "state_asc", label: "State A→Z" },
  { value: "last_verified", label: "Recently verified" },
  { value: "closest", label: "Closest" },
] as const;

// View modes — mirrors the vanilla RE builds' shared grid engine: card Grid, compact
// List, and dense all-columns Table. Persisted to localStorage (homesonspec.viewMode).
type ViewMode = "grid" | "list" | "table";
const VIEW_MODES: { value: ViewMode; label: string }[] = [
  { value: "grid", label: "Grid" },
  { value: "list", label: "List" },
  { value: "table", label: "Table" },
];

// Table columns: [sort key asc, sort key desc] so a header click toggles direction.
// null means the column isn't server-sortable (e.g. street address).
type TableCol = {
  key: string;
  label: string;
  asc: string | null;
  desc: string | null;
  align?: "right";
};
const TABLE_COLS: TableCol[] = [
  { key: "address", label: "Address", asc: null, desc: null },
  { key: "city", label: "City", asc: "city_asc", desc: "city_desc" },
  { key: "state", label: "State", asc: "state_asc", desc: "state_desc" },
  { key: "price", label: "Price", asc: "price_asc", desc: "price_desc", align: "right" },
  { key: "beds", label: "Beds", asc: "beds_asc", desc: "beds_desc", align: "right" },
  { key: "baths", label: "Baths", asc: "baths_asc", desc: "baths_desc", align: "right" },
  { key: "sqft", label: "Sq Ft", asc: "sqft_asc", desc: "sqft_desc", align: "right" },
  { key: "stories", label: "Stories", asc: "stories_asc", desc: "stories_desc", align: "right" },
  { key: "garage", label: "Garage", asc: "garage_asc", desc: "garage_desc", align: "right" },
  { key: "homeType", label: "Type", asc: null, desc: null },
  { key: "status", label: "Status", asc: null, desc: null },
  { key: "completion", label: "Completion", asc: "completion_asc", desc: "completion_desc" },
  { key: "builder", label: "Builder", asc: null, desc: null },
  { key: "verified", label: "Verified", asc: "last_verified_asc", desc: "last_verified" },
];

const STATUS_LABELS: Record<string, string> = {
  MOVE_IN_READY: "Move-in ready",
  UNDER_CONSTRUCTION: "Under construction",
  PLANNED: "Planned",
};

const HOME_TYPE_LABELS: Record<string, string> = {
  SINGLE_FAMILY: "Single-family",
  TOWNHOME: "Townhome",
  CONDO: "Condo",
  DUPLEX: "Duplex",
  OTHER: "Other",
};

// national-site card upgrades: colored urgency badges + an estimated monthly payment.
const STATUS_BADGE: Record<string, { label: string; cls: string }> = {
  MOVE_IN_READY: { label: "Move-in ready", cls: "bg-amber-100 text-amber-800 ring-1 ring-amber-200" },
  UNDER_CONSTRUCTION: { label: "Under construction", cls: "bg-sky-100 text-sky-800 ring-1 ring-sky-200" },
  PLANNED: { label: "Planned", cls: "bg-neutral-100 text-neutral-600 ring-1 ring-neutral-200" },
};
// est. all-in monthly: 20% down, 7% APR, 30yr P&I + ~1.25%/yr taxes+insurance.
function estMonthly(price: string | number | null): number | null {
  const p = Number(price);
  if (!Number.isFinite(p) || p <= 0) return null;
  const loan = p * 0.8, r = 0.07 / 12, n = 360;
  const pi = (loan * (r * Math.pow(1 + r, n))) / (Math.pow(1 + r, n) - 1);
  return Math.round(pi + (p * 0.0125) / 12);
}

// Price/sqft are continuous, so their card atoms drill to a sensible BAND around
// the home's value (±15% price, ±20% sqft) rather than an exact match that would
// usually return just the one home. Rounds to clean numbers for a shareable URL.
function priceBandDrill(price: string | number | null, drill: (kv: Record<string, string>) => string): string {
  const p = Number(price);
  if (!Number.isFinite(p) || p <= 0) return drill({});
  const lo = Math.max(0, Math.floor((p * 0.85) / 10000) * 10000);
  const hi = Math.ceil((p * 1.15) / 10000) * 10000;
  return drill({ priceMin: String(lo), priceMax: String(hi) });
}
function sqftBandDrill(sqft: number, drill: (kv: Record<string, string>) => string): string {
  if (!Number.isFinite(sqft) || sqft <= 0) return drill({});
  const lo = Math.max(0, Math.floor((sqft * 0.8) / 100) * 100);
  const hi = Math.ceil((sqft * 1.2) / 100) * 100;
  return drill({ sqftMin: String(lo), sqftMax: String(hi) });
}

// The rail's data-point panels, in display order. Each is a collapsed accordion whose
// open/closed state persists to localStorage (standing "one collapsed tab per data field" rule).
const FACET_KEYS = [
  "state", "price", "beds", "baths", "sqft", "homeType",
  "status", "stories", "garage", "city", "builder", "more",
] as const;

export default function SearchClient({
  initialQuery,
  initialParams = "",
}: {
  initialQuery: string;
  initialParams?: string;
}) {
  const [q, setQ] = useState(initialQuery);
  // Seed filters from the URL so drill links (?city=…&status=…) land pre-filtered.
  const [filters, setFilters] = useState<Record<string, string | string[]>>(() =>
    hydrateFilters(initialParams),
  );
  // Sort + density persist across reloads (standing grid rule). Sort can also be
  // seeded from the URL (a drill link may pin a sort).
  const [sort, setSort] = useState<string>(() => {
    const s = new URLSearchParams(initialParams).get("sort");
    return s ?? "newest";
  });
  const [cols, setCols] = useState<number>(3);
  // View mode (grid | list | table), persisted across reloads.
  const [viewMode, setViewMode] = useState<ViewMode>("grid");
  const [data, setData] = useState<SearchResponse | null>(null);
  const [facets, setFacets] = useState<Facets | null>(null);
  const [loading, setLoading] = useState(true);
  const [page, setPage] = useState(1);
  const bboxRef = useRef<string | null>(null);
  // Per-panel collapsed/expanded state — persisted so the rail remembers what you opened.
  const [open, setOpen] = useState<Record<string, boolean>>({});
  // Favorites (national-site staple) — persisted client-side.
  const [favs, setFavs] = useState<Set<string>>(new Set());
  useEffect(() => { try { setFavs(new Set(JSON.parse(localStorage.getItem("homesonspec.favs") || "[]"))); } catch {} }, []);
  const toggleFav = (id: string) => setFavs((prev) => {
    const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id);
    localStorage.setItem("homesonspec.favs", JSON.stringify([...n])); return n;
  });

  useEffect(() => {
    const savedSort = localStorage.getItem("homesonspec.sort");
    const savedCols = localStorage.getItem("homesonspec.cols");
    const savedView = localStorage.getItem("homesonspec.viewMode");
    if (savedSort) setSort(savedSort);
    if (savedCols) setCols(Number(savedCols));
    if (savedView === "grid" || savedView === "list" || savedView === "table") setViewMode(savedView);
    try {
      const savedOpen = JSON.parse(localStorage.getItem("homesonspec.facetOpen") || "null");
      // First visit → everything collapsed except State (so the rail isn't a blank wall).
      setOpen(savedOpen ?? { state: true });
    } catch { setOpen({ state: true }); }
  }, []);

  const setSortPersist = (value: string) => {
    setSort(value);
    localStorage.setItem("homesonspec.sort", value);
  };
  const setColsPersist = (value: number) => {
    setCols(value);
    localStorage.setItem("homesonspec.cols", String(value));
  };
  const setViewModePersist = (value: ViewMode) => {
    setViewMode(value);
    localStorage.setItem("homesonspec.viewMode", value);
  };
  // A Table header click sets the sort param (server-side) and toggles direction when
  // the same column is clicked again. Resets to page 1 so the new order starts at top.
  const onColumnSort = (col: TableCol) => {
    if (!col.asc && !col.desc) return; // non-sortable column
    setPage(1);
    const next = sort === col.asc ? (col.desc ?? col.asc) : (col.asc ?? col.desc);
    if (next) setSortPersist(next);
  };
  const persistOpen = (next: Record<string, boolean>) => {
    setOpen(next);
    localStorage.setItem("homesonspec.facetOpen", JSON.stringify(next));
  };
  const toggleOpen = (key: string) => persistOpen({ ...open, [key]: !open[key] });
  const setAllOpen = (v: boolean) => persistOpen(Object.fromEntries(FACET_KEYS.map((k) => [k, v])));

  const queryString = useMemo(() => {
    const params = new URLSearchParams();
    if (q) params.set("q", q);
    if (bboxRef.current) params.set("bbox", bboxRef.current);
    for (const [key, value] of Object.entries(filters)) {
      if (Array.isArray(value)) value.forEach((v) => params.append(key, v));
      else if (value !== "") params.set(key, value);
    }
    params.set("sort", sort);
    params.set("page", String(page));
    return params.toString();
  }, [q, filters, sort, page]);

  // Keep the browser URL in sync with the active filters + sort + query so the
  // filtered grid is URL-addressable and shareable (href-drill rule). bbox/page
  // are intentionally excluded to keep the shareable URL stable and clean.
  useEffect(() => {
    const params = new URLSearchParams();
    if (q) params.set("q", q);
    for (const [key, value] of Object.entries(filters)) {
      if (Array.isArray(value)) value.forEach((v) => params.append(key, v));
      else if (value !== "") params.set(key, value);
    }
    if (sort !== "newest") params.set("sort", sort);
    const qs = params.toString();
    const url = qs ? `/search?${qs}` : "/search";
    window.history.replaceState(null, "", url);
  }, [q, filters, sort]);

  const fetchResults = useCallback(async () => {
    setLoading(true);
    try {
      const [searchRes, facetsRes] = await Promise.all([
        fetch(`/api/search?${queryString}`),
        fetch(`/api/facets?${queryString}`),
      ]);
      setData(await searchRes.json());
      setFacets(await facetsRes.json());
    } finally {
      setLoading(false);
    }
  }, [queryString]);

  useEffect(() => {
    void fetchResults();
  }, [fetchResults]);

  const toggleMulti = (key: string, value: string) => {
    setPage(1);
    setFilters((prev) => {
      const current = Array.isArray(prev[key]) ? (prev[key] as string[]) : [];
      const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
      return { ...prev, [key]: next };
    });
  };
  const setSingle = (key: string, value: string) => {
    setPage(1);
    setFilters((prev) => ({ ...prev, [key]: value }));
  };
  // Range bands set a min+max pair together; clicking the active band clears both.
  const bandActive = (minKey: string, maxKey: string, b: Band) =>
    (filters[minKey] ?? "") === (b.min === null ? "" : String(b.min)) &&
    (filters[maxKey] ?? "") === (b.max === null ? "" : String(b.max));
  const toggleBand = (minKey: string, maxKey: string, b: Band) => {
    setPage(1);
    setFilters((prev) => {
      const next = { ...prev };
      if (bandActive(minKey, maxKey, b)) { delete next[minKey]; delete next[maxKey]; return next; }
      if (b.min === null) delete next[minKey]; else next[minKey] = String(b.min);
      if (b.max === null) delete next[maxKey]; else next[maxKey] = String(b.max);
      return next;
    });
  };

  // How many active selections live inside each panel — drives the teal header badge
  // so you can see where your filters are even while a panel is collapsed.
  const activeCount = (key: string): number => {
    const arr = (k: string) => (Array.isArray(filters[k]) ? (filters[k] as string[]).length : 0);
    const one = (k: string) => (filters[k] && filters[k] !== "" ? 1 : 0);
    switch (key) {
      case "state": return arr("st");
      case "builder": return arr("builder");
      case "status": return arr("status");
      case "homeType": return arr("homeType");
      case "beds": return one("bedsMin");
      case "baths": return one("bathsMin");
      case "stories": return one("stories");
      case "garage": return one("garageMin");
      case "city": return one("city");
      case "price": return one("priceMin") + one("priceMax");
      case "sqft": return one("sqftMin") + one("sqftMax");
      case "more": return one("incentives") + one("ageRestricted") + one("moveInByMonths");
      default: return 0;
    }
  };
  const totalActive = FACET_KEYS.reduce((n, k) => n + activeCount(k), 0);

  const markers = (data?.homes ?? [])
    .filter((h) => h.lat !== null && h.lon !== null)
    .map((h) => ({
      id: h.id,
      lat: Number(h.lat),
      lon: Number(h.lon),
      label: h.street ?? `${h.community.name} home`,
      sublabel: fmtPrice(h.price),
      href: `/homes/${h.id}`,
    }));

  const onMapMove = useCallback(
    (bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number }) => {
      bboxRef.current = `${bbox.minLat},${bbox.minLon},${bbox.maxLat},${bbox.maxLon}`;
    },
    [],
  );

  const totalPages = data ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1;

  // Drill-href builder: every displayed data point on a card links to its deeper,
  // URL-addressable filtered view (href-drill rule — no dead-end data points).
  const drill = (kv: Record<string, string>): string => {
    // Preserve the CURRENT active rail filters so a card drill NARROWS context instead of
    // clobbering it (TK-10482 contrarian SHOULD-FIX: a TX+AZ user clicking a city kept only TX).
    // Multi-value keys (st/builder/status/homeType) UNION with the drilled value; single keys replace.
    const MULTI = new Set<string>(MULTI_FILTER_KEYS as readonly string[]);
    const params = new URLSearchParams();
    const kvKeys = new Set(Object.keys(kv));
    for (const [k, val] of Object.entries(filters)) {
      if (kvKeys.has(k) && !MULTI.has(k)) continue; // single key: the drill value replaces it below
      if (Array.isArray(val)) { for (const v of val) if (v) params.append(k, v); }
      else if (val) params.set(k, String(val));
    }
    for (const [k, v] of Object.entries(kv)) {
      if (!v) continue;
      if (MULTI.has(k)) { if (!params.getAll(k).includes(v)) params.append(k, v); }
      else params.set(k, v);
    }
    return `/search?${params.toString()}`;
  };

  return (
    <div className="mx-auto max-w-7xl px-4 py-6">
      {/* Search bar */}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          bboxRef.current = null;
          setPage(1);
          void fetchResults();
        }}
        className="flex gap-2"
      >
        <input
          value={q}
          onChange={(e) => setQ(e.target.value)}
          placeholder="City, ZIP, or community"
          className="field max-w-md flex-1 py-2"
          data-testid="search-input"
        />
        <button type="submit" className="btn-primary">
          Search
        </button>
      </form>

      <div className="mt-6 grid gap-6 lg:grid-cols-[280px_1fr]">
        {/* Amazon-style faceted rail — every data point a collapsed panel with live counts. */}
        <aside className="text-sm lg:sticky lg:top-[68px] lg:max-h-[calc(100vh-84px)] lg:overflow-y-auto lg:pr-1" data-testid="filters">
          <div className="mb-2 flex items-center justify-between">
            <h2 className="font-display text-base font-semibold text-brand-900">
              Filter homes
              {totalActive > 0 && (
                <span className="ml-2 rounded-full bg-teal-100 px-1.5 py-0.5 align-middle text-[11px] font-bold text-teal-700 ring-1 ring-teal-200">
                  {totalActive}
                </span>
              )}
            </h2>
            {totalActive > 0 && (
              <button type="button" className="text-xs font-semibold text-brand-700 hover:text-brand-800"
                onClick={() => { setPage(1); setFilters({}); }} data-testid="clear-all">
                Clear all
              </button>
            )}
          </div>
          {/* Expand / collapse every panel at once. */}
          <div className="mb-1 flex gap-3 text-[11px] font-semibold text-neutral-500">
            <button type="button" className="hover:text-brand-700" onClick={() => setAllOpen(true)} data-testid="expand-all">Expand all</button>
            <span className="text-neutral-300">·</span>
            <button type="button" className="hover:text-brand-700" onClick={() => setAllOpen(false)} data-testid="collapse-all">Collapse all</button>
          </div>

          {/* State — multi-select, top 12 + overflow. */}
          <FacetPanel id="state" title="State" optionCount={facets?.state.length} active={activeCount("state")} open={!!open.state} onToggle={() => toggleOpen("state")}>
            <ChecklistOverflow
              items={(facets?.state ?? []).map((s) => ({ value: s.value, label: STATE_NAMES[s.value] ?? s.value, count: s.count }))}
              selected={Array.isArray(filters.st) ? (filters.st as string[]) : []}
              onToggle={(v) => toggleMulti("st", v)}
              testid="facet-state"
            />
          </FacetPanel>

          {/* Price — quick range bands (counts) + custom min/max. */}
          <FacetPanel id="price" title="Price" optionCount={facets?.priceBands.length} active={activeCount("price")} open={!!open.price} onToggle={() => toggleOpen("price")}>
            <div className="space-y-0.5" data-testid="facet-price">
              {(facets?.priceBands ?? []).map((b) => (
                <FacetCheck key={b.label} label={b.label} count={b.count}
                  checked={bandActive("priceMin", "priceMax", b)} onToggle={() => toggleBand("priceMin", "priceMax", b)} />
              ))}
            </div>
            <div className="mt-2 flex gap-2">
              <input type="number" placeholder="Min $" value={(filters.priceMin as string) ?? ""} className="w-full rounded-lg border-0 px-3 py-1.5 shadow-sm ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-brand-400"
                onChange={(e) => setSingle("priceMin", e.target.value)} data-testid="price-min" />
              <input type="number" placeholder="Max $" value={(filters.priceMax as string) ?? ""} className="w-full rounded-lg border-0 px-3 py-1.5 shadow-sm ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-brand-400"
                onChange={(e) => setSingle("priceMax", e.target.value)} data-testid="price-max" />
            </div>
          </FacetPanel>

          {/* Beds — single-select "N+" min, with live counts. */}
          <FacetPanel id="beds" title="Beds (min)" optionCount={facets?.beds.length} active={activeCount("beds")} open={!!open.beds} onToggle={() => toggleOpen("beds")}>
            <MinChips values={(facets?.beds ?? []).map((b) => b.value)} suffix="+" field="bedsMin" filters={filters} onPick={setSingle} testid="facet-beds" />
          </FacetPanel>

          {/* Baths — single-select "N+" min, with live counts. */}
          <FacetPanel id="baths" title="Baths (min)" optionCount={facets?.baths.length} active={activeCount("baths")} open={!!open.baths} onToggle={() => toggleOpen("baths")}>
            <MinChips values={(facets?.baths ?? []).map((b) => b.value)} suffix="+" field="bathsMin" filters={filters} onPick={setSingle} testid="facet-baths" />
          </FacetPanel>

          {/* Sq ft — quick range bands (counts). */}
          <FacetPanel id="sqft" title="Sq ft" optionCount={facets?.sqftBands.length} active={activeCount("sqft")} open={!!open.sqft} onToggle={() => toggleOpen("sqft")}>
            <div className="space-y-0.5" data-testid="facet-sqft">
              {(facets?.sqftBands ?? []).map((b) => (
                <FacetCheck key={b.label} label={b.label} count={b.count}
                  checked={bandActive("sqftMin", "sqftMax", b)} onToggle={() => toggleBand("sqftMin", "sqftMax", b)} />
              ))}
            </div>
          </FacetPanel>

          {/* Home type — multi-select. */}
          <FacetPanel id="homeType" title="Home type" optionCount={facets?.homeType.length} active={activeCount("homeType")} open={!!open.homeType} onToggle={() => toggleOpen("homeType")}>
            <div className="space-y-0.5" data-testid="facet-hometype">
              {(facets?.homeType ?? []).map((t) => (
                <FacetCheck key={t.value} label={HOME_TYPE_LABELS[t.value] ?? t.value} count={t.count}
                  checked={Array.isArray(filters.homeType) && (filters.homeType as string[]).includes(t.value)}
                  onToggle={() => toggleMulti("homeType", t.value)} />
              ))}
            </div>
          </FacetPanel>

          {/* Construction status — multi-select. */}
          <FacetPanel id="status" title="Construction status" optionCount={facets?.constructionStatus.length} active={activeCount("status")} open={!!open.status} onToggle={() => toggleOpen("status")}>
            <div className="space-y-0.5" data-testid="facet-status">
              {(facets?.constructionStatus ?? []).map((facet) => (
                <FacetCheck key={facet.value}
                  label={STATUS_LABELS[facet.value] ?? facet.value} count={facet.count}
                  checked={Array.isArray(filters.status) && (filters.status as string[]).includes(facet.value)}
                  onToggle={() => toggleMulti("status", facet.value)} />
              ))}
            </div>
          </FacetPanel>

          {/* Stories — single-select exact, with live counts. */}
          <FacetPanel id="stories" title="Stories" optionCount={facets?.stories.length} active={activeCount("stories")} open={!!open.stories} onToggle={() => toggleOpen("stories")}>
            <MinChips values={(facets?.stories ?? []).map((s) => s.value)} suffix="" field="stories" filters={filters} onPick={setSingle} testid="facet-stories" />
          </FacetPanel>

          {/* Garage — single-select "N+" min, with live counts. */}
          <FacetPanel id="garage" title="Garage (min)" optionCount={facets?.garage.length} active={activeCount("garage")} open={!!open.garage} onToggle={() => toggleOpen("garage")}>
            <MinChips values={(facets?.garage ?? []).map((g) => g.value)} suffix="+" field="garageMin" filters={filters} onPick={setSingle} testid="facet-garage" />
          </FacetPanel>

          {/* City — single-select, top 25 + overflow. */}
          <FacetPanel id="city" title="City" optionCount={facets?.city.length} active={activeCount("city")} open={!!open.city} onToggle={() => toggleOpen("city")}>
            <ChecklistOverflow
              items={(facets?.city ?? []).map((c) => ({ value: c.value, label: c.value, count: c.count }))}
              selected={filters.city ? [filters.city as string] : []}
              onToggle={(v) => setSingle("city", filters.city === v ? "" : v)}
              testid="facet-city"
            />
          </FacetPanel>

          {/* Builder / firm — multi-select. */}
          <FacetPanel id="builder" title="Builder" optionCount={facets?.builder.length} active={activeCount("builder")} open={!!open.builder} onToggle={() => toggleOpen("builder")}>
            <ChecklistOverflow
              items={(facets?.builder ?? []).map((b) => ({ value: b.value, label: b.label, count: b.count }))}
              selected={Array.isArray(filters.builder) ? (filters.builder as string[]) : []}
              onToggle={(v) => toggleMulti("builder", v)}
              testid="facet-builder"
            />
          </FacetPanel>

          {/* More — incentives / age-restricted / move-in timeframe. */}
          <FacetPanel id="more" title="More" active={activeCount("more")} open={!!open.more} onToggle={() => toggleOpen("more")}>
            <div data-testid="facet-more">
              <label className="flex cursor-pointer items-center gap-2 py-0.5">
                <input type="checkbox" className="h-4 w-4 rounded accent-brand-700" checked={filters.incentives === "1"}
                  onChange={() => setSingle("incentives", filters.incentives === "1" ? "" : "1")} />
                Has incentives
              </label>
              <label className="flex cursor-pointer items-center gap-2 py-0.5">
                <input type="checkbox" className="h-4 w-4 rounded accent-brand-700" checked={filters.ageRestricted === "1"}
                  onChange={() => setSingle("ageRestricted", filters.ageRestricted === "1" ? "" : "1")} />
                Age-restricted community
              </label>
              <select className="mt-2 w-full rounded-lg border-0 px-3 py-1.5 shadow-sm ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-brand-400"
                value={(filters.moveInByMonths as string) ?? ""}
                onChange={(e) => setSingle("moveInByMonths", e.target.value)}>
                <option value="">Any move-in timeframe</option>
                <option value="0">Ready now</option>
                <option value="3">Within 3 months</option>
                <option value="6">Within 6 months</option>
                <option value="12">Within 12 months</option>
              </select>
            </div>
          </FacetPanel>
        </aside>

        {/* Results */}
        <div>
          <div className="flex flex-wrap items-center justify-between gap-3">
            <div className="text-sm text-neutral-600" data-testid="result-count">
              {loading ? "Searching…" : `${data?.total ?? 0} homes${data?.location ? ` near ${data.location.label}` : ""}`}
            </div>
            {/* Mandatory grid controls: view-mode toggle + sort select + density slider, localStorage-persisted */}
            <div className="flex items-center gap-4">
              {/* View mode: Grid | List | Table (matches the vanilla RE builds' shared grid engine). */}
              <div className="inline-flex overflow-hidden rounded-lg text-sm shadow-sm ring-1 ring-inset ring-neutral-200" role="group" aria-label="View mode" data-testid="view-toggle">
                {VIEW_MODES.map((v) => (
                  <button key={v.value} type="button"
                    onClick={() => setViewModePersist(v.value)}
                    aria-pressed={viewMode === v.value}
                    data-testid={`view-${v.value}`}
                    className={`px-3 py-1 transition ${viewMode === v.value ? "bg-brand-700 text-white" : "bg-white text-neutral-600 hover:bg-brand-50"}`}>
                    {v.label}
                  </button>
                ))}
              </div>
              <label className="flex items-center gap-2 text-sm">
                Sort
                <select value={sort} onChange={(e) => setSortPersist(e.target.value)}
                  className="rounded-lg border-0 px-2 py-1 shadow-sm ring-1 ring-inset ring-neutral-200 focus:ring-2 focus:ring-brand-400" data-testid="sort-select">
                  {SORTS.map((s) => (
                    <option key={s.value} value={s.value}>{s.label}</option>
                  ))}
                </select>
              </label>
              {/* Density controls the card grid columns — only meaningful in Grid view. */}
              {viewMode === "grid" && (
                <label className="flex items-center gap-2 text-sm">
                  Density
                  <input type="range" min={2} max={4} step={1} value={cols} className="accent-brand-700"
                    onChange={(e) => setColsPersist(Number(e.target.value))} data-testid="density-slider" />
                </label>
              )}
            </div>
          </div>

          {viewMode === "table" ? (
            <TableView
              homes={data?.homes ?? []}
              loading={loading}
              sort={sort}
              onColumnSort={onColumnSort}
              drill={drill}
              priceBandDrill={priceBandDrill}
              sqftBandDrill={sqftBandDrill}
            />
          ) : (
          <div className="mt-4 grid gap-4 lg:grid-cols-[1fr_380px]">
            {viewMode === "list" ? (
              <ListView
                homes={data?.homes ?? []}
                loading={loading}
                favs={favs}
                toggleFav={toggleFav}
                drill={drill}
                priceBandDrill={priceBandDrill}
                sqftBandDrill={sqftBandDrill}
              />
            ) : (
            <div className="grid gap-4" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }} data-testid="results-grid">
              {(data?.homes ?? []).map((home) => (
                // href-drill: the card is a container with a STRETCHED overlay link to the
                // detail page, and every data point below is its own real drill link sitting
                // ABOVE the overlay (z-10). No nested anchors — clicking blank card area opens
                // the detail; clicking a data atom drills to that atom's deeper view.
                <div key={home.id}
                  className="card-interactive group relative overflow-hidden"
                  data-testid="home-card">
                  <Link href={`/homes/${home.id}`} aria-label={`View ${home.street ?? "home"} in ${home.city}, ${home.state}`}
                    className="absolute inset-0 z-0" data-testid="home-card-overlay" tabIndex={-1} />
                  {/* Photo header — the primary scan signal for a real-estate grid. Falls back
                      to a branded gradient when a home has no photo (image-less builders) or when
                      the media-rights gate blanks images upstream, so the grid stays uniform. */}
                  <div className="relative h-44 bg-gradient-to-br from-brand-800 via-brand-700 to-brand-500">
                    {home.images?.[0]?.startsWith("http") ? (
                      // eslint-disable-next-line @next/next/no-img-element
                      <img src={home.images[0]} alt={`${home.community.name} home in ${home.city}, ${home.state}`}
                        loading="lazy" className="absolute inset-0 h-full w-full object-cover" />
                    ) : null}
                    <button type="button" aria-label="Save home" data-testid="fav-btn"
                      onClick={(e) => { e.preventDefault(); e.stopPropagation(); toggleFav(home.id); }}
                      className="absolute right-3 top-3 z-20 text-lg leading-none drop-shadow transition hover:scale-110">
                      <span className={favs.has(home.id) ? "text-rose-500" : "text-white/90"}>{favs.has(home.id) ? "♥" : "♡"}</span>
                    </button>
                    {/* Status badge drills to the same construction status. */}
                    {STATUS_BADGE[home.constructionStatus] && (
                      <Link href={drill({ status: home.constructionStatus })} data-testid="drill-status"
                        onClick={(e) => e.stopPropagation()}
                        className={`absolute left-3 top-3 z-20 inline-block rounded-full px-2 py-0.5 text-[11px] font-medium shadow-sm transition hover:brightness-95 ${STATUS_BADGE[home.constructionStatus]!.cls}`}>
                        {STATUS_BADGE[home.constructionStatus]!.label}
                      </Link>
                    )}
                  </div>
                  <div className="relative z-10 p-4">
                    <div className="flex items-baseline gap-2">
                      {/* Price drills to a price band around this home's price (±15%). */}
                      <Link href={priceBandDrill(home.price, drill)} data-testid="drill-price"
                        className="font-display text-xl font-semibold text-brand-900 hover:text-brand-700 hover:underline decoration-brand-300 underline-offset-2">
                        {fmtPrice(home.price)}
                      </Link>
                      {estMonthly(home.price) !== null && (
                        <span className="text-xs text-neutral-500">~${estMonthly(home.price)!.toLocaleString()}/mo</span>
                      )}
                    </div>
                    <div className="mt-0.5 truncate text-sm text-neutral-700">
                      {/* Detail link for the street, then city + state each drill to their scope. */}
                      <Link href={`/homes/${home.id}`} className="hover:underline">
                        {home.street ?? "Address on request"}
                      </Link>{" · "}
                      <Link href={drill({ city: home.city, st: home.state })} data-testid="drill-city"
                        className="hover:text-brand-700 hover:underline">{home.city}</Link>{", "}
                      <Link href={drill({ st: home.state })} data-testid="drill-state"
                        className="hover:text-brand-700 hover:underline">{home.state}</Link>
                    </div>
                    <div className="mt-1 text-sm text-neutral-500">
                      {/* Beds / baths / sqft each drill to the matching min filter. */}
                      {home.beds != null ? (
                        <Link href={drill({ bedsMin: String(home.beds) })} data-testid="drill-beds"
                          className="hover:text-brand-700 hover:underline">{home.beds} bd</Link>
                      ) : "— bd"}{" · "}
                      {home.bathsTotal != null ? (
                        <Link href={drill({ bathsMin: String(home.bathsTotal) })} data-testid="drill-baths"
                          className="hover:text-brand-700 hover:underline">{home.bathsTotal} ba</Link>
                      ) : "— ba"}{" · "}
                      {home.sqft != null ? (
                        <Link href={sqftBandDrill(home.sqft, drill)} data-testid="drill-sqft"
                          className="hover:text-brand-700 hover:underline">{home.sqft.toLocaleString()} sqft</Link>
                      ) : "— sqft"}
                      {home.estCompletionDate ? ` · ready ${new Date(home.estCompletionDate).toLocaleDateString(undefined, { month: "short", year: "numeric" })}` : ""}
                    </div>
                    <div className="mt-2 flex items-center justify-between">
                      {/* Builder drills to the builder's own page (existing route). */}
                      <Link href={`/builders/${home.builder.slug}`} data-testid="drill-builder"
                        className="truncate text-xs text-neutral-500 hover:text-brand-700 hover:underline">
                        {home.builder.name}
                      </Link>
                      <VerificationBadge label={home.verificationLabel} />
                    </div>
                    {/* Community drill — the "project" this home belongs to. */}
                    <Link href={`/communities/${home.community.slug}`} data-testid="drill-community"
                      className="mt-1 block truncate text-xs text-neutral-400 hover:text-brand-700 hover:underline">
                      {home.community.name}
                    </Link>
                  </div>
                </div>
              ))}
              {!loading && (data?.homes.length ?? 0) === 0 && (
                <p className="col-span-full py-10 text-center text-neutral-500">
                  No homes match these filters.
                </p>
              )}
            </div>
            )}
            <div className="h-[540px] overflow-hidden rounded-2xl border border-neutral-200 shadow-[var(--shadow-card)]" data-testid="map">
              <MapView markers={markers} onMoveEnd={onMapMove} />
            </div>
          </div>
          )}

          {totalPages > 1 && (
            <div className="mt-6 flex items-center justify-center gap-3 text-sm">
              <button disabled={page <= 1} onClick={() => setPage((p) => p - 1)}
                className="rounded-full border border-neutral-300 px-4 py-1.5 hover:border-brand-300 disabled:opacity-40">Prev</button>
              <span className="text-neutral-600">Page {page} of {totalPages}</span>
              <button disabled={page >= totalPages} onClick={() => setPage((p) => p + 1)}
                className="rounded-full border border-neutral-300 px-4 py-1.5 hover:border-brand-300 disabled:opacity-40">Next</button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Faceted-rail primitives ────────────────────────────────────────────────

// A collapsed accordion panel for one data point. Header shows the option count and a
// teal badge with the # of active selections inside (visible even while collapsed).
function FacetPanel({
  id, title, optionCount, active, open, onToggle, children,
}: {
  id: string;
  title: string;
  optionCount?: number;
  active: number;
  open: boolean;
  onToggle: () => void;
  children: React.ReactNode;
}) {
  return (
    <div className="border-t border-neutral-100 first:border-t-0">
      <button type="button" onClick={onToggle} aria-expanded={open}
        className="flex w-full items-center justify-between gap-2 py-2.5 text-left"
        data-testid={`facet-head-${id}`}>
        <span className="flex items-center gap-2">
          <span className="text-xs font-semibold uppercase tracking-[0.14em] text-brand-600">{title}</span>
          {active > 0 && (
            <span className="rounded-full bg-teal-100 px-1.5 py-0.5 text-[10px] font-bold text-teal-700 ring-1 ring-teal-200">{active}</span>
          )}
        </span>
        <span className="flex items-center gap-2 text-neutral-400">
          {optionCount != null && optionCount > 0 && <span className="text-[11px]">{optionCount}</span>}
          <svg className={`h-3.5 w-3.5 transition-transform ${open ? "rotate-180" : ""}`} viewBox="0 0 12 12" fill="none" aria-hidden="true">
            <path d="M2.5 4.5L6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </span>
      </button>
      {open && <div className="pb-3">{children}</div>}
    </div>
  );
}

// Single-select "N+" (or exact) numeric chips, backed by live facet counts.
function MinChips({
  values, suffix, field, filters, onPick, testid,
}: {
  values: number[];
  suffix: string;
  field: string;
  filters: Record<string, string | string[]>;
  onPick: (key: string, value: string) => void;
  testid?: string;
}) {
  if (values.length === 0) return <p className="px-1 text-xs text-neutral-400">No data</p>;
  return (
    <div className="flex flex-wrap gap-1.5" data-testid={testid}>
      {values.map((n) => {
        const active = filters[field] === String(n);
        return (
          <button key={n} type="button"
            onClick={() => onPick(field, active ? "" : String(n))}
            className={`rounded-full border px-3 py-1 text-xs transition ${active ? "border-brand-600 bg-brand-50 font-semibold text-brand-800" : "border-neutral-300 hover:border-brand-300"}`}>
            {n % 1 === 0 ? n : n.toFixed(1)}{suffix}
          </button>
        );
      })}
    </div>
  );
}

function FacetCheck({
  label, count, checked, onToggle,
}: { label: string; count: number; checked: boolean; onToggle: () => void }) {
  return (
    <label className="flex cursor-pointer items-center justify-between gap-2 rounded-lg px-1 py-1 hover:bg-brand-50">
      <span className="flex items-center gap-2">
        <input type="checkbox" className="h-4 w-4 rounded accent-brand-700" checked={checked} onChange={onToggle} />
        <span className="truncate">{label}</span>
      </span>
      <span className="shrink-0 text-xs text-neutral-400">{count.toLocaleString()}</span>
    </label>
  );
}

// Shows the first `cap` options; the rest live inside a native <details> so the
// rail stays short with long lists (all 50 states, many builders, top cities).
function ChecklistOverflow({
  items, selected, onToggle, cap = 12, testid,
}: {
  items: { value: string; label: string; count: number }[];
  selected: string[];
  onToggle: (v: string) => void;
  cap?: number;
  testid?: string;
}) {
  const head = items.slice(0, cap);
  const rest = items.slice(cap);
  return (
    <div className="space-y-0.5" data-testid={testid}>
      {head.map((it) => (
        <FacetCheck key={it.value} label={it.label} count={it.count}
          checked={selected.includes(it.value)} onToggle={() => onToggle(it.value)} />
      ))}
      {rest.length > 0 && (
        <details className="group">
          <summary className="cursor-pointer list-none px-1 py-1 text-xs font-semibold text-brand-700 hover:text-brand-800">
            + {rest.length} more<span className="group-open:hidden"> ▾</span><span className="hidden group-open:inline"> ▴</span>
          </summary>
          <div className="space-y-0.5">
            {rest.map((it) => (
              <FacetCheck key={it.value} label={it.label} count={it.count}
                checked={selected.includes(it.value)} onToggle={() => onToggle(it.value)} />
            ))}
          </div>
        </details>
      )}
    </div>
  );
}

// ── View modes ─────────────────────────────────────────────────────────────
// Shared drill helper types so the view components can build the same href-drill
// links the Grid uses (no dead-end data points).
type Drill = (kv: Record<string, string>) => string;
type BandDrill = (v: string | number | null, drill: Drill) => string;

const fmtCompletion = (d: string | null) =>
  d ? new Date(d).toLocaleDateString(undefined, { month: "short", year: "numeric" }) : "—";
const fmtVerified = (d: string | null) =>
  d ? new Date(d).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) : "—";

// ListView — one compact row per home: address/city + price + key stats inline.
// Keeps every data point a drill link (href-drill rule), like the card grid.
function ListView({
  homes, loading, favs, toggleFav, drill, priceBandDrill, sqftBandDrill,
}: {
  homes: Home[];
  loading: boolean;
  favs: Set<string>;
  toggleFav: (id: string) => void;
  drill: Drill;
  priceBandDrill: BandDrill;
  sqftBandDrill: (sqft: number, drill: Drill) => string;
}) {
  if (!loading && homes.length === 0) {
    return <p className="py-10 text-center text-neutral-500">No homes match these filters.</p>;
  }
  return (
    <div className="divide-y divide-neutral-100 rounded-2xl border border-neutral-200 shadow-[var(--shadow-card)]" data-testid="results-list">
      {homes.map((home) => (
        <div key={home.id} className="group relative flex items-center gap-4 p-3 transition hover:bg-brand-50/40" data-testid="home-row">
          <Link href={`/homes/${home.id}`} aria-label={`View ${home.street ?? "home"} in ${home.city}, ${home.state}`}
            className="absolute inset-0 z-0" tabIndex={-1} />
          {/* Thumbnail (or branded gradient fallback). */}
          <div className="relative z-10 h-16 w-24 shrink-0 overflow-hidden rounded-lg bg-gradient-to-br from-brand-800 via-brand-700 to-brand-500">
            {home.images?.[0]?.startsWith("http") ? (
              // eslint-disable-next-line @next/next/no-img-element
              <img src={home.images[0]} alt="" loading="lazy" className="h-full w-full object-cover" />
            ) : null}
          </div>
          <div className="relative z-10 min-w-0 flex-1">
            <div className="flex items-baseline gap-2">
              <Link href={priceBandDrill(home.price, drill)} data-testid="row-price"
                className="font-display text-lg font-semibold text-brand-900 hover:text-brand-700 hover:underline">
                {fmtPrice(home.price)}
              </Link>
              {STATUS_BADGE[home.constructionStatus] && (
                <Link href={drill({ status: home.constructionStatus })} onClick={(e) => e.stopPropagation()}
                  className={`rounded-full px-2 py-0.5 text-[11px] font-medium ${STATUS_BADGE[home.constructionStatus]!.cls}`}>
                  {STATUS_BADGE[home.constructionStatus]!.label}
                </Link>
              )}
            </div>
            <div className="mt-0.5 truncate text-sm text-neutral-700">
              <Link href={`/homes/${home.id}`} className="hover:underline">{home.street ?? "Address on request"}</Link>{" · "}
              <Link href={drill({ city: home.city, st: home.state })} className="hover:text-brand-700 hover:underline">{home.city}</Link>{", "}
              <Link href={drill({ st: home.state })} className="hover:text-brand-700 hover:underline">{home.state}</Link>
            </div>
            <div className="mt-0.5 truncate text-sm text-neutral-500">
              {home.beds != null ? (
                <Link href={drill({ bedsMin: String(home.beds) })} className="hover:text-brand-700 hover:underline">{home.beds} bd</Link>
              ) : "— bd"}{" · "}
              {home.bathsTotal != null ? (
                <Link href={drill({ bathsMin: String(home.bathsTotal) })} className="hover:text-brand-700 hover:underline">{home.bathsTotal} ba</Link>
              ) : "— ba"}{" · "}
              {home.sqft != null ? (
                <Link href={sqftBandDrill(home.sqft, drill)} className="hover:text-brand-700 hover:underline">{home.sqft.toLocaleString()} sqft</Link>
              ) : "— sqft"}
              {" · "}
              <Link href={`/builders/${home.builder.slug}`} className="hover:text-brand-700 hover:underline">{home.builder.name}</Link>
              {home.estCompletionDate ? ` · ready ${fmtCompletion(home.estCompletionDate)}` : ""}
            </div>
          </div>
          <div className="relative z-10 flex shrink-0 items-center gap-3">
            <VerificationBadge label={home.verificationLabel} />
            <button type="button" aria-label="Save home"
              onClick={(e) => { e.preventDefault(); e.stopPropagation(); toggleFav(home.id); }}
              className="text-lg leading-none transition hover:scale-110">
              <span className={favs.has(home.id) ? "text-rose-500" : "text-neutral-300"}>{favs.has(home.id) ? "♥" : "♡"}</span>
            </button>
          </div>
        </div>
      ))}
    </div>
  );
}

// TableView — dense spreadsheet, a column per meaningful field, click-to-sort headers.
// A header click drives the SAME server-side `sort` param (over the full result set),
// toggling asc/desc. Sortable cells stay drill links (href-drill rule).
function TableView({
  homes, loading, sort, onColumnSort, drill, priceBandDrill, sqftBandDrill,
}: {
  homes: Home[];
  loading: boolean;
  sort: string;
  onColumnSort: (col: TableCol) => void;
  drill: Drill;
  priceBandDrill: BandDrill;
  sqftBandDrill: (sqft: number, drill: Drill) => string;
}) {
  // Direction arrow shown on the active column's header.
  const arrow = (col: TableCol): string => {
    if (sort === col.asc && sort === col.desc) return "↕";
    if (sort === col.asc) return "↑";
    if (sort === col.desc) return "↓";
    return "";
  };
  const isActive = (col: TableCol) => sort === col.asc || sort === col.desc;
  return (
    <div className="mt-4 overflow-x-auto rounded-2xl border border-neutral-200 shadow-[var(--shadow-card)]" data-testid="results-table">
      <table className="w-full border-collapse text-sm">
        <thead>
          <tr className="border-b border-neutral-200 bg-neutral-50 text-left">
            {TABLE_COLS.map((col) => {
              const sortable = !!(col.asc || col.desc);
              return (
                <th key={col.key} scope="col"
                  className={`whitespace-nowrap px-3 py-2 text-xs font-semibold uppercase tracking-wide ${col.align === "right" ? "text-right" : ""} ${isActive(col) ? "text-brand-800" : "text-neutral-500"}`}
                  data-testid={`th-${col.key}`}
                  aria-sort={isActive(col) ? (sort === col.desc ? "descending" : "ascending") : "none"}>
                  {sortable ? (
                    <button type="button" onClick={() => onColumnSort(col)}
                      className="inline-flex items-center gap-1 hover:text-brand-700"
                      data-testid={`sort-col-${col.key}`}>
                      {col.label}<span className="text-[10px]">{arrow(col)}</span>
                    </button>
                  ) : col.label}
                </th>
              );
            })}
          </tr>
        </thead>
        <tbody className="divide-y divide-neutral-100">
          {homes.map((home) => (
            <tr key={home.id} className="transition hover:bg-brand-50/40" data-testid="table-row">
              <td className="max-w-[220px] truncate px-3 py-2">
                <Link href={`/homes/${home.id}`} className="text-brand-800 hover:underline">{home.street ?? "Address on request"}</Link>
              </td>
              <td className="whitespace-nowrap px-3 py-2">
                <Link href={drill({ city: home.city, st: home.state })} className="hover:text-brand-700 hover:underline">{home.city}</Link>
              </td>
              <td className="whitespace-nowrap px-3 py-2">
                <Link href={drill({ st: home.state })} className="hover:text-brand-700 hover:underline">{home.state}</Link>
              </td>
              <td className="whitespace-nowrap px-3 py-2 text-right font-medium">
                <Link href={priceBandDrill(home.price, drill)} className="text-brand-900 hover:text-brand-700 hover:underline">{fmtPrice(home.price)}</Link>
              </td>
              <td className="px-3 py-2 text-right">
                {home.beds != null ? (
                  <Link href={drill({ bedsMin: String(home.beds) })} className="hover:text-brand-700 hover:underline">{home.beds}</Link>
                ) : "—"}
              </td>
              <td className="px-3 py-2 text-right">
                {home.bathsTotal != null ? (
                  <Link href={drill({ bathsMin: String(home.bathsTotal) })} className="hover:text-brand-700 hover:underline">{home.bathsTotal}</Link>
                ) : "—"}
              </td>
              <td className="whitespace-nowrap px-3 py-2 text-right">
                {home.sqft != null ? (
                  <Link href={sqftBandDrill(home.sqft, drill)} className="hover:text-brand-700 hover:underline">{home.sqft.toLocaleString()}</Link>
                ) : "—"}
              </td>
              <td className="px-3 py-2 text-right">{home.stories ?? "—"}</td>
              <td className="px-3 py-2 text-right">{home.garageSpaces ?? "—"}</td>
              <td className="whitespace-nowrap px-3 py-2 text-neutral-600">{home.homeType ? (HOME_TYPE_LABELS[home.homeType] ?? home.homeType) : "—"}</td>
              <td className="whitespace-nowrap px-3 py-2">
                <Link href={drill({ status: home.constructionStatus })} className="hover:text-brand-700 hover:underline">
                  {STATUS_LABELS[home.constructionStatus] ?? home.constructionStatus}
                </Link>
              </td>
              <td className="whitespace-nowrap px-3 py-2 text-neutral-600">{fmtCompletion(home.estCompletionDate)}</td>
              <td className="max-w-[160px] truncate px-3 py-2">
                <Link href={`/builders/${home.builder.slug}`} className="hover:text-brand-700 hover:underline">{home.builder.name}</Link>
              </td>
              <td className="whitespace-nowrap px-3 py-2 text-neutral-500" title={home.lastVerifiedAt ?? undefined}>{fmtVerified(home.lastVerifiedAt)}</td>
            </tr>
          ))}
        </tbody>
      </table>
      {!loading && homes.length === 0 && (
        <p className="py-10 text-center text-neutral-500">No homes match these filters.</p>
      )}
    </div>
  );
}