<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title>Find a Consultant — sfpermits.ai</title>
{% include "fragments/head_obsidian.html" %}
<script nonce="{{ csp_nonce }}" src="/static/htmx.min.js"></script>
<style nonce="{{ csp_nonce }}">
/* ── Page-scoped tokens ─────────────────────────────────────────────── */
:root {
--obsidian: #0a0a0f;
--obsidian-mid: #12121a;
--obsidian-light: #1a1a26;
--glass: rgba(255, 255, 255, 0.04);
--glass-border: rgba(255, 255, 255, 0.06);
--glass-hover: rgba(255, 255, 255, 0.10);
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.55);
--text-tertiary: rgba(255, 255, 255, 0.30);
--text-ghost: rgba(255, 255, 255, 0.15);
--accent: #5eead4;
--accent-glow: rgba(94, 234, 212, 0.08);
--accent-ring: rgba(94, 234, 212, 0.30);
--signal-green: #34d399;
--signal-amber: #fbbf24;
--signal-red: #f87171;
--signal-blue: #60a5fa;
--dot-green: #22c55e;
--dot-amber: #f59e0b;
--dot-red: #ef4444;
--mono: 'JetBrains Mono', ui-monospace, 'Cascadia Code', monospace;
--sans: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--text-xs: clamp(0.65rem, 0.6rem + 0.2vw, 0.75rem);
--text-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-base: clamp(0.8125rem, 0.75rem + 0.3vw, 1rem);
--text-lg: clamp(0.875rem, 0.8rem + 0.4vw, 1.125rem);
--text-xl: clamp(1.125rem, 1rem + 0.5vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.2rem + 1.2vw, 2.5rem);
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-full: 9999px;
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 20px; --space-6: 24px; --space-8: 32px;
--space-10: 40px; --space-12: 48px; --space-16: 64px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--obsidian);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
main { padding: var(--space-10) 0 var(--space-16); }
.page-title {
font-family: var(--sans);
font-size: var(--text-2xl);
font-weight: 300;
color: var(--text-primary);
margin-bottom: var(--space-2);
}
.page-subtitle {
font-family: var(--sans);
font-size: var(--text-base);
font-weight: 300;
color: var(--text-secondary);
margin-bottom: var(--space-8);
line-height: 1.5;
}
/* ── Consultant-specific form layout ─────────────────────────────── */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
margin-bottom: var(--space-4);
}
.form-row.three-col { grid-template-columns: 1fr 1fr 1fr; }
.form-group { display: flex; flex-direction: column; gap: var(--space-2); }
.checkbox-row {
display: flex;
gap: var(--space-6);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
/* ── Consultant card ─────────────────────────────────────────────── */
.consultant-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: var(--space-6);
margin-bottom: var(--space-4);
transition: border-color 0.2s;
}
.consultant-card:hover { border-color: var(--glass-hover); }
.exp-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-3);
}
.exp-rank {
font-family: var(--mono);
font-size: var(--text-lg);
font-weight: 300;
color: var(--accent);
margin-right: var(--space-3);
}
.exp-name {
font-family: var(--sans);
font-size: var(--text-lg);
font-weight: 400;
color: var(--text-primary);
}
.exp-firm {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.exp-score {
background: var(--obsidian-light);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: var(--space-2) var(--space-3);
font-family: var(--mono);
font-weight: 300;
font-size: var(--text-lg);
color: var(--accent);
white-space: nowrap;
}
.exp-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-2) var(--space-6);
margin-bottom: var(--space-2);
}
.exp-detail {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.exp-detail strong {
font-family: var(--mono);
font-weight: 300;
color: var(--text-primary);
}
.exp-registered {
display: inline-flex;
align-items: center;
gap: var(--space-1);
color: var(--signal-green);
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
}
.exp-contact {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin-top: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--glass-border);
}
.exp-contact a {
color: var(--accent);
text-decoration: none;
}
.exp-contact a:hover { text-decoration: underline; }
.exp-breakdown {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-top: var(--space-2);
font-style: italic;
}
.exp-neighborhoods {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-top: var(--space-2);
}
.exp-hood-tag {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
padding: 2px var(--space-2);
}
/* ── Smart badges ────────────────────────────────────────────────── */
.exp-badges { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0; }
.exp-badge {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 400;
padding: 2px var(--space-2);
border-radius: var(--radius-sm);
white-space: nowrap;
}
/* Badge colors use token signal colors only */
.badge-address {
background: rgba(94, 234, 212, 0.08);
color: var(--accent);
border: 1px solid var(--accent-ring);
}
.badge-ethics {
background: rgba(52, 211, 153, 0.08);
color: var(--signal-green);
border: 1px solid rgba(52, 211, 153, 0.25);
}
.badge-hood {
background: rgba(96, 165, 250, 0.08);
color: var(--signal-blue);
border: 1px solid rgba(96, 165, 250, 0.25);
}
.badge-volume {
background: rgba(251, 191, 36, 0.08);
color: var(--signal-amber);
border: 1px solid rgba(251, 191, 36, 0.25);
}
.badge-network {
background: rgba(96, 165, 250, 0.08);
color: var(--signal-blue);
border: 1px solid rgba(96, 165, 250, 0.25);
}
.badge-recent {
background: rgba(52, 211, 153, 0.06);
color: var(--signal-green);
border: 1px solid rgba(52, 211, 153, 0.2);
}
/* ── Sort bar ────────────────────────────────────────────────────── */
.sort-bar {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-4);
flex-wrap: wrap;
}
.sort-label {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.sort-chip {
font-family: var(--mono);
font-size: var(--text-xs);
padding: 5px var(--space-3);
border-radius: var(--radius-full);
background: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.sort-chip:hover {
color: var(--text-primary);
border-color: var(--glass-hover);
}
.sort-chip.active {
background: var(--accent-glow);
color: var(--accent);
border-color: var(--accent-ring);
}
/* ── Status messages ─────────────────────────────────────────────── */
.msg-error {
background: rgba(248, 113, 113, 0.06);
border-left: 2px solid var(--signal-red);
color: var(--signal-red);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-family: var(--sans);
font-size: var(--text-sm);
}
.msg-info {
background: rgba(94, 234, 212, 0.06);
border-left: 2px solid var(--accent-ring);
color: var(--text-secondary);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-sm);
font-family: var(--sans);
font-size: var(--text-sm);
}
.loading {
text-align: center;
color: var(--text-secondary);
font-family: var(--sans);
padding: var(--space-10);
}
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: block; }
.htmx-request .action-btn { opacity: 0.5; }
/* ── Prefill context banner ──────────────────────────────────────── */
.context-banner {
margin-bottom: var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: var(--radius-sm);
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
}
.context-banner strong {
font-family: var(--mono);
font-weight: 300;
color: var(--text-primary);
}
/* ── Results area ────────────────────────────────────────────────── */
.results-area { margin-top: var(--space-8); }
/* ── Responsive ──────────────────────────────────────────────────── */
@media (max-width: 640px) {
.form-row, .form-row.three-col { grid-template-columns: 1fr; }
.exp-details { grid-template-columns: 1fr; }
.checkbox-row { flex-direction: column; gap: var(--space-2); }
}
</style>
</head>
<body>
{% set active_page = 'consultants' %}
{% include "fragments/nav.html" %}
<main>
<div class="obs-container">
<h1 class="page-title">Find a Consultant</h1>
<p class="page-subtitle">
Get ranked recommendations based on permit volume, neighborhood expertise,
professional network, and SF Ethics registration.
</p>
{% if prefill and prefill.signal %}
<div class="context-banner">
Searching for consultants matching your property at
<strong>Block {{ prefill.block }}, Lot {{ prefill.lot }}</strong>
{% if prefill.neighborhood %} in {{ prefill.neighborhood }}{% endif %}...
</div>
{% endif %}
<div class="glass-card" style="margin-bottom: var(--space-6);">
<form id="consultant-form" hx-post="/consultants/search"
hx-target="#results"
hx-indicator="#loading">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-row three-col">
<div class="form-group">
<label class="form-label" for="address">Street Name</label>
<input class="form-input" type="text" id="address" name="address"
placeholder="e.g., ROBIN HOOD"
value="{{ prefill.address if prefill and prefill.address else '' }}">
</div>
<div class="form-group">
<label class="form-label" for="block">Block</label>
<input class="form-input" type="text" id="block" name="block"
placeholder="e.g., 2920"
value="{{ prefill.block if prefill and prefill.block else '' }}">
</div>
<div class="form-group">
<label class="form-label" for="lot">Lot</label>
<input class="form-input" type="text" id="lot" name="lot"
placeholder="e.g., 020"
value="{{ prefill.lot if prefill and prefill.lot else '' }}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="neighborhood">Neighborhood</label>
<select class="form-select" id="neighborhood" name="neighborhood">
<option value="">Any neighborhood</option>
{% for n in neighborhoods %}
{% if n %}
<option value="{{ n }}" {{ 'selected' if prefill and prefill.neighborhood == n else '' }}>{{ n }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label" for="permit_type">Permit Type</label>
<select class="form-select" id="permit_type" name="permit_type">
<option value="">Any type</option>
<option value="additions alterations or repairs">Additions / Alterations / Repairs</option>
<option value="otc alterations">OTC Alterations</option>
<option value="new construction">New Construction</option>
<option value="demolitions">Demolitions</option>
</select>
</div>
</div>
<div class="checkbox-row">
<label class="form-check">
<input type="checkbox" class="form-check__input" name="has_active_complaint" value="on"
{{ 'checked' if prefill and prefill.has_complaint else '' }}>
<span class="form-check__box"></span>
<span class="form-check__label">Active complaint on property</span>
</label>
<label class="form-check">
<input type="checkbox" class="form-check__input" name="needs_planning" value="on">
<span class="form-check__box"></span>
<span class="form-check__label">Needs Planning Dept coordination</span>
</label>
</div>
<input type="hidden" name="sort_by" id="sort_by" value="{{ sort_by if sort_by is defined else 'score' }}">
<button class="action-btn" type="submit">Find Consultants →</button>
</form>
</div>
{% if prefill and prefill.block and prefill.lot %}
<script nonce="{{ csp_nonce }}">
// Auto-submit when arriving from property report with context
document.addEventListener('DOMContentLoaded', function() {
var form = document.getElementById('consultant-form');
if (form && typeof htmx !== 'undefined') {
setTimeout(function() { htmx.trigger(form, 'submit'); }, 300);
}
});
</script>
{% endif %}
<div id="loading" class="htmx-indicator loading">
Analyzing 1M+ permit records...
</div>
<div id="results" class="results-area">
{% if results %}
<div class="sort-bar">
<span class="sort-label">Sort by</span>
<button class="sort-chip {{ 'active' if (sort_by is not defined or sort_by == 'score') else '' }}"
onclick="sortResults('score')">Best Match</button>
<button class="sort-chip {{ 'active' if sort_by is defined and sort_by == 'permits' else '' }}"
onclick="sortResults('permits')">Most Permits</button>
<button class="sort-chip {{ 'active' if sort_by is defined and sort_by == 'recency' else '' }}"
onclick="sortResults('recency')">Most Recent</button>
<button class="sort-chip {{ 'active' if sort_by is defined and sort_by == 'network' else '' }}"
onclick="sortResults('network')">Largest Network</button>
</div>
{% for exp in results %}
<div class="consultant-card">
<div class="exp-header">
<div>
<span class="exp-rank">#{{ loop.index }}</span>
<span class="exp-name">{{ exp.name }}</span>
{% if exp.firm %}
<div class="exp-firm">{{ exp.firm }}</div>
{% endif %}
</div>
<div class="exp-score">{{ exp.score|int }}</div>
</div>
{% if exp.badges is defined and exp.badges %}
<div class="exp-badges">
{% for label, css in exp.badges %}
<span class="exp-badge {{ css }}">{{ label }}</span>
{% endfor %}
</div>
{% elif exp.is_registered %}
<div class="exp-badges">
<span class="exp-badge badge-ethics">Ethics Registered</span>
</div>
{% endif %}
<div class="exp-details">
<div class="exp-detail"><strong>{{ exp.permit_count }}</strong> permits</div>
<div class="exp-detail"><strong>{{ exp.network_size }}</strong> collaborators</div>
<div class="exp-detail">Last active: <strong>{{ exp.date_range_end[:10] if exp.date_range_end else 'N/A' }}</strong></div>
<div class="exp-detail">Entity: <strong>{{ exp.entity_id }}</strong></div>
</div>
{% if exp.neighborhoods %}
<div class="exp-neighborhoods">
{% for hood in exp.neighborhoods[:6] %}
<span class="exp-hood-tag">{{ hood }}</span>
{% endfor %}
{% if exp.neighborhoods|length > 6 %}
<span class="exp-hood-tag">+{{ exp.neighborhoods|length - 6 }} more</span>
{% endif %}
</div>
{% endif %}
{% if exp.contact_info and (exp.contact_info.email or exp.contact_info.phone) %}
<div class="exp-contact">
{% if exp.contact_info.email %}
<a href="mailto:{{ exp.contact_info.email }}">{{ exp.contact_info.email }}</a>
{% endif %}
{% if exp.contact_info.phone %}
· {{ exp.contact_info.phone }}
{% endif %}
</div>
{% endif %}
<div class="exp-breakdown">
{% for key, val in exp.breakdown.items() %}
{{ key|replace('_', ' ')|title }}: {{ val }}{% if not loop.last %} | {% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
{% elif error %}
<div class="msg-error">{{ error }}</div>
{% endif %}
</div>
</div>
</main>
<script nonce="{{ csp_nonce }}">
// Pre-fill form fields from query parameters (e.g., from Property Report links)
(function() {
var params = new URLSearchParams(window.location.search);
var fields = ['block', 'lot', 'address', 'neighborhood', 'permit_type'];
fields.forEach(function(name) {
var val = params.get(name);
if (val) {
var el = document.querySelector('[name=' + name + ']');
if (el) el.value = val;
}
});
// Check boolean flags
if (params.get('has_active_complaint') === 'on') {
var cb = document.querySelector('[name=has_active_complaint]');
if (cb) cb.checked = true;
}
if (params.get('needs_planning') === 'on') {
var cb2 = document.querySelector('[name=needs_planning]');
if (cb2) cb2.checked = true;
}
})();
// Sort results by re-submitting the form with updated sort_by
function sortResults(criterion) {
var sortInput = document.getElementById('sort_by');
if (sortInput) sortInput.value = criterion;
var form = document.getElementById('consultant-form');
if (form && typeof htmx !== 'undefined') {
htmx.trigger(form, 'submit');
}
}
</script>
</body>
</html>