← back to Beverlyhillsbutler
public/property-profile.html
340 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Property Profile — Beverly Hills Butler</title>
<meta name="description" content="Beverly Hills property dossier — assessor data, valuations, and county records.">
<script>
(function() {
var saved = localStorage.getItem('bhb-theme');
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
:root {
--bg: #f9f7f4;
--ink: #1a1a1a;
--ink-soft: #4a4a4a;
--ink-faint: #9a9a9a;
--line: #e2ddd8;
--card-bg: #ffffff;
--header-bg: rgba(249,247,244,0.94);
--accent: #8b6f4e;
--accent-light: #c4a882;
--accent-dim: #f3ede5;
--green: #2d7a4f;
--green-dim: #e8f5ee;
}
[data-theme="dark"] {
--bg: #111010;
--ink: #f0ede8;
--ink-soft: #b8b2aa;
--ink-faint: #6a6460;
--line: #2a2826;
--card-bg: #1c1a18;
--header-bg: rgba(17,16,16,0.94);
--accent-dim: #2a2318;
--green-dim: #112218;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Georgia', 'Times New Roman', serif;
background: var(--bg);
color: var(--ink);
min-height: 100vh;
}
/* ── Header ── */
header {
position: sticky; top: 0; z-index: 100;
background: var(--header-bg);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--line);
display: flex; align-items: center; justify-content: space-between;
padding: 0 1.5rem; height: 56px;
}
.logo { font-size: 1rem; font-weight: 600; letter-spacing: .04em; color: var(--ink); text-decoration: none; }
.logo em { font-style: normal; color: var(--accent); }
.header-actions { display: flex; align-items: center; gap: .75rem; }
.theme-btn { background: none; border: 1px solid var(--line); border-radius: 6px; padding: .3rem .6rem; cursor: pointer; font-size: .8rem; color: var(--ink-soft); }
.back-btn { background: var(--accent); color: #fff; border: none; border-radius: 6px; padding: .35rem .8rem; cursor: pointer; font-size: .85rem; text-decoration: none; font-family: inherit; }
/* ── Search bar (top of profile) ── */
.search-bar { max-width: 700px; margin: 1.5rem auto 0; padding: 0 1rem; }
.search-bar form { display: flex; gap: .5rem; }
.search-bar input { flex: 1; padding: .6rem 1rem; border: 1.5px solid var(--line); border-radius: 8px; background: var(--card-bg); color: var(--ink); font-family: inherit; font-size: .95rem; }
.search-bar input:focus { outline: none; border-color: var(--accent); }
.search-bar button { padding: .6rem 1.2rem; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-family: inherit; font-size: .95rem; cursor: pointer; }
/* ── Main ── */
main { max-width: 860px; margin: 0 auto; padding: 1.5rem 1rem 3rem; }
/* ── Loading / error ── */
#state-msg { text-align: center; padding: 3rem 1rem; color: var(--ink-soft); font-size: 1rem; }
#state-msg .spinner { display: inline-block; width: 28px; height: 28px; border: 3px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; margin-bottom: 1rem; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Property hero ── */
.prop-hero { background: var(--card-bg); border: 1px solid var(--line); border-radius: 12px; padding: 1.75rem; margin-bottom: 1.25rem; }
.prop-address { font-size: 1.55rem; font-weight: 700; line-height: 1.2; margin-bottom: .35rem; }
.prop-sub { font-size: .95rem; color: var(--ink-soft); margin-bottom: 1rem; }
.prop-badges { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: 1.25rem; }
.badge { display: inline-block; padding: .25rem .65rem; border-radius: 20px; font-size: .78rem; font-weight: 600; letter-spacing: .03em; }
.badge-use { background: var(--accent-dim); color: var(--accent); }
.badge-zip { background: var(--green-dim); color: var(--green); }
.prop-value-row { display: flex; gap: 1.5rem; flex-wrap: wrap; }
.val-block { flex: 1; min-width: 140px; }
.val-label { font-size: .75rem; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint); margin-bottom: .2rem; }
.val-num { font-size: 1.45rem; font-weight: 700; color: var(--ink); }
.val-sub { font-size: .78rem; color: var(--ink-soft); margin-top: .1rem; }
/* ── Dossier grid ── */
.dossier-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 1.25rem; }
@media (max-width: 560px) { .dossier-grid { grid-template-columns: 1fr; } }
.dossier-card { background: var(--card-bg); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; }
.dossier-card h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .1em; color: var(--ink-faint); margin-bottom: 1rem; }
.fact-row { display: flex; justify-content: space-between; align-items: baseline; padding: .45rem 0; border-bottom: 1px solid var(--line); font-size: .9rem; }
.fact-row:last-child { border-bottom: none; }
.fact-label { color: var(--ink-soft); }
.fact-value { font-weight: 600; text-align: right; }
.fact-value.accent { color: var(--accent); }
/* ── Valuation breakdown ── */
.val-breakdown { background: var(--card-bg); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; margin-bottom: 1.25rem; }
.val-breakdown h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .1em; color: var(--ink-faint); margin-bottom: 1rem; }
.val-bar-row { margin-bottom: .75rem; }
.val-bar-label { display: flex; justify-content: space-between; font-size: .88rem; margin-bottom: .3rem; }
.val-bar-label span { font-weight: 600; }
.val-bar-track { height: 8px; background: var(--line); border-radius: 4px; overflow: hidden; }
.val-bar-fill { height: 100%; border-radius: 4px; }
.fill-land { background: #8b6f4e; }
.fill-imp { background: #2d7a4f; }
/* ── County links ── */
.county-links { background: var(--card-bg); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; margin-bottom: 1.25rem; }
.county-links h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .1em; color: var(--ink-faint); margin-bottom: 1rem; }
.link-row { display: flex; flex-direction: column; gap: .5rem; }
.county-link { display: flex; align-items: center; gap: .6rem; padding: .7rem 1rem; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; text-decoration: none; color: var(--ink); font-size: .88rem; transition: border-color .15s; }
.county-link:hover { border-color: var(--accent); color: var(--accent); }
.county-link .link-icon { font-size: 1.1rem; }
.county-link .link-desc { font-size: .75rem; color: var(--ink-faint); margin-top: .1rem; }
/* ── Map placeholder ── */
.map-section { background: var(--card-bg); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; margin-bottom: 1.25rem; }
.map-section iframe { width: 100%; height: 260px; border: none; display: block; }
.map-fallback { height: 140px; display: flex; align-items: center; justify-content: center; color: var(--ink-faint); font-size: .9rem; gap: .5rem; }
/* ── APN chip ── */
.apn-chip { font-family: 'Courier New', monospace; font-size: .8rem; background: var(--line); padding: .2rem .55rem; border-radius: 4px; letter-spacing: .06em; }
footer { text-align: center; padding: 2rem 1rem; font-size: .78rem; color: var(--ink-faint); border-top: 1px solid var(--line); margin-top: 1rem; }
footer a { color: var(--ink-faint); }
</style>
</head>
<body>
<header>
<a class="logo" href="/">Beverly Hills <em>Butler</em></a>
<div class="header-actions">
<a class="back-btn" href="/property-search.html">Search Properties</a>
<button class="theme-btn" onclick="toggleTheme()">☼</button>
</div>
</header>
<div class="search-bar">
<form onsubmit="searchAddress(event)">
<input type="text" id="quick-search" placeholder="Search another address or APN…" autocomplete="off">
<button type="submit">Go</button>
</form>
</div>
<main>
<div id="state-msg">
<div class="spinner"></div>
<p>Loading property data…</p>
</div>
<div id="profile" style="display:none"></div>
</main>
<footer>
<p>Data: LA County Assessor Roll 2025 · Public open data · <a href="https://assessor.lacounty.gov" target="_blank" rel="noopener">assessor.lacounty.gov</a></p>
</footer>
<script>
function toggleTheme() {
var c = document.documentElement.getAttribute('data-theme');
var n = c === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', n);
localStorage.setItem('bhb-theme', n);
}
function fmtNum(n) { return n ? n.toLocaleString() : '—'; }
function fmtUSD(n) {
if (!n) return '—';
return '$' + Math.round(n).toLocaleString();
}
function fmtDate(s) {
if (!s) return '—';
try { return new Date(s + 'T00:00:00').toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }
catch (_) { return s; }
}
function searchAddress(e) {
e.preventDefault();
var q = document.getElementById('quick-search').value.trim();
if (!q) return;
window.location.href = '/property-search.html?q=' + encodeURIComponent(q);
}
function renderProfile(p) {
var totalVal = p.total_value || 0;
var landPct = totalVal ? Math.round((p.land_value || 0) / totalVal * 100) : 0;
var impPct = totalVal ? Math.round((p.improvement_value || 0) / totalVal * 100) : 0;
// Google Maps embed (no API key needed for embed link)
var mapUrl = (p.lat && p.lon)
? 'https://maps.google.com/maps?q=' + p.lat + ',' + p.lon + '&z=17&output=embed'
: null;
var html = `
<div class="prop-hero">
<div class="prop-address">${p.address || p.property_location || 'Address Unknown'}</div>
<div class="prop-sub">${p.city || 'Beverly Hills'}, CA ${p.zip} · <span class="apn-chip">APN ${p.apn}</span></div>
<div class="prop-badges">
<span class="badge badge-use">${p.use_desc || 'Unknown Use'}</span>
${p.use_desc2 ? '<span class="badge badge-use">' + p.use_desc2 + '</span>' : ''}
<span class="badge badge-zip">ZIP ${p.zip}</span>
${p.parcel_classification ? '<span class="badge badge-use">' + p.parcel_classification + '</span>' : ''}
</div>
<div class="prop-value-row">
<div class="val-block">
<div class="val-label">Total Assessed Value</div>
<div class="val-num">${p.total_value_fmt || fmtUSD(p.total_value)}</div>
<div class="val-sub">Roll Year ${p.roll_year || '2025'}</div>
</div>
<div class="val-block">
<div class="val-label">Land Value</div>
<div class="val-num">${p.land_value_fmt || fmtUSD(p.land_value)}</div>
<div class="val-sub">${landPct}% of total</div>
</div>
<div class="val-block">
<div class="val-label">Improvement Value</div>
<div class="val-num">${p.improvement_value_fmt || fmtUSD(p.improvement_value)}</div>
<div class="val-sub">${impPct}% of total</div>
</div>
</div>
</div>
<div class="dossier-grid">
<div class="dossier-card">
<h3>Property Details</h3>
<div class="fact-row"><span class="fact-label">Year Built</span><span class="fact-value">${p.year_built || '—'}</span></div>
<div class="fact-row"><span class="fact-label">Effective Year</span><span class="fact-value">${p.effective_year_built || '—'}</span></div>
<div class="fact-row"><span class="fact-label">Building SqFt</span><span class="fact-value">${p.sqft ? fmtNum(p.sqft) + ' sf' : '—'}</span></div>
<div class="fact-row"><span class="fact-label">Bedrooms</span><span class="fact-value">${p.bedrooms || '—'}</span></div>
<div class="fact-row"><span class="fact-label">Bathrooms</span><span class="fact-value">${p.bathrooms || '—'}</span></div>
<div class="fact-row"><span class="fact-label">Units</span><span class="fact-value">${p.units || '—'}</span></div>
</div>
<div class="dossier-card">
<h3>Sale & Tax Info</h3>
<div class="fact-row"><span class="fact-label">Last Recording Date</span><span class="fact-value">${fmtDate(p.last_sale_date)}</span></div>
<div class="fact-row"><span class="fact-label">Net Taxable Value</span><span class="fact-value accent">${fmtUSD(p.net_taxable_value)}</span></div>
<div class="fact-row"><span class="fact-label">Homeowner Exemption</span><span class="fact-value">${p.homeowner_exemption ? fmtUSD(p.homeowner_exemption) : '—'}</span></div>
<div class="fact-row"><span class="fact-label">Taxable Parcel</span><span class="fact-value">${p.is_taxable || '—'}</span></div>
<div class="fact-row"><span class="fact-label">Admin Region</span><span class="fact-value">${p.admin_region || '—'}</span></div>
<div class="fact-row"><span class="fact-label">AIN</span><span class="fact-value"><span class="apn-chip">${p.ain}</span></span></div>
</div>
</div>
<div class="val-breakdown">
<h3>Valuation Breakdown</h3>
<div class="val-bar-row">
<div class="val-bar-label"><span>Land Value</span><span>${p.land_value_fmt || fmtUSD(p.land_value)} (${landPct}%)</span></div>
<div class="val-bar-track"><div class="val-bar-fill fill-land" style="width:${landPct}%"></div></div>
</div>
<div class="val-bar-row">
<div class="val-bar-label"><span>Improvement Value</span><span>${p.improvement_value_fmt || fmtUSD(p.improvement_value)} (${impPct}%)</span></div>
<div class="val-bar-track"><div class="val-bar-fill fill-imp" style="width:${impPct}%"></div></div>
</div>
</div>
${mapUrl ? `
<div class="map-section">
<iframe src="${mapUrl}" allowfullscreen loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>` : ''}
<div class="county-links">
<h3>Official County Records</h3>
<div class="link-row">
<a class="county-link" href="${p.assessor_url}" target="_blank" rel="noopener noreferrer">
<span class="link-icon">🏠</span>
<div>
<div>LA County Assessor — Property Search</div>
<div class="link-desc">AIN ${p.ain} · Official assessor record, deed history, parcel map</div>
</div>
</a>
<a class="county-link" href="https://tax.lacounty.gov/" target="_blank" rel="noopener noreferrer">
<span class="link-icon">📋</span>
<div>
<div>LA County Property Tax Portal</div>
<div class="link-desc">Tax bills, payment history, supplemental assessments</div>
</div>
</a>
<a class="county-link" href="https://planning.beverlyhills.org/" target="_blank" rel="noopener noreferrer">
<span class="link-icon">📍</span>
<div>
<div>Beverly Hills Planning</div>
<div class="link-desc">Permits, zoning, building records for ${p.address}</div>
</div>
</a>
<a class="county-link" href="https://www.beverlyhills.org/cobhweb/bin/cobhweb.dll/main?department=1271" target="_blank" rel="noopener noreferrer">
<span class="link-icon">📄</span>
<div>
<div>Beverly Hills Building & Safety</div>
<div class="link-desc">Building permits and inspection records</div>
</div>
</a>
</div>
</div>
`;
document.getElementById('state-msg').style.display = 'none';
var el = document.getElementById('profile');
el.innerHTML = html;
el.style.display = '';
document.title = (p.address || 'Property') + ' — Beverly Hills Butler';
}
function showError(msg) {
document.getElementById('state-msg').innerHTML = '<p style="color:#c0392b">' + msg + '</p><p style="margin-top:.5rem; font-size:.85rem"><a href="/property-search.html">← Back to search</a></p>';
}
async function loadProperty() {
var params = new URLSearchParams(window.location.search);
var apn = params.get('apn') || params.get('ain');
if (!apn) {
showError('No property APN specified. Please search for a property first.');
return;
}
try {
var resp = await fetch('/api/property/' + encodeURIComponent(apn));
if (resp.status === 404) { showError('Property APN ' + apn + ' not found in the Beverly Hills assessor database.'); return; }
if (!resp.ok) throw new Error('Server error ' + resp.status);
var prop = await resp.json();
renderProfile(prop);
} catch (e) {
showError('Failed to load property: ' + e.message);
}
}
loadProperty();
</script>
</body>
</html>