{% include '@Application/inc/central_header.html.twig' %}
{% include '@HoneybeeWeb/inc/_web_design_system.html.twig' %}
{% include '@HoneybeeWeb/inc/_hb360_tags.html.twig' %}
{% import '@HoneybeeWeb/inc/_hb360_tags.html.twig' as hb360 %}
<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: 380px; 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-headline { background: var(--n-dark); border-radius: var(--n-radius-sm); padding: 22px 20px; text-align: center; margin-bottom: 14px; }
.re-headline .v { font-family: 'Montserrat', sans-serif; font-weight: 900; font-size: 2.4rem; color: #fff; line-height: 1.05; }
.re-headline .l { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: rgba(255,255,255,.6); margin-top: 6px; }
.re-kpis { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 4px 0 14px; }
.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.35rem; color: var(--n-dark); line-height: 1.1; }
.re-kpi .l { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--n-muted); margin: 4px 0 6px; }
.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; }
.re-advanced-toggle { background:none; border:none; color:var(--n-amber); font-weight:700; font-size:13px; cursor:pointer; padding:0; font-family:var(--n-font); }
#re-advanced { display:none; margin-top:14px; border-top:1px dashed var(--n-border-md); padding-top:14px; }
/* ── the visibly-locked full design (behind the signup wall, H1b) ── */
.re-locked { position: relative; margin-top: 16px; border: 1.5px dashed var(--n-border-md); border-radius: var(--n-radius-sm); overflow: hidden; }
/* Blurred BoQ sits BEHIND the overlay as a background layer; the overlay is in
normal flow so its content always fits — nothing clips. */
.re-locked-blur { position: absolute; inset: 0; filter: blur(4px); pointer-events: none; user-select: none; padding: 16px; opacity: .55; overflow: hidden; }
.re-locked-blur table { width: 100%; border-collapse: collapse; font-size: 13px; }
.re-locked-blur td { padding: 8px 10px; border-bottom: 1px solid var(--n-border); color: var(--n-dark); }
.re-locked-over { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; background: rgba(255,255,255,.6); padding: 24px 18px; text-align: center; }
.re-locked-over .ttl { font-family:'Montserrat',sans-serif; font-weight: 900; font-size: 1.05rem; color: var(--n-dark); }
.re-locked-teaser { font-size: 12.5px; color: var(--n-muted); line-height: 1.9; text-align: left; margin: 0; padding-left: 2px; list-style: none; }
.re-locked-teaser li:before { content: '🔒 '; font-size: 12px; }
/* Tidy stats table under the KPI cards (label · value · tag) */
.re-meta { width: 100%; border-collapse: collapse; font-size: 12.5px; margin: 2px 0 10px; }
.re-meta td { padding: 7px 4px; border-bottom: 1px solid var(--n-border); vertical-align: middle; }
.re-meta tr:last-child td { border-bottom: none; }
.re-meta .ml { color: var(--n-muted); font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; width: 1%; padding-right: 14px; }
.re-meta .mv { color: var(--n-dark); font-weight: 600; }
.re-meta .mt { text-align: right; white-space: nowrap; padding-left: 10px; }
.re-legend { margin: 0 0 14px; }
.re-legend summary { font-size: 12px; font-weight: 600; color: var(--n-muted); cursor: pointer; }
.re-legend .hb360-legend { margin-top: 6px; }
/* ── SDS0 · Solar Design Studio shell (additive — simple flow untouched) ── */
#sds-editor { position: relative; }
#sds-editor.sds-focused { position: fixed; inset: 0; z-index: 100000; background: var(--n-cream); display: flex; flex-direction: column; animation: sdsIn .18s ease; }
@keyframes sdsIn { from { opacity: .55; transform: scale(.992); } to { opacity: 1; transform: none; } }
body.sds-focused-body { overflow: hidden; }
#sds-bar { display: none; }
.sds-focused #sds-bar { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: var(--n-dark); color: #fff; flex-wrap: wrap; }
.sds-focused .re-map { flex: 1; height: auto; border-radius: 0; border: none; }
#sds-bar .sds-title { font-family: 'Montserrat', sans-serif; font-weight: 900; font-size: 14px; margin-right: 4px; white-space: nowrap; }
#sds-bar .sds-sub { font-size: 12px; color: rgba(255,255,255,.55); }
.sds-tbtn { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.18); color: #fff; border-radius: 6px; padding: 7px 12px; font-size: 12.5px; font-weight: 600; cursor: pointer; font-family: var(--n-font); }
.sds-tbtn:hover { background: rgba(255,255,255,.16); }
.sds-tbtn.active { background: var(--n-amber); border-color: var(--n-amber); color: #fff; }
.sds-tbtn:disabled { opacity: .35; cursor: default; }
.sds-spacer { flex: 1; }
#sds-panel { display: none; margin-top: 12px; }
#sds-panel.sds-show { display: block; }
body.sds-focused-body #sds-panel.sds-show { position: fixed; top: 58px; right: 14px; width: 322px; max-height: calc(100vh - 76px); overflow: auto; margin: 0; z-index: 100001; }
.sds-panel-card { background: var(--n-white); border: 1px solid var(--n-border-md); border-radius: var(--n-radius-sm); box-shadow: var(--n-shadow-sm); padding: 12px 14px; }
.sds-zrow { border: 1px solid var(--n-border); border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; cursor: pointer; background: var(--n-white); }
.sds-zrow.sel { border-color: var(--n-amber); box-shadow: 0 0 0 2px var(--n-amber-dim); }
.sds-zrow .t { display: flex; align-items: center; gap: 8px; }
.sds-dot { width: 12px; height: 12px; border-radius: 3px; flex: none; }
.sds-zname { font-weight: 700; font-size: 13px; color: var(--n-dark); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sds-zarea { font-size: 12px; color: var(--n-muted); white-space: nowrap; }
.sds-ibtn { background: none; border: none; color: var(--n-muted); cursor: pointer; padding: 3px 5px; font-size: 13px; }
.sds-ibtn:hover { color: var(--n-dark); }
.sds-pill { display: inline-flex; gap: 5px; align-items: baseline; font-size: 12px; margin-top: 6px; background: var(--n-amber-dim); color: var(--n-amber); font-weight: 700; border-radius: 100px; padding: 3px 10px; }
.sds-pill .q { font-weight: 600; font-size: 11.5px; opacity: .8; }
.sds-props { border-top: 1px dashed var(--n-border-md); margin-top: 8px; padding-top: 8px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; cursor: default; }
.sds-props label { font-size: 11.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--n-muted); display: block; margin-bottom: 3px; }
.sds-props input { width: 100%; padding: 6px 8px; border: 1px solid var(--n-border-md); border-radius: 6px; font-size: 12.5px; font-family: var(--n-font); }
.sds-props .full { grid-column: 1 / -1; }
.sds-azauto { background: none; border: none; color: var(--n-amber); font-weight: 700; font-size: 11.5px; cursor: pointer; padding: 0; text-transform: none; }
.sds-hint { font-size: 12px; color: var(--n-muted); }
.sds-total { border-top: 1px solid var(--n-border-md); margin-top: 6px; padding-top: 8px; font-size: 13px; font-weight: 700; color: var(--n-dark); display: flex; justify-content: space-between; gap: 8px; align-items: baseline; }
.sds-settings { display: flex; gap: 8px; margin-top: 8px; }
.sds-settings input { width: 100%; padding: 5px 7px; border: 1px solid var(--n-border-md); border-radius: 6px; font-size: 12px; font-family: var(--n-font); }
.sds-note { font-size: 11.5px; color: var(--n-muted); margin-top: 6px; line-height: 1.5; }
</style>
<section class="re-hero">
<div class="re-wrap">
<div class="n-label">HoneyBee 360 · Instant Solar Estimate</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:20ch">
Your address + your bill → <em style="font-style:italic;color:var(--n-amber)">what solar would save you.</em>
</h1>
<p class="n-body" style="max-width:62ch;margin:0">Enter a site address and your monthly electricity bill. HoneyBee detects the roof, sizes the array with our PV engine, and shows the indicative system size, annual yield, bill saving and payback — with every figure honestly tagged for what is modelled and what still needs a site survey.</p>
</div>
</section>
<section style="background:var(--n-cream);padding:8px 0 100px">
<div class="re-wrap">
<div class="re-grid">
{# ── LEFT: address + bill + map ── #}
<div class="re-card">
<div class="re-field">
<label>Site address</label>
<input id="re-search" class="re-search" style="margin-bottom:0" type="text" placeholder="🔍 e.g. Alexanderplatz, Berlin…">
</div>
<div class="re-field">
<label>Monthly electricity bill (€)</label>
<input id="re-bill" type="number" min="0" step="10" placeholder="e.g. 1400">
</div>
<button id="re-auto" class="n-btn n-btn-amber" style="width:100%;justify-content:center"><i class="fa-solid fa-bolt"></i> Get my instant estimate</button>
<p id="re-auto-status" class="re-muted" style="margin:10px 0 0;display:none"></p>
<p id="re-err" class="re-muted" style="color:#b4452f;margin-top:10px;display:none"></p>
<span id="re-spin" class="re-spinner" style="display:none;margin-top:10px;text-align:center">Detecting roof & modelling yield…</span>
<div style="margin:16px 0 8px">
{# SDS0: the studio wraps the SAME map element — focused mode is pure CSS
repositioning, so the Maps instance survives and the simple flow is untouched.
#sds-bar / #sds-panel stay empty+hidden unless sds-shell.js attaches. #}
<div id="sds-editor">
<div id="sds-bar"></div>
<div id="re-map" class="re-map"></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px;flex-wrap:wrap;gap:10px">
<span id="re-area-pill" class="re-area-pill" style="display:none;margin-bottom:0"><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">
{# inline SVG — no icon-font dependency in SDS surfaces (SDS-LIB addendum) #}
<button id="sds-expand" class="n-btn n-btn-outline" style="padding:8px 16px;font-size:13px;display:none"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" style="vertical-align:-2px"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></svg> Expand editor</button>
<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>
<button id="re-go" class="n-btn n-btn-outline" style="padding:8px 16px;font-size:13px">Estimate from drawn roof</button>
</div>
</div>
<p class="re-muted" style="margin-top:8px"><i class="fa-solid fa-circle-info" style="color:var(--n-amber)"></i> No auto-match? Zoom to your roof and click each corner to trace the outline, then use “Estimate from drawn roof”.</p>
<div id="sds-panel"></div>
{# ── FUNNEL-3: the login-revealed DETAIL for an owned saved design
(?mydesign=N — own-checked server-side). Figures are the stored
studio pills, said so; the live economics panel above refreshes
as you edit (the same indicative PVGIS-lite path — no tenant
pricing internals exist on this surface, the deny-list law). ── #}
{% if my_design is defined and my_design %}
<div class="sds-panel-card" style="margin-top:12px;border-left:3px solid var(--n-amber)">
<div style="font-weight:700;font-size:13.5px;margin-bottom:4px">
Editing from your account: {{ my_design.title }}</div>
<div style="font-size:12.5px;color:var(--n-muted);margin-bottom:8px">
{% if my_design.address %}{{ my_design.address }} · {% endif %}
{{ my_design.summary.zones }} zone{{ my_design.summary.zones == 1 ? '' : 's' }} ·
{{ my_design.summary.modules|number_format(0, ',', '.') }} modules ·
{{ my_design.summary.kwp|number_format(1, ',', '.') }} kWp
{%- if my_design.summary.components > 0 %} · {{ my_design.summary.components }} component{{ my_design.summary.components == 1 ? '' : 's' }}{% endif %}
<span style="opacity:.8">(as designed — the panel above updates live as you edit)</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px">
<thead><tr>
<th style="text-align:left;color:var(--n-muted);padding:3px 6px 3px 0">Zone</th>
<th style="text-align:left;color:var(--n-muted);padding:3px 6px">Modules</th>
<th style="text-align:left;color:var(--n-muted);padding:3px 6px">kWp</th>
<th style="text-align:left;color:var(--n-muted);padding:3px 6px">Mount</th>
</tr></thead>
<tbody>
{% for z in my_design.payload.zones %}
<tr>
<td style="padding:3px 6px 3px 0;border-top:1px dashed var(--n-border-md)">{{ z.name|default('Zone ' ~ loop.index) }}</td>
<td style="padding:3px 6px;border-top:1px dashed var(--n-border-md)">{{ z.shown.count|default('—') }}</td>
<td style="padding:3px 6px;border-top:1px dashed var(--n-border-md)">{{ z.shown.kwp is defined ? z.shown.kwp|number_format(1, ',', '.') : '—' }}</td>
<td style="padding:3px 6px;border-top:1px dashed var(--n-border-md)">{{ z.mountMode|default('south') }}{{ z.orientation is defined and z.orientation == 'landscape' ? ' · landscape' : '' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="font-size:12px;color:var(--n-muted);margin-top:6px">
Saving updates this design on your account ·
<a href="{{ url('hb360_my_estimate') }}" style="color:var(--n-amber);font-weight:600">back to My designs</a>
</div>
</div>
{% endif %}
{# FUNNEL-1: the save-prompt CTA. Lives OUTSIDE #sds-panel (the shell owns that
element's contents) and entirely in this twig + server files — the shell JS
files are untouched. Shown only once the studio is active (sds-panel visible). #}
<div id="sdsf-card" class="sds-panel-card" style="display:none;margin-top:12px">
<div style="font-weight:700;font-size:13.5px;margin-bottom:4px">Save your design</div>
<div style="font-size:12.5px;color:var(--n-muted);margin-bottom:10px">
Keep this layout — it stays with this browser and attaches to your account
the moment you sign in or create one.
</div>
<button id="sdsf-save" class="n-btn n-btn-amber" type="button" style="padding:7px 14px;font-size:13px">Save design</button>
<div id="sdsf-msg" style="font-size:12.5px;margin-top:8px"></div>
{# FUNNEL-2: Request an offer — revealed after a successful save. The consent
copy NAMES the receiving company (fetched per country) and is ENTWURF-
watermarked until counsel clears the final text. #}
<div id="sdsf-offer" style="display:none;border-top:1px solid var(--n-border-md);margin-top:12px;padding-top:12px">
<div style="font-weight:700;font-size:13.5px;margin-bottom:6px">Request an offer</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<input id="sdsf-name" type="text" placeholder="Your name *" style="padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px">
<input id="sdsf-email" type="email" placeholder="Email *" style="padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px">
<input id="sdsf-phone" type="text" placeholder="Phone (optional)" style="padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px">
<select id="sdsf-country" style="padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px">
<option value="">Country *</option>
<option value="DE">Germany</option><option value="BD">Bangladesh</option>
<option value="SG">Singapore</option><option value="AT">Austria</option>
<option value="CH">Switzerland</option><option value="NL">Netherlands</option>
<option value="BE">Belgium</option><option value="FR">France</option>
<option value="IT">Italy</option><option value="ES">Spain</option>
<option value="PL">Poland</option><option value="GB">United Kingdom</option>
<option value="IN">India</option><option value="US">United States</option>
<option value="__other">Other…</option>
</select>
</div>
<input id="sdsf-country-other" type="text" maxlength="2" placeholder="2-letter country code (e.g. PT)" style="display:none;margin-top:8px;padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px;width:100%">
<textarea id="sdsf-message" placeholder="Anything the partner should know? (optional)" rows="2" style="margin-top:8px;width:100%;padding:8px;border:1.5px solid var(--n-border-md);border-radius:6px;font-size:13px"></textarea>
<label style="display:flex;gap:8px;align-items:flex-start;margin-top:8px;font-size:12px;color:var(--n-muted);cursor:pointer">
<input id="sdsf-consent" type="checkbox" style="margin-top:2px">
<span>
<span style="display:inline-block;background:#fff3cd;color:#8a6d1a;border:1px solid #e8d48a;border-radius:4px;padding:0 6px;font-weight:700;font-size:11.5px;letter-spacing:.5px;margin-right:4px">ENTWURF</span>
I agree that my roof design and my contact details are sent to
<b id="sdsf-target">the responsible solar partner for my country</b>
so they can prepare an offer and contact me. <span style="opacity:.8">(Draft consent text — legal review pending / Rechtstext in Prüfung.)</span>
</span>
</label>
<button id="sdsf-send" class="n-btn n-btn-amber" type="button" style="margin-top:10px;padding:7px 14px;font-size:13px">Request offer</button>
<div id="sdsf-offer-msg" style="font-size:12.5px;margin-top:8px"></div>
</div>
</div>
</div>
<button class="re-advanced-toggle" id="re-advanced-btn" type="button">⚙ Advanced settings ▾</button>
<div id="re-advanced">
<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">
<label>Electricity tariff (€/kWh)</label>
<input id="re-tariff" type="number" min="0.01" step="0.01" value="0.22">
</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>Monthly use (kWh) — overrides the bill if set</label>
<input id="re-monthly" type="number" min="0" step="100" placeholder="optional, e.g. 25000">
</div>
<div class="re-field">
<label>Roof area (m²) — manual, if the map is unavailable</label>
<input id="re-manual-area" type="number" min="10" step="10" placeholder="e.g. 1200">
</div>
</div>
</div>
{# ── RIGHT: the tagged estimate + the locked full design ── #}
<div>
<div id="re-results" class="re-card">
<div class="re-headline">
<div class="v" id="k-headline">—</div>
<div class="l">estimated bill saving / year <span id="k-headline-tag"></span></div>
</div>
<div class="re-kpis">
<div class="re-kpi"><div class="v" id="k-kwp">—</div><div class="l">System size (kWp)</div><span id="t-kwp"></span></div>
<div class="re-kpi"><div class="v" id="k-gen">—</div><div class="l">Annual yield (kWh)</div><span id="t-gen"></span></div>
<div class="re-kpi"><div class="v" id="k-capex">—</div><div class="l">Indicative CAPEX</div><span id="t-capex"></span></div>
<div class="re-kpi"><div class="v" id="k-payback">—</div><div class="l">Simple payback</div><span id="t-payback"></span></div>
</div>
<table class="re-meta">
<tr><td class="ml">Modules</td><td class="mv"><span id="k-panels">—</span> pcs</td><td class="mt"></td></tr>
<tr><td class="ml">Roof area</td><td class="mv"><span id="k-area">—</span> m²</td><td class="mt"><span id="t-roof"></span></td></tr>
<tr><td class="ml">CO₂ avoided</td><td class="mv"><span id="k-co2">—</span> t/yr</td><td class="mt"><span id="t-co2"></span></td></tr>
<tr><td class="ml">Grid export</td><td class="mv"><span id="k-export">—</span> kWh/yr</td><td class="mt"><span id="t-export"></span></td></tr>
<tr><td class="ml">Data sources</td><td class="mv" colspan="2"><span id="k-src">—</span></td></tr>
</table>
<details class="re-legend"><summary>What the tags mean</summary>{{ hb360.legend() }}</details>
{# The visibly-locked full design — the HB360 signup wall (H1b) #}
<div class="re-locked">
<div class="re-locked-blur">
<table>
<tr><td>PV modules</td><td style="text-align:right">◼◼◼ pcs</td><td style="text-align:right">€ ◼◼,◼◼◼</td></tr>
<tr><td>Grid inverters</td><td style="text-align:right">◼ pcs</td><td style="text-align:right">€ ◼,◼◼◼</td></tr>
<tr><td>Mounting structure</td><td style="text-align:right">◼◼◼ set</td><td style="text-align:right">€ ◼,◼◼◼</td></tr>
<tr><td>DC / AC cabling & protection</td><td style="text-align:right">◼◼◼ m</td><td style="text-align:right">€ ◼,◼◼◼</td></tr>
<tr><td>Monthly generation profile</td><td style="text-align:right" colspan="2">▁▂▄▆█▇▆▄▂▁</td></tr>
</table>
</div>
<div class="re-locked-over">
<div class="ttl"><i class="fa-solid fa-lock" style="color:var(--n-amber)"></i> The full design is ready</div>
<ul class="re-locked-teaser" id="re-locked-teaser"></ul>
{# Logged-in applicants skip the wall; guests go to LOGIN (which offers
signup) — the hb360_anon cookie routes them back to their saved
estimate after auth, so no refRoute plumbing is needed. #}
{% set _hbApplicant = app.session.get('userType') == 8 %}
<a href="{{ _hbApplicant ? url('hb360_my_estimate') : url('user_login') }}" class="n-btn n-btn-amber" style="justify-content:center">See the full design & save this <i class="fa-solid fa-arrow-right"></i></a>
<div class="re-muted" style="font-size:12px" id="re-saved-note">Free account · your estimate is kept for you</div>
</div>
</div>
<div class="re-note" id="re-disclaimer"></div>
<a href="{{ url('honeybee_contact') }}" class="n-btn n-btn-outline" style="width:100%;justify-content:center;margin-top:14px">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 class="re-card" id="re-empty" style="text-align:center;color:var(--n-muted);font-size:14px;line-height:1.7">
<i class="fa-solid fa-solar-panel" style="font-size:28px;color:var(--n-amber);display:block;margin-bottom:10px"></i>
Enter an address and your monthly bill on the left —<br>your instant estimate appears here.
</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var state = { area: 0, lat: 0, lng: 0, mode: 'roof', map: null, poly: null, manual: false };
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-mode-roof').onclick = function () { setMode('roof'); };
$('re-mode-load').onclick = function () { setMode('load'); };
$('re-advanced-btn').onclick = function () {
var a = $('re-advanced');
a.style.display = a.style.display === 'block' ? 'none' : 'block';
};
$('re-manual-area').oninput = function () { state.area = parseFloat(this.value) || 0; state.manual = true; 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 (draw-to-trace fallback). 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;
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 */ }
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());
state.manual = false;
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) {
// SDS0: while the studio owns the drawing (focused, or zones exist),
// the simple one-polygon trace stands down.
if (window.SDSShell && SDSShell.blocksSimpleTrace()) return;
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;
// SDS0 studio shell (optional asset — page fully works without it).
var sdsBridge = { simple: state, clearSimple: function () { state.clearDraw(); $('re-redraw').style.display = 'none'; } };
if (window.SDSShell) { SDSShell.attach(map, sdsBridge); } else { window.__SDS_PENDING = { map: map, bridge: sdsBridge }; }
} catch (e) {
$('re-map').innerHTML = '<div style="padding:40px;text-align:center;color:#6B6E7F">Map unavailable — use “Advanced settings → Roof area (manual)”.</div>';
}
};
window.gm_authFailure = function () {
$('re-map').innerHTML = '<div style="padding:40px;text-align:center;color:#6B6E7F">Map unavailable — use “Advanced settings → Roof area (manual)”.</div>';
};
function err(msg) { $('re-err').textContent = msg; $('re-err').style.display = 'block'; }
function commonParams() {
return {
mode: state.mode,
monthly_kwh: $('re-monthly').value || 0,
monthly_bill: $('re-bill').value || 0,
tariff: $('re-tariff').value || 0.22,
tilt: $('re-rooftype').value || 10
};
}
// A drawn outline is the user's explicit statement of the roof — it always
// wins over auto-detection. Typing a NEW address invalidates the old drawing.
$('re-search').addEventListener('input', function () {
if (state.points && state.points.length && state.clearDraw) { state.clearDraw(); $('re-redraw').style.display = 'none'; }
// A new address invalidates studio zones too (different site).
if (window.SDSShell && SDSShell.clearAllZones) { SDSShell.clearAllZones(); }
});
// ── Primary flow: address (+ bill) → auto roof → estimate ──
$('re-auto').onclick = function () {
var addr = ($('re-search').value || '').trim();
$('re-err').style.display = 'none'; $('re-auto-status').style.display = 'none';
// If the user traced the roof, use THAT — not the auto-detected footprint.
if (state.points && state.points.length >= 3 && state.area > 5) {
var s = $('re-auto-status'); s.style.display = 'block';
s.innerHTML = '<i class="fa-solid fa-draw-polygon" style="color:var(--n-amber)"></i> Using your drawn roof outline (' + Math.round(state.area).toLocaleString() + ' m²).';
estimateFromArea();
return;
}
if (!addr) { $('re-search').focus(); return; }
$('re-auto').disabled = true; $('re-auto').innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Detecting roof & modelling…';
var p = commonParams(); p.address = addr;
fetch('{{ url('honeybee_rooftop_auto') }}', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(p).toString() })
.then(function (r) { return r.json(); })
.then(function (d) {
$('re-auto').disabled = false; $('re-auto').innerHTML = '<i class="fa-solid fa-bolt"></i> Get my instant estimate';
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 {
err(d.error || 'Could not auto-detect — trace the roof on the map.');
}
})
.catch(function () { $('re-auto').disabled = false; $('re-auto').innerHTML = '<i class="fa-solid fa-bolt"></i> Get my instant estimate'; err('Network error — please try again.'); });
};
// ── Drawn / manual roof → estimate (shared by both buttons) ──
function estimateFromArea() {
$('re-err').style.display = 'none';
// SDS0: when studio zones exist they ARE the roof — use their SUM
// (with a single zone this equals the old single-polygon behavior).
var zs = window.SDSShell && SDSShell.estimateSource && SDSShell.estimateSource();
if (zs && zs.area > 5) { state.area = zs.area; state.lat = zs.lat; state.lng = zs.lng; state.manual = false; }
if (!state.area || state.area < 5) { err('Trace the roof on the map (or enter an area in Advanced settings) first.'); return; }
var lat = state.lat || 50, lng = state.lng || 9;
$('re-spin').style.display = 'block';
var p = commonParams();
p.lat = lat; p.lng = lng; p.area_m2 = state.area; p.roof_source = state.manual ? 'manual' : 'map';
fetch('{{ url('honeybee_rooftop_calc') }}', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams(p).toString() })
.then(function (r) { return r.json(); })
.then(function (d) {
$('re-spin').style.display = 'none';
if (!d.ok) { err(d.error || 'Could not calculate.'); return; }
render(d);
})
.catch(function () { $('re-spin').style.display = 'none'; err('Network error — please try again.'); });
}
$('re-go').onclick = estimateFromArea;
function eur(n) { return '€' + Math.round(n).toLocaleString('de-DE'); }
function tag(el, fig) { if (fig) { $(el).innerHTML = hb360Tag(fig.tag, fig.basis); } }
function render(d) {
var f = d.figures || {};
$('k-headline').textContent = eur(d.annual_savings);
$('k-headline-tag').innerHTML = d.headline ? hb360Tag(d.headline.tag, (f.annual_savings || {}).basis) : '';
$('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' : '—';
tag('t-kwp', f.system_kwp); tag('t-gen', f.annual_yield); tag('t-capex', f.capex); tag('t-payback', f.payback);
$('k-panels').textContent = d.panels.toLocaleString();
$('k-co2').textContent = d.co2_tonnes_yr; tag('t-co2', f.co2);
$('k-area').textContent = (d.area_m2 || 0).toLocaleString(); tag('t-roof', f.roof_area);
$('k-export').textContent = Math.round(d.export_kwh || 0).toLocaleString(); tag('t-export', f.grid_export);
$('k-src').textContent = (d.yield_source || '—') + (d.roof_source ? ' · roof via ' + d.roof_source : '');
var tz = $('re-locked-teaser'); tz.innerHTML = '';
((d.locked && d.locked.teaser) || []).forEach(function (t) {
var li = document.createElement('li'); li.textContent = t; tz.appendChild(li);
});
$('re-disclaimer').textContent = (d.formatted_address ? '📍 ' + d.formatted_address + ' — ' : '') + d.disclaimer;
if (d.saved) { $('re-saved-note').innerHTML = '<i class="fa-solid fa-check" style="color:var(--n-amber)"></i> Estimate saved — it will be waiting for you after sign-in.'; }
$('re-empty').style.display = 'none';
$('re-results').style.display = 'block';
$('re-results').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
})();
</script>
{# ── SDS0 · Solar Design Studio shell (page-scoped assets, NOT in the gulp bundles).
sds-layout.js is SDS1's file — the shell probe-loads it and degrades gracefully
while it does not exist yet. ── #}
{# SDS3: the PUBLIC studio stays TRANSIENT (no saveUrl — no new public write surface);
signInMsg renders the "sign in to save" hint in the panel. Persistence lives on the
tenant boxes at /solar-studio (session-gated). #}
<script>window.SDS_BOOT = { layoutUrl: '{{ asset('honeybee_web_assets/js/sds-layout.js') }}?v=21', calcUrl: '{{ url('honeybee_rooftop_calc') }}', density: 0.18, usable: 0.75, signInMsg: 'Sign in to your HoneyBee ERP to save designs and generate priced proposals.' };</script>
<script src="{{ asset('honeybee_web_assets/js/sds-shell.js') }}?v=30" defer onerror="console.warn('SDS shell unavailable — simple estimator only')"></script>
{# ── FUNNEL-1: anonymous design save (CTA + wire live ONLY in this twig + server files;
the shell JS is untouched — the card consumes the shell's public exportDesign() seam).
Fully guarded: if the shell/seam is absent the card simply never shows. ── #}
<script>
(function () {
var saveUrl = '{{ url('honeybee_rooftop_design_save') }}';
var signUpUrl = '{{ path('sign_up', {'refRoute': 'hb360_my_estimate'}) }}';
var loginUrl = '{{ path('central_login') }}';
// FUNNEL-3: the signed-in applicant's owned design + account prefill (null for guests)
var MY_DESIGN = {{ my_design is defined and my_design ? my_design|json_encode|raw : 'null' }};
var PREFILL = {{ applicant is defined and applicant ? applicant|json_encode|raw : 'null' }};
var busy = false;
// load the owned design into the studio once the shell + engine are up (the same
// probe-wait the tenant studio uses; gives up silently — the page stays usable)
if (MY_DESIGN && MY_DESIGN.payload) {
var importTries = 0;
var importTimer = setInterval(function () {
importTries++;
var ready = window.SDSShell && typeof window.SDSShell.importDesign === 'function'
&& window.google && window.google.maps && window.SolarLayout;
if (ready || importTries > 40) {
clearInterval(importTimer);
if (ready) {
try { window.SDSShell.importDesign(MY_DESIGN.payload); } catch (e) { /* page stays usable */ }
}
}
}, 500);
}
function el(id) { return document.getElementById(id); }
function esc(s) { var d = document.createElement('div'); d.textContent = String(s == null ? '' : s); return d.innerHTML; }
function msg(html, isErr) {
var m = el('sdsf-msg');
if (m) { m.innerHTML = html; m.style.color = isErr ? '#b4452f' : 'var(--n-muted)'; m.style.fontWeight = isErr ? '600' : ''; }
}
// Show the card only while the studio is actually active (the shell toggles sds-show).
setInterval(function () {
var card = el('sdsf-card'), panel = el('sds-panel');
if (!card || !panel) { return; }
var active = panel.classList.contains('sds-show')
&& window.SDSShell && typeof window.SDSShell.exportDesign === 'function';
card.style.display = active ? 'block' : 'none';
}, 800);
document.addEventListener('click', function (ev) {
if (!ev.target || ev.target.id !== 'sdsf-save' || busy) { return; }
if (!window.SDSShell || typeof window.SDSShell.exportDesign !== 'function') { return; }
var ex = window.SDSShell.exportDesign();
if (ex.error) { msg(esc(ex.error), true); return; }
busy = true;
msg('Saving…', false);
var addr = el('re-search') ? el('re-search').value : '';
fetch(saveUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ payload: ex.payload, address: addr,
project_id: MY_DESIGN ? MY_DESIGN.id : 0 })
}).then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
.then(function (res) {
busy = false;
var j = res.j || {};
if (j.ok && j.saved) {
var s = j.saved.summary || {};
if (MY_DESIGN && j.saved.project_id === MY_DESIGN.id) {
msg('Design updated on your account — ' + esc(s.zones) + ' zone(s) · '
+ esc(s.modules) + ' modules · ' + esc(s.kwp) + ' kWp.', false);
} else {
msg('Design saved — ' + esc(s.zones) + ' zone(s) · ' + esc(s.modules) + ' modules · '
+ esc(s.kwp) + ' kWp (as designed in the studio).<br>'
+ '<a href="' + signUpUrl + '" style="color:var(--n-amber);font-weight:600">Create a free account</a> or '
+ '<a href="' + loginUrl + '" style="color:var(--n-amber);font-weight:600">sign in</a> to keep it '
+ 'and see it under “My Solar Estimates”.', false);
}
var off = el('sdsf-offer');
if (off) { off.style.display = 'block'; }
} else {
msg(esc(j.error || ('Could not save (HTTP ' + res.status + ') — please try again.')), true);
}
}).catch(function () {
busy = false;
msg('Could not reach the server — your design stays in this browser tab. Try again in a moment.', true);
});
});
// ── FUNNEL-2: Request an offer (outbox insert on the server; delivery is the cron's) ──
var targetUrl = '{{ url('honeybee_rooftop_offer_target') }}';
var offerUrl = '{{ url('honeybee_rooftop_request_offer') }}';
var offerBusy = false;
// FUNNEL-3: pre-fill the contact fields from the signed-in account (editable)
if (PREFILL) {
var pn = el('sdsf-name'), pe = el('sdsf-email');
if (pn && !pn.value && PREFILL.name) { pn.value = PREFILL.name; }
if (pe && !pe.value && PREFILL.email) { pe.value = PREFILL.email; }
}
function offerMsg(html, isErr) {
var m = el('sdsf-offer-msg');
if (m) { m.innerHTML = html; m.style.color = isErr ? '#b4452f' : 'var(--n-muted)'; m.style.fontWeight = isErr ? '600' : ''; }
}
function offerCountry() {
var sel = el('sdsf-country');
if (!sel) { return ''; }
if (sel.value === '__other') {
var o = el('sdsf-country-other');
return o ? o.value.trim().toUpperCase() : '';
}
return sel.value;
}
document.addEventListener('change', function (ev) {
if (!ev.target) { return; }
if (ev.target.id === 'sdsf-country') {
var other = el('sdsf-country-other');
if (other) { other.style.display = ev.target.value === '__other' ? 'block' : 'none'; }
}
if (ev.target.id === 'sdsf-country' || ev.target.id === 'sdsf-country-other') {
var cc = offerCountry();
var tgt = el('sdsf-target');
if (!tgt || !/^[A-Z]{2}$/.test(cc)) { return; }
fetch(targetUrl + '?country=' + encodeURIComponent(cc))
.then(function (r) { return r.json(); })
.then(function (j) {
if (j.ok && j.company) { tgt.textContent = j.company; }
else { tgt.textContent = 'the responsible solar partner for my country'; if (j.unrouted) { offerMsg(esc(j.error), false); } }
}).catch(function () { /* preview is best-effort */ });
}
});
document.addEventListener('click', function (ev) {
if (!ev.target || ev.target.id !== 'sdsf-send' || offerBusy) { return; }
var cc = offerCountry();
var consent = el('sdsf-consent');
offerBusy = true;
offerMsg('Sending…', false);
fetch(offerUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: el('sdsf-name') ? el('sdsf-name').value : '',
email: el('sdsf-email') ? el('sdsf-email').value : '',
phone: el('sdsf-phone') ? el('sdsf-phone').value : '',
country: cc,
message: el('sdsf-message') ? el('sdsf-message').value : '',
consent: !!(consent && consent.checked),
project_id: MY_DESIGN ? MY_DESIGN.id : 0
})
}).then(function (r) { return r.json().then(function (j) { return { status: r.status, j: j }; }); })
.then(function (res) {
offerBusy = false;
var j = res.j || {};
if (j.ok) {
offerMsg('<b style="color:#1e7b34">Request received.</b> ' + esc(j.note || ''), false);
} else {
offerMsg(esc(j.error || ('Could not send (HTTP ' + res.status + ').')), true);
}
}).catch(function () {
offerBusy = false;
offerMsg('Could not reach the server — please try again in a moment.', true);
});
});
})();
</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' %}