// =============================================================
// page-shippers.jsx — Shipper Directory + per-shipper detail
//
// /#/shippers           → directory list (anon-readable)
// /#/shipper/<slug>     → per-shipper deep dive (anon sees overview;
//                          contacts paywalled once Stripe lands)
// =============================================================

const {
  useState: useStateSh,
  useEffect: useEffectSh,
  useMemo: useMemoSh,
} = React;

// Router-aware wrapper — picks list or detail view based on hash slug
function ShippersPage({ onNav, slug }) {
  if (slug) return <ShipperDetail onNav={onNav} slug={slug} />;
  return <ShipperList onNav={onNav} />;
}

// Facility cluster map — uses Leaflet markercluster (loaded in index.html)
// to handle thousands of pins efficiently. Click a leaf pin → navigate to
// the shipper detail page.
function ShipperFacilityMap({ facilities }) {
  const containerRef = React.useRef(null);
  const mapRef = React.useRef(null);
  const layerRef = React.useRef(null);

  React.useEffect(() => {
    if (!containerRef.current || !window.L) return;
    if (!mapRef.current) {
      mapRef.current = window.L.map(containerRef.current, {
        center: [38.0, -96.0], zoom: 4, scrollWheelZoom: false,
        attributionControl: true,
      });
      window.L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", {
        attribution: "© OpenStreetMap contributors © CARTO",
        subdomains: "abcd", maxZoom: 18,
      }).addTo(mapRef.current);
    }
    if (layerRef.current) {
      layerRef.current.clearLayers();
      layerRef.current.remove();
    }
    const cluster = (window.L.markerClusterGroup || window.L.MarkerClusterGroup)
      ? window.L.markerClusterGroup({
          chunkedLoading: true,
          maxClusterRadius: 60,
          spiderfyOnMaxZoom: false,
          showCoverageOnHover: false,
        })
      : window.L.layerGroup();

    let added = 0;
    for (const f of facilities) {
      if (f.lat == null || f.lng == null) continue;
      const m = window.L.circleMarker([f.lat, f.lng], {
        radius: 4, color: "#2563eb", weight: 1, fillColor: "#2563eb", fillOpacity: 0.6,
      });
      const ftype = f.facility_type || "";
      m.bindPopup(`
        <div style="font-family:inherit;font-size:13px;line-height:1.4">
          <strong>${(f.name || "facility").slice(0, 80)}</strong>
          <div style="margin-top:4px;font-family:var(--font-mono);font-size:11px;color:#666">
            ${[f.city, f.state, f.zip].filter(Boolean).join(" · ")}${ftype ? ` · ${ftype}` : ""}
          </div>
        </div>
      `);
      cluster.addLayer(m);
      added++;
    }
    cluster.addTo(mapRef.current);
    layerRef.current = cluster;
  }, [facilities]);

  return (
    <div style={{ width: "100%", height: 520, borderRadius: 8, overflow: "hidden", border: "1px solid var(--rule)" }}>
      <div ref={containerRef} style={{ width: "100%", height: "100%" }} />
    </div>
  );
}

// Customer concentration + freight-lanes graphics for an individual shipper page.
// Two side-by-side panels:
//   1) "Revenue concentration" — donut showing each named customer as a slice
//      sized by rel_strength. The 10-K disclosure rule kicks in at 10% revenue,
//      so any "over_10pct_revenue" entry is rendered as a 10% slice; "named_top_3"
//      gets ~12%, "frequent" gets 6%, "one_off" gets 3%. Remaining %% = unnamed.
//   2) "Lanes of freight" — small US SVG with the shipper's HQ as origin and
//      arrows to each named customer's resolved HQ. Customers without a
//      resolved HQ are listed as a tail under the map ("+3 unmapped").
// Inputs:
//   shipper        — full shippers row (we use hq_lat/hq_lng/hq_state)
//   outRels        — array of shipper_relationships rows from this shipper
//   customerHubs   — array of resolved customer shippers rows {id, canonical_name,
//                     hq_city, hq_state, hq_lat, hq_lng}
const STATE_CENT_SH = {
  AL:[32.7,-86.8], AK:[63.6,-152.0], AZ:[34.0,-111.6], AR:[34.7,-92.3], CA:[36.8,-119.4],
  CO:[39.0,-105.5], CT:[41.6,-72.7], DE:[38.9,-75.5], FL:[28.6,-82.4], GA:[32.6,-83.4],
  HI:[20.7,-156.3], ID:[44.2,-114.5], IL:[40.0,-89.2], IN:[39.9,-86.3], IA:[42.0,-93.5],
  KS:[38.5,-98.4], KY:[37.5,-85.3], LA:[31.0,-91.8], ME:[45.4,-69.2], MD:[39.0,-76.8],
  MA:[42.2,-71.5], MI:[44.3,-85.4], MN:[46.3,-94.3], MS:[32.7,-89.7], MO:[38.4,-92.5],
  MT:[47.0,-109.6], NE:[41.5,-99.8], NV:[39.3,-116.6], NH:[43.7,-71.6], NJ:[40.2,-74.7],
  NM:[34.4,-106.1], NY:[42.9,-75.5], NC:[35.6,-79.4], ND:[47.5,-100.5], OH:[40.3,-82.8],
  OK:[35.6,-96.9], OR:[44.0,-120.5], PA:[40.6,-77.2], RI:[41.7,-71.5], SC:[33.9,-80.9],
  SD:[44.4,-100.2], TN:[35.7,-86.3], TX:[31.0,-99.9], UT:[39.3,-111.7], VT:[44.0,-72.7],
  VA:[37.8,-78.2], WA:[47.4,-121.5], WV:[38.5,-80.5], WI:[44.6,-89.7], WY:[42.9,-107.3],
  DC:[38.9,-77.0],
};
function lonLatToXY(lon, lat, w, h) {
  // Simple equirectangular projection clipped to CONUS bounds.
  const minLon = -125, maxLon = -66, minLat = 24, maxLat = 50;
  const x = ((lon - minLon) / (maxLon - minLon)) * w;
  const y = h - ((lat - minLat) / (maxLat - minLat)) * h;
  return [x, y];
}
function ShipperCustomerGraphics({ shipper, outRels, customerHubs }) {
  // Dedupe outbound relationships by to_shipper_id (or to_name_raw if null) —
  // keep the strongest tier seen and the most recent disclosure date.
  const STRENGTH_RANK = { over_10pct_revenue: 4, named_top_3: 3, frequent: 2, one_off: 1 };
  const STRENGTH_PCT  = { over_10pct_revenue: 10, named_top_3: 12, frequent: 6, one_off: 3 };
  const dedup = React.useMemo(() => {
    const map = new Map();
    for (const r of outRels) {
      const key = r.to_shipper_id || (r.to_name_raw || "").toLowerCase();
      if (!key) continue;
      const prev = map.get(key);
      if (!prev) { map.set(key, r); continue; }
      // Keep the stronger relationship; tiebreak on most recent date.
      const a = STRENGTH_RANK[r.rel_strength] || 0;
      const b = STRENGTH_RANK[prev.rel_strength] || 0;
      if (a > b || (a === b && (r.source_date || "") > (prev.source_date || ""))) {
        map.set(key, r);
      }
    }
    return Array.from(map.values()).sort((a, b) =>
      (STRENGTH_RANK[b.rel_strength] || 0) - (STRENGTH_RANK[a.rel_strength] || 0)
    );
  }, [outRels]);

  // Concentration totals — sum of named slices, plus "rest" = the undisclosed
  // remainder (clamped at 100%).
  const slices = dedup.map((r, i) => {
    const pct = STRENGTH_PCT[r.rel_strength] || 5;
    return { name: r.to_name_raw || "(unknown)", pct, key: r.to_shipper_id || `idx-${i}` };
  });
  const namedTotal = Math.min(100, slices.reduce((s, x) => s + x.pct, 0));
  const restPct = Math.max(0, 100 - namedTotal);

  // Donut geometry — concentric arcs, stroked stroke-dasharray segments.
  const R = 70, CIRC = 2 * Math.PI * R;
  let cumulative = 0;
  const COLORS = ["#1d4ed8", "#16a34a", "#b45309", "#7c3aed", "#dc2626", "#0891b2", "#db2777", "#ca8a04"];
  const segs = slices.map((s, i) => {
    const offset = cumulative;
    const length = (s.pct / 100) * CIRC;
    cumulative += length;
    return { ...s, offset, length, color: COLORS[i % COLORS.length] };
  });
  const restOffset = cumulative;
  const restLength = (restPct / 100) * CIRC;

  // Build the map data — origin = shipper HQ, destinations = resolved customer HQs.
  const W = 360, H = 200;
  const hubsByName = React.useMemo(() => {
    const m = new Map();
    for (const h of customerHubs || []) {
      m.set((h.canonical_name || "").toLowerCase(), h);
      m.set(h.id, h);
    }
    return m;
  }, [customerHubs]);

  const origin = (shipper.hq_lat && shipper.hq_lng) ? [shipper.hq_lat, shipper.hq_lng]
    : (shipper.hq_state && STATE_CENT_SH[shipper.hq_state.toUpperCase()]) || null;
  const originXY = origin ? lonLatToXY(origin[1], origin[0], W, H) : null;

  const lanes = [];
  let unmappedCount = 0;
  for (const d of dedup) {
    let pt = null, label = d.to_name_raw || "(unknown)";
    const hub = hubsByName.get(d.to_shipper_id) || hubsByName.get((d.to_name_raw || "").toLowerCase());
    if (hub && hub.hq_lat && hub.hq_lng) pt = [hub.hq_lat, hub.hq_lng];
    else if (hub && hub.hq_state) pt = STATE_CENT_SH[(hub.hq_state || "").toUpperCase()];
    if (!pt) { unmappedCount += 1; continue; }
    const [x, y] = lonLatToXY(pt[1], pt[0], W, H);
    lanes.push({ key: d.to_shipper_id || label, x, y, label, strength: d.rel_strength });
  }

  return (
    <section className="sd-section">
      <div className="sd-section-h">
        <span>Revenue + freight pattern</span>
        <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>EDGAR · DEDUPED</span>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "minmax(220px, 1fr) minmax(280px, 1.6fr)", gap: 18 }}>
        {/* CONCENTRATION DONUT */}
        <div style={{ background: "#fff", border: "1px solid var(--rule)", borderRadius: 6, padding: "18px 20px" }}>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 12 }}>
            Customer concentration
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 18, flexWrap: "wrap" }}>
            <svg width="180" height="180" viewBox="0 0 180 180" style={{ flexShrink: 0 }}>
              <g transform="translate(90 90) rotate(-90)">
                <circle r={R} cx="0" cy="0" fill="none" stroke="#f0efe9" strokeWidth="22" />
                {segs.map((s, i) => (
                  <circle key={s.key} r={R} cx="0" cy="0" fill="none"
                    stroke={s.color} strokeWidth="22"
                    strokeDasharray={`${s.length} ${CIRC}`}
                    strokeDashoffset={-s.offset} />
                ))}
                {restPct > 0 && (
                  <circle r={R} cx="0" cy="0" fill="none"
                    stroke="#d6d3c5" strokeWidth="22"
                    strokeDasharray={`${restLength} ${CIRC}`}
                    strokeDashoffset={-restOffset} />
                )}
              </g>
              <text x="90" y="86" textAnchor="middle" style={{ fontSize: 22, fontWeight: 800, fontFamily: "var(--font-serif)" }}>
                {namedTotal}%
              </text>
              <text x="90" y="106" textAnchor="middle" style={{ fontSize: 9, letterSpacing: "0.14em", textTransform: "uppercase", fill: "#7a7a72" }}>
                disclosed
              </text>
            </svg>
            <div style={{ flex: 1, minWidth: 160 }}>
              {segs.map((s, i) => (
                <div key={s.key} style={{ display: "flex", alignItems: "center", gap: 8, padding: "3px 0", fontSize: 12 }}>
                  <span style={{ display: "inline-block", width: 10, height: 10, borderRadius: 2, background: s.color, flexShrink: 0 }} />
                  <span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.name}</span>
                  <span style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink-soft)", fontFamily: "var(--font-mono)", fontSize: 11 }}>≥{s.pct}%</span>
                </div>
              ))}
              {restPct > 0 && (
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "3px 0", fontSize: 12, marginTop: 4, borderTop: "1px solid var(--rule-soft, #f0efe9)" }}>
                  <span style={{ display: "inline-block", width: 10, height: 10, borderRadius: 2, background: "#d6d3c5", flexShrink: 0 }} />
                  <span style={{ flex: 1, color: "var(--ink-soft)" }}>Undisclosed remainder</span>
                  <span style={{ fontVariantNumeric: "tabular-nums", color: "var(--ink-soft)", fontFamily: "var(--font-mono)", fontSize: 11 }}>~{restPct}%</span>
                </div>
              )}
            </div>
          </div>
          <div style={{ marginTop: 14, fontSize: 11, color: "var(--ink-soft)", lineHeight: 1.5 }}>
            Slice size = SEC 10-K disclosure tier. <code>over 10pct revenue</code> means
            ≥10% of revenue per the company's own filing. Multi-year filings are
            deduped to one slice per customer.
          </div>
        </div>

        {/* FREIGHT LANES MAP */}
        <div style={{ background: "#fff", border: "1px solid var(--rule)", borderRadius: 6, padding: "18px 20px" }}>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 12 }}>
            Lanes of freight · {shipper.canonical_name} → named customers
          </div>
          {originXY && lanes.length > 0 ? (
            <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block", maxHeight: 260 }}>
              {/* Soft US background — a rounded rect that suggests CONUS without
                  shipping a 200KB GeoJSON. Good enough for a directional map. */}
              <rect x="0" y="0" width={W} height={H} fill="#f8f7f1" rx="10" />
              <rect x="6" y="14" width={W - 12} height={H - 28} fill="#fff" stroke="#e5e3d8" strokeWidth="1" rx="8" />
              <text x={W / 2} y={H - 6} textAnchor="middle" fill="#bdbcb1" style={{ fontSize: 9, letterSpacing: "0.18em", textTransform: "uppercase" }}>
                CONUS · approximate
              </text>
              <defs>
                <marker id="laneArrow" viewBox="0 0 10 10" refX="9" refY="5" markerUnits="strokeWidth" markerWidth="6" markerHeight="6" orient="auto">
                  <path d="M 0 0 L 10 5 L 0 10 z" fill="#1d4ed8" opacity="0.7" />
                </marker>
              </defs>
              {/* Lanes from origin to each customer */}
              {lanes.map((l) => {
                const dx = l.x - originXY[0], dy = l.y - originXY[1];
                const mx = originXY[0] + dx / 2;
                const my = originXY[1] + dy / 2 - Math.min(40, Math.hypot(dx, dy) * 0.18);
                return (
                  <g key={l.key}>
                    <path d={`M ${originXY[0]} ${originXY[1]} Q ${mx} ${my} ${l.x} ${l.y}`}
                      stroke="#1d4ed8" strokeWidth="1.5" fill="none" opacity="0.55"
                      markerEnd="url(#laneArrow)" />
                  </g>
                );
              })}
              {/* Customer dots */}
              {lanes.map((l) => (
                <g key={`d-${l.key}`}>
                  <circle cx={l.x} cy={l.y} r="5" fill="#1d4ed8" />
                  <circle cx={l.x} cy={l.y} r="9" fill="#1d4ed8" opacity="0.18" />
                  <text x={l.x} y={l.y - 10} textAnchor="middle" style={{ fontSize: 9, fontWeight: 600 }} fill="#1d4ed8">
                    {(l.label || "").split(" ").slice(0, 2).join(" ")}
                  </text>
                </g>
              ))}
              {/* Origin dot */}
              <g>
                <circle cx={originXY[0]} cy={originXY[1]} r="7" fill="#b45309" />
                <circle cx={originXY[0]} cy={originXY[1]} r="13" fill="#b45309" opacity="0.18" />
                <text x={originXY[0]} y={originXY[1] + 22} textAnchor="middle" style={{ fontSize: 10, fontWeight: 700 }} fill="#b45309">
                  {shipper.hq_city ? `${shipper.hq_city}, ${shipper.hq_state || ""}` : (shipper.hq_state || "HQ")}
                </text>
              </g>
            </svg>
          ) : (
            <div style={{ fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.5, padding: "20px 0" }}>
              {!originXY
                ? "Shipper HQ not yet on file — lanes graphic will populate once HQ is set."
                : "No customer HQs resolved yet for this shipper. Run the Sonnet enrichment to fill in customer locations."}
            </div>
          )}
          <div style={{ marginTop: 12, display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--ink-soft)" }}>
            <span>{lanes.length} mapped {lanes.length === 1 ? "lane" : "lanes"}</span>
            {unmappedCount > 0 && <span>+{unmappedCount} unmapped (HQ not on file)</span>}
          </div>
        </div>
      </div>
    </section>
  );
}

// Top-customer leaderboard. Walks all EDGAR-derived shipper_relationships,
// counts how many filers named each company as a customer, ranks them.
// Walmart at the top because 8+ CPG companies disclose them as #1 customer.
function TopCustomersWidget({ relationships, onShipper }) {
  const ranked = React.useMemo(() => {
    const norm = (s) => (s || "")
      .toLowerCase()
      .replace(/\b(inc|corp|company|co|llc|ltd|the)\.?\b/g, "")
      .replace(/[^a-z0-9 ]/g, " ")
      .replace(/\s+/g, " ")
      .trim();
    const counts = {};
    for (const r of (relationships || [])) {
      const key = norm(r.to_name_raw);
      if (!key || key.startsWith("anonymized") || key.length < 3) continue;
      // Use first encountered name as canonical for display
      if (!counts[key]) counts[key] = { name: r.to_name_raw, count: 0, examples: [] };
      counts[key].count += 1;
      if (counts[key].examples.length < 5 && r.from_name_raw)
        counts[key].examples.push(r.from_name_raw);
    }
    return Object.values(counts).sort((a, b) => b.count - a.count).slice(0, 8);
  }, [relationships]);

  if (!ranked.length) return null;

  return (
    <section style={{ maxWidth: 1180, margin: "32px auto 0", padding: "0 24px" }}>
      <style>{`
        .tc-eye {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.32em;
          text-transform: uppercase; color: var(--ink-soft);
          padding-bottom: 12px; border-bottom: 1px solid var(--rule); margin-bottom: 16px;
          display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; gap: 10px;
        }
        .tc-grid {
          display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px;
        }
        @media (max-width: 760px) { .tc-grid { grid-template-columns: 1fr; } }
        .tc-row {
          display: grid; grid-template-columns: 32px 1fr 60px;
          gap: 14px; align-items: center;
          padding: 12px 16px; border: 1px solid var(--rule); border-radius: 6px;
          background: #fff; cursor: pointer;
          transition: border-color 0.12s, transform 0.12s;
        }
        .tc-row:hover { border-color: var(--ink); transform: translateY(-1px); }
        .tc-rank {
          font-family: var(--font-serif); font-size: 22px; font-weight: 600;
          color: var(--ink-soft); text-align: right;
        }
        .tc-rank.top { color: oklch(0.45 0.20 25); }
        .tc-name {
          font-family: var(--font-serif); font-size: 17px; line-height: 1.2;
          letter-spacing: -0.005em; color: var(--ink);
        }
        .tc-meta {
          font-family: var(--font-mono); font-size: 11px;
          color: var(--ink-soft); margin-top: 4px; letter-spacing: 0.04em;
        }
        .tc-count {
          font-family: var(--font-serif); font-size: 22px; font-weight: 600;
          text-align: right; color: var(--ink);
        }
        .tc-count-label {
          font-family: var(--font-mono); font-size: 9px;
          color: var(--ink-soft); letter-spacing: 0.14em; text-align: right; text-transform: uppercase;
        }
      `}</style>
      <div className="tc-eye">
        <span>Most-named customers · EDGAR 10-K disclosures</span>
        <span style={{fontSize:10, letterSpacing:"0.16em"}}>The companies everyone else depends on</span>
      </div>
      <div className="tc-grid">
        {ranked.map((c, i) => (
          <button key={c.name} className="tc-row" onClick={() => onShipper && onShipper(c.name)}>
            <div className={`tc-rank${i === 0 ? " top" : ""}`}>{i + 1}</div>
            <div>
              <div className="tc-name">{c.name}</div>
              <div className="tc-meta">named by: {c.examples.slice(0, 3).join(" · ")}{c.examples.length > 3 ? ` +${c.examples.length - 3}` : ""}</div>
            </div>
            <div>
              <div className="tc-count">{c.count}</div>
              <div className="tc-count-label">filers</div>
            </div>
          </button>
        ))}
      </div>
    </section>
  );
}

// 2-digit NAICS sector → human-friendly industry label. Covers every
// real freight-shipping sector; rare codes fall through to "Other".
// Reference: census.gov/naics
const NAICS_INDUSTRY = {
  "11": "Agriculture / Forestry",
  "21": "Mining / Oil & Gas",
  "22": "Utilities",
  "23": "Construction",
  "31": "Manufacturing — Food / Beverage",
  "32": "Manufacturing — Materials / Chemical",
  "33": "Manufacturing — Goods / Vehicles",
  "42": "Wholesale Trade",
  "44": "Retail Trade",
  "45": "Retail Trade",
  "48": "Transportation",
  "49": "Warehousing / Postal",
  "51": "Information",
  "52": "Finance / Insurance",
  "53": "Real Estate",
  "54": "Professional Services",
  "55": "Management of Companies",
  "56": "Admin / Support / Waste",
  "61": "Educational Services",
  "62": "Health Care",
  "71": "Arts / Entertainment",
  "72": "Accommodation / Food Services",
  "81": "Other Services",
  "92": "Public Administration",
};

function industryFor(naics) {
  if (!naics) return "Unknown";
  return NAICS_INDUSTRY[String(naics).slice(0, 2)] || "Other";
}

// 3-digit NAICS prefix → array of typical freight types this shipper
// produces. Industry-curated mapping; multi-typed shippers (food mfg
// shipping both reefer + dry van, etc.) get both labels.
const NAICS_FREIGHT_MAP = {
  "111": ["Bulk", "Reefer"],            // crop production
  "112": ["Reefer", "Livestock"],        // animal production
  "113": ["Lumber"],                     // forestry
  "114": ["Reefer"],                     // fishing
  "211": ["Tanker"],                     // oil + gas extraction
  "212": ["Bulk", "Flatbed"],            // mining
  "311": ["Reefer", "Dry van"],          // food mfg
  "312": ["Reefer", "Dry van"],          // beverage + tobacco mfg
  "313": ["Dry van"],                    // textile mills
  "314": ["Dry van"],                    // textile products
  "315": ["Dry van"],                    // apparel
  "316": ["Dry van"],                    // leather
  "321": ["Lumber", "Flatbed"],          // wood products
  "322": ["Dry van"],                    // paper mfg
  "323": ["Dry van"],                    // printing
  "324": ["Tanker", "Hazmat"],           // petroleum + coal
  "325": ["Tanker", "Hazmat"],           // chemical mfg
  "326": ["Dry van"],                    // plastics + rubber
  "327": ["Bulk", "Flatbed"],            // nonmetallic mineral (cement, glass)
  "331": ["Flatbed", "Bulk"],            // primary metal
  "332": ["Flatbed"],                    // fabricated metal
  "333": ["Auto / Heavy", "Flatbed"],    // machinery
  "334": ["Dry van"],                    // computer + electronics
  "335": ["Dry van"],                    // electrical appliances
  "336": ["Auto / Heavy", "Dry van"],    // transportation equipment (auto, aero)
  "337": ["Dry van"],                    // furniture
  "339": ["Dry van"],                    // misc mfg
  "423": ["Dry van", "Flatbed"],         // merchant wholesalers, durable
  "424": ["Reefer", "Dry van"],          // merchant wholesalers, nondurable
  "425": ["Dry van"],                    // wholesale electronic markets
  "441": ["Auto / Heavy"],               // motor vehicle dealers
  "442": ["Dry van"],                    // furniture stores
  "443": ["Dry van"],                    // electronics stores
  "444": ["Lumber", "Dry van"],          // building material dealers
  "445": ["Reefer"],                     // food + beverage stores
  "446": ["Dry van"],                    // health + personal care
  "447": ["Tanker"],                     // gasoline stations
  "448": ["Dry van"],                    // clothing
  "451": ["Dry van"],                    // sporting goods, hobby
  "452": ["Dry van", "Reefer"],          // general merchandise (Walmart, Target, big box)
  "453": ["Dry van"],                    // misc retail
  "454": ["Dry van"],                    // nonstore retailers (Amazon, e-com)
  "722": ["Reefer"],                     // restaurants → reefer inbound
};

const ALL_FREIGHT_TYPES = [
  "Reefer", "Dry van", "Flatbed", "Tanker", "Bulk",
  "Hazmat", "Auto / Heavy", "Lumber", "Livestock",
];

function freightTypesFor(naics) {
  if (!naics) return [];
  const p3 = String(naics).slice(0, 3);
  return NAICS_FREIGHT_MAP[p3] || [];
}

function ShipperList({ onNav }) {
  const [shippers, setShippers] = useStateSh([]);
  const [facilities, setFacilities] = useStateSh([]);
  const [relationships, setRelationships] = useStateSh([]);
  const [loading, setLoading]   = useStateSh(true);
  const [stats, setStats]       = useStateSh({ total: null, public: null, relationships: null });
  const [query, setQuery]       = useStateSh("");
  // RPC-backed search results — populated when the user types 2+ chars.
  // Searches the WHOLE shippers table (30K+ rows) via search_shippers(),
  // not just the pre-fetched 2K. Without this, anchor shippers like
  // Tyson Foods that live in the long tail were unfindable from the
  // directory even when the user typed the company name verbatim.
  const [searchResults, setSearchResults] = useStateSh(null); // null = not active
  const [searching, setSearching] = useStateSh(false);
  const [stateFilter, setStateFilter] = useStateSh("all");
  const [sizeFilter, setSizeFilter]   = useStateSh("all");
  const [industryFilter, setIndustryFilter] = useStateSh("all");
  const [freightFilter, setFreightFilter]   = useStateSh("all");
  const [sortBy, setSortBy]     = useStateSh("depth");  // depth | name | state | industry | size
  // Default to "With data" — surfaces shippers with relationships, named
  // customers, or material signals first. The 29K-row long tail is mostly
  // OSM-pinned facility shells without B2B disclosures yet, so showing them
  // alphabetically by default makes the directory look hollow.
  const [hasRel, setHasRel]     = useStateSh(true);
  const [page, setPage]         = useStateSh(0);
  const PAGE_SIZE = 50;

  // Names that look like OSM industrial polygons but AREN'T freight shippers.
  // Power plants, substations, water + wastewater treatment, cell towers,
  // schools, hospitals, etc. The OSM ingest grabbed every landuse=industrial
  // polygon — these slipped in. Filtered client-side here; SQL hygiene pass
  // is a separate followup that flags these in the underlying tables.
  const NON_SHIPPER_RE = /\b(power\s+plant|generating\s+station|substation|switchyard|water\s+plant|water\s+works|wastewater|water\s+treatment|sewage|sewer\s+plant|wwtp|cell\s+tower|antenna|transmitter|cellular\s+tower|broadcast\s+tower|fire\s+station|police\s+station|sheriff|elementary\s+school|middle\s+school|high\s+school|university|community\s+college|hospital|medical\s+center|landfill|recycling\s+center|gas\s+well|oil\s+well|wind\s+farm|solar\s+farm|reservoir|dam|cemetery|park|sports\s+complex|stadium|arena|airport\s+terminal|train\s+station|bus\s+station|post\s+office|courthouse|jail|prison|correctional\s+facility|library|museum)\b/i;

  // ----- initial fetch ------------------------------------------------------
  useEffectSh(() => {
    let alive = true;
    if (!window.SI_DB || !window.SI_DB.raw) return;
    (async () => {
      // Fire the three real stat counts in parallel. PostgREST caps response
      // rows at 1000 by default, so any `rows.length` count was wrong; these
      // use Prefer:count=exact + Range:0-0 so the server returns just the
      // total in the Content-Range header.
      try {
        const [totalCount, publicCount, relCount] = await Promise.all([
          window.SI_DB.raw.count("shippers", "").catch(() => null),
          window.SI_DB.raw.count("shippers", "is_public=eq.true").catch(() => null),
          window.SI_DB.raw.count("shipper_relationships", "").catch(() => null),
        ]);
        if (alive) setStats({
          total: totalCount,
          public: publicCount,
          // Relationships are stored bidirectionally (one row per pair),
          // so divide by 2 to get unique pairs. Cheap approximation —
          // not all relationships are perfectly mirrored.
          relationships: relCount != null ? Math.round(relCount / 2) : null,
        });
      } catch {}

      try {
        // Pull view that includes signal_count + distinct_sources + relationships.
        // Sort multi-key so populated profiles surface before bare shells:
        // 1) data_score (relationships * 3 + distinct_sources + signal_count)
        // 2) shipper_score
        // 3) name (alphabetical tiebreaker)
        const rows = await window.SI_DB.raw.select(
          "shippers_with_signal_counts",
          "select=id,slug,canonical_name,hq_state,hq_city,primary_naics,is_public,ticker,size_band,shipper_score,signal_count,distinct_sources,outbound_relationships,inbound_relationships&order=shipper_score.desc,canonical_name.asc&limit=2000"
        );
        if (!alive) return;
        // Drop OSM noise (Bryan Power Plant et al) before ranking.
        const cleaned = (rows || []).filter(r => !NON_SHIPPER_RE.test(r.canonical_name || ""));
        // Client-side resort by data richness so directory leads with depth.
        const enriched = cleaned.map(r => {
          const rels = (r.outbound_relationships || 0) + (r.inbound_relationships || 0);
          const sources = r.distinct_sources || 0;
          const sigs = r.signal_count || 0;
          const sized = r.size_band && r.size_band !== "unknown" ? 1 : 0;
          // Heuristic: relationships are the strongest signal of real depth,
          // sources next, then signal volume, with a small bonus for a known
          // size band so curated mega-shippers don't fall below thin EDGAR
          // entries with one stray signal.
          r._depth = rels * 5 + sources * 2 + sigs + sized;
          return r;
        });
        enriched.sort((a, b) => {
          if (b._depth !== a._depth) return b._depth - a._depth;
          if ((b.shipper_score || 0) !== (a.shipper_score || 0)) {
            return (b.shipper_score || 0) - (a.shipper_score || 0);
          }
          return (a.canonical_name || "").localeCompare(b.canonical_name || "");
        });
        setShippers(enriched);
      } catch (e) {
        // silent — empty list shown
      } finally {
        if (alive) setLoading(false);
      }

      // Background fetch facilities for the map + relationships for top
      // customers. All in parallel so the page hydrates fast.
      try {
        const [a, b, c, rels] = await Promise.all([
          window.SI_DB.raw.select("shipper_facilities",
            "select=lat,lng,name,city,state,zip,facility_type&lat=not.is.null&order=id&limit=1000"),
          window.SI_DB.raw.select("shipper_facilities",
            "select=lat,lng,name,city,state,zip,facility_type&lat=not.is.null&order=id&limit=1000&offset=1000"),
          window.SI_DB.raw.select("shipper_facilities",
            "select=lat,lng,name,city,state,zip,facility_type&lat=not.is.null&order=id&limit=1000&offset=2000"),
          window.SI_DB.raw.select("shipper_relationships",
            "select=from_name_raw,to_name_raw,rel_strength&source=eq.edgar-10k&to_shipper_id=not.is.null&limit=500"),
        ]);
        if (!alive) return;
        // Strip OSM noise from the map too — power plants etc. were the
        // first dots in the dataset, so removing them pulls the map away
        // from College Station + reveals the true national distribution.
        const allFac = [...(a || []), ...(b || []), ...(c || [])];
        setFacilities(allFac.filter(f => !NON_SHIPPER_RE.test(f.name || "")));
        setRelationships(rels || []);
      } catch {}
    })();
    return () => { alive = false; };
  }, []);

  // ----- search via search_shippers RPC -------------------------------------
  // Fires when the user types 2+ chars, debounced 250ms. Replaces the
  // previous in-memory .includes() filter that was blind to anchor
  // shippers in the long tail (30K rows total, only 2K pre-fetched).
  // The trigram-fuzzy RPC catches typos and abbreviations too.
  useEffectSh(() => {
    const q = query.trim();
    if (q.length < 2) {
      setSearchResults(null);   // hand the list back to in-memory filter
      setSearching(false);
      return;
    }
    if (!window.SI_DB || !window.SI_DB.raw || !window.SI_DB.raw.rpc) return;
    setSearching(true);
    let alive = true;
    const t = setTimeout(async () => {
      const rows = await window.SI_DB.raw.rpc("search_shippers", { q, lim: 50 });
      if (!alive) return;
      // Annotate each result with the same _depth field the in-memory
      // path computes, so downstream sorting + display stays uniform.
      const enriched = (rows || []).map(r => {
        const rels = (r.outbound_relationships || 0) + (r.inbound_relationships || 0);
        const sources = r.distinct_sources || 0;
        const sigs = r.signal_count || 0;
        const sized = r.size_band && r.size_band !== "unknown" ? 1 : 0;
        r._depth = rels * 5 + sources * 2 + sigs + sized;
        return r;
      });
      setSearchResults(enriched);
      setSearching(false);
    }, 250);
    return () => { alive = false; clearTimeout(t); };
  }, [query]);

  // ----- derived list -------------------------------------------------------
  const states = useMemoSh(() => {
    const s = new Set(shippers.map(r => r.hq_state).filter(Boolean));
    return ["all", ...[...s].sort()];
  }, [shippers]);

  const filtered = useMemoSh(() => {
    // When a search is active, the RPC has already done the name-match
    // work over the FULL shippers table — we just apply the secondary
    // filters (state / size / industry / freight / has-relationships)
    // on top. Fall back to the pre-fetched 2K rows when the search box
    // is empty so the directory renders instantly on load.
    const source = searchResults != null ? searchResults : shippers;
    const inSearchMode = searchResults != null;
    return source.filter(r => {
      // Skip the in-memory name match when the RPC already handled it.
      if (!inSearchMode) {
        const q = query.trim().toLowerCase();
        if (q && !(r.canonical_name || "").toLowerCase().includes(q) &&
                !(r.ticker || "").toLowerCase().includes(q)) return false;
      }
      if (stateFilter !== "all" && r.hq_state !== stateFilter) return false;
      if (sizeFilter !== "all" && r.size_band !== sizeFilter) return false;
      if (industryFilter !== "all" && industryFor(r.primary_naics) !== industryFilter) return false;
      if (freightFilter !== "all") {
        const types = freightTypesFor(r.primary_naics);
        if (!types.includes(freightFilter)) return false;
      }
      // The "with data" toggle is intentionally ignored when actively
      // searching — if a user types "Tyson", they want Tyson, even
      // if Tyson has zero relationships in our DB yet.
      if (!inSearchMode && hasRel && !(r.outbound_relationships || r.inbound_relationships)) return false;
      return true;
    });
  }, [shippers, searchResults, query, stateFilter, sizeFilter, industryFilter, freightFilter, hasRel]);

  // Sort the filtered list. The default 'depth' is whatever order the
  // initial fetch produced (already depth-sorted by data richness); the
  // other options re-sort client-side.
  const sorted = useMemoSh(() => {
    if (sortBy === "depth") return filtered;
    const arr = [...filtered];
    if (sortBy === "name") {
      arr.sort((a, b) => (a.canonical_name || "").localeCompare(b.canonical_name || ""));
    } else if (sortBy === "state") {
      arr.sort((a, b) =>
        (a.hq_state || "ZZ").localeCompare(b.hq_state || "ZZ") ||
        (a.canonical_name || "").localeCompare(b.canonical_name || "")
      );
    } else if (sortBy === "industry") {
      arr.sort((a, b) =>
        industryFor(a.primary_naics).localeCompare(industryFor(b.primary_naics)) ||
        (a.canonical_name || "").localeCompare(b.canonical_name || "")
      );
    } else if (sortBy === "size") {
      const order = { enterprise: 0, mid: 1, small: 2, unknown: 3 };
      arr.sort((a, b) =>
        (order[a.size_band] ?? 4) - (order[b.size_band] ?? 4) ||
        (a.canonical_name || "").localeCompare(b.canonical_name || "")
      );
    }
    return arr;
  }, [filtered, sortBy]);

  const visible = useMemoSh(() => sorted.slice(0, (page + 1) * PAGE_SIZE), [sorted, page]);
  const hasMore = sorted.length > visible.length;

  // ----- render -------------------------------------------------------------
  return (
    <div className="sh-page">
      <style>{`
        .sh-page { background: var(--paper, #fff); min-height: 100vh; }
        .sh-hero { max-width: 1180px; margin: 0 auto; padding: 64px 24px 32px; }
        .sh-eyebrow {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.32em;
          text-transform: uppercase; color: var(--ink-soft); margin-bottom: 18px;
          display: inline-flex; align-items: center; gap: 10px;
        }
        .sh-eyebrow .dot { width: 8px; height: 8px; border-radius: 50%;
          background: oklch(0.55 0.18 250); animation: sh-pulse 1.6s infinite; }
        @keyframes sh-pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
        .sh-h1 {
          font-family: var(--font-serif); font-size: 56px; line-height: 1.04;
          letter-spacing: -0.025em; margin: 0; font-weight: 500; color: var(--ink);
          max-width: 860px;
        }
        @media (max-width: 700px) { .sh-h1 { font-size: 36px; } }
        .sh-sub {
          margin-top: 18px; font-size: 18px; line-height: 1.5; color: var(--ink-soft);
          max-width: 720px;
        }

        .sh-stats {
          margin-top: 40px; padding: 24px 0; border-top: 1px solid var(--rule);
          border-bottom: 1px solid var(--rule);
          display: grid; grid-template-columns: repeat(4, 1fr); gap: 0;
        }
        @media (max-width: 720px) { .sh-stats { grid-template-columns: repeat(2, 1fr); } }
        .sh-stat { padding: 0 24px; border-right: 1px solid var(--rule); }
        .sh-stat:first-child { padding-left: 0; }
        .sh-stat:last-child { border-right: 0; padding-right: 0; }
        @media (max-width: 720px) {
          .sh-stat:nth-child(2n) { border-right: 0; }
          .sh-stat:nth-child(2n+1) { padding-left: 0; }
        }
        .sh-stat-num {
          font-family: var(--font-serif); font-size: 32px; line-height: 1;
          letter-spacing: -0.02em; font-weight: 600; color: var(--ink);
        }
        .sh-stat-l {
          margin-top: 6px; font-family: var(--font-mono); font-size: 10px;
          letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-soft);
        }

        .sh-controls {
          max-width: 1180px; margin: 32px auto 0; padding: 0 24px;
          display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
        }
        .sh-search {
          flex: 1; min-width: 240px;
          padding: 12px 14px; border: 1px solid var(--rule); border-radius: 6px;
          background: var(--paper); color: var(--ink); outline: none;
          font: inherit; font-size: 15px;
        }
        .sh-search:focus { border-color: var(--ink); }
        .sh-select {
          padding: 11px 12px; border: 1px solid var(--rule); border-radius: 6px;
          background: var(--paper); color: var(--ink); font: inherit; font-size: 14px;
          cursor: pointer;
        }
        .sh-pill {
          padding: 9px 14px; border: 1px solid var(--rule); border-radius: 999px;
          background: var(--paper); color: var(--ink-soft); cursor: pointer;
          font: inherit; font-size: 12px; letter-spacing: 0.04em;
          transition: border-color 0.15s, color 0.15s, background 0.15s;
        }
        .sh-pill:hover {
          border-color: oklch(0.50 0.18 250); color: oklch(0.40 0.16 250);
        }
        .sh-pill.active {
          background: oklch(0.50 0.18 250); color: #fff;
          border-color: oklch(0.50 0.18 250);
          box-shadow: 0 3px 10px oklch(0.50 0.18 250 / 0.30);
        }

        .sh-list { max-width: 1180px; margin: 24px auto 0; padding: 0 24px 96px; }
        .sh-empty {
          padding: 64px 24px; text-align: center; color: var(--ink-soft);
          font-size: 15px; line-height: 1.5;
        }
        .sh-empty strong { color: var(--ink); }
        .sh-row {
          display: grid;
          grid-template-columns: 1fr 110px 110px 90px 110px;
          gap: 16px; align-items: center;
          padding: 16px 0 16px 16px;
          border-bottom: 1px solid var(--rule);
          cursor: pointer;
          border-left: 3px solid transparent;
          margin-left: -16px;
          transition: background 0.12s, border-left-color 0.18s, padding-left 0.12s;
        }
        .sh-row:hover {
          background: oklch(0.97 0.025 250);
          border-left-color: oklch(0.50 0.18 250);
        }
        @media (max-width: 880px) {
          .sh-row { grid-template-columns: 1fr 80px 80px; }
          .sh-row .sh-row-naics, .sh-row .sh-row-rels { display: none; }
        }
        .sh-row-name {
          font-family: var(--font-serif); font-size: 18px; letter-spacing: -0.005em;
          color: var(--ink); display: flex; align-items: center; gap: 10px;
        }
        .sh-pub-tag {
          font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.14em;
          padding: 2px 6px; border-radius: 2px;
          background: oklch(0.94 0.04 250); color: oklch(0.40 0.10 250);
          font-weight: 700; text-transform: uppercase;
        }
        .sh-row-loc, .sh-row-naics, .sh-row-score, .sh-row-rels {
          font-family: var(--font-mono); font-size: 12px; color: var(--ink-soft);
          letter-spacing: 0.04em;
        }
        .sh-row-score strong { color: var(--ink); font-size: 14px; }

        .sh-loadmore {
          margin: 32px auto 0; display: block; padding: 14px 28px;
          font: inherit; font-size: 14px; cursor: pointer;
          border: 1px solid oklch(0.50 0.18 250);
          background: var(--paper); color: oklch(0.40 0.18 250);
          border-radius: 6px; font-weight: 600;
          transition: background 0.15s, color 0.15s, box-shadow 0.15s, transform 0.1s;
        }
        .sh-loadmore:hover {
          background: oklch(0.50 0.18 250); color: #fff;
          box-shadow: 0 6px 18px oklch(0.50 0.18 250 / 0.28);
          transform: translateY(-1px);
        }

        .sh-paid-bar {
          margin-top: 32px; padding: 24px;
          background: linear-gradient(135deg, oklch(0.96 0.04 250) 0%, oklch(0.98 0.02 250) 100%);
          border: 1px solid oklch(0.80 0.08 250);
          border-left: 5px solid oklch(0.50 0.18 250);
          border-radius: 8px;
          box-shadow: 0 4px 14px oklch(0.50 0.18 250 / 0.10);
        }
        .sh-paid-bar-h {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.22em;
          text-transform: uppercase; color: oklch(0.40 0.16 250);
          margin-bottom: 8px; font-weight: 700;
        }
        .sh-paid-bar-t {
          font-family: var(--font-serif); font-size: 22px; letter-spacing: -0.01em;
          margin: 0 0 6px; color: var(--ink);
        }
        .sh-paid-bar-s { color: var(--ink-soft); font-size: 14px; line-height: 1.5; margin: 0; }
      `}</style>

      <section className="sh-hero">
        <div className="sh-eyebrow"><span className="dot" /> Shipper Directory</div>
        <h1 className="sh-h1">Every meaningful U.S. freight shipper, indexed.</h1>
        <p className="sh-sub">
          Names, locations, NAICS, B2B relationships, physical-footprint signals.
          Built from public SEC filings, OpenStreetMap industrial polygons, and
          state corporation records. Free directory; paid contacts and detail.
        </p>

        <div className="sh-stats">
          <div className="sh-stat">
            <div className="sh-stat-num">{stats.total == null ? "…" : stats.total.toLocaleString()}</div>
            <div className="sh-stat-l">Shippers indexed</div>
          </div>
          <div className="sh-stat">
            <div className="sh-stat-num">{stats.public == null ? "…" : stats.public.toLocaleString()}</div>
            <div className="sh-stat-l">Public companies</div>
          </div>
          <div className="sh-stat">
            <div className="sh-stat-num">{stats.relationships == null ? "…" : stats.relationships.toLocaleString()}</div>
            <div className="sh-stat-l">B2B relationships</div>
          </div>
          <div className="sh-stat">
            <div className="sh-stat-num">$0</div>
            <div className="sh-stat-l"><strong style={{color:"oklch(0.55 0.18 145)"}}>Free</strong> · directory tier</div>
          </div>
        </div>
      </section>

      {/* Facility map — visceral proof of national coverage */}
      <section style={{ maxWidth: 1180, margin: "20px auto 0", padding: "0 24px" }}>
        <div style={{
          fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.32em",
          textTransform: "uppercase", color: "var(--ink-soft)",
          paddingBottom: 12, borderBottom: "1px solid var(--rule)", marginBottom: 14,
          display: "flex", justifyContent: "space-between", alignItems: "baseline", flexWrap: "wrap", gap: 10,
        }}>
          <span>National facility footprint · click clusters to zoom</span>
          <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>
            {facilities.length > 0 ? `${facilities.length.toLocaleString()} facilities mapped` : "Loading…"}
          </span>
        </div>
        {facilities.length > 0 ? (
          <ShipperFacilityMap facilities={facilities} />
        ) : !loading ? (
          <div style={{ padding: 24, textAlign: "center", fontSize: 13, color: "var(--ink-soft)", border: "1px solid var(--rule)", borderRadius: 6 }}>
            No facility coords available yet.
          </div>
        ) : null}
      </section>

      {/* Top-customer leaderboard — Walmart leads because 8+ CPG companies
          name them as the #1 customer */}
      {relationships.length > 0 && (
        <TopCustomersWidget
          relationships={relationships}
          onShipper={(name) => setQuery(name.split(" ")[0])}
        />
      )}

      <div className="sh-controls" style={{ marginTop: 32 }}>
        <input
          type="text"
          className="sh-search"
          placeholder="Search by name or ticker…"
          value={query}
          onChange={e => { setQuery(e.target.value); setPage(0); }}
          autoComplete="off"
        />
        <select className="sh-select" value={stateFilter} onChange={e => { setStateFilter(e.target.value); setPage(0); }}>
          {states.map(s => (
            <option key={s} value={s}>{s === "all" ? "All states" : s}</option>
          ))}
        </select>
        <select className="sh-select" value={sizeFilter} onChange={e => { setSizeFilter(e.target.value); setPage(0); }}>
          <option value="all">All sizes</option>
          <option value="enterprise">Enterprise</option>
          <option value="mid">Mid-market</option>
          <option value="small">Small</option>
          <option value="unknown">Unknown</option>
        </select>
        <select className="sh-select" value={industryFilter}
          onChange={e => { setIndustryFilter(e.target.value); setPage(0); }}
          title="Filter by 2-digit NAICS industry">
          <option value="all">All industries</option>
          {[...new Set(Object.values(NAICS_INDUSTRY))].sort().map(name =>
            <option key={name} value={name}>{name}</option>
          )}
        </select>
        <select className="sh-select" value={freightFilter}
          onChange={e => { setFreightFilter(e.target.value); setPage(0); }}
          title="Filter by likely freight type, derived from NAICS">
          <option value="all">All freight types</option>
          {ALL_FREIGHT_TYPES.map(t =>
            <option key={t} value={t}>{t}</option>
          )}
        </select>
        <select className="sh-select" value={sortBy}
          onChange={e => { setSortBy(e.target.value); setPage(0); }}
          title="Sort order">
          <option value="depth">Sort: data depth</option>
          <option value="name">Sort: name A→Z</option>
          <option value="state">Sort: state</option>
          <option value="industry">Sort: industry</option>
          <option value="size">Sort: size band</option>
        </select>
        <button className={`sh-pill ${hasRel ? "active" : ""}`}
          onClick={() => { setHasRel(!hasRel); setPage(0); }}
          title={hasRel ? "Showing shippers with relationships, customers, or signals on file" : "Click to also show the full directory long tail"}>
          {hasRel ? "Showing populated · click for all 29,762" : "Show all 29,762"}
        </button>
      </div>

      <div className="sh-list">
        {loading ? (
          <div className="sh-empty">Loading…</div>
        ) : filtered.length === 0 ? (
          <div className="sh-empty">
            {shippers.length === 0
              ? <>The directory is being built right now. <strong>New rows land continuously</strong> — refresh in a few minutes.</>
              : "No shippers match those filters."}
          </div>
        ) : (
          <>
            {visible.map(r => (
              <div key={r.id} className="sh-row" onClick={() => { window.location.hash = `#/shipper/${r.slug}`; }}>
                <div className="sh-row-name">
                  {r.canonical_name}
                  {r.is_public && r.ticker && <span className="sh-pub-tag">{r.ticker}</span>}
                </div>
                <div className="sh-row-loc">{r.hq_state || "—"}{r.hq_city ? ` · ${r.hq_city}` : ""}</div>
                <div className="sh-row-naics">
                  <div>{industryFor(r.primary_naics)}</div>
                  {(() => {
                    const types = freightTypesFor(r.primary_naics);
                    return types.length > 0 ? (
                      <div style={{ marginTop: 3, fontSize: 10, color: "var(--ink-soft)", fontFamily: "var(--font-mono)" }}>
                        {types.join(" · ")}
                      </div>
                    ) : null;
                  })()}
                </div>
                <div className="sh-row-score">score <strong>{r.shipper_score || 0}</strong></div>
                <div className="sh-row-rels">
                  {(r.outbound_relationships || 0) + (r.inbound_relationships || 0)} rel · {r.distinct_sources || 0} src
                </div>
              </div>
            ))}
            {hasMore && (
              <button className="sh-loadmore" onClick={() => setPage(page + 1)}>
                Load {Math.min(PAGE_SIZE, filtered.length - visible.length)} more · {filtered.length - visible.length} remaining
              </button>
            )}
            <div className="sh-paid-bar">
              <div className="sh-paid-bar-h">Paid tier · coming soon</div>
              <h3 className="sh-paid-bar-t">Per-shipper detail, decision-maker emails, and the full B2B graph.</h3>
              <p className="sh-paid-bar-s">
                Click into any shipper to see their named carriers, their suppliers, every disclosed major-customer percentage, plus mapped facilities. Decision-maker emails (procurement VPs, logistics directors) and verified contact data — paid tier. Schema is live; pricing & checkout shipping with the rest of Tier-1 revenue.
              </p>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// =============================================================
// ShipperDetail — per-shipper deep dive at /#/shipper/<slug>
// =============================================================
function ShipperDetail({ onNav, slug }) {
  const [shipper, setShipper] = useStateSh(null);
  const [outRels, setOutRels] = useStateSh([]);   // outbound: this shipper's named customers
  const [customerHubs, setCustomerHubs] = useStateSh([]);  // resolved HQs for outbound customers (freight-lane graphic)
  const [inRels,  setInRels]  = useStateSh([]);   // inbound: who names this shipper as a customer
  const [facilities, setFacilities] = useStateSh([]);
  const [signals,    setSignals]    = useStateSh([]);
  const [contactCount, setContactCount] = useStateSh(0);
  const [aliases, setAliases] = useStateSh([]);
  const [peers, setPeers] = useStateSh([]);
  const [nearbyLanes, setNearbyLanes] = useStateSh([]);
  const [oshaHits, setOshaHits] = useStateSh([]);   // OSHA SIR + fatality matching shipper name
  const [epaHits, setEpaHits]   = useStateSh([]);   // EPA ECHO compliance hits matching shipper name
  const [loading, setLoading] = useStateSh(true);
  const [notFound, setNotFound] = useStateSh(false);

  useEffectSh(() => {
    let alive = true;
    if (!window.SI_DB || !window.SI_DB.raw) return;
    (async () => {
      try {
        const sRows = await window.SI_DB.raw.select(
          "shippers_with_signal_counts",
          `select=*&slug=eq.${encodeURIComponent(slug)}&limit=1`
        );
        if (!alive) return;
        if (!sRows || sRows.length === 0) { setNotFound(true); return; }
        const s = sRows[0];
        setShipper(s);
        // Pull related data in parallel
        // Build a "loose name pattern" for fuzzy matching against the
        // OSHA + EPA federal datasets, which key on employer_name strings
        // not our canonical shipper IDs. Strip the legal-entity suffixes
        // that vary across data sources (Inc / LLC / Corp / Holdings / etc.)
        // so the ilike pattern catches "Walmart Stores Inc" / "Walmart Inc"
        // / "Walmart" all from a single fetch.
        const stripSuffix = (n) => (n || "")
          .toLowerCase()
          .replace(/[\s,.]*(inc|incorporated|corp|corporation|llc|l\.l\.c|ltd|limited|co|company|holdings|group|enterprises)\.?\s*$/i, "")
          .trim();
        const namePattern = "*" + encodeURIComponent(stripSuffix(s.canonical_name)) + "*";

        const [out, inb, fac, sig, ali, osha, epa] = await Promise.all([
          window.SI_DB.raw.select("shipper_relationships",
            `select=*&from_shipper_id=eq.${s.id}&order=source_date.desc&limit=50`),
          window.SI_DB.raw.select("shipper_relationships",
            `select=*&to_shipper_id=eq.${s.id}&order=source_date.desc&limit=50`),
          window.SI_DB.raw.select("shipper_facilities",
            `select=*&shipper_id=eq.${s.id}&limit=200`),
          window.SI_DB.raw.select("shipper_signals",
            `select=source,signal_type,signal_data,source_url,observed_at&shipper_id=eq.${s.id}&order=observed_at.desc&limit=100`),
          window.SI_DB.raw.select("shipper_aliases",
            `select=alias,alias_type&shipper_id=eq.${s.id}&limit=20`),
          // OSHA severe-injury + fatality records matching the shipper name
          window.SI_DB.raw.select(
            "osha_injury_reports",
            `select=source_id,report_type,employer_name,city,state,naics_code,industry_description,event_date,hospitalized,amputation,body_part,event_description&employer_name_lower=ilike.${namePattern}&order=event_date.desc&limit=80`
          ).catch(() => []),
          // EPA ECHO compliance hits matching the shipper name
          window.SI_DB.raw.select(
            "epa_facility_compliance",
            `select=registry_id,facility_name,city,state,primary_naics,significant_violator,compliance_status,violations_3yr,enforcement_actions_5yr,total_penalties_5yr,last_inspection_date,last_violation_date&facility_name_lower=ilike.${namePattern}&limit=80`
          ).catch(() => []),
        ]);
        if (!alive) return;
        setOutRels(out || []); setInRels(inb || []);
        setFacilities(fac || []); setSignals(sig || []);
        setAliases(ali || []);
        setOshaHits(Array.isArray(osha) ? osha : []);
        setEpaHits(Array.isArray(epa) ? epa : []);

        // Resolve named-customer HQ locations for the freight-lane graphic.
        // Outbound rels with a to_shipper_id can be joined to the shippers
        // table to get hq_city/state. We dedupe by to_shipper_id first so
        // multi-year filings of the same customer only count once.
        try {
          const outArr = out || [];
          const ids = Array.from(new Set(outArr.map(r => r.to_shipper_id).filter(Boolean)));
          if (ids.length) {
            const inList = ids.map(encodeURIComponent).join(",");
            const targets = await window.SI_DB.raw.select(
              "shippers",
              `select=id,canonical_name,hq_city,hq_state,hq_lat,hq_lng&id=in.(${inList})&limit=${ids.length}`
            );
            if (alive) setCustomerHubs(Array.isArray(targets) ? targets : []);
          } else {
            if (alive) setCustomerHubs([]);
          }
        } catch { if (alive) setCustomerHubs([]); }

        // Cheap count of contacts (paywalled — show count, not values)
        const cc = await window.SI_DB.raw.count("shipper_contacts", `shipper_id=eq.${s.id}`);
        if (alive) setContactCount(cc || 0);

        // Peer shippers — same NAICS or same size band, exclude self.
        // Falls back to size-band peers when no NAICS is on file. Either way
        // every populated shipper page gets a "Compare to peers" block.
        try {
          const peerRows = s.primary_naics
            ? await window.SI_DB.raw.select(
                "shippers",
                `select=canonical_name,slug,size_band,description,hq_state&primary_naics=eq.${encodeURIComponent(s.primary_naics)}&id=neq.${s.id}&order=size_band.asc&limit=8`
              )
            : await window.SI_DB.raw.select(
                "shippers",
                `select=canonical_name,slug,size_band,description,hq_state&size_band=eq.${encodeURIComponent(s.size_band || "enterprise")}&id=neq.${s.id}&order=canonical_name.asc&limit=8`
              );
          if (alive) setPeers(Array.isArray(peerRows) ? peerRows : []);
        } catch {}

        // Lanes touching the shipper's footprint — query dat_lane_rates by
        // the first 3 city names that resolve to a tracked metro. The
        // graph then connects the shipper page to live rate intelligence
        // without any new tables.
        try {
          const cityNames = [...new Set((fac || []).map(f => f.city).filter(Boolean))].slice(0, 3);
          if (cityNames.length > 0) {
            const inOrFilter = cityNames
              .map(c => `origin_city.eq.${encodeURIComponent(c)},dest_city.eq.${encodeURIComponent(c)}`)
              .join(",");
            const lanes = await window.SI_DB.raw.select(
              "dat_lane_rates",
              `select=*&or=(${inOrFilter})&order=lane_rate_per_mi.desc&limit=8`
            );
            if (alive) setNearbyLanes(Array.isArray(lanes) ? lanes : []);
          }
        } catch {}
      } catch (e) {
        // empty state
      } finally {
        if (alive) setLoading(false);
      }
    })();
    return () => { alive = false; };
  }, [slug]);

  if (loading) return <div className="sh-page"><div className="sh-empty">Loading…</div></div>;
  if (notFound) return (
    <div className="sh-page">
      <div className="sh-empty">
        Shipper not found · <a href="#/shippers" onClick={e => { e.preventDefault(); onNav && onNav("shippers"); }}>back to directory</a>
      </div>
    </div>
  );

  // Group signals by type for at-a-glance summary
  const signalGroups = signals.reduce((acc, s) => {
    const k = s.signal_type || "other";
    (acc[k] = acc[k] || []).push(s);
    return acc;
  }, {});
  const riskSignals = signals.filter(s => {
    const sd = s.signal_data || {};
    const severity = sd.severity;
    if (severity !== "critical" && severity !== "high") return false;
    return (
      s.source === "edgar-dirt"   ||
      s.source === "nlrb"          ||
      s.source === "warn"          ||
      s.source === "courtlistener" ||
      s.source === "osha"
    );
  });
  const SIGNAL_SOURCE_LABELS = {
    "edgar-dirt":    "SEC filing",
    "nlrb":          "NLRB case",
    "warn":          "WARN notice",
    "courtlistener": "PACER docket",
    "osha":          "OSHA report",
  };

  return (
    <div className="sh-page">
      <style>{`
        .sd-back {
          display: inline-block; margin-top: 24px; margin-left: 24px;
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em;
          text-transform: uppercase; color: var(--ink-soft); text-decoration: none;
        }
        .sd-back:hover { color: var(--ink); }
        .sd-hero { max-width: 1180px; margin: 0 auto; padding: 24px 24px 32px; }
        .sd-eyebrow {
          font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.32em;
          text-transform: uppercase; color: var(--ink-soft); margin-bottom: 14px;
        }
        .sd-h1 {
          font-family: var(--font-serif); font-size: 48px; line-height: 1.05;
          letter-spacing: -0.02em; margin: 0 0 8px; font-weight: 600;
        }
        @media (max-width: 700px) { .sd-h1 { font-size: 32px; } }
        .sd-meta { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px;
          font-family: var(--font-mono); font-size: 12px; color: var(--ink-soft); }
        .sd-meta strong { color: var(--ink); font-weight: 700; }
        .sd-pill {
          padding: 3px 10px; border-radius: 999px; font-size: 11px;
          background: oklch(0.96 0.04 250); color: oklch(0.40 0.10 250); font-weight: 700;
        }
        .sd-pill-food { background: oklch(0.95 0.06 145); color: oklch(0.40 0.14 145); }
        .sd-pill-risk { background: oklch(0.95 0.08 25); color: oklch(0.45 0.20 25); }

        .sd-section { max-width: 1180px; margin: 24px auto; padding: 0 24px; }
        .sd-section-h {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.32em;
          text-transform: uppercase; color: var(--ink-soft);
          padding-bottom: 12px; border-bottom: 1px solid var(--rule); margin-bottom: 16px;
          display: flex; justify-content: space-between; align-items: baseline;
        }
        .sd-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
        @media (max-width: 760px) { .sd-grid-2 { grid-template-columns: 1fr; } }

        .sd-card {
          background: #fff; border: 1px solid var(--rule); border-radius: 6px;
          padding: 18px;
        }
        .sd-card-h {
          font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.18em;
          text-transform: uppercase; color: var(--ink-soft); margin-bottom: 8px;
        }
        .sd-card-num {
          font-family: var(--font-serif); font-size: 30px; letter-spacing: -0.02em;
          line-height: 1; font-weight: 600; color: var(--ink);
        }
        .sd-card-sub { margin-top: 6px; font-size: 13px; color: var(--ink-soft); line-height: 1.5; }

        .sd-rel-row {
          display: grid; grid-template-columns: 1fr 80px 100px;
          gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--rule);
          align-items: baseline;
        }
        .sd-rel-name { font-family: var(--font-serif); font-size: 16px; color: var(--ink); }
        .sd-rel-strength {
          font-family: var(--font-mono); font-size: 11px; color: var(--ink-soft);
        }
        .sd-rel-date {
          font-family: var(--font-mono); font-size: 11px; color: var(--ink-soft);
          text-align: right;
        }

        .sd-fac-row {
          padding: 10px 0; border-bottom: 1px solid var(--rule);
          font-size: 14px; color: var(--ink);
        }
        .sd-fac-row .meta {
          font-family: var(--font-mono); font-size: 11px; color: var(--ink-soft);
        }

        .sd-paywall {
          margin: 24px auto; padding: 20px;
          background: oklch(0.97 0.03 145); border: 1px solid oklch(0.85 0.08 145);
          border-radius: 8px;
        }
        .sd-paywall-h {
          font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.22em;
          text-transform: uppercase; color: oklch(0.40 0.14 145); margin-bottom: 6px;
        }
        .sd-paywall-t {
          font-family: var(--font-serif); font-size: 22px; letter-spacing: -0.01em;
          margin: 0 0 4px; color: var(--ink);
        }
        .sd-paywall-s { color: var(--ink-soft); font-size: 14px; line-height: 1.5; margin: 0; }

        .sd-risk-row {
          padding: 12px 14px; border-left: 3px solid oklch(0.55 0.20 25);
          background: oklch(0.99 0.01 25); margin-bottom: 8px; border-radius: 4px;
        }
        .sd-risk-row .sev {
          font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.14em;
          text-transform: uppercase; padding: 2px 6px; border-radius: 2px;
          background: oklch(0.55 0.20 25); color: #fff; font-weight: 700;
        }
      `}</style>

      <a href="#/shippers" className="sd-back" onClick={e => { e.preventDefault(); onNav && onNav("shippers"); }}>
        ← Directory
      </a>

      <section className="sd-hero">
        <div className="sd-eyebrow">Shipper profile</div>
        <h1 className="sd-h1">{shipper.canonical_name}</h1>
        <div className="sd-meta">
          {shipper.is_public && shipper.ticker && <span className="sd-pill">{shipper.ticker}</span>}
          {shipper.size_band && shipper.size_band !== "unknown" && (
            <span><strong>Size:</strong> {shipper.size_band}</span>
          )}
          {shipper.hq_state && (
            <span><strong>HQ:</strong> {shipper.hq_city ? `${shipper.hq_city}, ` : ""}{shipper.hq_state}</span>
          )}
          {shipper.primary_naics && <span><strong>NAICS:</strong> {shipper.primary_naics}</span>}
          {shipper.cik && <span><strong>CIK:</strong> {shipper.cik}</span>}
          {signalGroups.industry_food && <span className="sd-pill sd-pill-food">FOOD</span>}
          {riskSignals.length > 0 && <span className="sd-pill sd-pill-risk">{riskSignals.length} RISK SIGNAL{riskSignals.length > 1 ? "S" : ""}</span>}
        </div>
        {shipper.description && (
          <p style={{ marginTop: 18, color: "var(--ink-soft)", fontSize: 15, lineHeight: 1.5, maxWidth: 720 }}>
            {shipper.description}
          </p>
        )}
        {shipper.website_url && (
          <p style={{ marginTop: 12, fontFamily: "var(--font-mono)", fontSize: 12 }}>
            <a href={shipper.website_url} target="_blank" rel="noopener noreferrer" style={{ color: "var(--ink)", borderBottom: "1px solid var(--rule)" }}>
              {shipper.website_url.replace(/^https?:\/\//, "")}
            </a>
          </p>
        )}
      </section>

      {/* Stat cards — only render the metrics with actual data, so the page
          doesn't look empty for shippers we've just started tracking. When
          nothing is loaded yet, show a single honest coverage-status panel. */}
      {(() => {
        const cards = [
          outRels.length > 0 && { h: "Outbound relationships", n: outRels.length, s: "Customers this shipper has named in its own disclosures" },
          inRels.length > 0 && { h: "Inbound relationships", n: inRels.length, s: `Times ${shipper.canonical_name} has been named as a customer by other companies` },
          facilities.length > 0 && { h: "Facilities tracked", n: facilities.length, s: "Warehouses, plants, and distribution centers tied to this entity" },
          signals.length > 0 && { h: "Signals on file", n: signals.length, s: "Material events corroborated across the watchlist" },
        ].filter(Boolean);
        if (cards.length === 0) {
          return (
            <section className="sd-section">
              <div style={{
                padding: "32px 36px", background: "#fff", border: "1px solid var(--rule)",
                borderLeft: "3px solid var(--ink)", borderRadius: 4,
              }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 10 }}>
                  Profile · building
                </div>
                <h3 style={{ fontFamily: "var(--font-serif)", fontSize: 24, lineHeight: 1.25, margin: "0 0 14px", color: "var(--ink)" }}>
                  {shipper.canonical_name} is on the watchlist — detail is being assembled.
                </h3>
                <p style={{ fontSize: 15, lineHeight: 1.6, color: "var(--ink)", margin: "0 0 14px", maxWidth: 720 }}>
                  Every shipper we track passes through the same pipeline: identity and aliases get
                  resolved, the trading network around the company gets mapped, the physical footprint
                  gets pinned, and material events get watched in real time. {shipper.canonical_name}
                  has been added to that pipeline. The page below fills in automatically as each layer
                  reaches this filer — and once it's populated, every change downstream shows up here
                  the day it happens.
                </p>
                <p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--ink-soft)", margin: "0 0 14px", maxWidth: 720 }}>
                  {aliases.length > 1 ? <><strong>{aliases.length} legal aliases</strong> already cross-referenced — that's how we keep one company from showing up as three. </> : null}
                  What rolls in next: customer and supplier relationships disclosed in financial filings,
                  warehouses and distribution centers tied to the corporate entity, decision-maker
                  contacts at the procurement and transportation level, and any material event the
                  company files with regulators. The point of building it once, properly, is that the
                  picture stays current without you having to chase it.
                </p>
                <p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--ink-soft)", margin: 0, maxWidth: 720 }}>
                  Have ground truth on {shipper.canonical_name} — a relationship we should know about,
                  a facility on the wrong list, a contact at the right level? Email <a href={`mailto:hello@shippingclarity.com?subject=${encodeURIComponent(shipper.canonical_name + " — profile detail")}`} style={{ color: "var(--ink)", borderBottom: "1px solid var(--rule)" }}>hello@shippingclarity.com</a> and
                  it goes into the file the same week.
                </p>
              </div>
            </section>
          );
        }
        return (
          <section className="sd-section">
            <div className="sd-grid-2">
              {cards.map((c, i) => (
                <div key={i} className="sd-card">
                  <div className="sd-card-h">{c.h}</div>
                  <div className="sd-card-num">{c.n}</div>
                  <p className="sd-card-sub">{c.s}</p>
                </div>
              ))}
            </div>
          </section>
        );
      })()}

      {/* Risk signals (highlighted) */}
      {riskSignals.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Distress + risk signals</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>SEC · NLRB · WARN · PACER · OSHA</span>
          </div>
          {riskSignals.map((s, i) => {
            const sd = s.signal_data || {};
            const linkLabel = SIGNAL_SOURCE_LABELS[s.source] || "Source";
            const dateLabel = sd.filing_date || sd.filed_date || sd.notice_date
                              || sd.effective_date || sd.event_date
                              || (s.observed_at ? String(s.observed_at).slice(0, 10) : null);
            return (
              <div key={i} className="sd-risk-row">
                <span className="sev">{sd.severity || "high"}</span>
                <strong style={{ marginLeft: 8 }}>{sd.headline || s.signal_type}</strong>
                {sd.item && <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>Item {sd.item}</span>}
                {dateLabel && <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>{dateLabel}</span>}
                {s.source_url && (
                  <a href={s.source_url} target="_blank" rel="noopener noreferrer"
                     style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink)" }}>
                    {linkLabel} →
                  </a>
                )}
              </div>
            );
          })}
        </section>
      )}

      {/* Inbound relationships */}
      {inRels.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Named as a customer by</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{inRels.length} suppliers / providers</span>
          </div>
          {inRels.map(r => (
            <div key={r.id} className="sd-rel-row">
              <span className="sd-rel-name">{r.from_name_raw || "(unknown)"}</span>
              <span className="sd-rel-strength">{(r.rel_strength || "").replace(/_/g, " ")}</span>
              <span className="sd-rel-date">{r.source_date || ""}</span>
            </div>
          ))}
        </section>
      )}

      {/* Customer concentration + freight lanes — visual layer over outRels */}
      {outRels.length > 0 && (
        <ShipperCustomerGraphics
          shipper={shipper}
          outRels={outRels}
          customerHubs={customerHubs}
        />
      )}

      {/* Outbound relationships */}
      {outRels.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Customers disclosed</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{outRels.length} named</span>
          </div>
          {outRels.map(r => (
            <div key={r.id} className="sd-rel-row">
              <span className="sd-rel-name">{r.to_name_raw || "(unknown)"}</span>
              <span className="sd-rel-strength">{(r.rel_strength || "").replace(/_/g, " ")}</span>
              <span className="sd-rel-date">{r.source_date || ""}</span>
            </div>
          ))}
        </section>
      )}

      {/* Facilities */}
      {facilities.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Facilities</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{facilities.length} sites</span>
          </div>
          {facilities.slice(0, 30).map(f => (
            <div key={f.id} className="sd-fac-row">
              <strong>{f.name || "(unnamed facility)"}</strong>
              <div className="meta">
                {[f.address_line, f.city, f.state, f.zip].filter(Boolean).join(" · ")}
                {f.facility_type && ` · ${f.facility_type}`}
              </div>
            </div>
          ))}
          {facilities.length > 30 && (
            <p style={{ marginTop: 12, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
              + {facilities.length - 30} more facilities (paid tier)
            </p>
          )}
        </section>
      )}

      {/* Facility risk records — OSHA + EPA ECHO matched by employer name.
          The fuzzy ilike match catches name-variant filings ('Walmart Inc'
          vs 'Walmart Stores Inc' vs 'Walmart Stores LLC') that EDGAR
          dirt scanner can't reach because they're not SEC filers. */}
      {(oshaHits.length > 0 || epaHits.length > 0) && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Facility risk records</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>
              {oshaHits.length} OSHA · {epaHits.length} EPA ECHO
            </span>
          </div>

          {oshaHits.length > 0 && (
            <div style={{ marginBottom: 24 }}>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.18em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>
                OSHA severe-injury + fatality reports
              </div>
              {oshaHits.slice(0, 12).map((o, i) => {
                const isFatal = o.report_type === "fatality";
                const sev = isFatal ? "critical" : (o.amputation ? "high" : "moderate");
                const sevColor = sev === "critical" ? "oklch(0.50 0.22 25)"
                              : sev === "high"     ? "oklch(0.55 0.18 25)"
                              :                       "oklch(0.55 0.14 80)";
                return (
                  <div key={i} className="sd-risk-row">
                    <span className="sev" style={{ background: sevColor, color: "#fff" }}>{sev}</span>
                    <strong style={{ marginLeft: 8 }}>
                      {isFatal ? "Workplace fatality"
                        : o.amputation ? `Amputation reported (${o.body_part || "body part unspecified"})`
                        : `Severe injury reported${o.hospitalized ? ` — ${o.hospitalized} hospitalized` : ""}`}
                    </strong>
                    {(o.city || o.state) && (
                      <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                        {[o.city, o.state].filter(Boolean).join(", ")}
                      </span>
                    )}
                    {o.event_date && (
                      <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                        {o.event_date}
                      </span>
                    )}
                    {o.industry_description && (
                      <div style={{ marginLeft: 0, marginTop: 4, fontSize: 12, color: "var(--ink-soft)", lineHeight: 1.4 }}>
                        {o.industry_description}
                      </div>
                    )}
                  </div>
                );
              })}
              {oshaHits.length > 12 && (
                <p style={{ marginTop: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                  + {oshaHits.length - 12} more OSHA reports (paid tier)
                </p>
              )}
            </div>
          )}

          {epaHits.length > 0 && (
            <div>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.18em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 8 }}>
                EPA ECHO facility compliance
              </div>
              {epaHits.slice(0, 10).map((e, i) => {
                const sev = e.significant_violator ? "critical"
                          : (e.violations_3yr && e.violations_3yr > 0) ? "high"
                          : "moderate";
                const sevColor = sev === "critical" ? "oklch(0.50 0.22 25)"
                              : sev === "high"     ? "oklch(0.55 0.18 25)"
                              :                       "oklch(0.55 0.14 80)";
                return (
                  <div key={i} className="sd-risk-row">
                    <span className="sev" style={{ background: sevColor, color: "#fff" }}>{sev}</span>
                    <strong style={{ marginLeft: 8 }}>
                      {e.significant_violator ? "Significant Non-Compliance"
                        : (e.violations_3yr ? `${e.violations_3yr} formal violation${e.violations_3yr === 1 ? "" : "s"} (3yr)`
                        : "Inspection / enforcement history")}
                    </strong>
                    {(e.city || e.state) && (
                      <span style={{ marginLeft: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                        {[e.city, e.state].filter(Boolean).join(", ")}
                      </span>
                    )}
                    <div style={{ marginTop: 4, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                      {e.facility_name}
                      {e.primary_naics && ` · NAICS ${e.primary_naics}`}
                      {e.total_penalties_5yr ? ` · $${Math.round(parseFloat(e.total_penalties_5yr)).toLocaleString()} penalties (5yr)` : ""}
                      {e.last_inspection_date && ` · last inspected ${e.last_inspection_date}`}
                    </div>
                  </div>
                );
              })}
              {epaHits.length > 10 && (
                <p style={{ marginTop: 8, fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>
                  + {epaHits.length - 10} more ECHO facilities (paid tier)
                </p>
              )}
            </div>
          )}
        </section>
      )}

      {/* Aliases */}
      {aliases.length > 1 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Also known as</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{aliases.length} aliases</span>
          </div>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
            {aliases.map((a, i) => (
              <span key={i} style={{
                padding: "6px 12px", background: "#fff", border: "1px solid var(--rule)",
                borderRadius: 4, fontSize: 13, color: "var(--ink-soft)",
              }}>
                {a.alias} <span style={{ fontSize: 10, marginLeft: 4, opacity: 0.6 }}>({a.alias_type})</span>
              </span>
            ))}
          </div>
        </section>
      )}

      {/* Contacts paywall — only show when we have contacts to paywall.
          A "0 contacts" panel makes the page look hollow. */}
      {contactCount > 0 && (
        <section className="sd-section">
          <div className="sd-paywall">
            <div className="sd-paywall-h">Decision-maker contacts · paid tier</div>
            <h3 className="sd-paywall-t">{contactCount} contact{contactCount === 1 ? "" : "s"} on file for {shipper.canonical_name}</h3>
            <p className="sd-paywall-s">
              Verified emails, direct phones, and named decision-makers at the
              procurement, logistics, and transportation level — the people who
              actually award and route freight. Gated for the paid tier; pricing
              and checkout ship with the rest of Tier-1.
            </p>
          </div>
        </section>
      )}

      {/* Lanes touching this shipper's footprint — pulled live from
          dat_lane_rates by the cities in the facility list. Every shipper
          page automatically gets a rate-intelligence block when its
          footprint matches priced lanes. */}
      {nearbyLanes.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Lanes touching {shipper.canonical_name}'s footprint</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{nearbyLanes.length} priced</span>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 10 }}>
            {nearbyLanes.map((l, i) => (
              <a key={i} href="#/rates"
                 onClick={(e) => { e.preventDefault(); window.location.hash = "#/rates"; }}
                 style={{ textDecoration: "none", color: "inherit", padding: 14, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 6 }}>
                  {l.equipment_type} · {l.heat_band || "—"}
                </div>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 16, color: "var(--ink)", lineHeight: 1.3, marginBottom: 6 }}>
                  {l.origin_city}, {l.origin_state} → {l.dest_city}, {l.dest_state}
                </div>
                <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
                  <span style={{ fontFamily: "var(--font-serif)", fontSize: 22, fontWeight: 600 }}>
                    ${parseFloat(l.lane_rate_per_mi).toFixed(2)}
                  </span>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)" }}>/mi</span>
                </div>
              </a>
            ))}
          </div>
        </section>
      )}

      {/* Peer shippers — same NAICS code (or size band fallback). Lets a
          visitor click sideways into comparable companies, which makes the
          directory feel deeper than a single isolated profile. */}
      {peers.length > 0 && (
        <section className="sd-section">
          <div className="sd-section-h">
            <span>Peer shippers · {shipper.primary_naics ? `NAICS ${shipper.primary_naics}` : (shipper.size_band || "comparable scale")}</span>
            <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>{peers.length} comparable</span>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 10 }}>
            {peers.map(p => (
              <a key={p.slug} href={`#/shipper/${p.slug}`}
                 onClick={(e) => { e.preventDefault(); window.location.hash = `#/shipper/${p.slug}`; }}
                 style={{ textDecoration: "none", color: "inherit", padding: 14, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 16, color: "var(--ink)", lineHeight: 1.3 }}>
                  {p.canonical_name}
                </div>
                {(p.size_band && p.size_band !== "unknown") || p.hq_state ? (
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--ink-soft)", marginTop: 4 }}>
                    {[p.size_band !== "unknown" && p.size_band, p.hq_state].filter(Boolean).join(" · ")}
                  </div>
                ) : null}
                {p.description && (
                  <div style={{ fontSize: 13, color: "var(--ink-soft)", marginTop: 6, lineHeight: 1.4, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>
                    {p.description}
                  </div>
                )}
              </a>
            ))}
          </div>
        </section>
      )}

      {/* Cross-links — every entity page should expose the surrounding graph.
          From a shipper, visitors most often want: lane rates touching its
          metros, the directory to compare peers, and verification of any
          carrier currently moving its freight. */}
      <section className="sd-section">
        <div className="sd-section-h">
          <span>Connected surfaces</span>
          <span style={{ fontSize: 10, letterSpacing: "0.16em" }}>From {shipper.canonical_name} · into the rest of the graph</span>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 12 }}>
          {(() => {
            // First facility city we recognize as a tracked metro
            const firstMetroId = (() => {
              if (!window.SI_DATA?.ALL_CITIES) return null;
              for (const f of facilities) {
                if (!f.city) continue;
                const target = f.city.toLowerCase().trim();
                const hit = window.SI_DATA.ALL_CITIES.find(c => c.name.toLowerCase() === target);
                if (hit) return { id: hit.id, name: hit.name, state: hit.state };
              }
              return null;
            })();
            const cards = [];
            if (firstMetroId) {
              cards.push(
                <a key="metro" href={`#/report/${firstMetroId.id}`}
                   onClick={(e) => { e.preventDefault(); window.location.hash = `#/report/${firstMetroId.id}`; }}
                   style={{ textDecoration: "none", color: "inherit", padding: 16, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 6 }}>{firstMetroId.name}, {firstMetroId.state} report →</div>
                  <div style={{ fontFamily: "var(--font-serif)", fontSize: 17, color: "var(--ink)", lineHeight: 1.3 }}>
                    Carrier signal, weather, port pulse, and lane heat for the metro this shipper operates in.
                  </div>
                </a>
              );
            }
            cards.push(
              <a key="rates" href="#/rates"
                 onClick={(e) => { e.preventDefault(); window.location.hash = "#/rates"; }}
                 style={{ textDecoration: "none", color: "inherit", padding: 16, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 6 }}>Lane rates →</div>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 17, color: "var(--ink)", lineHeight: 1.3 }}>
                  See $/mi, hot/cold bands, and headhaul-vs-backhaul asymmetry on lanes touching this footprint.
                </div>
              </a>,
              <a key="verify" href="#/find-carrier"
                 onClick={(e) => { e.preventDefault(); window.location.hash = "#/find-carrier"; }}
                 style={{ textDecoration: "none", color: "inherit", padding: 16, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 6 }}>Verify a carrier →</div>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 17, color: "var(--ink)", lineHeight: 1.3 }}>
                  Run any carrier moving freight for {shipper.canonical_name} against the federal registry.
                </div>
              </a>,
              <a key="directory" href="#/shippers"
                 onClick={(e) => { e.preventDefault(); window.location.hash = "#/shippers"; }}
                 style={{ textDecoration: "none", color: "inherit", padding: 16, border: "1px solid var(--rule)", borderRadius: 6, background: "#fff", display: "block" }}>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-soft)", marginBottom: 6 }}>Shipper directory →</div>
                <div style={{ fontFamily: "var(--font-serif)", fontSize: 17, color: "var(--ink)", lineHeight: 1.3 }}>
                  Compare {shipper.canonical_name} to 29,762 other shippers in the index — by industry, footprint, or filings.
                </div>
              </a>
            );
            return cards;
          })()}
        </div>
      </section>

      <div style={{ height: 96 }} />
    </div>
  );
}

window.ShippersPage = ShippersPage;
