"""
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' '
else:
dots += f' '
# 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' '
bubbles += f' '
bubbles += f'{vol_k} '
geo_map_html = f'''
{dots}
{bubbles}
'''
region_legend = ""
for region, vol in region_agg.items():
rc = region_map_colors.get(region, "#00515D")
region_legend += f'
{region}
{fmt_num(vol)} tCO₂
'
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''''''
kpi_snapshot_html = f''
# 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'''
{n_total}
Projects
On Track {n_green}
Watch {n_amber}
Alert / Critical {n_red}
'''
# Escalation Protocol
escalation_html = '''
WATCH
Single KPI approaching threshold or first-time minor breach
Flag in report · Request developer clarification · Increase monitoring frequency
Within 2 weeks
→
ALERT
Multiple KPIs in breach or single KPI in sustained breach (2 consecutive periods)
Escalate to IC · Site visit · Require remediation plan within 30 days
Within 1 week
→
CRITICAL
Knock-out criteria breached, integrity compromised, or developer non-responsive >60 days
Emergency IC review · Suspend purchases · Consider exit · Engage legal
Immediately
'''
# ============ 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: ' ',
2: ' ',
3: ' ',
4: ' ',
5: ' ',
}
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'''
{dim_names[d]} {badge_text}
{avg}%
'''
# KPI Scorecard table (traffic light per project per dimension)
scorecard_html = 'Project Carbon Ecological Community Operational Risk Overall '
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' '
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'{p["name"][:30]} {dots}{ol} '
scorecard_html += '
'
# 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'''
{pname} — {a["kpi"]} {a["description"]}
{sev.upper()} {a["date"]}
'''
# 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'''{kpi_name} {info["target"]} {info["freq"]} {info["source"]} {info["score"]}% '''
avg = dim_avgs.get(d, 75)
oc = "green" if avg >= 80 else "amber" if avg >= 60 else "red"
dim_details_html += f'''
KPI Target Frequency Data Source Status {rows}
'''
# 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'''
{dim_cards_html}
{dim_details_html}
{kpi_snapshot_html}
{donut_html}
{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'{row["name"][:35]} {fmt_num(contracted)} tCO₂
'
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'Other Projects ({others_count}) {fmt_num(others_vol)} tCO₂
'
# 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'{pathway} {fmt_num(int(vol))} tCO₂
'
# 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'{o_label}
' if o > 0 else ''
bars = f'{d_label}
' + bars
timeline_html += f''
# 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'{icon}
{a["description"]}
{a["date"]}
'
# 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' '
label = fmt_num(int(yv), compact=True) + " tCO₂" if yv > 0 else "0"
area_grid += f'{label} '
for i, y in enumerate(area_years):
xp = ax(i)
area_grid += f'{area_year_labels[y]} '
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' '
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' '
for i, y in enumerate(area_years):
xp = ax(i)
yp = ay(year_stacked[y][-1])
val = year_stacked[y][-1]
area_paths += f' '
area_paths += f'{fmt_num(int(val), compact=True)} '
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' '
area_legend += f'{abbr} '
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'''
{area_legend}
{area_grid}
{area_paths}
Delivered {fmt_num(total_delivered)} tCO₂
Open {fmt_num(total_open)} tCO₂
Total Contracted {fmt_num(total_all)} tCO₂
'''
# ============ PRO VIEW — Forward-looking portfolio planning ============
pro_view_html = f'''
By Project
By Pathway
Firm + Top-up · 2030–2035
Extension · 2036–2040
Firm (2030–35)
321,000
tCO₂
Top-up (2030–35)
138,000
tCO₂ additional
Extension (2036–40)
215,000
tCO₂
Total incl. Options
674,000
tCO₂
Volume: 310,000 tCO₂
Budget: € 935,000
Avg.: € 3.02 / tCO₂
Vol.
Firm 156k
Top-up 52k
Ext. 102k
Sundarbans Delta, Bangladesh
Wetland / Mangrove · Gold Standard
2030–2040
Volume: 195,000 tCO₂
Budget: € 681,000
Avg.: € 3.49 / tCO₂
Vol.
Firm 89k
Top-up 46k
Ext. 60k
Great Plains SOC, USA
Soil Organic Carbon · ACR
2030–2040
Volume: 169,000 tCO₂
Budget: € 685,000
Avg.: € 4.05 / tCO₂
Vol.
Firm 76k
Top-up 40k
Ext. 53k
Total Portfolio
2030–2040
Volume: 674,000 tCO₂
Budget: € 2,300,000
Avg.: € 3.41 / tCO₂
Vol.
Firm 321k
Top-up 138k
Ext. 215k
Budget
Firm €1.0M
€500k
Ext. €800k
Firm Contract Budget
2030–2035
Volume: 321,000 tCO₂
Budget: € 1,000,000
Avg.: € 3.12 / tCO₂
Vol.
A/R 156k
Wetland 89k
SOC 76k
Budget
A/R €402k
Wetland €299k
SOC €299k
Top-up Option Budget
2030–2035
Volume: 138,000 tCO₂
Budget: € 500,000
Avg.: € 3.62 / tCO₂
Vol.
A/R 52k
Wetland 46k
SOC 40k
Budget
A/R €220k
Wetl. €140k
SOC €140k
Extension Option Budget
2036–2040
Volume: 215,000 tCO₂
Budget: € 800,000
Avg.: € 3.72 / tCO₂
Vol.
A/R 102k
Wetland 60k
SOC 53k
Budget
A/R €313k
Wetl. €242k
SOC €246k
Total Portfolio
2030–2040
Volume: 674,000 tCO₂
Budget: € 2,300,000
Avg.: € 3.41 / tCO₂
Vol.
A/R 310k
Wetland 195k
SOC 169k
Budget
A/R €935k
Wetl. €681k
SOC €685k
'''
# ============ 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' '
gantt_grid += f'{y} '
else:
gantt_grid += f' '
today_x = gx(2026)
gantt_grid += f' '
gantt_bars = ""
# Background panel behind project name labels
gantt_bars += f' '
gantt_bars += f' '
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' '
name_display = name if len(name) <= 35 else name[:33] + "…"
gantt_bars += f'{name_display} '
x1 = gx(max(cs, gantt_min_year))
x2 = gx(min(ce, gantt_max_year))
bar_w = max(x2 - x1, 4)
gantt_bars += f' '
# Always put label inside bar if bar is wide enough (>60px), regardless of height
if bar_w > 60:
gantt_bars += f'{contracted_k} '
elif bar_w > 30:
gantt_bars += f'{contracted_k} '
else:
gantt_bars += f'{contracted_k} '
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 = ''
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'
'
gantt_legend_html += '
'
project_timeline_html = f'''
{gantt_grid}
{gantt_bars}
{gantt_legend_html}
'''
# 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'''
{p["name"]} {p["pathway"]} {p["country"]}
{fmt_num(int(p["contracted_tco2"]))} Contracted
{fmt_num(int(p["delivered_tco2"]))} Delivered
{fmt_num(int(p["open_tco2"]))} Open
'''
# ============ 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'''
Total Issued
{fmt_num(total_issued)}
tCO₂
Retired
{fmt_num(total_retired_vint)}
tCO₂
Available to Retire
{fmt_num(total_available)}
tCO₂
Active Registries
{len(unique_standards)}
registries
'''
# 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 = ''
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'''
{ben_short}
{fmt_num(int(qty))} tCO₂ · {pct}%
'''
ben_bars_html += '
'
# Distribution by Purpose
purp_agg = retirements.groupby("purpose")["quantity_tco2"].sum().sort_values(ascending=False)
purp_bars_html = ''
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'''
{purp}
{fmt_num(int(qty))} tCO₂ · {pct}%
'''
purp_bars_html += '
'
# 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'''
{r["name"]}
{r["standard"]}
{fmt_num(int(r["issued"]))}
{fmt_num(int(r["available"]))}
{fmt_num(int(r["retired"]))}
'''
holdings_table = f'''
Project
Registry
Issued
Available
Retired
Progress
{holdings_rows}
'''
# 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'''
{date_str}
{r["beneficiary"]}
{proj_name}
{r["purpose"]}
{fmt_num(int(r["quantity_tco2"]))}
{r["serial_number"]}
'''
retirement_table = f'''
Date
Beneficiary
Project
Purpose
Quantity
Serial Number
{ret_rows}
'''
retirements_content = f'''{ret_kpis}
{ben_bars_html}
{purp_bars_html}
{holdings_table}
{retirement_table}'''
html = f"""
Welcome back Sign in to your NbS Portfolio
Total Volume Contracted
{fmt_num(total_contracted)}
tCO₂
Delivered
{fmt_num(total_delivered)}
tCO₂
Open
{fmt_num(total_open)}
tCO₂
Active Projects
{num_projects}
projects
{dist_bars_html}
Delivered
{fmt_num(total_delivered)} tCO₂
Open
{fmt_num(total_open)} tCO₂
{geo_map_html}
{region_legend}
{max_year_total//1000}K
{max_year_total*3//4//1000}K
{max_year_total//2//1000}K
{max_year_total//4//1000}K
0
{timeline_html}
{pathway_bars_html}
{activity_html}
{project_timeline_html}
{projects_list_html}
"""
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"])