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