Spaces:
Sleeping
Sleeping
Aryanshh commited on
Commit ·
3358e03
1
Parent(s): 4ceb41c
feat: Official production release with overhauled Command Console and Interactive Tour
Browse files- dashboard/app.js +94 -90
- dashboard/index.html +94 -89
- dashboard/style.css +291 -217
dashboard/app.js
CHANGED
|
@@ -1,40 +1,43 @@
|
|
| 1 |
const API_BASE = window.location.origin;
|
| 2 |
|
| 3 |
-
// Simulation Specs (
|
| 4 |
const SPECS = {
|
| 5 |
-
sea: { cost: 1.0, carbon: 0.1
|
| 6 |
-
air: { cost: 5.0, carbon: 2.0
|
| 7 |
-
rail: { cost: 2.5, carbon: 0.5
|
| 8 |
};
|
| 9 |
|
| 10 |
-
|
| 11 |
-
"EcoPhone": { chips: 1, sensors: 2 },
|
| 12 |
-
"GreenTab": { chips: 2, sensors: 1 }
|
| 13 |
-
};
|
| 14 |
-
|
| 15 |
-
// Global State
|
| 16 |
-
let latestInventory = { chips: 0, sensors: 0 };
|
| 17 |
-
let currentTutStep = 0;
|
| 18 |
-
|
| 19 |
const TUTORIAL_STEPS = [
|
| 20 |
{
|
| 21 |
-
title: "
|
| 22 |
-
text: "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
},
|
| 24 |
{
|
| 25 |
-
title: "
|
| 26 |
-
text: "
|
|
|
|
| 27 |
},
|
| 28 |
{
|
| 29 |
-
title: "
|
| 30 |
-
text: "
|
|
|
|
| 31 |
},
|
| 32 |
{
|
| 33 |
-
title: "
|
| 34 |
-
text: "
|
|
|
|
| 35 |
}
|
| 36 |
];
|
| 37 |
|
|
|
|
|
|
|
| 38 |
// State Management
|
| 39 |
async function updateState() {
|
| 40 |
try {
|
|
@@ -42,12 +45,6 @@ async function updateState() {
|
|
| 42 |
if (!response.ok) throw new Error('API Unreachable');
|
| 43 |
const data = await response.json();
|
| 44 |
|
| 45 |
-
// Pulse animation on inventory change
|
| 46 |
-
if (data.inventory.chips !== latestInventory.chips) pulse('chips-count');
|
| 47 |
-
if (data.inventory.sensors !== latestInventory.sensors) pulse('sensors-count');
|
| 48 |
-
|
| 49 |
-
latestInventory = data.inventory;
|
| 50 |
-
|
| 51 |
const status = document.getElementById('connection-status');
|
| 52 |
status.textContent = 'ONLINE';
|
| 53 |
status.className = 'status-badge status-online';
|
|
@@ -57,8 +54,6 @@ async function updateState() {
|
|
| 57 |
document.getElementById('chips-count').textContent = data.inventory.chips;
|
| 58 |
document.getElementById('sensors-count').textContent = data.inventory.sensors;
|
| 59 |
|
| 60 |
-
updateProducePreview();
|
| 61 |
-
|
| 62 |
const newsAlert = document.getElementById('news-alert');
|
| 63 |
if (data.news) {
|
| 64 |
document.getElementById('news-text').textContent = data.news;
|
|
@@ -72,80 +67,49 @@ async function updateState() {
|
|
| 72 |
container.innerHTML = data.orders.map(order => `
|
| 73 |
<div class="shipment-item">
|
| 74 |
<span>${order.product} (Required: ${order.quantity})</span>
|
| 75 |
-
<small>
|
| 76 |
</div>
|
| 77 |
`).join('');
|
| 78 |
} else {
|
| 79 |
-
container.innerHTML = '<p class="placeholder">All
|
| 80 |
}
|
| 81 |
|
| 82 |
} catch (error) {
|
| 83 |
-
console.error('
|
| 84 |
-
document.getElementById('connection-status')
|
| 85 |
-
|
|
|
|
| 86 |
}
|
| 87 |
}
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
el.style.transform = 'scale(1.2)';
|
| 92 |
-
el.style.color = '#7A9B7A';
|
| 93 |
-
setTimeout(() => {
|
| 94 |
-
el.style.transform = 'scale(1)';
|
| 95 |
-
el.style.color = 'inherit';
|
| 96 |
-
}, 300);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
function updateProducePreview() {
|
| 100 |
-
const product = document.getElementById('product-select').value;
|
| 101 |
-
const req = RECIPES[product];
|
| 102 |
-
const preview = document.getElementById('req-preview');
|
| 103 |
-
if (!preview) return;
|
| 104 |
-
|
| 105 |
-
const chipClass = latestInventory.chips >= req.chips ? "sufficient" : "shortage";
|
| 106 |
-
const sensorClass = latestInventory.sensors >= req.sensors ? "sufficient" : "shortage";
|
| 107 |
-
|
| 108 |
-
preview.innerHTML = `
|
| 109 |
-
<div class="req-item">
|
| 110 |
-
<span>Chips: ${req.chips} req.</span>
|
| 111 |
-
<span class="${chipClass}">In Stock: ${latestInventory.chips} (Min: ${req.chips})</span>
|
| 112 |
-
</div>
|
| 113 |
-
<div class="req-item">
|
| 114 |
-
<span>Sensors: ${req.sensors} req.</span>
|
| 115 |
-
<span class="${sensorClass}">In Stock: ${latestInventory.sensors} (Min: ${req.sensors})</span>
|
| 116 |
-
</div>
|
| 117 |
-
`;
|
| 118 |
-
}
|
| 119 |
|
| 120 |
-
function
|
| 121 |
-
const mode =
|
| 122 |
const s = SPECS[mode];
|
| 123 |
const cost = 10 * 5 * s.cost;
|
| 124 |
const carbon = 5 * s.carbon;
|
| 125 |
|
| 126 |
document.getElementById('preview-cost').textContent = `$${cost.toFixed(0)}`;
|
| 127 |
document.getElementById('preview-carbon').textContent = `+${carbon.toFixed(1)}kg CO2`;
|
| 128 |
-
|
| 129 |
-
// Add visual feedback to impact based on carbon
|
| 130 |
-
const carbonLabel = document.getElementById('preview-carbon');
|
| 131 |
-
if (s.carbon > 1.0) carbonLabel.style.color = '#E76F51';
|
| 132 |
-
else carbonLabel.style.color = '#2D5F2D';
|
| 133 |
}
|
| 134 |
|
| 135 |
-
|
| 136 |
-
window.execute = async function(type) {
|
| 137 |
-
if (type === 'produce') {
|
| 138 |
-
const product = document.getElementById('product-select').value;
|
| 139 |
-
const req = RECIPES[product];
|
| 140 |
-
if (latestInventory.chips < req.chips || latestInventory.sensors < req.sensors) {
|
| 141 |
-
log(`STALLED: Insufficient materials for ${product}.`, 'error');
|
| 142 |
-
return;
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
document.querySelector('main').classList.add('transitioning');
|
| 147 |
|
| 148 |
-
|
| 149 |
if (type === 'order_parts') {
|
| 150 |
actionObj.part_type = document.getElementById('part-select').value;
|
| 151 |
actionObj.mode = document.getElementById('mode-select').value;
|
|
@@ -167,28 +131,41 @@ window.execute = async function(type) {
|
|
| 167 |
const result = await response.json();
|
| 168 |
|
| 169 |
if (result.info && result.info.error) {
|
| 170 |
-
log(`
|
| 171 |
} else {
|
| 172 |
-
log(`
|
| 173 |
}
|
| 174 |
|
| 175 |
await updateState();
|
| 176 |
} catch (e) {
|
| 177 |
-
log(`CRITICAL:
|
| 178 |
} finally {
|
| 179 |
setTimeout(() => {
|
| 180 |
document.querySelector('main').classList.remove('transitioning');
|
| 181 |
-
},
|
| 182 |
}
|
| 183 |
}
|
| 184 |
|
| 185 |
// Tutorial Logic
|
| 186 |
function showStep(step) {
|
| 187 |
const content = document.getElementById('tutorial-step');
|
|
|
|
| 188 |
const data = TUTORIAL_STEPS[step];
|
|
|
|
| 189 |
content.innerHTML = `<h2>${data.title}</h2><p>${data.text}</p>`;
|
| 190 |
document.getElementById('tut-prev').classList.toggle('hidden', step === 0);
|
| 191 |
-
document.getElementById('tut-next').textContent = step === TUTORIAL_STEPS.length - 1 ? "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
document.getElementById('tut-next').addEventListener('click', () => {
|
|
@@ -218,11 +195,38 @@ document.getElementById('info-btn').addEventListener('click', () => {
|
|
| 218 |
});
|
| 219 |
|
| 220 |
// Initialization
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
|
|
|
| 224 |
updateState();
|
| 225 |
setInterval(updateState, 3000);
|
| 226 |
-
|
| 227 |
showStep(0);
|
| 228 |
document.getElementById('tutorial-modal').classList.remove('hidden');
|
|
|
|
| 1 |
const API_BASE = window.location.origin;
|
| 2 |
|
| 3 |
+
// Simulation Specs (Mirrors env.py)
|
| 4 |
const SPECS = {
|
| 5 |
+
sea: { cost: 1.0, carbon: 0.1 },
|
| 6 |
+
air: { cost: 5.0, carbon: 2.0 },
|
| 7 |
+
rail: { cost: 2.5, carbon: 0.5 }
|
| 8 |
};
|
| 9 |
|
| 10 |
+
// Tutorial Content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const TUTORIAL_STEPS = [
|
| 12 |
{
|
| 13 |
+
title: "Welcome to NetZero-Nav",
|
| 14 |
+
text: "You are now in command of a global eco-resilient logistics network. Your mission: Deliver products profitably while staying within carbon limits.",
|
| 15 |
+
target: "header"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
title: "Supply Logistics",
|
| 19 |
+
text: "Use this section to order parts. Remember: Sea is green but slow, Air is fast but high emissions.",
|
| 20 |
+
target: "#section-order"
|
| 21 |
},
|
| 22 |
{
|
| 23 |
+
title: "Manufacturing",
|
| 24 |
+
text: "Once parts arrive in your inventory, start your production runs here to fulfill orders.",
|
| 25 |
+
target: "#section-produce"
|
| 26 |
},
|
| 27 |
{
|
| 28 |
+
title: "ESG Offsetting",
|
| 29 |
+
text: "Strategically buy carbon offsets here if you are nearing your quota limits.",
|
| 30 |
+
target: "#section-offset"
|
| 31 |
},
|
| 32 |
{
|
| 33 |
+
title: "Mission Control",
|
| 34 |
+
text: "Advance time or trigger crisis scenarios here. Good luck, Operator.",
|
| 35 |
+
target: ".time-controls"
|
| 36 |
}
|
| 37 |
];
|
| 38 |
|
| 39 |
+
let currentTutStep = 0;
|
| 40 |
+
|
| 41 |
// State Management
|
| 42 |
async function updateState() {
|
| 43 |
try {
|
|
|
|
| 45 |
if (!response.ok) throw new Error('API Unreachable');
|
| 46 |
const data = await response.json();
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const status = document.getElementById('connection-status');
|
| 49 |
status.textContent = 'ONLINE';
|
| 50 |
status.className = 'status-badge status-online';
|
|
|
|
| 54 |
document.getElementById('chips-count').textContent = data.inventory.chips;
|
| 55 |
document.getElementById('sensors-count').textContent = data.inventory.sensors;
|
| 56 |
|
|
|
|
|
|
|
| 57 |
const newsAlert = document.getElementById('news-alert');
|
| 58 |
if (data.news) {
|
| 59 |
document.getElementById('news-text').textContent = data.news;
|
|
|
|
| 67 |
container.innerHTML = data.orders.map(order => `
|
| 68 |
<div class="shipment-item">
|
| 69 |
<span>${order.product} (Required: ${order.quantity})</span>
|
| 70 |
+
<small>Deadline: Day ${order.due_date}</small>
|
| 71 |
</div>
|
| 72 |
`).join('');
|
| 73 |
} else {
|
| 74 |
+
container.innerHTML = '<p class="placeholder">All orders fulfilled. Environment reset recommended.</p>';
|
| 75 |
}
|
| 76 |
|
| 77 |
} catch (error) {
|
| 78 |
+
console.error('Update failed:', error);
|
| 79 |
+
const status = document.getElementById('connection-status');
|
| 80 |
+
status.textContent = 'RECONNECTING...';
|
| 81 |
+
status.className = 'status-badge';
|
| 82 |
}
|
| 83 |
}
|
| 84 |
|
| 85 |
+
// UI Controls
|
| 86 |
+
const modeSelect = document.getElementById('mode-select');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
function updatePreview() {
|
| 89 |
+
const mode = modeSelect.value;
|
| 90 |
const s = SPECS[mode];
|
| 91 |
const cost = 10 * 5 * s.cost;
|
| 92 |
const carbon = 5 * s.carbon;
|
| 93 |
|
| 94 |
document.getElementById('preview-cost').textContent = `$${cost.toFixed(0)}`;
|
| 95 |
document.getElementById('preview-carbon').textContent = `+${carbon.toFixed(1)}kg CO2`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
}
|
| 97 |
|
| 98 |
+
modeSelect.addEventListener('change', updatePreview);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
function log(message, type = 'system') {
|
| 101 |
+
const box = document.getElementById('activity-log');
|
| 102 |
+
const entry = document.createElement('p');
|
| 103 |
+
entry.className = `log-entry ${type}`;
|
| 104 |
+
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
| 105 |
+
box.prepend(entry);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Global Execute Function (exposed to onclick in HTML)
|
| 109 |
+
window.execute = async function(type) {
|
| 110 |
document.querySelector('main').classList.add('transitioning');
|
| 111 |
|
| 112 |
+
let actionObj = { action_type: type };
|
| 113 |
if (type === 'order_parts') {
|
| 114 |
actionObj.part_type = document.getElementById('part-select').value;
|
| 115 |
actionObj.mode = document.getElementById('mode-select').value;
|
|
|
|
| 131 |
const result = await response.json();
|
| 132 |
|
| 133 |
if (result.info && result.info.error) {
|
| 134 |
+
log(`ERROR: ${result.info.error}`, 'error');
|
| 135 |
} else {
|
| 136 |
+
log(`SUCCESS: ${type === 'skip' ? 'Skipped Day' : 'Executed ' + type}`, 'action');
|
| 137 |
}
|
| 138 |
|
| 139 |
await updateState();
|
| 140 |
} catch (e) {
|
| 141 |
+
log(`CRITICAL: Connection lost`, 'error');
|
| 142 |
} finally {
|
| 143 |
setTimeout(() => {
|
| 144 |
document.querySelector('main').classList.remove('transitioning');
|
| 145 |
+
}, 300);
|
| 146 |
}
|
| 147 |
}
|
| 148 |
|
| 149 |
// Tutorial Logic
|
| 150 |
function showStep(step) {
|
| 151 |
const content = document.getElementById('tutorial-step');
|
| 152 |
+
const modal = document.querySelector('.modal-card');
|
| 153 |
const data = TUTORIAL_STEPS[step];
|
| 154 |
+
|
| 155 |
content.innerHTML = `<h2>${data.title}</h2><p>${data.text}</p>`;
|
| 156 |
document.getElementById('tut-prev').classList.toggle('hidden', step === 0);
|
| 157 |
+
document.getElementById('tut-next').textContent = step === TUTORIAL_STEPS.length - 1 ? "Start Simulation" : "Next";
|
| 158 |
+
|
| 159 |
+
// Position modal near target
|
| 160 |
+
const targetEl = document.querySelector(data.target);
|
| 161 |
+
if (targetEl && step > 0) {
|
| 162 |
+
const rect = targetEl.getBoundingClientRect();
|
| 163 |
+
document.getElementById('tutorial-modal').style.alignItems = 'flex-start';
|
| 164 |
+
document.getElementById('tutorial-modal').style.paddingTop = `${Math.max(20, rect.top - 100)}px`;
|
| 165 |
+
} else {
|
| 166 |
+
document.getElementById('tutorial-modal').style.alignItems = 'center';
|
| 167 |
+
document.getElementById('tutorial-modal').style.paddingTop = '0';
|
| 168 |
+
}
|
| 169 |
}
|
| 170 |
|
| 171 |
document.getElementById('tut-next').addEventListener('click', () => {
|
|
|
|
| 195 |
});
|
| 196 |
|
| 197 |
// Initialization
|
| 198 |
+
async function manualReset() {
|
| 199 |
+
if (!confirm("Are you sure you want to reset the simulation?")) return;
|
| 200 |
+
try {
|
| 201 |
+
await fetch(`${API_BASE}/reset`, {
|
| 202 |
+
method: 'POST',
|
| 203 |
+
headers: { 'Content-Type': 'application/json' },
|
| 204 |
+
body: JSON.stringify({ task: 'easy' })
|
| 205 |
+
});
|
| 206 |
+
log("Environment Reset to Day 0", "system");
|
| 207 |
+
await updateState();
|
| 208 |
+
} catch (e) {
|
| 209 |
+
log("Reset Failed", "error");
|
| 210 |
+
}
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
async function triggerSuezJam() {
|
| 214 |
+
try {
|
| 215 |
+
await fetch(`${API_BASE}/trigger`, { method: 'POST' });
|
| 216 |
+
log(`CRISIS: Suez Jam Triggered!`, 'error');
|
| 217 |
+
await updateState();
|
| 218 |
+
} catch (e) {
|
| 219 |
+
log(`Trigger failed`, 'error');
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
document.getElementById('skip-btn').addEventListener('click', () => window.execute('skip'));
|
| 224 |
+
document.getElementById('trigger-btn').addEventListener('click', triggerSuezJam);
|
| 225 |
+
document.getElementById('reset-btn').addEventListener('click', manualReset);
|
| 226 |
|
| 227 |
+
// Start
|
| 228 |
updateState();
|
| 229 |
setInterval(updateState, 3000);
|
| 230 |
+
updatePreview();
|
| 231 |
showStep(0);
|
| 232 |
document.getElementById('tutorial-modal').classList.remove('hidden');
|
dashboard/index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>NetZero-Nav
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
|
| 9 |
</head>
|
|
@@ -15,7 +15,7 @@
|
|
| 15 |
<h1>NETZERO-NAV</h1>
|
| 16 |
<button id="info-btn" class="help-circle">?</button>
|
| 17 |
</div>
|
| 18 |
-
<div id="connection-status" class="status-badge">
|
| 19 |
</header>
|
| 20 |
|
| 21 |
<!-- Interactive Walkthrough -->
|
|
@@ -25,153 +25,158 @@
|
|
| 25 |
<!-- Dynamic Content -->
|
| 26 |
</div>
|
| 27 |
<div class="modal-footer">
|
| 28 |
-
<button id="tut-prev" class="btn btn-outline hidden">Back</button>
|
| 29 |
-
<button id="tut-next" class="btn btn-primary">Next</button>
|
| 30 |
-
<button id="tut-close" class="btn btn-ghost">Skip Tour</button>
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
|
| 35 |
<main>
|
| 36 |
-
<
|
| 37 |
-
|
| 38 |
-
<h3><span class="i-icon">🕹️</span> Command Console</h3>
|
| 39 |
|
| 40 |
<div class="operation-sections">
|
| 41 |
-
<!--
|
| 42 |
<div id="section-order" class="op-section">
|
| 43 |
<h4>📦 Supply Logistics</h4>
|
| 44 |
<div class="op-row">
|
| 45 |
<select id="part-select">
|
| 46 |
-
<option value="chips">
|
| 47 |
-
<option value="sensors">
|
| 48 |
</select>
|
| 49 |
<select id="mode-select">
|
| 50 |
-
<option value="sea">SEA (
|
| 51 |
-
<option value="rail">RAIL (
|
| 52 |
-
<option value="air">AIR (
|
| 53 |
</select>
|
| 54 |
-
<button onclick="execute('order_parts')" class="btn btn-primary">
|
| 55 |
</div>
|
| 56 |
</div>
|
| 57 |
|
| 58 |
<hr class="divider">
|
| 59 |
|
| 60 |
-
<!--
|
| 61 |
<div id="section-produce" class="op-section">
|
| 62 |
<h4>🏭 Manufacturing</h4>
|
| 63 |
<div class="op-row">
|
| 64 |
<select id="product-select">
|
| 65 |
-
<option value="EcoPhone">
|
| 66 |
-
<option value="GreenTab">
|
| 67 |
</select>
|
| 68 |
-
<button onclick="execute('produce')" class="btn btn-primary">
|
| 69 |
</div>
|
| 70 |
-
<div id="req-preview" class="req-preview"></div>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
<hr class="divider">
|
| 74 |
|
| 75 |
-
<!-- Offsetting -->
|
| 76 |
<div id="section-offset" class="op-section">
|
| 77 |
-
<h4>
|
| 78 |
<div class="op-row">
|
| 79 |
-
<
|
|
|
|
| 80 |
</div>
|
| 81 |
</div>
|
| 82 |
</div>
|
| 83 |
|
| 84 |
-
<
|
| 85 |
-
|
| 86 |
-
<
|
| 87 |
-
<
|
| 88 |
-
|
| 89 |
-
<span id="preview-carbon" style="color: var(--sage-green)">0kg CO2</span>
|
| 90 |
-
</div>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
-
<div class="time-controls"
|
| 94 |
-
<button id="skip-btn"
|
| 95 |
-
<button id="trigger-btn" class="btn btn-danger">
|
|
|
|
| 96 |
</div>
|
| 97 |
-
|
| 98 |
<div id="news-alert" class="news-alert hidden">
|
|
|
|
| 99 |
<span id="news-text"></span>
|
| 100 |
</div>
|
| 101 |
</section>
|
| 102 |
|
| 103 |
-
<
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
<div class="
|
| 107 |
-
<
|
| 108 |
-
|
| 109 |
-
<div class="gauge">
|
| 110 |
-
<span id="carbon-value">0</span><small>kg</small>
|
| 111 |
-
</div>
|
| 112 |
</div>
|
| 113 |
-
<div style="font-size: 0.75rem; color: var(--text-muted);">Regional Quota: 1000kg</div>
|
| 114 |
</div>
|
|
|
|
|
|
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
</div>
|
| 133 |
</div>
|
| 134 |
</div>
|
| 135 |
-
</
|
|
|
|
| 136 |
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
</div>
|
| 143 |
</div>
|
| 144 |
-
</
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
</div>
|
| 152 |
</div>
|
| 153 |
-
</
|
|
|
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
</div>
|
| 163 |
-
<div>
|
| 164 |
-
<h4
|
| 165 |
-
<p
|
| 166 |
</div>
|
| 167 |
</div>
|
| 168 |
</div>
|
| 169 |
-
</
|
| 170 |
-
</
|
| 171 |
</main>
|
| 172 |
|
| 173 |
-
<footer
|
| 174 |
-
© 2026 NetZero-Nav |
|
| 175 |
</footer>
|
| 176 |
</div>
|
| 177 |
<script src="app.js"></script>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>NetZero-Nav Dashboard</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
|
| 9 |
</head>
|
|
|
|
| 15 |
<h1>NETZERO-NAV</h1>
|
| 16 |
<button id="info-btn" class="help-circle">?</button>
|
| 17 |
</div>
|
| 18 |
+
<div id="connection-status" class="status-badge">Connecting...</div>
|
| 19 |
</header>
|
| 20 |
|
| 21 |
<!-- Interactive Walkthrough -->
|
|
|
|
| 25 |
<!-- Dynamic Content -->
|
| 26 |
</div>
|
| 27 |
<div class="modal-footer">
|
| 28 |
+
<button id="tut-prev" class="btn btn-outline small hidden">Back</button>
|
| 29 |
+
<button id="tut-next" class="btn btn-primary small">Next</button>
|
| 30 |
+
<button id="tut-close" class="btn btn-ghost small">Skip Tour</button>
|
| 31 |
</div>
|
| 32 |
</div>
|
| 33 |
</div>
|
| 34 |
|
| 35 |
<main>
|
| 36 |
+
<section class="control-panel card">
|
| 37 |
+
<h3><span class="i-icon">ℹ️</span> Command Console</h3>
|
|
|
|
| 38 |
|
| 39 |
<div class="operation-sections">
|
| 40 |
+
<!-- Section 1: Logistics -->
|
| 41 |
<div id="section-order" class="op-section">
|
| 42 |
<h4>📦 Supply Logistics</h4>
|
| 43 |
<div class="op-row">
|
| 44 |
<select id="part-select">
|
| 45 |
+
<option value="chips">Order Microchips</option>
|
| 46 |
+
<option value="sensors">Order Sensors</option>
|
| 47 |
</select>
|
| 48 |
<select id="mode-select">
|
| 49 |
+
<option value="sea">SEA ($50, 10d, 0.5kg)</option>
|
| 50 |
+
<option value="rail">RAIL ($125, 5d, 2.5kg)</option>
|
| 51 |
+
<option value="air">AIR ($250, 2d, 10kg)</option>
|
| 52 |
</select>
|
| 53 |
+
<button onclick="execute('order_parts')" class="btn btn-primary small">Order</button>
|
| 54 |
</div>
|
| 55 |
</div>
|
| 56 |
|
| 57 |
<hr class="divider">
|
| 58 |
|
| 59 |
+
<!-- Section 2: Manufacturing -->
|
| 60 |
<div id="section-produce" class="op-section">
|
| 61 |
<h4>🏭 Manufacturing</h4>
|
| 62 |
<div class="op-row">
|
| 63 |
<select id="product-select">
|
| 64 |
+
<option value="EcoPhone">Produce EcoPhone</option>
|
| 65 |
+
<option value="GreenTab">Produce GreenTab</option>
|
| 66 |
</select>
|
| 67 |
+
<button onclick="execute('produce')" class="btn btn-primary small">Start Run</button>
|
| 68 |
</div>
|
|
|
|
| 69 |
</div>
|
| 70 |
|
| 71 |
<hr class="divider">
|
| 72 |
|
| 73 |
+
<!-- Section 3: ESG / Offsetting -->
|
| 74 |
<div id="section-offset" class="op-section">
|
| 75 |
+
<h4>🌳 ESG Strategy</h4>
|
| 76 |
<div class="op-row">
|
| 77 |
+
<span class="op-text">Carbon Offset Purchase (100 units)</span>
|
| 78 |
+
<button onclick="execute('offset')" class="btn btn-primary small">Buy Offset</button>
|
| 79 |
</div>
|
| 80 |
</div>
|
| 81 |
</div>
|
| 82 |
|
| 83 |
+
<div id="impact-preview" class="impact-preview">
|
| 84 |
+
<span class="preview-label">Live Estimate:</span>
|
| 85 |
+
<span id="preview-cost" class="val">$0</span>
|
| 86 |
+
<span class="sep">|</span>
|
| 87 |
+
<span id="preview-carbon" class="val">0kg CO2</span>
|
|
|
|
|
|
|
| 88 |
</div>
|
| 89 |
|
| 90 |
+
<div class="time-controls">
|
| 91 |
+
<button id="skip-btn" class="btn btn-secondary">Advance to Next Day</button>
|
| 92 |
+
<button id="trigger-btn" class="btn btn-danger">Trigger Suez Jam</button>
|
| 93 |
+
<button id="reset-btn" class="btn btn-outline">Reset Sim</button>
|
| 94 |
</div>
|
|
|
|
| 95 |
<div id="news-alert" class="news-alert hidden">
|
| 96 |
+
<span class="warning-icon">⚠️</span>
|
| 97 |
<span id="news-text"></span>
|
| 98 |
</div>
|
| 99 |
</section>
|
| 100 |
|
| 101 |
+
<section class="metrics-grid">
|
| 102 |
+
<div class="card metric-card">
|
| 103 |
+
<h3>Carbon Footprint</h3>
|
| 104 |
+
<div class="gauge-container">
|
| 105 |
+
<div id="carbon-gauge" class="gauge">
|
| 106 |
+
<span id="carbon-value">0</span><small>kg</small>
|
|
|
|
|
|
|
|
|
|
| 107 |
</div>
|
|
|
|
| 108 |
</div>
|
| 109 |
+
<p class="limit">Limit: <span id="carbon-limit">1000</span>kg</p>
|
| 110 |
+
</div>
|
| 111 |
|
| 112 |
+
<div class="card metric-card">
|
| 113 |
+
<h3>Capital Balance</h3>
|
| 114 |
+
<div class="value-large">$<span id="cash-value">0</span></div>
|
| 115 |
+
<div class="trend" id="cash-trend">Fulfillment: <span id="orders-done">0</span></div>
|
| 116 |
+
</div>
|
| 117 |
|
| 118 |
+
<div class="card metric-card">
|
| 119 |
+
<h3>Raw Inventory</h3>
|
| 120 |
+
<div class="inventory-grid">
|
| 121 |
+
<div class="inv-item">
|
| 122 |
+
<span class="label">Chips</span>
|
| 123 |
+
<span id="chips-count" class="count">0</span>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="inv-item">
|
| 126 |
+
<span class="label">Sensors</span>
|
| 127 |
+
<span id="sensors-count" class="count">0</span>
|
|
|
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</section>
|
| 132 |
|
| 133 |
+
<section class="details-section">
|
| 134 |
+
<div class="card full-width">
|
| 135 |
+
<h3>Active Logistics Stream</h3>
|
| 136 |
+
<div id="shipments-container" class="shipment-list">
|
| 137 |
+
<p class="placeholder">Awaiting shipment data...</p>
|
|
|
|
| 138 |
</div>
|
| 139 |
+
</div>
|
| 140 |
+
</section>
|
| 141 |
|
| 142 |
+
<section class="log-section">
|
| 143 |
+
<div class="card full-width">
|
| 144 |
+
<h3>Simulation Activity Log</h3>
|
| 145 |
+
<div id="activity-log" class="log-box">
|
| 146 |
+
<p class="log-entry system">[SYSTEM] Dashboard initialized. Awaiting user action...</p>
|
|
|
|
| 147 |
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</section>
|
| 150 |
|
| 151 |
+
<section class="guide-section">
|
| 152 |
+
<div class="card full-width mission-card">
|
| 153 |
+
<h3>📁 Mission Briefing (Operator's Manual)</h3>
|
| 154 |
+
<div class="guide-content">
|
| 155 |
+
<div class="guide-grid">
|
| 156 |
+
<div class="guide-item">
|
| 157 |
+
<h4>🎯 Objective</h4>
|
| 158 |
+
<p>Fulfill "EcoPhone" and "GreenTab" orders by managing a global supply chain. You must keep <strong>Carbon Footprint</strong> below the quota while maximizing <strong>Capital Balance</strong>.</p>
|
| 159 |
+
</div>
|
| 160 |
+
<div class="guide-item">
|
| 161 |
+
<h4>🚢 Logistics Trade-offs</h4>
|
| 162 |
+
<ul class="guide-list">
|
| 163 |
+
<li><strong>SEA:</strong> Cheapest ($50), cleanest (0.5kg CO2), but very slow (10 days).</li>
|
| 164 |
+
<li><strong>RAIL:</strong> Balanced cost ($125), low CO2 (2.5kg), medium speed (5 days).</li>
|
| 165 |
+
<li><strong>AIR:</strong> Expensive ($250), high CO2 (10kg), but ultra-fast (2 days).</li>
|
| 166 |
+
</ul>
|
| 167 |
</div>
|
| 168 |
+
<div class="guide-item">
|
| 169 |
+
<h4>☢️ Crisis Handling</h4>
|
| 170 |
+
<p>Clicking <strong>Trigger Suez Jam</strong> simulates a real-world blockade. Sea routes become unavailable for 7 days, forcing you to pivot to expensive Rail/Air routes to meet your deadlines.</p>
|
| 171 |
</div>
|
| 172 |
</div>
|
| 173 |
</div>
|
| 174 |
+
</div>
|
| 175 |
+
</section>
|
| 176 |
</main>
|
| 177 |
|
| 178 |
+
<footer>
|
| 179 |
+
<p>© 2026 NetZero-Nav | Eco-Resilient Supply Chain Intelligence</p>
|
| 180 |
</footer>
|
| 181 |
</div>
|
| 182 |
<script src="app.js"></script>
|
dashboard/style.css
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
:root {
|
| 2 |
-
--
|
| 3 |
-
--
|
| 4 |
-
--
|
| 5 |
-
--
|
| 6 |
-
--
|
| 7 |
-
--border-
|
| 8 |
-
--text-main: #
|
| 9 |
-
--text-muted: #
|
| 10 |
-
--
|
| 11 |
}
|
| 12 |
|
| 13 |
* {
|
|
@@ -17,345 +17,419 @@
|
|
| 17 |
}
|
| 18 |
|
| 19 |
body {
|
| 20 |
-
background-color: var(--
|
| 21 |
color: var(--text-main);
|
| 22 |
font-family: 'Outfit', sans-serif;
|
| 23 |
line-height: 1.6;
|
| 24 |
-
padding: 2rem;
|
| 25 |
-
background-image:
|
| 26 |
-
radial-gradient(circle at 10% 20%, rgba(122, 155, 122, 0.05) 0%, transparent 40%),
|
| 27 |
-
radial-gradient(circle at 90% 80%, rgba(212, 180, 131, 0.05) 0%, transparent 40%);
|
| 28 |
-
min-height: 100vh;
|
| 29 |
}
|
| 30 |
|
| 31 |
.container {
|
| 32 |
-
max-width:
|
| 33 |
margin: 0 auto;
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
-
/* Header */
|
| 37 |
header {
|
| 38 |
display: flex;
|
| 39 |
justify-content: space-between;
|
| 40 |
align-items: center;
|
| 41 |
margin-bottom: 3rem;
|
| 42 |
-
|
| 43 |
-
|
| 44 |
}
|
| 45 |
|
| 46 |
.logo {
|
| 47 |
display: flex;
|
| 48 |
align-items: center;
|
| 49 |
-
gap:
|
| 50 |
}
|
| 51 |
|
| 52 |
-
h1 {
|
| 53 |
font-family: 'Playfair Display', serif;
|
| 54 |
font-size: 1.8rem;
|
|
|
|
| 55 |
color: var(--deep-forest);
|
| 56 |
-
letter-spacing: 0.05em;
|
| 57 |
}
|
| 58 |
|
| 59 |
-
.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
cursor: pointer;
|
| 67 |
-
|
| 68 |
-
|
| 69 |
}
|
| 70 |
|
| 71 |
-
.
|
| 72 |
-
background: var(--
|
| 73 |
color: white;
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
.status-badge {
|
|
|
|
| 78 |
padding: 0.5rem 1rem;
|
| 79 |
-
border-radius:
|
| 80 |
-
font-size: 0.
|
| 81 |
font-weight: 600;
|
| 82 |
-
background: #F0F0F0;
|
| 83 |
-
color: #888;
|
| 84 |
-
backdrop-filter: blur(5px);
|
| 85 |
-
border: 1px solid var(--border-soft);
|
| 86 |
}
|
| 87 |
|
| 88 |
-
.status-online {
|
| 89 |
-
background: rgba(45, 95, 45, 0.1);
|
| 90 |
-
color: var(--deep-forest);
|
| 91 |
-
border: 1px solid rgba(45, 95, 45, 0.2);
|
| 92 |
-
}
|
| 93 |
|
| 94 |
-
|
| 95 |
-
main {
|
| 96 |
display: grid;
|
| 97 |
-
grid-template-columns:
|
| 98 |
-
gap:
|
| 99 |
-
|
| 100 |
}
|
| 101 |
|
| 102 |
-
/* Cards */
|
| 103 |
.card {
|
| 104 |
-
background: var(--
|
| 105 |
-
|
| 106 |
-
border:
|
| 107 |
-
border-radius: 20px;
|
| 108 |
padding: 2rem;
|
| 109 |
-
box-shadow: 0
|
| 110 |
-
|
| 111 |
}
|
| 112 |
|
| 113 |
.card h3 {
|
| 114 |
-
font-
|
| 115 |
-
|
|
|
|
|
|
|
| 116 |
margin-bottom: 1.5rem;
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
display: flex;
|
|
|
|
| 119 |
align-items: center;
|
| 120 |
-
gap: 0.5rem;
|
| 121 |
}
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
border-left: 4px solid var(--sage-green);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
.op-section {
|
| 131 |
-
|
| 132 |
}
|
| 133 |
|
| 134 |
.op-section h4 {
|
| 135 |
-
font-size: 0.
|
| 136 |
-
text-transform: uppercase;
|
| 137 |
color: var(--text-muted);
|
| 138 |
-
|
| 139 |
-
margin-bottom:
|
| 140 |
}
|
| 141 |
|
| 142 |
.op-row {
|
| 143 |
display: flex;
|
| 144 |
-
|
| 145 |
-
|
| 146 |
}
|
| 147 |
|
| 148 |
-
|
| 149 |
-
select {
|
| 150 |
-
width: 100%;
|
| 151 |
-
padding: 1rem;
|
| 152 |
-
border-radius: 12px;
|
| 153 |
-
border: 1px solid var(--border-soft);
|
| 154 |
-
background: white;
|
| 155 |
-
font-family: inherit;
|
| 156 |
font-size: 0.9rem;
|
| 157 |
color: var(--text-main);
|
| 158 |
-
|
| 159 |
-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%232D5F2D' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
| 160 |
-
background-repeat: no-repeat;
|
| 161 |
-
background-position: right 1rem center;
|
| 162 |
-
background-size: 1em;
|
| 163 |
-
cursor: pointer;
|
| 164 |
-
transition: all 0.2s;
|
| 165 |
}
|
| 166 |
|
| 167 |
-
|
| 168 |
-
border
|
| 169 |
-
|
| 170 |
}
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
font-weight: 600;
|
| 179 |
-
font-size: 0.9rem;
|
| 180 |
-
cursor: pointer;
|
| 181 |
-
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 182 |
-
display: inline-flex;
|
| 183 |
-
align-items: center;
|
| 184 |
-
justify-content: center;
|
| 185 |
-
gap: 0.5rem;
|
| 186 |
}
|
| 187 |
|
| 188 |
-
.
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
-
.
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
}
|
| 198 |
|
| 199 |
-
.
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
}
|
| 203 |
|
| 204 |
-
.
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
| 208 |
}
|
| 209 |
|
| 210 |
-
.
|
| 211 |
-
|
| 212 |
-
border: 1px solid var(--border-soft);
|
| 213 |
color: var(--text-muted);
|
| 214 |
}
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
background: rgba(45, 95, 45, 0.03);
|
| 221 |
-
border-radius: 12px;
|
| 222 |
-
font-size: 0.8rem;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
.
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
margin-bottom: 0.5rem;
|
| 229 |
}
|
| 230 |
|
| 231 |
-
.
|
| 232 |
-
|
|
|
|
| 233 |
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
| 244 |
}
|
| 245 |
|
| 246 |
-
.
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
color: var(--text-muted);
|
| 252 |
}
|
| 253 |
|
| 254 |
-
.
|
| 255 |
-
font-
|
| 256 |
-
font-size: 2.5rem;
|
| 257 |
color: var(--deep-forest);
|
| 258 |
-
margin: 1rem 0;
|
| 259 |
}
|
| 260 |
|
| 261 |
-
.
|
| 262 |
-
|
| 263 |
-
height: 120px;
|
| 264 |
-
margin: 0 auto 1rem;
|
| 265 |
-
position: relative;
|
| 266 |
-
border: 8px solid rgba(45, 95, 45, 0.05);
|
| 267 |
-
border-radius: 50%;
|
| 268 |
-
display: flex;
|
| 269 |
-
align-items: center;
|
| 270 |
-
justify-content: center;
|
| 271 |
}
|
| 272 |
|
| 273 |
-
.
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
color: var(--deep-forest);
|
| 277 |
}
|
| 278 |
|
| 279 |
-
.
|
| 280 |
display: grid;
|
| 281 |
-
grid-template-columns:
|
| 282 |
-
gap:
|
|
|
|
| 283 |
}
|
| 284 |
|
| 285 |
-
.
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
}
|
| 291 |
|
| 292 |
-
.
|
| 293 |
-
|
| 294 |
-
font-size: 0.7rem;
|
| 295 |
-
text-transform: uppercase;
|
| 296 |
color: var(--text-muted);
|
| 297 |
-
margin-bottom: 0.25rem;
|
| 298 |
}
|
| 299 |
|
| 300 |
-
.
|
| 301 |
-
|
| 302 |
-
font-weight: 600;
|
| 303 |
}
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
background: #1A1D1A;
|
| 308 |
-
color: #A0C0A0;
|
| 309 |
-
padding: 1.5rem;
|
| 310 |
-
border-radius: 15px;
|
| 311 |
-
height: 250px;
|
| 312 |
-
overflow-y: auto;
|
| 313 |
-
font-family: 'Courier New', Courier, monospace;
|
| 314 |
-
font-size: 0.85rem;
|
| 315 |
}
|
| 316 |
|
| 317 |
-
|
| 318 |
-
.log-entry.error { color: #E76F51; }
|
| 319 |
-
.log-entry.action { color: #88B04B; }
|
| 320 |
-
|
| 321 |
-
/* Modal Overlay */
|
| 322 |
.modal-overlay {
|
| 323 |
position: fixed;
|
| 324 |
top: 0;
|
| 325 |
left: 0;
|
| 326 |
width: 100%;
|
| 327 |
height: 100%;
|
| 328 |
-
background: rgba(
|
| 329 |
-
backdrop-filter: blur(
|
| 330 |
display: flex;
|
| 331 |
justify-content: center;
|
| 332 |
align-items: center;
|
| 333 |
-
z-index:
|
|
|
|
| 334 |
}
|
| 335 |
|
| 336 |
.modal-card {
|
| 337 |
background: white;
|
| 338 |
-
padding:
|
| 339 |
-
border-radius:
|
| 340 |
-
max-width:
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
| 342 |
}
|
| 343 |
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
border-radius: 12px;
|
| 348 |
-
border: 1px solid rgba(231, 111, 81, 0.2);
|
| 349 |
-
color: var(--danger);
|
| 350 |
-
font-weight: 600;
|
| 351 |
-
font-size: 0.9rem;
|
| 352 |
-
margin-top: 1.5rem;
|
| 353 |
}
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
| 361 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
:root {
|
| 2 |
+
--bg-cream: #FDFBF7;
|
| 3 |
+
--card-white: #FFFFFF;
|
| 4 |
+
--primary-green: #2C5F2D;
|
| 5 |
+
--sage-green: #97BC62;
|
| 6 |
+
--deep-forest: #004225;
|
| 7 |
+
--border-tan: #E9E1D4;
|
| 8 |
+
--text-main: #333333;
|
| 9 |
+
--text-muted: #666666;
|
| 10 |
+
--accent-gold: #D4AF37;
|
| 11 |
}
|
| 12 |
|
| 13 |
* {
|
|
|
|
| 17 |
}
|
| 18 |
|
| 19 |
body {
|
| 20 |
+
background-color: var(--bg-cream);
|
| 21 |
color: var(--text-main);
|
| 22 |
font-family: 'Outfit', sans-serif;
|
| 23 |
line-height: 1.6;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
.container {
|
| 27 |
+
max-width: 1200px;
|
| 28 |
margin: 0 auto;
|
| 29 |
+
padding: 2rem;
|
| 30 |
}
|
| 31 |
|
|
|
|
| 32 |
header {
|
| 33 |
display: flex;
|
| 34 |
justify-content: space-between;
|
| 35 |
align-items: center;
|
| 36 |
margin-bottom: 3rem;
|
| 37 |
+
border-bottom: 2px solid var(--border-tan);
|
| 38 |
+
padding-bottom: 1rem;
|
| 39 |
}
|
| 40 |
|
| 41 |
.logo {
|
| 42 |
display: flex;
|
| 43 |
align-items: center;
|
| 44 |
+
gap: 0.75rem;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
.logo h1 {
|
| 48 |
font-family: 'Playfair Display', serif;
|
| 49 |
font-size: 1.8rem;
|
| 50 |
+
letter-spacing: 2px;
|
| 51 |
color: var(--deep-forest);
|
|
|
|
| 52 |
}
|
| 53 |
|
| 54 |
+
.controls {
|
| 55 |
+
display: flex;
|
| 56 |
+
align-items: center;
|
| 57 |
+
gap: 1rem;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.btn {
|
| 61 |
+
padding: 0.6rem 1.2rem;
|
| 62 |
+
border-radius: 8px;
|
| 63 |
+
font-size: 0.85rem;
|
| 64 |
+
font-weight: 600;
|
| 65 |
cursor: pointer;
|
| 66 |
+
transition: all 0.2s ease;
|
| 67 |
+
border: none;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
.btn-primary {
|
| 71 |
+
background-color: var(--primary-green);
|
| 72 |
color: white;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.btn-primary:hover {
|
| 76 |
+
background-color: var(--deep-forest);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.btn-outline {
|
| 80 |
+
background-color: transparent;
|
| 81 |
+
border: 1.5px solid var(--primary-green);
|
| 82 |
+
color: var(--primary-green);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.btn-outline:hover {
|
| 86 |
+
background-color: #E1F5E1;
|
| 87 |
}
|
| 88 |
|
| 89 |
.status-badge {
|
| 90 |
+
background: var(--border-tan);
|
| 91 |
padding: 0.5rem 1rem;
|
| 92 |
+
border-radius: 20px;
|
| 93 |
+
font-size: 0.8rem;
|
| 94 |
font-weight: 600;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
}
|
| 96 |
|
| 97 |
+
.status-online { background: #E1F5E1; color: #2C5F2D; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
|
| 99 |
+
.metrics-grid {
|
|
|
|
| 100 |
display: grid;
|
| 101 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 102 |
+
gap: 2rem;
|
| 103 |
+
margin-bottom: 2rem;
|
| 104 |
}
|
| 105 |
|
|
|
|
| 106 |
.card {
|
| 107 |
+
background: var(--card-white);
|
| 108 |
+
border: 1px solid var(--border-tan);
|
| 109 |
+
border-radius: 12px;
|
|
|
|
| 110 |
padding: 2rem;
|
| 111 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.02);
|
| 112 |
+
transition: transform 0.3s ease;
|
| 113 |
}
|
| 114 |
|
| 115 |
.card h3 {
|
| 116 |
+
font-size: 0.9rem;
|
| 117 |
+
text-transform: uppercase;
|
| 118 |
+
letter-spacing: 1px;
|
| 119 |
+
color: var(--text-muted);
|
| 120 |
margin-bottom: 1.5rem;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.gauge-container {
|
| 124 |
+
height: 100px;
|
| 125 |
display: flex;
|
| 126 |
+
justify-content: center;
|
| 127 |
align-items: center;
|
|
|
|
| 128 |
}
|
| 129 |
|
| 130 |
+
.gauge {
|
| 131 |
+
font-size: 2.5rem;
|
| 132 |
+
font-weight: 700;
|
| 133 |
+
color: var(--primary-green);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.gauge small {
|
| 137 |
+
font-size: 1rem;
|
| 138 |
+
margin-left: 0.2rem;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.value-large {
|
| 142 |
+
font-size: 3rem;
|
| 143 |
+
font-weight: 600;
|
| 144 |
+
color: var(--deep-forest);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.shipment-list {
|
| 148 |
+
display: flex;
|
| 149 |
+
flex-direction: column;
|
| 150 |
+
gap: 0.75rem;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.shipment-item {
|
| 154 |
+
background: var(--bg-cream);
|
| 155 |
+
padding: 1rem;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
border-left: 4px solid var(--sage-green);
|
| 158 |
+
display: flex;
|
| 159 |
+
justify-content: space-between;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.ticker-box {
|
| 163 |
+
background: var(--bg-cream);
|
| 164 |
+
padding: 1rem;
|
| 165 |
+
border-radius: 8px;
|
| 166 |
+
font-style: italic;
|
| 167 |
+
color: var(--deep-forest);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.operation-sections {
|
| 171 |
+
display: flex;
|
| 172 |
+
flex-direction: column;
|
| 173 |
+
gap: 1.5rem;
|
| 174 |
}
|
| 175 |
|
| 176 |
.op-section {
|
| 177 |
+
padding: 0.5rem 0;
|
| 178 |
}
|
| 179 |
|
| 180 |
.op-section h4 {
|
| 181 |
+
font-size: 0.8rem;
|
|
|
|
| 182 |
color: var(--text-muted);
|
| 183 |
+
text-transform: uppercase;
|
| 184 |
+
margin-bottom: 0.75rem;
|
| 185 |
}
|
| 186 |
|
| 187 |
.op-row {
|
| 188 |
display: flex;
|
| 189 |
+
gap: 1rem;
|
| 190 |
+
align-items: center;
|
| 191 |
}
|
| 192 |
|
| 193 |
+
.op-text {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
font-size: 0.9rem;
|
| 195 |
color: var(--text-main);
|
| 196 |
+
flex-grow: 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
}
|
| 198 |
|
| 199 |
+
.divider {
|
| 200 |
+
border: none;
|
| 201 |
+
border-top: 1px solid var(--border-tan);
|
| 202 |
}
|
| 203 |
|
| 204 |
+
.time-controls {
|
| 205 |
+
margin-top: 2rem;
|
| 206 |
+
display: flex;
|
| 207 |
+
gap: 1rem;
|
| 208 |
+
padding-top: 1.5rem;
|
| 209 |
+
border-top: 2px solid var(--border-tan);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
+
.hidden { display: none !important; }
|
| 213 |
+
|
| 214 |
+
.sub-inputs {
|
| 215 |
+
display: flex;
|
| 216 |
+
gap: 1rem;
|
| 217 |
}
|
| 218 |
|
| 219 |
+
.log-box {
|
| 220 |
+
height: 150px;
|
| 221 |
+
overflow-y: auto;
|
| 222 |
+
background: #1A1A1A;
|
| 223 |
+
color: #00FF41;
|
| 224 |
+
font-family: 'Courier New', Courier, monospace;
|
| 225 |
+
padding: 1rem;
|
| 226 |
+
border-radius: 8px;
|
| 227 |
+
font-size: 0.85rem;
|
| 228 |
}
|
| 229 |
|
| 230 |
+
.log-entry { margin-bottom: 0.4rem; }
|
| 231 |
+
.log-entry.system { color: #AAAAAA; }
|
| 232 |
+
.log-entry.action { color: #00FF41; }
|
| 233 |
+
.log-entry.error { color: #FF4136; }
|
| 234 |
+
|
| 235 |
+
.inventory-grid {
|
| 236 |
+
display: flex;
|
| 237 |
+
justify-content: space-around;
|
| 238 |
+
padding-top: 0.5rem;
|
| 239 |
}
|
| 240 |
|
| 241 |
+
.inv-item {
|
| 242 |
+
display: flex;
|
| 243 |
+
flex-direction: column;
|
| 244 |
+
align-items: center;
|
| 245 |
+
gap: 0.5rem;
|
| 246 |
}
|
| 247 |
|
| 248 |
+
.inv-item .label {
|
| 249 |
+
font-size: 0.7rem;
|
|
|
|
| 250 |
color: var(--text-muted);
|
| 251 |
}
|
| 252 |
|
| 253 |
+
.inv-item .count {
|
| 254 |
+
font-size: 2rem;
|
| 255 |
+
font-weight: 600;
|
| 256 |
+
color: var(--primary-green);
|
|
|
|
|
|
|
|
|
|
| 257 |
}
|
| 258 |
|
| 259 |
+
.btn-danger {
|
| 260 |
+
background-color: #FF4136;
|
| 261 |
+
color: white;
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
+
.btn-danger:hover {
|
| 265 |
+
background-color: #B22222;
|
| 266 |
+
}
|
| 267 |
|
| 268 |
+
.news-alert {
|
| 269 |
+
margin-top: 1.5rem;
|
| 270 |
+
padding: 1rem;
|
| 271 |
+
background: #FFF5F5;
|
| 272 |
+
border: 1px solid #FFC1C1;
|
| 273 |
+
border-radius: 8px;
|
| 274 |
+
color: #FF4136;
|
| 275 |
+
font-weight: 600;
|
| 276 |
+
display: flex;
|
| 277 |
+
gap: 1rem;
|
| 278 |
+
animation: pulse-border 2s infinite;
|
| 279 |
}
|
| 280 |
|
| 281 |
+
@keyframes pulse-border {
|
| 282 |
+
0% { border-color: #FFC1C1; }
|
| 283 |
+
50% { border-color: #FF4136; }
|
| 284 |
+
100% { border-color: #FFC1C1; }
|
| 285 |
}
|
| 286 |
|
| 287 |
+
.impact-preview {
|
| 288 |
+
margin: 1.5rem 0;
|
| 289 |
+
padding: 1rem;
|
| 290 |
+
background: #F0F4F0;
|
| 291 |
+
border-radius: 8px;
|
| 292 |
+
border-left: 4px solid var(--primary-green);
|
| 293 |
+
font-size: 0.9rem;
|
| 294 |
+
display: flex;
|
| 295 |
+
gap: 0.75rem;
|
| 296 |
+
align-items: center;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.impact-preview .preview-label {
|
| 300 |
+
font-weight: 600;
|
| 301 |
color: var(--text-muted);
|
| 302 |
}
|
| 303 |
|
| 304 |
+
.impact-preview .val {
|
| 305 |
+
font-weight: 600;
|
|
|
|
| 306 |
color: var(--deep-forest);
|
|
|
|
| 307 |
}
|
| 308 |
|
| 309 |
+
.impact-preview .sep {
|
| 310 |
+
color: var(--border-tan);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
+
.mission-card {
|
| 314 |
+
background: #F4F7F9;
|
| 315 |
+
border: 1px dashed var(--sage-green);
|
|
|
|
| 316 |
}
|
| 317 |
|
| 318 |
+
.guide-grid {
|
| 319 |
display: grid;
|
| 320 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 321 |
+
gap: 2rem;
|
| 322 |
+
margin-top: 1rem;
|
| 323 |
}
|
| 324 |
|
| 325 |
+
.guide-item h4 {
|
| 326 |
+
color: var(--primary-green);
|
| 327 |
+
margin-bottom: 0.5rem;
|
| 328 |
+
font-size: 0.9rem;
|
| 329 |
+
text-transform: uppercase;
|
| 330 |
}
|
| 331 |
|
| 332 |
+
.guide-item p, .guide-item li {
|
| 333 |
+
font-size: 0.85rem;
|
|
|
|
|
|
|
| 334 |
color: var(--text-muted);
|
|
|
|
| 335 |
}
|
| 336 |
|
| 337 |
+
.guide-list {
|
| 338 |
+
padding-left: 1.2rem;
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
+
.guide-list li {
|
| 342 |
+
margin-bottom: 0.3rem;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
}
|
| 344 |
|
| 345 |
+
/* Modal & Tutorial Styles */
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
.modal-overlay {
|
| 347 |
position: fixed;
|
| 348 |
top: 0;
|
| 349 |
left: 0;
|
| 350 |
width: 100%;
|
| 351 |
height: 100%;
|
| 352 |
+
background: rgba(0, 0, 0, 0.4);
|
| 353 |
+
backdrop-filter: blur(4px);
|
| 354 |
display: flex;
|
| 355 |
justify-content: center;
|
| 356 |
align-items: center;
|
| 357 |
+
z-index: 1000;
|
| 358 |
+
transition: all 0.3s ease;
|
| 359 |
}
|
| 360 |
|
| 361 |
.modal-card {
|
| 362 |
background: white;
|
| 363 |
+
padding: 2.5rem;
|
| 364 |
+
border-radius: 20px;
|
| 365 |
+
max-width: 500px;
|
| 366 |
+
width: 90%;
|
| 367 |
+
box-shadow: 0 20px 50px rgba(0,0,0,0.1);
|
| 368 |
+
border: 1px solid var(--border-tan);
|
| 369 |
+
animation: slideUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 370 |
}
|
| 371 |
|
| 372 |
+
@keyframes slideUp {
|
| 373 |
+
from { opacity: 0; transform: translateY(30px); }
|
| 374 |
+
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
}
|
| 376 |
|
| 377 |
+
.tutorial-content h2 {
|
| 378 |
+
font-family: 'Playfair Display', serif;
|
| 379 |
+
color: var(--deep-forest);
|
| 380 |
+
margin-bottom: 1rem;
|
| 381 |
+
}
|
| 382 |
|
| 383 |
+
.tutorial-content p {
|
| 384 |
+
color: var(--text-muted);
|
| 385 |
+
font-size: 1rem;
|
| 386 |
+
margin-bottom: 2rem;
|
| 387 |
}
|
| 388 |
+
|
| 389 |
+
.modal-footer {
|
| 390 |
+
display: flex;
|
| 391 |
+
justify-content: space-between;
|
| 392 |
+
align-items: center;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.help-circle {
|
| 396 |
+
width: 24px;
|
| 397 |
+
height: 24px;
|
| 398 |
+
border-radius: 50%;
|
| 399 |
+
border: 1px solid var(--sage-green);
|
| 400 |
+
background: white;
|
| 401 |
+
color: var(--primary-green);
|
| 402 |
+
cursor: pointer;
|
| 403 |
+
font-weight: bold;
|
| 404 |
+
font-size: 12px;
|
| 405 |
+
transition: all 0.2s;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.help-circle:hover { background: var(--sage-green); color: white; }
|
| 409 |
+
|
| 410 |
+
.small { padding: 0.4rem 0.8rem; font-size: 0.75rem; }
|
| 411 |
+
.btn-ghost { background: transparent; color: var(--text-muted); }
|
| 412 |
+
|
| 413 |
+
/* Day Transition Blur */
|
| 414 |
+
.transitioning {
|
| 415 |
+
filter: blur(2px);
|
| 416 |
+
pointer-events: none;
|
| 417 |
+
transition: filter 0.3s ease;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
footer {
|
| 421 |
+
text-align: center;
|
| 422 |
+
margin-top: 4rem;
|
| 423 |
+
padding-top: 1rem;
|
| 424 |
+
border-top: 1px solid var(--border-tan);
|
| 425 |
+
color: var(--text-muted);
|
| 426 |
+
font-size: 0.8rem;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
@media (max-width: 768px) {
|
| 430 |
+
.container { padding: 1rem; }
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.action-buttons { display: flex; gap: 0.75rem; }
|
| 434 |
+
.btn-secondary { background-color: var(--sage-green); color: var(--deep-forest); }
|
| 435 |
+
.btn-secondary:hover { background-color: #7AA346; }
|