ALAMDIENG commited on
Commit
41b21d8
·
1 Parent(s): e672f33

feat(ui): complete Smart City Decision Intelligence HUD overhaul on 12-col grid with Chart.js curve and provenance audit

Browse files
Files changed (4) hide show
  1. docs/UI_UX_AUDIT.md +129 -0
  2. frontend/app.js +296 -89
  3. frontend/index.html +482 -301
  4. frontend/style.css +1041 -227
docs/UI_UX_AUDIT.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AETERNA AI v4.1 — UI/UX Audit & Refactor Report
2
+
3
+ **Branch**: `refactor/smartcity-forecast-ui`
4
+ **Date**: 2026-08-30
5
+ **Role**: Senior Product Designer, UI/UX Engineer & Responsible AI Reviewer
6
+ **Scope**: Complete UI/UX redesign and structural refactor of the Waste Forecast Intelligence page into a professional Smart City Decision Support & Operational Command Center.
7
+
8
+ ---
9
+
10
+ ## 1. Executive Summary & Design Vision
11
+
12
+ AETERNA AI is an environmental decision-intelligence platform and research prototype exploring spatial-temporal waste forecasting across the 44 sub-districts (*kecamatan*) of DKI Jakarta.
13
+
14
+ The previous iteration suffered from a vertically stacked output architecture that dumped raw technical data, creating excessive vertical scrolling, unbalanced visual hierarchy, and confusing layout scaling on laptops and mobile devices.
15
+
16
+ This refactor transforms the Forecast page into an **executive Smart City Decision Intelligence Dashboard** (inspired by Palantir / municipal command center interfaces). It prioritizes **Decision Clarity over Raw Data Density**, ensuring that stakeholders can immediately answer:
17
+ 1. **What will happen?** (Tonnage volume, trend curve, daily averages)
18
+ 2. **Where will it happen?** (High-contrast spatial routing, sub-district baseline comparisons)
19
+ 3. **How serious is it?** (Operational risk thresholds: Safe / Warning / Critical)
20
+ 4. **What operational response is suggested?** (15T compactor fleet sizing, crew allocation, collection hours)
21
+ 5. **How reliable is the data?** (Unambiguous provenance labels: Live Observed, Reference, Estimated, Forecast, Simulation, Mode B Pending).
22
+
23
+ ---
24
+
25
+ ## 2. Audit: UX Problems Identified & Solved
26
+
27
+ | # | Problem in Previous UI | Solution in Smart City Command Center Refactor |
28
+ |---|---|---|
29
+ | 1 | **Excessive Vertical Scrolling** | Unified into a structured 12-column responsive layout with 6 clear hierarchical sections. |
30
+ | 2 | **Confusing Grid Cell Placement** | Fixed CSS Grid distributing mismatched elements by establishing strict column containers (`.col-8`, `.col-4`, `.col-6`, etc.). |
31
+ | 3 | **Buried Decision Outputs** | Placed 4 high-contrast KPI cards (*Forecast Volume, Risk Level, Suggested Fleet, Forecast Readiness*) immediately below the control panel. |
32
+ | 4 | **Overly Bulky Config Panel** | Streamlined into a single-row control grid with quick horizon pills (`[1D] [3D] [7D] [14D] [30D]`) and collapsible scenario settings. |
33
+ | 5 | **Action Button Equality** | Made `RUN FORECAST` the dominant primary CTA (with glow & play icon), demoting `Export CSV` to a clean secondary action. |
34
+ | 6 | **Map Too Small / Distorted** | Expanded map to 8 columns (~440px desktop height), integrated Esri World Dark Canvas (zero watermark), and added a floating route status overlay. |
35
+ | 7 | **Crude Daily Timeline Cards** | Replaced compressed text boxes with an interactive **Chart.js Area/Line Chart** showing curve trends, peak day, minimum day, and daily averages. |
36
+ | 8 | **Plain-Text Waste Composition** | Converted into a segmented multi-bar visual and 2-column detailed progress bars with exact tonnages and percentages. |
37
+ | 9 | **Raw Debug-Style Logistics Block** | Redesigned into a 6-card operational grid with clear vehicle capacity (15T), crew requirements, throughput duration, and an expandable formula drawer. |
38
+ | 10 | **Misleading Statistical Confidence** | Renamed false "Confidence/Reliability" score to **Forecast Readiness (Model Indicator)** to honestly reflect model & data health rather than statistical probability. |
39
+ | 11 | **Excessive Monospace & Neon Glow** | Established modern typography hierarchy (`Outfit` / `Space Grotesk` for UI and headings, `JetBrains Mono` strictly for metadata/tags) and restrained borders. |
40
+ | 12 | **Mobile / Laptop Clipping** | Implemented fluid responsive breakpoints: 12-col desktop (>=1200px), 2-col tablet, and 1-col mobile with 0 horizontal overflow. |
41
+
42
+ ---
43
+
44
+ ## 3. Information Architecture & Section Hierarchy
45
+
46
+ ```
47
+ ┌────────────────────────────────────────────────────────────────────────────────────────┐
48
+ │ PAGE HEADER: Waste Forecast Intelligence (Context Pill: Menteng · 7D · Stacking v1.0) │
49
+ ├────────────────────────────────────────────────────────────────────────────────────────┤
50
+ │ 1. FORECAST CONTROL PANEL │
51
+ │ [Location Select] [Model Select] [Horizon Pills: 1D | 3D | 7D | 14D | 30D] │
52
+ │ [▸ Advanced Scenario Settings (Rainfall / Headcount Auto vs Manual Toggles)] │
53
+ │ [ ▶ RUN FORECAST ] (Dominant CTA) [ ⬇ Export CSV ] (Secondary) │
54
+ ├────────────────���───────────────────────────────────────────────────────────────────────┤
55
+ │ 2. DECISION SUMMARY — 4 COMPACT KPI CARDS │
56
+ │ [ FORECAST VOLUME ] [ RISK LEVEL ] [ SUGGESTED FLEET ] [ READINESS ] │
57
+ │ 539.48 T [FORECAST] SAFE [DERIVED] 40 Trucks [SIMULATION] 85.4% [MODEL] │
58
+ ├────────────────────────────────────────────────────────────────────────────────────────┤
59
+ │ 3. GEOSPATIAL & CONTEXT INTELLIGENCE (8 cols + 4 cols) │
60
+ │ Left (8 cols): Large Interactive Map (~440px) with Bantargebang Route & Overlay │
61
+ │ Right (4 cols): Live Weather (Open-Meteo) & Local Event / Crowd Activity Card │
62
+ ├────────────────────────────────────────────────────────────────────────────────────────┤
63
+ │ 4. FORECAST TIMELINE & TREND INTELLIGENCE (12 cols) │
64
+ │ Chart.js Area Curve with Tooltips | Summary: Total, Daily Avg, Peak Day, Min Day │
65
+ ├────────────────────────────────────────────────────────────────────────────────────────┤
66
+ │ 5. OPERATIONAL SCENARIO & WASTE COMPOSITION (6 cols + 6 cols) │
67
+ │ Left (6 cols): 6 Operational Metric Cards (Fleet, 15T Cap, Crew, Duration, Loads) │
68
+ │ Right (6 cols): Segmented Multi-Bar & Detailed Composition Breakdown (Organic...) │
69
+ ├────────────────────────────────────────────────────────────────────────────────────────┤
70
+ │ 6. HOURLY DISPATCH RISK & DATA PROVENANCE (7 cols + 5 cols) │
71
+ │ Left (7 cols): 24-Hour Diurnal Pressure Blocks with Tooltips & Legend │
72
+ │ Right (5 cols): Data Transparency Table (Live, Ref, Est, Forecast) & Notice Box │
73
+ └────────────────────────────────────────────────────────────────────────────────────────┘
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 4. Scientific Data Integrity & Provenance Guarantee
79
+
80
+ The UI refactor strictly adheres to Responsible AI and Scientific Transparency guidelines:
81
+
82
+ 1. **Explicit Data Badges Maintained**:
83
+ - `[LIVE OBSERVED]`: Real-time weather data fetched from Open-Meteo API.
84
+ - `[REFERENCE]`: Demographic headcount baseline from BPS DKI Jakarta.
85
+ - `[ESTIMATED]`: Baseline historical normal volumes.
86
+ - `[FORECAST]`: Model outputs from StackingRegressor or Chronos-T5.
87
+ - `[SIMULATION]`: Deterministic logistics calculations based on prototype operational assumptions.
88
+ - `[DERIVED PROPORTIONS]`: Fixed solid waste characterization percentages.
89
+ - `[MODE B PENDING]`: DLH DKI Jakarta field observation data pending data-sharing agreement.
90
+ 2. **No Fabricated Accuracy Claims**:
91
+ - Model readiness is framed as a **Model & Data Health Indicator**, not an operational efficiency percentage or real-world accuracy guarantee.
92
+ - Logistics outputs carry the prominent notice: `⚠️ SIMULATION — NOT OFFICIAL DLH OPERATIONAL INSTRUCTION`.
93
+ 3. **No Retraining or Parameter Alteration**:
94
+ - The ML inference pipeline, FastAPI endpoints (`/api/v1/predict`, `/api/v1/kecamatan`, `/api/v1/predict/csv`), and mathematical formulas in `services/logistics_engine.py` remain 100% untouched.
95
+
96
+ ---
97
+
98
+ ## 5. Files Modified in this Refactor
99
+
100
+ - **`frontend/index.html`**:
101
+ - Integrated Chart.js (`chart.umd.min.js`).
102
+ - Restructured `page-predictor` into the 12-column Smart City Command Center layout.
103
+ - Implemented Horizon quick pills, collapsible scenario drawer, 4 KPI cards, Chart canvas, and refined provenance table.
104
+ - **`frontend/style.css`**:
105
+ - Implemented design tokens (`--bg-primary`, `--bg-surface`, `--text-primary`, `--accent-primary`, `--status-live`, etc.).
106
+ - Added 12-column responsive grid classes and media queries for desktop, tablet, and mobile.
107
+ - Cleaned all legacy duplicate rules and CSS invert filters.
108
+ - **`frontend/app.js`**:
109
+ - Implemented `renderForecastChart()` using Chart.js with gradient fill and summary bar calculations.
110
+ - Added event listeners for quick horizon pills, scenario auto/manual toggles, and context badge updates.
111
+ - Enhanced map routing to display real-time distance and travel time in the floating map overlay.
112
+ - Preserved all existing API contracts and state handling.
113
+ - **`docs/UI_UX_AUDIT.md`**:
114
+ - Comprehensive documentation of design decisions, architecture, and verification.
115
+
116
+ ---
117
+
118
+ ## 6. Verification & Test Results
119
+
120
+ - **Automated Test Suite**:
121
+ ```bash
122
+ python -m pytest tests/ -v
123
+ ```
124
+ **Result**: `14 passed in 26.41s (100% PASS)`
125
+ - Schema integrity, provenance fields, logistics formulas, and news validation all passed.
126
+ - **Multi-Device Responsiveness**:
127
+ - Desktop (1440px / 1920px): Full 12-column HUD layout with high visual density.
128
+ - Laptop (1024px / 1280px): Compact, zero-clipping layout with smooth scaling.
129
+ - Mobile (375px - 428px): Single-column linear layout with touch-friendly controls and zero horizontal overflow.
frontend/app.js CHANGED
@@ -1,3 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  // Coordinates and Map Data for all 44 Kecamatan of DKI Jakarta
2
  const KECAMATAN_DATABASE = {
3
  // 1. JAKARTA PUSAT (8 Kecamatan)
@@ -393,8 +566,8 @@ function drawTransitRoute(locName) {
393
  routeLine = L.polyline([startCoords, BANTARGEBANG_COORDS], {
394
  color: '#00F0FF',
395
  weight: 3.5,
396
- opacity: 0.75,
397
- dashArray: '8, 8',
398
  className: 'glowing-route'
399
  }).addTo(map);
400
 
@@ -402,18 +575,26 @@ function drawTransitRoute(locName) {
402
  const roadDist = directDist * 1.35;
403
  const travelTimeHours = roadDist / 28.0;
404
 
 
 
 
 
 
 
 
 
405
  routeLine.bindPopup(`
406
  <div class="route-popup">
407
  <h3>LOGISTICS DISPATCH ROUTE</h3>
408
- <div>Kecamatan: <b>${locName}</b></div>
409
- <div>Destination: <b>TPST Bantargebang</b></div>
410
  <div>Transit Distance: <b class="highlight">${roadDist.toFixed(1)} km</b></div>
411
- <div>Est. Travel Time: <b class="highlight">${travelTimeHours.toFixed(1)} Hours</b></div>
412
  </div>
413
- `).openPopup();
414
 
415
  map.fitBounds([startCoords, BANTARGEBANG_COORDS], {
416
- padding: [60, 60]
417
  });
418
  }
419
 
@@ -536,11 +717,16 @@ async function runPrediction() {
536
 
537
  function updateDashboardData(data, confScore, message) {
538
  const results = data.prediction_results;
539
- if (results.length === 0) return;
540
 
541
  const totalVolume = results.reduce((acc, curr) => acc + curr.total_volume_ton, 0);
542
- if (statTotalVolume) statTotalVolume.innerHTML = `${totalVolume.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <span class="unit">Tons</span>`;
543
 
 
 
 
 
 
 
544
  let maxRisk = "SAFE";
545
  results.forEach(r => {
546
  if (r.risk_status === "CRITICAL") maxRisk = "CRITICAL";
@@ -549,35 +735,41 @@ function updateDashboardData(data, confScore, message) {
549
 
550
  if (statRiskStatus) {
551
  statRiskStatus.textContent = maxRisk;
552
- statRiskStatus.className = `card-value status-badge ${maxRisk.toLowerCase()}`;
553
  }
554
 
 
 
 
 
555
  updateMarkerRisk(selectedLocation, maxRisk);
556
  drawTransitRoute(selectedLocation);
557
 
558
- // Operational Logistics Plan parsing
559
  const logPlan = data.logistics_plan || {};
560
- const uiPres = logPlan.ui_presentation || {};
561
  const recFleet = logPlan.recommended_fleet || {};
562
  const manpowerObj = logPlan.manpower_breakdown || {};
563
  const colTimeObj = logPlan.collection_time || {};
564
  const effObj = logPlan.operational_efficiency || {};
565
  const relObj = logPlan.reliability || {};
566
 
567
- const trucksCount = logPlan.trucks_needed || recFleet.recommended_trucks || 0;
568
  const personnelCount = logPlan.manpower || manpowerObj.total_personnel || (trucksCount * 3);
569
  const durationHours = (logPlan.estimated_duration_hours !== undefined ? logPlan.estimated_duration_hours : (colTimeObj.adjusted_hours || 0.0)).toFixed(1);
570
  const truckLoadsVal = logPlan.required_truck_loads !== undefined ? Math.ceil(logPlan.required_truck_loads) : Math.ceil(totalVolume / 15.0);
571
 
572
- // 1. KPI Stat Recommended Fleet (15T)
573
- if (statTrucks) statTrucks.innerHTML = `${trucksCount} <span class="unit">Trucks (15T)</span>`;
574
 
575
- const startDateStr = results[0].date;
576
- const endDateStr = results[results.length - 1].date;
577
-
578
- if (statPeriodMeta) statPeriodMeta.textContent = `Period: ${startDateStr} to ${endDateStr}`;
579
- if (statLocationMeta) statLocationMeta.textContent = `${selectedLocation} (Radius ${KECAMATAN_DATABASE[selectedLocation].radius})`;
580
 
 
 
 
 
581
  const totalOrganic = results.reduce((acc, curr) => acc + curr.organic_waste_ton, 0);
582
  const totalPlastic = results.reduce((acc, curr) => acc + curr.plastic_waste_ton, 0);
583
  const totalPaper = results.reduce((acc, curr) => acc + curr.paper_waste_ton, 0);
@@ -593,30 +785,49 @@ function updateDashboardData(data, confScore, message) {
593
  if (valMetal) valMetal.textContent = `${totalMetal.toFixed(2)} Ton`;
594
 
595
  const getPct = (val) => totalVolume > 0 ? (val / totalVolume) * 100 : 0;
596
-
597
- if (barOrganic) barOrganic.style.width = `${getPct(totalOrganic)}%`;
598
- if (barPlastic) barPlastic.style.width = `${getPct(totalPlastic)}%`;
599
- if (barPaper) barPaper.style.width = `${getPct(totalPaper)}%`;
600
- if (barGlass) barGlass.style.width = `${getPct(totalGlass)}%`;
601
- if (barTextile) barTextile.style.width = `${getPct(totalTextile)}%`;
602
- if (barMetal) barMetal.style.width = `${getPct(totalMetal)}%`;
603
-
604
- // 2. Operational Logistics Plan 6 Cards
605
- const elFleet = document.getElementById("log-fleet") || logFleet;
606
- const elFleetSub = document.getElementById("log-fleet-sub") || logFleetSub;
607
- const elManpower = document.getElementById("log-manpower") || logManpower;
608
- const elManpowerSub = document.getElementById("log-manpower-sub") || logManpowerSub;
609
- const elDuration = document.getElementById("log-duration") || logDuration;
610
- const elDurationSub = document.getElementById("log-duration-sub") || logDurationSub;
611
- const elTruckLoads = document.getElementById("log-truck-loads") || logTruckLoads;
612
- const elTruckLoadsSub = document.getElementById("log-truck-loads-sub") || logTruckLoadsSub;
613
- const elEfficiency = document.getElementById("log-efficiency") || logEfficiency;
614
- const elEfficiencySub = document.getElementById("log-efficiency-sub") || logEfficiencySub;
615
- const elConfidence = document.getElementById("log-confidence") || logConfidence;
616
- const elConfidenceSub = document.getElementById("log-confidence-sub") || logConfidenceSub;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
 
618
  if (elFleet) elFleet.textContent = `${trucksCount} Trucks`;
619
- if (elFleetSub) elFleetSub.textContent = uiPres.fleet_subtitle || "15 ton capacity / truck";
620
 
621
  if (elManpower) elManpower.textContent = `${personnelCount} Personnel`;
622
  if (elManpowerSub) {
@@ -626,73 +837,47 @@ function updateDashboardData(data, confScore, message) {
626
  }
627
 
628
  if (elDuration) elDuration.textContent = `${durationHours} Hours`;
629
- if (elDurationSub) elDurationSub.textContent = uiPres.collection_time_subtitle || "Adjusted for traffic, weather & events";
630
 
631
  if (elTruckLoads) elTruckLoads.textContent = `~${truckLoadsVal} Loads`;
632
- if (elTruckLoadsSub) elTruckLoadsSub.textContent = "forecast volume ÷ 15T gross capacity";
633
 
634
  if (elEfficiency) elEfficiency.textContent = logPlan.efficiency_rate || effObj.display || "85% — Optimal";
635
  if (elEfficiencySub) elEfficiencySub.textContent = effObj.status ? `Status: ${effObj.status}` : "Multi-factor operational index";
636
 
637
- if (elConfidence) {
638
- const relScore = relObj.score_percent !== undefined ? relObj.score_percent.toFixed(1) : (confScore * 100).toFixed(1);
639
- elConfidence.textContent = `${relScore}%`;
640
- }
641
- if (elConfidenceSub) elConfidenceSub.textContent = "Empirical model quality & data health";
642
-
643
  const eventDay = results.find(r => r.event_info !== null);
644
  if (eventDay) {
645
  if (eventDescText) eventDescText.innerHTML = `⚠️ <strong>${eventDay.event_info}</strong> on ${eventDay.date}. Heavy crowd expected near site.`;
646
  const eBox = document.getElementById("event-box");
647
- if (eBox) eBox.style.borderColor = "var(--red)";
648
  } else {
649
  if (eventDescText) eventDescText.textContent = "No major public events scheduled for this location in the forecast window.";
650
  const eBox = document.getElementById("event-box");
651
- if (eBox) eBox.style.borderColor = "var(--yellow)";
652
  }
653
 
654
- if (timelineList) {
655
- timelineList.innerHTML = "";
656
- results.forEach(day => {
657
- const card = document.createElement("div");
658
- card.className = "timeline-card";
 
 
659
 
660
- const dateObj = new Date(day.date);
661
- const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'short' });
662
- const displayDate = `${dayName}, ${dateObj.getDate()} ${dateObj.toLocaleString('en-US', { month: 'short' })}`;
663
-
664
- card.innerHTML = `
665
- <span class="timeline-date">${displayDate}</span>
666
- <span class="timeline-vol">${day.total_volume_ton.toFixed(1)} T</span>
667
- <span class="timeline-status ${day.risk_status.toLowerCase()}">${day.risk_status}</span>
668
  `;
669
- timelineList.appendChild(card);
670
  });
671
  }
672
 
673
- const hourlyDay = results[0];
674
- if (hourlyDay && hourlyDay.hourly_breakdown) {
675
- if (hourlySection) hourlySection.style.display = "block";
676
- if (hourlyGrid) {
677
- hourlyGrid.innerHTML = "";
678
- hourlyDay.hourly_breakdown.forEach(hour => {
679
- const cell = document.createElement("div");
680
- cell.className = "hourly-cell";
681
-
682
- let intensityClass = "low";
683
- if (hour.risk_indicator === "MEDIUM") intensityClass = "medium";
684
- else if (hour.risk_indicator === "HIGH") intensityClass = "high";
685
-
686
- cell.innerHTML = `
687
- <div class="cell-block ${intensityClass}" title="Vol: ${hour.estimated_volume_ton} Ton - Risk: ${hour.risk_indicator}"></div>
688
- <span class="cell-time">${hour.hour}</span>
689
- `;
690
- hourlyGrid.appendChild(cell);
691
- });
692
- }
693
- } else {
694
- if (hourlySection) hourlySection.style.display = "none";
695
- }
696
  }
697
 
698
  // Request CSV from Backend API and download it
@@ -1448,3 +1633,25 @@ window.addEventListener("click", function(e) {
1448
  modal.style.display = "none";
1449
  }
1450
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Chart.js instance tracking
2
+ let forecastChartInstance = null;
3
+ let rainOverrideMode = 'auto'; // 'auto' | 'manual'
4
+ let popOverrideMode = 'auto'; // 'auto' | 'manual'
5
+
6
+ function setRainfallMode(mode) {
7
+ rainOverrideMode = mode;
8
+ const btnAuto = document.getElementById('rain-mode-auto');
9
+ const btnManual = document.getElementById('rain-mode-manual');
10
+ const slider = document.getElementById('rain-override');
11
+ const display = document.getElementById('rain-override-val');
12
+ if (mode === 'auto') {
13
+ if (btnAuto) btnAuto.classList.add('active');
14
+ if (btnManual) btnManual.classList.remove('active');
15
+ if (slider) { slider.disabled = true; slider.value = 0; }
16
+ rainValue = 0;
17
+ if (display) display.textContent = 'Auto (Open-Meteo)';
18
+ } else {
19
+ if (btnAuto) btnAuto.classList.remove('active');
20
+ if (btnManual) btnManual.classList.add('active');
21
+ if (slider) { slider.disabled = false; slider.value = slider.value > 0 ? slider.value : 15; }
22
+ rainValue = slider ? slider.value : 15;
23
+ if (display) display.textContent = `${rainValue} mm (Manual)`;
24
+ }
25
+ runPrediction();
26
+ }
27
+
28
+ function setPopulationMode(mode) {
29
+ popOverrideMode = mode;
30
+ const btnAuto = document.getElementById('event-mode-auto');
31
+ const btnManual = document.getElementById('event-mode-manual');
32
+ const slider = document.getElementById('event-override');
33
+ const display = document.getElementById('event-override-val');
34
+ const defaultPop = KECAMATAN_DATABASE[selectedLocation]?.population_jiwa || 100000;
35
+ if (mode === 'auto') {
36
+ if (btnAuto) btnAuto.classList.add('active');
37
+ if (btnManual) btnManual.classList.remove('active');
38
+ if (slider) { slider.disabled = true; slider.value = defaultPop; }
39
+ if (display) display.textContent = `Auto (${defaultPop.toLocaleString()} Jiwa)`;
40
+ } else {
41
+ if (btnAuto) btnAuto.classList.remove('active');
42
+ if (btnManual) btnManual.classList.add('active');
43
+ if (slider) { slider.disabled = false; }
44
+ const currentVal = slider ? parseInt(slider.value) : defaultPop;
45
+ if (display) display.textContent = `${currentVal.toLocaleString()} Jiwa (Manual)`;
46
+ }
47
+ runPrediction();
48
+ }
49
+
50
+ function toggleAdvancedScenario() {
51
+ const box = document.getElementById('advanced-scenario-box');
52
+ const btn = document.getElementById('advanced-scenario-toggle');
53
+ if (box && btn) {
54
+ const isClosed = box.style.display === 'none';
55
+ box.style.display = isClosed ? 'block' : 'none';
56
+ btn.classList.toggle('open', isClosed);
57
+ btn.setAttribute('aria-expanded', isClosed ? 'true' : 'false');
58
+ }
59
+ }
60
+
61
+ function updateContextBadge() {
62
+ const locText = document.getElementById('context-location-text');
63
+ const horizText = document.getElementById('context-horizon-text');
64
+ const modText = document.getElementById('context-model-text');
65
+ const city = KECAMATAN_DATABASE[selectedLocation]?.city || 'DKI Jakarta';
66
+ if (locText) locText.textContent = `${selectedLocation} · ${city}`;
67
+ if (horizText) {
68
+ const days = forecastSlider ? forecastSlider.value : 7;
69
+ horizText.textContent = `${days}-Day Horizon`;
70
+ }
71
+ if (modText) {
72
+ const m = modelSelect ? modelSelect.value : 'gradient_boosting';
73
+ modText.textContent = m === 'chronos' ? 'Chronos-T5 Neural' : 'Stacking Regressor';
74
+ }
75
+ }
76
+
77
+ // Render Interactive Forecast Curve using Chart.js
78
+ function renderForecastChart(results) {
79
+ const canvas = document.getElementById('forecast-chart');
80
+ if (!canvas || typeof Chart === 'undefined') return;
81
+
82
+ const labels = results.map(r => {
83
+ const d = new Date(r.date);
84
+ return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
85
+ });
86
+ const volumes = results.map(r => r.total_volume_ton);
87
+
88
+ // Summary Statistics Calculation
89
+ const total = volumes.reduce((a, b) => a + b, 0);
90
+ const avg = total / volumes.length;
91
+ let maxIdx = 0, minIdx = 0;
92
+ volumes.forEach((v, i) => {
93
+ if (v > volumes[maxIdx]) maxIdx = i;
94
+ if (v < volumes[minIdx]) minIdx = i;
95
+ });
96
+
97
+ const elTotal = document.getElementById('chart-total-vol');
98
+ const elAvg = document.getElementById('chart-avg-vol');
99
+ const elPeak = document.getElementById('chart-peak-day');
100
+ const elMin = document.getElementById('chart-min-day');
101
+
102
+ if (elTotal) elTotal.textContent = `${total.toFixed(2)} Tons`;
103
+ if (elAvg) elAvg.textContent = `${avg.toFixed(2)} Tons/Day`;
104
+ if (elPeak) elPeak.textContent = `${labels[maxIdx]} (${volumes[maxIdx].toFixed(1)} T)`;
105
+ if (elMin) elMin.textContent = `${labels[minIdx]} (${volumes[minIdx].toFixed(1)} T)`;
106
+
107
+ if (forecastChartInstance) {
108
+ forecastChartInstance.destroy();
109
+ }
110
+
111
+ const ctx = canvas.getContext('2d');
112
+ const gradient = ctx.createLinearGradient(0, 0, 0, 200);
113
+ gradient.addColorStop(0, 'rgba(112, 173, 71, 0.35)');
114
+ gradient.addColorStop(1, 'rgba(112, 173, 71, 0.0)');
115
+
116
+ forecastChartInstance = new Chart(ctx, {
117
+ type: 'line',
118
+ data: {
119
+ labels: labels,
120
+ datasets: [{
121
+ label: 'Projected Waste Generation (Tons)',
122
+ data: volumes,
123
+ borderColor: '#70AD47',
124
+ backgroundColor: gradient,
125
+ borderWidth: 2.5,
126
+ fill: true,
127
+ tension: 0.35,
128
+ pointBackgroundColor: volumes.map((v, i) => i === maxIdx ? '#F59E0B' : (i === minIdx ? '#22C55E' : '#70AD47')),
129
+ pointBorderColor: '#070C08',
130
+ pointBorderWidth: 2,
131
+ pointRadius: volumes.map((v, i) => (i === maxIdx || i === minIdx) ? 6 : 4),
132
+ pointHoverRadius: 7
133
+ }]
134
+ },
135
+ options: {
136
+ responsive: true,
137
+ maintainAspectRatio: false,
138
+ plugins: {
139
+ legend: { display: false },
140
+ tooltip: {
141
+ backgroundColor: 'rgba(13, 20, 15, 0.95)',
142
+ titleColor: '#F8FAF9',
143
+ bodyColor: '#00F0FF',
144
+ borderColor: 'rgba(112, 173, 71, 0.4)',
145
+ borderWidth: 1,
146
+ padding: 10,
147
+ callbacks: {
148
+ label: function(ctx) {
149
+ const val = ctx.parsed.y;
150
+ const r = results[ctx.dataIndex];
151
+ return [`Volume: ${val.toFixed(2)} Tons`, `Risk Level: ${r.risk_status}`, `Suggested Trucks: ${r.recommended_trucks}`];
152
+ }
153
+ }
154
+ }
155
+ },
156
+ scales: {
157
+ x: {
158
+ grid: { color: 'rgba(255, 255, 255, 0.05)' },
159
+ ticks: { color: '#88A385', font: { family: "'JetBrains Mono', monospace", size: 11 } }
160
+ },
161
+ y: {
162
+ grid: { color: 'rgba(255, 255, 255, 0.05)' },
163
+ ticks: {
164
+ color: '#88A385',
165
+ font: { family: "'JetBrains Mono', monospace", size: 11 },
166
+ callback: (v) => `${v} T`
167
+ }
168
+ }
169
+ }
170
+ }
171
+ });
172
+ }
173
+
174
  // Coordinates and Map Data for all 44 Kecamatan of DKI Jakarta
175
  const KECAMATAN_DATABASE = {
176
  // 1. JAKARTA PUSAT (8 Kecamatan)
 
566
  routeLine = L.polyline([startCoords, BANTARGEBANG_COORDS], {
567
  color: '#00F0FF',
568
  weight: 3.5,
569
+ opacity: 0.8,
570
+ dashArray: '6, 8',
571
  className: 'glowing-route'
572
  }).addTo(map);
573
 
 
575
  const roadDist = directDist * 1.35;
576
  const travelTimeHours = roadDist / 28.0;
577
 
578
+ // Update floating map overlay
579
+ const routeTarget = document.getElementById('route-target-name');
580
+ const routeDist = document.getElementById('route-distance-val');
581
+ const routeTime = document.getElementById('route-time-val');
582
+ if (routeTarget) routeTarget.textContent = `${locName} → TPST Bantargebang`;
583
+ if (routeDist) routeDist.textContent = `${roadDist.toFixed(1)} km`;
584
+ if (routeTime) routeTime.textContent = `${travelTimeHours.toFixed(1)} Hours`;
585
+
586
  routeLine.bindPopup(`
587
  <div class="route-popup">
588
  <h3>LOGISTICS DISPATCH ROUTE</h3>
589
+ <div>Origin: <b>${locName} (${KECAMATAN_DATABASE[locName]?.city || 'DKI Jakarta'})</b></div>
590
+ <div>Destination: <b>TPST Bantargebang (Bekasi)</b></div>
591
  <div>Transit Distance: <b class="highlight">${roadDist.toFixed(1)} km</b></div>
592
+ <div>Est. Transit Time: <b class="highlight">${travelTimeHours.toFixed(1)} Hours</b></div>
593
  </div>
594
+ `);
595
 
596
  map.fitBounds([startCoords, BANTARGEBANG_COORDS], {
597
+ padding: [40, 40]
598
  });
599
  }
600
 
 
717
 
718
  function updateDashboardData(data, confScore, message) {
719
  const results = data.prediction_results;
720
+ if (!results || results.length === 0) return;
721
 
722
  const totalVolume = results.reduce((acc, curr) => acc + curr.total_volume_ton, 0);
 
723
 
724
+ // 1. KPI Decision Summary: Forecast Volume
725
+ if (statTotalVolume) {
726
+ statTotalVolume.innerHTML = `${totalVolume.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <span class="unit">Tons</span>`;
727
+ }
728
+
729
+ // 2. KPI Decision Summary: Risk Level
730
  let maxRisk = "SAFE";
731
  results.forEach(r => {
732
  if (r.risk_status === "CRITICAL") maxRisk = "CRITICAL";
 
735
 
736
  if (statRiskStatus) {
737
  statRiskStatus.textContent = maxRisk;
738
+ statRiskStatus.className = `kpi-status-badge ${maxRisk.toLowerCase()}`;
739
  }
740
 
741
+ const baselineTon = KECAMATAN_DATABASE[selectedLocation]?.normal_avg || 100.0;
742
+ if (statPeriodMeta) statPeriodMeta.textContent = `Next ${results.length} days (${results[0].date} to ${results[results.length - 1].date})`;
743
+ if (statLocationMeta) statLocationMeta.textContent = `${selectedLocation} · Baseline: ${baselineTon.toFixed(1)} T/D`;
744
+
745
  updateMarkerRisk(selectedLocation, maxRisk);
746
  drawTransitRoute(selectedLocation);
747
 
748
+ // Logistics breakdown
749
  const logPlan = data.logistics_plan || {};
 
750
  const recFleet = logPlan.recommended_fleet || {};
751
  const manpowerObj = logPlan.manpower_breakdown || {};
752
  const colTimeObj = logPlan.collection_time || {};
753
  const effObj = logPlan.operational_efficiency || {};
754
  const relObj = logPlan.reliability || {};
755
 
756
+ const trucksCount = logPlan.trucks_needed || recFleet.recommended_trucks || Math.ceil(totalVolume / 14.25);
757
  const personnelCount = logPlan.manpower || manpowerObj.total_personnel || (trucksCount * 3);
758
  const durationHours = (logPlan.estimated_duration_hours !== undefined ? logPlan.estimated_duration_hours : (colTimeObj.adjusted_hours || 0.0)).toFixed(1);
759
  const truckLoadsVal = logPlan.required_truck_loads !== undefined ? Math.ceil(logPlan.required_truck_loads) : Math.ceil(totalVolume / 15.0);
760
 
761
+ // 3. KPI Decision Summary: Suggested Fleet
762
+ if (statTrucks) statTrucks.innerHTML = `${trucksCount} <span class="unit">Trucks</span>`;
763
 
764
+ // 4. KPI Decision Summary: Forecast Readiness
765
+ const readinessScore = relObj.score_percent !== undefined ? relObj.score_percent.toFixed(1) : (confScore * 100).toFixed(1);
766
+ const elReadiness = document.getElementById("stat-readiness") || logConfidence;
767
+ if (elReadiness) elReadiness.textContent = `${readinessScore}%`;
 
768
 
769
+ // 5. Chart.js Forecast Timeline Curve
770
+ renderForecastChart(results);
771
+
772
+ // 6. Waste Composition Breakdown
773
  const totalOrganic = results.reduce((acc, curr) => acc + curr.organic_waste_ton, 0);
774
  const totalPlastic = results.reduce((acc, curr) => acc + curr.plastic_waste_ton, 0);
775
  const totalPaper = results.reduce((acc, curr) => acc + curr.paper_waste_ton, 0);
 
785
  if (valMetal) valMetal.textContent = `${totalMetal.toFixed(2)} Ton`;
786
 
787
  const getPct = (val) => totalVolume > 0 ? (val / totalVolume) * 100 : 0;
788
+ const pctOrganic = getPct(totalOrganic);
789
+ const pctPlastic = getPct(totalPlastic);
790
+ const pctPaper = getPct(totalPaper);
791
+ const pctGlass = getPct(totalGlass);
792
+ const pctTextile = getPct(totalTextile);
793
+ const pctMetal = getPct(totalMetal);
794
+
795
+ if (barOrganic) barOrganic.style.width = `${pctOrganic}%`;
796
+ if (barPlastic) barPlastic.style.width = `${pctPlastic}%`;
797
+ if (barPaper) barPaper.style.width = `${pctPaper}%`;
798
+ if (barGlass) barGlass.style.width = `${pctGlass}%`;
799
+ if (barTextile) barTextile.style.width = `${pctTextile}%`;
800
+ if (barMetal) barMetal.style.width = `${pctMetal}%`;
801
+
802
+ // Stacked Segmented Bar
803
+ const segOrg = document.getElementById("seg-organic");
804
+ const segPla = document.getElementById("seg-plastic");
805
+ const segPap = document.getElementById("seg-paper");
806
+ const segGla = document.getElementById("seg-glass");
807
+ const segTex = document.getElementById("seg-textile");
808
+ const segMet = document.getElementById("seg-metal");
809
+
810
+ if (segOrg) segOrg.style.width = `${pctOrganic}%`;
811
+ if (segPla) segPla.style.width = `${pctPlastic}%`;
812
+ if (segPap) segPap.style.width = `${pctPaper}%`;
813
+ if (segGla) segGla.style.width = `${pctGlass}%`;
814
+ if (segTex) segTex.style.width = `${pctTextile}%`;
815
+ if (segMet) segMet.style.width = `${pctMetal}%`;
816
+
817
+ // 7. Operational Scenario Grid
818
+ const elFleet = document.getElementById("log-fleet");
819
+ const elFleetSub = document.getElementById("log-fleet-sub");
820
+ const elManpower = document.getElementById("log-manpower");
821
+ const elManpowerSub = document.getElementById("log-manpower-sub");
822
+ const elDuration = document.getElementById("log-duration");
823
+ const elDurationSub = document.getElementById("log-duration-sub");
824
+ const elTruckLoads = document.getElementById("log-truck-loads");
825
+ const elTruckLoadsSub = document.getElementById("log-truck-loads-sub");
826
+ const elEfficiency = document.getElementById("log-efficiency");
827
+ const elEfficiencySub = document.getElementById("log-efficiency-sub");
828
 
829
  if (elFleet) elFleet.textContent = `${trucksCount} Trucks`;
830
+ if (elFleetSub) elFleetSub.textContent = "15 ton capacity / truck";
831
 
832
  if (elManpower) elManpower.textContent = `${personnelCount} Personnel`;
833
  if (elManpowerSub) {
 
837
  }
838
 
839
  if (elDuration) elDuration.textContent = `${durationHours} Hours`;
840
+ if (elDurationSub) elDurationSub.textContent = "Throughput adjusted";
841
 
842
  if (elTruckLoads) elTruckLoads.textContent = `~${truckLoadsVal} Loads`;
843
+ if (elTruckLoadsSub) elTruckLoadsSub.textContent = "Volume ÷ 15T gross";
844
 
845
  if (elEfficiency) elEfficiency.textContent = logPlan.efficiency_rate || effObj.display || "85% — Optimal";
846
  if (elEfficiencySub) elEfficiencySub.textContent = effObj.status ? `Status: ${effObj.status}` : "Multi-factor operational index";
847
 
848
+ // 8. Event Schedule Update
 
 
 
 
 
849
  const eventDay = results.find(r => r.event_info !== null);
850
  if (eventDay) {
851
  if (eventDescText) eventDescText.innerHTML = `⚠️ <strong>${eventDay.event_info}</strong> on ${eventDay.date}. Heavy crowd expected near site.`;
852
  const eBox = document.getElementById("event-box");
853
+ if (eBox) eBox.style.borderColor = "var(--status-warning)";
854
  } else {
855
  if (eventDescText) eventDescText.textContent = "No major public events scheduled for this location in the forecast window.";
856
  const eBox = document.getElementById("event-box");
857
+ if (eBox) eBox.style.borderColor = "var(--border-subtle)";
858
  }
859
 
860
+ // 9. Hourly 24-hr Diurnal Risk Grid
861
+ const hourlyDay = results[0];
862
+ if (hourlyDay && hourlyDay.hourly_breakdown && hourlyGrid) {
863
+ hourlyGrid.innerHTML = "";
864
+ hourlyDay.hourly_breakdown.forEach(hour => {
865
+ const cell = document.createElement("div");
866
+ cell.className = "hourly-cell";
867
 
868
+ let intensityClass = "low";
869
+ if (hour.risk_indicator === "MEDIUM") intensityClass = "medium";
870
+ else if (hour.risk_indicator === "HIGH") intensityClass = "high";
871
+
872
+ cell.innerHTML = `
873
+ <div class="cell-block ${intensityClass}" title="${hour.hour} — Vol: ${hour.estimated_volume_ton} Ton (${hour.risk_indicator} Risk)"></div>
874
+ <span class="cell-time">${hour.hour}</span>
 
875
  `;
876
+ hourlyGrid.appendChild(cell);
877
  });
878
  }
879
 
880
+ updateContextBadge();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
881
  }
882
 
883
  // Request CSV from Backend API and download it
 
1633
  modal.style.display = "none";
1634
  }
1635
  });
1636
+
1637
+ // Horizon pill buttons setup
1638
+ document.addEventListener("DOMContentLoaded", () => {
1639
+ document.querySelectorAll(".horizon-pill").forEach(btn => {
1640
+ btn.addEventListener("click", (e) => {
1641
+ document.querySelectorAll(".horizon-pill").forEach(p => p.classList.remove("active"));
1642
+ btn.classList.add("active");
1643
+ const days = parseInt(btn.getAttribute("data-days"));
1644
+ if (forecastSlider) forecastSlider.value = days;
1645
+ if (forecastVal) forecastVal.textContent = days;
1646
+ updateContextBadge();
1647
+ runPrediction();
1648
+ });
1649
+ });
1650
+
1651
+ if (modelSelect) {
1652
+ modelSelect.addEventListener("change", () => {
1653
+ updateContextBadge();
1654
+ runPrediction();
1655
+ });
1656
+ }
1657
+ });
frontend/index.html CHANGED
@@ -56,12 +56,13 @@
56
 
57
  <!-- Leaflet.js Map Library -->
58
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
 
59
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
60
 
61
  <!-- Three.js 3D Library -->
62
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
63
 
64
- <link rel="stylesheet" href="/static/style.css?v=4.5.0">
65
 
66
  <!-- Schema.org Multi-Type JSON-LD Graph for Rapid Search Indexing -->
67
  <script type="application/ld+json">
@@ -396,370 +397,550 @@
396
  </section>
397
  </div>
398
 
 
399
  <!-- 3. PREDICTOR & SIMULATION PAGE -->
400
  <!-- 3. PREDICTOR & SIMULATION PAGE -->
401
  <div id="page-predictor" class="page-container">
402
- <!-- Main Dual-Column Simulation Workspace -->
403
- <div class="predictor-workspace">
404
- <!-- Left Column: Controls & Context -->
405
- <div class="predictor-sidebar-col">
406
- <!-- Forecast Configuration Panel -->
407
- <section class="panel control-panel">
408
- <div class="panel-header-compact">
409
- <h2 class="panel-title">FORECAST CONFIG</h2>
410
- <button type="button" class="info-pill-btn" onclick="openModelInfoModal()" title="View Model Architecture & Provenance">
411
- <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
412
- Model Info
413
- </button>
414
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
- <div class="control-group">
417
- <label for="location-select">Target Kecamatan</label>
418
- <select id="location-select" class="form-control">
419
- <!-- Populated dynamically -->
420
- </select>
421
- </div>
 
 
 
 
 
422
 
423
- <div class="control-group">
424
- <label for="model-select">Forecasting Engine</label>
425
- <select id="model-select" class="form-control">
426
- <option value="gradient_boosting" selected>Stacking Regressor Ensemble</option>
427
- <option value="chronos">Amazon Chronos-T5 Tiny</option>
428
- </select>
 
 
429
  </div>
430
-
431
- <div class="control-group">
432
- <div class="control-label-row">
433
- <label for="forecast-slider">Forecast Horizon</label>
434
- <span class="control-val-badge"><span id="forecast-val">7</span> Days</span>
435
- </div>
436
- <input type="range" id="forecast-slider" min="1" max="30" value="7" class="slider">
437
  </div>
 
 
 
438
 
439
- <div class="divider-compact"></div>
440
- <div class="section-micro-label">SCENARIO OVERRIDES</div>
 
 
 
 
 
441
 
442
- <div class="control-group">
443
- <div class="control-label-row">
444
- <label for="rain-override">Rainfall Override</label>
445
- <span id="rain-override-val" class="control-val-badge">Auto (Open-Meteo)</span>
 
 
 
 
 
 
 
 
 
 
 
446
  </div>
447
- <input type="range" id="rain-override" min="0" max="100" value="0" class="slider">
448
- </div>
449
 
450
- <div class="control-group">
451
- <div class="control-label-row">
452
- <label for="event-override">Population Headcount</label>
453
- <span id="event-override-val" class="control-val-badge">Auto (BPS Reference)</span>
 
 
 
 
 
 
 
 
 
454
  </div>
455
- <input type="range" id="event-override" min="10000" max="700000" step="5000" value="88000" class="slider">
456
  </div>
 
 
457
 
458
- <div class="button-row">
459
- <button id="predict-btn" class="action-btn">
460
- <span class="btn-text">RUN FORECAST</span>
461
- <span class="btn-glow"></span>
462
- </button>
463
- <button id="export-btn" class="action-btn secondary-btn">
464
- <span class="btn-text">EXPORT CSV</span>
465
- <span class="btn-glow"></span>
466
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  </div>
 
468
 
469
- <!-- Active Data Sources Ingestion Tags -->
470
- <div class="sources-embedded-strip">
471
- <span class="sources-strip-title">Data Ingestion Provenance</span>
472
- <div class="sources-tag-list">
473
- <span class="source-tag active" id="src-weather"><span class="dot"></span> Open-Meteo <span class="badge badge-observed">LIVE</span></span>
474
- <span class="source-tag" id="src-bps"><span class="dot"></span> BPS DKI <span class="badge badge-unverified">REF</span></span>
475
- <span class="source-tag" id="src-baseline"><span class="dot"></span> Baselines <span class="badge badge-estimated">ESTIMATED</span></span>
476
- <span class="source-tag" id="src-dlh"><span class="dot"></span> DLH Real <span class="badge badge-pending">PENDING</span></span>
 
 
 
477
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  </div>
479
- </section>
480
 
481
- <!-- Weather & Event Context Widget -->
482
- <section class="panel weather-event-panel">
483
- <div class="panel-header-compact">
484
- <h3 class="panel-title">WEATHER &amp; EVENTS</h3>
485
- <span class="badge badge-observed">LIVE API</span>
486
  </div>
487
- <div class="weather-grid">
488
- <div class="weather-info">
489
- <span class="weather-temp" id="weather-forecast-text">Fetching Live...</span>
490
- <span class="weather-label" id="weather-location-text">Jakarta, Indonesia</span>
491
  </div>
492
- <div class="weather-details">
493
- <div>Precipitation: <span id="weather-precip" class="highlight">0.0 mm</span></div>
494
- <div>Impact: <span id="weather-alert" class="highlight">Normal</span></div>
 
 
 
 
 
 
495
  </div>
496
  </div>
497
- <div class="event-box" id="event-box">
498
- <span class="event-title">Event &amp; Crowd Activity</span>
499
- <p class="event-desc" id="event-desc-text">No major events registered for today.</p>
 
 
 
 
 
500
  </div>
501
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  </div>
503
 
504
- <!-- Right Column: Map, Live Stats, Composition & Logistics -->
505
- <div class="predictor-main-col">
506
- <!-- Data Provenance Status Banner -->
507
- <div id="data-status-badge" class="data-status-banner" style="display:none;">
508
- <div class="status-banner-content">
509
- <div class="status-banner-icon">📊</div>
510
- <div class="status-banner-text">
511
- <div class="status-banner-title">
512
- <span style="font-weight: 700;">PROVENANCE STATUS:</span>
513
- <span id="data-status-text" class="status-highlight">FORECAST · Stacking v1.0 · Synthetic Baseline</span>
514
- </div>
515
- <p class="status-banner-disclaimer">
516
- ⚠️ <strong>Research Prototype Notice:</strong> Hasil prediksi dan simulasi di bawah ini merupakan estimasi riset. Validasi operasional penuh memerlukan data observasi DLH DKI Jakarta.
517
- </p>
518
- </div>
519
- </div>
 
 
520
  </div>
 
 
 
 
 
521
 
522
- <!-- Interactive Spatial Map Panel -->
523
- <section class="panel map-panel">
524
- <div class="panel-header-compact">
525
- <h2 class="panel-title">JAKARTA SPATIAL MAP</h2>
526
- <span class="badge badge-derived">44 KECAMATAN ZONES</span>
 
 
 
 
 
 
 
 
 
527
  </div>
528
- <div class="map-container">
529
- <div id="map"></div>
 
 
 
 
530
  </div>
531
- </section>
532
 
533
- <!-- High-Impact Live Stats Row -->
534
- <section class="stats-row">
535
- <div class="panel stat-card text-glow">
536
- <div class="stat-card-header">
537
- <span class="card-label">ESTIMATED VOLUME</span>
538
- <span class="badge badge-forecast">FORECAST</span>
539
- </div>
540
- <span id="stat-total-volume" class="card-value">0.00 <span class="unit">Tons</span></span>
541
- <span class="card-meta" id="stat-period-meta">Period: N/A</span>
542
  </div>
543
- <div class="panel stat-card">
544
- <div class="stat-card-header">
545
- <span class="card-label">RISK STATUS</span>
546
- <span class="badge badge-derived">DERIVED</span>
547
- </div>
548
- <span id="stat-risk-status" class="card-value status-badge safe">SAFE</span>
549
- <span class="card-meta" id="stat-location-meta">Menteng</span>
550
  </div>
551
- <div class="panel stat-card">
552
- <div class="stat-card-header">
553
- <span class="card-label">SUGGESTED FLEET</span>
554
- <span class="badge badge-simulation">SIMULATION</span>
555
- </div>
556
- <span id="stat-trucks" class="card-value">0 <span class="unit">Trucks (15T)</span></span>
557
- <span class="card-meta">15T compactor basis</span>
558
- </div>
559
- </section>
560
-
561
- <!-- Waste Composition Breakdown -->
562
- <section class="panel category-panel">
563
- <div class="panel-header-compact">
564
- <h3 class="panel-title">ESTIMATED WASTE COMPOSITION</h3>
565
- <span class="badge badge-derived">PROPORTIONS</span>
566
- </div>
567
- <div class="progress-container-grid">
568
- <div class="progress-item">
569
- <div class="progress-header">
570
- <span>Organic / Sisa Makanan (~50.2%)</span>
571
- <span id="val-organic">0.00 Ton</span>
572
- </div>
573
- <div class="progress-bar-bg">
574
- <div id="bar-organic" class="progress-bar-fill organic" style="width: 0%;"></div>
575
- </div>
576
- </div>
577
- <div class="progress-item">
578
- <div class="progress-header">
579
- <span>Plastic / Plastik (~22.8%)</span>
580
- <span id="val-plastic">0.00 Ton</span>
581
- </div>
582
- <div class="progress-bar-bg">
583
- <div id="bar-plastic" class="progress-bar-fill plastic" style="width: 0%;"></div>
584
- </div>
585
- </div>
586
- <div class="progress-item">
587
- <div class="progress-header">
588
- <span>Paper &amp; Cardboard (~11.5%)</span>
589
- <span id="val-paper">0.00 Ton</span>
590
- </div>
591
- <div class="progress-bar-bg">
592
- <div id="bar-paper" class="progress-bar-fill paper" style="width: 0%;"></div>
593
- </div>
594
  </div>
595
- <div class="progress-item">
596
- <div class="progress-header">
597
- <span>Glass &amp; Ceramics (~3.2%)</span>
598
- <span id="val-glass">0.00 Ton</span>
599
- </div>
600
- <div class="progress-bar-bg">
601
- <div id="bar-glass" class="progress-bar-fill glass" style="width: 0%;"></div>
602
- </div>
603
  </div>
604
- <div class="progress-item">
605
- <div class="progress-header">
606
- <span>Textile &amp; Leather (~4.2%)</span>
607
- <span id="val-textile">0.00 Ton</span>
608
- </div>
609
- <div class="progress-bar-bg">
610
- <div id="bar-textile" class="progress-bar-fill textile" style="width: 0%;"></div>
611
- </div>
612
  </div>
613
- <div class="progress-item">
614
- <div class="progress-header">
615
- <span>Metals &amp; Others (~8.1%)</span>
616
- <span id="val-metal">0.00 Ton</span>
617
- </div>
618
- <div class="progress-bar-bg">
619
- <div id="bar-metal" class="progress-bar-fill metal" style="width: 0%;"></div>
620
- </div>
621
  </div>
622
  </div>
623
- </section>
 
624
 
625
- <!-- Operational Logistics Simulation Panel -->
626
- <section class="panel logistics-panel">
627
- <div class="panel-header-compact">
628
- <div>
629
- <h3 class="panel-title">OPERATIONAL LOGISTICS SIMULATION</h3>
630
- <p class="panel-desc">Simulasi alokasi armada berdasarkan kapasitas 15T compactor dan throughput operasional.</p>
631
- </div>
632
- <div style="display: flex; gap: 8px; align-items: center;">
633
- <span class="badge badge-simulation">SIMULATION</span>
634
- <button type="button" class="how-calculated-btn" onclick="toggleLogisticsExplainability()" id="btn-how-calculated">
635
- <svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
636
- <span>Formula</span>
637
- </button>
638
- </div>
639
  </div>
 
 
640
 
641
- <div class="logistics-grid-compact">
642
- <div class="log-item">
643
- <div class="log-header-row">
644
- <span class="log-label">Suggested Fleet</span>
645
- <span class="badge badge-simulation">SIMULATION</span>
646
- </div>
647
- <span id="log-fleet" class="log-value">-- Trucks</span>
648
- <span id="log-fleet-sub" class="log-meta">15 ton capacity / truck</span>
649
- </div>
650
 
651
- <div class="log-item">
652
- <div class="log-header-row">
653
- <span class="log-label">Crew Requirement</span>
654
- <span class="badge badge-simulation">CREW</span>
655
- </div>
656
- <span id="log-manpower" class="log-value">-- Personnel</span>
657
- <span id="log-manpower-sub" class="log-meta">3 crew per truck</span>
658
  </div>
 
 
659
 
660
- <div class="log-item">
661
- <div class="log-header-row">
662
- <span class="log-label">Collection Duration</span>
663
- <span class="badge badge-simulation">TIME</span>
664
- </div>
665
- <span id="log-duration" class="log-value">-- Hours</span>
666
- <span id="log-duration-sub" class="log-meta">Throughput adjusted</span>
667
  </div>
 
 
668
 
669
- <div class="log-item">
670
- <div class="log-header-row">
671
- <span class="log-label">Required Truck Loads</span>
672
- <span class="badge badge-derived">TRIPS</span>
673
- </div>
674
- <span id="log-truck-loads" class="log-value">~-- Loads</span>
675
- <span id="log-truck-loads-sub" class="log-meta">Volume &divide; 15T</span>
676
  </div>
 
 
677
 
678
- <div class="log-item">
679
- <div class="log-header-row">
680
- <span class="log-label">Efficiency Index</span>
681
- <span class="badge badge-simulation">INDEX</span>
682
- </div>
683
- <span id="log-efficiency" class="log-value">--%</span>
684
- <span id="log-efficiency-sub" class="log-meta">Scenario score</span>
685
  </div>
 
 
686
 
687
- <div class="log-item">
688
- <div class="log-header-row">
689
- <span class="log-label">Reliability Index</span>
690
- <span class="badge badge-derived">CONFIDENCE</span>
691
- </div>
692
- <span id="log-confidence" class="log-value highlight">--%</span>
693
- <span id="log-confidence-sub" class="log-meta">Data confidence</span>
694
  </div>
 
695
  </div>
696
 
697
- <!-- Expandable Calculation Breakdown Drawer -->
698
- <div id="logistics-explain-box" class="logistics-explain-box" style="display: none;">
699
- <div class="explain-header">
700
- <strong>📖 Deterministic Logistics Formulas (15-Ton Standard)</strong>
701
- </div>
702
- <div class="explain-grid">
703
- <div class="explain-card">
704
- <span class="explain-title">1. Suggested Fleet (15T Baseline)</span>
705
- <p>
706
- <code>Effective = 15.0T &times; 95% = 14.25 Ton/trip</code><br>
707
- <code>Suggested Trucks = ceil(ceil(Waste / 14.25) &times; 1.05 Buffer)</code>
708
- </p>
709
- </div>
710
- <div class="explain-card">
711
- <span class="explain-title">2. Crew Sizing</span>
712
- <p>
713
- <code>1 Driver + 2 Sanitarian Collectors = 3 Personel/Truk</code><br>
714
- <code>Total Personnel = Suggested Trucks &times; 3</code>
715
- </p>
716
- </div>
717
- <div class="explain-card">
718
- <span class="explain-title">3. Collection Time (Throughput)</span>
719
- <p>
720
- <code>Throughput = Trucks &times; 2.0 Ton/Jam</code><br>
721
- <code>Hours = (Forecast / Throughput &times; Factors) / Efficiency</code>
722
- </p>
723
- </div>
724
- <div class="explain-card">
725
- <span class="explain-title">4. Efficiency &amp; Reliability</span>
726
- <p>
727
- <code>Efficiency = Fleet (35%) + Weather (20%) + Traffic (20%) + Event (15%) + Utilization (10%)</code><br>
728
- <code>Reliability = Model Quality (50%) + Data Verification (30%) + Horizon (20%)</code>
729
- </p>
730
- </div>
731
  </div>
 
732
  </div>
733
- </section>
734
- </div>
735
  </div>
736
 
737
- <!-- Full-Width Bottom Panels: Timeline & Hourly Breakdown -->
738
- <div class="predictor-bottom-workspace">
739
- <!-- Timeline Harian -->
740
- <section class="panel timeline-panel">
741
- <div class="panel-header-compact">
742
- <h3 class="panel-title">DAILY FORECAST TIMELINE</h3>
743
- <span class="badge badge-forecast">MULTI-DAY HORIZON</span>
 
 
 
 
 
 
 
 
 
 
744
  </div>
745
- <div class="timeline-scroll-container">
746
- <div id="timeline-list" class="timeline-cards-row">
747
- <div class="empty-timeline">Pilih kecamatan dan jalankan prediksi untuk melihat rincian timeline harian.</div>
 
 
 
 
748
  </div>
 
749
  </div>
750
  </section>
751
 
752
- <!-- Estimasi Risiko Per Jam -->
753
- <section id="hourly-section" class="panel hourly-panel" style="display: none;">
754
- <div class="panel-header-compact">
755
- <h3 class="panel-title">HOURLY DISPATCH RISK ESTIMATION</h3>
756
- <span class="badge badge-derived">24-HR DIURNAL PROFILE</span>
 
 
 
 
 
757
  </div>
758
- <div class="hourly-bar-container">
759
- <div id="hourly-grid" class="hourly-cells-grid">
760
- <!-- Dynamic 24-hr cells -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
  </div>
762
  </div>
 
 
 
 
 
 
 
 
763
  </section>
764
  </div>
765
  </div>
@@ -1177,6 +1358,6 @@
1177
  <div id="cursor-ring"></div>
1178
 
1179
  <!-- Scripts -->
1180
- <script src="/static/app.js?v=4.3.2"></script>
1181
  </body>
1182
  </html>
 
56
 
57
  <!-- Leaflet.js Map Library -->
58
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
59
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
60
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
61
 
62
  <!-- Three.js 3D Library -->
63
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
64
 
65
+ <link rel="stylesheet" href="/static/style.css?v=5.0.0">
66
 
67
  <!-- Schema.org Multi-Type JSON-LD Graph for Rapid Search Indexing -->
68
  <script type="application/ld+json">
 
397
  </section>
398
  </div>
399
 
400
+ <!-- 3. PREDICTOR & SIMULATION PAGE -->
401
  <!-- 3. PREDICTOR & SIMULATION PAGE -->
402
  <!-- 3. PREDICTOR & SIMULATION PAGE -->
403
  <div id="page-predictor" class="page-container">
404
+ <!-- Compact Page Header -->
405
+ <header class="page-header-hud">
406
+ <div class="header-text-group">
407
+ <h1 class="page-title-hud">Waste Forecast Intelligence</h1>
408
+ <p class="page-subtitle-hud">Forecast waste generation and explore operational scenarios across Jakarta.</p>
409
+ </div>
410
+ <div class="header-context-badge" id="context-summary-pill">
411
+ <span class="context-dot"></span>
412
+ <span id="context-location-text">Menteng · Jakarta Pusat</span>
413
+ <span class="context-divider">|</span>
414
+ <span id="context-horizon-text">7-Day Horizon</span>
415
+ <span class="context-divider">|</span>
416
+ <span id="context-model-text">Stacking Regressor</span>
417
+ </div>
418
+ </header>
419
+
420
+ <!-- 1. Forecast Control Panel (Compact 12-col Grid) -->
421
+ <section class="panel control-panel-hud">
422
+ <div class="control-grid-top">
423
+ <!-- Location Selector -->
424
+ <div class="control-item">
425
+ <label for="location-select" class="hud-label">
426
+ <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
427
+ Target Kecamatan
428
+ </label>
429
+ <select id="location-select" class="form-control-hud">
430
+ <!-- Populated dynamically -->
431
+ </select>
432
+ </div>
433
 
434
+ <!-- Model Engine Selector -->
435
+ <div class="control-item">
436
+ <label for="model-select" class="hud-label">
437
+ <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
438
+ Forecasting Engine
439
+ </label>
440
+ <select id="model-select" class="form-control-hud">
441
+ <option value="gradient_boosting" selected>Stacking Regressor Ensemble (Research Prototype)</option>
442
+ <option value="chronos">Amazon Chronos-T5 Tiny (Neural Time-Series)</option>
443
+ </select>
444
+ </div>
445
 
446
+ <!-- Forecast Horizon Quick Pills -->
447
+ <div class="control-item">
448
+ <div class="control-label-row">
449
+ <label for="forecast-slider" class="hud-label">
450
+ <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
451
+ Forecast Horizon
452
+ </label>
453
+ <span class="control-val-badge"><span id="forecast-val">7</span> Days</span>
454
  </div>
455
+ <div class="horizon-pill-group" id="horizon-pills">
456
+ <button type="button" class="horizon-pill" data-days="1">1D</button>
457
+ <button type="button" class="horizon-pill" data-days="3">3D</button>
458
+ <button type="button" class="horizon-pill active" data-days="7">7D</button>
459
+ <button type="button" class="horizon-pill" data-days="14">14D</button>
460
+ <button type="button" class="horizon-pill" data-days="30">30D</button>
 
461
  </div>
462
+ <input type="range" id="forecast-slider" min="1" max="30" value="7" class="slider-hud visually-hidden">
463
+ </div>
464
+ </div>
465
 
466
+ <!-- Collapsible Advanced Scenario Settings -->
467
+ <div class="advanced-scenario-wrapper">
468
+ <button type="button" class="advanced-toggle-btn" id="advanced-scenario-toggle" onclick="toggleAdvancedScenario()" aria-expanded="false">
469
+ <span class="toggle-icon">▸</span>
470
+ <span>Advanced Scenario Settings</span>
471
+ <span class="toggle-hint">(Rainfall &amp; Headcount Overrides)</span>
472
+ </button>
473
 
474
+ <div class="advanced-scenario-drawer" id="advanced-scenario-box" style="display: none;">
475
+ <div class="scenario-grid-hud">
476
+ <!-- Rainfall Override -->
477
+ <div class="scenario-card">
478
+ <div class="scenario-card-header">
479
+ <span class="scenario-label">Rainfall Override (mm)</span>
480
+ <div class="toggle-pill-wrap">
481
+ <button type="button" class="mode-toggle-btn active" id="rain-mode-auto" onclick="setRainfallMode('auto')">AUTO (Open-Meteo)</button>
482
+ <button type="button" class="mode-toggle-btn" id="rain-mode-manual" onclick="setRainfallMode('manual')">MANUAL</button>
483
+ </div>
484
+ </div>
485
+ <div class="slider-with-val">
486
+ <input type="range" id="rain-override" min="0" max="100" value="0" class="slider-hud" disabled>
487
+ <span id="rain-override-val" class="scenario-val-display">Auto (0.0 mm)</span>
488
+ </div>
489
  </div>
 
 
490
 
491
+ <!-- Population Headcount Override -->
492
+ <div class="scenario-card">
493
+ <div class="scenario-card-header">
494
+ <span class="scenario-label">Population Headcount (Jiwa)</span>
495
+ <div class="toggle-pill-wrap">
496
+ <button type="button" class="mode-toggle-btn active" id="event-mode-auto" onclick="setPopulationMode('auto')">AUTO (BPS)</button>
497
+ <button type="button" class="mode-toggle-btn" id="event-mode-manual" onclick="setPopulationMode('manual')">MANUAL</button>
498
+ </div>
499
+ </div>
500
+ <div class="slider-with-val">
501
+ <input type="range" id="event-override" min="10000" max="700000" step="5000" value="88000" class="slider-hud" disabled>
502
+ <span id="event-override-val" class="scenario-val-display">Auto (88,000 Jiwa)</span>
503
+ </div>
504
  </div>
 
505
  </div>
506
+ </div>
507
+ </div>
508
 
509
+ <!-- Action Bar -->
510
+ <div class="control-actions-bar">
511
+ <div class="left-actions">
512
+ <button type="button" class="model-info-link" onclick="openModelInfoModal()">
513
+ <svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
514
+ <span>Model Architecture &amp; Provenance</span>
515
+ </button>
516
+ </div>
517
+ <div class="right-actions">
518
+ <button id="export-btn" class="btn-hud-secondary" onclick="runExport()" title="Export Forecast Dataset as CSV">
519
+ <svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
520
+ <span>Export CSV</span>
521
+ </button>
522
+ <button id="predict-btn" class="btn-hud-primary" onclick="runPrediction()">
523
+ <svg class="btn-icon-play" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
524
+ <span class="btn-text">RUN FORECAST</span>
525
+ </button>
526
+ </div>
527
+ </div>
528
+ </section>
529
+
530
+ <!-- 2. Decision Summary (4 Compact KPI Cards) -->
531
+ <section class="decision-kpi-grid">
532
+ <!-- KPI 1: Forecast Volume -->
533
+ <div class="panel kpi-card-hud">
534
+ <div class="kpi-header">
535
+ <span class="kpi-label">Forecast Volume</span>
536
+ <span class="badge badge-forecast">FORECAST</span>
537
+ </div>
538
+ <div class="kpi-body">
539
+ <span id="stat-total-volume" class="kpi-value">0.00 <span class="unit">Tons</span></span>
540
+ <span class="kpi-subtext" id="stat-period-meta">Next 7 days</span>
541
+ </div>
542
+ </div>
543
+
544
+ <!-- KPI 2: Risk Level -->
545
+ <div class="panel kpi-card-hud">
546
+ <div class="kpi-header">
547
+ <span class="kpi-label">Risk Level</span>
548
+ <span class="badge badge-derived">DERIVED</span>
549
+ </div>
550
+ <div class="kpi-body">
551
+ <span id="stat-risk-status" class="kpi-status-badge safe">SAFE</span>
552
+ <span class="kpi-subtext" id="stat-location-meta">Menteng · Baseline: 135.5 T/D</span>
553
+ </div>
554
+ </div>
555
+
556
+ <!-- KPI 3: Suggested Fleet -->
557
+ <div class="panel kpi-card-hud">
558
+ <div class="kpi-header">
559
+ <span class="kpi-label">Suggested Fleet</span>
560
+ <span class="badge badge-simulation">SIMULATION</span>
561
+ </div>
562
+ <div class="kpi-body">
563
+ <span id="stat-trucks" class="kpi-value">0 <span class="unit">Trucks</span></span>
564
+ <span class="kpi-subtext">15T compactor basis</span>
565
+ </div>
566
+ </div>
567
+
568
+ <!-- KPI 4: Forecast Readiness -->
569
+ <div class="panel kpi-card-hud">
570
+ <div class="kpi-header">
571
+ <span class="kpi-label">Forecast Readiness</span>
572
+ <span class="badge badge-derived">MODEL INDICATOR</span>
573
+ </div>
574
+ <div class="kpi-body">
575
+ <span id="stat-readiness" class="kpi-value highlight-cyan">--%</span>
576
+ <span class="kpi-subtext">Model &amp; data health score</span>
577
+ </div>
578
+ </div>
579
+ </section>
580
+
581
+ <!-- 3. Spatial & Context Intelligence (8 cols + 4 cols) -->
582
+ <div class="dashboard-grid-12">
583
+ <!-- Left (8 cols): Large Interactive Spatial Map -->
584
+ <section class="col-8 panel map-panel-hud">
585
+ <div class="panel-header-hud">
586
+ <div class="header-title-group">
587
+ <h2 class="panel-heading">JAKARTA SPATIAL INTELLIGENCE</h2>
588
+ <span class="panel-caption">Geospatial waste routing &amp; dispatch radius across 44 sub-districts</span>
589
+ </div>
590
+ <div class="header-badges">
591
+ <span class="badge badge-derived">44 KECAMATAN ZONES</span>
592
+ <span class="badge badge-live">LIVE ROUTING</span>
593
  </div>
594
+ </div>
595
 
596
+ <div class="map-container-hud">
597
+ <div id="map"></div>
598
+ <div class="map-overlay-status" id="map-route-badge">
599
+ <div class="overlay-item">
600
+ <span class="overlay-label">TRANSIT ROUTE</span>
601
+ <span class="overlay-val" id="route-target-name">Menteng TPST Bantargebang</span>
602
+ </div>
603
+ <div class="overlay-divider"></div>
604
+ <div class="overlay-item">
605
+ <span class="overlay-label">EST. DISTANCE</span>
606
+ <span class="overlay-val highlight" id="route-distance-val">33.3 km</span>
607
  </div>
608
+ <div class="overlay-divider"></div>
609
+ <div class="overlay-item">
610
+ <span class="overlay-label">EST. TRANSIT</span>
611
+ <span class="overlay-val highlight" id="route-time-val">1.2 Hours</span>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </section>
616
+
617
+ <!-- Right (4 cols): Context Intelligence (Weather & Events) -->
618
+ <section class="col-4 panel context-panel-hud">
619
+ <div class="panel-header-hud">
620
+ <div class="header-title-group">
621
+ <h2 class="panel-heading">CONTEXT INTELLIGENCE</h2>
622
+ <span class="panel-caption">Live meteorological &amp; local crowd activity</span>
623
  </div>
624
+ </div>
625
 
626
+ <!-- Weather Card -->
627
+ <div class="context-card-hud weather-card-hud">
628
+ <div class="context-card-header">
629
+ <span class="context-card-title">METEOROLOGICAL CONDITIONS</span>
630
+ <span class="badge badge-live">LIVE OBSERVED</span>
631
  </div>
632
+ <div class="weather-display-row">
633
+ <div class="weather-main">
634
+ <span class="weather-temp-hud" id="weather-forecast-text">Fetching Live...</span>
635
+ <span class="weather-sub-hud" id="weather-location-text">Jakarta, Indonesia</span>
636
  </div>
637
+ <div class="weather-metrics">
638
+ <div class="metric-row">
639
+ <span class="metric-name">Precipitation:</span>
640
+ <span class="metric-val" id="weather-precip">0.0 mm</span>
641
+ </div>
642
+ <div class="metric-row">
643
+ <span class="metric-name">Weather Impact:</span>
644
+ <span class="metric-val" id="weather-alert">Normal</span>
645
+ </div>
646
  </div>
647
  </div>
648
+ <span class="context-source-tag">Source: Open-Meteo Forecast API (Realtime)</span>
649
+ </div>
650
+
651
+ <!-- Event Card -->
652
+ <div class="context-card-hud event-card-hud" id="event-box">
653
+ <div class="context-card-header">
654
+ <span class="context-card-title">EVENT &amp; CROWD SCHEDULE</span>
655
+ <span class="badge badge-derived">LOCAL SCHEDULE</span>
656
  </div>
657
+ <p class="event-desc-hud" id="event-desc-text">No major public events scheduled for this location in the forecast window.</p>
658
+ <span class="context-source-tag">Source: Manually Curated Event Schedule (2026)</span>
659
+ </div>
660
+ </section>
661
+ </div>
662
+
663
+ <!-- 4. Forecast Timeline & Trend Intelligence (12 cols) -->
664
+ <section class="panel timeline-chart-panel-hud">
665
+ <div class="panel-header-hud">
666
+ <div class="header-title-group">
667
+ <h2 class="panel-heading">7-DAY WASTE FORECAST TIMELINE</h2>
668
+ <span class="panel-caption">Daily projected tonnage trend with model confidence intervals</span>
669
+ </div>
670
+ <span class="badge badge-forecast">MULTI-DAY HORIZON</span>
671
  </div>
672
 
673
+ <!-- Chart Container -->
674
+ <div class="chart-wrapper-hud">
675
+ <canvas id="forecast-chart" height="200"></canvas>
676
+ </div>
677
+
678
+ <!-- Chart Summary Bar -->
679
+ <div class="chart-summary-bar">
680
+ <div class="chart-summary-item">
681
+ <span class="summary-label">Total Forecast</span>
682
+ <span class="summary-val" id="chart-total-vol">0.00 Tons</span>
683
+ </div>
684
+ <div class="chart-summary-item">
685
+ <span class="summary-label">Daily Average</span>
686
+ <span class="summary-val" id="chart-avg-vol">0.00 Tons/Day</span>
687
+ </div>
688
+ <div class="chart-summary-item">
689
+ <span class="summary-label">Peak Forecast Day</span>
690
+ <span class="summary-val highlight-amber" id="chart-peak-day">--</span>
691
  </div>
692
+ <div class="chart-summary-item">
693
+ <span class="summary-label">Minimum Day</span>
694
+ <span class="summary-val highlight-green" id="chart-min-day">--</span>
695
+ </div>
696
+ </div>
697
 
698
+ <!-- Daily Cards Fallback Grid -->
699
+ <div id="timeline-list" class="timeline-cards-row visually-hidden">
700
+ <!-- Populated dynamically -->
701
+ </div>
702
+ </section>
703
+
704
+ <!-- 5. Operational Scenario & Waste Composition (6 cols + 6 cols) -->
705
+ <div class="dashboard-grid-12">
706
+ <!-- Left (6 cols): Operational Scenario Simulation -->
707
+ <section class="col-6 panel operational-panel-hud">
708
+ <div class="panel-header-hud">
709
+ <div class="header-title-group">
710
+ <h2 class="panel-heading">OPERATIONAL SCENARIO</h2>
711
+ <span class="panel-caption">Prototype logistics simulation based on configurable operational assumptions.</span>
712
  </div>
713
+ <div class="header-badges">
714
+ <span class="badge badge-simulation">SIMULATION</span>
715
+ <button type="button" class="btn-formula-toggle" onclick="toggleLogisticsExplainability()" id="btn-how-calculated">
716
+ <svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
717
+ <span>Formula</span>
718
+ </button>
719
  </div>
720
+ </div>
721
 
722
+ <div class="operational-grid-hud">
723
+ <div class="op-card">
724
+ <span class="op-label">Suggested Fleet</span>
725
+ <span class="op-val" id="log-fleet">-- Trucks</span>
726
+ <span class="op-sub" id="log-fleet-sub">15 ton capacity / truck</span>
 
 
 
 
727
  </div>
728
+ <div class="op-card">
729
+ <span class="op-label">Vehicle Capacity</span>
730
+ <span class="op-val" id="log-capacity">15.0 Tons</span>
731
+ <span class="op-sub">Gross standard compactor</span>
 
 
 
732
  </div>
733
+ <div class="op-card">
734
+ <span class="op-label">Crew Requirement</span>
735
+ <span class="op-val" id="log-manpower">-- Personnel</span>
736
+ <span class="op-sub" id="log-manpower-sub">3 crew per truck</span>
737
+ </div>
738
+ <div class="op-card">
739
+ <span class="op-label">Estimated Collection Time</span>
740
+ <span class="op-val" id="log-duration">-- Hours</span>
741
+ <span class="op-sub" id="log-duration-sub">Throughput adjusted</span>
742
+ </div>
743
+ <div class="op-card">
744
+ <span class="op-label">Required Loads</span>
745
+ <span class="op-val" id="log-truck-loads">~-- Loads</span>
746
+ <span class="op-sub" id="log-truck-loads-sub">Volume ÷ 15T gross</span>
747
+ </div>
748
+ <div class="op-card">
749
+ <span class="op-label">Scenario Score</span>
750
+ <span class="op-val highlight" id="log-efficiency">--%</span>
751
+ <span class="op-sub" id="log-efficiency-sub">Multi-factor operational index</span>
752
+ </div>
753
+ </div>
754
+
755
+ <div class="operational-assumptions-strip">
756
+ <span class="assumptions-tag">Assumptions: 15T Compactor Capacity · Traffic Factor · Weather Adjustment · Event Buffer</span>
757
+ <span class="disclaimer-micro">⚠️ SIMULATION — NOT OFFICIAL DLH OPERATIONAL INSTRUCTION</span>
758
+ </div>
759
+
760
+ <!-- Expandable Formula Breakdown Drawer -->
761
+ <div id="logistics-explain-box" class="logistics-explain-box" style="display: none;">
762
+ <div class="explain-header">
763
+ <strong>📖 Deterministic Logistics Formulas (15-Ton Standard)</strong>
764
+ </div>
765
+ <div class="explain-grid">
766
+ <div class="explain-card">
767
+ <span class="explain-title">1. Suggested Fleet (15T Baseline)</span>
768
+ <p>
769
+ <code>Effective = 15.0T &times; 95% = 14.25 Ton/trip</code><br>
770
+ <code>Suggested Trucks = ceil(ceil(Waste / 14.25) &times; 1.05 Buffer)</code>
771
+ </p>
 
 
 
 
772
  </div>
773
+ <div class="explain-card">
774
+ <span class="explain-title">2. Crew Sizing</span>
775
+ <p>
776
+ <code>1 Driver + 2 Sanitarian Collectors = 3 Personel/Truk</code><br>
777
+ <code>Total Personnel = Suggested Trucks &times; 3</code>
778
+ </p>
 
 
779
  </div>
780
+ <div class="explain-card">
781
+ <span class="explain-title">3. Collection Time (Throughput)</span>
782
+ <p>
783
+ <code>Throughput = Trucks &times; 2.0 Ton/Jam</code><br>
784
+ <code>Hours = (Forecast / Throughput &times; Factors) / Efficiency</code>
785
+ </p>
 
 
786
  </div>
787
+ <div class="explain-card">
788
+ <span class="explain-title">4. Efficiency &amp; Reliability</span>
789
+ <p>
790
+ <code>Efficiency = Fleet (35%) + Weather (20%) + Traffic (20%) + Event (15%) + Utilization (10%)</code><br>
791
+ <code>Reliability = Model Quality (50%) + Data Verification (30%) + Horizon (20%)</code>
792
+ </p>
 
 
793
  </div>
794
  </div>
795
+ </div>
796
+ </section>
797
 
798
+ <!-- Right (6 cols): Waste Composition -->
799
+ <section class="col-6 panel composition-panel-hud">
800
+ <div class="panel-header-hud">
801
+ <div class="header-title-group">
802
+ <h2 class="panel-heading">ESTIMATED WASTE COMPOSITION</h2>
803
+ <span class="panel-caption">Municipal solid waste characterization proportions</span>
 
 
 
 
 
 
 
 
804
  </div>
805
+ <span class="badge badge-derived">DERIVED PROPORTIONS</span>
806
+ </div>
807
 
808
+ <!-- Segmented Stacked Multi-Bar Visual -->
809
+ <div class="composition-stacked-bar">
810
+ <div class="segment organic" id="seg-organic" style="width: 50.2%;" title="Organic: 50.2%"></div>
811
+ <div class="segment plastic" id="seg-plastic" style="width: 22.8%;" title="Plastic: 22.8%"></div>
812
+ <div class="segment paper" id="seg-paper" style="width: 11.5%;" title="Paper: 11.5%"></div>
813
+ <div class="segment glass" id="seg-glass" style="width: 3.2%;" title="Glass: 3.2%"></div>
814
+ <div class="segment textile" id="seg-textile" style="width: 4.2%;" title="Textile: 4.2%"></div>
815
+ <div class="segment metal" id="seg-metal" style="width: 8.1%;" title="Metals & Others: 8.1%"></div>
816
+ </div>
817
 
818
+ <!-- Detailed Rows Grid -->
819
+ <div class="composition-rows-grid">
820
+ <div class="comp-row">
821
+ <div class="comp-header">
822
+ <span class="comp-name"><span class="dot dot-organic"></span> Organic / Sisa Makanan (~50.2%)</span>
823
+ <span class="comp-ton" id="val-organic">0.00 Ton</span>
 
824
  </div>
825
+ <div class="comp-bar-bg"><div id="bar-organic" class="comp-bar-fill organic" style="width: 0%;"></div></div>
826
+ </div>
827
 
828
+ <div class="comp-row">
829
+ <div class="comp-header">
830
+ <span class="comp-name"><span class="dot dot-plastic"></span> Plastic / Plastik (~22.8%)</span>
831
+ <span class="comp-ton" id="val-plastic">0.00 Ton</span>
 
 
 
832
  </div>
833
+ <div class="comp-bar-bg"><div id="bar-plastic" class="comp-bar-fill plastic" style="width: 0%;"></div></div>
834
+ </div>
835
 
836
+ <div class="comp-row">
837
+ <div class="comp-header">
838
+ <span class="comp-name"><span class="dot dot-paper"></span> Paper &amp; Cardboard (~11.5%)</span>
839
+ <span class="comp-ton" id="val-paper">0.00 Ton</span>
 
 
 
840
  </div>
841
+ <div class="comp-bar-bg"><div id="bar-paper" class="comp-bar-fill paper" style="width: 0%;"></div></div>
842
+ </div>
843
 
844
+ <div class="comp-row">
845
+ <div class="comp-header">
846
+ <span class="comp-name"><span class="dot dot-glass"></span> Glass &amp; Ceramics (~3.2%)</span>
847
+ <span class="comp-ton" id="val-glass">0.00 Ton</span>
 
 
 
848
  </div>
849
+ <div class="comp-bar-bg"><div id="bar-glass" class="comp-bar-fill glass" style="width: 0%;"></div></div>
850
+ </div>
851
 
852
+ <div class="comp-row">
853
+ <div class="comp-header">
854
+ <span class="comp-name"><span class="dot dot-textile"></span> Textile &amp; Leather (~4.2%)</span>
855
+ <span class="comp-ton" id="val-textile">0.00 Ton</span>
 
 
 
856
  </div>
857
+ <div class="comp-bar-bg"><div id="bar-textile" class="comp-bar-fill textile" style="width: 0%;"></div></div>
858
  </div>
859
 
860
+ <div class="comp-row">
861
+ <div class="comp-header">
862
+ <span class="comp-name"><span class="dot dot-metal"></span> Metals &amp; Others (~8.1%)</span>
863
+ <span class="comp-ton" id="val-metal">0.00 Ton</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
864
  </div>
865
+ <div class="comp-bar-bg"><div id="bar-metal" class="comp-bar-fill metal" style="width: 0%;"></div></div>
866
  </div>
867
+ </div>
868
+ </section>
869
  </div>
870
 
871
+ <!-- 6. Hourly Dispatch Risk & Data Provenance (7 cols + 5 cols) -->
872
+ <div class="dashboard-grid-12">
873
+ <!-- Left (7 cols): Hourly Dispatch Risk Estimation -->
874
+ <section class="col-7 panel hourly-panel-hud" id="hourly-section">
875
+ <div class="panel-header-hud">
876
+ <div class="header-title-group">
877
+ <h2 class="panel-heading">HOURLY DISPATCH RISK PROFILE</h2>
878
+ <span class="panel-caption">24-hour diurnal operational pressure &amp; congestion index</span>
879
+ </div>
880
+ <span class="badge badge-derived">24-HR DIURNAL PROFILE</span>
881
+ </div>
882
+
883
+ <!-- 24-hr Grid Container -->
884
+ <div class="hourly-bar-container">
885
+ <div id="hourly-grid" class="hourly-cells-grid">
886
+ <!-- Populated dynamically -->
887
+ </div>
888
  </div>
889
+
890
+ <!-- Legend & Factors -->
891
+ <div class="hourly-footer-hud">
892
+ <div class="hourly-legend">
893
+ <span class="legend-item"><span class="legend-swatch low"></span> Low</span>
894
+ <span class="legend-item"><span class="legend-swatch medium"></span> Moderate</span>
895
+ <span class="legend-item"><span class="legend-swatch high"></span> High Risk</span>
896
  </div>
897
+ <span class="hourly-factors-note">Factors: Diurnal Activity (60%) + Traffic Delay (25%) + Weather Factor (15%)</span>
898
  </div>
899
  </section>
900
 
901
+ <!-- Right (5 cols): Data Provenance & Scientific Integrity -->
902
+ <section class="col-5 panel provenance-panel-hud">
903
+ <div class="panel-header-hud">
904
+ <div class="header-title-group">
905
+ <h2 class="panel-heading">DATA PROVENANCE &amp; METHODOLOGY</h2>
906
+ <span class="panel-caption">Transparent source classification &amp; scientific audit</span>
907
+ </div>
908
+ <button type="button" class="btn-methodology-link" onclick="switchPage('page-methodology')">
909
+ <span>View Docs →</span>
910
+ </button>
911
  </div>
912
+
913
+ <!-- Provenance Status Rows -->
914
+ <div class="provenance-status-table">
915
+ <div class="prov-row">
916
+ <span class="prov-source"><span class="dot dot-live"></span> Open-Meteo Weather API</span>
917
+ <span class="badge badge-live">LIVE OBSERVED</span>
918
+ </div>
919
+ <div class="prov-row">
920
+ <span class="prov-source"><span class="dot dot-ref"></span> BPS DKI Demographic Reference</span>
921
+ <span class="badge badge-unverified">REFERENCE</span>
922
+ </div>
923
+ <div class="prov-row">
924
+ <span class="prov-source"><span class="dot dot-est"></span> Spatial Baseline Waste Statistics</span>
925
+ <span class="badge badge-estimated">ESTIMATED</span>
926
+ </div>
927
+ <div class="prov-row">
928
+ <span class="prov-source"><span class="dot dot-model"></span> AETERNA Stacking ML Model</span>
929
+ <span class="badge badge-forecast">FORECAST</span>
930
+ </div>
931
+ <div class="prov-row">
932
+ <span class="prov-source"><span class="dot dot-pending"></span> DLH DKI Ground Truth Data</span>
933
+ <span class="badge badge-pending">MODE B PENDING</span>
934
  </div>
935
  </div>
936
+
937
+ <!-- Research Prototype Disclaimer Box -->
938
+ <div class="research-disclaimer-box">
939
+ <div class="disclaimer-icon">ℹ️</div>
940
+ <p class="disclaimer-text">
941
+ <strong>Research Prototype Notice:</strong> Forecasts and operational outputs are decision-support estimates. Real-world validation requires authoritative observed data from DLH DKI Jakarta.
942
+ </p>
943
+ </div>
944
  </section>
945
  </div>
946
  </div>
 
1358
  <div id="cursor-ring"></div>
1359
 
1360
  <!-- Scripts -->
1361
+ <script src="/static/app.js?v=5.0.0"></script>
1362
  </body>
1363
  </html>
frontend/style.css CHANGED
@@ -455,319 +455,1133 @@ body {
455
  white-space: nowrap;
456
  }
457
 
458
- /* Grid Layout */
459
- .dashboard-grid {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  display: grid;
461
- grid-template-columns: 320px 1fr 340px;
462
- gap: 1.5rem;
463
- padding: 0.75rem 1rem;
464
- flex: 1;
465
- z-index: 5;
466
- position: relative;
467
  }
468
 
469
- @media (max-width: 1200px) {
470
- .dashboard-grid {
471
- grid-template-columns: 1fr;
472
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  }
474
 
475
- /* Panels */
476
- .panel {
477
- background: var(--bg-panel);
478
- border: 1px solid var(--border-color);
479
- border-radius: 12px;
480
- padding: 1.5rem;
481
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
482
- transition: border-color 0.3s, box-shadow 0.3s, transform 0.3s;
483
- backdrop-filter: blur(12px);
484
- -webkit-backdrop-filter: blur(12px);
485
  }
486
 
487
- .panel:hover {
488
- border-color: var(--border-hover);
489
- box-shadow: 0 10px 25px rgba(112, 173, 71, 0.1) !important;
 
490
  }
491
 
492
- .panel-title {
493
- font-family: var(--font-display);
494
- font-size: 0.95rem;
495
- font-weight: 600;
496
- letter-spacing: 1.5px;
497
- color: var(--text-main);
498
- margin-bottom: 1.2rem;
499
- border-left: 3px solid var(--cyan);
500
- padding-left: 8px;
501
- text-transform: uppercase;
 
502
  }
503
 
504
- .panel-subtitle {
505
- font-family: var(--font-display);
506
- font-size: 0.85rem;
507
- font-weight: 600;
508
- color: var(--cyan);
 
 
 
 
 
 
 
 
 
 
509
  margin-bottom: 1rem;
510
- text-transform: uppercase;
 
 
 
511
  }
512
 
513
- /* Controls */
514
- .control-group {
515
- margin-bottom: 1.2rem;
 
 
516
  }
517
 
518
- .control-group label {
519
- display: block;
520
- font-size: 0.8rem;
521
- color: var(--text-muted);
522
- margin-bottom: 6px;
523
- text-transform: uppercase;
524
  font-family: var(--font-mono);
 
 
 
 
 
 
525
  }
526
 
527
- .form-control {
 
 
 
 
528
  width: 100%;
529
- background: rgba(0, 0, 0, 0.3);
530
- border: 1px solid var(--border-color);
531
- color: var(--text-main);
532
- padding: 10px 14px;
533
- border-radius: 8px;
534
  outline: none;
535
  font-family: var(--font-body);
536
- font-size: 0.9rem;
537
- transition: border-color 0.3s, box-shadow 0.3s;
 
538
  }
539
 
540
- .form-control:focus {
541
- border-color: var(--cyan);
542
- box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.1);
543
  }
544
 
545
- .slider {
 
 
 
546
  width: 100%;
547
- -webkit-appearance: none;
548
- height: 6px;
549
- border-radius: 3px;
550
- background: rgba(0, 0, 0, 0.08);
551
- outline: none;
552
  }
553
 
554
- .slider::-webkit-slider-thumb {
555
- -webkit-appearance: none;
556
- width: 16px;
557
- height: 16px;
558
- border-radius: 50%;
559
- background: var(--cyan);
 
 
 
 
560
  cursor: pointer;
561
- transition: transform 0.1s;
 
562
  }
563
 
564
- .slider::-webkit-slider-thumb:hover {
565
- transform: scale(1.25);
 
 
566
  }
567
 
568
- .value-display, .override-display {
569
- font-family: var(--font-mono);
570
- color: var(--cyan);
571
- font-weight: bold;
572
- float: right;
 
573
  }
574
 
575
- .range-override-container {
576
- display: flex;
577
- flex-direction: column;
578
- gap: 5px;
 
579
  }
580
 
581
- .divider {
582
- height: 1px;
583
- background: var(--border-color);
584
- margin: 1.5rem 0;
 
 
 
 
 
 
 
 
585
  }
586
 
587
- /* Buttons */
588
- .action-btn {
589
- width: 100%;
590
- padding: 12px;
591
- background: var(--cyan);
592
- border: 1px solid var(--cyan);
593
- color: #FFFFFF;
594
- font-family: var(--font-display);
595
- font-size: 0.90rem;
596
- font-weight: bold;
597
- border-radius: 8px;
598
- cursor: pointer;
599
- position: relative;
600
- overflow: hidden;
601
- transition: background 0.3s, box-shadow 0.3s, transform 0.2s;
602
  }
603
 
604
- .action-btn:hover {
605
- background: #047857;
606
- box-shadow: 0 4px 12px rgba(5, 150, 105, 0.25);
607
- transform: translateY(-1px);
608
  }
609
 
610
- .action-btn:active {
611
- transform: translateY(1px);
612
  }
613
 
614
- /* Map Panel with Leaflet Map */
615
- .map-panel {
616
- display: flex;
617
- flex-direction: column;
618
- height: 380px;
619
- margin-bottom: 1.5rem;
620
  }
621
 
622
- .map-container {
623
- flex: 1;
624
- position: relative;
625
- background: rgba(0, 0, 0, 0.3);
 
626
  border-radius: 8px;
627
- overflow: hidden;
628
- border: 1px solid var(--border-color);
629
  }
630
 
631
- #map {
632
- width: 100%;
633
- height: 100%;
634
- background: var(--bg-void) !important;
635
  }
636
 
637
- /* Custom Marker Styling for Leaflet */
638
- .leaflet-custom-marker {
639
- position: absolute;
640
- width: 24px !important;
641
- height: 24px !important;
642
- /* Allow Leaflet's dynamic engine to set margins from iconAnchor without CSS overrides */
643
- transform-origin: center center;
644
- backface-visibility: hidden;
645
- -webkit-backface-visibility: hidden;
646
- cursor: pointer;
647
  }
648
 
649
- .leaflet-custom-marker .marker-pulse {
650
- position: absolute;
651
- top: 50%;
652
- left: 50%;
653
- width: 28px;
654
- height: 28px;
655
- margin-left: -14px;
656
- margin-top: -14px;
657
- background: var(--cyan);
658
- border-radius: 50%;
659
- opacity: 0.15;
660
- transform: scale(1);
661
- animation: pulse-node 2s infinite;
662
- pointer-events: none;
663
  }
664
 
665
- .leaflet-custom-marker .marker-core {
666
- position: absolute;
667
- top: 50%;
668
- left: 50%;
669
- width: 12px;
670
- height: 12px;
671
- margin-left: -6px;
672
- margin-top: -6px;
673
- background: var(--cyan);
674
- border: 2px solid #FFF;
675
- border-radius: 50%;
676
- box-shadow: 0 0 10px var(--cyan-glow);
677
- transition: transform 0.2s, background 0.3s;
678
  }
679
 
680
- .leaflet-custom-marker .marker-label {
681
- position: absolute;
682
- top: -24px;
683
- left: 50%;
684
- transform: translateX(-50%);
 
 
 
 
 
 
 
 
685
  font-family: var(--font-mono);
686
- font-size: 10px;
687
- font-weight: bold;
688
- color: var(--text-main);
689
- background: rgba(14, 22, 16, 0.95);
690
  padding: 2px 6px;
691
- border-radius: 4px;
692
- border: 1px solid var(--border-color);
693
- white-space: nowrap;
694
- pointer-events: none;
695
- transition: color 0.3s, border-color 0.3s;
696
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
697
  }
698
 
699
- /* Hover and Active State */
700
- .leaflet-custom-marker:hover .marker-core {
701
- transform: scale(1.2);
 
702
  }
703
 
704
- .leaflet-custom-marker.active .marker-core {
705
- transform: scale(1.3);
706
- background: var(--cyan);
707
- box-shadow: 0 0 10px var(--cyan);
708
  }
709
 
710
- .leaflet-custom-marker.active .marker-label {
711
- color: var(--cyan);
712
- border-color: var(--cyan);
713
- box-shadow: 0 4px 12px rgba(5, 150, 105, 0.15);
714
  }
715
 
716
- /* Risk indicator classes for Leaflet Markers */
717
- .leaflet-custom-marker.safe .marker-core {
718
- background: var(--green);
719
- box-shadow: 0 0 8px rgba(16, 185, 129, 0.3);
 
 
 
720
  }
721
- .leaflet-custom-marker.safe .marker-pulse {
722
- background: var(--green);
 
 
 
 
 
 
 
 
 
723
  }
724
 
725
- .leaflet-custom-marker.warning .marker-core {
726
- background: var(--yellow);
727
- box-shadow: 0 0 8px rgba(217, 119, 6, 0.3);
 
 
 
 
 
 
 
 
728
  }
729
- .leaflet-custom-marker.warning .marker-pulse {
730
- background: var(--yellow);
 
731
  }
732
 
733
- .leaflet-custom-marker.critical .marker-core {
734
- background: var(--red);
735
- box-shadow: 0 0 8px rgba(239, 68, 68, 0.3);
 
736
  }
737
- .leaflet-custom-marker.critical .marker-pulse {
738
- background: var(--red);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
  }
740
 
741
- /* Leaflet Layout UI Overrides */
742
- .leaflet-bar {
743
- border: 1px solid var(--border-color) !important;
744
- border-radius: 6px !important;
745
- overflow: hidden;
746
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5) !important;
747
  }
748
 
749
- .leaflet-bar a {
750
- background-color: rgba(14, 22, 16, 0.95) !important;
751
- color: var(--text-main) !important;
752
- border-bottom: 1px solid var(--border-color) !important;
753
- transition: background-color 0.2s, color 0.2s;
754
- width: 28px !important;
755
- height: 28px !important;
756
- line-height: 28px !important;
757
  }
758
 
759
- .leaflet-bar a:hover {
760
- background-color: rgba(0, 242, 254, 0.15) !important;
761
- color: var(--cyan) !important;
 
 
 
 
 
 
 
 
 
 
 
762
  }
763
 
764
- .leaflet-container {
765
- background: #111 !important;
 
 
766
  }
767
 
768
- .leaflet-tile {
769
- /* Pure native rendering for Esri Dark Canvas */
770
- filter: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  }
772
 
773
  /* Route Popup */
 
455
  white-space: nowrap;
456
  }
457
 
458
+ /* ==========================================
459
+ SMART CITY DECISION INTELLIGENCE HUD (12-COL)
460
+ ========================================== */
461
+
462
+ /* Design Tokens & CSS Variables */
463
+ :root {
464
+ --bg-primary: #060A07;
465
+ --bg-surface: rgba(13, 20, 15, 0.88);
466
+ --bg-elevated: rgba(20, 30, 23, 0.95);
467
+ --border-subtle: rgba(255, 255, 255, 0.08);
468
+ --border-hover: rgba(112, 173, 71, 0.35);
469
+ --border-focus: rgba(112, 173, 71, 0.6);
470
+
471
+ --text-primary: #F8FAF9;
472
+ --text-secondary: #94A398;
473
+ --text-muted: #64746B;
474
+
475
+ --accent-primary: #70AD47;
476
+ --accent-hover: #5d9239;
477
+
478
+ --status-live: #00F0FF;
479
+ --status-estimated: #F59E0B;
480
+ --status-forecast: #22C55E;
481
+ --status-simulation: #38BDF8;
482
+ --status-derived: #A855F7;
483
+ --status-warning: #F59E0B;
484
+ --status-danger: #EF4444;
485
+ }
486
+
487
+ /* 12-Column Responsive Grid System */
488
+ .dashboard-grid-12 {
489
  display: grid;
490
+ grid-template-columns: repeat(12, 1fr);
491
+ gap: 1rem;
492
+ width: 100%;
493
+ margin-bottom: 1rem;
 
 
494
  }
495
 
496
+ .col-12 { grid-column: span 12; }
497
+ .col-8 { grid-column: span 8; }
498
+ .col-7 { grid-column: span 7; }
499
+ .col-6 { grid-column: span 6; }
500
+ .col-5 { grid-column: span 5; }
501
+ .col-4 { grid-column: span 4; }
502
+ .col-3 { grid-column: span 3; }
503
+
504
+ /* Page Header HUD */
505
+ .page-header-hud {
506
+ display: flex;
507
+ justify-content: space-between;
508
+ align-items: center;
509
+ margin-bottom: 1.1rem;
510
+ padding-bottom: 0.85rem;
511
+ border-bottom: 1px solid var(--border-subtle);
512
+ flex-wrap: wrap;
513
+ gap: 12px;
514
  }
515
 
516
+ .page-title-hud {
517
+ font-family: var(--font-display);
518
+ font-size: 1.55rem;
519
+ font-weight: 700;
520
+ letter-spacing: 0.5px;
521
+ color: var(--text-primary);
522
+ margin: 0;
523
+ line-height: 1.2;
 
 
524
  }
525
 
526
+ .page-subtitle-hud {
527
+ font-size: 0.82rem;
528
+ color: var(--text-secondary);
529
+ margin: 3px 0 0 0;
530
  }
531
 
532
+ .header-context-badge {
533
+ display: inline-flex;
534
+ align-items: center;
535
+ gap: 8px;
536
+ background: rgba(112, 173, 71, 0.08);
537
+ border: 1px solid rgba(112, 173, 71, 0.25);
538
+ padding: 6px 14px;
539
+ border-radius: 20px;
540
+ font-family: var(--font-mono);
541
+ font-size: 0.75rem;
542
+ color: var(--text-primary);
543
  }
544
 
545
+ .context-dot {
546
+ width: 6px;
547
+ height: 6px;
548
+ border-radius: 50%;
549
+ background: var(--accent-primary);
550
+ box-shadow: 0 0 8px var(--accent-primary);
551
+ }
552
+
553
+ .context-divider {
554
+ color: var(--text-muted);
555
+ opacity: 0.5;
556
+ }
557
+
558
+ /* Control Panel HUD */
559
+ .control-panel-hud {
560
  margin-bottom: 1rem;
561
+ background: var(--bg-surface);
562
+ border: 1px solid var(--border-subtle);
563
+ border-radius: 10px;
564
+ padding: 1.1rem 1.3rem;
565
  }
566
 
567
+ .control-grid-top {
568
+ display: grid;
569
+ grid-template-columns: 1fr 1.2fr 1.1fr;
570
+ gap: 1.2rem;
571
+ align-items: flex-start;
572
  }
573
 
574
+ .hud-label {
575
+ display: inline-flex;
576
+ align-items: center;
577
+ gap: 6px;
 
 
578
  font-family: var(--font-mono);
579
+ font-size: 0.74rem;
580
+ font-weight: 600;
581
+ color: var(--text-secondary);
582
+ text-transform: uppercase;
583
+ letter-spacing: 0.5px;
584
+ margin-bottom: 6px;
585
  }
586
 
587
+ .hud-label svg {
588
+ color: var(--accent-primary);
589
+ }
590
+
591
+ .form-control-hud {
592
  width: 100%;
593
+ background: rgba(0, 0, 0, 0.45);
594
+ border: 1px solid var(--border-subtle);
595
+ color: var(--text-primary);
596
+ padding: 8px 12px;
597
+ border-radius: 7px;
598
  outline: none;
599
  font-family: var(--font-body);
600
+ font-size: 0.84rem;
601
+ transition: all 0.2s ease;
602
+ box-sizing: border-box;
603
  }
604
 
605
+ .form-control-hud:focus {
606
+ border-color: var(--accent-primary);
607
+ box-shadow: 0 0 0 2px rgba(112, 173, 71, 0.2);
608
  }
609
 
610
+ /* Horizon Quick Pills */
611
+ .horizon-pill-group {
612
+ display: flex;
613
+ gap: 6px;
614
  width: 100%;
 
 
 
 
 
615
  }
616
 
617
+ .horizon-pill {
618
+ flex: 1;
619
+ padding: 7px 4px;
620
+ background: rgba(255, 255, 255, 0.04);
621
+ border: 1px solid var(--border-subtle);
622
+ border-radius: 6px;
623
+ color: var(--text-secondary);
624
+ font-family: var(--font-mono);
625
+ font-size: 0.76rem;
626
+ font-weight: 600;
627
  cursor: pointer;
628
+ text-align: center;
629
+ transition: all 0.2s ease;
630
  }
631
 
632
+ .horizon-pill:hover {
633
+ background: rgba(255, 255, 255, 0.08);
634
+ color: var(--text-primary);
635
+ border-color: rgba(255, 255, 255, 0.15);
636
  }
637
 
638
+ .horizon-pill.active {
639
+ background: var(--accent-primary);
640
+ border-color: var(--accent-primary);
641
+ color: #050B08;
642
+ font-weight: 700;
643
+ box-shadow: 0 0 12px rgba(112, 173, 71, 0.4);
644
  }
645
 
646
+ /* Collapsible Advanced Scenario Settings */
647
+ .advanced-scenario-wrapper {
648
+ margin-top: 1rem;
649
+ padding-top: 0.85rem;
650
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
651
  }
652
 
653
+ .advanced-toggle-btn {
654
+ display: inline-flex;
655
+ align-items: center;
656
+ gap: 6px;
657
+ background: transparent;
658
+ border: none;
659
+ color: var(--text-secondary);
660
+ font-family: var(--font-mono);
661
+ font-size: 0.75rem;
662
+ cursor: pointer;
663
+ padding: 0;
664
+ transition: color 0.2s;
665
  }
666
 
667
+ .advanced-toggle-btn:hover {
668
+ color: var(--text-primary);
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  }
670
 
671
+ .advanced-toggle-btn .toggle-icon {
672
+ font-size: 0.7rem;
673
+ transition: transform 0.2s ease;
 
674
  }
675
 
676
+ .advanced-toggle-btn.open .toggle-icon {
677
+ transform: rotate(90deg);
678
  }
679
 
680
+ .advanced-toggle-btn .toggle-hint {
681
+ color: var(--text-muted);
682
+ font-size: 0.7rem;
 
 
 
683
  }
684
 
685
+ .advanced-scenario-drawer {
686
+ margin-top: 0.85rem;
687
+ padding: 1rem;
688
+ background: rgba(0, 0, 0, 0.35);
689
+ border: 1px solid var(--border-subtle);
690
  border-radius: 8px;
 
 
691
  }
692
 
693
+ .scenario-grid-hud {
694
+ display: grid;
695
+ grid-template-columns: 1fr 1fr;
696
+ gap: 1.2rem;
697
  }
698
 
699
+ .scenario-card {
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: 6px;
 
 
 
 
 
 
703
  }
704
 
705
+ .scenario-card-header {
706
+ display: flex;
707
+ justify-content: space-between;
708
+ align-items: center;
709
+ margin-bottom: 2px;
 
 
 
 
 
 
 
 
 
710
  }
711
 
712
+ .scenario-label {
713
+ font-family: var(--font-mono);
714
+ font-size: 0.72rem;
715
+ color: var(--text-secondary);
716
+ text-transform: uppercase;
 
 
 
 
 
 
 
 
717
  }
718
 
719
+ .toggle-pill-wrap {
720
+ display: inline-flex;
721
+ background: rgba(0, 0, 0, 0.4);
722
+ border: 1px solid var(--border-subtle);
723
+ border-radius: 4px;
724
+ padding: 2px;
725
+ gap: 2px;
726
+ }
727
+
728
+ .mode-toggle-btn {
729
+ background: transparent;
730
+ border: none;
731
+ color: var(--text-muted);
732
  font-family: var(--font-mono);
733
+ font-size: 0.65rem;
 
 
 
734
  padding: 2px 6px;
735
+ border-radius: 3px;
736
+ cursor: pointer;
737
+ transition: all 0.15s ease;
 
 
 
738
  }
739
 
740
+ .mode-toggle-btn.active {
741
+ background: rgba(112, 173, 71, 0.2);
742
+ color: var(--accent-primary);
743
+ font-weight: 700;
744
  }
745
 
746
+ .slider-with-val {
747
+ display: flex;
748
+ align-items: center;
749
+ gap: 12px;
750
  }
751
 
752
+ .slider-with-val input[type="range"] {
753
+ flex: 1;
 
 
754
  }
755
 
756
+ .scenario-val-display {
757
+ font-family: var(--font-mono);
758
+ font-size: 0.72rem;
759
+ color: var(--text-secondary);
760
+ white-space: nowrap;
761
+ min-width: 110px;
762
+ text-align: right;
763
  }
764
+
765
+ /* Control Actions Bar */
766
+ .control-actions-bar {
767
+ display: flex;
768
+ justify-content: space-between;
769
+ align-items: center;
770
+ margin-top: 1.1rem;
771
+ padding-top: 0.85rem;
772
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
773
+ flex-wrap: wrap;
774
+ gap: 10px;
775
  }
776
 
777
+ .model-info-link {
778
+ display: inline-flex;
779
+ align-items: center;
780
+ gap: 6px;
781
+ background: transparent;
782
+ border: none;
783
+ color: var(--text-muted);
784
+ font-family: var(--font-mono);
785
+ font-size: 0.72rem;
786
+ cursor: pointer;
787
+ transition: color 0.2s ease;
788
  }
789
+
790
+ .model-info-link:hover {
791
+ color: var(--accent-primary);
792
  }
793
 
794
+ .right-actions {
795
+ display: flex;
796
+ align-items: center;
797
+ gap: 8px;
798
  }
799
+
800
+ .btn-hud-primary {
801
+ display: inline-flex;
802
+ align-items: center;
803
+ justify-content: center;
804
+ gap: 8px;
805
+ padding: 9px 20px;
806
+ background: var(--accent-primary);
807
+ border: 1px solid var(--accent-primary);
808
+ color: #050B08;
809
+ font-family: var(--font-display);
810
+ font-size: 0.85rem;
811
+ font-weight: 700;
812
+ letter-spacing: 0.5px;
813
+ border-radius: 7px;
814
+ cursor: pointer;
815
+ transition: all 0.2s ease;
816
+ box-shadow: 0 0 16px rgba(112, 173, 71, 0.35);
817
  }
818
 
819
+ .btn-hud-primary:hover {
820
+ background: var(--accent-hover);
821
+ box-shadow: 0 0 22px rgba(112, 173, 71, 0.55);
822
+ transform: translateY(-1px);
 
 
823
  }
824
 
825
+ .btn-hud-primary:disabled {
826
+ opacity: 0.6;
827
+ cursor: not-allowed;
828
+ transform: none;
 
 
 
 
829
  }
830
 
831
+ .btn-hud-secondary {
832
+ display: inline-flex;
833
+ align-items: center;
834
+ gap: 6px;
835
+ padding: 8px 14px;
836
+ background: rgba(255, 255, 255, 0.04);
837
+ border: 1px solid var(--border-subtle);
838
+ color: var(--text-secondary);
839
+ font-family: var(--font-mono);
840
+ font-size: 0.76rem;
841
+ font-weight: 600;
842
+ border-radius: 7px;
843
+ cursor: pointer;
844
+ transition: all 0.2s ease;
845
  }
846
 
847
+ .btn-hud-secondary:hover {
848
+ background: rgba(255, 255, 255, 0.08);
849
+ border-color: rgba(255, 255, 255, 0.2);
850
+ color: var(--text-primary);
851
  }
852
 
853
+ /* 2. Decision KPI Grid (4 Cards) */
854
+ .decision-kpi-grid {
855
+ display: grid;
856
+ grid-template-columns: repeat(4, 1fr);
857
+ gap: 0.9rem;
858
+ margin-bottom: 1rem;
859
+ }
860
+
861
+ .kpi-card-hud {
862
+ padding: 0.9rem 1.1rem;
863
+ background: var(--bg-surface);
864
+ border: 1px solid var(--border-subtle);
865
+ border-radius: 8px;
866
+ display: flex;
867
+ flex-direction: column;
868
+ justify-content: space-between;
869
+ min-height: 88px;
870
+ transition: border-color 0.2s, box-shadow 0.2s;
871
+ }
872
+
873
+ .kpi-card-hud:hover {
874
+ border-color: var(--border-hover);
875
+ }
876
+
877
+ .kpi-header {
878
+ display: flex;
879
+ justify-content: space-between;
880
+ align-items: center;
881
+ margin-bottom: 4px;
882
+ }
883
+
884
+ .kpi-label {
885
+ font-family: var(--font-mono);
886
+ font-size: 0.7rem;
887
+ font-weight: 600;
888
+ color: var(--text-secondary);
889
+ text-transform: uppercase;
890
+ letter-spacing: 0.5px;
891
+ }
892
+
893
+ .kpi-body {
894
+ display: flex;
895
+ flex-direction: column;
896
+ }
897
+
898
+ .kpi-value {
899
+ font-family: var(--font-display);
900
+ font-size: 1.55rem;
901
+ font-weight: 800;
902
+ color: var(--text-primary);
903
+ line-height: 1.15;
904
+ }
905
+
906
+ .kpi-value .unit {
907
+ font-size: 0.8rem;
908
+ font-weight: 400;
909
+ color: var(--text-muted);
910
+ }
911
+
912
+ .kpi-value.highlight-cyan {
913
+ color: var(--status-live);
914
+ }
915
+
916
+ .kpi-status-badge {
917
+ display: inline-block;
918
+ padding: 3px 10px;
919
+ border-radius: 5px;
920
+ font-size: 0.88rem;
921
+ font-weight: 700;
922
+ font-family: var(--font-mono);
923
+ width: fit-content;
924
+ }
925
+
926
+ .kpi-status-badge.safe {
927
+ background: rgba(34, 197, 94, 0.15);
928
+ border: 1px solid rgba(34, 197, 94, 0.4);
929
+ color: #4ade80;
930
+ }
931
+
932
+ .kpi-status-badge.warning {
933
+ background: rgba(245, 158, 11, 0.15);
934
+ border: 1px solid rgba(245, 158, 11, 0.4);
935
+ color: #fbbf24;
936
+ }
937
+
938
+ .kpi-status-badge.critical {
939
+ background: rgba(239, 68, 68, 0.15);
940
+ border: 1px solid rgba(239, 68, 68, 0.4);
941
+ color: #f87171;
942
+ }
943
+
944
+ .kpi-subtext {
945
+ font-family: var(--font-mono);
946
+ font-size: 0.66rem;
947
+ color: var(--text-muted);
948
+ margin-top: 3px;
949
+ }
950
+
951
+ /* Panel Common Headings */
952
+ .panel-header-hud {
953
+ display: flex;
954
+ justify-content: space-between;
955
+ align-items: center;
956
+ margin-bottom: 0.85rem;
957
+ flex-wrap: wrap;
958
+ gap: 6px;
959
+ }
960
+
961
+ .panel-heading {
962
+ font-family: var(--font-display);
963
+ font-size: 0.88rem;
964
+ font-weight: 700;
965
+ letter-spacing: 1px;
966
+ color: var(--text-primary);
967
+ margin: 0;
968
+ border-left: 3px solid var(--accent-primary);
969
+ padding-left: 7px;
970
+ text-transform: uppercase;
971
+ line-height: 1.2;
972
+ }
973
+
974
+ .panel-caption {
975
+ display: block;
976
+ font-size: 0.74rem;
977
+ color: var(--text-muted);
978
+ margin-top: 2px;
979
+ }
980
+
981
+ /* Map Panel HUD */
982
+ .map-panel-hud {
983
+ display: flex;
984
+ flex-direction: column;
985
+ }
986
+
987
+ .map-container-hud {
988
+ position: relative;
989
+ width: 100%;
990
+ height: 420px;
991
+ background: #111;
992
+ border-radius: 8px;
993
+ overflow: hidden;
994
+ border: 1px solid var(--border-subtle);
995
+ }
996
+
997
+ #map {
998
+ width: 100%;
999
+ height: 100%;
1000
+ background: #111 !important;
1001
+ }
1002
+
1003
+ .map-overlay-status {
1004
+ position: absolute;
1005
+ bottom: 12px;
1006
+ left: 12px;
1007
+ right: 12px;
1008
+ background: rgba(7, 12, 8, 0.9);
1009
+ backdrop-filter: blur(14px);
1010
+ -webkit-backdrop-filter: blur(14px);
1011
+ border: 1px solid var(--border-subtle);
1012
+ border-radius: 6px;
1013
+ padding: 8px 14px;
1014
+ display: flex;
1015
+ align-items: center;
1016
+ justify-content: space-between;
1017
+ z-index: 1000;
1018
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
1019
+ }
1020
+
1021
+ .overlay-item {
1022
+ display: flex;
1023
+ flex-direction: column;
1024
+ }
1025
+
1026
+ .overlay-label {
1027
+ font-family: var(--font-mono);
1028
+ font-size: 0.62rem;
1029
+ color: var(--text-muted);
1030
+ text-transform: uppercase;
1031
+ letter-spacing: 0.4px;
1032
+ }
1033
+
1034
+ .overlay-val {
1035
+ font-family: var(--font-mono);
1036
+ font-size: 0.78rem;
1037
+ font-weight: 700;
1038
+ color: var(--text-primary);
1039
+ }
1040
+
1041
+ .overlay-val.highlight {
1042
+ color: var(--status-live);
1043
+ }
1044
+
1045
+ .overlay-divider {
1046
+ width: 1px;
1047
+ height: 24px;
1048
+ background: rgba(255, 255, 255, 0.1);
1049
+ }
1050
+
1051
+ /* Context Intelligence Panel HUD */
1052
+ .context-panel-hud {
1053
+ display: flex;
1054
+ flex-direction: column;
1055
+ gap: 0.85rem;
1056
+ }
1057
+
1058
+ .context-card-hud {
1059
+ background: rgba(0, 0, 0, 0.3);
1060
+ border: 1px solid var(--border-subtle);
1061
+ border-radius: 8px;
1062
+ padding: 0.9rem 1rem;
1063
+ display: flex;
1064
+ flex-direction: column;
1065
+ gap: 8px;
1066
+ }
1067
+
1068
+ .context-card-header {
1069
+ display: flex;
1070
+ justify-content: space-between;
1071
+ align-items: center;
1072
+ }
1073
+
1074
+ .context-card-title {
1075
+ font-family: var(--font-mono);
1076
+ font-size: 0.68rem;
1077
+ font-weight: 700;
1078
+ color: var(--text-secondary);
1079
+ letter-spacing: 0.6px;
1080
+ text-transform: uppercase;
1081
+ }
1082
+
1083
+ .weather-display-row {
1084
+ display: flex;
1085
+ justify-content: space-between;
1086
+ align-items: center;
1087
+ gap: 10px;
1088
+ }
1089
+
1090
+ .weather-temp-hud {
1091
+ font-family: var(--font-display);
1092
+ font-size: 1.15rem;
1093
+ font-weight: 700;
1094
+ color: var(--text-primary);
1095
+ display: block;
1096
+ }
1097
+
1098
+ .weather-sub-hud {
1099
+ font-size: 0.72rem;
1100
+ color: var(--text-muted);
1101
+ }
1102
+
1103
+ .weather-metrics {
1104
+ display: flex;
1105
+ flex-direction: column;
1106
+ gap: 3px;
1107
+ text-align: right;
1108
+ }
1109
+
1110
+ .metric-row {
1111
+ font-family: var(--font-mono);
1112
+ font-size: 0.72rem;
1113
+ color: var(--text-muted);
1114
+ }
1115
+
1116
+ .metric-val {
1117
+ color: var(--status-live);
1118
+ font-weight: 600;
1119
+ }
1120
+
1121
+ .event-desc-hud {
1122
+ font-size: 0.78rem;
1123
+ color: var(--text-primary);
1124
+ margin: 0;
1125
+ line-height: 1.4;
1126
+ }
1127
+
1128
+ .context-source-tag {
1129
+ font-family: var(--font-mono);
1130
+ font-size: 0.62rem;
1131
+ color: var(--text-muted);
1132
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1133
+ padding-top: 6px;
1134
+ }
1135
+
1136
+ /* 4. Forecast Timeline Chart Panel HUD */
1137
+ .timeline-chart-panel-hud {
1138
+ margin-bottom: 1rem;
1139
+ }
1140
+
1141
+ .chart-wrapper-hud {
1142
+ width: 100%;
1143
+ position: relative;
1144
+ height: 200px;
1145
+ margin: 0.5rem 0;
1146
+ }
1147
+
1148
+ .chart-summary-bar {
1149
+ display: grid;
1150
+ grid-template-columns: repeat(4, 1fr);
1151
+ gap: 0.8rem;
1152
+ margin-top: 0.85rem;
1153
+ padding-top: 0.85rem;
1154
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
1155
+ }
1156
+
1157
+ .chart-summary-item {
1158
+ display: flex;
1159
+ flex-direction: column;
1160
+ }
1161
+
1162
+ .summary-label {
1163
+ font-family: var(--font-mono);
1164
+ font-size: 0.65rem;
1165
+ color: var(--text-muted);
1166
+ text-transform: uppercase;
1167
+ letter-spacing: 0.4px;
1168
+ }
1169
+
1170
+ .summary-val {
1171
+ font-family: var(--font-mono);
1172
+ font-size: 0.85rem;
1173
+ font-weight: 700;
1174
+ color: var(--text-primary);
1175
+ margin-top: 2px;
1176
+ }
1177
+
1178
+ .summary-val.highlight-amber { color: var(--status-warning); }
1179
+ .summary-val.highlight-green { color: var(--status-forecast); }
1180
+
1181
+ /* 5. Operational Scenario Panel HUD */
1182
+ .operational-panel-hud {
1183
+ display: flex;
1184
+ flex-direction: column;
1185
+ }
1186
+
1187
+ .operational-grid-hud {
1188
+ display: grid;
1189
+ grid-template-columns: repeat(3, 1fr);
1190
+ gap: 0.7rem;
1191
+ margin-top: 0.5rem;
1192
+ }
1193
+
1194
+ .op-card {
1195
+ background: rgba(0, 0, 0, 0.3);
1196
+ border: 1px solid var(--border-subtle);
1197
+ border-radius: 7px;
1198
+ padding: 0.75rem 0.85rem;
1199
+ display: flex;
1200
+ flex-direction: column;
1201
+ }
1202
+
1203
+ .op-label {
1204
+ font-family: var(--font-mono);
1205
+ font-size: 0.66rem;
1206
+ color: var(--text-muted);
1207
+ text-transform: uppercase;
1208
+ letter-spacing: 0.4px;
1209
+ margin-bottom: 2px;
1210
+ }
1211
+
1212
+ .op-val {
1213
+ font-family: var(--font-display);
1214
+ font-size: 1.15rem;
1215
+ font-weight: 800;
1216
+ color: var(--text-primary);
1217
+ line-height: 1.2;
1218
+ }
1219
+
1220
+ .op-val.highlight {
1221
+ color: var(--accent-primary);
1222
+ }
1223
+
1224
+ .op-sub {
1225
+ font-family: var(--font-mono);
1226
+ font-size: 0.62rem;
1227
+ color: var(--text-muted);
1228
+ margin-top: 2px;
1229
+ }
1230
+
1231
+ .operational-assumptions-strip {
1232
+ margin-top: 0.85rem;
1233
+ padding-top: 0.75rem;
1234
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1235
+ display: flex;
1236
+ flex-direction: column;
1237
+ gap: 3px;
1238
+ }
1239
+
1240
+ .assumptions-tag {
1241
+ font-family: var(--font-mono);
1242
+ font-size: 0.65rem;
1243
+ color: var(--text-muted);
1244
+ }
1245
+
1246
+ .disclaimer-micro {
1247
+ font-family: var(--font-mono);
1248
+ font-size: 0.62rem;
1249
+ color: var(--status-warning);
1250
+ font-weight: 600;
1251
+ }
1252
+
1253
+ .btn-formula-toggle {
1254
+ display: inline-flex;
1255
+ align-items: center;
1256
+ gap: 4px;
1257
+ background: rgba(255, 255, 255, 0.04);
1258
+ border: 1px solid var(--border-subtle);
1259
+ color: var(--text-secondary);
1260
+ font-family: var(--font-mono);
1261
+ font-size: 0.7rem;
1262
+ padding: 3px 8px;
1263
+ border-radius: 5px;
1264
+ cursor: pointer;
1265
+ transition: all 0.2s ease;
1266
+ }
1267
+
1268
+ .btn-formula-toggle:hover {
1269
+ background: rgba(255, 255, 255, 0.08);
1270
+ color: var(--text-primary);
1271
+ border-color: var(--accent-primary);
1272
+ }
1273
+
1274
+ /* 5. Waste Composition Panel HUD */
1275
+ .composition-panel-hud {
1276
+ display: flex;
1277
+ flex-direction: column;
1278
+ }
1279
+
1280
+ .composition-stacked-bar {
1281
+ width: 100%;
1282
+ height: 12px;
1283
+ background: rgba(0, 0, 0, 0.3);
1284
+ border-radius: 6px;
1285
+ overflow: hidden;
1286
+ display: flex;
1287
+ margin: 0.6rem 0 1rem 0;
1288
+ border: 1px solid var(--border-subtle);
1289
+ }
1290
+
1291
+ .segment {
1292
+ height: 100%;
1293
+ transition: width 0.6s ease;
1294
+ }
1295
+
1296
+ .segment.organic { background: #22C55E; }
1297
+ .segment.plastic { background: #00F0FF; }
1298
+ .segment.paper { background: #F59E0B; }
1299
+ .segment.glass { background: #EC4899; }
1300
+ .segment.textile { background: #A855F7; }
1301
+ .segment.metal { background: #94A3B8; }
1302
+
1303
+ .composition-rows-grid {
1304
+ display: grid;
1305
+ grid-template-columns: 1fr 1fr;
1306
+ gap: 0.65rem 1.2rem;
1307
+ }
1308
+
1309
+ .comp-row {
1310
+ display: flex;
1311
+ flex-direction: column;
1312
+ gap: 3px;
1313
+ }
1314
+
1315
+ .comp-header {
1316
+ display: flex;
1317
+ justify-content: space-between;
1318
+ align-items: center;
1319
+ font-size: 0.72rem;
1320
+ }
1321
+
1322
+ .comp-name {
1323
+ font-family: var(--font-mono);
1324
+ color: var(--text-secondary);
1325
+ display: inline-flex;
1326
+ align-items: center;
1327
+ gap: 6px;
1328
+ }
1329
+
1330
+ .comp-ton {
1331
+ font-family: var(--font-mono);
1332
+ color: var(--text-primary);
1333
+ font-weight: 700;
1334
+ }
1335
+
1336
+ .dot-organic { background: #22C55E; }
1337
+ .dot-plastic { background: #00F0FF; }
1338
+ .dot-paper { background: #F59E0B; }
1339
+ .dot-glass { background: #EC4899; }
1340
+ .dot-textile { background: #A855F7; }
1341
+ .dot-metal { background: #94A3B8; }
1342
+
1343
+ .comp-bar-bg {
1344
+ width: 100%;
1345
+ height: 5px;
1346
+ background: rgba(255, 255, 255, 0.06);
1347
+ border-radius: 3px;
1348
+ overflow: hidden;
1349
+ }
1350
+
1351
+ .comp-bar-fill {
1352
+ height: 100%;
1353
+ border-radius: 3px;
1354
+ width: 0%;
1355
+ transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
1356
+ }
1357
+
1358
+ .comp-bar-fill.organic { background: #22C55E; }
1359
+ .comp-bar-fill.plastic { background: #00F0FF; }
1360
+ .comp-bar-fill.paper { background: #F59E0B; }
1361
+ .comp-bar-fill.glass { background: #EC4899; }
1362
+ .comp-bar-fill.textile { background: #A855F7; }
1363
+ .comp-bar-fill.metal { background: #94A3B8; }
1364
+
1365
+ /* 6. Hourly Dispatch Risk Panel HUD */
1366
+ .hourly-panel-hud {
1367
+ display: flex;
1368
+ flex-direction: column;
1369
+ }
1370
+
1371
+ .hourly-footer-hud {
1372
+ display: flex;
1373
+ justify-content: space-between;
1374
+ align-items: center;
1375
+ margin-top: 0.75rem;
1376
+ padding-top: 0.65rem;
1377
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1378
+ flex-wrap: wrap;
1379
+ gap: 8px;
1380
+ }
1381
+
1382
+ .hourly-legend {
1383
+ display: flex;
1384
+ align-items: center;
1385
+ gap: 12px;
1386
+ }
1387
+
1388
+ .legend-item {
1389
+ display: inline-flex;
1390
+ align-items: center;
1391
+ gap: 5px;
1392
+ font-family: var(--font-mono);
1393
+ font-size: 0.65rem;
1394
+ color: var(--text-secondary);
1395
+ }
1396
+
1397
+ .legend-swatch {
1398
+ width: 8px;
1399
+ height: 8px;
1400
+ border-radius: 2px;
1401
+ }
1402
+
1403
+ .legend-swatch.low { background: #22C55E; }
1404
+ .legend-swatch.medium { background: #F59E0B; }
1405
+ .legend-swatch.high { background: #EF4444; }
1406
+
1407
+ .hourly-factors-note {
1408
+ font-family: var(--font-mono);
1409
+ font-size: 0.62rem;
1410
+ color: var(--text-muted);
1411
+ }
1412
+
1413
+ /* 6. Data Provenance Panel HUD */
1414
+ .provenance-panel-hud {
1415
+ display: flex;
1416
+ flex-direction: column;
1417
+ }
1418
+
1419
+ .btn-methodology-link {
1420
+ background: transparent;
1421
+ border: none;
1422
+ color: var(--status-live);
1423
+ font-family: var(--font-mono);
1424
+ font-size: 0.72rem;
1425
+ cursor: pointer;
1426
+ padding: 0;
1427
+ transition: all 0.2s ease;
1428
+ }
1429
+
1430
+ .btn-methodology-link:hover {
1431
+ text-decoration: underline;
1432
+ }
1433
+
1434
+ .provenance-status-table {
1435
+ display: flex;
1436
+ flex-direction: column;
1437
+ gap: 6px;
1438
+ margin: 0.5rem 0;
1439
+ }
1440
+
1441
+ .prov-row {
1442
+ display: flex;
1443
+ justify-content: space-between;
1444
+ align-items: center;
1445
+ background: rgba(0, 0, 0, 0.25);
1446
+ border: 1px solid var(--border-subtle);
1447
+ border-radius: 6px;
1448
+ padding: 6px 10px;
1449
+ }
1450
+
1451
+ .prov-source {
1452
+ font-family: var(--font-mono);
1453
+ font-size: 0.7rem;
1454
+ color: var(--text-secondary);
1455
+ display: inline-flex;
1456
+ align-items: center;
1457
+ gap: 6px;
1458
+ }
1459
+
1460
+ .dot {
1461
+ width: 6px;
1462
+ height: 6px;
1463
+ border-radius: 50%;
1464
+ }
1465
+
1466
+ .dot-live { background: var(--status-live); box-shadow: 0 0 6px var(--status-live); }
1467
+ .dot-ref { background: #94A3B8; }
1468
+ .dot-est { background: var(--status-estimated); }
1469
+ .dot-model { background: var(--status-forecast); }
1470
+ .dot-pending { background: #64748B; }
1471
+
1472
+ .research-disclaimer-box {
1473
+ display: flex;
1474
+ gap: 8px;
1475
+ background: rgba(245, 158, 11, 0.06);
1476
+ border: 1px solid rgba(245, 158, 11, 0.2);
1477
+ border-radius: 6px;
1478
+ padding: 8px 10px;
1479
+ margin-top: 0.6rem;
1480
+ }
1481
+
1482
+ .disclaimer-icon {
1483
+ font-size: 0.85rem;
1484
+ line-height: 1;
1485
+ }
1486
+
1487
+ .disclaimer-text {
1488
+ font-size: 0.72rem;
1489
+ color: var(--text-secondary);
1490
+ margin: 0;
1491
+ line-height: 1.35;
1492
+ }
1493
+
1494
+ .disclaimer-text strong {
1495
+ color: var(--status-warning);
1496
+ }
1497
+
1498
+ /* Badges System */
1499
+ .badge {
1500
+ font-family: var(--font-mono);
1501
+ font-size: 0.62rem;
1502
+ font-weight: 700;
1503
+ padding: 2px 6px;
1504
+ border-radius: 4px;
1505
+ text-transform: uppercase;
1506
+ letter-spacing: 0.4px;
1507
+ display: inline-block;
1508
+ width: fit-content;
1509
+ }
1510
+
1511
+ .badge-live { background: rgba(0, 240, 255, 0.12); color: var(--status-live); border: 1px solid rgba(0, 240, 255, 0.3); }
1512
+ .badge-forecast { background: rgba(34, 197, 94, 0.12); color: var(--status-forecast); border: 1px solid rgba(34, 197, 94, 0.3); }
1513
+ .badge-derived { background: rgba(168, 85, 247, 0.12); color: var(--status-derived); border: 1px solid rgba(168, 85, 247, 0.3); }
1514
+ .badge-simulation { background: rgba(56, 189, 248, 0.12); color: var(--status-simulation); border: 1px solid rgba(56, 189, 248, 0.3); }
1515
+ .badge-unverified { background: rgba(148, 163, 184, 0.12); color: #94A3B8; border: 1px solid rgba(148, 163, 184, 0.3); }
1516
+ .badge-estimated { background: rgba(245, 158, 11, 0.12); color: var(--status-estimated); border: 1px solid rgba(245, 158, 11, 0.3); }
1517
+ .badge-pending { background: rgba(100, 116, 139, 0.12); color: #64748B; border: 1px solid rgba(100, 116, 139, 0.3); }
1518
+ .badge-observed { background: rgba(0, 240, 255, 0.12); color: var(--status-live); border: 1px solid rgba(0, 240, 255, 0.3); }
1519
+
1520
+ /* Responsive Grid Breakpoints */
1521
+ @media (max-width: 1200px) {
1522
+ .col-8, .col-7, .col-6, .col-5, .col-4, .col-3 {
1523
+ grid-column: span 12;
1524
+ }
1525
+
1526
+ .control-grid-top {
1527
+ grid-template-columns: 1fr 1fr;
1528
+ }
1529
+
1530
+ .control-item:last-child {
1531
+ grid-column: span 2;
1532
+ }
1533
+
1534
+ .decision-kpi-grid {
1535
+ grid-template-columns: repeat(2, 1fr);
1536
+ }
1537
+
1538
+ .chart-summary-bar {
1539
+ grid-template-columns: repeat(2, 1fr);
1540
+ }
1541
+ }
1542
+
1543
+ @media (max-width: 768px) {
1544
+ .control-grid-top {
1545
+ grid-template-columns: 1fr;
1546
+ }
1547
+
1548
+ .control-item:last-child {
1549
+ grid-column: span 1;
1550
+ }
1551
+
1552
+ .scenario-grid-hud {
1553
+ grid-template-columns: 1fr;
1554
+ }
1555
+
1556
+ .decision-kpi-grid {
1557
+ grid-template-columns: 1fr;
1558
+ }
1559
+
1560
+ .operational-grid-hud {
1561
+ grid-template-columns: 1fr 1fr;
1562
+ }
1563
+
1564
+ .composition-rows-grid {
1565
+ grid-template-columns: 1fr;
1566
+ }
1567
+
1568
+ .map-container-hud {
1569
+ height: 300px;
1570
+ }
1571
+
1572
+ .map-overlay-status {
1573
+ flex-direction: column;
1574
+ align-items: flex-start;
1575
+ gap: 6px;
1576
+ }
1577
+
1578
+ .overlay-divider {
1579
+ display: none;
1580
+ }
1581
+
1582
+ .chart-summary-bar {
1583
+ grid-template-columns: 1fr;
1584
+ }
1585
  }
1586
 
1587
  /* Route Popup */