""" 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_label}
{score}%
''' kpi_snapshot_html = f'
Cross-Portfolio KPI Snapshot
{kpi_bars_html}
' # 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'''
Portfolio Status Distribution
{n_total} Projects
On Track{n_green}
Watch{n_amber}
Alert / Critical{n_red}
''' # Escalation Protocol escalation_html = '''
Alarm Signal Escalation Protocol
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 = '' 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'{dots}' scorecard_html += '
ProjectCarbonEcologicalCommunityOperationalRiskOverall
{p["name"][:30]}{ol}
' # 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'''''' # 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}
Portfolio KPI Scorecard
{scorecard_html}
Alarm Feed
{n_alarms} Active
{alarm_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₂
{open_label}
' 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₂
{others_open_label}
' # 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₂
{open_label}
' # 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'
{bars}
{year_labels[y]}
' # 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'''
Delivery Schedule by Pathway (Current Portfolio)
{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'''
Forward Portfolio · Delivery Schedule (2030–2040)
Firm Contract
A/R
Wetland
SOC
+ Top-up Option
A/R option
Wetland option
SOC option
+ Extension Option
A/R ext.
Wetland ext.
SOC ext.
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₂
Budget Allocation by Project
Firm Contract
Top-up Option
Extension Option
Cordillera Azul, Peru
A/R · VERRA VCS
2030–2040
Volume: 310,000 tCO₂
Budget: € 935,000
Avg.: € 3.02 / tCO₂
Vol.
Firm 156k
Top-up 52k
Ext. 102k
Budget
€402k
€220k
€313k
Firm€ 2.58/t
Top-up€ 4.23/t
Ext.€ 3.07/t
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
Budget
€299k
€140k
€242k
Firm€ 3.36/t
Top-up€ 3.04/t
Ext.€ 4.03/t
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
Budget
€299k
€140k
€246k
Firm€ 3.93/t
Top-up€ 3.50/t
Ext.€ 4.64/t
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
Volume by Geography
Volume by Standard
Volume by Rating
''' # ============ 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'
{abbr}
' gantt_legend_html += '
' project_timeline_html = f'''
Project Timelines
{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"]))}
{pct_ret}%
''' holdings_table = f'''
Vintage Holdings by Project
{holdings_rows}
Project Registry Issued Available Retired Progress
''' # 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'''
Retirement History
{ret_rows}
Date Beneficiary Project Purpose Quantity Serial Number
''' retirements_content = f'''{ret_kpis}
Retirements by Beneficiary
{ben_bars_html}
Retirements by Purpose
{purp_bars_html}
{holdings_table} {retirement_table}''' html = f"""
""" 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"])