{% include '@Application/inc/central_header.html.twig' %}
{% include '@HoneybeeWeb/inc/_web_design_system.html.twig' %}
<style>
.re-wrap { max-width: 1200px; margin: 0 auto; padding: 0 28px; }
.re-hero { background: var(--n-cream); padding: 120px 0 40px; }
.re-grid { display: grid; grid-template-columns: 1.15fr .85fr; gap: 24px; align-items: start; }
@media (max-width: 940px) { .re-grid { grid-template-columns: 1fr; } }
.re-card { background: var(--n-white); border: 1px solid var(--n-border-md); border-radius: var(--n-radius); padding: 22px; box-shadow: var(--n-shadow-sm); }
.re-map { width: 100%; height: 420px; border-radius: var(--n-radius-sm); border: 1px solid var(--n-border-md); background: var(--n-cream-2); }
.re-search { width: 100%; padding: 12px 14px; border: 1.5px solid var(--n-border-md); border-radius: var(--n-radius-sm); font-size: 14px; font-family: var(--n-font); margin-bottom: 12px; }
.re-field { margin-bottom: 14px; }
.re-field label { display: block; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--n-muted); margin-bottom: 6px; }
.re-field input, .re-field select { width: 100%; padding: 11px 12px; border: 1.5px solid var(--n-border-md); border-radius: var(--n-radius-sm); font-size: 14px; font-family: var(--n-font); }
.re-toggle { display: flex; gap: 0; border: 1.5px solid var(--n-border-md); border-radius: var(--n-radius-sm); overflow: hidden; }
.re-toggle button { flex: 1; padding: 11px; background: var(--n-white); border: none; font-size: 13px; font-weight: 600; cursor: pointer; color: var(--n-muted); font-family: var(--n-font); }
.re-toggle button.active { background: var(--n-dark); color: #fff; }
.re-area-pill { display: inline-flex; align-items: center; gap: 8px; background: var(--n-amber-dim); color: var(--n-amber); font-weight: 700; font-size: 13px; padding: 7px 14px; border-radius: 100px; margin-bottom: 12px; }
.re-kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 4px 0 18px; }
.re-kpi { background: var(--n-cream); border: 1px solid var(--n-border); border-radius: var(--n-radius-sm); padding: 14px 16px; }
.re-kpi .v { font-family: 'Montserrat', sans-serif; font-weight: 900; font-size: 1.45rem; color: var(--n-dark); line-height: 1.1; }
.re-kpi .l { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--n-muted); margin-top: 4px; }
.re-kpi.hl { background: var(--n-dark); } .re-kpi.hl .v { color: #fff; } .re-kpi.hl .l { color: rgba(255,255,255,.55); }
.re-boq { width: 100%; border-collapse: collapse; font-size: 13px; margin-top: 8px; }
.re-boq th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--n-muted); padding: 8px 10px; border-bottom: 1.5px solid var(--n-border-md); }
.re-boq td { padding: 9px 10px; border-bottom: 1px solid var(--n-border); color: var(--n-dark); }
.re-boq td.num, .re-boq th.num { text-align: right; font-variant-numeric: tabular-nums; }
.re-boq tfoot td { font-weight: 800; border-top: 1.5px solid var(--n-border-md); border-bottom: none; }
.re-note { font-size: 12px; color: var(--n-muted); line-height: 1.6; margin-top: 14px; padding: 12px 14px; background: var(--n-cream); border-radius: var(--n-radius-sm); border-left: 3px solid var(--n-amber); }
.re-muted { color: var(--n-muted); font-size: 13px; }
#re-results { display: none; }
.re-spinner { display:none; font-size:13px; color:var(--n-amber); font-weight:600; }
</style>
<section class="re-hero">
<div class="re-wrap">
<div class="n-label">Instant Estimate · C&I Rooftop Solar</div>
<h1 style="font-family:'Montserrat',sans-serif;font-size:clamp(1.9rem,4vw,2.8rem);font-weight:900;color:var(--n-dark);letter-spacing:-.025em;line-height:1.08;margin:0 0 14px;max-width:18ch">
Your address → an <em style="font-style:italic;color:var(--n-amber)">instant rooftop solar design.</em>
</h1>
<p class="n-body" style="max-width:62ch;margin:0">Enter a site address and HoneyBee auto-detects the roof, sizes the array, and returns an indicative system size, bill of quantities and payback — using location-specific yield data. No match? Trace the roof on the map. Confirmed after a HoneyCore site assessment.</p>
</div>
</section>
<section style="background:var(--n-cream);padding:8px 0 100px">
<div class="re-wrap">
<div class="re-grid">
{# ── LEFT: map + roof capture ── #}
<div class="re-card">
<label style="display:block;font-size:12px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--n-muted);margin-bottom:6px">Enter a site address</label>
<div style="display:flex;gap:8px;margin-bottom:6px">
<input id="re-search" class="re-search" style="margin-bottom:0;flex:1" type="text" placeholder="🔍 e.g. Alexanderplatz, Berlin…">
<button id="re-auto" class="n-btn n-btn-amber" style="white-space:nowrap"><i class="fa-solid fa-bolt"></i> Auto design</button>
</div>
<p id="re-auto-status" class="re-muted" style="margin:0 0 10px;display:none"></p>
<div id="re-map" class="re-map"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;flex-wrap:wrap;gap:10px">
<span id="re-area-pill" class="re-area-pill" style="display:none"><i class="fa-solid fa-draw-polygon"></i> <span id="re-area-val">0</span> m² roof drawn</span>
<div style="display:flex;gap:8px">
<button id="re-redraw" class="n-btn n-btn-outline" style="padding:8px 16px;font-size:13px;display:none"><i class="fa-solid fa-rotate-left"></i> Redraw</button>
</div>
</div>
<p class="re-muted" style="margin-top:10px"><i class="fa-solid fa-circle-info" style="color:var(--n-amber)"></i> Tip: zoom to your roof, click each corner to trace the outline, then close the shape. No map? <a href="#" id="re-manual-link" style="color:var(--n-amber);font-weight:600">Enter area manually</a>.</p>
<div id="re-manual" class="re-field" style="display:none;margin-top:10px">
<label>Roof area (m²)</label>
<input id="re-manual-area" type="number" min="10" step="10" placeholder="e.g. 1200">
</div>
</div>
{# ── RIGHT: controls + results ── #}
<div>
<div class="re-card">
<div class="re-field">
<label>Sizing mode</label>
<div class="re-toggle">
<button type="button" id="re-mode-roof" class="active">Fit the roof</button>
<button type="button" id="re-mode-load">Match my usage</button>
</div>
</div>
<div class="re-field" id="re-load-field" style="display:none">
<label>Monthly electricity use (kWh)</label>
<input id="re-monthly" type="number" min="0" step="100" placeholder="e.g. 25000">
</div>
<div class="re-field">
<label>Roof type</label>
<select id="re-rooftype">
<option value="10">Flat roof (C&I) — 10° tilt</option>
<option value="30">Pitched roof — 30° tilt</option>
</select>
</div>
<div class="re-field">
<label>Electricity tariff (€/kWh)</label>
<input id="re-tariff" type="number" min="0.01" step="0.01" value="0.22">
</div>
<button id="re-go" class="n-btn n-btn-amber" style="width:100%;justify-content:center">Generate estimate <i class="fa-solid fa-bolt"></i></button>
<span id="re-spin" class="re-spinner" style="display:block;margin-top:10px;text-align:center">Calculating yield & BoQ…</span>
<p id="re-err" class="re-muted" style="color:#b4452f;margin-top:10px;display:none"></p>
</div>
<div id="re-results" class="re-card" style="margin-top:16px">
<div class="re-kpis">
<div class="re-kpi hl"><div class="v" id="k-kwp">—</div><div class="l">System size (kWp)</div></div>
<div class="re-kpi"><div class="v" id="k-gen">—</div><div class="l">Annual yield (kWh)</div></div>
<div class="re-kpi"><div class="v" id="k-capex">—</div><div class="l">Indicative CAPEX</div></div>
<div class="re-kpi"><div class="v" id="k-payback">—</div><div class="l">Simple payback</div></div>
</div>
<div style="font-size:12px;color:var(--n-muted);margin-bottom:6px"><span id="k-panels">—</span> modules · <span id="k-savings">—</span>/yr saved · <span id="k-co2">—</span> t CO₂/yr avoided · yield source: <span id="k-src">—</span></div>
<table class="re-boq">
<thead><tr><th>Item</th><th class="num">Qty</th><th>Unit</th><th class="num">Unit €</th><th class="num">Total €</th></tr></thead>
<tbody id="re-boq-body"></tbody>
<tfoot><tr><td colspan="4">Indicative CAPEX (ex-VAT)</td><td class="num" id="re-boq-total">—</td></tr></tfoot>
</table>
<div class="re-note" id="re-disclaimer"></div>
<a href="{{ url('honeybee_contact') }}" class="n-btn n-btn-amber" style="width:100%;justify-content:center;margin-top:16px">Book a HoneyCore Site Assessment <i class="fa-solid fa-arrow-right"></i></a>
<p class="re-muted" style="text-align:center;margin-top:8px">From €499/site, credited against deployment.</p>
</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var state = { area: 0, lat: 0, lng: 0, mode: 'roof', map: null, poly: null, drawer: null };
// ── Controls ──
var $ = function (id) { return document.getElementById(id); };
function setMode(m) {
state.mode = m;
$('re-mode-roof').classList.toggle('active', m === 'roof');
$('re-mode-load').classList.toggle('active', m === 'load');
$('re-load-field').style.display = (m === 'load') ? 'block' : 'none';
}
$('re-mode-roof').onclick = function () { setMode('roof'); };
$('re-mode-load').onclick = function () { setMode('load'); };
$('re-manual-link').onclick = function (e) { e.preventDefault(); $('re-manual').style.display = 'block'; };
$('re-manual-area').oninput = function () { state.area = parseFloat(this.value) || 0; reflectArea(); };
function reflectArea() {
if (state.area > 0) { $('re-area-pill').style.display = 'inline-flex'; $('re-area-val').textContent = Math.round(state.area).toLocaleString(); }
}
// ── Google Maps (drawing + places). Defined globally for the API callback. ──
window.initRooftop = function () {
try {
var map = new google.maps.Map($('re-map'), { center: { lat: 48.40, lng: 9.99 }, zoom: 18, mapTypeId: 'satellite', tilt: 0, streetViewControl: false });
state.map = map;
// Address autocomplete is OPTIONAL — the legacy Places Autocomplete isn't available to
// newer Google Maps projects, so isolate it so its failure never disables the map/draw.
try {
if (google.maps.places && google.maps.places.Autocomplete) {
var ac = new google.maps.places.Autocomplete($('re-search'));
ac.bindTo('bounds', map);
ac.addListener('place_changed', function () {
var p = ac.getPlace();
if (p.geometry) { map.setCenter(p.geometry.location); map.setZoom(20); }
});
}
} catch (acErr) { /* no autocomplete — typing a full address still works for Auto design */ }
// Google removed DrawingManager from the Maps JS API — use a lightweight click-to-trace polygon.
state.points = [];
state.poly = new google.maps.Polygon({ map: map, paths: [], fillColor: '#C07D2A', fillOpacity: .25, strokeColor: '#C07D2A', strokeWeight: 2, clickable: false });
var recompute = function () {
state.poly.setPath(state.points);
if (state.points.length >= 3) {
state.area = google.maps.geometry.spherical.computeArea(state.poly.getPath());
var lat = 0, lng = 0;
state.points.forEach(function (p) { lat += p.lat(); lng += p.lng(); });
state.lat = lat / state.points.length; state.lng = lng / state.points.length;
reflectArea();
}
};
map.addListener('click', function (e) { state.points.push(e.latLng); recompute(); $('re-redraw').style.display = 'inline-flex'; });
state.clearDraw = function () { state.points = []; state.poly.setPath([]); state.area = 0; $('re-area-pill').style.display = 'none'; };
$('re-redraw').onclick = state.clearDraw;
} catch (e) {
$('re-map').innerHTML = '<div style="padding:40px;text-align:center;color:#6B6E7F">Map unavailable — use “Enter area manually”.</div>';
$('re-manual').style.display = 'block';
}
};
window.gm_authFailure = function () {
$('re-map').innerHTML = '<div style="padding:40px;text-align:center;color:#6B6E7F">Map unavailable — use “Enter area manually”.</div>';
$('re-manual').style.display = 'block';
};
// ── Generate estimate ──
$('re-go').onclick = function () {
$('re-err').style.display = 'none';
if (!state.area || state.area < 5) { $('re-err').textContent = 'Draw a roof outline on the map (or enter an area) first.'; $('re-err').style.display = 'block'; return; }
// manual-area mode without a map: approximate location from search text is not available → require map for yield, else use lat 50 fallback
var lat = state.lat || 50, lng = state.lng || 9;
$('re-spin').style.display = 'block';
var body = new URLSearchParams({
lat: lat, lng: lng, area_m2: state.area, mode: state.mode,
monthly_kwh: $('re-monthly').value || 0, tariff: $('re-tariff').value || 0.22, tilt: $('re-rooftype').value || 10
});
fetch('{{ url('honeybee_rooftop_calc') }}', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
.then(function (r) { return r.json(); })
.then(function (d) {
$('re-spin').style.display = 'none';
if (!d.ok) { $('re-err').textContent = d.error || 'Could not calculate.'; $('re-err').style.display = 'block'; return; }
render(d);
})
.catch(function () { $('re-spin').style.display = 'none'; $('re-err').textContent = 'Network error — please try again.'; $('re-err').style.display = 'block'; });
};
// ── Auto design from address (geocode → Solar API / OSM → PVGIS) ──
$('re-auto').onclick = function () {
var addr = ($('re-search').value || '').trim();
$('re-err').style.display = 'none'; $('re-auto-status').style.display = 'none';
if (!addr) { $('re-search').focus(); return; }
$('re-auto').disabled = true; $('re-auto').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Designing…';
var body = new URLSearchParams({ address: addr, mode: state.mode, monthly_kwh: $('re-monthly').value || 0, tariff: $('re-tariff').value || 0.22, tilt: $('re-rooftype').value || 10 });
fetch('{{ url('honeybee_rooftop_auto') }}', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
.then(function (r) { return r.json(); })
.then(function (d) {
$('re-auto').disabled = false; $('re-auto').innerHTML = '<i class="fa-solid fa-bolt"></i> Auto design';
if (d.ok) { render(d); return; }
if (d.needs_manual && state.map && d.lat) {
state.map.setCenter({ lat: d.lat, lng: d.lng }); state.map.setZoom(20);
if (state.clearDraw) state.clearDraw();
var s = $('re-auto-status'); s.style.display = 'block';
s.innerHTML = '<i class="fa-solid fa-circle-info" style="color:var(--n-amber)"></i> ' + (d.error || 'Trace the roof on the map below.') + (d.formatted_address ? ' <b>' + d.formatted_address + '</b>' : '');
} else {
$('re-err').textContent = d.error || 'Could not auto-detect — draw the roof on the map.'; $('re-err').style.display = 'block';
}
})
.catch(function () { $('re-auto').disabled = false; $('re-auto').innerHTML = '<i class="fa-solid fa-bolt"></i> Auto design'; $('re-err').textContent = 'Network error — please try again.'; $('re-err').style.display = 'block'; });
};
function eur(n) { return '€' + Math.round(n).toLocaleString('de-DE'); }
function render(d) {
$('k-kwp').textContent = d.kwp.toLocaleString();
$('k-gen').textContent = Math.round(d.annual_gen_kwh).toLocaleString();
$('k-capex').textContent = eur(d.capex_eur);
$('k-payback').textContent = d.payback_years ? d.payback_years + ' yrs' : '—';
$('k-panels').textContent = d.panels.toLocaleString();
$('k-savings').textContent = eur(d.annual_savings);
$('k-co2').textContent = d.co2_tonnes_yr;
$('k-src').textContent = (d.yield_source === 'PVGIS' ? 'PVGIS (live)' : 'climate estimate') + (d.roof_source ? ' · roof via ' + d.roof_source : '');
var tb = $('re-boq-body'); tb.innerHTML = '';
d.boq.forEach(function (r) {
tb.innerHTML += '<tr><td>' + r.item + '</td><td class="num">' + r.qty.toLocaleString() + '</td><td>' + r.unit + '</td><td class="num">' + eur(r.unit_price) + '</td><td class="num">' + eur(r.total) + '</td></tr>';
});
$('re-boq-total').textContent = eur(d.capex_eur) + ' (' + eur(d.eur_per_kwp) + '/kWp)';
$('re-disclaimer').textContent = (d.formatted_address ? '📍 ' + d.formatted_address + ' — ' : '') + d.disclaimer;
$('re-results').style.display = 'block';
$('re-results').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
})();
</script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key={{ maps_key|default('AIzaSyBJxyUy8a_U2rSdIUApVDoK_dcvgGkoeDk') }}&libraries=places,geometry&callback=initRooftop&v=weekly"></script>
{% include '@HoneybeeWeb/footer/central_footer.html.twig' %}