<!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">
{% include "fragments/head_obsidian.html" %}
<style nonce="{{ csp_nonce }}">
body {
font-family: var(--sans);
background: var(--obsidian);
color: var(--text-primary);
line-height: 1.7;
min-height: 100vh;
margin: 0;
}
/* Demo badge — chip pattern */
.demo-badge {
font-family: var(--mono);
font-size: var(--text-xs);
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;
display: inline-block;
margin-left: var(--space-3);
vertical-align: middle;
}
.hero {
padding: var(--space-8) 0 var(--space-6);
border-bottom: 1px solid var(--glass-border);
}
.hero h1 {
font-family: var(--sans);
font-size: var(--text-lg);
font-weight: 300;
color: var(--text-secondary);
margin: 0 0 var(--space-1) 0;
}
.hero .address {
font-family: var(--mono);
font-size: var(--text-2xl);
font-weight: 300;
color: var(--text-primary);
}
.hero .subtitle {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-tertiary);
margin-top: var(--space-1);
}
/* Annotation callouts — chip/insight pattern */
.callout {
font-family: var(--mono);
font-size: var(--text-xs);
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: var(--space-2);
letter-spacing: 0.02em;
}
/* Grid layout */
.demo-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-5);
margin-top: var(--space-6);
}
.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);
padding: var(--space-5);
transition: border-color 0.3s;
}
.card:hover { border-color: var(--glass-hover); }
.card-title {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 400;
color: var(--accent);
margin: 0 0 var(--space-3) 0;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.stat-row { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-4); }
.stat-pill {
background: var(--obsidian-light);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: 10px var(--space-4);
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: var(--text-xl);
font-weight: 300;
color: var(--text-primary);
}
.stat-pill .stat-label {
font-family: var(--sans);
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-top: var(--space-1);
}
/* Permit table — obs-table pattern */
.data-table {
width: 100%;
border-collapse: collapse;
font-family: var(--sans);
font-size: var(--text-sm);
}
.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: var(--space-2) 10px;
border-bottom: 1px solid var(--glass-border);
}
.data-table td {
padding: var(--space-2) 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: var(--text-xs);
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: var(--text-xs);
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: var(--space-2);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-sm);
margin-top: var(--space-2);
}
.severity-banner .sev-label {
font-family: var(--mono);
font-size: var(--text-xs);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Progress bars for routing */
.routing-item {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: 10px;
}
.routing-station {
font-family: var(--mono);
font-size: var(--text-sm);
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: var(--text-xs);
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: var(--space-2) 0;
border-bottom: 1px solid var(--glass-border);
}
.entity-item:last-child { border-bottom: none; }
.entity-name {
font-family: var(--sans);
font-size: var(--text-base);
color: var(--text-primary);
}
.entity-role {
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 300;
color: var(--text-tertiary);
}
.entity-permits {
font-family: var(--mono);
font-size: var(--text-sm);
font-weight: 300;
color: var(--accent);
}
/* Violation/complaint items — insight pattern */
.alert-item {
padding: var(--space-2) var(--space-3);
margin-bottom: var(--space-2);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
font-size: var(--text-sm);
}
.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: var(--text-xs);
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: var(--space-1); }
/* Timeline estimate */
.timeline-row {
display: flex;
gap: var(--space-4);
align-items: center;
margin-bottom: var(--space-2);
}
.timeline-label {
font-family: var(--mono);
font-size: var(--text-sm);
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: var(--space-2);
font-family: var(--mono);
font-size: var(--text-xs);
font-weight: 300;
color: rgba(255,255,255, 0.9);
}
.empty-state {
text-align: center;
padding: var(--space-8);
font-family: var(--sans);
color: var(--text-tertiary);
font-size: var(--text-base);
}
/* Architecture / capability showcase */
.arch-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-4);
margin-top: var(--space-6);
}
.arch-card {
background: var(--obsidian-mid);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
padding: var(--space-5);
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: var(--text-2xl);
font-weight: 300;
color: var(--accent);
}
.arch-card .arch-label {
font-family: var(--sans);
font-size: var(--text-sm);
color: var(--text-secondary);
margin-top: var(--space-1);
}
.arch-card .arch-detail {
font-family: var(--sans);
font-size: var(--text-xs);
color: var(--text-tertiary);
margin-top: var(--space-2);
}
/* CTA section — ghost-cta pattern */
.cta-section {
text-align: center;
padding: var(--space-10) var(--space-6);
margin-top: var(--space-8);
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: var(--text-xl);
font-weight: 300;
color: var(--text-primary);
margin: 0 0 var(--space-2) 0;
}
.cta-section p {
font-family: var(--sans);
color: var(--text-secondary);
font-size: var(--text-base);
margin: 0 0 var(--space-5) 0;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
/* Demo footer */
.demo-footer {
border-top: 1px solid var(--glass-border);
padding: var(--space-6) 0;
margin-top: var(--space-8);
}
.demo-footer p {
font-family: var(--sans);
color: var(--text-tertiary);
font-size: var(--text-sm);
text-align: center;
margin: 0 0 var(--space-2) 0;
}
.demo-footer a { color: var(--accent); text-decoration: none; }
.demo-footer a:hover { text-decoration: underline; }
@media (max-width: 768px) {
.arch-grid { grid-template-columns: repeat(2, 1fr); }
.demo-grid { grid-template-columns: 1fr; }
.hero .address { font-size: var(--text-xl); }
}
@media (max-width: 480px) {
.obs-container { padding: 0 var(--space-4); }
.callout { display: block; max-width: 100%; box-sizing: border-box; }
.arch-grid { grid-template-columns: 1fr; }
.stat-row { gap: var(--space-2); }
.stat-pill { min-width: 70px; }
}
/* Density override */
{% if density_max %}
.card { padding: 14px; }
.demo-grid { gap: var(--space-3); }
.data-table { font-size: var(--text-xs); }
.data-table th, .data-table td { padding: var(--space-2) var(--space-2); }
{% endif %}
</style>
</head>
<body>
{% include "fragments/nav.html" %}
<main>
<div class="obs-container">
<div class="hero">
<h1>Property Intelligence <span class="demo-badge">Live Demo</span></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: var(--text-sm); color: var(--text-secondary);">{{ severity_score }}/100</span>
{% endif %}
</div>
{% endif %}
</div>
<!-- Summary stats -->
<div class="stat-row" style="margin-top: var(--space-5);">
<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">Live SF building permit data — 1.1M permits searchable by address, permit number, or contractor</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: var(--text-xs);">—</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: var(--text-sm); margin-top: var(--space-2);">
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">Timeline estimate based on historical station dwell times — p25, p50, p75 ranges</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: var(--text-xs); margin-top: var(--space-2);">
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: var(--space-8);">
<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>Ready to search your property?</h2>
<p>Search any SF address to see permit history, routing status, and timeline predictions. No setup required.</p>
<a href="/auth/login" class="ghost-cta">Get Started →</a>
</div>
<footer class="demo-footer">
<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>
All data from City and County of San Francisco open data. Not affiliated with or endorsed by SF DBI.
</p>
</footer>
</div>
</main>
{% include 'fragments/feedback_widget.html' %}
<script nonce="{{ csp_nonce }}" src="/static/admin-feedback.js" defer></script>
<script nonce="{{ csp_nonce }}" src="/static/admin-tour.js" defer></script>
</body>
</html>