All ChatGPT created, I just was scrivener and Feerless Leader.
This commit is contained in:
parent
12a180dbd5
commit
19bd89c405
7 changed files with 596 additions and 2 deletions
46
README.md
46
README.md
|
|
@ -1,3 +1,45 @@
|
||||||
# filament-drybox-cost
|
# Filament Drybox Cost Model
|
||||||
|
|
||||||
Ascertain the out-of-pocket costs to create one or more dryboxes.
|
20260319 ChatGPT
|
||||||
|
$Header$
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This project provides a transparent and reproducible cost model for the
|
||||||
|
"Filament Storage 2026" drybox project by GunplaMark.
|
||||||
|
|
||||||
|
The model is intended to answer questions such as:
|
||||||
|
|
||||||
|
- What is the out-of-pocket cost to build 1 drybox?
|
||||||
|
- How does the per-unit cost change when building 4 dryboxes?
|
||||||
|
- How does the per-unit cost change when building 12 dryboxes?
|
||||||
|
- What inventory remains after each build scenario?
|
||||||
|
- How much do filament and electricity contribute to the total?
|
||||||
|
|
||||||
|
The model accounts for:
|
||||||
|
|
||||||
|
- lot-based purchasing
|
||||||
|
- per-box usage
|
||||||
|
- printed part filament cost
|
||||||
|
- electrical cost of printing
|
||||||
|
- optional waste factor
|
||||||
|
|
||||||
|
See video at https://youtu.be/lJDoVH7qTKs?si=cQMhUda7RQcuIDJU by user GunplaMark entitled "Filament Storage 2026 - The Final Boss of My Drybox Journey".
|
||||||
|
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── LICENSE
|
||||||
|
├── README.md
|
||||||
|
├── data
|
||||||
|
│ ├── assumptions.json
|
||||||
|
│ └── bom.json
|
||||||
|
├── export
|
||||||
|
├── tools
|
||||||
|
│ └── generate_csv.pl
|
||||||
|
└── web
|
||||||
|
├── app.js
|
||||||
|
├── index.html
|
||||||
|
└── style.css
|
||||||
|
|
|
||||||
33
data/assumptions.json
Normal file
33
data/assumptions.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"currency": "USD",
|
||||||
|
"waste_factor": 1.15,
|
||||||
|
"electricity_cost_per_kwh": 0.12,
|
||||||
|
"printer_average_watts": 120,
|
||||||
|
"print_hours_per_box": 8.0,
|
||||||
|
"target_box_counts": [1, 4, 12],
|
||||||
|
"printed_materials": [
|
||||||
|
{
|
||||||
|
"id": "petg",
|
||||||
|
"name": "PETG",
|
||||||
|
"cost_per_kg": 22.00,
|
||||||
|
"grams_per_box": 220
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "asa",
|
||||||
|
"name": "ASA",
|
||||||
|
"cost_per_kg": 28.00,
|
||||||
|
"grams_per_box": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tpu",
|
||||||
|
"name": "TPU",
|
||||||
|
"cost_per_kg": 26.00,
|
||||||
|
"grams_per_box": 20
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notes": [
|
||||||
|
"Printed weights are initial estimates and should later be replaced with slicer-derived actuals.",
|
||||||
|
"Waste factor is applied to printed material cost only.",
|
||||||
|
"Electricity uses printer_average_watts multiplied by print_hours_per_box."
|
||||||
|
]
|
||||||
|
}
|
||||||
85
data/bom.json
Normal file
85
data/bom.json
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"project_name": "Filament Drybox Cost Model",
|
||||||
|
"source_model": "Filament Storage 2026 by GunplaMark",
|
||||||
|
"source_url": "https://www.printables.com/",
|
||||||
|
"currency": "USD",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"id": "container",
|
||||||
|
"name": "4L cereal container",
|
||||||
|
"category": "hardware",
|
||||||
|
"unit_type": "each",
|
||||||
|
"pack_qty": 4,
|
||||||
|
"pack_cost": 39.99,
|
||||||
|
"units_used_per_box": 1,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Praki, Wildone, or Skroam style 4L container"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bearing",
|
||||||
|
"name": "608 bearing",
|
||||||
|
"category": "hardware",
|
||||||
|
"unit_type": "each",
|
||||||
|
"pack_qty": 10,
|
||||||
|
"pack_cost": 11.99,
|
||||||
|
"units_used_per_box": 4,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Open, ungreased fidget-spinner style bearings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "badge_reel",
|
||||||
|
"name": "badge reel",
|
||||||
|
"category": "hardware",
|
||||||
|
"unit_type": "each",
|
||||||
|
"pack_qty": 10,
|
||||||
|
"pack_cost": 14.99,
|
||||||
|
"units_used_per_box": 1,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Used to harvest spring"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "foam_cord",
|
||||||
|
"name": "3mm silicone foam cord",
|
||||||
|
"category": "consumable",
|
||||||
|
"unit_type": "meter",
|
||||||
|
"pack_qty": 10,
|
||||||
|
"pack_cost": 9.99,
|
||||||
|
"units_used_per_box": 0.70,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Length per box is an estimate; measure actual lid circumference"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "desiccant",
|
||||||
|
"name": "activated alumina desiccant",
|
||||||
|
"category": "consumable",
|
||||||
|
"unit_type": "gram",
|
||||||
|
"pack_qty": 2000,
|
||||||
|
"pack_cost": 24.99,
|
||||||
|
"units_used_per_box": 150,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Silica gel could be modeled as an alternative later"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hygrometer",
|
||||||
|
"name": "rectangular hygrometer",
|
||||||
|
"category": "hardware",
|
||||||
|
"unit_type": "each",
|
||||||
|
"pack_qty": 4,
|
||||||
|
"pack_cost": 17.99,
|
||||||
|
"units_used_per_box": 1,
|
||||||
|
"optional": false,
|
||||||
|
"notes": "Common small rectangular hygrometer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nfc_tag",
|
||||||
|
"name": "25mm NFC tag",
|
||||||
|
"category": "optional",
|
||||||
|
"unit_type": "each",
|
||||||
|
"pack_qty": 20,
|
||||||
|
"pack_cost": 9.99,
|
||||||
|
"units_used_per_box": 1,
|
||||||
|
"optional": true,
|
||||||
|
"notes": "Optional spool identification tag"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
tools/generate_cvs.pl
Executable file
21
tools/generate_cvs.pl
Executable file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/perl
|
||||||
|
#
|
||||||
|
# Example command lines:
|
||||||
|
#
|
||||||
|
# perl generate_csv.pl
|
||||||
|
#
|
||||||
|
# 20260319 ChatGPT
|
||||||
|
# $Header$
|
||||||
|
#
|
||||||
|
# Purpose:
|
||||||
|
# Placeholder script for later CSV export from the BOM and assumptions data.
|
||||||
|
#
|
||||||
|
|
||||||
|
use strict;
|
||||||
|
use warnings;
|
||||||
|
|
||||||
|
print "generate_csv.pl: placeholder script\n";
|
||||||
|
print "Later revision will read ../data/bom.json and ../data/assumptions.json\n";
|
||||||
|
print "and produce a CSV summary under ../export/.\n";
|
||||||
|
|
||||||
|
exit 0;
|
||||||
249
web/app.js
Normal file
249
web/app.js
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*
|
||||||
|
20260319 ChatGPT
|
||||||
|
$Header$
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function currency(value) {
|
||||||
|
return new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD"
|
||||||
|
}).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ceilDivUsage(requiredUnits, packQty) {
|
||||||
|
return Math.ceil(requiredUnits / packQty);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computePrintingCost(assumptions, boxCount) {
|
||||||
|
let materialCost = 0;
|
||||||
|
|
||||||
|
for (const material of assumptions.printed_materials) {
|
||||||
|
const totalGrams = material.grams_per_box * boxCount;
|
||||||
|
const totalKg = totalGrams / 1000.0;
|
||||||
|
materialCost += totalKg * material.cost_per_kg;
|
||||||
|
}
|
||||||
|
|
||||||
|
materialCost *= assumptions.waste_factor;
|
||||||
|
|
||||||
|
const totalKwh =
|
||||||
|
(assumptions.printer_average_watts / 1000.0) *
|
||||||
|
assumptions.print_hours_per_box *
|
||||||
|
boxCount;
|
||||||
|
|
||||||
|
const electricityCost = totalKwh * assumptions.electricity_cost_per_kwh;
|
||||||
|
|
||||||
|
return {
|
||||||
|
material_cost: materialCost,
|
||||||
|
electricity_cost: electricityCost,
|
||||||
|
total_printing_cost: materialCost + electricityCost
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeHardwareScenario(bom, boxCount, includeOptional = true) {
|
||||||
|
const componentRows = [];
|
||||||
|
let totalHardwareCost = 0;
|
||||||
|
|
||||||
|
for (const component of bom.components) {
|
||||||
|
if (!includeOptional && component.optional) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredUnits = component.units_used_per_box * boxCount;
|
||||||
|
const packsRequired = ceilDivUsage(requiredUnits, component.pack_qty);
|
||||||
|
const purchasedUnits = packsRequired * component.pack_qty;
|
||||||
|
const leftoverUnits = purchasedUnits - requiredUnits;
|
||||||
|
const totalCost = packsRequired * component.pack_cost;
|
||||||
|
|
||||||
|
totalHardwareCost += totalCost;
|
||||||
|
|
||||||
|
componentRows.push({
|
||||||
|
id: component.id,
|
||||||
|
name: component.name,
|
||||||
|
required_units: requiredUnits,
|
||||||
|
pack_qty: component.pack_qty,
|
||||||
|
pack_cost: component.pack_cost,
|
||||||
|
packs_required: packsRequired,
|
||||||
|
purchased_units: purchasedUnits,
|
||||||
|
leftover_units: leftoverUnits,
|
||||||
|
total_cost: totalCost,
|
||||||
|
optional: component.optional,
|
||||||
|
unit_type: component.unit_type,
|
||||||
|
notes: component.notes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
box_count: boxCount,
|
||||||
|
total_hardware_cost: totalHardwareCost,
|
||||||
|
component_rows: componentRows
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAssumptions(assumptions) {
|
||||||
|
const div = document.getElementById("assumptions");
|
||||||
|
|
||||||
|
const materialLines = assumptions.printed_materials.map((m) => {
|
||||||
|
return `<li>${m.name}: ${m.grams_per_box} g/box at ${currency(m.cost_per_kg)}/kg</li>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<ul>
|
||||||
|
<li>Waste factor: ${assumptions.waste_factor}</li>
|
||||||
|
<li>Electricity: ${currency(assumptions.electricity_cost_per_kwh)}/kWh</li>
|
||||||
|
<li>Printer average load: ${assumptions.printer_average_watts} W</li>
|
||||||
|
<li>Print hours per box: ${assumptions.print_hours_per_box}</li>
|
||||||
|
<li>Target scenarios: ${assumptions.target_box_counts.join(", ")}</li>
|
||||||
|
</ul>
|
||||||
|
<p><strong>Printed materials</strong></p>
|
||||||
|
<ul>${materialLines}</ul>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummaryTable(bom, assumptions) {
|
||||||
|
const tbody = document.querySelector("#summary-table tbody");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
|
for (const boxCount of assumptions.target_box_counts) {
|
||||||
|
const hardware = computeHardwareScenario(bom, boxCount, true);
|
||||||
|
const printing = computePrintingCost(assumptions, boxCount);
|
||||||
|
const totalCost = hardware.total_hardware_cost + printing.total_printing_cost;
|
||||||
|
const costPerBox = totalCost / boxCount;
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${boxCount}</td>
|
||||||
|
<td>${currency(hardware.total_hardware_cost)}</td>
|
||||||
|
<td>${currency(printing.total_printing_cost)}</td>
|
||||||
|
<td>${currency(totalCost)}</td>
|
||||||
|
<td>${currency(costPerBox)}</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponentBreakdowns(bom, assumptions) {
|
||||||
|
const container = document.getElementById("component-breakdowns");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
for (const boxCount of assumptions.target_box_counts) {
|
||||||
|
const hardware = computeHardwareScenario(bom, boxCount, true);
|
||||||
|
|
||||||
|
const section = document.createElement("div");
|
||||||
|
section.className = "scenario-block";
|
||||||
|
|
||||||
|
let rowsHtml = "";
|
||||||
|
for (const row of hardware.component_rows) {
|
||||||
|
rowsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${row.name}${row.optional ? " (optional)" : ""}</td>
|
||||||
|
<td>${row.required_units}</td>
|
||||||
|
<td>${row.pack_qty}</td>
|
||||||
|
<td>${currency(row.pack_cost)}</td>
|
||||||
|
<td>${row.packs_required}</td>
|
||||||
|
<td>${currency(row.total_cost)}</td>
|
||||||
|
<td>${row.notes || ""}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.innerHTML = `
|
||||||
|
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Required Units</th>
|
||||||
|
<th>Pack Qty</th>
|
||||||
|
<th>Pack Cost</th>
|
||||||
|
<th>Packs Required</th>
|
||||||
|
<th>Total Cost</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rowsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLeftovers(bom, assumptions) {
|
||||||
|
const container = document.getElementById("leftovers");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
for (const boxCount of assumptions.target_box_counts) {
|
||||||
|
const hardware = computeHardwareScenario(bom, boxCount, true);
|
||||||
|
|
||||||
|
const section = document.createElement("div");
|
||||||
|
section.className = "scenario-block";
|
||||||
|
|
||||||
|
let rowsHtml = "";
|
||||||
|
for (const row of hardware.component_rows) {
|
||||||
|
rowsHtml += `
|
||||||
|
<tr>
|
||||||
|
<td>${row.name}</td>
|
||||||
|
<td>${row.purchased_units}</td>
|
||||||
|
<td>${row.required_units}</td>
|
||||||
|
<td>${row.leftover_units}</td>
|
||||||
|
<td>${row.unit_type}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.innerHTML = `
|
||||||
|
<h3>${boxCount} box${boxCount === 1 ? "" : "es"}</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Component</th>
|
||||||
|
<th>Purchased Units</th>
|
||||||
|
<th>Used Units</th>
|
||||||
|
<th>Leftover Units</th>
|
||||||
|
<th>Unit Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${rowsHtml}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadJson(url) {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load ${url}: ${response.status}`);
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const bom = await loadJson("../data/bom.json");
|
||||||
|
const assumptions = await loadJson("../data/assumptions.json");
|
||||||
|
|
||||||
|
renderAssumptions(assumptions);
|
||||||
|
renderSummaryTable(bom, assumptions);
|
||||||
|
renderComponentBreakdowns(bom, assumptions);
|
||||||
|
renderLeftovers(bom, assumptions);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<main style="padding: 2rem; font-family: Arial, Helvetica, sans-serif;">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>Unable to load project data.</p>
|
||||||
|
<pre>${String(err)}</pre>
|
||||||
|
</main>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
89
web/index.html
Normal file
89
web/index.html
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--
|
||||||
|
20260319 ChatGPT
|
||||||
|
$Header$
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
Static cost model page for the filament drybox project.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This page loads:
|
||||||
|
../data/bom.json
|
||||||
|
../data/assumptions.json
|
||||||
|
-->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Filament Drybox Cost Model</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>Filament Drybox Cost Model</h1>
|
||||||
|
<p class="subtitle">
|
||||||
|
Lot-based purchasing, filament cost, and electricity for 1, 4, and 12 dryboxes
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="card">
|
||||||
|
<h2>Overview</h2>
|
||||||
|
<p>
|
||||||
|
This page estimates the out-of-pocket cost of building filament dryboxes,
|
||||||
|
taking into account both purchased hardware sold in lots and the printing
|
||||||
|
cost of the required parts.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Assumptions</h2>
|
||||||
|
<div id="assumptions"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<table id="summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Boxes</th>
|
||||||
|
<th>Total Hardware</th>
|
||||||
|
<th>Total Printing</th>
|
||||||
|
<th>Total Cost</th>
|
||||||
|
<th>Cost / Box</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Component Breakdown</h2>
|
||||||
|
<div id="component-breakdowns"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Inventory Leftovers</h2>
|
||||||
|
<div id="leftovers"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Hardware pricing is modeled using whole-pack purchases.</li>
|
||||||
|
<li>Printed material pricing is based on estimated grams per box.</li>
|
||||||
|
<li>Electricity is based on average printer wattage over total print time.</li>
|
||||||
|
<li>All initial prices and print weights should be revised with measured values.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
Project intended for publication on salemdata.net and in Forgejo for public inspection.
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
75
web/style.css
Normal file
75
web/style.css
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
20260319 ChatGPT
|
||||||
|
$Header$
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #f4f6f8;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
footer {
|
||||||
|
background: #1f2933;
|
||||||
|
color: #fff;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1,
|
||||||
|
footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: #d9e2ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #d9e2ec;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid #cbd2d9;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background: #e9eff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-block {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-note {
|
||||||
|
color: #52606d;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mono {
|
||||||
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue