MVP / app.py
pSchomaker's picture
Update app.py
2c97f26 verified
"""
goodcarbon Credits — Carbon Portfolio Management
Demo App for Siemens AG — Data-driven Version
Hosted on HuggingFace Spaces
"""
import gradio as gr
import pandas as pd
import base64
import os
import json
from datetime import datetime, timedelta
# ============ STATIC PATHS FOR LAZY-LOADED MEDIA ============
from pathlib import Path
gr.set_static_paths(paths=[Path.cwd().absolute() / "Media"])
# ============ NUMBER FORMATTING ============
def fmt_num(n, compact=False):
"""Consistent number formatting: compact (89K, 1.0M) or full (1,039,500)"""
n = int(n)
if compact:
if n >= 1_000_000:
return f"{n/1_000_000:.1f}M"
elif n >= 1_000:
return f"{n//1_000}K"
return str(n)
return f"{n:,}"
# ============ DATA LOADING ============
def load_image_base64(path):
try:
with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
ext = path.rsplit(".", 1)[-1].lower()
mime = {"png":"image/png","jpg":"image/jpeg","jpeg":"image/jpeg"}.get(ext,"image/png")
return f"data:{mime};base64,{data}"
except:
return ""
def load_data():
"""Load all data from Excel"""
for path in ["data.xlsx", "/app/data.xlsx", os.path.join(os.path.dirname(__file__), "data.xlsx")]:
if os.path.exists(path):
print(f"Found data file at: {path}, size: {os.path.getsize(path)} bytes")
xls = pd.read_excel(path, sheet_name=None)
print(f"Sheets found: {list(xls.keys())}")
# Be flexible with sheet names - use first 4 sheets if names don't match
sheets = list(xls.keys())
if "projects" in xls:
projects = xls["projects"].fillna("")
else:
projects = xls[sheets[0]].fillna("")
print(f"Using '{sheets[0]}' as projects sheet")
if "vintages" in xls:
vintages = xls["vintages"].fillna("")
elif len(sheets) > 1:
vintages = xls[sheets[1]].fillna("")
else:
vintages = pd.DataFrame()
if "retirements" in xls:
retirements = xls["retirements"].fillna("")
elif len(sheets) > 2:
retirements = xls[sheets[2]].fillna("")
else:
retirements = pd.DataFrame()
if "activity" in xls:
activity = xls["activity"].fillna("")
elif len(sheets) > 3:
activity = xls[sheets[3]].fillna("")
else:
activity = pd.DataFrame()
# Monitoring data (optional)
mon_scores = xls.get("monitoring_scores", pd.DataFrame()).fillna("")
mon_kpis = xls.get("monitoring_kpis", pd.DataFrame()).fillna("")
mon_alarms = xls.get("monitoring_alarms", pd.DataFrame()).fillna("")
return projects, vintages, retirements, activity, mon_scores, mon_kpis, mon_alarms
raise FileNotFoundError("data.xlsx not found.")
def find_media_files(prefix):
"""Find all image files for a project in Media folder"""
if not prefix:
return []
files = []
for i in range(1, 10):
fname = f"Media/{prefix}_image_{i:02d}.png"
if os.path.exists(fname):
files.append(fname)
return files
LOGO_GC = load_image_base64("Media/01_logos_goodcarbon.png")
LOGO_SIEMENS = load_image_base64("Media/01_logos_siemens.png")
# Load SDG icons as base64
SDG_ICONS = {}
for i in range(1, 18):
SDG_ICONS[i] = load_image_base64(f"Media/E-WEB-Goal-{i:02d}.png")
# ============ BUILD DASHBOARD ============
def build_dashboard():
projects, vintages, retirements, activity, mon_scores, mon_kpis, mon_alarms = load_data()
# Compute totals
total_contracted = int(projects["contracted_tco2"].sum())
total_delivered = int(projects["delivered_tco2"].sum())
total_open = int(projects["open_tco2"].sum())
num_projects = len(projects)
# Pathway aggregation
pathway_agg = projects.groupby("pathway")["contracted_tco2"].sum().sort_values(ascending=False)
pathway_total = pathway_agg.sum()
# Region aggregation
region_agg = projects.groupby("region")["contracted_tco2"].sum().sort_values(ascending=False)
# Premium dot-matrix world map (from curated JSON)
import json
with open("gc_world_dots_v5_regions_50x114.json") as f:
map_data = json.load(f)
region_map_colors = {
"Europe": "#00515D", "North America": "#87C314", "Middle East": "#2590A8",
"South America": "#335362", "Oceania": "#637C87", "Africa": "#00515D", "Asia": "#9CA89B",
"East Africa": "#00515D", "West Africa": "#335362", "Southern Africa": "#2590A8",
"Central America": "#87C314", "Latin America": "#CBE561",
"South Asia": "#9CA89B", "Southeast Asia": "#637C87"
}
active_regions = set(region_agg.index)
spacing = 6
dot_r = 1.5
mx, my = 8, 8
NC, NR = map_data["cols"], map_data["rows"]
sw = mx * 2 + NC * spacing
sh = my * 2 + NR * spacing
dots = ""
for d in map_data["dots"]:
x = mx + d["c"] * spacing
y = my + d["r"] * spacing
region = d["region"]
if region in active_regions:
color = region_map_colors.get(region, "#d5cfc7")
dots += f'<circle cx="{x}" cy="{y}" r="{dot_r}" fill="{color}" opacity="0.65"/>'
else:
dots += f'<circle cx="{x}" cy="{y}" r="{dot_r}" fill="#c5bfb7" opacity="0.4"/>'
# Region bubbles from anchors
max_vol = region_agg.max() if len(region_agg) > 0 else 1
bubbles = ""
for region, vol in region_agg.items():
anchor = map_data["anchors"].get(region)
if anchor:
bx = mx + anchor["c"] * spacing
by = my + anchor["r"] * spacing
color = region_map_colors.get(region, "#00515D")
r_size = max(12, min(24, int(vol / max_vol * 24)))
vol_k = fmt_num(int(vol), compact=True)
bubbles += f'<circle cx="{bx}" cy="{by}" r="{r_size+5}" fill="{color}" opacity="0.06"/>'
bubbles += f'<circle cx="{bx}" cy="{by}" r="{r_size}" fill="{color}" opacity="0.92" stroke="rgba(255,255,255,0.9)" stroke-width="1.5"/>'
bubbles += f'<text x="{bx}" y="{by+3.5}" text-anchor="middle" font-size="9" font-weight="700" fill="white" font-family="Inter,system-ui,sans-serif">{vol_k}</text>'
geo_map_html = f'''<div class="gc-map-container">
<svg viewBox="0 0 {sw} {sh}" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block" xmlns="http://www.w3.org/2000/svg">
<rect width="{sw}" height="{sh}" fill="#f7f5f1" rx="8"/>
{dots}
{bubbles}
</svg>
</div>'''
region_legend = ""
for region, vol in region_agg.items():
rc = region_map_colors.get(region, "#00515D")
region_legend += f'<div class="gc-legend-item"><div class="gc-legend-color" style="background:{rc}"></div><div class="gc-legend-label">{region}</div><div class="gc-legend-val">{fmt_num(vol)} tCO₂</div></div>'
years = ["2025","2026","2027","2028","2029","2030plus"]
del_by_year = {y: int(projects[f"del_{y}"].sum()) for y in years}
open_by_year = {y: int(projects[f"open_{y}"].sum()) for y in years}
max_year_total = max((del_by_year[y] + open_by_year[y]) for y in years)
# Project distribution (top 5 + others)
proj_sorted = projects.sort_values("contracted_tco2", ascending=False)
top5 = proj_sorted.head(5)
others_count = len(proj_sorted) - 5
others_vol = int(proj_sorted.iloc[5:]["contracted_tco2"].sum())
# Media files for projects — store as URLs for lazy loading via Gradio's static file serving
project_media = {}
for _, p in projects.iterrows():
prefix = p["media_prefix"]
if prefix:
files = find_media_files(prefix)
project_media[p["project_id"]] = [f"/gradio_api/file={f}" for f in files]
else:
project_media[p["project_id"]] = []
# Build projects JSON for JavaScript
# Pre-compute per-project monitoring data
_mon_by_project = {}
dim_cols_map = {1: "dim1_carbon", 2: "dim2_ecological", 3: "dim3_community", 4: "dim4_operational", 5: "dim5_risk"}
dim_names_map = {1: "Carbon Performance", 2: "Ecological Impact", 3: "Community Impact", 4: "Operational Health", 5: "Risk & Permanence"}
for _, ms in mon_scores.iterrows():
pid = str(ms["project_id"])
dims = {}
for d in range(1, 6):
col = dim_cols_map[d]
dims[f"dim{d}"] = int(ms[col]) if col in mon_scores.columns else 75
_mon_by_project[pid] = dims
# Per-project KPIs
_kpis_by_project = {}
if len(mon_kpis) > 0:
for pid_val, group in mon_kpis.groupby("project_id"):
pid_str = str(pid_val)
kpi_list = []
for _, krow in group.iterrows():
kpi_list.append({
"dimension": str(krow["dimension"]),
"kpi": str(krow["kpi"]),
"score": int(krow["score"]),
"target": str(krow["target"]),
"frequency": str(krow["frequency"]),
"source": str(krow["source"]),
})
_kpis_by_project[pid_str] = kpi_list
# Per-project alarms
_alarms_by_project = {}
if len(mon_alarms) > 0:
for _, arow in mon_alarms.iterrows():
pid_str = str(arow["project_id"])
if pid_str not in _alarms_by_project:
_alarms_by_project[pid_str] = []
_alarms_by_project[pid_str].append({
"severity": str(arow["severity"]),
"kpi": str(arow["kpi"]),
"description": str(arow["description"]),
"date": str(arow["date"]),
})
projects_js = []
for _, p in projects.iterrows():
pid = p["project_id"]
p_vintages = vintages[vintages["project_id"]==pid].to_dict("records")
p_retirements = retirements[retirements["project_id"]==pid].to_dict("records")
sdg_list = [int(x.strip()) for x in str(p["sdg_goals"]).split(",") if x.strip()]
projects_js.append({
"id": pid,
"name": str(p["name"]),
"pathway": str(p["pathway"]),
"project_type": str(p["project_type"]),
"mechanism": str(p["mechanism"]),
"country": str(p["country"]),
"region": str(p["region"]),
"standard": str(p["standard"]),
"registry_id": str(p["registry_id"]),
"status": str(p["status"]),
"area_ha": int(p["area_ha"]) if p["area_ha"] else 0,
"crediting_start": int(p["crediting_start"]) if p["crediting_start"] else 0,
"crediting_end": int(p["crediting_end"]) if p["crediting_end"] else 0,
"description": str(p["description"]),
"full_description": str(p["full_description"]),
"sdg_goals": sdg_list,
"contracted": int(p["contracted_tco2"]),
"delivered": int(p["delivered_tco2"]),
"open": int(p["open_tco2"]),
"vintages": p_vintages,
"retirements": p_retirements,
"media": project_media.get(pid, []),
"monitoring": _mon_by_project.get(pid, {}),
"kpis": _kpis_by_project.get(pid, []),
"alarms": _alarms_by_project.get(pid, []),
})
projects_json = json.dumps(projects_js, default=str)
sdg_icons_json = json.dumps(SDG_ICONS)
# ============ MONITORING: KPI Snapshot + Donut + Escalation ============
import math
# Cross-Portfolio KPI Snapshot (progress bars) - computed from monitoring KPIs
kpi_snapshot_items = [
("Carbon Delivery", "dim1", "#2F7A47"),
("Credit Issuance", "dim1", "#D4A017"),
("Environmental Additionality", "dim2", "#2F7A47"),
("Co-benefit Verification", "dim2", "#D4A017"),
("Community Benefit Sharing", "dim3", "#2F7A47"),
("Stakeholder Engagement", "dim3", "#2F7A47"),
("Milestone Adherence", "dim4", "#D4A017"),
("Financial Runway", "dim4", "#C24B3A"),
("Buffer Pool Adequacy", "dim5", "#2F7A47"),
("Regulatory Compliance", "dim5", "#2F7A47"),
]
kpi_bars_html = ""
for kpi_label, dim_key, fallback_color in kpi_snapshot_items:
# Try to find matching KPI in data
matched = mon_kpis[mon_kpis["kpi"].str.contains(kpi_label.split()[0], case=False, na=False)] if len(mon_kpis) > 0 else pd.DataFrame()
if len(matched) > 0:
score = int(matched["score"].mean())
else:
score = 75
if score >= 80: bar_color = "#2F7A47"
elif score >= 60: bar_color = "#D4A017"
else: bar_color = "#C24B3A"
kpi_bars_html += f'''<div class="gc-kpi-bar-row">
<span class="gc-kpi-bar-label">{kpi_label}</span>
<div class="gc-kpi-bar-track"><div class="gc-kpi-bar-fill" data-width="{score}%" style="width:0%;background:{bar_color}"></div></div>
<span class="gc-kpi-bar-val" style="color:{bar_color}">{score}%</span>
</div>'''
kpi_snapshot_html = f'<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Cross-Portfolio KPI Snapshot</div></div><div class="gc-kpi-bars-body">{kpi_bars_html}</div></div>'
# Portfolio Status Distribution (Donut) - count green/amber/red projects
n_green = n_amber = n_red = 0
for _, r in mon_scores.iterrows():
total = 0
for d in range(1, 6):
col = ["","dim1_carbon","dim2_ecological","dim3_community","dim4_operational","dim5_risk"][d]
if col in mon_scores.columns:
total += int(r[col])
avg = total // 5
if avg >= 80: n_green += 1
elif avg >= 60: n_amber += 1
else: n_red += 1
n_total = n_green + n_amber + n_red
circ = 2 * math.pi * 80
green_len = (n_green / n_total * circ) if n_total > 0 else 0
amber_len = (n_amber / n_total * circ) if n_total > 0 else 0
red_len = (n_red / n_total * circ) if n_total > 0 else 0
donut_html = f'''<div class="gc-chart-card">
<div class="gc-chart-header"><div class="gc-chart-title">Portfolio Status Distribution</div></div>
<div class="gc-donut-body">
<svg width="180" height="180" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="80" fill="none" stroke="#ECEFF1" stroke-width="26"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#2F7A47" stroke-width="26" stroke-dasharray="{green_len:.1f} {circ:.1f}" stroke-dashoffset="0" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#D4A017" stroke-width="26" stroke-dasharray="{amber_len:.1f} {circ:.1f}" stroke-dashoffset="-{green_len:.1f}" transform="rotate(-90 100 100)"/>
<circle cx="100" cy="100" r="80" fill="none" stroke="#C24B3A" stroke-width="26" stroke-dasharray="{red_len:.1f} {circ:.1f}" stroke-dashoffset="-{green_len+amber_len:.1f}" transform="rotate(-90 100 100)"/>
<text x="100" y="95" text-anchor="middle" font-size="32" font-weight="700" fill="#022B3D">{n_total}</text>
<text x="100" y="116" text-anchor="middle" font-size="11" fill="#637C87">Projects</text>
</svg>
<div class="gc-donut-legend">
<div class="gc-donut-legend-item"><span class="gc-donut-dot" style="background:#2F7A47"></span><span>On Track</span><span class="gc-donut-count" style="color:#2F7A47">{n_green}</span></div>
<div class="gc-donut-legend-item"><span class="gc-donut-dot" style="background:#D4A017"></span><span>Watch</span><span class="gc-donut-count" style="color:#D4A017">{n_amber}</span></div>
<div class="gc-donut-legend-item"><span class="gc-donut-dot" style="background:#C24B3A"></span><span>Alert / Critical</span><span class="gc-donut-count" style="color:#C24B3A">{n_red}</span></div>
</div>
</div>
</div>'''
# Escalation Protocol
escalation_html = '''<div class="gc-chart-card gc-chart-full" style="margin-top:16px">
<div class="gc-chart-header"><div class="gc-chart-title">Alarm Signal Escalation Protocol</div></div>
<div class="gc-esc-body">
<div class="gc-esc-step gc-esc-watch">
<div class="gc-esc-title"><span class="gc-esc-dot" style="background:#2F7A47"></span> WATCH</div>
<p>Single KPI approaching threshold or first-time minor breach</p>
<p class="gc-esc-action">Flag in report · Request developer clarification · Increase monitoring frequency</p>
<span class="gc-esc-tag" style="background:#2F7A47">Within 2 weeks</span>
</div>
<div class="gc-esc-arrow">→</div>
<div class="gc-esc-step gc-esc-alert">
<div class="gc-esc-title"><span class="gc-esc-dot" style="background:#D4A017"></span> ALERT</div>
<p>Multiple KPIs in breach or single KPI in sustained breach (2 consecutive periods)</p>
<p class="gc-esc-action">Escalate to IC · Site visit · Require remediation plan within 30 days</p>
<span class="gc-esc-tag" style="background:#D4A017">Within 1 week</span>
</div>
<div class="gc-esc-arrow">→</div>
<div class="gc-esc-step gc-esc-critical">
<div class="gc-esc-title"><span class="gc-esc-dot" style="background:#C24B3A"></span> CRITICAL</div>
<p>Knock-out criteria breached, integrity compromised, or developer non-responsive &gt;60 days</p>
<p class="gc-esc-action">Emergency IC review · Suspend purchases · Consider exit · Engage legal</p>
<span class="gc-esc-tag" style="background:#C24B3A">Immediately</span>
</div>
</div>
</div>'''
# ============ MONITORING DASHBOARD CONTENT ============
# Dimension summary cards (clickable)
dim_names = {1: "Carbon Performance", 2: "Ecological Impact", 3: "Community Impact", 4: "Operational Health", 5: "Risk & Permanence"}
dim_cols = {1: "dim1_carbon", 2: "dim2_ecological", 3: "dim3_community", 4: "dim4_operational", 5: "dim5_risk"}
dim_icons = {
1: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>',
2: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
3: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg>',
4: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg>',
5: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
}
dim_cards_html = ""
dim_avgs = {}
for d in range(1, 6):
col = dim_cols[d]
if col in mon_scores.columns:
avg = int(mon_scores[col].mean())
else:
avg = 75
dim_avgs[d] = avg
if avg >= 80: badge_class, badge_text = "gc-badge-green", "ON TRACK"
elif avg >= 60: badge_class, badge_text = "gc-badge-amber", "WATCH"
else: badge_class, badge_text = "gc-badge-red", "ALERT"
dim_cards_html += f'''<div class="gc-dim-card gc-dim-{badge_class.split('-')[-1]}" onclick="toggleDimDetail({d})">
<div class="gc-dim-name-row"><span class="gc-dim-name">{dim_names[d]}</span><span class="{badge_class}">{badge_text}</span></div>
<div class="gc-dim-score">{avg}%</div>
</div>'''
# KPI Scorecard table (traffic light per project per dimension)
scorecard_html = '<table class="gc-mon-table"><thead><tr><th>Project</th><th>Carbon</th><th>Ecological</th><th>Community</th><th>Operational</th><th>Risk</th><th>Overall</th></tr></thead><tbody>'
for _, p in projects.sort_values("contracted_tco2", ascending=False).iterrows():
pid = p["project_id"]
row_scores = mon_scores[mon_scores["project_id"]==pid]
if len(row_scores) == 0:
continue
r = row_scores.iloc[0]
dots = ""
total = 0
for d in range(1, 6):
val = int(r[dim_cols[d]])
total += val
color = "green" if val >= 80 else "amber" if val >= 60 else "red"
dots += f'<td><span class="gc-status-dot gc-dot-{color}"></span></td>'
overall = total // 5
oc = "green" if overall >= 80 else "amber" if overall >= 60 else "red"
ol = "ON TRACK" if overall >= 80 else "WATCH" if overall >= 60 else "ALERT"
scorecard_html += f'<tr class="gc-mon-clickable" onclick="showProjectMonitoring(\'{pid}\')"><td class="gc-mon-proj-name">{p["name"][:30]}</td>{dots}<td><span class="gc-status-label gc-sl-{oc}">{ol}</span></td></tr>'
scorecard_html += '</tbody></table>'
# Alarm feed
alarm_html = ""
sev_colors = {"critical": "#C24B3A", "alert": "#D4A017", "watch": "#2F7A47"}
if len(mon_alarms) > 0:
for _, a in mon_alarms.iterrows():
sev = str(a["severity"])
color = sev_colors.get(sev, "#637C87")
pname = ""
pm = projects[projects["project_id"]==str(a["project_id"])]
if len(pm) > 0:
pname = str(pm.iloc[0]["name"])[:25]
alarm_html += f'''<div class="gc-alarm-item">
<div class="gc-alarm-dot" style="background:{color}"></div>
<div class="gc-alarm-body"><strong>{pname}{a["kpi"]}</strong><div class="gc-alarm-desc">{a["description"]}</div></div>
<div class="gc-alarm-meta"><span class="gc-alarm-sev" style="color:{color}">{sev.upper()}</span><span class="gc-alarm-date">{a["date"]}</span></div>
</div>'''
# Dimension detail panels (hidden, toggled by card click)
dim_details_html = ""
for d in range(1, 6):
dim_key = f"dim{d}"
dim_kpis = mon_kpis[mon_kpis["dimension"]==dim_key] if len(mon_kpis) > 0 else pd.DataFrame()
# Aggregate KPIs across projects
kpi_summary = {}
if len(dim_kpis) > 0:
for kpi_name, group in dim_kpis.groupby("kpi"):
avg_score = int(group["score"].mean())
status = "green" if avg_score >= 80 else "amber" if avg_score >= 60 else "red"
kpi_summary[kpi_name] = {"score": avg_score, "status": status, "target": str(group.iloc[0]["target"]), "freq": str(group.iloc[0]["frequency"]), "source": str(group.iloc[0]["source"])}
rows = ""
for kpi_name, info in kpi_summary.items():
sc = info["status"]
rows += f'''<tr><td class="gc-mon-proj-name">{kpi_name}</td><td>{info["target"]}</td><td>{info["freq"]}</td><td>{info["source"]}</td><td><span class="gc-status-label gc-sl-{sc}">{info["score"]}%</span></td></tr>'''
avg = dim_avgs.get(d, 75)
oc = "green" if avg >= 80 else "amber" if avg >= 60 else "red"
dim_details_html += f'''<div class="gc-dim-detail" id="gcDimDetail{d}" style="display:none">
<div class="gc-dim-detail-head"><div class="gc-dim-badge">{d}</div><div class="gc-dim-detail-info"><div class="gc-dim-detail-title">{dim_names[d]}</div></div><span class="gc-status-label gc-sl-{oc}">{avg}%</span></div>
<table class="gc-mon-table gc-mon-detail-table"><thead><tr><th>KPI</th><th>Target</th><th>Frequency</th><th>Data Source</th><th>Status</th></tr></thead><tbody>{rows}</tbody></table>
</div>'''
# Count alarms
n_critical = len(mon_alarms[mon_alarms["severity"]=="critical"]) if len(mon_alarms) > 0 else 0
n_alarms = len(mon_alarms)
# Combine full monitoring content
monitoring_content = f'''
<div class="gc-dim-cards">{dim_cards_html}</div>
{dim_details_html}
<div class="gc-mon-grid">
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Portfolio KPI Scorecard</div></div>{scorecard_html}</div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Alarm Feed</div><span class="gc-alarm-count">{n_alarms} Active</span></div><div class="gc-alarm-feed">{alarm_html}</div></div>
</div>
<div class="gc-mon-grid-2col" style="margin-top:16px">
{kpi_snapshot_html}
{donut_html}
</div>
{escalation_html}'''
# ============ BUILD HTML ============
# Distribution bars - stacked: Delivered (lime) + Open (dark teal)
dist_bars_html = ""
for i, (_, row) in enumerate(top5.iterrows()):
contracted = int(row["contracted_tco2"])
delivered = int(row["delivered_tco2"])
opn = int(row["open_tco2"])
pct = round(contracted / pathway_total * 100)
open_pct = round(opn / contracted * 100) if contracted > 0 else 0
del_pct_of_bar = round(delivered / contracted * 100) if contracted > 0 else 0
open_pct_of_bar = 100 - del_pct_of_bar
open_label = f'{open_pct}% open' if open_pct_of_bar >= 15 else ''
dist_bars_html += f'<div class="gc-dist-item"><div class="gc-dist-head"><span class="gc-dist-label">{row["name"][:35]}</span><span class="gc-dist-value">{fmt_num(contracted)} tCO₂</span></div><div class="gc-dist-bar"><div class="gc-dist-fill-stacked"><div class="gc-dist-seg-del" data-width="{del_pct_of_bar}%" style="width:0%"></div><div class="gc-dist-seg-open" data-width="{open_pct_of_bar}%" style="width:0%"><span class="gc-bar-label">{open_label}</span></div></div></div></div>'
others_pct = round(others_vol / pathway_total * 100)
# For "Others" use a blended approach
others_del = sum(int(r["delivered_tco2"]) for _, r in proj_sorted.iloc[5:].iterrows())
others_del_pct = round(others_del / others_vol * 100) if others_vol > 0 else 0
others_open = others_vol - others_del
others_open_pct = round(others_open / others_vol * 100) if others_vol > 0 else 0
others_open_pct_bar = 100 - others_del_pct
others_open_label = f'{others_open_pct}% open' if others_open_pct_bar >= 15 else ''
dist_bars_html += f'<div class="gc-dist-item"><div class="gc-dist-head"><span class="gc-dist-label">Other Projects ({others_count})</span><span class="gc-dist-value">{fmt_num(others_vol)} tCO₂</span></div><div class="gc-dist-bar"><div class="gc-dist-fill-stacked"><div class="gc-dist-seg-del" data-width="{others_del_pct}%" style="width:0%"></div><div class="gc-dist-seg-open" data-width="{others_open_pct_bar}%" style="width:0%"><span class="gc-bar-label">{others_open_label}</span></div></div></div></div>'
# Pathway bars - stacked
pathway_bars_html = ""
pathway_del = projects.groupby("pathway")["delivered_tco2"].sum()
for pathway, vol in pathway_agg.items():
pct = round(vol / pathway_total * 100)
p_del = int(pathway_del.get(pathway, 0))
p_open = int(vol) - p_del
open_pct = round(p_open / vol * 100) if vol > 0 else 0
del_pct_of_bar = round(p_del / vol * 100) if vol > 0 else 0
open_pct_of_bar = 100 - del_pct_of_bar
open_label = f'{open_pct}% open' if open_pct_of_bar >= 15 else ''
pathway_bars_html += f'<div class="gc-dist-item"><div class="gc-dist-head"><span class="gc-dist-label">{pathway}</span><span class="gc-dist-value">{fmt_num(int(vol))} tCO₂</span></div><div class="gc-dist-bar"><div class="gc-dist-fill-stacked"><div class="gc-dist-seg-del" data-width="{del_pct_of_bar}%" style="width:0%"></div><div class="gc-dist-seg-open" data-width="{open_pct_of_bar}%" style="width:0%"><span class="gc-bar-label">{open_label}</span></div></div></div></div>'
# Timeline bars
timeline_html = ""
year_labels = {"2025":"2025","2026":"2026","2027":"2027","2028":"2028","2029":"2029","2030plus":"2030+"}
for y in years:
d = del_by_year[y]
o = open_by_year[y]
d_pct = round(d / max_year_total * 90) if max_year_total else 0
o_pct = round(o / max_year_total * 90) if max_year_total else 0
d_label = fmt_num(d, compact=True)
o_label = fmt_num(o, compact=True)
bars = f'<div class="gc-tbar gc-bg-open-bar" data-height="{max(o_pct,2)}%" style="height:0%">{o_label}</div>' if o > 0 else ''
bars = f'<div class="gc-tbar gc-bg-lime" data-height="{max(d_pct,3)}%" style="height:0%">{d_label}</div>' + bars
timeline_html += f'<div class="gc-tbar-col"><div class="gc-tbar-stack">{bars}</div><div class="gc-tbar-label">{year_labels[y]}</div></div>'
# Activity feed
activity_html = ""
icon_map = {"verification":"✓","delivery":"📦","credit":"📄","alert":"🛡","report":"📊","retirement":"✓"}
for _, a in activity.head(6).iterrows():
icon = icon_map.get(str(a["type"]), "•")
# Calculate relative time
activity_html += f'<div class="gc-activity-item"><div class="gc-activity-icon">{icon}</div><div class="gc-activity-content"><div class="gc-activity-text">{a["description"]}</div><div class="gc-activity-time">{a["date"]}</div></div></div>'
# Stacked Area Chart — Delivery Schedule by Pathway (for Analytics tab)
pathway_colors_area = {
"Biochar": "#00515D", "DACCS": "#2590A8", "Enhanced Rock Weathering": "#87C314",
"Improved Agricultural Land Management": "#CBE561", "BECCS": "#335362",
"Afforestation/Reforestation": "#2F7A47"
}
pathway_short = {
"Biochar": "Biochar", "DACCS": "DACCS", "Enhanced Rock Weathering": "ERW",
"Improved Agricultural Land Management": "IALM", "BECCS": "BECCS",
"Afforestation/Reforestation": "A/R"
}
area_years = ["2025","2026","2027","2028","2029","2030plus"]
area_year_labels = {"2025":"2025","2026":"2026","2027":"2027","2028":"2028","2029":"2029","2030plus":"2030+"}
area_pathways = list(projects["pathway"].unique())
pw_totals = {pw: int(projects[projects["pathway"]==pw]["contracted_tco2"].sum()) for pw in area_pathways}
area_pathways = sorted(area_pathways, key=lambda pw: pw_totals.get(pw, 0), reverse=True)
pathway_year_data = {}
for pw in area_pathways:
pw_proj = projects[projects["pathway"]==pw]
pathway_year_data[pw] = {}
for y in area_years:
d = int(pw_proj[f"del_{y}"].sum())
o = int(pw_proj[f"open_{y}"].sum())
pathway_year_data[pw][y] = d + o
year_stacked = {}
for y in area_years:
cumul = 0
year_stacked[y] = [0]
for pw in area_pathways:
cumul += pathway_year_data[pw].get(y, 0)
year_stacked[y].append(cumul)
area_max_total = max(year_stacked[y][-1] for y in area_years)
area_max_total = max(area_max_total, 1)
AW = 900
AH = 340
APL = 60
APR = 20
APT = 30
APB = 40
a_chart_w = AW - APL - APR
a_chart_h = AH - APT - APB
import math
y_step_raw = area_max_total / 5
magnitude = 10 ** math.floor(math.log10(max(y_step_raw, 1)))
y_step = math.ceil(y_step_raw / magnitude) * magnitude
y_max = y_step * 5
if y_max < area_max_total:
y_max = y_step * 6
def ax(idx):
return APL + idx * (a_chart_w / (len(area_years) - 1))
def ay(val):
return APT + a_chart_h - (val / y_max * a_chart_h)
area_grid = ""
for i in range(6):
yv = i * y_step
yp = ay(yv)
area_grid += f'<line x1="{APL}" y1="{yp:.1f}" x2="{AW-APR}" y2="{yp:.1f}" stroke="#E0D9D3" stroke-width="0.5"/>'
label = fmt_num(int(yv), compact=True) + " tCO₂" if yv > 0 else "0"
area_grid += f'<text x="{APL-8}" y="{yp+3:.1f}" text-anchor="end" font-size="8" fill="#637C87" font-family="Inter,system-ui,sans-serif">{label}</text>'
for i, y in enumerate(area_years):
xp = ax(i)
area_grid += f'<text x="{xp:.1f}" y="{AH-8}" text-anchor="middle" font-size="9" fill="#637C87" font-family="Inter,system-ui,sans-serif">{area_year_labels[y]}</text>'
area_paths = ""
for layer_idx in range(len(area_pathways)-1, -1, -1):
pw = area_pathways[layer_idx]
color = pathway_colors_area.get(pw, "#637C87")
top_points = []
for i, y in enumerate(area_years):
top_points.append(f"{ax(i):.1f},{ay(year_stacked[y][layer_idx + 1]):.1f}")
bottom_points = []
for i, y in enumerate(reversed(area_years)):
ri = len(area_years) - 1 - i
bottom_points.append(f"{ax(ri):.1f},{ay(year_stacked[y][layer_idx]):.1f}")
path_d = "M" + " L".join(top_points) + " L" + " L".join(bottom_points) + " Z"
area_paths += f'<path d="{path_d}" fill="{color}" opacity="0.75"/>'
total_line_points = []
for i, y in enumerate(area_years):
total_line_points.append(f"{ax(i):.1f},{ay(year_stacked[y][-1]):.1f}")
area_paths += f'<polyline points="{" ".join(total_line_points)}" fill="none" stroke="#022B3D" stroke-width="1.5" stroke-dasharray="4,3"/>'
for i, y in enumerate(area_years):
xp = ax(i)
yp = ay(year_stacked[y][-1])
val = year_stacked[y][-1]
area_paths += f'<circle cx="{xp:.1f}" cy="{yp:.1f}" r="3" fill="#022B3D" stroke="#fff" stroke-width="1.5"/>'
area_paths += f'<text x="{xp:.1f}" y="{yp-10:.1f}" text-anchor="middle" font-size="8" font-weight="600" fill="#022B3D" font-family="Inter,system-ui,sans-serif">{fmt_num(int(val), compact=True)}</text>'
area_legend = ""
lx = APL
for pw in area_pathways:
color = pathway_colors_area.get(pw, "#637C87")
abbr = pathway_short.get(pw, pw[:6])
area_legend += f'<rect x="{lx}" y="6" width="10" height="10" rx="2" fill="{color}" opacity="0.82"/>'
area_legend += f'<text x="{lx+14}" y="14" font-size="8" fill="#637C87" font-family="Inter,system-ui,sans-serif">{abbr}</text>'
lx += len(abbr) * 6 + 26
total_delivered = sum(del_by_year[y] for y in area_years)
total_open = sum(open_by_year[y] for y in area_years)
total_all = total_delivered + total_open
# Keep old pathway stacked area as part of Pro View
old_pathway_area = f'''<div class="gc-chart-card gc-chart-full" style="margin-bottom:16px">
<div class="gc-chart-header"><div class="gc-chart-title">Delivery Schedule by Pathway (Current Portfolio)</div></div>
<div style="padding:0 16px 8px">
<svg viewBox="0 0 {AW} {AH}" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block" xmlns="http://www.w3.org/2000/svg">
{area_legend}
{area_grid}
{area_paths}
</svg>
</div>
<div class="gc-area-kpis">
<div class="gc-area-kpi"><span class="gc-area-kpi-label">Delivered</span><span class="gc-area-kpi-val gc-kpi-green">{fmt_num(total_delivered)} tCO₂</span></div>
<div class="gc-area-kpi"><span class="gc-area-kpi-label">Open</span><span class="gc-area-kpi-val">{fmt_num(total_open)} tCO₂</span></div>
<div class="gc-area-kpi gc-area-kpi-total"><span class="gc-area-kpi-label">Total Contracted</span><span class="gc-area-kpi-val">{fmt_num(total_all)} tCO₂</span></div>
</div>
</div>'''
# ============ PRO VIEW — Forward-looking portfolio planning ============
pro_view_html = f'''
<!-- Sub-nav toggle -->
<div style="display:flex;gap:8px;margin-bottom:16px">
<button class="gc-pv-toggle active" onclick="switchProView('project',this)" id="pvBtnProject">By Project</button>
<button class="gc-pv-toggle" onclick="switchProView('pathway',this)" id="pvBtnPathway">By Pathway</button>
</div>
<!-- Forward Portfolio Chart (shared) -->
<div class="gc-chart-card gc-chart-full" style="margin-bottom:16px">
<div class="gc-chart-header"><div class="gc-chart-title">Forward Portfolio · Delivery Schedule (2030–2040)</div></div>
<div style="padding:12px 20px 0">
<div style="display:flex;gap:28px;flex-wrap:wrap;padding-bottom:12px;border-bottom:1px solid #E0D9D3">
<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#637C87">Firm Contract</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:#0e7e6e"></div>A/R</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:#2e7da6"></div>Wetland</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:#c4a03a"></div>SOC</div>
</div>
<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#637C87">+ Top-up Option</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:repeating-linear-gradient(45deg,transparent,transparent 2px,#3aa88e 2px,#3aa88e 3px);border:1px solid #3aa88e"></div>A/R option</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:repeating-linear-gradient(45deg,transparent,transparent 2px,#5ca3c8 2px,#5ca3c8 3px);border:1px solid #5ca3c8"></div>Wetland option</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:repeating-linear-gradient(45deg,transparent,transparent 2px,#dabe5c 2px,#dabe5c 3px);border:1px solid #dabe5c"></div>SOC option</div>
</div>
<div style="display:flex;flex-direction:column;gap:4px"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#637C87">+ Extension Option</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:rgba(58,168,142,.18);border:1px solid #3aa88e"></div>A/R ext.</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:rgba(92,163,200,.18);border:1px solid #5ca3c8"></div>Wetland ext.</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#022B3D"><div style="width:12px;height:12px;border-radius:2px;background:rgba(218,190,92,.18);border:1px solid #dabe5c"></div>SOC ext.</div>
</div>
</div>
</div>
<div style="padding:8px 20px 16px">
<div style="display:flex;border-radius:6px;overflow:hidden;font-size:11px;font-weight:600;letter-spacing:.3px;margin-bottom:8px">
<div style="flex:5;background:#00515D;color:#fff;padding:8px 12px">Firm + Top-up · 2030–2035</div>
<div style="flex:5;background:#5ca3c8;color:#fff;padding:8px 12px;text-align:right">Extension · 2036–2040</div>
</div>
<div style="position:relative;width:100%;height:300px">
<canvas id="pvAreaChart" style="width:100%!important;height:100%!important"></canvas>
<div id="pvTooltip" style="position:absolute;background:#00515D;color:#fff;padding:8px 12px;border-radius:6px;font-size:11px;pointer-events:none;opacity:0;z-index:10;white-space:nowrap;box-shadow:0 4px 12px rgba(0,43,61,.25)"></div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);background:#F0EBE4;border-top:1px solid #E0D9D3">
<div style="padding:16px;text-align:center;border-right:1px solid #E0D9D3"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#637C87;margin-bottom:6px">Firm (2030–35)</div><div style="font-family:'Space Mono',monospace;font-size:20px;font-weight:700;color:#022B3D">321,000</div><div style="font-size:11px;color:#637C87;margin-top:2px">tCO₂</div></div>
<div style="padding:16px;text-align:center;border-right:1px solid #E0D9D3"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#637C87;margin-bottom:6px">Top-up (2030–35)</div><div style="font-family:'Space Mono',monospace;font-size:20px;font-weight:700;color:#022B3D">138,000</div><div style="font-size:11px;color:#637C87;margin-top:2px">tCO₂ additional</div></div>
<div style="padding:16px;text-align:center;border-right:1px solid #E0D9D3"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#637C87;margin-bottom:6px">Extension (2036–40)</div><div style="font-family:'Space Mono',monospace;font-size:20px;font-weight:700;color:#022B3D">215,000</div><div style="font-size:11px;color:#637C87;margin-top:2px">tCO₂</div></div>
<div style="padding:16px;text-align:center"><div style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#637C87;margin-bottom:6px">Total incl. Options</div><div style="font-family:'Space Mono',monospace;font-size:20px;font-weight:700;color:#00515D">674,000</div><div style="font-size:11px;color:#637C87;margin-top:2px">tCO₂</div></div>
</div>
</div>
<!-- BY PROJECT VIEW -->
<div id="pvProject" class="gc-pv-section">
<div class="gc-charts-grid">
<div class="gc-chart-card" style="grid-column:1/-1">
<div class="gc-chart-header"><div class="gc-chart-title">Budget Allocation by Project</div></div>
<div style="padding:16px;display:flex;gap:10px;flex-wrap:wrap;margin-bottom:4px">
<div style="display:flex;align-items:center;gap:6px;font-size:12px;font-weight:500"><div style="width:14px;height:14px;border-radius:3px;background:#00515D"></div>Firm Contract</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;font-weight:500"><div style="width:14px;height:14px;border-radius:3px;background:repeating-linear-gradient(45deg,transparent,transparent 2px,#3aa88e 2px,#3aa88e 4px);background-color:rgba(58,168,142,.2)"></div>Top-up Option</div>
<div style="display:flex;align-items:center;gap:6px;font-size:12px;font-weight:500"><div style="width:14px;height:14px;border-radius:3px;background:#5ca3c8;opacity:.7"></div>Extension Option</div>
</div>
<div style="padding:0 16px 16px;display:flex;flex-direction:column;gap:14px" id="pvBudgetProject">
<!-- Cordillera Azul -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div><div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Cordillera Azul, Peru</div><div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#637C87;margin-top:3px"><div style="width:7px;height:7px;border-radius:50%;background:#0e7e6e"></div>A/R · VERRA VCS</div></div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">310,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 935,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 3.02</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:156;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Firm 156k</div><div style="flex:52;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Top-up 52k</div><div style="flex:102;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Ext. 102k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:402;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€402k</div><div style="flex:220;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€220k</div><div style="flex:313;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€313k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#00515D"></div><span style="color:#637C87">Firm</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 2.58</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#3aa88e"></div><span style="color:#637C87">Top-up</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.23</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#5ca3c8"></div><span style="color:#637C87">Ext.</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.07</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Sundarbans -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div><div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Sundarbans Delta, Bangladesh</div><div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#637C87;margin-top:3px"><div style="width:7px;height:7px;border-radius:50%;background:#2e7da6"></div>Wetland / Mangrove · Gold Standard</div></div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">195,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 681,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 3.49</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:89;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Firm 89k</div><div style="flex:46;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Top-up 46k</div><div style="flex:60;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Ext. 60k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:299;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€299k</div><div style="flex:140;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€140k</div><div style="flex:242;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€242k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#00515D"></div><span style="color:#637C87">Firm</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.36</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#3aa88e"></div><span style="color:#637C87">Top-up</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.04</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#5ca3c8"></div><span style="color:#637C87">Ext.</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.03</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Great Plains SOC -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div><div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Great Plains SOC, USA</div><div style="display:flex;align-items:center;gap:5px;font-size:11px;color:#637C87;margin-top:3px"><div style="width:7px;height:7px;border-radius:50%;background:#c4a03a"></div>Soil Organic Carbon · ACR</div></div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">169,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 685,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 4.05</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:76;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Firm 76k</div><div style="flex:40;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Top-up 40k</div><div style="flex:53;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Ext. 53k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:299;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€299k</div><div style="flex:140;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€140k</div><div style="flex:246;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€246k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#00515D"></div><span style="color:#637C87">Firm</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.93</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#3aa88e"></div><span style="color:#637C87">Top-up</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.50</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#5ca3c8"></div><span style="color:#637C87">Ext.</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.64</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Total -->
<div style="background:#F0EBE4;border:2px solid #00515D;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Total Portfolio</div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#00515D">674,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700;color:#00515D">€ 2,300,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700;color:#00515D">€ 3.41</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:321;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Firm 321k</div><div style="flex:138;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Top-up 138k</div><div style="flex:215;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Ext. 215k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:100;background:#00515D;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Firm €1.0M</div><div style="flex:50;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">€500k</div><div style="flex:80;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Ext. €800k</div></div></div>
</div>
</div>
</div>
</div>
</div>
<!-- Pie Charts -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px">
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Volume by Geography</div></div><div style="padding:20px;text-align:center"><canvas id="pieGeo" width="200" height="200" style="max-width:180px;max-height:180px"></canvas><div id="legendGeo" style="margin-top:14px"></div></div></div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Volume by Standard</div></div><div style="padding:20px;text-align:center"><canvas id="pieStd" width="200" height="200" style="max-width:180px;max-height:180px"></canvas><div id="legendStd" style="margin-top:14px"></div></div></div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Volume by Rating</div></div><div style="padding:20px;text-align:center"><canvas id="pieRat" width="200" height="200" style="max-width:180px;max-height:180px"></canvas><div id="legendRat" style="margin-top:14px"></div></div></div>
</div>
</div>
<!-- BY PATHWAY VIEW -->
<div id="pvPathway" class="gc-pv-section" style="display:none">
<div class="gc-chart-card gc-chart-full">
<div class="gc-chart-header"><div class="gc-chart-title">Budget Allocation by Pathway</div></div>
<div style="padding:0 16px 16px;display:flex;flex-direction:column;gap:14px">
<!-- Firm Contract -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Firm Contract Budget</div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2035</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">321,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 1,000,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 3.12</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:156;background:#0e7e6e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R 156k</div><div style="flex:89;background:#2e7da6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetland 89k</div><div style="flex:76;background:#c4a03a;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC 76k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:402;background:#0e7e6e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R €402k</div><div style="flex:299;background:#2e7da6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetland €299k</div><div style="flex:299;background:#c4a03a;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC €299k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#0e7e6e"></div><span style="color:#637C87">A/R</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 2.58</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#2e7da6"></div><span style="color:#637C87">Wetland</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.36</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#c4a03a"></div><span style="color:#637C87">SOC</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.93</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Top-up Option -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Top-up Option Budget</div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2035</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">138,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 500,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 3.62</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:52;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R 52k</div><div style="flex:46;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetland 46k</div><div style="flex:40;background:#dabe5c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC 40k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:220;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R €220k</div><div style="flex:140;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetl. €140k</div><div style="flex:140;background:#dabe5c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC €140k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#3aa88e"></div><span style="color:#637C87">A/R</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.23</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#5ca3c8"></div><span style="color:#637C87">Wetland</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.04</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#dabe5c"></div><span style="color:#637C87">SOC</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.50</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Extension Option -->
<div style="background:#fff;border:1.5px solid #E0D9D3;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Extension Option Budget</div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2036–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700">215,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 800,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700">€ 3.72</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:102;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R 102k</div><div style="flex:60;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetland 60k</div><div style="flex:53;background:#dabe5c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC 53k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:313;background:#3aa88e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R €313k</div><div style="flex:242;background:#5ca3c8;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetl. €242k</div><div style="flex:246;background:#dabe5c;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC €246k</div></div></div>
</div>
<div style="display:flex;justify-content:space-between;padding-top:10px;border-top:1px solid #E0D9D3;font-size:11px">
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#3aa88e"></div><span style="color:#637C87">A/R</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 3.07</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#5ca3c8"></div><span style="color:#637C87">Wetland</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.03</span><span style="color:#637C87">/t</span></div>
<div style="display:flex;align-items:center;gap:5px"><div style="width:7px;height:7px;border-radius:50%;background:#dabe5c"></div><span style="color:#637C87">SOC</span><span style="font-family:'Space Mono',monospace;font-weight:700">€ 4.64</span><span style="color:#637C87">/t</span></div>
</div>
</div>
<!-- Pathway Total -->
<div style="background:#F0EBE4;border:2px solid #00515D;border-radius:12px;padding:18px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#022B3D">Total Portfolio</div>
<div style="font-size:11px;color:#637C87;border:1px solid #E0D9D3;padding:3px 10px;border-radius:12px">2030–2040</div>
</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;margin-bottom:14px">
<div><span style="font-size:12px;color:#637C87">Volume:</span> <span style="font-family:'Space Mono',monospace;font-size:18px;font-weight:700;color:#00515D">674,000</span> <span style="font-size:11px;color:#637C87">tCO₂</span></div>
<div><span style="font-size:12px;color:#637C87">Budget:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700;color:#00515D">€ 2,300,000</span></div>
<div><span style="font-size:12px;color:#637C87">Avg.:</span> <span style="font-family:'Space Mono',monospace;font-size:16px;font-weight:700;color:#00515D">€ 3.41</span> <span style="font-size:11px;color:#637C87">/ tCO₂</span></div>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Vol.</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:310;background:#0e7e6e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R 310k</div><div style="flex:195;background:#2e7da6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetland 195k</div><div style="flex:169;background:#c4a03a;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC 169k</div></div></div>
<div style="display:flex;align-items:center;gap:10px"><div style="font-size:10px;font-weight:600;color:#637C87;width:48px;text-align:right;text-transform:uppercase;letter-spacing:.5px">Budget</div><div style="flex:1;display:flex;border-radius:6px;overflow:hidden;height:28px"><div style="flex:935;background:#0e7e6e;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">A/R €935k</div><div style="flex:681;background:#2e7da6;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">Wetl. €681k</div><div style="flex:685;background:#c4a03a;color:#fff;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:700">SOC €685k</div></div></div>
</div>
</div>
</div>
</div>
</div>'''
# ============ GANTT — Project Timelines sorted by start year, then volume ============
pathway_colors_gantt = {
"Biochar": "#00515D", "DACCS": "#2590A8", "Enhanced Rock Weathering": "#87C314",
"Improved Agricultural Land Management": "#CBE561", "BECCS": "#335362",
"Afforestation/Reforestation": "#2F7A47",
"SOC": "#87C314", "ARR": "#00515D", "Mangroves": "#2590A8"
}
pathway_short_gantt = {
"Biochar": "Biochar", "DACCS": "DACCS", "Enhanced Rock Weathering": "ERW",
"Improved Agricultural Land Management": "IALM", "BECCS": "BECCS",
"Afforestation/Reforestation": "A/R",
"SOC": "SOC", "ARR": "ARR", "Mangroves": "Mang."
}
# Sort: by crediting_start asc, then contracted_tco2 desc
gantt_sorted = projects.sort_values(["crediting_start", "contracted_tco2"], ascending=[False, True])
# This puts latest-start/smallest at top, earliest-start/biggest at bottom
gantt_min_year = 2020
gantt_max_year = 2060
GW = 1000
GPL = 230
GPR = 30
GPT = 6
GPW = GW - GPL - GPR
max_contracted = int(gantt_sorted["contracted_tco2"].max())
min_h = 10
max_h = 34
row_gap = 4
gantt_rows = []
for _, p in gantt_sorted.iterrows():
contracted = int(p["contracted_tco2"])
h = min_h + (max_h - min_h) * (contracted / max_contracted) if max_contracted > 0 else min_h
gantt_rows.append({"p": p, "h": h, "contracted": contracted})
GH = sum(r["h"] + row_gap for r in gantt_rows) + GPT + 24
def gx(year): return GPL + (year - gantt_min_year) / (gantt_max_year - gantt_min_year) * GPW
gantt_grid = ""
for y in range(gantt_min_year, gantt_max_year + 1):
xp = gx(y)
if y % 5 == 0:
gantt_grid += f'<line x1="{xp:.0f}" y1="{GPT}" x2="{xp:.0f}" y2="{GH-18}" stroke="#d5cfc7" stroke-width="0.5"/>'
gantt_grid += f'<text x="{xp:.0f}" y="{GH-4}" text-anchor="middle" font-size="7.5" fill="#637C87">{y}</text>'
else:
gantt_grid += f'<line x1="{xp:.0f}" y1="{GPT}" x2="{xp:.0f}" y2="{GH-18}" stroke="#ece7e0" stroke-width="0.3"/>'
today_x = gx(2026)
gantt_grid += f'<line x1="{today_x:.0f}" y1="{GPT}" x2="{today_x:.0f}" y2="{GH-18}" stroke="#FF6B6B" stroke-width="1" opacity="0.4"/>'
gantt_bars = ""
# Background panel behind project name labels
gantt_bars += f'<rect x="0" y="{GPT}" width="{GPL-4}" height="{GH-GPT-20}" fill="#f7f4f0" rx="0"/>'
gantt_bars += f'<line x1="{GPL-4}" y1="{GPT}" x2="{GPL-4}" y2="{GH-20}" stroke="#E0D9D3" stroke-width="0.5"/>'
y_pos = GPT
for i, row in enumerate(gantt_rows):
p = row["p"]
h = row["h"]
contracted = row["contracted"]
pw = str(p["pathway"])
name = str(p["name"])
cs = int(p["crediting_start"]) if p["crediting_start"] else 2024
ce = int(p["crediting_end"]) if p["crediting_end"] else 2035
color = pathway_colors_gantt.get(pw, "#637C87")
contracted_k = fmt_num(contracted, compact=True)
if i % 2 == 0:
gantt_bars += f'<rect x="{GPL}" y="{y_pos-1:.1f}" width="{GPW}" height="{h+2:.1f}" fill="rgba(240,235,228,0.3)" rx="0"/>'
name_display = name if len(name) <= 35 else name[:33] + "…"
gantt_bars += f'<text x="{GPL-12}" y="{y_pos+h/2+4:.1f}" text-anchor="end" font-size="8" fill="#022B3D">{name_display}</text>'
x1 = gx(max(cs, gantt_min_year))
x2 = gx(min(ce, gantt_max_year))
bar_w = max(x2 - x1, 4)
gantt_bars += f'<rect x="{x1:.1f}" y="{y_pos:.1f}" width="{bar_w:.1f}" height="{h:.1f}" rx="3" fill="{color}" opacity="0.78"/>'
# Always put label inside bar if bar is wide enough (>60px), regardless of height
if bar_w > 60:
gantt_bars += f'<text x="{x1+8:.1f}" y="{y_pos+h/2+4:.1f}" text-anchor="start" font-size="7.5" font-weight="600" fill="rgba(255,255,255,.95)" font-family="Space Mono,monospace">{contracted_k}</text>'
elif bar_w > 30:
gantt_bars += f'<text x="{x1+4:.1f}" y="{y_pos+h/2+4:.1f}" text-anchor="start" font-size="7" font-weight="600" fill="rgba(255,255,255,.9)" font-family="Space Mono,monospace">{contracted_k}</text>'
else:
gantt_bars += f'<text x="{x2+5:.1f}" y="{y_pos+h/2+4:.1f}" text-anchor="start" font-size="7.5" font-weight="600" fill="{color}" font-family="Space Mono,monospace">{contracted_k}</text>'
y_pos += h + row_gap
# Legend — sorted by frequency (most projects first)
# Legend is now HTML below the SVG
pw_counts = gantt_sorted["pathway"].value_counts()
# Legend below chart as HTML
gantt_legend_html = '<div style="display:flex;gap:14px;justify-content:center;padding:4px 16px 12px;flex-wrap:wrap">'
pw_counts = gantt_sorted["pathway"].value_counts()
for pw in pw_counts.index:
color = pathway_colors_gantt.get(pw, "#637C87")
abbr = pathway_short_gantt.get(pw, pw[:4])
gantt_legend_html += f'<div style="display:flex;align-items:center;gap:5px"><div style="width:10px;height:10px;border-radius:2px;background:{color};opacity:.82"></div><span style="font-size:11px;color:#637C87">{abbr}</span></div>'
gantt_legend_html += '</div>'
project_timeline_html = f'''<div class="gc-chart-card gc-chart-full" style="margin-bottom:16px">
<div class="gc-chart-header"><div class="gc-chart-title">Project Timelines</div></div>
<div style="overflow-x:auto;padding:0 16px 4px">
<svg viewBox="0 0 {GW} {GH:.0f}" preserveAspectRatio="xMidYMid meet" style="width:100%;height:auto;display:block;min-width:700px" xmlns="http://www.w3.org/2000/svg">
{gantt_grid}
{gantt_bars}
</svg>
</div>
{gantt_legend_html}
</div>'''
# Projects list for Projects tab (sorted by contracted desc)
projects_list_html = ""
for _, p in projects.sort_values("contracted_tco2", ascending=False).iterrows():
pid = p["project_id"]
pct_delivered = round(p["delivered_tco2"] / p["contracted_tco2"] * 100) if p["contracted_tco2"] > 0 else 0
projects_list_html += f'''<div class="gc-proj-row" onclick="showProjectDetail('{pid}')">
<div class="gc-proj-row-name"><strong>{p["name"]}</strong><span class="gc-proj-row-tags"><span class="gc-tag">{p["pathway"]}</span><span class="gc-tag gc-tag-outline">{p["country"]}</span></span></div>
<div class="gc-proj-row-stat"><span class="gc-proj-row-val">{fmt_num(int(p["contracted_tco2"]))}</span><span class="gc-proj-row-label">Contracted</span></div>
<div class="gc-proj-row-stat"><span class="gc-proj-row-val">{fmt_num(int(p["delivered_tco2"]))}</span><span class="gc-proj-row-label">Delivered</span></div>
<div class="gc-proj-row-stat"><span class="gc-proj-row-val">{fmt_num(int(p["open_tco2"]))}</span><span class="gc-proj-row-label">Open</span></div>
<div class="gc-proj-row-bar"><div class="gc-proj-row-fill-del" style="width:{pct_delivered}%"></div><div class="gc-proj-row-fill-open" style="width:{100-pct_delivered}%"></div></div>
<div class="gc-proj-row-arrow"><svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14"><path d="M6 4l4 4-4 4"/></svg></div>
</div>'''
# ============ RETIREMENTS TAB ============
total_issued = int(vintages["issued_tco2"].sum())
total_available = int(vintages["available_tco2"].sum())
total_retired_vint = int(vintages["retired_tco2"].sum())
total_retired_count = len(retirements)
unique_standards = list(projects["standard"].unique())
# KPI cards
ret_kpis = f'''<div class="gc-metrics-grid">
<div class="gc-metric-card"><div class="gc-metric-label">Total Issued</div><div class="gc-metric-value">{fmt_num(total_issued)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Retired</div><div class="gc-metric-value">{fmt_num(total_retired_vint)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Available to Retire</div><div class="gc-metric-value">{fmt_num(total_available)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Active Registries</div><div class="gc-metric-value">{len(unique_standards)}</div><div class="gc-metric-unit">registries</div></div>
</div>'''
# Distribution by Beneficiary (from retirements data)
ben_agg = retirements.groupby("beneficiary")["quantity_tco2"].sum().sort_values(ascending=False)
total_ret_qty = int(ben_agg.sum())
ben_bars_html = '<div style="padding:0 16px 16px">'
for ben, qty in ben_agg.items():
pct = round(qty / total_ret_qty * 100) if total_ret_qty > 0 else 0
bar_w = max(pct, 3)
ben_short = str(ben).replace("Siemens ", "")
ben_bars_html += f'''<div style="margin-bottom:10px">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<span style="font-size:12px;font-weight:500;color:#022B3D">{ben_short}</span>
<span style="font-size:11px;font-weight:600;color:#637C87">{fmt_num(int(qty))} tCO₂ · {pct}%</span>
</div>
<div style="height:20px;background:#F0EBE4;border-radius:6px;overflow:hidden">
<div style="height:100%;width:{bar_w}%;background:#00515D;border-radius:6px;opacity:.78"></div>
</div>
</div>'''
ben_bars_html += '</div>'
# Distribution by Purpose
purp_agg = retirements.groupby("purpose")["quantity_tco2"].sum().sort_values(ascending=False)
purp_bars_html = '<div style="padding:0 16px 16px">'
for purp, qty in purp_agg.items():
pct = round(qty / total_ret_qty * 100) if total_ret_qty > 0 else 0
bar_w = max(pct, 3)
purp_bars_html += f'''<div style="margin-bottom:10px">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<span style="font-size:12px;font-weight:500;color:#022B3D">{purp}</span>
<span style="font-size:11px;font-weight:600;color:#637C87">{fmt_num(int(qty))} tCO₂ · {pct}%</span>
</div>
<div style="height:20px;background:#F0EBE4;border-radius:6px;overflow:hidden">
<div style="height:100%;width:{bar_w}%;background:#87C314;border-radius:6px;opacity:.78"></div>
</div>
</div>'''
purp_bars_html += '</div>'
# Vintage Holdings Table (grouped by project)
proj_vintage = vintages.merge(projects[["project_id","name","standard"]], on="project_id", how="left")
proj_vint_agg = proj_vintage.groupby(["project_id","name","standard"]).agg(
issued=("issued_tco2","sum"), available=("available_tco2","sum"), retired=("retired_tco2","sum")
).sort_values("issued", ascending=False).reset_index()
holdings_rows = ""
for _, r in proj_vint_agg.iterrows():
pct_ret = round(r["retired"] / r["issued"] * 100) if r["issued"] > 0 else 0
holdings_rows += f'''<tr>
<td style="font-weight:600">{r["name"]}</td>
<td><span class="gc-tag" style="font-size:10px">{r["standard"]}</span></td>
<td style="text-align:right;font-weight:600">{fmt_num(int(r["issued"]))}</td>
<td style="text-align:right">{fmt_num(int(r["available"]))}</td>
<td style="text-align:right">{fmt_num(int(r["retired"]))}</td>
<td style="text-align:right"><div style="display:flex;align-items:center;gap:8px;justify-content:flex-end">
<div style="width:60px;height:6px;background:#F0EBE4;border-radius:3px;overflow:hidden"><div style="height:100%;width:{pct_ret}%;background:#00515D;border-radius:3px"></div></div>
<span style="font-size:11px;color:#637C87">{pct_ret}%</span>
</div></td>
</tr>'''
holdings_table = f'''<div class="gc-chart-card gc-chart-full" style="margin-bottom:16px">
<div class="gc-chart-header"><div class="gc-chart-title">Vintage Holdings by Project</div></div>
<div style="overflow-x:auto;padding:0 16px 16px">
<table class="gc-mon-table" style="table-layout:auto">
<thead><tr>
<th style="text-align:left;width:auto">Project</th>
<th style="text-align:left;width:100px">Registry</th>
<th style="text-align:right;width:90px">Issued</th>
<th style="text-align:right;width:80px">Available</th>
<th style="text-align:right;width:70px">Retired</th>
<th style="text-align:right;width:100px">Progress</th>
</tr></thead>
<tbody>{holdings_rows}</tbody>
</table>
</div>
</div>'''
# Retirement Activity Table (individual retirements)
ret_sorted = retirements.sort_values("date", ascending=False)
ret_rows = ""
proj_name_map = dict(zip(projects["project_id"], projects["name"]))
for _, r in ret_sorted.iterrows():
proj_name = proj_name_map.get(r["project_id"], r["project_id"])
date_str = str(r["date"])[:10]
ret_rows += f'''<tr>
<td>{date_str}</td>
<td style="font-weight:600">{r["beneficiary"]}</td>
<td>{proj_name}</td>
<td>{r["purpose"]}</td>
<td style="text-align:right;font-weight:600">{fmt_num(int(r["quantity_tco2"]))}</td>
<td style="font-size:11px;color:#637C87;font-family:monospace">{r["serial_number"]}</td>
</tr>'''
retirement_table = f'''<div class="gc-chart-card gc-chart-full">
<div class="gc-chart-header"><div class="gc-chart-title">Retirement History</div></div>
<div style="overflow-x:auto;padding:0 16px 16px">
<table class="gc-mon-table" style="table-layout:auto">
<thead><tr>
<th style="text-align:left;width:100px">Date</th>
<th style="text-align:left;width:auto">Beneficiary</th>
<th style="text-align:left;width:auto">Project</th>
<th style="text-align:left;width:auto">Purpose</th>
<th style="text-align:right;width:80px">Quantity</th>
<th style="text-align:left;width:160px">Serial Number</th>
</tr></thead>
<tbody>{ret_rows}</tbody>
</table>
</div>
</div>'''
retirements_content = f'''{ret_kpis}
<div class="gc-charts-grid">
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Retirements by Beneficiary</div></div>{ben_bars_html}</div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Retirements by Purpose</div></div>{purp_bars_html}</div>
</div>
{holdings_table}
{retirement_table}'''
html = f"""
<div id="gcRoot" style="height:100vh;max-height:100vh;overflow:hidden;position:fixed;top:0;left:0;right:0;bottom:0;z-index:999;">
<!-- LOGIN -->
<div class="gc-login-screen" id="gcLogin">
<div class="gc-login-bg"><div class="gc-login-orb gc-login-orb-1"></div><div class="gc-login-orb gc-login-orb-2"></div><div class="gc-login-orb gc-login-orb-3"></div></div>
<div class="gc-login-card">
<div class="gc-login-header"><div class="gc-login-logo"><img src="{LOGO_GC}" alt="goodcarbon" class="gc-login-logo-img"/></div></div>
<div class="gc-login-body">
<h1 class="gc-login-title">Welcome back</h1><p class="gc-login-subtitle">Sign in to your NbS Portfolio</p>
<div class="gc-login-form">
<div class="gc-login-field"><label>Email</label><div class="gc-login-input-wrap"><input type="email" id="loginEmail" value="anna.schlusche@siemens.com" onkeydown="if(event.key==='Enter')attemptLogin()"/></div></div>
<div class="gc-login-field"><label>Password</label><div class="gc-login-input-wrap"><input type="password" id="loginPassword" value="goodcarbon" onkeydown="if(event.key==='Enter')attemptLogin()"/></div></div>
<div class="gc-login-error" id="loginError">Invalid credentials</div>
<button class="gc-login-btn" onclick="attemptLogin()" id="loginBtn"><span id="loginBtnText">Sign In</span><div class="gc-login-spinner" id="loginSpinner"></div></button>
</div>
</div>
<div class="gc-login-footer"><span>Powered by goodcarbon</span></div>
</div>
</div>
<!-- APP -->
<div class="gc-app" id="gcApp" style="display:none">
<aside class="gc-sidebar"><div class="gc-sidebar-inner">
<div class="gc-logo"><img src="{LOGO_GC}" alt="goodcarbon" class="gc-sidebar-logo-img"/></div>
<div class="gc-user"><img src="{LOGO_SIEMENS}" alt="Siemens" class="gc-user-siemens-logo"/></div>
<nav class="gc-nav">
<a class="gc-nav-item active" href="#" onclick="switchView('overview',this);return false"><svg viewBox="0 0 20 20" width="18" height="18" fill="currentColor"><path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/></svg>Overview</a>
<a class="gc-nav-item" href="#" onclick="switchView('projects',this);return false"><svg viewBox="0 0 20 20" width="18" height="18" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"/></svg>Projects</a>
<a class="gc-nav-item" href="#" onclick="switchView('monitoring',this);return false"><svg viewBox="0 0 20 20" width="18" height="18" fill="currentColor"><path fill-rule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clip-rule="evenodd"/></svg>Monitoring</a>
<a class="gc-nav-item" href="#" onclick="switchView('retirements',this);return false"><svg viewBox="0 0 20 20" width="18" height="18" fill="currentColor"><path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/><path fill-rule="evenodd" d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm9.707 5.707a1 1 0 00-1.414-1.414L9 12.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>Retirements</a>
<a class="gc-nav-item" href="#" onclick="switchView('analytics',this);return false"><svg viewBox="0 0 20 20" width="18" height="18" fill="currentColor"><path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"/><path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"/></svg>Pro View</a>
</nav>
<div class="gc-sidebar-footer"><div class="gc-powered">Powered by goodcarbon</div></div>
</div></aside>
<main class="gc-main" id="gcMain">
<!-- OVERVIEW -->
<div id="viewOverview" class="gc-view gc-view-active">
<div class="gc-header"><h2><span class="gc-title-prefix">Siemens</span> <span class="gc-title-sep">·</span> CDR Overview</h2></div>
<div class="gc-content">
<div class="gc-metrics-grid">
<div class="gc-metric-card"><div class="gc-metric-label">Total Volume Contracted</div><div class="gc-metric-value">{fmt_num(total_contracted)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Delivered</div><div class="gc-metric-value">{fmt_num(total_delivered)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Open</div><div class="gc-metric-value">{fmt_num(total_open)}</div><div class="gc-metric-unit">tCO₂</div></div>
<div class="gc-metric-card"><div class="gc-metric-label">Active Projects</div><div class="gc-metric-value">{num_projects}</div><div class="gc-metric-unit">projects</div></div>
</div>
<div class="gc-charts-grid">
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Portfolio Distribution by Project</div></div>{dist_bars_html}<div class="gc-legend"><div class="gc-legend-item"><div class="gc-legend-color gc-bg-lime"></div><div class="gc-legend-label">Delivered</div><div class="gc-legend-val">{fmt_num(total_delivered)} tCO₂</div></div><div class="gc-legend-item"><div class="gc-legend-color gc-bg-open"></div><div class="gc-legend-label">Open</div><div class="gc-legend-val">{fmt_num(total_open)} tCO₂</div></div></div></div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Geographic Distribution</div></div>{geo_map_html}<div class="gc-legend">{region_legend}</div></div>
</div>
<div class="gc-chart-card gc-chart-full"><div class="gc-chart-header"><div class="gc-chart-title">Delivery Timeline by Year</div></div><div class="gc-timeline-chart"><div class="gc-timeline-y"><div>{max_year_total//1000}K</div><div>{max_year_total*3//4//1000}K</div><div>{max_year_total//2//1000}K</div><div>{max_year_total//4//1000}K</div><div>0</div></div><div class="gc-timeline-bars">{timeline_html}</div></div></div>
<div class="gc-charts-grid">
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Portfolio Distribution by Pathway</div></div>{pathway_bars_html}</div>
<div class="gc-chart-card"><div class="gc-chart-header"><div class="gc-chart-title">Recent Activity</div></div>{activity_html}</div>
</div>
</div>
</div>
<!-- PROJECTS -->
<div id="viewProjects" class="gc-view">
<div class="gc-header"><h2><span class="gc-title-prefix">Siemens</span> <span class="gc-title-sep">·</span> CDR Projects ({num_projects})</h2></div>
<div class="gc-content">{project_timeline_html}<div class="gc-proj-list">{projects_list_html}</div></div>
</div>
<!-- MONITORING (placeholder) -->
<div id="viewMonitoring" class="gc-view"><div class="gc-header"><h2><span class="gc-title-prefix">Siemens</span> <span class="gc-title-sep">·</span> CDR Monitoring</h2></div><div class="gc-content gc-mon-content">{monitoring_content}</div></div>
<!-- RETIREMENTS -->
<div id="viewRetirements" class="gc-view"><div class="gc-header"><h2><span class="gc-title-prefix">Siemens</span> <span class="gc-title-sep">·</span> CDR Retirements</h2></div><div class="gc-content">{retirements_content}</div></div>
<div id="viewAnalytics" class="gc-view"><div class="gc-header"><h2><span class="gc-title-prefix">Siemens</span> <span class="gc-title-sep">·</span> Portfolio Pro View</h2></div><div class="gc-content">{pro_view_html}</div></div>
<!-- PROJECT DETAIL -->
<div id="viewDetail" class="gc-view"><div id="detailContent"></div></div>
</main>
</div>
</div>
<script>
const PROJECTS = {projects_json};
const SDG_ICONS = {sdg_icons_json};
/* Consistent number formatter (JS-side) */
function gcFmt(n, compact) {{
n = Number(n);
if (compact) {{
if (n >= 1e6) return (n/1e6).toFixed(1) + 'M';
if (n >= 1e3) return Math.round(n/1e3) + 'K';
return n.toString();
}}
return n.toLocaleString('en-US');
}}
/* Bar animation: animate all data-width and data-height elements */
function gcAnimateBars(container) {{
if (!container) container = document;
/* Horizontal bars */
container.querySelectorAll('[data-width]').forEach(function(el, i) {{
setTimeout(function() {{
el.style.width = el.getAttribute('data-width');
}}, 80 + i * 30);
}});
/* Vertical bars (timeline) */
container.querySelectorAll('[data-height]').forEach(function(el, i) {{
setTimeout(function() {{
el.style.height = el.getAttribute('data-height');
}}, 120 + i * 60);
}});
}}
function switchView(name, el) {{
document.querySelectorAll('.gc-view').forEach(v=>v.classList.remove('gc-view-active'));
var map = {{'overview':'viewOverview','projects':'viewProjects','monitoring':'viewMonitoring','analytics':'viewAnalytics','retirements':'viewRetirements'}};
if(map[name]) {{
var view = document.getElementById(map[name]);
view.classList.add('gc-view-active');
/* Trigger bar animations on tab switch */
setTimeout(function() {{ gcAnimateBars(view); }}, 200);
}}
document.querySelectorAll('.gc-nav-item').forEach(n=>n.classList.remove('active'));
if(el) el.classList.add('active');
document.getElementById('gcMain').scrollTop=0;
}}
function showProjectDetail(id) {{
var p = PROJECTS.find(x=>x.id===id);
if(!p) return;
var pctDel = p.contracted>0 ? Math.round(p.delivered/p.contracted*100) : 0;
var sdgHtml = p.sdg_goals.map(g => SDG_ICONS[g] ? '<img src="'+SDG_ICONS[g]+'" class="gc-sdg-icon"/>' : '').join('');
var mediaHtml = p.media.length > 0 ? '<div class="gc-detail-section"><h3 class="gc-detail-section-title">Project Media</h3><div class="gc-media-grid">' + p.media.map(m=>'<img src="'+m+'" class="gc-media-img" loading="lazy"/>').join('') + '</div></div>' : '';
var vintageRows = (p.vintages||[]).map(v => '<tr><td><strong>'+v.vintage_year+'</strong></td><td>'+gcFmt(v.issued_tco2)+'</td><td>'+gcFmt(v.available_tco2)+'</td><td>'+gcFmt(v.retired_tco2)+'</td><td class="gc-price">€'+Number(v.price_eur).toFixed(2)+'</td></tr>').join('');
var retRows = (p.retirements||[]).map(r => '<div class="gc-retirement-item"><div class="gc-retirement-main"><div class="gc-retirement-qty">'+gcFmt(r.quantity_tco2)+' tCO₂</div><div class="gc-retirement-details">Vintage '+r.vintage_year+' &middot; '+r.beneficiary+'</div><div class="gc-retirement-serial">'+r.serial_number+'</div></div><div class="gc-retirement-date">'+r.date+'</div></div>').join('');
document.getElementById('detailContent').innerHTML = '<div class="gc-header"><h2><button class="gc-back-btn" onclick="backToProjects()"><svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14"><path d="M10 12L6 8l4-4"/></svg></button>'+p.name+'</h2></div><div class="gc-content"><div class="gc-detail-top"><div class="gc-detail-left"><div class="gc-detail-tags"><span class="gc-tag gc-tag-primary">'+p.pathway+'</span></div><div class="gc-detail-tags"><span class="gc-tag gc-tag-outline">'+p.country+'</span><span class="gc-tag gc-tag-outline">'+p.mechanism+'</span><span class="gc-tag gc-tag-outline">'+p.standard+'</span></div><p class="gc-detail-desc">'+p.description+'</p><div class="gc-detail-meta-grid"><div class="gc-detail-meta"><span class="gc-meta-label">Project Type</span><span class="gc-meta-value">'+p.project_type+'</span></div><div class="gc-detail-meta"><span class="gc-meta-label">Registry</span><span class="gc-meta-value">'+p.standard+' — '+p.registry_id+'</span></div><div class="gc-detail-meta"><span class="gc-meta-label">Area</span><span class="gc-meta-value">'+(p.area_ha>0?gcFmt(p.area_ha)+' ha':'N/A')+'</span></div><div class="gc-detail-meta"><span class="gc-meta-label">Crediting Period</span><span class="gc-meta-value">'+p.crediting_start+' – '+p.crediting_end+'</span></div></div></div><div class="gc-detail-right"><div class="gc-detail-kpis"><div class="gc-detail-kpi"><span class="gc-detail-kpi-val">'+gcFmt(p.contracted)+'</span><span class="gc-detail-kpi-label">Contracted tCO₂</span></div><div class="gc-detail-kpi gc-kpi-green"><span class="gc-detail-kpi-val">'+gcFmt(p.delivered)+'</span><span class="gc-detail-kpi-label">Delivered tCO₂</span></div><div class="gc-detail-kpi gc-kpi-lime"><span class="gc-detail-kpi-val">'+gcFmt(p.open)+'</span><span class="gc-detail-kpi-label">Open tCO₂</span></div></div><div class="gc-detail-bar-wrap"><div class="gc-detail-bar-track"><div class="gc-detail-bar-fill" style="width:'+pctDel+'%"></div><div class="gc-detail-bar-open" style="width:'+(100-pctDel)+'%"></div></div><span class="gc-detail-bar-pct">'+pctDel+'% delivered</span></div>'+(sdgHtml?'<div class="gc-detail-section"><h3 class="gc-detail-section-title">UN Sustainable Development Goals</h3><div class="gc-sdg-grid">'+sdgHtml+'</div></div>':'')+'</div></div>'+mediaHtml+(p.full_description?'<div class="gc-detail-section"><h3 class="gc-detail-section-title">Full Description</h3><p class="gc-detail-fulldesc">'+p.full_description+'</p></div>':'')+(vintageRows?'<div class="gc-detail-section"><h3 class="gc-detail-section-title">Vintage Breakdown</h3><table class="gc-vintage-table"><thead><tr><th>Vintage</th><th>Issued</th><th>Available</th><th>Retired</th><th>Price</th></tr></thead><tbody>'+vintageRows+'</tbody></table></div>':'')+(retRows||true?'<div class="gc-detail-bottom-grid"><div class="gc-detail-section">'+(retRows?'<h3 class="gc-detail-section-title">Retirement History</h3><div class="gc-retirement-list">'+retRows+'</div>':'')+'</div><div class="gc-detail-section"><h3 class="gc-detail-section-title">Download Section</h3><div class="gc-downloads-list"><a class="gc-download-item" href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" target="_blank"><div class="gc-download-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 18 15 15"/></svg></div><div class="gc-download-text"><span class="gc-download-name">Project Summary</span><span class="gc-download-meta">PDF · Overview &amp; Key Metrics</span></div><div class="gc-download-arrow"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg></div></a><a class="gc-download-item" href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" target="_blank"><div class="gc-download-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 18 15 15"/></svg></div><div class="gc-download-text"><span class="gc-download-name">Project Review (last quarter)</span><span class="gc-download-meta">PDF · Q4 2025 Performance Report</span></div><div class="gc-download-arrow"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg></div></a><a class="gc-download-item" href="https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf" target="_blank"><div class="gc-download-icon"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 18 15 15"/></svg></div><div class="gc-download-text"><span class="gc-download-name">Project Review (last year)</span><span class="gc-download-meta">PDF · Annual Report 2024</span></div><div class="gc-download-arrow"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg></div></a></div></div></div>':'')+'</div>';
document.querySelectorAll('.gc-view').forEach(v=>v.classList.remove('gc-view-active'));
document.getElementById('viewDetail').classList.add('gc-view-active');
document.getElementById('gcMain').scrollTop=0;
}}
function backToProjects() {{
document.getElementById('viewDetail').classList.remove('gc-view-active');
document.getElementById('viewProjects').classList.add('gc-view-active');
}}
function toggleDimDetail(dim) {{
for(var i=1;i<=5;i++){{
var el=document.getElementById('gcDimDetail'+i);
if(el){{
if(i===dim){{el.style.display=el.style.display==='none'?'block':'none';}}
else{{el.style.display='none';}}
}}
}}
}}
function showProjectMonitoring(id) {{
var p = PROJECTS.find(x=>x.id===id);
if(!p || !p.monitoring) return;
var m = p.monitoring;
var dimNames = {{1:'Carbon Performance',2:'Ecological Impact',3:'Community Impact',4:'Operational Health',5:'Risk & Permanence'}};
var dimKeys = {{1:'dim1',2:'dim2',3:'dim3',4:'dim4',5:'dim5'}};
var dimColors = {{green:'#2F7A47',amber:'#D4A017',red:'#C24B3A'}};
/* Overall score */
var total = 0, cnt = 0;
for (var d=1;d<=5;d++) {{ var v=m['dim'+d]; if(v) {{ total+=v; cnt++; }} }}
var overall = cnt>0 ? Math.round(total/cnt) : 0;
var oc = overall>=80?'green':overall>=60?'amber':'red';
var ol = overall>=80?'ON TRACK':overall>=60?'WATCH':'ALERT';
/* SVG donut for overall score */
var donutR = 54, donutC = 251.3*2*54/80;
var donutCirc = 2 * Math.PI * donutR;
var donutFill = donutCirc * overall / 100;
var donutColor = dimColors[oc];
var donutSvg = '<svg viewBox="0 0 140 140" class="gc-pmd-donut-svg">' +
'<circle cx="70" cy="70" r="'+donutR+'" fill="none" stroke="#F0EBE4" stroke-width="10"/>' +
'<circle cx="70" cy="70" r="'+donutR+'" fill="none" stroke="'+donutColor+'" stroke-width="10" stroke-dasharray="'+donutFill.toFixed(1)+' '+donutCirc.toFixed(1)+'" stroke-dashoffset="0" transform="rotate(-90 70 70)" stroke-linecap="round"/>' +
'<text x="70" y="64" text-anchor="middle" font-size="28" font-weight="700" fill="'+donutColor+'" font-family="Source Serif 4,Georgia,serif">'+overall+'%</text>' +
'<text x="70" y="82" text-anchor="middle" font-size="9" font-weight="600" fill="'+donutColor+'" letter-spacing="0.5" font-family="Inter,system-ui,sans-serif">'+ol+'</text>' +
'</svg>';
/* Dimension cards row */
var dimCardsHtml = '';
for (var d=1;d<=5;d++) {{
var val = m['dim'+d] || 0;
var dc = val>=80?'green':val>=60?'amber':'red';
var dt = val>=80?'ON TRACK':val>=60?'WATCH':'ALERT';
var badgeCls = val>=80?'gc-badge-green':val>=60?'gc-badge-amber':'gc-badge-red';
dimCardsHtml += '<div class="gc-pmd-dim-card gc-dim-'+dc+'">' +
'<div class="gc-dim-name-row"><span class="gc-dim-name">'+dimNames[d]+'</span><span class="'+badgeCls+'">'+dt+'</span></div>' +
'<div class="gc-dim-score">'+val+'%</div></div>';
}}
/* KPI tables grouped by dimension */
var kpiHtml = '';
for (var d=1;d<=5;d++) {{
var dk = dimKeys[d];
var kpis = (p.kpis||[]).filter(function(k){{ return k.dimension===dk; }});
if (kpis.length===0) continue;
var dimVal = m['dim'+d] || 0;
var dsc = dimVal>=80?'green':dimVal>=60?'amber':'red';
kpiHtml += '<div class="gc-pmd-dim-section">';
kpiHtml += '<div class="gc-pmd-dim-heading"><span class="gc-pmd-dim-num">'+d+'</span><span>'+dimNames[d]+'</span><span class="gc-status-label gc-sl-'+dsc+'" style="margin-left:auto;font-size:11px">'+dimVal+'%</span></div>';
kpiHtml += '<table class="gc-mon-table gc-pmd-kpi-table"><thead><tr><th>KPI</th><th>Score</th><th>Target</th><th>Freq.</th><th>Source</th></tr></thead><tbody>';
kpis.forEach(function(k) {{
var sc = k.score>=80?'green':k.score>=60?'amber':'red';
kpiHtml += '<tr><td class="gc-mon-proj-name">'+k.kpi+'</td>' +
'<td><span class="gc-pmd-score gc-pmd-sc-'+sc+'">'+k.score+'%</span></td>' +
'<td class="gc-pmd-td-target">'+k.target+'</td><td>'+k.frequency.replace('Semi-annually','Semi-ann.')+'</td><td>'+k.source+'</td></tr>';
}});
kpiHtml += '</tbody></table></div>';
}}
/* Alarms for this project */
var alarmsHtml = '';
var sevColors = {{critical:'#C24B3A',alert:'#D4A017',watch:'#2F7A47'}};
if (p.alarms && p.alarms.length > 0) {{
alarmsHtml = '<div class="gc-pmd-alarms-card"><div class="gc-chart-header"><div class="gc-chart-title">Active Alarms</div><span class="gc-alarm-count">'+p.alarms.length+' Active</span></div>';
p.alarms.forEach(function(a) {{
var color = sevColors[a.severity]||'#637C87';
alarmsHtml += '<div class="gc-pmd-alarm-row"><div class="gc-alarm-dot" style="background:'+color+'"></div>' +
'<div class="gc-pmd-alarm-text"><strong>'+a.kpi+'</strong><span class="gc-pmd-alarm-desc">'+a.description+'</span></div>' +
'<span class="gc-alarm-sev" style="color:'+color+'">'+String(a.severity).toUpperCase()+'</span>' +
'<span class="gc-alarm-date">'+a.date+'</span></div>';
}});
alarmsHtml += '</div>';
}}
/* Radar chart (SVG pentagon) */
var cx = 100, cy = 105, maxR = 75;
var angles = [-90, -18, 54, 126, 198].map(function(a){{ return a * Math.PI / 180; }});
var dimLabels = ['Carbon','Ecological','Community','Operational','Risk'];
var radarSvg = '<svg viewBox="0 0 200 220" class="gc-pmd-radar-svg" xmlns="http://www.w3.org/2000/svg">';
/* Grid rings with labels */
[0.25,0.5,0.75,1.0].forEach(function(s) {{
var pts = angles.map(function(a){{ return (cx+Math.cos(a)*maxR*s).toFixed(1)+','+(cy+Math.sin(a)*maxR*s).toFixed(1); }}).join(' ');
radarSvg += '<polygon points="'+pts+'" fill="none" stroke="#E0D9D3" stroke-width="0.5"/>';
}});
angles.forEach(function(a,i) {{
radarSvg += '<line x1="'+cx+'" y1="'+cy+'" x2="'+(cx+Math.cos(a)*maxR).toFixed(1)+'" y2="'+(cy+Math.sin(a)*maxR).toFixed(1)+'" stroke="#E0D9D3" stroke-width="0.3"/>';
var lx = cx + Math.cos(a)*(maxR+16);
var ly = cy + Math.sin(a)*(maxR+16);
radarSvg += '<text x="'+lx.toFixed(1)+'" y="'+ly.toFixed(1)+'" text-anchor="middle" dominant-baseline="central" font-size="8" font-weight="500" fill="#637C87" font-family="Inter,system-ui,sans-serif">'+dimLabels[i]+'</text>';
}});
/* Data polygon */
var vals = [m.dim1||0,m.dim2||0,m.dim3||0,m.dim4||0,m.dim5||0];
var dataPts = angles.map(function(a,i){{ return (cx+Math.cos(a)*maxR*vals[i]/100).toFixed(1)+','+(cy+Math.sin(a)*maxR*vals[i]/100).toFixed(1); }}).join(' ');
radarSvg += '<polygon points="'+dataPts+'" fill="rgba(0,81,93,0.12)" stroke="#00515D" stroke-width="2"/>';
vals.forEach(function(v,i) {{
var px = cx+Math.cos(angles[i])*maxR*v/100;
var py = cy+Math.sin(angles[i])*maxR*v/100;
radarSvg += '<circle cx="'+px.toFixed(1)+'" cy="'+py.toFixed(1)+'" r="3.5" fill="#00515D" stroke="#fff" stroke-width="1"/>';
}});
radarSvg += '</svg>';
/* ============ ASSEMBLE PAGE ============ */
var html = '<div class="gc-header"><h2><button class="gc-back-btn" onclick="backToMonitoring()"><svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14"><path d="M10 12L6 8l4-4"/></svg></button>' +
'<span class="gc-title-prefix">Monitoring</span> <span class="gc-title-sep">·</span> ' + p.name + '</h2></div>';
html += '<div class="gc-content">';
/* Top row: donut + meta + radar */
html += '<div class="gc-pmd-top">';
html += '<div class="gc-pmd-hero">';
html += '<div class="gc-pmd-hero-left">'+donutSvg+'</div>';
html += '<div class="gc-pmd-hero-meta">';
html += '<div class="gc-pmd-tags"><span class="gc-tag gc-tag-primary" style="width:auto">'+p.pathway+'</span><span class="gc-tag gc-tag-outline">'+p.country+'</span><span class="gc-tag gc-tag-outline">'+p.standard+'</span></div>';
html += '<div class="gc-pmd-meta-grid">';
html += '<div class="gc-detail-meta"><span class="gc-meta-label">Contracted</span><span class="gc-meta-value">'+gcFmt(p.contracted)+' tCO₂</span></div>';
html += '<div class="gc-detail-meta"><span class="gc-meta-label">Delivered</span><span class="gc-meta-value">'+gcFmt(p.delivered)+' tCO₂</span></div>';
html += '<div class="gc-detail-meta"><span class="gc-meta-label">Registry</span><span class="gc-meta-value">'+p.registry_id+'</span></div>';
html += '<div class="gc-detail-meta"><span class="gc-meta-label">Crediting</span><span class="gc-meta-value">'+p.crediting_start+' – '+p.crediting_end+'</span></div>';
html += '</div></div></div>';
html += '<div class="gc-pmd-radar">'+radarSvg+'</div>';
html += '</div>';
/* Dimension score cards */
html += '<div class="gc-pmd-dims">'+dimCardsHtml+'</div>';
/* Alarms (in card) */
html += alarmsHtml;
/* KPI detail tables */
html += '<div class="gc-pmd-section"><h3 class="gc-detail-section-title">KPI Detail by Dimension</h3>'+kpiHtml+'</div>';
html += '</div>';
document.getElementById('detailContent').innerHTML = html;
document.querySelectorAll('.gc-view').forEach(v=>v.classList.remove('gc-view-active'));
document.getElementById('viewDetail').classList.add('gc-view-active');
document.getElementById('gcMain').scrollTop=0;
}}
function backToMonitoring() {{
document.getElementById('viewDetail').classList.remove('gc-view-active');
document.getElementById('viewMonitoring').classList.add('gc-view-active');
}}
function attemptLogin() {{
var email=document.getElementById('loginEmail').value.trim().toLowerCase();
var pw=document.getElementById('loginPassword').value;
var err=document.getElementById('loginError');
var btn=document.getElementById('loginBtn');
err.classList.remove('gc-login-error-show');
document.getElementById('loginBtnText').style.opacity='0';
document.getElementById('loginSpinner').style.display='block';
btn.style.pointerEvents='none';
setTimeout(function(){{
if((email==='carbon@siemens.com'&&pw==='goodcarbon')||(email==='anna.schlusche@siemens.com'&&pw==='goodcarbon')){{
var ls=document.getElementById('gcLogin');ls.classList.add('gc-login-exit');
setTimeout(function(){{ls.style.display='none';var app=document.getElementById('gcApp');app.style.display='flex';app.classList.add('gc-app-enter');setTimeout(function(){{gcAnimateBars(document.getElementById('viewOverview'));}},300);}},500);
}}else{{
document.getElementById('loginSpinner').style.display='none';
document.getElementById('loginBtnText').style.opacity='1';
btn.style.pointerEvents='auto';
err.textContent='Invalid email or password';err.classList.add('gc-login-error-show');
}}
}},1200);
}}
/* Reveal login card only after fonts are fully loaded to prevent layout shift */
(function(){{
var revealed=false;
function revealCard(){{
if(revealed) return;
revealed=true;
var card=document.querySelector('.gc-login-card');
if(card) card.style.animation='cardEnter .6s cubic-bezier(.4,0,.2,1) forwards';
}}
function checkFonts(){{
if(document.fonts&&document.fonts.check){{
return document.fonts.check('600 1em Inter')&&document.fonts.check('700 1em "Source Serif 4"');
}}
return false;
}}
/* Poll for fonts up to 3s */
var attempts=0;
(function poll(){{
if(checkFonts()||attempts>30){{revealCard();return;}}
attempts++;
setTimeout(poll,100);
}})();
/* Hard fallback */
setTimeout(revealCard,3000);
}})();
/* ===== PRO VIEW ===== */
function switchProView(view, btn) {{
document.querySelectorAll('.gc-pv-toggle').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.getElementById('pvProject').style.display = view === 'project' ? 'block' : 'none';
document.getElementById('pvPathway').style.display = view === 'pathway' ? 'block' : 'none';
if (view === 'project' && !window._pvPiesDrawn) {{ initPVPies(); window._pvPiesDrawn = true; }}
}}
function initPVChart() {{
var c = document.getElementById('pvAreaChart');
if (!c) return;
var cx = c.getContext('2d');
var dpr = window.devicePixelRatio || 1;
var r = c.parentElement.getBoundingClientRect();
c.width = r.width * dpr; c.height = r.height * dpr;
cx.scale(dpr, dpr);
c.style.width = r.width + 'px'; c.style.height = r.height + 'px';
var w = r.width, h = r.height;
var pad = {{top:16,right:16,bottom:32,left:52}};
var years = [2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040];
var coreSoc=[10,12,12,11,10,9,0,0,0,0,0], coreWet=[10,11,10,10,9,9,0,0,0,0,0], coreAR=[20,18,18,17,16,15,0,0,0,0,0];
var topupSoc=[0,3,4,4,5,5,0,0,0,0,0], topupWet=[0,3,3,3,4,4,0,0,0,0,0], topupAR=[0,4,5,6,7,7,0,0,0,0,0];
var extSoc=[0,0,0,0,0,0,14,14,13,12,11], extWet=[0,0,0,0,0,0,13,13,12,12,10], extAR=[0,0,0,0,0,0,22,21,21,19,19];
var maxY = 80;
function gx(i){{ return pad.left + (i/(years.length-1)) * (w-pad.left-pad.right); }}
function gy(v){{ return pad.top + (h-pad.top-pad.bottom) - (v/maxY)*(h-pad.top-pad.bottom); }}
var stack = years.map(function(_,i){{
var cs=coreSoc[i], cw=cs+coreWet[i], ca=cw+coreAR[i];
var ts=ca+topupSoc[i], tw=ts+topupWet[i], ta=tw+topupAR[i];
var es=ta+extSoc[i], ew=es+extWet[i], ea=ew+extAR[i];
return {{cs:cs,cw:cw,ca:ca,ts:ts,tw:tw,ta:ta,es:es,ew:ew,ea:ea}};
}});
cx.strokeStyle='#e4ded6'; cx.lineWidth=1;
for(var v=0;v<=maxY;v+=10){{ var y=gy(v); cx.beginPath(); cx.moveTo(pad.left,y); cx.lineTo(w-pad.right,y); cx.stroke(); }}
cx.fillStyle='#637C87'; cx.font='10px Inter,system-ui,sans-serif'; cx.textAlign='right'; cx.textBaseline='middle';
for(var v=0;v<=maxY;v+=20) cx.fillText(v+'k',pad.left-6,gy(v));
cx.textAlign='center'; cx.textBaseline='top';
years.forEach(function(yr,i){{ cx.fillText(yr,gx(i),h-pad.bottom+8); }});
function fillA(gb,gt,color){{
cx.beginPath();
for(var i=0;i<years.length;i++){{ var x=gx(i),y=gy(gt(i)); i===0?cx.moveTo(x,y):cx.lineTo(x,y); }}
for(var i=years.length-1;i>=0;i--) cx.lineTo(gx(i),gy(gb(i)));
cx.closePath(); cx.fillStyle=color; cx.fill();
}}
fillA(function(i){{return 0}},function(i){{return stack[i].cs}},'#c4a03a');
fillA(function(i){{return stack[i].cs}},function(i){{return stack[i].cw}},'#2e7da6');
fillA(function(i){{return stack[i].cw}},function(i){{return stack[i].ca}},'#0e7e6e');
fillA(function(i){{return stack[i].ca}},function(i){{return stack[i].ts}},'rgba(218,190,92,0.3)');
fillA(function(i){{return stack[i].ts}},function(i){{return stack[i].tw}},'rgba(92,163,200,0.3)');
fillA(function(i){{return stack[i].tw}},function(i){{return stack[i].ta}},'rgba(58,168,142,0.3)');
fillA(function(i){{return stack[i].ta}},function(i){{return stack[i].es}},'rgba(218,190,92,0.15)');
fillA(function(i){{return stack[i].es}},function(i){{return stack[i].ew}},'rgba(92,163,200,0.15)');
fillA(function(i){{return stack[i].ew}},function(i){{return stack[i].ea}},'rgba(58,168,142,0.15)');
cx.beginPath(); cx.strokeStyle='#00515D'; cx.lineWidth=2; cx.setLineDash([]);
for(var i=0;i<years.length;i++){{ var x=gx(i),y=gy(stack[i].ca); i===0?cx.moveTo(x,y):cx.lineTo(x,y); }} cx.stroke();
cx.beginPath(); cx.strokeStyle='#3aa88e'; cx.lineWidth=1.5;
for(var i=0;i<years.length;i++){{ var x=gx(i),y=gy(stack[i].ea); i===0?cx.moveTo(x,y):cx.lineTo(x,y); }} cx.stroke();
for(var i=0;i<years.length;i++){{ cx.beginPath(); cx.arc(gx(i),gy(stack[i].ca),4,0,Math.PI*2); cx.fillStyle='#fff'; cx.fill(); cx.strokeStyle='#00515D'; cx.lineWidth=2; cx.stroke(); }}
for(var i=0;i<years.length;i++){{ cx.beginPath(); cx.arc(gx(i),gy(stack[i].ea),3,0,Math.PI*2); cx.fillStyle='#fff'; cx.fill(); cx.strokeStyle='#3aa88e'; cx.lineWidth=1.5; cx.stroke(); }}
}}
function initPVPies() {{
function drawPie(canvasId,legendId,data) {{
var c = document.getElementById(canvasId);
if (!c) return;
var cx = c.getContext('2d');
var d = window.devicePixelRatio || 1;
var size = 180;
c.width = size*d; c.height = size*d;
c.style.width = size+'px'; c.style.height = size+'px';
cx.scale(d,d);
var total = data.reduce(function(s,i){{return s+i.value}},0);
var center = size/2, radius = size/2-6, inner = radius*0.52;
var angle = -Math.PI/2;
data.forEach(function(item){{
var slice = (item.value/total)*Math.PI*2;
cx.beginPath(); cx.moveTo(center+Math.cos(angle)*inner, center+Math.sin(angle)*inner);
cx.arc(center,center,radius,angle,angle+slice); cx.arc(center,center,inner,angle+slice,angle,true);
cx.closePath(); cx.fillStyle=item.color; cx.fill(); angle+=slice;
}});
cx.fillStyle='#022B3D'; cx.font='bold 18px "Space Mono",monospace'; cx.textAlign='center'; cx.textBaseline='middle';
cx.fillText((total/1000).toFixed(0)+'k',center,center-6);
cx.font='10px Inter,system-ui,sans-serif'; cx.fillStyle='#637C87'; cx.fillText('tCO\u2082',center,center+10);
var leg = document.getElementById(legendId);
if (leg) leg.innerHTML = data.map(function(item){{
var pct = ((item.value/total)*100).toFixed(0);
return '<div style="display:flex;justify-content:space-between;align-items:center;font-size:12px;padding:3px 0"><div style="display:flex;align-items:center;gap:6px"><div style="width:10px;height:10px;border-radius:2px;background:'+item.color+'"></div><span>'+item.label+'</span></div><div><span style="font-family:Space Mono,monospace;font-weight:700;font-size:11px">'+(item.value/1000).toFixed(0)+'k</span><span style="font-size:10px;color:#637C87;margin-left:3px">('+pct+'%)</span></div></div>';
}}).join('');
}}
drawPie('pieGeo','legendGeo',[{{label:'South America',value:310000,color:'#0e7e6e'}},{{label:'South Asia',value:195000,color:'#2e7da6'}},{{label:'North America',value:169000,color:'#c4a03a'}}]);
drawPie('pieStd','legendStd',[{{label:'VERRA VCS',value:310000,color:'#00515D'}},{{label:'Gold Standard',value:195000,color:'#3aa88e'}},{{label:'ACR',value:169000,color:'#5ca3c8'}}]);
drawPie('pieRat','legendRat',[{{label:'AA',value:195000,color:'#0e7e6e'}},{{label:'A',value:310000,color:'#2e7da6'}},{{label:'BBB',value:169000,color:'#c4a03a'}}]);
}}
/* Init Pro View charts when tab is shown */
var _pvObs = new MutationObserver(function(){{
var el = document.getElementById('viewAnalytics');
if (el && el.classList.contains('gc-view-active') && !window._pvChartDrawn) {{
setTimeout(function(){{ initPVChart(); initPVPies(); window._pvChartDrawn=true; window._pvPiesDrawn=true; }}, 200);
}}
}});
var _pvTarget = document.getElementById('viewAnalytics');
if (_pvTarget) _pvObs.observe(_pvTarget, {{attributes:true, attributeFilter:['class']}});
</script>
"""
return html
# ============ CSS ============
css = """
@import url('https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;14..32,400;14..32,500;14..32,600;14..32,700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,700&family=Space+Mono:wght@400;700&display=swap');
.gradio-container{background:#F0EBE4!important;font-family:'Inter',system-ui,sans-serif!important;max-width:100%!important;padding:0!important;margin:0!important}
.gradio-container>.main,.gradio-container>div{max-width:100%!important;padding:0!important;margin:0!important;gap:0!important}
footer{display:none!important}
.gc-wrapper,.gc-wrapper>div,.gc-wrapper .block{background:transparent!important;border:none!important;box-shadow:none!important;padding:0!important}
.gc-app{display:flex;height:100vh;background:#F0EBE4;font-family:'Inter',system-ui,sans-serif;color:#022B3D;overflow:hidden}
.gc-app-enter{animation:fadeIn .5s ease}
/* LOGIN */
.gc-login-screen{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:linear-gradient(145deg,#022B3D,#00515D 35%,#2590A8 70%,#022B3D);font-family:'Inter',system-ui,sans-serif;transition:opacity .5s,transform .5s}
.gc-login-exit{opacity:0;transform:scale(1.02);pointer-events:none}
.gc-login-bg{position:absolute;inset:0;overflow:hidden;pointer-events:none}
.gc-login-orb{position:absolute;border-radius:50%;filter:blur(80px);opacity:.15;animation:orbFloat 20s ease-in-out infinite}
.gc-login-orb-1{width:500px;height:500px;background:#CBE561;top:-10%;left:-5%}
.gc-login-orb-2{width:400px;height:400px;background:#2590A8;bottom:-15%;right:-5%;animation-delay:-7s}
.gc-login-orb-3{width:300px;height:300px;background:#87C314;top:40%;right:20%;animation-delay:-13s;opacity:.08}
@keyframes orbFloat{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(30px,-20px) scale(1.05)}66%{transform:translate(-20px,30px) scale(.95)}}
.gc-login-card{position:relative;z-index:1;width:420px;max-width:calc(100vw - 40px);background:rgba(255,255,255,.04);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,.08);border-radius:24px;box-shadow:0 24px 80px rgba(0,0,0,.3);overflow:hidden;opacity:0;transform:scale(.97)}
@keyframes cardEnter{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
.gc-login-logo-img{height:28px;width:auto}
.gc-login-header{display:flex;justify-content:space-between;align-items:center;padding:24px 32px;border-bottom:1px solid rgba(255,255,255,.06)}
.gc-login-product{font-size:.72rem;font-weight:700;color:#CBE561;text-transform:uppercase;letter-spacing:.08em;background:rgba(203,229,97,.12);padding:4px 10px;border-radius:6px}
.gc-login-body{padding:32px 32px 28px}
.gc-login-title{font-size:1.5rem;font-weight:700;color:#fff;margin:0 0 6px;font-family:'Source Serif 4',Georgia,serif}
.gc-login-subtitle{font-size:.88rem;color:rgba(255,255,255,.4);margin:0 0 28px}
.gc-login-field{margin-bottom:18px}
.gc-login-field label{display:block;font-size:.72rem;font-weight:600;color:rgba(255,255,255,.45);text-transform:uppercase;letter-spacing:.05em;margin-bottom:7px;font-family:'Space Mono',monospace}
.gc-login-input-wrap{display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:0 14px;transition:all .25s}
.gc-login-input-wrap:focus-within{border-color:rgba(203,229,97,.4);box-shadow:0 0 0 3px rgba(203,229,97,.08)}
.gc-login-input-wrap input{flex:1;background:transparent!important;border:none!important;color:#fff!important;font-size:.9rem;font-family:inherit;padding:12px 0;outline:none;box-shadow:none!important}
.gc-login-error{font-size:.8rem;color:#f27b8a;height:0;overflow:hidden;opacity:0;transition:all .25s}
.gc-login-error-show{height:auto;opacity:1;margin-bottom:14px}
.gc-login-btn{width:100%;padding:13px;background:linear-gradient(135deg,#87C314,#2F7A47);color:#fff;border:none;border-radius:12px;font-size:.92rem;font-weight:700;font-family:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .25s;box-shadow:0 4px 16px rgba(135,195,20,.3);position:relative}
.gc-login-btn:hover{box-shadow:0 6px 24px rgba(135,195,20,.4);transform:translateY(-1px)}
.gc-login-spinner{display:none;width:20px;height:20px;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite;position:absolute}
@keyframes spin{to{transform:rotate(360deg)}}
.gc-login-footer{padding:18px 32px;border-top:1px solid rgba(255,255,255,.04);text-align:center}
.gc-login-footer span{font-size:.72rem;color:rgba(255,255,255,.15)}
/* SIDEBAR */
.gc-sidebar{width:240px;min-width:240px;background:#00515D;color:#fff;position:fixed;top:0;left:0;bottom:0;z-index:50}
.gc-sidebar-inner{display:flex;flex-direction:column;height:100%;padding:0}
.gc-sidebar-top{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 20px;border-bottom:1px solid rgba(255,255,255,.1);margin-bottom:10px}
.gc-logo{height:64px;display:flex;align-items:center;padding:0 20px;border-bottom:1px solid rgba(255,255,255,.1)}
.gc-sidebar-logo-img{height:24px;filter:brightness(0) invert(1)}
.gc-user{display:flex;align-items:center;justify-content:center;padding:14px 20px;margin:10px 12px 16px;background:rgba(255,255,255,.08);border-radius:10px}
.gc-user-siemens-logo{height:22px;filter:brightness(0) invert(1)}
.gc-nav{display:flex;flex-direction:column;gap:2px;padding:12px 8px 0}
.gc-nav-item{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:8px;font-size:14px;font-weight:500;color:rgba(255,255,255,.9);text-decoration:none;transition:all .2s;cursor:pointer}
.gc-nav-item svg{color:rgba(255,255,255,.9);fill:rgba(255,255,255,.9);flex-shrink:0}
.gc-nav-item:hover{background:rgba(255,255,255,.15)}
.gc-nav-item.active{background:#CBE561;color:#022B3D;font-weight:600}
.gc-nav-item.active svg{color:#022B3D;fill:#022B3D}
.gc-sidebar-footer{margin-top:auto;padding:20px;border-top:1px solid rgba(255,255,255,.1)}
.gc-powered{font-size:.7rem;color:rgba(255,255,255,.3);text-align:center;font-family:'Space Mono',monospace}
/* MAIN */
.gc-main{flex:1;margin-left:240px;height:100vh;background:#F0EBE4;overflow-y:auto}
.gc-view{display:none}
.gc-view-active{display:block}
.gc-header{background:#fff;padding:0 40px;border-bottom:1px solid rgba(2,43,61,.1);height:64px;display:flex;align-items:center}
.gc-header h2{font-size:22px;font-weight:600;color:#022B3D;margin:0;display:flex;align-items:center;gap:12px;font-family:'Source Serif 4',Georgia,serif}
.gc-title-sep{color:#CBE561;font-weight:300;margin:0 2px}
.gc-title-prefix{font-weight:400;color:#637C87;font-family:'Inter',system-ui,sans-serif}
.gc-content{padding:30px 40px}
/* STAGGERED ENTRANCE ANIMATIONS */
@keyframes gcSlideUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
@keyframes gcFadeScale{from{opacity:0;transform:scale(0.96)}to{opacity:1;transform:scale(1)}}
.gc-view-active .gc-metric-card,
.gc-view-active .gc-chart-card,
.gc-view-active .gc-chart-full,
.gc-view-active .gc-proj-row,
.gc-view-active .gc-dim-card{opacity:0;animation:gcSlideUp .45s cubic-bezier(.25,.46,.45,.94) forwards}
.gc-view-active .gc-metric-card:nth-child(1){animation-delay:.05s}
.gc-view-active .gc-metric-card:nth-child(2){animation-delay:.1s}
.gc-view-active .gc-metric-card:nth-child(3){animation-delay:.15s}
.gc-view-active .gc-metric-card:nth-child(4){animation-delay:.2s}
.gc-view-active .gc-charts-grid .gc-chart-card:nth-child(1){animation-delay:.2s}
.gc-view-active .gc-charts-grid .gc-chart-card:nth-child(2){animation-delay:.28s}
.gc-view-active .gc-chart-full{animation-delay:.32s}
.gc-view-active .gc-proj-row:nth-child(1){animation-delay:.06s}
.gc-view-active .gc-proj-row:nth-child(2){animation-delay:.1s}
.gc-view-active .gc-proj-row:nth-child(3){animation-delay:.14s}
.gc-view-active .gc-proj-row:nth-child(4){animation-delay:.18s}
.gc-view-active .gc-proj-row:nth-child(5){animation-delay:.22s}
.gc-view-active .gc-proj-row:nth-child(6){animation-delay:.26s}
.gc-view-active .gc-proj-row:nth-child(7){animation-delay:.3s}
.gc-view-active .gc-proj-row:nth-child(8){animation-delay:.34s}
.gc-view-active .gc-proj-row:nth-child(9){animation-delay:.38s}
.gc-view-active .gc-proj-row:nth-child(10){animation-delay:.42s}
.gc-view-active .gc-proj-row:nth-child(11){animation-delay:.46s}
.gc-view-active .gc-proj-row:nth-child(12){animation-delay:.5s}
.gc-view-active .gc-proj-row:nth-child(13){animation-delay:.54s}
.gc-view-active .gc-dim-card:nth-child(1){animation-delay:.06s}
.gc-view-active .gc-dim-card:nth-child(2){animation-delay:.12s}
.gc-view-active .gc-dim-card:nth-child(3){animation-delay:.18s}
.gc-view-active .gc-dim-card:nth-child(4){animation-delay:.24s}
.gc-view-active .gc-dim-card:nth-child(5){animation-delay:.3s}
.gc-view-active .gc-mon-grid .gc-chart-card:nth-child(1){animation-delay:.35s}
.gc-view-active .gc-mon-grid .gc-chart-card:nth-child(2){animation-delay:.4s}
.gc-view-active .gc-mon-grid-2col .gc-chart-card:nth-child(1){animation-delay:.45s}
.gc-view-active .gc-mon-grid-2col .gc-chart-card:nth-child(2){animation-delay:.5s}
/* BAR ANIMATION TRANSITIONS */
.gc-dist-seg-del,.gc-dist-seg-open{transition:width .7s cubic-bezier(.25,.46,.45,.94)}
.gc-tbar{transition:height .6s cubic-bezier(.25,.46,.45,.94)}
.gc-kpi-bar-fill{transition:width .8s cubic-bezier(.25,.46,.45,.94) .3s}
.gc-proj-row-fill-del,.gc-proj-row-fill-open{transition:width .6s cubic-bezier(.25,.46,.45,.94)}
/* METRICS */
.gc-metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:20px;margin-bottom:30px}
.gc-metric-card{background:#fff;padding:20px;border-radius:12px;box-shadow:0 1px 3px rgba(2,43,61,.1);transition:all .2s;border-top:3px solid #00515D}
.gc-metric-card:hover{box-shadow:0 4px 12px rgba(2,43,61,.15);transform:translateY(-2px)}
.gc-metric-label{font-size:16px;font-weight:600;color:#022B3D;margin-bottom:12px}
.gc-metric-value{font-size:32px;font-weight:700;color:#00515D;margin-bottom:2px;font-family:'Source Serif 4',Georgia,serif}
.gc-metric-unit{font-size:13px;color:#022B3D;opacity:.6;font-family:'Space Mono',monospace}
/* CHARTS */
.gc-charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:30px}
.gc-chart-card{background:#fff;padding:20px;border-radius:12px;box-shadow:0 1px 3px rgba(2,43,61,.1);overflow:hidden}
.gc-chart-full{margin-bottom:30px}
/* Stacked Area Chart KPIs */
.gc-area-kpis{display:flex;border-top:1px solid #E0D9D3;padding:14px 20px 8px;gap:0}
.gc-area-kpi{flex:1;text-align:center;padding:0 16px;border-right:1px solid #E0D9D3}
.gc-area-kpi:last-child{border-right:none}
.gc-area-kpi-total{background:rgba(0,81,93,.03);border-radius:6px;padding:4px 16px;border-right:none}
.gc-area-kpi-label{display:block;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:#637C87;font-family:'Space Mono',monospace;margin-bottom:2px}
.gc-area-kpi-val{font-size:18px;font-weight:700;color:#022B3D;font-family:'Source Serif 4',Georgia,serif}
.gc-pv-toggle{padding:8px 18px;border:1.5px solid #E0D9D3;border-radius:8px;background:#fff;font-size:12px;font-weight:600;color:#637C87;cursor:pointer;transition:all .2s;font-family:Inter,system-ui,sans-serif}
.gc-pv-toggle:hover{border-color:#00515D;color:#022B3D}
.gc-pv-toggle.active{background:#00515D;color:#fff;border-color:#00515D}
.gc-chart-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.gc-chart-title{font-size:16px;font-weight:600;color:#022B3D;font-family:'Source Serif 4',Georgia,serif}
/* DISTRIBUTION */
.gc-dist-item{margin-bottom:16px}
.gc-dist-head{display:flex;justify-content:space-between;margin-bottom:8px}
.gc-dist-label{font-size:13px;color:#022B3D;font-weight:500}
.gc-dist-value{font-size:13px;font-weight:600;color:#022B3D;font-family:'Space Mono',monospace}
.gc-dist-bar{height:32px;border-radius:8px;overflow:hidden;position:relative}
.gc-dist-fill-stacked{display:flex;height:100%;width:100%;border-radius:8px;overflow:hidden;gap:2px}
.gc-dist-seg-del{background:#00515D;height:100%}
.gc-dist-seg-open{background:#CBE561;height:100%;display:flex;align-items:center;justify-content:flex-end;overflow:hidden}
.gc-bar-label{font-size:11px;font-weight:600;color:#022B3D;opacity:0.7;white-space:nowrap;padding:0 8px}
.gc-dist-fill{height:100%;border-radius:8px;display:flex;align-items:center;padding:0 12px;font-size:12px;font-weight:600;min-width:36px}
.gc-fill-dark{background:#00515D;color:#fff}
.gc-fill-lime{background:#CBE561;color:#022B3D}
.gc-fill-muted{opacity:.6}
/* MAP */
.gc-map-container{border-radius:8px;overflow:hidden;margin-bottom:8px}
/* MONITORING — brand-derived traffic-light palette */
.gc-mon-content{max-width:100%}
.gc-dim-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin:20px 0}
.gc-dim-card{background:#fff;border-radius:10px;padding:20px;box-shadow:0 1px 3px rgba(2,43,61,.08);cursor:pointer;transition:all .25s;position:relative;overflow:hidden;border-top:3px solid transparent}
.gc-dim-card:hover{box-shadow:0 4px 14px rgba(2,43,61,.14);transform:translateY(-2px)}
.gc-dim-green{border-top-color:#2F7A47}
.gc-dim-amber{border-top-color:#D4A017}
.gc-dim-red{border-top-color:#C24B3A}
.gc-dim-card-top{display:flex;align-items:center;justify-content:flex-end;margin-bottom:10px}
.gc-dim-name-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
.gc-dim-icon{width:34px;height:34px;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f0ebe4;color:#022B3D}
.gc-badge-green{font-size:9px;font-weight:700;padding:3px 8px;border-radius:12px;background:rgba(47,122,71,.1);color:#2F7A47;letter-spacing:.5px}
.gc-badge-amber{font-size:9px;font-weight:700;padding:3px 8px;border-radius:12px;background:rgba(212,160,23,.1);color:#D4A017;letter-spacing:.5px}
.gc-badge-red{font-size:9px;font-weight:700;padding:3px 8px;border-radius:12px;background:rgba(194,75,58,.1);color:#C24B3A;letter-spacing:.5px}
.gc-dim-name{font-size:12px;font-weight:600;color:#335362;text-transform:uppercase;letter-spacing:.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}
.gc-dim-score{font-size:26px;font-weight:700;color:#022B3D;font-family:'Source Serif 4',Georgia,serif}
.gc-mon-grid{display:grid;grid-template-columns:3fr 2fr;gap:16px;margin-top:16px}
.gc-mon-table{width:100%;border-collapse:collapse;table-layout:fixed}
.gc-mon-table th{text-align:center;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:#637C87;padding:10px 8px;border-bottom:2px solid #E0D9D3;font-family:'Space Mono',monospace;background:#F0EBE4}
.gc-mon-table th:first-child{text-align:left;width:28%}
.gc-mon-table th:last-child{width:90px}
.gc-mon-table td{padding:10px 8px;font-size:13px;border-bottom:1px solid #f0ebe4;vertical-align:middle;text-align:center}
.gc-mon-table td:first-child{text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-mon-table tr:hover td{background:rgba(240,235,228,.4)}
.gc-mon-proj-name{font-weight:600;color:#022B3D}
.gc-status-dot{width:11px;height:11px;border-radius:50%;display:inline-block}
.gc-dot-green{background:#2F7A47;box-shadow:0 0 0 3px rgba(47,122,71,.15)}
.gc-dot-amber{background:#D4A017;box-shadow:0 0 0 3px rgba(212,160,23,.15)}
.gc-dot-red{background:#C24B3A;box-shadow:0 0 0 3px rgba(194,75,58,.15)}
.gc-status-label{font-size:10px;font-weight:700;padding:4px 12px;border-radius:12px;white-space:nowrap}
.gc-sl-green{background:rgba(47,122,71,.1);color:#2F7A47}
.gc-sl-amber{background:rgba(212,160,23,.1);color:#D4A017}
.gc-sl-red{background:rgba(194,75,58,.1);color:#C24B3A}
.gc-alarm-feed{padding:16px}
.gc-alarm-item{display:flex;align-items:flex-start;gap:12px;padding:12px 0;border-bottom:1px solid #f0ebe4}
.gc-alarm-item:last-child{border-bottom:none}
.gc-alarm-dot{width:9px;height:9px;border-radius:50%;margin-top:5px;flex-shrink:0}
.gc-alarm-body{flex:1;min-width:0;overflow:hidden}
.gc-alarm-body strong{font-size:12px;font-weight:600;color:#022B3D;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-alarm-desc{font-size:11px;color:#637C87;margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-alarm-meta{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0}
.gc-alarm-sev{font-size:9px;font-weight:700;letter-spacing:.5px}
.gc-alarm-date{font-size:10px;color:#637C87;font-family:'Space Mono',monospace}
.gc-alarm-count{font-size:9px;font-weight:700;padding:3px 8px;border-radius:12px;background:rgba(194,75,58,.1);color:#C24B3A}
.gc-dim-detail{background:#fff;border-radius:10px;box-shadow:0 1px 3px rgba(2,43,61,.08);margin:0 0 16px;overflow:hidden;animation:fadeDown .3s ease}
@keyframes fadeDown{from{opacity:0;max-height:0}to{opacity:1;max-height:600px}}
.gc-dim-detail-head{display:flex;align-items:center;gap:14px;padding:16px 20px;border-bottom:1px solid #E0D9D3}
.gc-dim-badge{width:38px;height:38px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:#fff;background:#00515D}
.gc-dim-detail-info{flex:1}
.gc-dim-detail-title{font-size:15px;font-weight:700;color:#022B3D;font-family:'Source Serif 4',Georgia,serif}
.gc-mon-detail-table th{font-size:9px;background:#F0EBE4}
.gc-mon-detail-table td{font-size:12px}
/* Clickable scorecard rows */
.gc-mon-clickable{cursor:pointer;transition:all .15s}
.gc-mon-clickable:hover td{background:rgba(0,81,93,.04)!important}
.gc-mon-clickable:hover td:first-child{color:#00515D}
/* Project Monitoring Detail — redesigned */
.gc-pmd-top{display:grid;grid-template-columns:1fr 240px;gap:20px;margin-bottom:20px}
.gc-pmd-hero{background:#fff;border-radius:12px;padding:24px;box-shadow:0 1px 3px rgba(2,43,61,.08);display:flex;gap:24px;align-items:center}
.gc-pmd-hero-left{flex-shrink:0}
.gc-pmd-donut-svg{width:130px;height:130px}
.gc-pmd-hero-meta{flex:1;display:flex;flex-direction:column;gap:14px}
.gc-pmd-tags{display:flex;flex-wrap:wrap;gap:6px}
.gc-pmd-meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.gc-pmd-meta-grid .gc-detail-meta{padding:8px 12px;border-radius:6px}
.gc-pmd-radar{background:#fff;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(2,43,61,.08);display:flex;align-items:center;justify-content:center}
.gc-pmd-radar-svg{width:100%;max-width:210px;height:auto}
.gc-pmd-dims{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:20px}
.gc-pmd-dim-card{background:#fff;border-radius:10px;padding:14px;box-shadow:0 1px 3px rgba(2,43,61,.08);border-top:3px solid transparent}
/* Compact KPI score badges */
.gc-pmd-score{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:10px;white-space:nowrap;font-family:'Space Mono',monospace}
.gc-pmd-sc-green{background:rgba(47,122,71,.1);color:#2F7A47}
.gc-pmd-sc-amber{background:rgba(212,160,23,.1);color:#D4A017}
.gc-pmd-sc-red{background:rgba(194,75,58,.1);color:#C24B3A}
/* Compact alarm card */
.gc-pmd-alarms-card{background:#fff;border-radius:12px;padding:16px 20px;box-shadow:0 1px 3px rgba(2,43,61,.08);margin-bottom:20px}
.gc-pmd-alarm-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid #f0ebe4}
.gc-pmd-alarm-row:last-child{border-bottom:none}
.gc-pmd-alarm-text{flex:1;display:flex;align-items:baseline;gap:8px;min-width:0}
.gc-pmd-alarm-text strong{font-size:12px;font-weight:600;color:#022B3D;white-space:nowrap}
.gc-pmd-alarm-desc{font-size:11px;color:#637C87;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gc-pmd-section{margin-bottom:20px}
.gc-pmd-dim-section{background:#fff;border-radius:10px;padding:16px 18px;margin-bottom:14px;box-shadow:0 1px 3px rgba(2,43,61,.08)}
.gc-pmd-dim-heading{font-size:13px;font-weight:600;color:#022B3D;margin:0 0 10px;display:flex;align-items:center;gap:8px;font-family:'Source Serif 4',Georgia,serif}
.gc-pmd-dim-num{width:22px;height:22px;border-radius:5px;background:#00515D;color:#fff;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;font-family:'Inter',system-ui,sans-serif}
.gc-pmd-kpi-table{table-layout:fixed}
.gc-pmd-kpi-table th:first-child{width:24%}
.gc-pmd-kpi-table th:nth-child(2){width:64px}
.gc-pmd-kpi-table th:nth-child(3){width:auto}
.gc-pmd-kpi-table th:nth-child(4){width:100px}
.gc-pmd-kpi-table th:nth-child(5){width:auto}
.gc-pmd-td-target{font-size:12px;color:#335362}
.gc-mon-grid-2col{display:grid;grid-template-columns:3fr 2fr;gap:16px}
/* KPI Snapshot bars */
.gc-kpi-bars-body{padding:18px 20px}
.gc-kpi-bar-row{display:flex;align-items:center;gap:12px;margin-bottom:12px}
.gc-kpi-bar-row:last-child{margin-bottom:0}
.gc-kpi-bar-label{width:160px;font-size:12px;font-weight:500;flex-shrink:0;color:#022B3D}
.gc-kpi-bar-track{flex:1;height:8px;background:#f0ebe4;border-radius:4px;overflow:hidden}
.gc-kpi-bar-fill{height:100%;border-radius:4px}
.gc-kpi-bar-val{width:42px;font-size:12px;font-weight:700;text-align:right;font-family:'Space Mono',monospace}
/* Donut */
.gc-donut-body{padding:20px;display:flex;flex-direction:column;align-items:center}
.gc-donut-legend{width:100%;max-width:220px;margin-top:16px}
.gc-donut-legend-item{display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:8px}
.gc-donut-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.gc-donut-count{margin-left:auto;font-weight:700;font-size:14px;font-family:'Source Serif 4',Georgia,serif}
/* Escalation — brand-derived */
.gc-esc-body{display:flex;gap:0;padding:20px;align-items:stretch}
.gc-esc-step{flex:1;border-radius:8px;padding:18px;text-align:center}
.gc-esc-watch{background:rgba(47,122,71,.08);border:1px solid rgba(47,122,71,.2)}
.gc-esc-alert{background:rgba(212,160,23,.08);border:1px solid rgba(212,160,23,.2)}
.gc-esc-critical{background:rgba(194,75,58,.08);border:1px solid rgba(194,75,58,.2)}
.gc-esc-title{font-size:12px;font-weight:700;letter-spacing:1px;margin-bottom:8px;display:flex;align-items:center;justify-content:center;gap:6px}
.gc-esc-watch .gc-esc-title{color:#2F7A47}
.gc-esc-alert .gc-esc-title{color:#D4A017}
.gc-esc-critical .gc-esc-title{color:#C24B3A}
.gc-esc-dot{width:10px;height:10px;border-radius:50%;display:inline-block}
.gc-esc-step p{font-size:11px;color:#022B3D;opacity:.7;margin-bottom:6px}
.gc-esc-action{font-weight:600!important;opacity:1!important}
.gc-esc-tag{display:inline-block;font-size:10px;font-weight:700;color:#fff;padding:3px 10px;border-radius:4px;margin-top:8px}
.gc-esc-arrow{display:flex;align-items:center;padding:0 8px;color:#D5CFC7;font-size:20px}
/* TIMELINE */
.gc-timeline-chart{height:300px;position:relative;display:flex;margin-top:20px;background:#f7f5f1;border-radius:8px;padding:16px 16px 0 0}
.gc-timeline-y{width:50px;display:flex;flex-direction:column;justify-content:space-between;font-size:11px;color:#022B3D;opacity:.6;text-align:right;padding-right:10px;padding-bottom:40px;font-family:'Space Mono',monospace}
.gc-timeline-bars{flex:1;display:flex;align-items:flex-end;gap:16px;padding-bottom:40px}
.gc-tbar-col{flex:1;display:flex;flex-direction:column;align-items:center;height:100%}
.gc-tbar-stack{width:100%;display:flex;flex-direction:column-reverse;flex:1;justify-content:flex-start}
.gc-tbar{width:100%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#022B3D;min-height:20px;font-family:'Space Mono',monospace}
.gc-tbar:first-child{border-radius:0 0 8px 8px}
.gc-tbar:last-child{border-radius:8px 8px 0 0}
.gc-tbar:only-child{border-radius:8px}
.gc-bg-lime{background:#00515D;color:#fff}
.gc-bg-open-bar{background:#CBE561;color:#022B3D;border:none}
.gc-tbar-label{margin-top:10px;font-size:13px;font-weight:600;color:#022B3D}
/* LEGEND */
.gc-legend{margin-top:20px;padding-top:20px;border-top:1px solid #F0EBE4}
.gc-legend-item{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:13px}
.gc-legend-color{width:12px;height:12px;border-radius:3px;flex-shrink:0}
.gc-bg-open{background:#CBE561}
.gc-legend-label{color:#022B3D;flex:1}
.gc-legend-val{color:#022B3D;opacity:.7;font-weight:600;font-family:'Space Mono',monospace}
/* ACTIVITY */
.gc-activity-item{display:flex;gap:12px;padding:14px 0;border-bottom:1px solid #F0EBE4}
.gc-activity-item:last-child{border-bottom:none}
.gc-activity-icon{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;flex-shrink:0;background:#CBE561;color:#022B3D;font-size:14px}
.gc-activity-content{flex:1}
.gc-activity-text{font-size:14px;color:#022B3D;margin-bottom:4px}
.gc-activity-time{font-size:12px;color:#022B3D;opacity:.6;font-family:'Space Mono',monospace}
/* PROJECTS LIST */
.gc-proj-list{display:flex;flex-direction:column;gap:8px}
.gc-proj-row{display:grid;grid-template-columns:2fr 1fr 1fr 1fr 120px 30px;align-items:center;gap:16px;background:#fff;padding:18px 24px;border-radius:12px;box-shadow:0 1px 3px rgba(2,43,61,.05);cursor:pointer;transition:all .2s;border-left:3px solid transparent}
.gc-proj-row:hover{box-shadow:0 4px 12px rgba(2,43,61,.1);transform:translateY(-1px);border-left-color:#00515D}
.gc-proj-row-name strong{display:block;font-size:14px;margin-bottom:4px}
.gc-proj-row-tags{display:flex;gap:6px;flex-wrap:wrap}
.gc-tag{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:600;background:#CBE561;color:#022B3D}
.gc-tag-outline{background:transparent;border:1px solid #E0D9D3;color:#637C87}
.gc-detail-tags{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:6px}
.gc-detail-tags:last-of-type{justify-content:flex-start}
.gc-detail-tags:last-of-type .gc-tag-outline{flex:1;text-align:center}
.gc-tag-primary{background:#CBE561;color:#022B3D;width:100%}
.gc-proj-row-stat{text-align:center}
.gc-proj-row-val{display:block;font-size:14px;font-weight:700;color:#022B3D;font-family:'Space Mono',monospace}
.gc-proj-row-label{font-size:11px;color:#637C87}
.gc-proj-row-bar{height:6px;background:transparent;border-radius:3px;overflow:hidden;display:flex}
.gc-proj-row-fill-del{height:100%;background:#00515D;border-radius:3px 0 0 3px}
.gc-proj-row-fill-open{height:100%;background:#CBE561;border-radius:0 3px 3px 0}
.gc-proj-row-arrow{color:#c5bfb7;transition:all .2s}
.gc-proj-row:hover .gc-proj-row-arrow{color:#00515D;transform:translateX(2px)}
/* PROJECT DETAIL */
.gc-back-btn{background:none;border:none;cursor:pointer;color:#00515D;padding:0;display:flex;align-items:center}
.gc-back-btn:hover{color:#87C314}
.gc-detail-top{display:grid;grid-template-columns:1fr 1fr;gap:30px;margin-bottom:30px}
.gc-detail-left{display:flex;flex-direction:column;gap:12px}
.gc-detail-desc{font-size:14px;color:#335362;line-height:1.65;margin:8px 0 0}
.gc-detail-meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}
.gc-detail-meta{background:#F0EBE4;padding:12px 16px;border-radius:8px}
.gc-meta-label{display:block;font-size:11px;color:#637C87;font-weight:600;text-transform:uppercase;letter-spacing:.04em;margin-bottom:4px;font-family:'Space Mono',monospace}
.gc-meta-value{font-size:13px;font-weight:600;color:#022B3D}
.gc-detail-right{}
.gc-detail-kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:20px}
.gc-detail-kpi{background:#fff;border:1px solid #E0D9D3;padding:16px;border-radius:10px;text-align:center}
.gc-kpi-green{border-color:#00515D;background:rgba(0,81,93,.06)}
.gc-kpi-lime{border-color:#CBE561;background:rgba(203,229,97,.1)}
.gc-detail-kpi-val{display:block;font-size:20px;font-weight:700;color:#00515D;font-family:'Source Serif 4',Georgia,serif}
.gc-detail-kpi-label{font-size:11px;color:#637C87;margin-top:4px}
.gc-detail-bar-wrap{margin-bottom:24px}
.gc-detail-bar-track{height:10px;background:transparent;border-radius:5px;overflow:hidden;margin-bottom:6px;display:flex}
.gc-detail-bar-fill{height:100%;background:#00515D;border-radius:5px 0 0 5px}
.gc-detail-bar-open{height:100%;background:#CBE561;border-radius:0 5px 5px 0}
.gc-detail-bar-pct{font-size:12px;color:#637C87;font-weight:600}
.gc-detail-section{margin-bottom:30px}
.gc-detail-section-title{font-size:16px;font-weight:600;color:#022B3D;margin:0 0 16px;padding-bottom:12px;border-bottom:1px solid #E0D9D3;font-family:'Source Serif 4',Georgia,serif}
.gc-sdg-grid{display:flex;flex-wrap:wrap;gap:8px}
.gc-sdg-icon{width:64px;height:64px;border-radius:6px}
.gc-media-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.gc-media-img{width:100%;border-radius:10px;aspect-ratio:4/3;object-fit:cover}
.gc-detail-fulldesc{font-size:14px;color:#335362;line-height:1.7}
.gc-vintage-table{width:100%;border-collapse:collapse;font-size:13px}
.gc-vintage-table th{text-align:left;padding:10px;font-size:11px;font-weight:600;color:#637C87;text-transform:uppercase;letter-spacing:.04em;border-bottom:2px solid #E0D9D3;font-family:'Space Mono',monospace}
.gc-vintage-table td{padding:12px 10px;border-bottom:1px solid #F0EBE4;color:#335362}
.gc-vintage-table strong{color:#022B3D}
.gc-price{color:#2F7A47;font-weight:600;font-family:'Space Mono',monospace}
.gc-retirement-list{display:flex;flex-direction:column}
.gc-retirement-item{display:flex;justify-content:space-between;align-items:flex-start;padding:14px 0;border-bottom:1px solid #F0EBE4}
.gc-retirement-item:last-child{border-bottom:none}
.gc-retirement-qty{font-size:14px;font-weight:700;color:#022B3D;margin-bottom:3px}
.gc-retirement-details{font-size:12px;color:#637C87}
.gc-retirement-serial{font-size:11px;color:#637C87;font-family:'Space Mono',monospace;margin-top:3px}
.gc-retirement-date{font-size:12px;color:#637C87;font-family:'Space Mono',monospace}
.gc-detail-bottom-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-top:8px}
.gc-downloads-list{display:flex;flex-direction:column;gap:6px}
.gc-download-item{display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:10px;background:#f7f5f1;text-decoration:none;color:#022B3D;transition:all .2s;border:1px solid transparent}
.gc-download-item:hover{background:#fff;border-color:#e0dbd4;box-shadow:0 2px 8px rgba(0,43,61,0.06)}
.gc-download-icon{width:38px;height:38px;border-radius:10px;background:rgba(47,122,71,.08);color:#00515D;display:flex;align-items:center;justify-content:center;flex-shrink:0}
.gc-download-text{display:flex;flex-direction:column;flex:1}
.gc-download-name{font-size:13px;font-weight:600;color:#022B3D}
.gc-download-meta{font-size:11px;color:#637C87;margin-top:3px}
.gc-download-arrow{color:#637C87;opacity:0;transition:opacity .2s}
.gc-download-item:hover .gc-download-arrow{opacity:1}
/* PLACEHOLDER */
.gc-placeholder-content{text-align:center;padding:80px 32px;background:#fff;border-radius:16px}
.gc-placeholder-title{font-size:1.3rem;font-weight:700;color:#022B3D;margin:0 0 10px;font-family:'Source Serif 4',Georgia,serif}
.gc-placeholder-desc{font-size:.9rem;color:#637C87;max-width:480px;margin:0 auto;line-height:1.6}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@media(max-width:1200px){.gc-metrics-grid{grid-template-columns:repeat(2,1fr)}.gc-charts-grid{grid-template-columns:1fr}.gc-detail-top{grid-template-columns:1fr}.gc-proj-row{grid-template-columns:1fr 1fr 1fr;gap:12px}.gc-proj-row-bar,.gc-proj-row-arrow{display:none}.gc-dim-cards{grid-template-columns:repeat(3,1fr)}.gc-mon-grid{grid-template-columns:1fr}.gc-pmd-top{grid-template-columns:1fr}.gc-pmd-dims{grid-template-columns:repeat(3,1fr)}}
@media(max-width:900px){.gc-sidebar{display:none}.gc-main{margin-left:0}.gc-content{padding:20px}.gc-metrics-grid{grid-template-columns:1fr 1fr}.gc-media-grid{grid-template-columns:repeat(2,1fr)}.gc-pmd-dims{grid-template-columns:repeat(2,1fr)}}
"""
# ============ GRADIO APP ============
with gr.Blocks(
css=css,
theme=gr.themes.Soft(primary_hue="green",secondary_hue="slate",neutral_hue="slate").set(
body_background_fill="#F0EBE4",body_background_fill_dark="#F0EBE4",
block_background_fill="transparent",block_background_fill_dark="transparent",
panel_background_fill="transparent",panel_background_fill_dark="transparent",
block_border_width="0",block_shadow="none",
body_text_color="#022B3D",body_text_color_dark="#022B3D",
),
title="goodcarbon Credits — Siemens AG"
) as app:
with gr.Column(elem_classes="gc-wrapper"):
dashboard = gr.HTML(value=build_dashboard())
if __name__ == "__main__":
app.launch(allowed_paths=["Media"])