← back to Lawyer Directory Builder

src/server/leads.ts

913 lines

/**
 * Lead-gen funnel.
 *
 *   GET  /find-a-lawyer       public form
 *   POST /find-a-lawyer       creates a lead row, redirects to /find-a-lawyer/thanks
 *   GET  /find-a-lawyer/thanks  confirmation page
 *   GET  /admin/leads         admin queue (list, filter by status/area)
 *   POST /admin/leads/:id/status   set status (new/matched/contacted/won/lost/spam)
 *   POST /admin/leads/:id/notes    update admin notes
 */
import express from 'express';
import { requireAdmin } from './auth.ts';
import { query } from '../db/pool.ts';
import { matchFirmsForLead, matchFirmsWithReason } from '../lib/match_firms.ts';

const router = express.Router();
router.use(express.urlencoded({ extended: false }));

const PRACTICE_AREAS = [
  ['personal_injury', 'Personal Injury · Car accident · Slip & fall'],
  ['family', 'Family · Divorce · Custody · Support'],
  ['criminal', 'Criminal Defense · DUI · Misdemeanor'],
  ['immigration', 'Immigration · Visa · Citizenship · Green card'],
  ['business', 'Business · Contract · Employment'],
  ['estate', 'Estate · Wills · Trusts · Probate'],
  ['real_estate', 'Real Estate · Landlord-tenant · Property'],
  ['bankruptcy', 'Bankruptcy · Debt'],
  ['ip', 'Intellectual Property · Trademark · Copyright'],
  ['other', 'Something else'],
] as const;

const URGENCY = [
  ['immediate', 'I plan to call a firm today'],
  ['within_week', 'This week'],
  ['within_month', 'Within a month'],
  ['just_researching', "Just researching for now"],
];

const BUDGETS = [
  ['under_500', 'Under $500 (consultation only)'],
  ['500_2k', '$500 – $2,000'],
  ['2k_10k', '$2,000 – $10,000'],
  ['10k_plus', '$10,000+'],
  ['unsure', 'Not sure yet'],
];

const STATUSES = ['new', 'matched', 'contacted', 'won', 'lost', 'spam'] as const;

function esc(s: any): string {
  if (s === null || s === undefined) return '';
  return String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
}

function layout(title: string, body: string, opts: { user?: any; variant?: 'public' | 'admin' | 'default' } = {}) {
  const userBar = opts.user
    ? `<div class="topnav-user">Signed in as <b>${esc(opts.user.email)}</b> · <a href="/dashboard">Dashboard</a>${opts.user.role === 'admin' ? ' · <a href="/admin">Admin</a> · <a href="/admin/leads">Leads</a>' : ''} · <form action="/logout" method="post" style="display:inline"><button class="link" type="submit">Sign out</button></form></div>`
    : `<div class="topnav-user"><a href="/login">Sign in</a> · <a href="/signup">Sign up</a></div>`;
  const isAdmin = title.includes('Admin');
  const monogram = `<svg viewBox="0 0 28 28" aria-hidden="true"><defs><linearGradient id="cb-mono-${opts.variant || 'd'}" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#d4b683"/><stop offset="50%" stop-color="#b89968"/><stop offset="100%" stop-color="#8a7044"/></linearGradient></defs><circle cx="14" cy="14" r="13" fill="none" stroke="url(#cb-mono-${opts.variant || 'd'})" stroke-width="1"/><path fill="url(#cb-mono-${opts.variant || 'd'})" d="M9.6 9.5c-1.6 0-2.7 1.2-2.7 3v3c0 1.8 1.1 3 2.7 3 1.4 0 2.4-.8 2.6-2.1h-1.3c-.1.6-.6 1-1.3 1-.9 0-1.4-.6-1.4-1.7v-2.4c0-1.1.5-1.7 1.4-1.7.7 0 1.2.4 1.3 1h1.3c-.2-1.3-1.2-2.1-2.6-2.1zm5.5.1v8.7h2.7c1.6 0 2.6-.9 2.6-2.4 0-1-.5-1.7-1.3-2 .7-.3 1.1-1 1.1-1.8 0-1.5-1-2.5-2.5-2.5h-2.6zm1.3 1.1h1.2c.8 0 1.4.5 1.4 1.4 0 .9-.5 1.4-1.4 1.4h-1.2v-2.8zm0 3.9h1.4c.9 0 1.4.6 1.4 1.5s-.5 1.5-1.4 1.5h-1.4V14.6z"/></svg>`;
  return `<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)} · Counsel &amp; Bar</title>
<meta name="description" content="${esc(title)} on Counsel &amp; Bar — California's indexed attorney directory, drawn from the State Bar public roll.">
<meta name="robots" content="index,follow">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="apple-touch-icon" href="/favicon-32.png">
<meta name="theme-color" content="#0a0a0c">

<meta property="og:type" content="website">
<meta property="og:site_name" content="Counsel &amp; Bar">
<meta property="og:title" content="${esc(title)} · Counsel &amp; Bar">
<meta property="og:description" content="California's indexed attorney directory, drawn from the State Bar public roll. Independent. Not a referral service. Not the State Bar.">
<meta property="og:locale" content="en_US">
<meta property="og:image" content="https://lawyers.agentabrams.com/og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Counsel &amp; Bar — every California-licensed attorney, indexed.">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${esc(title)} · Counsel &amp; Bar">
<meta name="twitter:description" content="California-licensed attorneys, indexed against the State Bar public roll. You choose who to call.">
<meta name="twitter:image" content="https://lawyers.agentabrams.com/og.png">

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;1,300;1,400&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
:root{
  --noir:#0a0a0c;--noir-rise:#131316;--noir-deep:#050507;
  --rule:#2a2724;--rule-faint:#1a1815;
  --ink:#f4f1ea;--ink-soft:#d8d2c5;--ink-mute:#8b857a;
  --metal:#b89968;--metal-glow:#d4b683;--metal-deep:#8a7044;
  --good:#34d399;--warn:#d4a04a;--bad:#d87a7a;
  --serif:"Cormorant Garamond","Cormorant","Georgia",serif;
  --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;
}
*{box-sizing:border-box}
html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}
body{margin:0;background:var(--noir);color:var(--ink);font-family:var(--sans);font-weight:300;font-size:15px;line-height:1.55}
::selection{background:var(--metal);color:var(--noir)}
a{color:var(--metal);text-decoration:none;transition:color 180ms ease}
a:hover{color:var(--metal-glow)}
header.brand{padding:22px 48px;display:flex;align-items:center;gap:18px;justify-content:space-between;border-bottom:1px solid var(--rule);background:var(--noir-deep)}
header.brand h1{margin:0;font-family:var(--serif);font-size:22px;font-weight:400;letter-spacing:.01em}
header.brand h1 a{color:var(--ink)}
header.brand h1 .amp{color:var(--metal);font-style:italic;padding:0 4px}
.topnav-user{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-mute)}
.topnav-user b{color:var(--ink);font-weight:500}
.topnav-user a{color:var(--ink-mute);text-transform:uppercase}
.topnav-user a:hover{color:var(--ink)}
main{max-width:780px;margin:0 auto;padding:64px 48px}
main.wide{max-width:1280px}
h2{margin:0 0 16px;font-family:var(--serif);font-weight:300;font-size:clamp(34px,5vw,52px);line-height:1.1;letter-spacing:-.015em;color:var(--ink)}
h3{margin:32px 0 12px;font-family:var(--serif);font-weight:400;font-size:22px;color:var(--ink);letter-spacing:-.005em}
.lede{color:var(--ink-soft);font-size:17px;margin:0 0 36px;max-width:60ch;font-weight:300}
form.stack{display:flex;flex-direction:column;gap:18px}
label{font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
input,select,textarea,button{font:inherit;font-family:var(--sans);color:var(--ink);background:var(--noir-rise);border:1px solid var(--rule);border-radius:0;padding:14px 16px;width:100%;transition:border-color 180ms ease,box-shadow 180ms ease}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--metal);box-shadow:0 0 0 3px rgba(184,153,104,.12)}
textarea{resize:vertical;min-height:120px;font-family:var(--sans)}
button{background:var(--metal);border-color:var(--metal);color:var(--noir);font-weight:500;font-size:12px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer;width:auto;padding:16px 28px;transition:background 220ms ease,transform 220ms ease}
button:hover{background:var(--metal-glow);border-color:var(--metal-glow);transform:translateY(-1px)}
button.danger{background:transparent;border-color:#5a2222;color:var(--bad)}
button.warn{background:transparent;border-color:#5a4520;color:var(--warn)}
button.good{background:transparent;border-color:#1a4a30;color:var(--good)}
button.link{background:none;border:none;padding:0;color:var(--metal);cursor:pointer;text-decoration:none;font:inherit;font-size:11px;letter-spacing:.14em;text-transform:uppercase;width:auto}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
@media (max-width:520px){.row2{grid-template-columns:1fr}main{padding:36px 20px}header.brand{padding:16px 20px;flex-wrap:wrap;gap:10px}}
.card{background:var(--noir-rise);border:1px solid var(--rule);padding:32px}
.flash{padding:14px 18px;margin-bottom:24px;border:1px solid var(--rule);font-size:13px;letter-spacing:.06em}
.flash.success{background:#0e2a1a;color:var(--good);border-color:#1a4a30}
.flash.error{background:#2a0e0e;color:var(--bad);border-color:#4a1a1a}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:14px 12px;border-bottom:1px solid var(--rule);text-align:left;vertical-align:top}
th{color:var(--ink-mute);font-weight:500;font-size:10px;text-transform:uppercase;letter-spacing:.18em}
.pill{display:inline-block;font-size:10px;padding:3px 9px;border-radius:0;border:1px solid var(--rule);letter-spacing:.14em;text-transform:uppercase;font-weight:500}
.pill.new{background:#1a1a2a;color:#a5a0d4;border-color:#2a2a4a}
.pill.matched{background:#0e2a26;color:#7fd1c4;border-color:#1a4a44}
.pill.contacted{background:#2a2010;color:var(--warn);border-color:#4a3a1a}
.pill.won{background:#0e2a1a;color:var(--good);border-color:#1a4a30}
.pill.lost{background:var(--noir-rise);color:var(--ink-mute);border-color:var(--rule)}
.pill.spam{background:#2a0e0e;color:var(--bad);border-color:#4a1a1a}
.muted{color:var(--ink-mute);font-size:13px}
.cta{display:inline-block;padding:16px 28px;background:var(--metal);color:var(--noir)!important;text-decoration:none;font-weight:500;font-size:12px;letter-spacing:.18em;text-transform:uppercase;transition:background 220ms ease}
.cta:hover{background:var(--metal-glow)}
.actions{display:flex;gap:6px;flex-wrap:wrap}
.actions form{display:inline}
.actions button{padding:7px 12px;font-size:10px;letter-spacing:.12em;width:auto}

/* ─── Public configurator (find-a-lawyer / signup) ─────────────────── */
.public main{max-width:980px;padding:80px 48px 96px}
@media (max-width:720px){.public main{padding:48px 24px 64px}}
.public-vol{display:flex;align-items:center;gap:18px;margin:0 0 26px;color:var(--metal)}
.public-vol .v-rule{flex:0 0 56px;height:1px;background:linear-gradient(90deg,transparent,var(--metal) 50%,transparent)}
.public-vol .v-text{font-family:var(--serif);font-style:italic;font-weight:400;font-size:14px;letter-spacing:.02em}
.public-vol .v-meta{font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
.public-display{font-family:var(--serif);font-weight:300;font-size:clamp(40px,6.2vw,72px);line-height:1.04;letter-spacing:-.018em;margin:0 0 22px;max-width:18ch;color:var(--ink)}
.public-display em{font-style:italic;color:var(--metal);font-weight:400}
.public-lede{color:var(--ink-soft);font-size:17px;line-height:1.6;margin:0 0 32px;max-width:62ch;font-weight:300}
.consumer-pact{display:grid;grid-template-columns:repeat(3,1fr);gap:0;margin:0 0 56px;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule)}
.consumer-pact-cell{padding:32px 28px 28px;border-right:1px solid var(--rule);display:flex;flex-direction:column;gap:10px}
.consumer-pact-cell:last-child{border-right:0}
.consumer-pact-num{font-family:var(--serif);font-style:italic;font-weight:300;font-size:44px;line-height:.9;color:transparent;-webkit-text-stroke:1px var(--metal);margin:0 0 6px;display:block;letter-spacing:-.02em}
.consumer-pact-h{font-family:var(--serif);font-weight:400;font-size:20px;line-height:1.2;color:var(--ink);margin:0;letter-spacing:-.005em}
.consumer-pact-h em{font-style:italic;color:var(--metal);font-weight:400}
.consumer-pact-p{font-size:13px;line-height:1.65;color:var(--ink-mute);margin:0;font-weight:300}
@media (max-width:780px){.consumer-pact{grid-template-columns:1fr}.consumer-pact-cell{border-right:0;border-bottom:1px solid var(--rule)}.consumer-pact-cell:last-child{border-bottom:0}}
.public-lede b,.public-lede strong{color:var(--ink);font-weight:500}
.config{display:flex;flex-direction:column;gap:0}
.config-section{padding:36px 0;border-top:1px solid var(--rule);display:grid;grid-template-columns:140px 1fr;gap:48px;align-items:start}
.config-section:first-of-type{border-top:none;padding-top:8px}
@media (max-width:720px){.config-section{grid-template-columns:1fr;gap:18px;padding:28px 0}}
.config-step{font-family:var(--serif);font-style:italic;font-weight:300;color:transparent;-webkit-text-stroke:1px var(--metal);font-size:56px;line-height:.9;letter-spacing:-.02em;margin:0}
.config-step .label{display:block;font-family:var(--sans);font-style:normal;font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-mute);font-weight:500;-webkit-text-stroke:0;margin-top:10px}
.config-fields{display:flex;flex-direction:column;gap:22px}
.config-fields .row2{display:grid;grid-template-columns:1fr 1fr;gap:22px}
@media (max-width:520px){.config-fields .row2{grid-template-columns:1fr}}
.config-fields label{font-size:10px;letter-spacing:.24em;text-transform:uppercase;color:var(--ink-mute);font-weight:500;display:block;margin-bottom:10px}
.config-fields input,.config-fields select,.config-fields textarea{padding:18px 18px;font-size:16px;background:rgba(184,153,104,.018);border:0;border-bottom:1px solid var(--rule);border-radius:0}
.config-fields input:hover,.config-fields select:hover,.config-fields textarea:hover{background:rgba(184,153,104,.035)}
.config-fields input:focus,.config-fields select:focus,.config-fields textarea:focus{border-color:var(--metal);box-shadow:none;background:rgba(184,153,104,.07)}
.config-fields textarea{font-size:15px;border:1px solid var(--rule);padding:16px;min-height:120px}
.config-fields .field-note{font-size:12px;letter-spacing:.04em;color:var(--ink-mute);margin-top:8px;font-style:italic;font-family:var(--serif)}
.config-cta-row{display:flex;justify-content:space-between;align-items:center;gap:24px;padding:36px 0 0;border-top:1px solid var(--rule);margin-top:8px;flex-wrap:wrap}
.cta-metal{display:inline-flex;align-items:center;gap:14px;padding:20px 36px;background:linear-gradient(180deg,#d4b683 0%,#b89968 38%,#9c7e4f 100%);color:var(--noir);border:1px solid var(--metal);font-size:12px;letter-spacing:.18em;text-transform:uppercase;font-weight:500;cursor:pointer;text-decoration:none;transition:all 240ms ease;box-shadow:inset 0 1px 0 rgba(255,235,200,.35),0 1px 0 rgba(0,0,0,.4);width:auto}
.cta-metal:hover{background:linear-gradient(180deg,#e3c89a 0%,#c8a674 50%,#a98559 100%);transform:translateY(-1px);box-shadow:inset 0 1px 0 rgba(255,240,210,.5),0 4px 18px rgba(184,153,104,.18);color:var(--noir)}
.cta-metal:disabled{cursor:not-allowed;opacity:.7;transform:none;background:linear-gradient(180deg,#9c8758 0%,#80684a 50%,#5e4a30 100%);box-shadow:inset 0 1px 0 rgba(255,235,200,.15)}
.cta-metal:disabled:hover{background:linear-gradient(180deg,#9c8758 0%,#80684a 50%,#5e4a30 100%);transform:none}
.cta-metal .cta-spinner{display:none;width:14px;height:14px;border:1.5px solid rgba(10,10,12,.25);border-top-color:var(--noir);border-radius:50%;animation:cta-spin 720ms linear infinite}
.cta-metal.is-loading .cta-arrow{display:none}
.cta-metal.is-loading .cta-spinner{display:inline-block}
@keyframes cta-spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion: reduce){
  .cta-metal .cta-spinner{animation:none;border-top-color:rgba(10,10,12,.25)}
  .cta-metal.is-loading .cta-spinner{position:relative}
  .cta-metal.is-loading .cta-spinner::after{content:'·';position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);color:var(--noir);font-weight:700;font-size:18px;line-height:1}
}
.public-trust{margin-top:64px;padding:32px 36px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.025),transparent)}
.public-trust .t-eyebrow{font-size:10px;letter-spacing:.3em;text-transform:uppercase;color:var(--metal);font-weight:500;margin:0 0 12px}
.public-trust p{margin:0;color:var(--ink-soft);font-size:13px;line-height:1.7;font-weight:300}
.public-trust p+p{margin-top:10px}
.brand-mono{display:inline-flex;align-items:center;gap:14px;text-decoration:none}
.brand-mono svg{width:28px;height:28px;flex:0 0 28px}

/* ─── Thanks / inquiry-logged "what happens next" ──────────────────── */
.thanks-next{margin:64px 0 0;padding:48px 0 0;border-top:1px solid var(--rule)}
.thanks-step{font-family:var(--serif);font-weight:300;font-size:28px;color:var(--ink);margin:0 0 32px;letter-spacing:-.005em}
.thanks-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:0}
.thanks-cell{padding:32px 32px 32px 0;border-right:1px solid var(--rule);position:relative}
.thanks-cell:last-child{border-right:0;padding-right:0}
.thanks-cell:not(:first-child){padding-left:32px}
@media (max-width:720px){.thanks-grid{grid-template-columns:1fr}.thanks-cell{padding:24px 0;border-right:0;border-bottom:1px solid var(--rule)}.thanks-cell:not(:first-child){padding-left:0}.thanks-cell:last-child{border-bottom:0}}
.thanks-num{font-family:var(--serif);font-style:italic;font-weight:400;color:var(--metal);font-size:14px;letter-spacing:.04em;display:block;margin-bottom:14px}
.thanks-cell h4{font-family:var(--serif);font-weight:400;font-size:20px;color:var(--ink);margin:0 0 12px;letter-spacing:-.005em;line-height:1.25}
.thanks-cell p{margin:0 0 18px;color:var(--ink-soft);font-size:14px;line-height:1.65;font-weight:300}
.thanks-link{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--metal);font-weight:500;border-bottom:1px solid var(--rule);padding-bottom:2px;transition:border-color 200ms ease,color 200ms ease}
.thanks-link:hover{color:var(--metal-glow);border-bottom-color:var(--metal)}

/* ─── Inquiry receipt strip ─────────────────────────────────────── */
.receipt{margin:32px 0 12px;padding:22px 28px;border:1px solid var(--rule);background:linear-gradient(180deg,rgba(184,153,104,.025),transparent);display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
@media (max-width:720px){.receipt{grid-template-columns:1fr;gap:14px;padding:18px 22px}}
.receipt-row{display:flex;flex-direction:column;gap:4px}
.r-label{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-mute);font-weight:500}
.r-val{font-family:var(--serif);font-style:italic;color:var(--metal);font-size:16px;font-variant-numeric:tabular-nums;letter-spacing:.02em}

/* ─── Firm result cards (find-a-lawyer/results) ──────────────────── */
.firms{display:flex;flex-direction:column;gap:0;margin:0 0 24px;border-top:1px solid var(--rule)}
.firm-card{display:grid;grid-template-columns:96px 1fr auto;gap:32px;padding:36px 28px;border-bottom:1px solid var(--rule);align-items:start;margin:0 -28px;border-left:1px solid transparent;border-right:1px solid transparent;transition:background 280ms ease,border-color 280ms ease,transform 280ms cubic-bezier(.16,.84,.3,1);position:relative}
.firm-card:hover{background:linear-gradient(180deg,rgba(184,153,104,.025),rgba(184,153,104,.012));border-left-color:var(--metal);border-right-color:var(--rule);transform:translateX(2px)}
.firm-card:hover .firm-num{-webkit-text-stroke-color:var(--metal-glow);opacity:1}
.firm-card:hover .firm-name{color:var(--metal-glow)}
.firm-card:hover .firm-distance{color:var(--metal-glow)}
@media (max-width:780px){.firm-card{grid-template-columns:64px 1fr;gap:18px;padding:28px 20px;margin:0 -20px}.firm-distance{grid-column:1 / -1;text-align:right;font-size:14px!important;margin-top:0!important;padding-top:8px;border-top:1px solid var(--rule-faint)}.firm-num{font-size:48px!important}}
@media (max-width:520px){.firm-card{grid-template-columns:1fr;gap:6px;padding:24px 16px;margin:0 -16px}.firm-distance{text-align:left;border-top:0;padding-top:0}.firm-num{font-size:40px!important}}
.firm-num-col{display:flex;flex-direction:column;gap:8px}
.firm-num-eyebrow{font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--ink-mute);font-weight:500;line-height:1}
.firm-num{font-family:var(--serif);font-style:italic;font-weight:300;color:transparent;-webkit-text-stroke:1px var(--metal);font-size:72px;line-height:.85;letter-spacing:-.02em;margin:0;transition:opacity 280ms ease,-webkit-text-stroke-color 280ms ease}
.firm-name{font-family:var(--serif);font-weight:400;font-size:26px;line-height:1.18;color:var(--ink);margin:0 0 10px;letter-spacing:-.008em;transition:color 280ms ease}
.firm-meta{font-size:12px;letter-spacing:.04em;color:var(--ink-mute);margin:0 0 22px;display:flex;flex-wrap:wrap;gap:8px}
.firm-meta.firm-meta-solo{font-size:11px;opacity:.78;letter-spacing:.06em}
.firm-meta .firm-dot{color:var(--metal);opacity:.6}
.firm-distance{font-family:var(--serif);font-style:italic;font-weight:400;color:var(--metal);font-size:24px;letter-spacing:.005em;line-height:1;margin-top:14px;font-variant-numeric:tabular-nums;text-align:right;transition:color 280ms ease;white-space:nowrap}
.firm-distance .unit{font-style:normal;font-family:var(--sans);font-weight:500;font-size:10px;letter-spacing:.22em;text-transform:uppercase;color:var(--ink-mute);margin-left:6px;vertical-align:1px}
.firm-actions{display:flex;flex-wrap:wrap;gap:14px 28px;align-items:baseline;padding-top:16px;border-top:1px solid var(--rule-faint);margin-top:6px}
.firm-tel{font-family:var(--serif);font-style:italic;font-weight:400;color:var(--metal);font-size:18px;letter-spacing:.02em;font-variant-numeric:tabular-nums}
.firm-web{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--metal);font-weight:500;border-bottom:1px solid var(--rule);padding-bottom:2px;transition:border-color 200ms ease,color 200ms ease}
.firm-web:hover{color:var(--metal-glow);border-bottom-color:var(--metal)}

/* ─── Firm chips (audit-grade + firm-size) ───────────────────────── */
.firm-chips{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 18px;align-items:center}
.firm-chip{display:inline-flex;align-items:baseline;gap:6px;padding:5px 10px 5px 9px;border:1px solid var(--rule);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);font-weight:500;background:linear-gradient(180deg,rgba(184,153,104,.018),transparent);transition:border-color 200ms ease,color 200ms ease}
.firm-card:hover .firm-chip{border-color:rgba(184,153,104,.4);color:var(--ink-soft)}
.firm-chip-label{color:var(--ink-mute);font-size:9px;letter-spacing:.22em}
.firm-chip-val{font-family:var(--serif);font-style:italic;font-weight:400;font-size:14px;letter-spacing:.01em;color:var(--metal);text-transform:none}
.firm-chip.audit-A .firm-chip-val{color:var(--metal-glow)}
.firm-chip.audit-B .firm-chip-val{color:var(--metal)}
.firm-chip.audit-C .firm-chip-val{color:#a07a40}
.firm-chip.audit-unaudited .firm-chip-val{color:var(--ink-mute);font-style:normal;font-family:var(--sans);font-size:10px;letter-spacing:.16em;text-transform:uppercase;font-weight:500}
.firm-verify{display:inline-flex;align-items:center;gap:4px;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-mute);font-weight:500;border-bottom:1px dotted var(--rule);padding-bottom:2px;transition:color 200ms ease,border-color 200ms ease}
.firm-verify:hover{color:var(--metal);border-bottom-color:var(--metal)}

/* ─── Empty state (zero firms in proximity) ─────────────────────── */
.empty-state{position:relative;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:96px 0 88px;margin:0 0 48px;overflow:hidden}
.empty-state::before{content:"\\002014";position:absolute;top:50%;left:50%;transform:translate(-50%,-58%);font-family:var(--serif);font-style:italic;font-weight:300;font-size:40vw;line-height:1;color:transparent;-webkit-text-stroke:1.5px rgba(184,153,104,.28);pointer-events:none;z-index:0;letter-spacing:-.04em}
@media (max-width:720px){.empty-state{padding:64px 0 60px}.empty-state::before{font-size:54vw;-webkit-text-stroke-width:1.25px}}
.empty-eyebrow{position:relative;z-index:1;font-size:10px;letter-spacing:.32em;text-transform:uppercase;color:var(--metal);font-weight:500;margin:0 0 22px;text-align:center}
.empty-headline{position:relative;z-index:1;font-family:var(--serif);font-weight:300;font-size:56px;line-height:1.05;color:var(--ink);margin:0 auto 18px;letter-spacing:-.015em;text-align:center;max-width:14ch}
.empty-headline em{font-style:italic;color:var(--metal);font-weight:300}
@media (max-width:720px){.empty-headline{font-size:38px}}
.empty-body{position:relative;z-index:1;font-size:15px;line-height:1.7;color:var(--ink-soft);max-width:54ch;margin:0 auto 44px;text-align:center}
.empty-paths{position:relative;z-index:1;display:grid;grid-template-columns:repeat(3,1fr);gap:0;border-top:1px solid var(--rule);max-width:880px;margin:0 auto}
@media (max-width:720px){.empty-paths{grid-template-columns:1fr}}
.empty-cell{padding:32px 28px;border-right:1px solid var(--rule);display:flex;flex-direction:column;gap:12px;transition:background 280ms ease}
.empty-cell:last-child{border-right:none}
.empty-cell:hover{background:linear-gradient(180deg,rgba(184,153,104,.025),transparent)}
@media (max-width:720px){.empty-cell{border-right:none;border-bottom:1px solid var(--rule);padding:28px 16px}.empty-cell:last-child{border-bottom:none}}
.empty-num{font-family:var(--serif);font-style:italic;font-weight:300;color:transparent;-webkit-text-stroke:1px var(--metal);font-size:28px;line-height:1;letter-spacing:-.02em}
.empty-cell h4{font-family:var(--serif);font-weight:400;font-size:18px;line-height:1.25;color:var(--ink);margin:0;letter-spacing:-.005em}
.empty-cell p{font-size:13px;line-height:1.6;color:var(--ink-mute);margin:0;flex:1}
.empty-cell a.empty-link{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--metal);font-weight:500;border-bottom:1px solid var(--rule);padding-bottom:3px;align-self:flex-start;transition:border-color 200ms ease,color 200ms ease;text-decoration:none}
.empty-cell a.empty-link:hover{color:var(--metal-glow);border-bottom-color:var(--metal)}

/* ─── Print stylesheet — receipt-friendly /thanks page ───────────── */
@media print {
  :root{--noir:#fff;--noir-rise:#fff;--noir-deep:#fff;--ink:#000;--ink-soft:#222;--ink-mute:#555;--metal:#7a5d2a;--metal-glow:#7a5d2a;--rule:#aaa;--rule-faint:#ddd}
  body{background:#fff!important;color:#000!important;font-size:11pt;line-height:1.5}
  header.brand,.topnav-user,nav,.cta-metal,.cta-row,.config-cta-row,.scroll-reveal{opacity:1!important;transform:none!important;animation:none!important}
  header.brand{padding:0 0 18pt;border-bottom:1px solid #000;margin-bottom:18pt;background:transparent!important}
  header.brand h1, header.brand h1 *{color:#000!important}
  .topnav-user, .signup-vol .v-rule, .signup-vol .v-meta, footer{display:none!important}
  main{padding:0!important;max-width:none}
  /* Receipt prominent */
  .receipt{border:2px solid #000!important;background:#fff!important;page-break-inside:avoid;margin:0 0 14pt!important;padding:12pt 16pt!important}
  .r-label{color:#666!important;font-size:8pt!important}
  .r-val{color:#000!important;font-size:12pt!important}
  /* Display headline */
  .public-display{color:#000!important;font-size:24pt!important;margin:0 0 10pt!important}
  .public-display em{color:#7a5d2a!important}
  .public-vol .v-text, .public-vol .v-meta{color:#7a5d2a!important}
  .public-lede{color:#000!important;font-size:11pt!important;margin:0 0 14pt!important}
  /* Steps as bullet list */
  .thanks-next{margin:14pt 0 0;padding:14pt 0 0;border-top:1px solid #000}
  .thanks-step{font-size:14pt!important;color:#000!important;margin:0 0 10pt!important}
  .thanks-grid{display:block!important}
  .thanks-cell{display:block!important;padding:6pt 0!important;border:none!important;page-break-inside:avoid}
  .thanks-cell:not(:first-child){padding-left:0!important}
  .thanks-num{color:#7a5d2a!important;font-size:10pt!important;margin:0 0 4pt!important}
  .thanks-cell h4{color:#000!important;font-size:12pt!important;margin:0 0 4pt!important}
  .thanks-cell p{color:#000!important;font-size:10pt!important;margin:0 0 6pt!important}
  .thanks-link{display:none!important}
  /* Trust block prominent */
  .public-trust{margin-top:14pt!important;padding:10pt 12pt!important;border:1px solid #000!important;background:#fff!important;page-break-inside:avoid}
  .public-trust .t-eyebrow{color:#7a5d2a!important}
  .public-trust p{color:#000!important;font-size:10pt!important}
  /* Hide all CTAs/buttons in print */
  button,.cta-metal,.config-cta-row{display:none!important}
}

/* ─── Scroll-reveal stagger (matches landing) ────────────────────── */
@media (prefers-reduced-motion: no-preference) {
  .scroll-reveal{opacity:0;transform:translateY(22px);transition:opacity 900ms cubic-bezier(.16,.84,.3,1),transform 900ms cubic-bezier(.16,.84,.3,1)}
  .scroll-reveal.in-view{opacity:1;transform:none}
  .scroll-reveal.s1{transition-delay:80ms}
  .scroll-reveal.s2{transition-delay:160ms}
  .scroll-reveal.s3{transition-delay:240ms}
  .scroll-reveal.s4{transition-delay:320ms}
}

*:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(184,153,104,.45);border-radius:1px}
*:focus:not(:focus-visible){outline:none}

</style>
</head><body class="${opts.variant === 'public' ? 'public' : ''}">
<header class="brand">
  <a class="brand-mono" href="/">${monogram}<h1 style="margin:0"><span style="color:var(--ink)">Counsel</span> <span class="amp">&amp;</span> <span style="color:var(--ink)">Bar</span></h1></a>
  ${userBar}
</header>
<main${isAdmin ? ' class="wide"' : ''}>${body}</main>
<script>
(function(){
  if(!('IntersectionObserver' in window))return;
  if(window.matchMedia('(prefers-reduced-motion: reduce)').matches){
    document.querySelectorAll('.scroll-reveal').forEach(function(el){el.classList.add('in-view');});return;
  }
  var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){e.target.classList.add('in-view');io.unobserve(e.target);}});},{threshold:0.12,rootMargin:'0px 0px -10% 0px'});
  document.querySelectorAll('.scroll-reveal').forEach(function(el){io.observe(el);});
})();
</script>
</body></html>`;
}

function flashFromQuery(req: express.Request): string {
  const msg = (req.query.msg as string) || '';
  const err = (req.query.err as string) || '';
  if (err) return `<div class="flash error">${esc(err)}</div>`;
  if (msg) return `<div class="flash success">${esc(msg)}</div>`;
  return '';
}

// ─── public form ────────────────────────────────────────────────────────────

router.get('/find-a-lawyer', (req, res) => {
  const areaOpts = PRACTICE_AREAS.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join('');
  const urgencyOpts = URGENCY.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join('');
  const budgetOpts = BUDGETS.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join('');
  res.send(layout('Find a Lawyer', `
    <div class="public-vol">
      <span class="v-rule"></span>
      <span class="v-text">Volume I</span>
      <span class="v-meta">Edition 2026 · Public Directory</span>
    </div>
    <h2 class="public-display">Find your California attorney, <em>indexed.</em></h2>
    <p class="public-lede">A short configurator. Three sections — about you, the matter, where you are. We'll show California-licensed firms that handle your practice area in your county. <b>You decide who to call.</b></p>

    <div class="consumer-pact" role="presentation" aria-label="What this directory is, and isn't">
      <div class="consumer-pact-cell">
        <span class="consumer-pact-num">i</span>
        <h3 class="consumer-pact-h">Indexed, <em>not sold.</em></h3>
        <p class="consumer-pact-p">Your details are not shared with firms when you submit this form. No call lists, no SMS blasts, no email forwards. The firms in your results never see this entry.</p>
      </div>
      <div class="consumer-pact-cell">
        <span class="consumer-pact-num">ii</span>
        <h3 class="consumer-pact-h">No fees <em>from firms.</em></h3>
        <p class="consumer-pact-p">No firm pays Counsel &amp; Bar to appear in your results, and we do not collect a per-lead fee from firms you contact. Ranking is by proximity to your ZIP — nearest first. Practice-area filtering is on the roadmap; today every California-licensed firm in your area is shown, ranked by distance.</p>
      </div>
      <div class="consumer-pact-cell">
        <span class="consumer-pact-num">iii</span>
        <h3 class="consumer-pact-h">You make <em>the call.</em></h3>
        <p class="consumer-pact-p">We show you a ranked list of California-licensed firms. Your details transmit only when you click to contact a specific one. Until then, every page on this site is browseable without identifying yourself.</p>
      </div>
    </div>

    ${flashFromQuery(req)}
    <form class="config" method="post" action="/find-a-lawyer" autocomplete="on">

      <div class="config-section scroll-reveal">
        <h3 class="config-step"><em>i</em><span class="label">You</span></h3>
        <div class="config-fields">
          <div>
            <label for="full_name">Your name</label>
            <input id="full_name" name="full_name" required autocomplete="name" placeholder="Jane Doe">
          </div>
          <div class="row2">
            <div>
              <label for="email">Email</label>
              <input id="email" name="email" type="email" required autocomplete="email" placeholder="jane@example.com">
            </div>
            <div>
              <label for="phone">Phone <span style="text-transform:none;letter-spacing:.04em;font-style:italic;color:var(--ink-mute)">(optional)</span></label>
              <input id="phone" name="phone" type="tel" autocomplete="tel" placeholder="(415) 555-0100">
            </div>
          </div>
        </div>
      </div>

      <div class="config-section scroll-reveal">
        <h3 class="config-step"><em>ii</em><span class="label">The matter</span></h3>
        <div class="config-fields">
          <div class="row2">
            <div>
              <label for="practice_area">Practice area</label>
              <select id="practice_area" name="practice_area" required>${areaOpts}</select>
            </div>
            <div>
              <label for="urgency">How soon?</label>
              <select id="urgency" name="urgency" required>${urgencyOpts}</select>
            </div>
          </div>
          <div>
            <label for="case_description">Briefly, what's going on?</label>
            <textarea id="case_description" name="case_description" rows="4" placeholder="A few sentences. Don't include sensitive details — that's for the consult." maxlength="2000"></textarea>
            <p class="field-note">Optional. Kept private — only shown to a firm if you click to contact them.</p>
          </div>
        </div>
      </div>

      <div class="config-section scroll-reveal">
        <h3 class="config-step"><em>iii</em><span class="label">Geography</span></h3>
        <div class="config-fields">
          <div class="row2">
            <div>
              <label for="zip">ZIP code</label>
              <input id="zip" name="zip" pattern="[0-9]{5}" placeholder="90210" required>
            </div>
            <div>
              <label for="budget">Budget</label>
              <select id="budget" name="budget">${budgetOpts}</select>
            </div>
          </div>
        </div>
      </div>

      <div class="config-cta-row">
        <button type="submit" class="cta-metal" data-default-text="See California-licensed firms" data-loading-text="Logging your inquiry…">
          <span class="cta-text">See California-licensed firms</span>
          <span class="cta-arrow" style="font-family:var(--serif);font-style:italic;font-size:18px;line-height:1">→</span>
          <span class="cta-spinner" aria-hidden="true"></span>
        </button>
        <p class="field-note" style="margin:0;max-width:32ch">By submitting you authorize us to display California-licensed firms in your area, ranked by proximity. Your details are not sent to any firm until you click to contact one.</p>
      </div>
    </form>
    <script>
      (function(){
        var f = document.querySelector('form.config');
        if (!f) return;
        f.addEventListener('submit', function(){
          var btn = f.querySelector('button[type="submit"]');
          if (!btn) return;
          btn.disabled = true;
          btn.classList.add('is-loading');
          var t = btn.querySelector('.cta-text');
          if (t) t.textContent = btn.dataset.loadingText || 'Submitting…';
        });
      })();
    </script>

    <aside class="public-trust scroll-reveal">
      <p class="t-eyebrow">A directory, not a referral service</p>
      <p>Every attorney is checked against the State Bar of California's active roll — verify status yourself at <a href="https://www.calbar.ca.gov" rel="noopener">calbar.ca.gov</a> before retaining anyone. We do not route, sell, or share your lead-form data; communications here are <strong>not</strong> privileged until an attorney-client relationship exists.</p>
    </aside>
  `, { variant: 'public' }));
});

router.post('/find-a-lawyer', async (req, res) => {
  if (!req.body || typeof req.body !== 'object') {
    return res.status(400).send('Invalid request body');
  }
  const b = req.body as any;
  try {
    if (!b.full_name || !b.email || !b.practice_area || !b.urgency || !b.zip) {
      throw new Error('Please fill the required fields');
    }
    if (!/^\d{5}$/.test(b.zip)) throw new Error('ZIP must be 5 digits');
    const allowedAreas = PRACTICE_AREAS.map(p => p[0]);
    if (!allowedAreas.includes(b.practice_area)) throw new Error('Invalid practice area');

    const ins = await query<{ id: number }>(`
      INSERT INTO leads (full_name, email, phone, practice_area, zip, case_description, budget, urgency, consent_to_contact, ip, user_agent, source)
      VALUES ($1, LOWER($2), $3, $4, $5, $6, $7, $8, $9, $10, $11, 'public_form')
      RETURNING id
    `, [
      String(b.full_name).trim().slice(0, 120),
      String(b.email).trim().slice(0, 200),
      b.phone ? String(b.phone).trim().slice(0, 40) : null,
      b.practice_area,
      String(b.zip).trim(),
      b.case_description ? String(b.case_description).trim().slice(0, 2000) : null,
      b.budget || null,
      b.urgency,
      !!b.consent_to_contact,
      req.ip || null,
      (req.headers['user-agent'] || '').toString().slice(0, 200),
    ]);
    const leadId = ins.rows[0].id;
    // §6155 NOTE: this lookup populates the list of nearby firms we show to
    // the CONSUMER on the thanks page so they can pick who to contact. We do
    // NOT push the lead to firms automatically, do not email firms about the
    // lead, and do not change status to "matched" — this is directory display,
    // not a referral. Status stays 'new' until the consumer explicitly clicks
    // to contact a specific firm (handled in a separate /contact-firm route).
    try {
      const candidates = await matchFirmsForLead({ zip: String(b.zip).trim(), limit: 5 });
      if (candidates.length > 0) {
        const firmIds = candidates.map(m => m.id);
        await query(
          `UPDATE leads SET matched_firm_ids = $2 WHERE id = $1`,
          [leadId, firmIds]);
      }
    } catch (matchErr) {
      console.error(`[leads] candidate firm lookup error for #${leadId}:`, matchErr);
    }
    const areaLabel = (PRACTICE_AREAS.find(a => a[0] === b.practice_area)?.[1]) || b.practice_area;
    res.redirect(`/find-a-lawyer/thanks?zip=${encodeURIComponent(String(b.zip).trim())}&area=${encodeURIComponent(areaLabel)}`);
  } catch (e) {
    res.redirect('/find-a-lawyer?err=' + encodeURIComponent((e as Error).message));
  }
});

router.get('/find-a-lawyer/results', async (req, res) => {
  const zip = ((req.query.zip as string) || '').trim().slice(0, 5);
  const area = ((req.query.area as string) || '').trim().slice(0, 80);
  const radiusRaw = parseInt((req.query.radius as string) || '', 10);
  const radiusKm = Number.isFinite(radiusRaw) && radiusRaw > 0 && radiusRaw <= 500 ? radiusRaw : 80;
  const safeZip = /^\d{5}$/.test(zip) ? zip : null;
  let firms: any[] = [];
  let reason: 'ok' | 'no_zip' | 'no_anchor' | 'no_firms_in_radius' = 'ok';
  try {
    const result = await matchFirmsWithReason({ zip: safeZip, limit: 8, radiusKm });
    firms = result.firms;
    reason = result.reason;
  } catch (e) {
    console.error('[find/results] match error', e);
  }
  const auditChip = (score: number | null): string => {
    if (score == null) {
      return `<span class="firm-chip audit-unaudited"><span class="firm-chip-label">Site audit</span><span class="firm-chip-val">unaudited</span></span>`;
    }
    let grade = 'C', cls = 'audit-C';
    if (score >= 90) { grade = 'A'; cls = 'audit-A'; }
    else if (score >= 80) { grade = 'A−'; cls = 'audit-A'; }
    else if (score >= 70) { grade = 'B+'; cls = 'audit-B'; }
    else if (score >= 60) { grade = 'B'; cls = 'audit-B'; }
    else if (score >= 50) { grade = 'B−'; cls = 'audit-B'; }
    else if (score >= 40) { grade = 'C+'; cls = 'audit-C'; }
    return `<span class="firm-chip ${cls}"><span class="firm-chip-label">Site audit</span><span class="firm-chip-val">${grade}</span></span>`;
  };
  const sizeChip = (band: string | null, count: number | null): string => {
    if (!band && !count) return '';
    const display = band ? esc(band).replace(/_/g, ' ').toLowerCase() : `${count} attorney${count === 1 ? '' : 's'}`;
    return `<span class="firm-chip"><span class="firm-chip-label">Firm size</span><span class="firm-chip-val">${display}</span></span>`;
  };
  const verifyLink = (name: string): string => {
    const q = encodeURIComponent(name);
    return `<a class="firm-verify" href="https://apps.calbar.ca.gov/attorney/Licensee/Search?Type=B&FreeText=${q}" target="_blank" rel="noopener" aria-label="State Bar of California licensee lookup (independent — no affiliation)">verify on State Bar →</a>`;
  };

  const firmCards = firms.length === 0
    ? ''
    : firms.map((f, i) => `
        <div class="firm-card scroll-reveal">
          <div class="firm-num-col">
            <span class="firm-num-eyebrow">№</span>
            <div class="firm-num">${String(i + 1).padStart(2, '0')}</div>
          </div>
          <div class="firm-body">
            <h3 class="firm-name">${esc(f.name || 'Firm')}</h3>
            ${(() => {
              const metaParts: string[] = [];
              if (f.city) metaParts.push(`<span>${esc(f.city)}${f.zip ? ', CA ' + esc(f.zip) : ''}</span>`);
              if (f.attorney_count) metaParts.push(`<span>${f.attorney_count} attorney${f.attorney_count === 1 ? '' : 's'}</span>`);
              if (!metaParts.length) return '';
              const cls = metaParts.length === 1 ? 'firm-meta firm-meta-solo' : 'firm-meta';
              return `<div class="${cls}">${metaParts.join(' <span class="firm-dot">·</span> ')}</div>`;
            })()}
            <div class="firm-chips">
              ${auditChip(f.marketing_score)}
              ${sizeChip(f.firm_size_band, f.attorney_count)}
            </div>
            <div class="firm-actions">
              ${f.phone ? `<a class="firm-tel" href="tel:${esc(f.phone)}">${esc(f.phone)}</a>` : ''}
              ${f.website ? `<a class="firm-web" href="${esc(f.website)}" target="_blank" rel="noopener">Visit website →</a>` : ''}
              ${verifyLink(f.name || '')}
            </div>
          </div>
          ${f.distance_km != null ? `<div class="firm-distance">${f.distance_km.toFixed(1)}<span class="unit">km</span></div>` : '<div></div>'}
        </div>`).join('');

  const isEmpty = firms.length === 0;

  // Reason-branched copy. Three cases produce different headlines, ledes, and path cells.
  const VOL_META: Record<typeof reason, string> = {
    ok: 'Your Directory Results',
    no_zip: 'Browse The Directory',
    no_anchor: 'Unmapped ZIP',
    no_firms_in_radius: 'No Match In Radius',
  };

  let headline: string;
  let lede: string;
  if (!isEmpty) {
    headline = `<h2 class="public-display">${firms.length} firm${firms.length === 1 ? '' : 's'} <em>near ${safeZip || 'you'}</em></h2>`;
    lede = `<p class="public-lede">Drawn from the State Bar of California public roll. <strong>Sorted by proximity${safeZip ? ' to ZIP ' + safeZip : ''}.</strong> You choose who to call. Verify license status before retaining anyone.</p>`;
  } else if (reason === 'no_anchor') {
    headline = `<h2 class="public-display">No anchor for <em>ZIP ${safeZip || ''}.</em></h2>`;
    lede = `<p class="public-lede">This directory has no firms registered in or adjacent to that ZIP, so we cannot compute a proximity ranking for it. <strong>This isn't a search failure — it's an honest limit of the data we hold.</strong> Two paths below.</p>`;
  } else if (reason === 'no_firms_in_radius') {
    headline = `<h2 class="public-display">No firms <em>within ${radiusKm}&nbsp;km.</em></h2>`;
    lede = `<p class="public-lede">This directory lists no California-licensed firms within ${radiusKm}&nbsp;km of ZIP ${safeZip}. Tens of thousands of attorneys are licensed statewide; the closest may simply sit outside this radius. Three honest options below.</p>`;
  } else {
    headline = `<h2 class="public-display">No results.</h2>`;
    lede = `<p class="public-lede">Try entering a California ZIP code to see firms ranked by proximity.</p>`;
  }

  // Path cells vary by reason. "Widen radius" suppressed when no_anchor (would still return empty).
  const cellWiden = `
    <div class="empty-cell scroll-reveal s1">
      <span class="empty-num">i.</span>
      <h4>Widen the radius</h4>
      <p>Re-run the search at 160&nbsp;km (~100 mi) to capture firms in the next county. Most will travel for a qualifying matter.</p>
      <a class="empty-link" href="/find-a-lawyer/results?zip=${safeZip}&radius=160">Search 160&nbsp;km →</a>
    </div>`;
  const cellMetro = `
    <div class="empty-cell scroll-reveal s2">
      <span class="empty-num">${reason === 'no_anchor' ? 'i.' : 'ii.'}</span>
      <h4>Anchor on a metro</h4>
      <p>Statewide-licensed firms in Los Angeles, San Francisco, and San Diego accept matters across California. Browse by metro instead of ZIP.</p>
      <a class="empty-link" href="/find-a-lawyer/results?zip=90013">Try downtown LA →</a>
    </div>`;
  const cellVerify = `
    <div class="empty-cell scroll-reveal s3">
      <span class="empty-num">${reason === 'no_anchor' ? 'ii.' : 'iii.'}</span>
      <h4>Verify a name</h4>
      <p>If you already have a lawyer in mind, look them up directly on the State Bar's public roll to confirm active license and standing.</p>
      <a class="empty-link" href="https://apps.calbar.ca.gov/attorney/Licensee/Search" target="_blank" rel="noopener">State Bar lookup →</a>
    </div>`;

  const cells = reason === 'no_anchor'
    ? cellMetro + cellVerify
    : cellWiden + cellMetro + cellVerify;

  const eyebrowText = reason === 'no_anchor' ? '— Outside the registry —' : '— Outside the radius —';
  const emptyHeadline = reason === 'no_anchor'
    ? `<h3 class="empty-headline">Try a <em>metro anchor</em>, or look the lawyer up directly.</h3>`
    : `<h3 class="empty-headline">A wider <em>circle</em> is usually all it takes.</h3>`;
  const emptyBody = reason === 'no_anchor'
    ? `<p class="empty-body">California is the country's largest legal market. Most matters can be handled by counsel statewide; physical proximity matters less than fit, fee structure, and bar standing. Pick the path that suits your matter.</p>`
    : `<p class="empty-body">California is the country's largest legal market. Most matters can be handled by counsel statewide; physical proximity matters less than fit, fee structure, and bar standing. Pick the path that suits your matter.</p>`;

  const emptyBlock = isEmpty ? `
    <section class="empty-state scroll-reveal" aria-label="${eyebrowText}">
      <p class="empty-eyebrow">${eyebrowText}</p>
      ${emptyHeadline}
      ${emptyBody}
      <div class="empty-paths" style="${reason === 'no_anchor' ? 'grid-template-columns:repeat(2,1fr);max-width:620px' : ''}">
        ${cells}
      </div>
    </section>
  ` : '';

  const vetPact = isEmpty ? '' : `
    <section aria-label="How to vet the firms below" style="margin:48px 0 40px">
      <p style="font-size:10px;letter-spacing:.3em;text-transform:uppercase;color:var(--metal);font-weight:500;margin:0 0 14px">— How to vet the names below —</p>
      <div class="consumer-pact" role="presentation">
        <div class="consumer-pact-cell">
          <span class="consumer-pact-num">i</span>
          <h3 class="consumer-pact-h">Verify <em>the license.</em></h3>
          <p class="consumer-pact-p">Each card below has a "verify on State Bar" link. Click it before contacting. The State Bar's public roll is the only authoritative source for active license, county of admission, and discipline history — not us, not Google, not the firm's own website.</p>
        </div>
        <div class="consumer-pact-cell">
          <span class="consumer-pact-num">ii</span>
          <h3 class="consumer-pact-h">Ask about <em>fees first.</em></h3>
          <p class="consumer-pact-p">Flat fee vs hourly vs contingency. Whether a phone consult is free or billed. Whether a retainer is required to engage. Get the engagement structure in writing before you describe the matter in detail — this is normal, expected, and the firm should welcome the question.</p>
        </div>
        <div class="consumer-pact-cell">
          <span class="consumer-pact-num">iii</span>
          <h3 class="consumer-pact-h">Get it <em>in writing.</em></h3>
          <p class="consumer-pact-p">Before retaining, ask the attorney to put the matter, the scope of representation, and the next concrete step in writing. You are entitled to that record. Counsel &amp; Bar does not draft, review, or evaluate that document — it stays between you and the firm.</p>
        </div>
      </div>
    </section>`;
  const firmsBlock = isEmpty ? '' : vetPact + `<section class="firms">${firmCards}</section>`;

  // Trust block varies — populated case references "names above"; empty cases don't.
  const trustBlock = !isEmpty
    ? `<aside class="public-trust scroll-reveal" style="margin-top:48px">
         <p class="t-eyebrow">For the record</p>
         <p>Names and contacts above are sourced from the State Bar of California's public licensing database. <strong>Counsel &amp; Bar does not refer your case</strong>, does not accept fees from these firms for placement, and has not contacted any of them on your behalf. Always confirm license status at <a href="https://www.calbar.ca.gov" rel="noopener">calbar.ca.gov</a> before engaging.</p>
       </aside>`
    : `<aside class="public-trust scroll-reveal" style="margin-top:48px">
         <p class="t-eyebrow">For the record</p>
         <p>This directory lists California-licensed attorneys and firms drawn from the State Bar's public roll. <strong>Counsel &amp; Bar does not refer cases, does not accept placement fees, and does not contact firms on a user's behalf.</strong> The State Bar at <a href="https://www.calbar.ca.gov" rel="noopener">calbar.ca.gov</a> is the authoritative source for license status — always verify before engaging.</p>
       </aside>`;

  res.send(layout('California-licensed firms', `
    <div class="public-vol">
      <span class="v-rule"></span>
      <span class="v-text">Volume I</span>
      <span class="v-meta">Edition 2026 · ${VOL_META[reason]}</span>
    </div>
    ${headline}
    ${lede}
    ${area && !isEmpty ? `<p class="config-tagline" style="font-family:var(--serif);font-style:italic;color:var(--metal);font-size:14px;letter-spacing:.04em;margin:-32px 0 56px">Your inquiry: ${esc(area)} &nbsp;·&nbsp; <span style="color:var(--ink-mute);font-style:normal;font-family:var(--sans);font-size:11px;letter-spacing:.18em;text-transform:uppercase">practice-area filter coming soon — verify each firm handles your matter before contacting</span></p>` : ''}

    ${emptyBlock}
    ${firmsBlock}
    ${trustBlock}
  `, { variant: 'public' }));
});

router.get('/find-a-lawyer/thanks', (req, res) => {
  // Generate a non-PII receipt token: YYYY-MM-DD-HHMM-XXXX (4-char rand)
  const now = new Date();
  const pad = (n: number) => String(n).padStart(2, '0');
  const rand = Math.random().toString(36).slice(2, 6).toUpperCase();
  const inquiryNo = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}-${rand}`;
  const tstamp = `${pad(now.getHours())}:${pad(now.getMinutes())} PT · ${now.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}`;
  const zip = esc((req.query.zip as string) || '');
  const area = esc((req.query.area as string) || '');
  const receiptDetails = [zip && `ZIP ${zip}`, area && area].filter(Boolean).join(' · ');
  const resultsHref = `/find-a-lawyer/results${zip || area ? `?${[zip && `zip=${zip}`, area && `area=${encodeURIComponent(area)}`].filter(Boolean).join('&')}` : ''}`;
  res.send(layout('Inquiry logged', `
    <div class="public-vol">
      <span class="v-rule"></span>
      <span class="v-text">Volume I</span>
      <span class="v-meta">Edition 2026 · Inquiry Logged</span>
    </div>
    <h2 class="public-display">Your inquiry is <em>logged.</em></h2>
    <p class="public-lede">We've recorded your details for our administrative records. <strong>We do not contact firms on your behalf.</strong> This is a directory, not a referral service — you choose who to call, when to call, and what to say.</p>

    <div class="receipt scroll-reveal">
      <div class="receipt-row">
        <span class="r-label">Inquiry №.</span>
        <span class="r-val">${inquiryNo}</span>
      </div>
      <div class="receipt-row">
        <span class="r-label">Logged</span>
        <span class="r-val">${tstamp}</span>
      </div>
      ${receiptDetails ? `<div class="receipt-row"><span class="r-label">Filed under</span><span class="r-val">${receiptDetails}</span></div>` : ''}
    </div>

    <section class="thanks-next">
      <h3 class="thanks-step">What happens next</h3>
      <div class="thanks-grid">
        <div class="thanks-cell scroll-reveal">
          <span class="thanks-num">i.</span>
          <h4>Browse the directory</h4>
          <p>Open the directory and filter California-licensed firms by city, practice area, and county. Read profiles, reviews, and audit notes. Take your time.</p>
          <a class="thanks-link" href="${resultsHref}">See your firms →</a>
        </div>
        <div class="thanks-cell scroll-reveal">
          <span class="thanks-num">ii.</span>
          <h4>Verify with the State Bar</h4>
          <p>Before retaining anyone, confirm the attorney's license is active and in good standing on the State Bar's public roll. We draw from that roll; the live source at calbar.ca.gov remains authoritative for any disciplinary status check.</p>
          <a class="thanks-link" href="https://apps.calbar.ca.gov/attorney/Licensee/Search" target="_blank" rel="noopener">State Bar licensee search →</a>
        </div>
        <div class="thanks-cell scroll-reveal">
          <span class="thanks-num">iii.</span>
          <h4>Contact firms directly</h4>
          <p>Use the published phone, email, or contact form on each firm's listing. Don't include privileged or sensitive details until you've established an attorney-client relationship with a specific lawyer.</p>
          <a class="thanks-link" href="${resultsHref}">Begin browsing →</a>
        </div>
      </div>
    </section>

    <aside class="public-trust scroll-reveal" style="margin-top:48px">
      <p class="t-eyebrow">For the record</p>
      <p>Communications submitted to this site are <strong>not</strong> privileged. We do not share your inquiry data with attorneys, do not sell consumer leads, and do not accept fees from attorneys for placement of any specific consumer. <a href="/privacy.html">Privacy</a> · <a href="/terms.html">Terms</a></p>
    </aside>

  `, { variant: 'public' }));
});

// ─── admin queue ────────────────────────────────────────────────────────────

router.get('/admin/leads', requireAdmin, async (req, res) => {
  const status = ((req.query.status as string) || '').trim();
  const area = ((req.query.area as string) || '').trim();
  const where: string[] = [];
  const params: any[] = [];
  if (status && (STATUSES as readonly string[]).includes(status)) { params.push(status); where.push(`status = $${params.length}`); }
  if (area) { params.push(area); where.push(`practice_area = $${params.length}`); }
  const sql = `
    SELECT
      l.id, l.full_name, l.email, l.phone, l.practice_area, l.zip, l.urgency, l.budget, l.status,
      LEFT(COALESCE(l.case_description,''), 140) AS case_short, l.admin_notes, l.created_at,
      l.matched_firm_ids,
      COALESCE(
        (SELECT json_agg(json_build_object(
            'id', o.id, 'name', o.name, 'city', o.city, 'phone', o.phone, 'website', o.website
          ) ORDER BY array_position(l.matched_firm_ids, o.id))
         FROM organizations o
         WHERE o.id = ANY(l.matched_firm_ids)),
        '[]'::json
      ) AS matched_firms
    FROM leads l
    ${where.length ? 'WHERE ' + where.join(' AND ') : ''}
    ORDER BY l.id DESC
    LIMIT 200
  `;
  const r = await query(sql, params);

  const counts = await query<{ k: string; v: string }>(`
    SELECT 'total' AS k, COUNT(*)::text AS v FROM leads
    UNION ALL SELECT 'new',       COUNT(*)::text FROM leads WHERE status='new'
    UNION ALL SELECT 'matched',   COUNT(*)::text FROM leads WHERE status='matched'
    UNION ALL SELECT 'contacted', COUNT(*)::text FROM leads WHERE status='contacted'
    UNION ALL SELECT 'won',       COUNT(*)::text FROM leads WHERE status='won'
  `);
  const cm: Record<string, string> = {}; for (const row of counts.rows) cm[row.k] = row.v;

  const filterPills = ['', ...STATUSES].map(s =>
    `<a class="pill ${esc(s)}" href="/admin/leads${s ? `?status=${esc(s)}` : ''}" style="text-decoration:none;${status === s ? 'outline:2px solid var(--accent)' : ''}">${s || 'all'}</a>`
  ).join(' ');

  const rows = r.rows.map((l: any) => {
    const opts = STATUSES.map(s => `<option value="${s}"${l.status === s ? ' selected' : ''}>${s}</option>`).join('');
    const firms = (l.matched_firms || []) as Array<{id:number;name:string;city:string|null;phone:string|null;website:string|null}>;
    const firmsCell = firms.length === 0
      ? '<span class="muted">—</span>'
      : firms.map(f => {
          const links: string[] = [];
          if (f.phone) links.push(`<a href="tel:${esc(f.phone)}" title="${esc(f.phone)}">☎</a>`);
          if (f.website) links.push(`<a href="${esc(f.website)}" target="_blank" rel="noopener" title="${esc(f.website)}">↗</a>`);
          return `<div class="match-row"><span class="match-name">${esc(f.name)}</span><span class="match-city">${esc(f.city || '')}</span> ${links.join(' ')}</div>`;
        }).join('');
    return `<tr>
      <td>#${l.id}<br><span class="muted">${new Date(l.created_at).toISOString().slice(0, 16).replace('T',' ')}</span></td>
      <td><b>${esc(l.full_name)}</b><br>
        <a href="mailto:${esc(l.email)}">${esc(l.email)}</a>${l.phone ? `<br><a href="tel:${esc(l.phone)}">${esc(l.phone)}</a>` : ''}
      </td>
      <td>${esc(l.practice_area)}<br><span class="muted">ZIP ${esc(l.zip || '—')}</span></td>
      <td><span class="muted">${esc(l.urgency || '—')}</span><br><span class="muted">${esc(l.budget || '—')}</span></td>
      <td style="max-width:240px">${esc(l.case_short || '—')}</td>
      <td style="min-width:240px;font-size:12px;line-height:1.5">${firmsCell}</td>
      <td><span class="pill ${esc(l.status)}">${esc(l.status)}</span></td>
      <td>
        <form method="post" action="/admin/leads/${l.id}/status" class="actions">
          <select name="status" onchange="this.form.submit()">${opts}</select>
        </form>
      </td>
    </tr>`;
  }).join('');

  res.send(layout('Admin · Leads', `
    <h2>Leads</h2>
    <div class="muted" style="margin-bottom:14px">
      <b>${cm.total || 0}</b> total · <b>${cm.new || 0}</b> new · <b>${cm.matched || 0}</b> matched · <b>${cm.contacted || 0}</b> contacted · <b>${cm.won || 0}</b> won
    </div>
    ${flashFromQuery(req)}
    <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px">${filterPills}</div>
    <style>
      .match-row{padding:3px 0;display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--rule-faint)}
      .match-row:last-child{border-bottom:none}
      .match-name{color:var(--ink);font-weight:500}
      .match-city{color:var(--ink-mute);font-style:italic;font-size:11px}
      .match-row a{color:var(--metal);text-decoration:none;padding:0 4px}
      .match-row a:hover{color:var(--metal-glow)}
    </style>
    <table>
      <thead><tr><th>ID / When</th><th>Person</th><th>Area</th><th>Urgency / $</th><th>Case</th><th>Matched firms</th><th>Status</th><th></th></tr></thead>
      <tbody>${rows || '<tr><td colspan="8" class="muted" style="padding:30px;text-align:center">No leads yet — share <a href="/find-a-lawyer">the form</a></td></tr>'}</tbody>
    </table>
  `, { user: req.user }));
});

router.post('/admin/leads/:id/status', requireAdmin, async (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (!req.body || typeof req.body !== 'object') {
    return res.status(400).send('Invalid request body');
  }
  const status = (req.body.status as string)?.trim();
  if (!(STATUSES as readonly string[]).includes(status)) {
    return res.redirect('/admin/leads?err=' + encodeURIComponent('Invalid status'));
  }
  await query(`UPDATE leads SET status = $2 WHERE id = $1`, [id, status]);
  res.redirect('/admin/leads?msg=' + encodeURIComponent(`#${id} → ${status}`));
});

router.post('/admin/leads/:id/notes', requireAdmin, async (req, res) => {
  const id = parseInt(req.params.id, 10);
  if (!req.body || typeof req.body !== 'object') {
    return res.status(400).send('Invalid request body');
  }
  const notes = (req.body.notes as string)?.trim() || null;
  await query(`UPDATE leads SET admin_notes = $2 WHERE id = $1`, [id, notes]);
  res.redirect('/admin/leads?msg=' + encodeURIComponent(`#${id} notes saved`));
});

export default router;