<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo — sfpermits.ai Property Intelligence</title>
<meta name="robots" content="noindex">
<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=JetBrains+Mono:wght@300;400;500&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<style nonce="{{ csp_nonce }}">
: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;
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-full: 9999px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--obsidian);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
}
.container { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
header {
border-bottom: 1px solid var(--glass-border);
padding: 14px 0;
background: var(--obsidian-mid);
}
header .container {
display: flex;
align-items: center;
justify-content: space-between;
}
.logo {
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 300;
letter-spacing: 0.35em;
text-transform: uppercase;
color: var(--text-tertiary);
text-decoration: none;
}
.logo span { color: var(--text-ghost); }
/* Demo badge — uses chip pattern */
.demo-badge {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 400;
color: var(--signal-amber);
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.2);
border-radius: var(--radius-sm);
padding: 2px 8px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.hero {
padding: 32px 0 24px;
border-bottom: 1px solid var(--glass-border);
}
.hero h1 {
font-family: var(--sans);
font-size: 1.3rem;
font-weight: 300;
color: var(--text-secondary);
margin-bottom: 4px;
}
.hero .address {
font-family: var(--mono);
font-size: 2rem;
font-weight: 300;
color: var(--text-primary);
}
.hero .subtitle {
font-family: var(--sans);
font-size: 0.9rem;
color: var(--text-tertiary);
margin-top: 4px;
}
/* Annotation callouts — uses chip/insight pattern */
.callout {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 400;
color: var(--accent);
background: var(--accent-glow);
border: 1px solid var(--accent-ring);
border-radius: var(--radius-sm);
padding: 3px 8px;
display: inline-block;
margin-bottom: 8px;
letter-spacing: 0.02em;
}
/* Grid layout */
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 24px;
}
.demo-grid .full-width {
grid-column: 1 / -1;
}
/* glass-card pattern */
.card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
padding: 20px;
transition: border-color 0.3s;
}
.card:hover { border-color: var(--glass-hover); }
.card-title {
font-family: var(--mono);
font-size: 0.8rem;
font-weight: 400;
color: var(--accent);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.stat-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.stat-pill {
background: var(--obsidian-light);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 10px 16px;
text-align: center;
flex: 1;
min-width: 100px;
transition: border-color 0.3s;
}
.stat-pill:hover { border-color: var(--glass-hover); }
.stat-pill .stat-value {
font-family: var(--mono);
font-size: 1.2rem;
font-weight: 300;
color: var(--text-primary);
}
.stat-pill .stat-label {
font-family: var(--sans);
font-size: 0.7rem;
color: var(--text-tertiary);
margin-top: 2px;
}
/* Permit table — obs-table pattern */
.data-table {
width: 100%;
border-collapse: collapse;
font-family: var(--sans);
font-size: 0.82rem;
}
.data-table th {
font-family: var(--mono);
font-size: 10px;
font-weight: 400;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
text-align: left;
padding: 6px 10px;
border-bottom: 1px solid var(--glass-border);
}
.data-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--glass-border);
color: var(--text-secondary);
vertical-align: top;
}
.data-table tr:hover td { background: var(--glass); }
.data-table .highlight {
font-family: var(--mono);
color: var(--accent);
font-weight: 300;
}
/* Status badges — chip + status-text pattern */
.badge {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 400;
padding: 1px 7px;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.badge-issued { background: rgba(52, 211, 153, 0.12); color: var(--signal-green); border: 1px solid rgba(52, 211, 153, 0.2); }
.badge-filed { background: rgba(96, 165, 250, 0.12); color: var(--signal-blue); border: 1px solid rgba(96, 165, 250, 0.2); }
.badge-complete { background: var(--glass); color: var(--text-secondary); border: 1px solid var(--glass-border); }
.badge-expired { background: rgba(248, 113, 113, 0.12); color: var(--signal-red); border: 1px solid rgba(248, 113, 113, 0.2); }
/* Severity tier badges */
.severity-pill {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 400;
padding: 1px 7px;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 0.04em;
display: inline-block;
}
.severity-CRITICAL { background: rgba(248, 113, 113, 0.12); color: var(--signal-red); border: 1px solid rgba(248, 113, 113, 0.2); }
.severity-HIGH { background: rgba(251, 191, 36, 0.12); color: var(--signal-amber); border: 1px solid rgba(251, 191, 36, 0.2); }
.severity-MEDIUM { background: rgba(251, 191, 36, 0.08); color: var(--signal-amber); border: 1px solid rgba(251, 191, 36, 0.15); }
.severity-LOW { background: rgba(96, 165, 250, 0.12); color: var(--signal-blue); border: 1px solid rgba(96, 165, 250, 0.2); }
.severity-GREEN { background: rgba(52, 211, 153, 0.12); color: var(--signal-green); border: 1px solid rgba(52, 211, 153, 0.2); }
/* Overall severity banner */
.severity-banner {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
border-radius: var(--radius-sm);
margin-top: 8px;
}
.severity-banner .sev-label {
font-family: var(--mono);
font-size: 0.68rem;
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Progress bars for routing */
.routing-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.routing-station {
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 300;
color: var(--text-primary);
min-width: 80px;
}
.routing-bar-bg {
flex: 1;
height: 4px;
background: var(--glass);
border-radius: 2px;
overflow: hidden;
}
.routing-bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s ease;
}
.routing-bar-fill.complete { background: var(--signal-green); }
.routing-bar-fill.in-progress { background: var(--signal-amber); }
.routing-bar-fill.pending { background: var(--text-tertiary); width: 10%; }
.routing-status {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 300;
color: var(--text-tertiary);
min-width: 70px;
text-align: right;
}
/* Entity network */
.entity-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--glass-border);
}
.entity-item:last-child { border-bottom: none; }
.entity-name {
font-family: var(--sans);
font-size: 0.85rem;
color: var(--text-primary);
}
.entity-role {
font-family: var(--mono);
font-size: 0.7rem;
font-weight: 300;
color: var(--text-tertiary);
}
.entity-permits {
font-family: var(--mono);
font-size: 0.8rem;
font-weight: 300;
color: var(--accent);
}
/* Violation/complaint items — insight pattern */
.alert-item {
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-size: 0.82rem;
}
.alert-item.violation {
background: rgba(248, 113, 113, 0.06);
border-left: 2px solid var(--signal-red);
}
.alert-item.complaint {
background: rgba(251, 191, 36, 0.06);
border-left: 2px solid var(--signal-amber);
}
.alert-item .alert-label {
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 400;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.alert-item.violation .alert-label { color: var(--signal-red); }
.alert-item.complaint .alert-label { color: var(--signal-amber); }
.alert-item .alert-text { font-family: var(--sans); color: var(--text-secondary); margin-top: 2px; }
/* Timeline estimate */
.timeline-row {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 8px;
}
.timeline-label {
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 300;
color: var(--text-tertiary);
min-width: 30px;
}
.timeline-bar-bg {
flex: 1;
height: 20px;
background: var(--obsidian-light);
border-radius: var(--radius-sm);
overflow: hidden;
position: relative;
}
.timeline-bar-fill {
height: 100%;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8px;
font-family: var(--mono);
font-size: 0.68rem;
font-weight: 300;
color: rgba(255,255,255, 0.9);
}
.empty-state {
text-align: center;
padding: 32px;
font-family: var(--sans);
color: var(--text-tertiary);
font-size: 0.9rem;
}
/* Architecture / capability showcase */
.arch-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-top: 24px;
}
.arch-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 20px;
text-align: center;
transition: border-color 0.3s;
}
.arch-card:hover { border-color: var(--glass-hover); }
.arch-card .arch-number {
font-family: var(--mono);
font-size: 1.8rem;
font-weight: 300;
color: var(--accent);
}
.arch-card .arch-label {
font-family: var(--sans);
font-size: 0.78rem;
color: var(--text-secondary);
margin-top: 4px;
}
.arch-card .arch-detail {
font-family: var(--sans);
font-size: 0.72rem;
color: var(--text-tertiary);
margin-top: 6px;
}
/* CTA section — ghost-cta pattern */
.cta-section {
text-align: center;
padding: 40px 24px;
margin-top: 32px;
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
transition: border-color 0.3s;
}
.cta-section:hover { border-color: var(--glass-hover); }
.cta-section h2 {
font-family: var(--sans);
font-size: 1.3rem;
font-weight: 300;
color: var(--text-primary);
margin-bottom: 8px;
}
.cta-section p {
font-family: var(--sans);
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 20px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
/* ghost-cta style for the CTA link */
.cta-button {
display: inline-block;
font-family: var(--mono);
font-size: 0.875rem;
font-weight: 300;
color: var(--text-secondary);
background: none;
border: none;
border-bottom: 1px solid transparent;
padding-bottom: 1px;
text-decoration: none;
letter-spacing: 0.04em;
transition: color 0.3s, border-color 0.3s;
}
.cta-button:hover {
color: var(--accent);
border-bottom-color: var(--accent);
}
@media (max-width: 768px) {
.arch-grid { grid-template-columns: repeat(2, 1fr); }
.demo-grid { grid-template-columns: 1fr; }
.hero .address { font-size: 1.4rem; }
}
footer {
border-top: 1px solid var(--glass-border);
padding: 24px 0;
margin-top: 32px;
}
footer p {
font-family: var(--sans);
color: var(--text-tertiary);
font-size: 0.78rem;
text-align: center;
}
footer a { color: var(--accent); text-decoration: none; }
footer a:hover { text-decoration: underline; }
@media (max-width: 480px) {
.container { padding: 0 16px; }
}
/* Density override */
{% if density_max %}
.card { padding: 14px; }
.demo-grid { gap: 12px; }
.data-table { font-size: 0.78rem; }
.data-table th, .data-table td { padding: 6px 8px; }
{% endif %}
</style>
</head>
<body>
<header>
<div class="container">
<a href="/" class="logo">sfpermits<span>.ai</span></a>
<span class="demo-badge">Live Demo</span>
</div>
</header>
<main class="container">
<div class="hero">
<h1>Property Intelligence</h1>
<div class="address">{{ demo_address }}</div>
<div class="subtitle">San Francisco, CA — Block {{ block }}, Lot {{ lot }} — {{ neighborhood }}</div>
{% if severity_tier %}
<div class="severity-banner severity-{{ severity_tier }}">
<span class="sev-label">Severity</span>
<span class="severity-pill severity-{{ severity_tier }}">{{ severity_tier }}</span>
{% if severity_score is not none %}
<span style="font-family: var(--mono); font-size: 0.75rem; color: var(--text-secondary);">{{ severity_score }}/100</span>
{% endif %}
</div>
{% endif %}
</div>
<!-- Summary stats -->
<div class="stat-row" style="margin-top: 20px;">
<div class="stat-pill">
<div class="stat-value">{{ permits|length }}</div>
<div class="stat-label">permits found</div>
</div>
<div class="stat-pill">
<div class="stat-value">{{ complaints|length }}</div>
<div class="stat-label">complaints</div>
</div>
<div class="stat-pill">
<div class="stat-value">{{ violations|length }}</div>
<div class="stat-label">violations</div>
</div>
<div class="stat-pill">
<div class="stat-value">{{ entities|length }}</div>
<div class="stat-label">entities</div>
</div>
</div>
<div class="demo-grid">
<!-- ── Permit History ── -->
<div class="card full-width">
<span class="callout">Queried from 1.1M building permits via SODA API (dataset i98e-djp9)</span>
<div class="card-title">Permit History</div>
{% if permits %}
<table class="data-table">
<thead>
<tr>
<th>Permit #</th>
<th>Type</th>
<th>Status</th>
<th>Severity</th>
<th>Filed</th>
<th>Cost</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for p in permits[:15] %}
<tr>
<td class="highlight">{{ p.permit_number }}</td>
<td>{{ p.permit_type or 'N/A' }}</td>
<td>
<span class="badge {% if p.status == 'issued' %}badge-issued{% elif p.status == 'filed' %}badge-filed{% elif p.status == 'complete' %}badge-complete{% elif p.status == 'expired' %}badge-expired{% endif %}">
{{ p.status or 'unknown' }}
</span>
</td>
<td>
{% if p.severity_tier %}
<span class="severity-pill severity-{{ p.severity_tier }}">{{ p.severity_tier }}</span>
{% else %}
<span style="color: var(--text-tertiary); font-size: 0.75rem;">—</span>
{% endif %}
</td>
<td>{{ p.filed_date or 'N/A' }}</td>
<td>{{ p.cost_display or 'N/A' }}</td>
<td>{{ p.description_short }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if permits|length > 15 %}
<p style="color: var(--text-tertiary); font-size: 0.8rem; margin-top: 8px;">
Showing 15 of {{ permits|length }} permits
</p>
{% endif %}
{% else %}
<div class="empty-state">No permit data available</div>
{% endif %}
</div>
<!-- ── Routing Progress ── -->
<div class="card">
<span class="callout">Real routing data from DBI's addenda system (3.9M records)</span>
<div class="card-title">Routing Progress</div>
{% if routing %}
{% for r in routing[:8] %}
<div class="routing-item">
<div class="routing-station">{{ r.station }}</div>
<div class="routing-bar-bg">
<div class="routing-bar-fill {{ r.bar_class }}" style="width: {{ r.pct }}%"></div>
</div>
<div class="routing-status">{{ r.result }}</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">No active routing</div>
{% endif %}
</div>
<!-- ── Timeline Estimate ── -->
<div class="card">
<span class="callout">Station-sum model from station_velocity_v2 (p50 values)</span>
<div class="card-title">Timeline Estimate</div>
{% if timeline %}
<div class="timeline-row">
<div class="timeline-label">p25</div>
<div class="timeline-bar-bg">
<div class="timeline-bar-fill" style="width: {{ timeline.p25_pct }}%; background: var(--signal-green);">{{ timeline.p25 }}d</div>
</div>
</div>
<div class="timeline-row">
<div class="timeline-label">p50</div>
<div class="timeline-bar-bg">
<div class="timeline-bar-fill" style="width: {{ timeline.p50_pct }}%; background: var(--signal-blue);">{{ timeline.p50 }}d</div>
</div>
</div>
<div class="timeline-row">
<div class="timeline-label">p75</div>
<div class="timeline-bar-bg">
<div class="timeline-bar-fill" style="width: {{ timeline.p75_pct }}%; background: var(--signal-amber);">{{ timeline.p75 }}d</div>
</div>
</div>
<div class="timeline-row">
<div class="timeline-label">p90</div>
<div class="timeline-bar-bg">
<div class="timeline-bar-fill" style="width: {{ timeline.p90_pct }}%; background: var(--signal-red);">{{ timeline.p90 }}d</div>
</div>
</div>
<p style="color: var(--text-tertiary); font-size: 0.75rem; margin-top: 8px;">
Based on {{ timeline.sample_size }} historical permits — {{ timeline.confidence }} confidence
</p>
{% else %}
<div class="empty-state">Timeline estimate unavailable</div>
{% endif %}
</div>
<!-- ── Entity Network ── -->
<div class="card">
<span class="callout">Entity resolution: 1.8M contacts resolved to 1M entities</span>
<div class="card-title">Connected Entities</div>
{% if entities %}
{% for e in entities[:10] %}
<div class="entity-item">
<div>
<div class="entity-name">{{ e.name }}</div>
<div class="entity-role">{{ e.role }}</div>
</div>
<div class="entity-permits">{{ e.permit_count }} permits</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">No entities found</div>
{% endif %}
</div>
<!-- ── Complaints & Violations ── -->
<div class="card">
<span class="callout">DBI complaint + violation databases cross-referenced</span>
<div class="card-title">Complaints & Violations</div>
{% if violations or complaints %}
{% for v in violations[:3] %}
<div class="alert-item violation">
<div class="alert-label">Violation</div>
<div class="alert-text">{{ v.description_short }} ({{ v.status }})</div>
</div>
{% endfor %}
{% for c in complaints[:3] %}
<div class="alert-item complaint">
<div class="alert-label">Complaint</div>
<div class="alert-text">{{ c.description_short }} ({{ c.status }})</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">No complaints or violations on record</div>
{% endif %}
</div>
</div>
<!-- ── Architecture Showcase ── -->
<div style="margin-top: 32px;">
<span class="callout">MCP Architecture — 30 tools accessible via Claude integration</span>
<div class="arch-grid">
<div class="arch-card">
<div class="arch-number">30</div>
<div class="arch-label">MCP Tools</div>
<div class="arch-detail">SODA API, entity network, AI vision, permit prediction, addenda routing</div>
</div>
<div class="arch-card">
<div class="arch-number">1M</div>
<div class="arch-label">Resolved Entities</div>
<div class="arch-detail">5-step entity resolution cascade from 1.8M raw contacts</div>
</div>
<div class="arch-card">
<div class="arch-number">576K</div>
<div class="arch-label">Relationship Edges</div>
<div class="arch-detail">SQL-first co-occurrence graph for network analysis</div>
</div>
<div class="arch-card">
<div class="arch-number">3.9M</div>
<div class="arch-label">Addenda Records</div>
<div class="arch-detail">Plan review routing with station velocity tracking</div>
</div>
</div>
</div>
<!-- ── CTA ── -->
<div class="cta-section">
<h2>Try it yourself</h2>
<p>Search any San Francisco address, analyze plan sets with AI vision, predict permit timelines, and explore the entity network.</p>
<a href="/auth/login?invite_code=friends-gridcare" class="cta-button">Get Started</a>
</div>
</main>
<footer>
<div class="container">
<p>
sfpermits.ai — Live property intelligence demo
· <a href="/methodology">Methodology</a>
· <a href="/about-data">About the Data</a>
· <a href="/">Home</a>
</p>
<p style="margin-top: 6px;">
All data from City and County of San Francisco open data. Not affiliated with or endorsed by SF DBI.
</p>
</div>
</footer>
<script nonce="{{ csp_nonce }}" src="/static/admin-feedback.js" defer></script>
<script nonce="{{ csp_nonce }}" src="/static/admin-tour.js" defer></script>
</body>
</html>