Files
2026-06-29 03:39:30 +00:00

1089 lines
94 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FortKart CNC — Retrofit Parts & OEM Supply</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<style>
:root{--bg:#09090b;--bg2:#111113;--card:#18181b;--card-hover:#1f1f23;--border:#27272a;--border-light:#3f3f46;--fg:#fafafa;--fg-muted:#71717a;--accent:#d4a853;--accent-hover:#e6bc6a;--accent-dim:rgba(212,168,83,.12);--success:#22c55e;--danger:#ef4444;--warn:#f59e0b;--info:#38bdf8}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Space Grotesk',sans-serif;background:var(--bg);color:var(--fg);overflow-x:hidden;min-height:100vh}
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:var(--bg2)}::-webkit-scrollbar-thumb{background:var(--border-light);border-radius:3px}
code,.mono{font-family:'JetBrains Mono',monospace}
#loader{position:fixed;inset:0;z-index:9999;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;transition:opacity .6s,visibility .6s}
#loader.hidden{opacity:0;visibility:hidden}
#loader .logo-text{font-size:2.5rem;font-weight:800;letter-spacing:-.03em;background:linear-gradient(135deg,var(--accent),#f0d78c);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
#loader .bar-track{width:280px;height:3px;background:var(--border);border-radius:2px;margin-top:2rem;overflow:hidden}
#loader .bar-fill{height:100%;background:var(--accent);border-radius:2px;width:0%;transition:width .4s ease}
#loader .status{color:var(--fg-muted);font-size:.8rem;margin-top:.75rem}
nav{position:sticky;top:0;z-index:100;background:rgba(9,9,11,.85);backdrop-filter:blur(16px);border-bottom:1px solid var(--border)}
.nav-link{position:relative;color:var(--fg-muted);font-size:.875rem;font-weight:500;padding:.5rem .75rem;border-radius:.5rem;transition:all .2s;cursor:pointer;white-space:nowrap}
.nav-link:hover,.nav-link.active{color:var(--fg);background:var(--accent-dim)}
.nav-link.active::after{content:'';position:absolute;bottom:-.6rem;left:50%;transform:translateX(-50%);width:20px;height:2px;background:var(--accent);border-radius:1px}
.hamburger{display:none;width:36px;height:36px;align-items:center;justify-content:center;border-radius:.5rem;border:1px solid var(--border);background:var(--card);color:var(--fg-muted);cursor:pointer;transition:all .2s}
.hamburger:hover{border-color:var(--accent);color:var(--accent)}
@media(max-width:768px){.hamburger{display:inline-flex}.nav-links{display:none!important;position:absolute;top:100%;left:0;right:0;background:var(--bg2);border-bottom:1px solid var(--border);padding:.75rem;flex-direction:column;gap:.25rem}.nav-links.open{display:flex!important}}
.badge{position:absolute;top:-.3rem;right:-.3rem;background:var(--accent);color:var(--bg);font-size:.6rem;font-weight:700;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center}
.product-card{background:var(--card);border:1px solid var(--border);border-radius:1rem;overflow:hidden;transition:all .3s ease;cursor:pointer}
.product-card:hover{border-color:var(--border-light);transform:translateY(-4px);box-shadow:0 12px 40px rgba(0,0,0,.4)}
.product-card img{width:100%;aspect-ratio:1;object-fit:cover;transition:transform .5s}
.product-card:hover img{transform:scale(1.05)}
.product-card .img-wrap{overflow:hidden;position:relative}
.product-card .cat-tag{position:absolute;top:.75rem;left:.75rem;background:rgba(9,9,11,.75);backdrop-filter:blur(8px);color:var(--accent);font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;padding:.25rem .6rem;border-radius:.375rem;border:1px solid rgba(212,168,83,.2)}
.product-card .sku-tag{position:absolute;bottom:.75rem;left:.75rem;background:rgba(9,9,11,.8);backdrop-filter:blur(8px);color:var(--fg-muted);font-size:.6rem;font-weight:500;padding:.2rem .5rem;border-radius:.25rem;font-family:'JetBrains Mono',monospace}
.btn-primary{background:var(--accent);color:var(--bg);font-weight:600;padding:.6rem 1.25rem;border-radius:.5rem;border:none;cursor:pointer;font-size:.875rem;transition:all .2s;font-family:inherit}
.btn-primary:hover{background:var(--accent-hover);transform:translateY(-1px);box-shadow:0 4px 16px rgba(212,168,83,.3)}
.btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none}
.btn-outline{background:transparent;color:var(--fg);font-weight:500;padding:.5rem 1rem;border-radius:.5rem;border:1px solid var(--border-light);cursor:pointer;font-size:.8rem;transition:all .2s;font-family:inherit}
.btn-outline:hover{border-color:var(--accent);color:var(--accent)}
.btn-sm{padding:.35rem .75rem;font-size:.75rem}
.btn-icon{width:36px;height:36px;display:inline-flex;align-items:center;justify-content:center;border-radius:.5rem;border:1px solid var(--border);background:var(--card);color:var(--fg-muted);cursor:pointer;transition:all .2s}
.btn-icon:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim)}
#cart-sidebar{position:fixed;top:0;right:0;bottom:0;width:420px;max-width:95vw;background:var(--bg2);border-left:1px solid var(--border);z-index:200;transform:translateX(100%);transition:transform .35s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column}
#cart-sidebar.open{transform:translateX(0)}
#cart-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:199;opacity:0;visibility:hidden;transition:all .3s}
#cart-overlay.open{opacity:1;visibility:visible}
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:300;display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:all .3s;padding:1rem}
.modal-overlay.open{opacity:1;visibility:visible}
.modal-content{background:var(--bg2);border:1px solid var(--border);border-radius:1rem;max-width:900px;width:100%;max-height:90vh;overflow-y:auto;transform:scale(.95);transition:transform .3s}
.modal-overlay.open .modal-content{transform:scale(1)}
.toast{padding:.75rem 1rem;border-radius:.5rem;font-size:.8rem;font-weight:500;display:flex;align-items:center;gap:.5rem;animation:slideInR .3s ease;border:1px solid;min-width:280px;max-width:400px}
.toast-success{background:rgba(34,197,94,.1);border-color:rgba(34,197,94,.3);color:var(--success)}
.toast-error{background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.3);color:var(--danger)}
.toast-info{background:rgba(56,189,248,.1);border-color:rgba(56,189,248,.3);color:var(--info)}
.toast-warn{background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.3);color:var(--warn)}
@keyframes slideInR{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}
.sec-dot{width:8px;height:8px;border-radius:50%;display:inline-block}
.sec-dot.on{background:var(--success);box-shadow:0 0 8px rgba(34,197,94,.5)}
.sec-dot.off{background:var(--danger);box-shadow:0 0 8px rgba(239,68,68,.5)}
.sec-dot.pending{background:var(--warn);animation:pulse 1.5s infinite}
.sec-dot.demo{background:var(--info);box-shadow:0 0 8px rgba(56,189,248,.4)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.code-block{background:#0d0d0f;border:1px solid var(--border);border-radius:.75rem;padding:1rem;overflow-x:auto;font-size:.75rem;line-height:1.7;tab-size:2}
.code-block .kw{color:#c678dd}.code-block .fn{color:#61afef}.code-block .str{color:#98c379}.code-block .cm{color:#5c6370;font-style:italic}.code-block .var{color:#e06c75}.code-block .num{color:#d19a66}.code-block .op{color:#56b6c2}
.tab-btn{padding:.5rem 1rem;font-size:.8rem;font-weight:500;color:var(--fg-muted);border-bottom:2px solid transparent;cursor:pointer;transition:all .2s;background:none;border-top:none;border-left:none;border-right:none;font-family:inherit}
.tab-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
.tab-btn:hover{color:var(--fg)}
.audit-table{width:100%;border-collapse:collapse;font-size:.75rem}
.audit-table th{text-align:left;padding:.6rem .75rem;color:var(--fg-muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;font-size:.65rem;border-bottom:1px solid var(--border)}
.audit-table td{padding:.6rem .75rem;border-bottom:1px solid var(--border);color:var(--fg);vertical-align:top}
.audit-table tr:hover td{background:var(--accent-dim)}
.field{width:100%;background:var(--card);border:1px solid var(--border);border-radius:.5rem;padding:.6rem .75rem;color:var(--fg);font-size:.875rem;font-family:inherit;transition:border-color .2s;outline:none}
.field:focus{border-color:var(--accent)}
.field.error{border-color:var(--danger)}
.field-label{font-size:.75rem;font-weight:500;color:var(--fg-muted);margin-bottom:.35rem;display:block}
.bg-glow{position:fixed;width:600px;height:600px;border-radius:50%;filter:blur(150px);opacity:.04;pointer-events:none;z-index:0}
.stars{color:var(--accent);font-size:.7rem;letter-spacing:1px}
.cat-pill{padding:.35rem .85rem;border-radius:2rem;font-size:.75rem;font-weight:500;border:1px solid var(--border);color:var(--fg-muted);cursor:pointer;transition:all .2s;background:transparent;font-family:inherit;white-space:nowrap}
.cat-pill:hover{border-color:var(--border-light);color:var(--fg)}
.cat-pill.active{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
.param-chip{padding:.2rem .6rem;border-radius:.25rem;font-size:.68rem;font-weight:500;border:1px solid var(--border);color:var(--fg-muted);cursor:pointer;transition:all .15s;background:transparent;font-family:inherit}
.param-chip:hover{border-color:var(--border-light);color:var(--fg)}
.param-chip.active{background:rgba(56,189,248,.12);border-color:var(--info);color:var(--info)}
.view{display:none;animation:fadeIn .3s ease}
.view.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
.qty-ctrl{display:inline-flex;align-items:center;border:1px solid var(--border);border-radius:.375rem;overflow:hidden}
.qty-ctrl button{width:28px;height:28px;background:var(--card);border:none;color:var(--fg-muted);cursor:pointer;font-size:.8rem;transition:all .15s}
.qty-ctrl button:hover{background:var(--accent-dim);color:var(--accent)}
.qty-ctrl span{width:32px;text-align:center;font-size:.8rem;font-weight:600;background:var(--bg2)}
.spec-tag{display:inline-block;background:var(--accent-dim);color:var(--accent);font-size:.65rem;font-weight:600;padding:.15rem .45rem;border-radius:.25rem;margin-right:.3rem;margin-bottom:.3rem}
.hero-stat{text-align:center;padding:1rem}
.hero-stat .val{font-size:1.5rem;font-weight:800;color:var(--accent)}
.hero-stat .lbl{font-size:.7rem;color:var(--fg-muted);margin-top:.15rem;text-transform:uppercase;letter-spacing:.05em}
.tier-row{display:flex;justify-content:space-between;padding:.3rem 0;font-size:.78rem;border-bottom:1px solid var(--border)}
.tier-row:last-child{border-bottom:none}
.tier-row.active-tier{color:var(--accent);font-weight:600}
.spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--bg);border-top-color:transparent;border-radius:50%;animation:spin .6s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.freight-badge{background:rgba(245,158,11,.12);color:var(--warn);font-size:.65rem;font-weight:600;padding:.2rem .5rem;border-radius:.25rem;border:1px solid rgba(245,158,11,.25)}
</style>
</head>
<body>
<div class="bg-glow" style="top:-200px;left:-200px;background:var(--accent)"></div>
<div class="bg-glow" style="bottom:-200px;right:-200px;background:#e06c75"></div>
<div id="loader">
<i class="fa-solid fa-gear" style="font-size:2.5rem;color:var(--accent);margin-bottom:1rem;animation:spin 2s linear infinite"></i>
<div class="logo-text">FortKart CNC</div>
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">Retrofit Parts & OEM Supply</p>
<div class="bar-track"><div class="bar-fill" id="load-bar"></div></div>
<div class="status" id="load-status">Initializing security engine...</div>
</div>
<nav id="main-nav" style="opacity:0;transition:opacity .5s">
<div style="max-width:1320px;margin:0 auto;padding:.75rem 1.5rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;position:relative">
<div style="display:flex;align-items:center;gap:.5rem;cursor:pointer" onclick="navigate('shop')">
<i class="fa-solid fa-gear" style="color:var(--accent);font-size:1.1rem"></i>
<span style="font-weight:700;font-size:1.1rem;letter-spacing:-.02em">FortKart <span style="color:var(--fg-muted);font-weight:400;font-size:.8rem">CNC</span></span>
</div>
<button class="hamburger" id="hamburger-btn" onclick="toggleMobileNav()" aria-label="Menu"><i class="fa-solid fa-bars"></i></button>
<div class="nav-links" id="nav-links" style="display:flex;align-items:center;gap:.25rem">
<div class="nav-link active" data-view="shop" onclick="navigate('shop');closeMobileNav()"><i class="fa-solid fa-wrench" style="margin-right:.35rem"></i>Parts</div>
<div class="nav-link" data-view="audit" onclick="navigate('audit');closeMobileNav()"><i class="fa-solid fa-clipboard-list" style="margin-right:.35rem"></i>Audit Log</div>
<div class="nav-link" data-view="architecture" onclick="navigate('architecture');closeMobileNav()"><i class="fa-solid fa-code" style="margin-right:.35rem"></i>Architecture</div>
<div class="nav-link" data-view="security" onclick="navigate('security');closeMobileNav()"><i class="fa-solid fa-lock" style="margin-right:.35rem"></i>Security</div>
</div>
<div style="display:flex;align-items:center;gap:.75rem">
<div id="sec-indicator" style="display:flex;align-items:center;gap:.4rem;font-size:.7rem;color:var(--fg-muted)">
<span class="sec-dot pending" id="sec-dot-wasm" title="WASM (demonstrational)"></span><span title="WASM demo module">WASM</span>
<span class="sec-dot pending" id="sec-dot-crypto" style="margin-left:.5rem" title="Web Crypto API"></span><span title="SHA-256 via Web Crypto API">Crypto</span>
</div>
<button class="btn-icon" style="position:relative" onclick="toggleCart()" aria-label="Open cart">
<i class="fa-solid fa-cart-shopping"></i>
<span class="badge" id="cart-count" style="display:none">0</span>
</button>
</div>
</div>
</nav>
<main style="max-width:1320px;margin:0 auto;padding:1.5rem;position:relative;z-index:1">
<div class="view active" id="view-shop">
<div style="background:linear-gradient(135deg,var(--card) 0%,rgba(212,168,83,.06) 100%);border:1px solid var(--border);border-radius:1rem;padding:2rem;margin-bottom:2rem;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1.5rem">
<div style="flex:1;min-width:280px">
<h1 style="font-size:2rem;font-weight:800;letter-spacing:-.03em;line-height:1.15">CNC Retrofit Components<br>& OEM Replacement Parts</h1>
<p style="color:var(--fg-muted);margin-top:.6rem;font-size:.9rem;max-width:520px;line-height:1.5">Ball screws, spindles, controllers, and tooling for legacy machines. All transactions verified through SHA-256 audit chain.</p>
</div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<div class="hero-stat"><div class="val">2,400+</div><div class="lbl">Parts in Stock</div></div>
<div class="hero-stat"><div class="val">98.7%</div><div class="lbl">Fill Rate</div></div>
<div class="hero-stat"><div class="val">48hr</div><div class="lbl">Ship Time</div></div>
</div>
</div>
<div style="display:flex;gap:1rem;margin-bottom:1rem;flex-wrap:wrap;align-items:center">
<div style="flex:1;min-width:200px;position:relative">
<i class="fa-solid fa-magnifying-glass" style="position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--fg-muted);font-size:.8rem"></i>
<input class="field" style="padding-left:2.2rem" placeholder="Search by SKU, name, or spec..." id="search-input" oninput="renderProducts()">
</div>
<div style="display:flex;gap:.4rem;flex-wrap:wrap" id="cat-filters"></div>
<select class="field" style="width:auto;min-width:140px" id="sort-select" onchange="renderProducts()">
<option value="featured">Featured</option><option value="price-asc">Price: LowHigh</option>
<option value="price-desc">Price: HighLow</option><option value="rating">Top Rated</option><option value="name">Name AZ</option>
</select>
</div>
<div id="param-filters" style="margin-bottom:1rem;display:none"></div>
<div id="product-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1.25rem"></div>
<div id="no-results" style="display:none;text-align:center;padding:4rem 1rem;color:var(--fg-muted)">
<i class="fa-solid fa-box-open" style="font-size:2.5rem;margin-bottom:1rem;display:block;opacity:.4"></i><p>No parts match your search.</p>
</div>
</div>
<div class="view" id="view-audit">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;flex-wrap:wrap;gap:1rem">
<div><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Audit Trail</h1>
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">SHA-256 hash-chained log — persisted to localStorage</p></div>
<div style="display:flex;gap:.5rem;align-items:center">
<span id="chain-status" style="font-size:.75rem;font-weight:600"></span>
<button class="btn-outline btn-sm" onclick="verifyChain()"><i class="fa-solid fa-link" style="margin-right:.35rem"></i>Verify Chain</button>
<button class="btn-outline btn-sm" onclick="clearAuditLog()"><i class="fa-solid fa-trash" style="margin-right:.35rem"></i>Clear</button>
</div>
</div>
<div id="audit-persist-note" style="background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.2);border-radius:.5rem;padding:.5rem .75rem;margin-bottom:1rem;font-size:.72rem;color:var(--info);display:none">
<i class="fa-solid fa-database" style="margin-right:.35rem"></i>Loaded <span id="persist-count">0</span> entries from localStorage. Chain integrity will be verified.
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;overflow:hidden">
<div style="overflow-x:auto;max-height:70vh;overflow-y:auto">
<table class="audit-table"><thead><tr><th>Time</th><th>Action</th><th>Details</th><th>Hash</th><th>Prev Hash</th></tr></thead>
<tbody id="audit-tbody"></tbody></table>
</div>
<div id="audit-empty" style="padding:3rem;text-align:center;color:var(--fg-muted)">
<i class="fa-solid fa-clipboard-list" style="font-size:2rem;margin-bottom:.75rem;display:block;opacity:.3"></i>
<p>Audit log is empty. Interact with the store to generate entries.</p>
</div>
</div>
</div>
<div class="view" id="view-architecture">
<div style="margin-bottom:1.5rem"><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">System Architecture</h1>
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">PHP backend, vanilla JS security engine with Web Crypto API, demonstrational WASM module</p></div>
<div style="display:flex;gap:.25rem;border-bottom:1px solid var(--border);margin-bottom:1rem;flex-wrap:wrap">
<button class="tab-btn active" onclick="switchArchTab('php',this)">PHP Backend</button>
<button class="tab-btn" onclick="switchArchTab('js',this)">JS Security Engine</button>
<button class="tab-btn" onclick="switchArchTab('wasm',this)">WASM (Demo)</button>
<button class="tab-btn" onclick="switchArchTab('flow',this)">Request Flow</button>
</div>
<div id="arch-php" class="arch-panel"></div>
<div id="arch-js" class="arch-panel" style="display:none"></div>
<div id="arch-wasm" class="arch-panel" style="display:none"></div>
<div id="arch-flow" class="arch-panel" style="display:none"></div>
</div>
<div class="view" id="view-security">
<div style="margin-bottom:1.5rem"><h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Security Dashboard</h1>
<p style="color:var(--fg-muted);font-size:.85rem;margin-top:.25rem">Real-time status of all security layers</p></div>
<div id="security-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem"></div>
</div>
<div class="view" id="view-checkout">
<div style="max-width:640px;margin:0 auto">
<button class="btn-outline btn-sm" onclick="navigate('shop')" style="margin-bottom:1.5rem"><i class="fa-solid fa-arrow-left" style="margin-right:.35rem"></i>Back to Parts</button>
<h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em;margin-bottom:.25rem">Checkout</h1>
<p style="color:var(--fg-muted);font-size:.85rem;margin-bottom:1.5rem">All fields sanitized through the JS security engine</p>
<div id="checkout-form"></div>
</div>
</div>
<div class="view" id="view-confirmation">
<div style="max-width:640px;margin:0 auto;text-align:center;padding:3rem 0">
<div style="width:72px;height:72px;border-radius:50%;background:rgba(34,197,94,.12);border:2px solid var(--success);display:inline-flex;align-items:center;justify-content:center;margin-bottom:1.5rem">
<i class="fa-solid fa-check" style="font-size:1.8rem;color:var(--success)"></i></div>
<h1 style="font-size:1.75rem;font-weight:800;letter-spacing:-.03em">Order Confirmed</h1>
<p style="color:var(--fg-muted);margin-top:.5rem;font-size:.9rem" id="confirm-text"></p>
<div id="confirm-details" style="margin-top:2rem;text-align:left"></div>
<button class="btn-primary" style="margin-top:2rem" onclick="navigate('shop')">Continue Shopping</button>
</div>
</div>
</main>
<div id="cart-overlay" onclick="toggleCart()"></div>
<aside id="cart-sidebar">
<div style="padding:1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
<h2 style="font-size:1.1rem;font-weight:700">Your Cart</h2>
<button class="btn-icon" onclick="toggleCart()" aria-label="Close cart"><i class="fa-solid fa-xmark"></i></button>
</div>
<div id="cart-items" style="flex:1;overflow-y:auto;padding:1rem"></div>
<div id="cart-footer" style="padding:1.25rem;border-top:1px solid var(--border)">
<div style="display:flex;justify-content:space-between;margin-bottom:.25rem;font-size:.85rem"><span style="color:var(--fg-muted)">Subtotal</span><span id="cart-subtotal" style="font-weight:600">$0.00</span></div>
<div style="display:flex;justify-content:space-between;margin-bottom:1rem;font-size:.85rem"><span style="color:var(--fg-muted)">Security Fee</span><span style="font-weight:600;color:var(--success)">$0.00</span></div>
<div style="display:flex;justify-content:space-between;margin-bottom:1.25rem;font-size:1.1rem;font-weight:700"><span>Total</span><span id="cart-total" style="color:var(--accent)">$0.00</span></div>
<button class="btn-primary" style="width:100%" id="checkout-btn" onclick="goToCheckout()" disabled>Proceed to Checkout</button>
<p style="text-align:center;font-size:.65rem;color:var(--fg-muted);margin-top:.5rem"><i class="fa-solid fa-shield-halved" style="margin-right:.25rem"></i>SHA-256 audit chain • CSRF protected</p>
</div>
</aside>
<div class="modal-overlay" id="product-modal"><div class="modal-content" id="product-modal-content"></div></div>
<div id="toast-container" style="position:fixed;top:5rem;right:1.5rem;z-index:500;display:flex;flex-direction:column;gap:.5rem"></div>
<script>
/* ================================================================
WASM MODULE — DJB2 hash (demonstrational, not cryptographic)
================================================================ */
const WASM_BYTES=new Uint8Array([0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,0x01,0x07,0x01,0x60,0x01,0x7f,0x01,0x7f,0x03,0x02,0x01,0x00,0x05,0x03,0x01,0x00,0x01,0x07,0x0e,0x02,0x03,0x6d,0x65,0x6d,0x02,0x00,0x04,0x68,0x61,0x73,0x68,0x00,0x00,0x0a,0x37,0x01,0x35,0x01,0x02,0x7f,0x41,0x85,0x2a,0x21,0x02,0x41,0x00,0x21,0x01,0x02,0x40,0x03,0x40,0x20,0x01,0x20,0x00,0x4f,0x0d,0x01,0x20,0x02,0x41,0x05,0x74,0x20,0x02,0x6a,0x20,0x01,0x2d,0x00,0x00,0x6a,0x21,0x02,0x20,0x01,0x41,0x01,0x6a,0x21,0x01,0x0c,0x00,0x0b,0x0b,0x20,0x02,0x0b]);
let wasmInstance=null,wasmReady=false;
function wasmHash(s){if(!wasmInstance)return jsHash(s);const b=new TextEncoder().encode(s);new Uint8Array(wasmInstance.exports.mem.buffer).set(b);return(wasmInstance.exports.hash(b.length)>>>0).toString(16).padStart(8,'0')}
function jsHash(s){let h=5381;for(let i=0;i<s.length;i++)h=((h<<5)+h+s.charCodeAt(i))|0;return(h>>>0).toString(16).padStart(8,'0')}
async function initWasm(){try{const{instance}=await WebAssembly.instantiate(WASM_BYTES);wasmInstance=instance;wasmReady=true;document.getElementById('sec-dot-wasm').className='sec-dot demo';return true}catch(e){console.warn('WASM init failed:',e);document.getElementById('sec-dot-wasm').className='sec-dot off';return false}}
/* ================================================================
SHA-256 — Web Crypto API with pure-JS fallback
================================================================ */
async function sha256(message){
if(window.crypto&&crypto.subtle){
const buf=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(message));
return[...new Uint8Array(buf)].map(b=>b.toString(16).padStart(2,'0')).join('');
}
return sha256Pure(message);
}
function sha256Pure(str){
const rotr=(v,n)=>(v>>>n)|(v<<(32-n));
const K=new Uint32Array([0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]);
const H=new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]);
const msg=new TextEncoder().encode(str);const bitLen=msg.length*8;
const padLen=((56-(msg.length+1)%64)+64)%64;
const padded=new Uint8Array(msg.length+1+padLen+8);padded.set(msg);padded[msg.length]=0x80;
const dv=new DataView(padded.buffer);dv.setUint32(padded.length-4,bitLen>>>0,false);
for(let o=0;o<padded.length;o+=64){
const W=new Uint32Array(64);
for(let i=0;i<16;i++)W[i]=dv.getUint32(o+i*4,false);
for(let i=16;i<64;i++){const s0=rotr(W[i-15],7)^rotr(W[i-15],18)^(W[i-15]>>>3);const s1=rotr(W[i-2],17)^rotr(W[i-2],19)^(W[i-2]>>>10);W[i]=(W[i-16]+s0+W[i-7]+s1)|0}
let[a,b,c,d,e,f,g,h]=H;
for(let i=0;i<64;i++){const S1=rotr(e,6)^rotr(e,11)^rotr(e,25);const ch=(e&f)(~e&g);const t1=(h+S1+ch+K[i]+W[i])|0;const S0=rotr(a,2)^rotr(a,13)^rotr(a,22);const maj=(a&b)^(a&c)^(b&c);const t2=(S0+maj)|0;h=g;g=f;f=e;e=(d+t1)|0;d=c;c=b;b=a;a=(t1+t2)|0}
H[0]=(H[0]+a)|0;H[1]=(H[1]+b)|0;H[2]=(H[2]+c)|0;H[3]=(H[3]+d)|0;H[4]=(H[4]+e)|0;H[5]=(H[5]+f)|0;H[6]=(H[6]+g)|0;H[7]=(H[7]+h)|0;
}
return[...H].map(v=>v.toString(16).padStart(8,'0')).join('');
}
/* ================================================================
SECURITY ENGINE — Pure vanilla JS, zero dependencies
================================================================ */
class SecurityEngine{
constructor(sessionId){
this.sessionId=sessionId;
this.logs=[];this.prevHash='GENESIS';
this.csrfToken=null;this.csrfCreated=0;
this.rateLimits={};
this.sanitizerPatterns=[/<script[\s\S]*?<\/script>/gi,/javascript\s*:/gi,/on\w+\s*=/gi,/<iframe/gi,/<object/gi,/<embed/gi,/<form/gi,/<input/gi,/expression\s*\(/gi,/@import/gi];
this._restoreFromStorage();
}
/* ── Audit Log with SHA-256 hash chain ── */
async logAudit(action,details){
const entry={ts:Date.now(),action,details,session:this.sessionId.slice(0,12),prevHash:this.prevHash};
const hash=await sha256(JSON.stringify(entry,Object.keys(entry).sort()));
entry.hash=hash.slice(0,16);
this.prevHash=entry.hash;
this.logs.push(entry);
this._persist();
return entry;
}
async verifyChain(){
let prev='GENESIS';
for(let i=0;i<this.logs.length;i++){
const check={...this.logs[i]};
const storedHash=check.hash;delete check.hash;
check.prevHash=prev;
const computed=(await sha256(JSON.stringify(check,Object.keys(check).sort()))).slice(0,16);
if(computed!==storedHash)return{valid:false,brokenAt:i,entry:this.logs[i]};
prev=storedHash;
}
return{valid:true,count:this.logs.length};
}
clearLogs(){this.logs=[];this.prevHash='GENESIS';this._persist()}
/* ── Input Sanitization ── */
sanitize(text){
if(typeof text!=='string')text=String(text);
let r=text;
for(const p of this.sanitizerPatterns)r=r.replace(p,'[SANITIZED]');
return r.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#x27;');
}
validateEmail(e){return/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(e)}
validateCC(n){n=n.replace(/\s/g,'');if(!/^\d{13,19}$/.test(n))return false;let d=[...n].map(Number);for(let i=d.length-2;i>=0;i-=2){d[i]*=2;if(d[i]>9)d[i]-=9}return d.reduce((a,b)=>a+b,0)%10===0}
/* ── CSRF ── */
generateCsrf(){
const buf=new Uint8Array(16);crypto.getRandomValues(buf);
this.csrfToken=[...buf].map(b=>b.toString(16).padStart(2,'0')).join('');
this.csrfCreated=Date.now();return this.csrfToken;
}
validateCsrf(token){
if(!this.csrfToken||this.csrfToken!==token)return false;
if(Date.now()-this.csrfCreated>3600000)return false;
return true;
}
rotateCsrf(){return this.generateCsrf()}
/* ── Rate Limiting ── */
checkRate(key,max=15,window=60000){
const now=Date.now();
if(!this.rateLimits[key])this.rateLimits[key]=[];
this.rateLimits[key]=this.rateLimits[key].filter(t=>now-t<window);
if(this.rateLimits[key].length>=max)return false;
this.rateLimits[key].push(now);return true;
}
/* ── Order Validation ── */
validateOrder(items,statedTotal){
const computed=items.reduce((s,i)=>s+parseFloat(i.price)*parseInt(i.qty),0);
if(Math.abs(computed-parseFloat(statedTotal))>0.01)return{valid:false,error:`Total mismatch: computed $${computed.toFixed(2)} vs stated $${statedTotal}`};
for(const i of items){const q=parseInt(i.qty);if(q<1||q>99)return{valid:false,error:`Invalid qty for ${i.name}`};if(parseFloat(i.price)<0)return{valid:false,error:`Negative price: ${i.name}`}}
return{valid:true,computedTotal:computed};
}
/* ── Persistence ── */
_persist(){try{localStorage.setItem('fortkart_audit',JSON.stringify({logs:this.logs,prevHash:this.prevHash}))}catch(e){}}
_restoreFromStorage(){try{const d=JSON.parse(localStorage.getItem('fortkart_audit'));if(d&&d.logs){this.logs=d.logs;this.prevHash=d.prevHash||'GENESIS';return this.logs.length}catch(e){}return 0}
}
/* ================================================================
PRODUCT DATA — with SKUs, weights, freight, tiers, params
================================================================ */
const PRODUCTS=[
// BALL SCREWS
{id:1,sku:'BS-1605-1000',name:'Rolled Ball Screw 1605 — 1000mm',price:89.99,weight:1.2,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs1605/400/400.jpg',description:'C7 precision rolled ball screw, 16mm dia x 5mm lead, 1000mm thread length. Chrome-plated shaft with preloaded single nut. Suitable for X/Y axes on benchtop CNC mills.',stock:45,rating:4.7,reviews:312,featured:true,specs:['16mm dia','5mm lead','C7 grade','1000mm L','Chrome-plated'],params:{diameter:'16mm',lead:'5mm',grade:'C7'},tiers:[{min:1,p:89.99},{min:5,p:82.99},{min:20,p:74.99}]},
{id:2,sku:'BS-2005G-800',name:'Precision Ground Ball Screw 2005 — 800mm',price:274.99,weight:2.8,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs2005g/400/400.jpg',description:'C3 precision ground ball screw, 20mm dia x 5mm lead, 800mm thread length. Hardened HRC 58-62. Double-nut preload for zero backlash. Ideal for plasma tables.',stock:12,rating:4.9,reviews:87,featured:true,specs:['20mm dia','5mm lead','C3 ground','800mm L','HRC 58-62','Double nut'],params:{diameter:'20mm',lead:'5mm',grade:'C3'}},
{id:3,sku:'BS-2510-1200',name:'Rolled Ball Screw 2510 — 1200mm',price:164.99,weight:3.5,freight:true,category:'ball-screws',image:'https://picsum.photos/seed/bs2510/400/400.jpg',description:'C7 rolled ball screw, 25mm dia x 10mm lead, 1200mm thread length. High-pitch for rapid traverse. Flanged single nut with dust wipers. Common Bridgeport retrofit.',stock:23,rating:4.6,reviews:156,specs:['25mm dia','10mm lead','C7 grade','1200mm L','Flanged nut'],params:{diameter:'25mm',lead:'10mm',grade:'C7'}},
{id:4,sku:'BS-3205G-1500',name:'Precision Ground Ball Screw 3205 — 1500mm',price:489.99,weight:6.2,freight:true,category:'ball-screws',image:'https://picsum.photos/seed/bs3205/400/400.jpg',description:'C3 ground ball screw, 32mm dia x 5mm lead, 1500mm length. Heavy-duty for knee mills. Induction-hardened, double-nut preload. OEM cross-ref for Fanuc/Bosch Rexroth.',stock:7,rating:4.8,reviews:64,featured:true,specs:['32mm dia','5mm lead','C3 ground','1500mm L','OEM cross-ref'],params:{diameter:'32mm',lead:'5mm',grade:'C3'}},
{id:5,sku:'BS-1204-400',name:'Miniature Ball Screw 1204 — 400mm',price:54.99,weight:0.4,freight:false,category:'ball-screws',image:'https://picsum.photos/seed/bs1204/400/400.jpg',description:'C7 rolled miniature ball screw, 12mm dia x 4mm lead, 400mm length. Compact for Z-axis on small engravers and CO2 laser cutters. Aluminum nut body.',stock:68,rating:4.5,reviews:201,specs:['12mm dia','4mm lead','C7 grade','400mm L','Aluminum nut'],params:{diameter:'12mm',lead:'4mm',grade:'C7'},tiers:[{min:1,p:54.99},{min:5,p:49.99},{min:25,p:42.99}]},
// LEAD NUTS
{id:6,sku:'BN-1605-S',name:'Flanged Ball Nut — 1605 Single',price:32.99,weight:0.15,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn1605/400/400.jpg',description:'Replacement single ball nut for 1605 ball screws. Flanged with 4x M5 bolt pattern. Adjustable preload via spacer. Includes return tube and 3mm ball set.',stock:120,rating:4.4,reviews:178,specs:['1605 compat','Flanged','M5 mount','3mm balls'],params:{size:'1605',type:'Ball'},tiers:[{min:1,p:32.99},{min:10,p:28.99},{min:50,p:24.99}]},
{id:7,sku:'BN-2005-DBL',name:'Double Ball Nut — 2005 Preloaded',price:79.99,weight:0.35,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn2005d/400/400.jpg',description:'Double-nut assembly for 2005 ball screws with adjustable preload. Eliminates axial backlash. Two flanged nuts connected by threaded spacer ring. Steel balls included.',stock:34,rating:4.7,reviews:92,featured:true,specs:['2005 compat','Double nut','Adjustable preload','Zero backlash'],params:{size:'2005',type:'Ball'}},
{id:8,sku:'BN-2510-S',name:'Flanged Ball Nut — 2510 Single',price:44.99,weight:0.5,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/bn2510/400/400.jpg',description:'Heavy-duty flanged ball nut for 2510 screws. Cast iron body with lubrication port. 6x M6 bolt pattern. Dust wiper standard.',stock:56,rating:4.5,reviews:67,specs:['2510 compat','Cast iron','M6 mount','Lube port'],params:{size:'2510',type:'Ball'}},
{id:9,sku:'AN-12ACME',name:'Bronze ACME Nut — 1/2"-10 TPI',price:18.99,weight:0.08,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/acmenut/400/400.jpg',description:'Solid bronze ACME lead nut for 1/2"-10 TPI leadscrews. Self-lubricating C95400 aluminum bronze. 2-bolt flange mount. Drop-in for import lathe cross-slide nuts.',stock:210,rating:4.3,reviews:445,specs:['1/2"-10 TPI','C95400 bronze','Self-lube','2-bolt flange'],params:{size:'1/2"',type:'ACME'},tiers:[{min:1,p:18.99},{min:10,p:15.99},{min:100,p:12.99}]},
{id:10,sku:'AN-34AB',name:'Anti-Backlash Nut Kit — 3/4"-6 TPI',price:29.99,weight:0.06,freight:false,category:'lead-nuts',image:'https://picsum.photos/seed/abnut/400/400.jpg',description:'Delrin anti-backlash nut for 3/4"-6 ACME leadscrew. Split-nut with spring preload eliminates backlash without binding. Includes mounting bracket. Popular for Sherline/Taig Z-axis.',stock:95,rating:4.6,reviews:234,specs:['3/4"-6 TPI','Delrin','Spring preload','Zero backlash'],params:{size:'3/4"',type:'Anti-Backlash'}},
// SPINDLES
{id:11,sku:'SP-22K-ER20',name:'2.2kW ER20 Air-Cooled Spindle',price:189.99,weight:3.8,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp22er20/400/400.jpg',description:'2.2kW air-cooled ER20 collet spindle, 24000 RPM max. 4-bearing 7005C angular contact pairs in DB preload. 80mm mount. Includes ER20 chuck and collet set (3-12mm). VFD required.',stock:28,rating:4.6,reviews:389,featured:true,specs:['2.2kW','ER20','24k RPM','80mm mount','4-bearing','Collets incl.'],params:{power:'2.2kW',taper:'ER20',cooling:'Air'}},
{id:12,sku:'SP-32K-ER20W',name:'3.2kW ER20 Water-Cooled Spindle',price:269.99,weight:4.5,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp32er20/400/400.jpg',description:'3.2kW water-cooled ER20 spindle, 18000 RPM. Water jacket for sustained heavy cuts in aluminum and steel. 80mm mount, drop-in upgrade from 2.2kW.',stock:15,rating:4.8,reviews:156,specs:['3.2kW','ER20','18k RPM','Water-cooled','80mm mount'],params:{power:'3.2kW',taper:'ER20',cooling:'Water'}},
{id:13,sku:'SP-BT30-55K',name:'BT30 5.5kW Belt-Drive Spindle',price:1899.99,weight:32,freight:true,category:'spindles',image:'https://picsum.photos/seed/bt30sp/400/400.jpg',description:'BT30 taper 5.5kW belt-drive spindle for VMC retrofit. 8000 RPM, grease-lubricated angular contacts. Includes belt pulley set, drawbar, and tool release cylinder. Replaces Haas/Fadal/Daewoo spindles.',stock:4,rating:4.9,reviews:23,featured:true,specs:['BT30 taper','5.5kW','8k RPM','Belt-drive','Drawbar incl.'],params:{power:'5.5kW',taper:'BT30',cooling:'Grease'}},
{id:14,sku:'SP-08K-ER11',name:'ER11 800W High-Speed Spindle',price:69.99,weight:0.8,freight:false,category:'spindles',image:'https://picsum.photos/seed/sp800er11/400/400.jpg',description:'800W ER11 spindle for PCB routing and engraving. 52mm mount, up to 36000 RPM. Quiet, low vibration. Includes 3.175mm ER11 collet. Upgrade for older desktop CNCs.',stock:82,rating:4.4,reviews:567,specs:['800W','ER11','36k RPM','52mm mount','PCB/engrave'],params:{power:'0.8kW',taper:'ER11',cooling:'Air'},tiers:[{min:1,p:69.99},{min:5,p:64.99}]},
{id:15,sku:'SP-40K-ER25W',name:'4kW ER25 Water-Cooled Spindle',price:389.99,weight:5.8,freight:true,category:'spindles',image:'https://picsum.photos/seed/sp4ker25/400/400.jpg',description:'4kW ER25 water-cooled spindle, 18000 RPM. ER25 accepts up to 16mm shank. 85mm mount, 6-bearing cartridge. For aluminum profiling and steel finishing on gantry routers.',stock:9,rating:4.7,reviews:78,specs:['4kW','ER25','18k RPM','85mm mount','6-bearing'],params:{power:'4.0kW',taper:'ER25',cooling:'Water'}},
// CONTROLLERS
{id:16,sku:'CT-M3USB-5',name:'Mach3 USB Breakout Board — 5 Axis',price:49.99,weight:0.12,freight:false,category:'controllers',image:'https://picsum.photos/seed/m3usb/400/400.jpg',description:'USB CNC breakout board for Mach3/LinuxCNC. 5-axis stepper output, 4 relay outputs, 6 opto-isolated inputs. 0-10V analog spindle speed. Plug-and-play with NEMA 23 drivers.',stock:73,rating:4.5,reviews:423,specs:['USB','5-axis','Opto-isolated','Relay x4','0-10V spindle'],params:{interface:'USB',axes:'5'},tiers:[{min:1,p:49.99},{min:10,p:44.99}]},
{id:17,sku:'CT-ETH6',name:'Ethernet Motion Controller — 6 Axis',price:199.99,weight:0.2,freight:false,category:'controllers',image:'https://picsum.photos/seed/ethctrl/400/400.jpg',description:'Ethernet motion controller with 1MHz step rate. 6-axis step/dir or servo output. Hardware motion planning, probe input, spindle encoder feedback, MPG port. Supports rigid tapping.',stock:19,rating:4.8,reviews:134,featured:true,specs:['Ethernet','6-axis','1MHz step','Probe input','MPG port'],params:{interface:'Ethernet',axes:'6'}},
{id:18,sku:'CT-N23-3PK',name:'NEMA 23 Stepper Driver Set (3x)',price:89.99,weight:0.6,freight:false,category:'controllers',image:'https://picsum.photos/seed/drv23set/400/400.jpg',description:'Set of 3 TB6600-style stepper drivers. 2.5A-4.2A selectable, microstepping to 1/32. Opto-isolated inputs, auto half-current hold. Compatible with Mach3/LinuxCNC/GRBL.',stock:41,rating:4.4,reviews:289,specs:['3x drivers','2.5-4.2A','1/32 microstep','Opto-isolated'],params:{interface:'Parallel',axes:'3'},tiers:[{min:1,p:89.99},{min:5,p:79.99}]},
{id:19,sku:'CT-VFD-22',name:'2.2kW VFD — Single Phase Input',price:119.99,weight:1.5,freight:false,category:'controllers',image:'https://picsum.photos/seed/vfd22/400/400.jpg',description:'2.2kW VFD, 220V single-phase in to 3-phase out. PID speed via 0-10V or PWM. Built-in braking transistor, RS485 Modbus. Pre-programmed CNC spindle profiles. EMC filter included.',stock:31,rating:4.6,reviews:198,specs:['2.2kW','1ph in/3ph out','0-10V/PWM','Modbus RTU'],params:{interface:'Analog',axes:'N/A'}},
{id:20,sku:'CT-GRBLESP',name:'GRBL ESP32 Wireless Controller',price:39.99,weight:0.05,freight:false,category:'controllers',image:'https://picsum.photos/seed/grblesp/400/400.jpg',description:'ESP32-based GRBL controller with WiFi and Bluetooth. 4-axis TMC2209 driver sockets, 4 limit inputs, spindle PWM. Web UI for jogging and file upload. Flash GRBL-ESP32 firmware.',stock:55,rating:4.3,reviews:312,specs:['ESP32','WiFi + BT','4-axis','TMC2209','Web UI'],params:{interface:'WiFi',axes:'4'},tiers:[{min:1,p:39.99},{min:5,p:34.99},{min:25,p:29.99}]},
// LINEAR GUIDES
{id:21,sku:'LG-HGR15-600',name:'Linear Guide Rail HGR15 — 600mm Pair',price:54.99,weight:2.4,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgr15/400/400.jpg',description:'Pair of HGR15 linear guide rails, 600mm length with 4 slider blocks. Chrome-plated steel, preloaded 4-row ball circulation. Hiwin HGR15 drop-in replacement.',stock:38,rating:4.6,reviews:176,specs:['HGR15','600mm','4 blocks','Chrome-plated'],params:{size:'HGR15'}},
{id:22,sku:'LG-HGR20-1000',name:'Linear Guide Rail HGR20 — 1000mm Pair',price:109.99,weight:5.2,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgr20/400/400.jpg',description:'Pair of HGR20 linear guide rails, 1000mm with 4 blocks. Higher load capacity for gantry beams and heavy saddles. Sealed blocks with grease nipples.',stock:17,rating:4.7,reviews:89,specs:['HGR20','1000mm','4 blocks','Sealed'],params:{size:'HGR20'}},
{id:23,sku:'LG-HGH20CA-800',name:'Linear Guide Rail HGH20CA — 800mm Pair',price:134.99,weight:4.8,freight:true,category:'linear-guides',image:'https://picsum.photos/seed/hgh20ca/400/400.jpg',description:'High-precision HGH20CA wide-block guides, 800mm. Flanged wide blocks for increased moment capacity. Double-lip seals. Suitable for Z-axis with high moment loads.',stock:11,rating:4.8,reviews:52,featured:true,specs:['HGH20CA','800mm','Wide block','Double seal'],params:{size:'HGH20CA'}},
// TOOLING
{id:24,sku:'TL-ER20-9PC',name:'ER20 Collet Set — 9 Piece',price:44.99,weight:0.3,freight:false,category:'tooling',image:'https://picsum.photos/seed/er20set/400/400.jpg',description:'Complete ER20 collet set: 1, 2, 3, 4, 5, 6, 8, 10, 12mm. Spring steel, 0.015mm runout tolerance. Precision ground bore. Storage case included.',stock:62,rating:4.5,reviews:356,specs:['ER20','9 sizes','0.015mm TIR','Spring steel','Case incl.'],params:{type:'Collets'},tiers:[{min:1,p:44.99},{min:5,p:39.99}]},
{id:25,sku:'TL-BT30ER32',name:'BT30 ER32 Tool Holder — Face Mill',price:89.99,weight:0.9,freight:false,category:'tooling',image:'https://picsum.photos/seed/bt30er32/400/400.jpg',description:'BT30 shank ER32 collet chuck. 40mm gauge length, G2.5 balanced at 12k RPM. Includes ER32 nut and bearing. Direct replacement for BT30 tool magazines.',stock:14,rating:4.7,reviews:67,specs:['BT30','ER32','G2.5 balanced','40mm gauge'],params:{type:'Tool Holder'}},
{id:26,sku:'TL-EM4F-6PC',name:'Carbide End Mill Set — 4 Flute (6pc)',price:79.99,weight:0.25,freight:false,category:'tooling',image:'https://picsum.photos/seed/emset4f/400/400.jpg',description:'6-piece solid carbide 4-flute set: 3, 4, 5, 6, 8, 10mm. AlTiN coated for aluminum and steel. 50mm cut length, 75mm OAL. Micrograin carbide.',stock:29,rating:4.6,reviews:189,specs:['6 pieces','4-flute','AlTiN coated','Micrograin','3-10mm'],params:{type:'End Mills'},tiers:[{min:1,p:79.99},{min:5,p:72.99}]},
// ACCESSORIES
{id:27,sku:'AC-CPL-55-5PK',name:'Flexible Coupling Set — 5x5mm (5pc)',price:12.99,weight:0.1,freight:false,category:'accessories',image:'https://picsum.photos/seed/coupling55/400/400.jpg',description:'Set of 5 aluminum beam couplings, 5mm x 5mm bore. Zero-backlash spiral cuts for misalignment compensation. Connect stepper motors to ball screws.',stock:150,rating:4.3,reviews:534,specs:['5x5mm bore','Aluminum','Zero backlash','5 pieces'],params:{type:'Coupling'},tiers:[{min:1,p:12.99},{min:10,p:9.99},{min:50,p:7.99}]},
{id:28,sku:'AC-N23-28',name:'NEMA 23 Stepper Motor — 2.8A 270oz-in',price:34.99,weight:0.9,freight:false,category:'accessories',image:'https://picsum.photos/seed/nema23/400/400.jpg',description:'NEMA 23 stepper, 2.8A/phase, 1.8° step angle. 1.9 Nm holding torque. 4-lead bipolar. Double-shaft for encoder. Common retrofit motor for benchtop mills.',stock:88,rating:4.5,reviews:412,specs:['NEMA 23','2.8A','1.9 Nm','1.8° step','4-lead'],params:{type:'Motor'},tiers:[{min:1,p:34.99},{min:3,p:32.99},{min:10,p:29.99}]},
{id:29,sku:'AC-LSW-6PK',name:'Limit Switch Set — Mechanical (6pc)',price:14.99,weight:0.3,freight:false,category:'accessories',image:'https://picsum.photos/seed/limsw/400/400.jpg',description:'Set of 6 mechanical roller limit switches. SPDT, 10A. Metal L-bracket mounts. Wired normally-closed for CNC safety.',stock:110,rating:4.2,reviews:378,specs:['6 pieces','Roller lever','SPDT','L-bracket incl.'],params:{type:'Switch'},tiers:[{min:1,p:14.99},{min:10,p:12.99}]},
{id:30,sku:'AC-WCK-22',name:'Water Cooling Kit — Spindle 2.2kW',price:39.99,weight:2.0,freight:false,category:'accessories',image:'https://picsum.photos/seed/waterkit/400/400.jpg',description:'Complete water cooling kit. 15L tank, submersible pump 1200L/hr, 8mm silicone hose 3m, 4 clamps, coolant fluid. Plug-and-play.',stock:37,rating:4.4,reviews:223,specs:['15L tank','1200L/hr pump','8mm hose','Coolant incl.'],params:{type:'Cooling'}},
];
const CATEGORIES=['all',...new Set(PRODUCTS.map(p=>p.category))];
const CAT_LABELS={all:'All Parts','ball-screws':'Ball Screws','lead-nuts':'Lead Nuts',spindles:'Spindles',controllers:'Controllers','linear-guides':'Linear Guides',tooling:'Tooling',accessories:'Accessories'};
const CAT_LABEL_SHORT={'ball-screws':'Ball Screw','lead-nuts':'Lead Nut',spindles:'Spindle',controllers:'Controller','linear-guides':'Linear Guide',tooling:'Tooling',accessories:'Accessory'};
/* Parametric filter definitions per category */
const PARAM_DEFS={
'ball-screws':[
{key:'diameter',label:'Diameter',values:['12mm','16mm','20mm','25mm','32mm']},
{key:'lead',label:'Lead',values:['4mm','5mm','10mm']},
{key:'grade',label:'Grade',values:['C3','C7']}
],
'lead-nuts':[
{key:'type',label:'Type',values:['Ball','ACME','Anti-Backlash']},
{key:'size',label:'Size',values:['1605','2005','2510','1/2"','3/4"']}
],
'spindles':[
{key:'taper',label:'Taper',values:['ER11','ER20','ER25','BT30']},
{key:'cooling',label:'Cooling',values:['Air','Water','Grease']},
{key:'power',label:'Power',values:['0.8kW','2.2kW','3.2kW','4.0kW','5.5kW']}
],
'controllers':[
{key:'interface',label:'Interface',values:['USB','Ethernet','WiFi','Parallel','Analog']},
{key:'axes',label:'Axes',values:['3','4','5','6','N/A']}
],
'linear-guides':[
{key:'size',label:'Size',values:['HGR15','HGR20','HGH20CA']}
],
'tooling':[
{key:'type',label:'Type',values:['Collets','Tool Holder','End Mills']}
],
'accessories':[
{key:'type',label:'Type',values:['Coupling','Motor','Switch','Cooling']}
]
};
/* ================================================================
APPLICATION STATE
================================================================ */
const state={
cart:[],view:'shop',category:'all',
sessionId:crypto.randomUUID?crypto.randomUUID():'s-'+Date.now()+'-'+Math.random().toString(36).slice(2),
orderHistory:[],paramSelections:{}
};
let sec; // SecurityEngine instance
/* ================================================================
UTILITY
================================================================ */
function toast(msg,type='info'){
const icons={success:'fa-check-circle',error:'fa-exclamation-circle',info:'fa-info-circle',warn:'fa-exclamation-triangle'};
const el=document.createElement('div');el.className=`toast toast-${type}`;
el.innerHTML=`<i class="fa-solid ${icons[type]||icons.info}"></i><span>${msg}</span>`;
document.getElementById('toast-container').appendChild(el);
setTimeout(()=>{el.style.opacity='0';el.style.transform='translateX(100%)';el.style.transition='all .3s';setTimeout(()=>el.remove(),300)},3500);
}
function toggleMobileNav(){document.getElementById('nav-links').classList.toggle('open')}
function closeMobileNav(){document.getElementById('nav-links').classList.remove('open')}
/* Get effective unit price based on quantity (tier pricing) */
function getUnitPrice(product,qty){
if(!product.tiers)return product.price;
let price=product.price;
for(const t of product.tiers){if(qty>=t.min)price=t.p}
return price;
}
/* ================================================================
NAVIGATION
================================================================ */
function navigate(view){
state.view=view;
document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));
const t=document.getElementById('view-'+view);if(t)t.classList.add('active');
document.querySelectorAll('.nav-link').forEach(l=>l.classList.toggle('active',l.dataset.view===view));
if(view==='audit')renderAuditLog();
if(view==='security')renderSecurityDashboard();
window.scrollTo({top:0,behavior:'smooth'});
}
/* ================================================================
PRODUCTS
================================================================ */
function renderCategoryFilters(){
document.getElementById('cat-filters').innerHTML=CATEGORIES.map(c=>
`<button class="cat-pill ${c===state.category?'active':''}" onclick="setCategory('${c}')">${CAT_LABELS[c]||c}</button>`
).join('');
}
function setCategory(cat){
state.category=cat;state.paramSelections={};
renderCategoryFilters();renderParamFilters();renderProducts();
sec.logAudit('CATEGORY_FILTER',{category:cat});
}
function renderParamFilters(){
const container=document.getElementById('param-filters');
const defs=PARAM_DEFS[state.category];
if(!defs||state.category==='all'){container.style.display='none';container.innerHTML='';return}
container.style.display='flex';container.style.flexWrap='wrap';container.style.gap='1rem';container.style.alignItems='flex-start';
container.innerHTML=defs.map(d=>{
const selected=state.paramSelections[d.key]||[];
const chips=d.values.map(v=>`<button class="param-chip ${selected.includes(v)?'active':''}" onclick="toggleParam('${d.key}','${v}')">${v}</button>`).join('');
return `<div><div style="font-size:.68rem;color:var(--fg-muted);font-weight:600;text-transform:uppercase;letter-spacing:.06em;margin-bottom:.35rem">${d.label}</div><div style="display:flex;gap:.3rem;flex-wrap:wrap">${chips}</div></div>`;
}).join('');
}
function toggleParam(key,value){
if(!state.paramSelections[key])state.paramSelections[key]=[];
const arr=state.paramSelections[key];
const idx=arr.indexOf(value);
if(idx>-1)arr.splice(idx,1);else arr.push(value);
if(!arr.length)delete state.paramSelections[key];
renderParamFilters();renderProducts();
}
function renderProducts(){
const search=(document.getElementById('search-input').value||'').toLowerCase();
const sort=document.getElementById('sort-select').value;
let items=PRODUCTS.filter(p=>{
if(state.category!=='all'&&p.category!==state.category)return false;
const specStr=(p.specs||[]).join(' ').toLowerCase();
const matchSearch=!search||p.name.toLowerCase().includes(search)||p.description.toLowerCase().includes(search)||p.sku.toLowerCase().includes(search)||specStr.includes(search)||p.category.includes(search);
if(!matchSearch)return false;
/* Parametric filters: OR within key, AND between keys */
for(const[key,values] of Object.entries(state.paramSelections)){
if(!values.length)continue;
if(!p.params||!values.includes(p.params[key]))return false;
}
return true;
});
switch(sort){
case'price-asc':items.sort((a,b)=>a.price-b.price);break;
case'price-desc':items.sort((a,b)=>b.price-a.price);break;
case'rating':items.sort((a,b)=>b.rating-a.rating);break;
case'name':items.sort((a,b)=>a.name.localeCompare(b.name));break;
default:items.sort((a,b)=>(b.featured?1:0)-(a.featured?1:0));
}
const grid=document.getElementById('product-grid');
const noRes=document.getElementById('no-results');
if(!items.length){grid.innerHTML='';noRes.style.display='block';return}
noRes.style.display='none';
grid.innerHTML=items.map(p=>{
const inCart=state.cart.find(c=>c.product.id===p.id);
const effectivePrice=getUnitPrice(p,inCart?inCart.qty:1);
return `<div class="product-card" onclick="openProduct(${p.id})">
<div class="img-wrap">
<img src="${p.image}" alt="${p.name}" loading="lazy">
<span class="cat-tag">${CAT_LABEL_SHORT[p.category]||p.category}</span>
${p.featured?'<span style="position:absolute;top:.75rem;right:.75rem;background:var(--accent);color:var(--bg);font-size:.6rem;font-weight:700;padding:.2rem .5rem;border-radius:.25rem">POPULAR</span>':''}
<span class="sku-tag">${p.sku}</span>
</div>
<div style="padding:1rem">
<h3 style="font-size:.88rem;font-weight:600;margin-bottom:.35rem;line-height:1.3;min-height:2.4em">${p.name}</h3>
<div style="margin-bottom:.5rem;min-height:1.2em">${(p.specs||[]).slice(0,3).map(s=>`<span class="spec-tag">${s}</span>`).join('')}</div>
<div class="stars" style="margin-bottom:.5rem">${'<i class="fa-solid fa-star"></i>'.repeat(Math.floor(p.rating))}${p.rating%1?'<i class="fa-solid fa-star-half-stroke"></i>':''} <span style="color:var(--fg-muted);font-size:.7rem;margin-left:.3rem">(${p.reviews})</span></div>
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.3rem">
<div>
<span style="font-size:1.1rem;font-weight:700;color:var(--accent)">$${effectivePrice.toFixed(2)}</span>
${p.tiers&&p.tiers.length>1?`<span style="font-size:.65rem;color:var(--fg-muted);margin-left:.3rem">${inCart&&inCart.qty>=p.tiers[p.tiers.length-1].min?'bulk':'from $'+p.tiers[p.tiers.length-1].p.toFixed(2)}</span>`:''}
</div>
<div style="display:flex;align-items:center;gap:.4rem">
${p.freight?'<span class="freight-badge"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight</span>':''}
<span style="font-size:.68rem;color:${p.stock<10?'var(--warn)':'var(--fg-muted)'}">${p.stock<10?p.stock+' left':p.stock+' in stock'}</span>
</div>
</div>
</div>
</div>`;
}).join('');
}
/* ================================================================
PRODUCT DETAIL MODAL
================================================================ */
function openProduct(id){
const p=PRODUCTS.find(x=>x.id===id);if(!p)return;
sec.logAudit('PRODUCT_VIEW',{product_id:p.id,sku:p.sku});
const inCart=state.cart.find(c=>c.product.id===p.id);
const unitPrice=getUnitPrice(p,inCart?inCart.qty:1);
document.getElementById('product-modal-content').innerHTML=`
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0" class="modal-grid">
<div style="overflow:hidden;border-radius:1rem 0 0 1rem;position:relative">
<img src="${p.image}" alt="${p.name}" style="width:100%;height:100%;object-fit:cover;min-height:320px">
<span style="position:absolute;bottom:.75rem;left:.75rem;background:rgba(9,9,11,.85);color:var(--fg-muted);font-size:.75rem;font-weight:500;padding:.3rem .6rem;border-radius:.3rem;font-family:'JetBrains Mono',monospace">${p.sku}</span>
</div>
<div style="padding:2rem;position:relative">
<button class="btn-icon" style="position:absolute;top:1rem;right:1rem" onclick="closeProductModal()"><i class="fa-solid fa-xmark"></i></button>
<div style="display:flex;align-items:center;gap:.5rem;margin-bottom:.25rem">
<span style="font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--accent)">${p.category.replace('-',' ')}</span>
${p.freight?'<span class="freight-badge"><i class="fa-solid fa-truck" style="margin-right:.2rem"></i>Freight Quote Required</span>':''}
</div>
<h2 style="font-size:1.3rem;font-weight:800;margin-top:.4rem;letter-spacing:-.02em;line-height:1.2">${p.name}</h2>
<div class="stars" style="margin:.6rem 0">${'<i class="fa-solid fa-star"></i>'.repeat(Math.floor(p.rating))}${p.rating%1?'<i class="fa-solid fa-star-half-stroke"></i>':''} <span style="color:var(--fg-muted);font-size:.8rem;margin-left:.3rem">${p.rating} (${p.reviews} reviews)</span></div>
<p style="color:var(--fg-muted);font-size:.85rem;line-height:1.6;margin-bottom:1rem">${p.description}</p>
<div style="display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:1rem">${(p.specs||[]).map(s=>`<span class="spec-tag">${s}</span>`).join('')}</div>
<div style="display:flex;align-items:baseline;gap:.75rem;margin-bottom:.25rem">
<span style="font-size:2rem;font-weight:800;color:var(--accent)">$${unitPrice.toFixed(2)}</span>
<span style="font-size:.75rem;color:var(--fg-muted)">${p.weight} kg</span>
</div>
${p.tiers&&p.tiers.length>1?`<div style="background:var(--bg);border:1px solid var(--border);border-radius:.5rem;padding:.6rem .75rem;margin-bottom:1rem">
<div style="font-size:.7rem;font-weight:600;color:var(--fg-muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:.4rem">Quantity Pricing</div>
${p.tiers.map(t=>`<div class="tier-row ${inCart&&inCart.qty>=t.min?'active-tier':''}"><span>${t.min}+ units</span><span>$${t.p.toFixed(2)} ea</span></div>`).join('')}
</div>`:''}
<p style="font-size:.8rem;color:${p.stock<10?'var(--warn)':'var(--success)'};margin-bottom:1.25rem"><i class="fa-solid ${p.stock<10?'fa-exclamation-triangle':'fa-check-circle'}" style="margin-right:.35rem"></i>${p.stock<10?'Only '+p.stock+' remaining — order soon':p.stock+' units in stock'}</p>
<div style="display:flex;gap:.75rem;flex-wrap:wrap">
${inCart?`<button class="btn-outline" onclick="closeProductModal()"><i class="fa-solid fa-check" style="margin-right:.35rem;color:var(--success)"></i>In Cart (${inCart.qty})</button>`:
`<button class="btn-primary" onclick="addToCart(${p.id});closeProductModal()"><i class="fa-solid fa-cart-plus" style="margin-right:.35rem"></i>Add to Cart</button>`}
<button class="btn-outline" onclick="addToCart(${p.id});closeProductModal();goToCheckout()">Buy Now</button>
</div>
<div style="margin-top:1rem;padding-top:1rem;border-top:1px solid var(--border);font-size:.7rem;color:var(--fg-muted);display:flex;flex-direction:column;gap:.25rem">
<span><i class="fa-solid fa-shield-halved" style="width:16px;color:var(--accent)"></i> Sanitized through JS security engine</span>
<span><i class="fa-solid fa-link" style="width:16px;color:var(--accent)"></i> View logged to SHA-256 audit chain</span>
<span><i class="fa-solid fa-truck" style="width:16px;color:var(--accent)"></i> ${p.freight?'Freight quote required — ships within 48hr':'Ships within 48 hours'}</span>
</div>
</div>
</div>
<style>@media(max-width:640px){.modal-grid{grid-template-columns:1fr!important}.modal-grid img{border-radius:1rem 1rem 0 0!important;min-height:200px!important}}</style>`;
document.getElementById('product-modal').classList.add('open');
}
function closeProductModal(){document.getElementById('product-modal').classList.remove('open')}
document.getElementById('product-modal').addEventListener('click',e=>{if(e.target.id==='product-modal')closeProductModal()});
/* ================================================================
CART — with tier pricing and stock decrement
================================================================ */
async function addToCart(id){
if(!sec.checkRate('add_to_cart')){toast('Rate limited — slow down','warn');return}
const p=PRODUCTS.find(x=>x.id===id);if(!p)return;
if(p.freight){toast(`${p.sku} requires a freight quote — contact sales`,'warn');sec.logAudit('FREIGHT_BLOCKED',{sku:p.sku});return}
const existing=state.cart.find(c=>c.product.id===id);
if(existing){if(existing.qty>=p.stock){toast('Maximum stock reached','warn');return}existing.qty++}
else{if(p.stock<1){toast('Out of stock','error');return}state.cart.push({product:p,qty:1})}
await sec.logAudit('ADD_TO_CART',{sku:p.sku,qty:1,price:p.price});
toast(`${p.sku} added to cart`,'success');renderCart();renderProducts();
}
async function removeFromCart(id){
const idx=state.cart.findIndex(c=>c.product.id===id);if(idx===-1)return;
const item=state.cart[idx];
await sec.logAudit('REMOVE_FROM_CART',{sku:item.product.sku,name:item.product.name});
state.cart.splice(idx,1);toast('Item removed','info');renderCart();renderProducts();
}
async function updateQty(id,delta){
const item=state.cart.find(c=>c.product.id===id);if(!item)return;
const nq=item.qty+delta;
if(nq<1){await removeFromCart(id);return}
if(nq>item.product.stock){toast('Maximum stock reached','warn');return}
item.qty=nq;
await sec.logAudit('QTY_CHANGE',{sku:item.product.sku,new_qty:nq});
renderCart();renderProducts();
}
function getCartTotal(){return state.cart.reduce((s,c)=>s+getUnitPrice(c.product,c.qty)*c.qty,0)}
function renderCart(){
const items=document.getElementById('cart-items');
const countEl=document.getElementById('cart-count');
const totalItems=state.cart.reduce((s,c)=>s+c.qty,0);
countEl.textContent=totalItems;countEl.style.display=totalItems>0?'flex':'none';
document.getElementById('checkout-btn').disabled=!state.cart.length;
if(!state.cart.length){
items.innerHTML=`<div style="text-align:center;padding:3rem 1rem;color:var(--fg-muted)"><i class="fa-solid fa-cart-shopping" style="font-size:2rem;margin-bottom:.75rem;display:block;opacity:.3"></i><p>Your cart is empty</p></div>`;
document.getElementById('cart-subtotal').textContent='$0.00';document.getElementById('cart-total').textContent='$0.00';return;
}
items.innerHTML=state.cart.map(c=>{
const up=getUnitPrice(c.product,c.qty);
return `<div style="display:flex;gap:.75rem;padding:.75rem 0;border-bottom:1px solid var(--border)">
<img src="${c.product.image}" alt="${c.product.sku}" style="width:56px;height:56px;border-radius:.5rem;object-fit:cover">
<div style="flex:1;min-width:0">
<h4 style="font-size:.75rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${c.product.name}">${c.product.sku}</h4>
<p style="font-size:.7rem;color:var(--fg-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${c.product.name}</p>
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.4rem">
<div class="qty-ctrl"><button onclick="event.stopPropagation();updateQty(${c.product.id},-1)"></button><span>${c.qty}</span><button onclick="event.stopPropagation();updateQty(${c.product.id},1)">+</button></div>
<span style="font-size:.7rem;color:var(--fg-muted)">$${up.toFixed(2)} ea</span>
<button style="background:none;border:none;color:var(--fg-muted);cursor:pointer;font-size:.7rem;margin-left:auto" onclick="event.stopPropagation();removeFromCart(${c.product.id})"><i class="fa-solid fa-trash"></i></button>
</div>
</div>
<span style="font-size:.85rem;font-weight:700;white-space:nowrap">$${(up*c.qty).toFixed(2)}</span>
</div>`;
}).join('');
const total=getCartTotal();
document.getElementById('cart-subtotal').textContent='$'+total.toFixed(2);
document.getElementById('cart-total').textContent='$'+total.toFixed(2);
}
function toggleCart(){document.getElementById('cart-sidebar').classList.toggle('open');document.getElementById('cart-overlay').classList.toggle('open')}
/* ================================================================
CHECKOUT — with loading state and stock decrement
================================================================ */
function goToCheckout(){
if(!state.cart.length){toast('Cart is empty','warn');return}
document.getElementById('cart-sidebar').classList.remove('open');document.getElementById('cart-overlay').classList.remove('open');
sec.logAudit('CHECKOUT_START',{items:state.cart.length,total:getCartTotal().toFixed(2)});
renderCheckoutForm();navigate('checkout');
}
function renderCheckoutForm(){
const total=getCartTotal();
document.getElementById('checkout-form').innerHTML=`
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-user" style="color:var(--accent);margin-right:.5rem"></i>Customer Information</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
<div><label class="field-label">First Name</label><input class="field" id="ch-first" placeholder="John"></div>
<div><label class="field-label">Last Name</label><input class="field" id="ch-last" placeholder="Doe"></div>
</div>
<div style="margin-top:.75rem"><label class="field-label">Email</label><input class="field" id="ch-email" type="email" placeholder="john@machine-shop.com"></div>
<div style="margin-top:.75rem"><label class="field-label">Company (optional)</label><input class="field" id="ch-company" placeholder="Acme Machine Co."></div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-credit-card" style="color:var(--accent);margin-right:.5rem"></i>Payment</h3>
<div style="margin-bottom:.75rem"><label class="field-label">Card Number</label><input class="field" id="ch-card" placeholder="4242 4242 4242 4242" maxlength="19" oninput="formatCard(this)"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
<div><label class="field-label">Expiry</label><input class="field" id="ch-expiry" placeholder="MM/YY" maxlength="5" oninput="formatExpiry(this)"></div>
<div><label class="field-label">CVC</label><input class="field" id="ch-cvc" placeholder="123" maxlength="4"></div>
</div>
</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;margin-bottom:1rem">
<h3 style="font-size:.9rem;font-weight:700;margin-bottom:1rem"><i class="fa-solid fa-truck" style="color:var(--accent);margin-right:.5rem"></i>Shipping</h3>
<div style="margin-bottom:.75rem"><label class="field-label">Address</label><input class="field" id="ch-addr" placeholder="123 Industrial Parkway, Unit 4"></div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:.75rem">
<div><label class="field-label">City</label><input class="field" id="ch-city" placeholder="Detroit"></div>
<div><label class="field-label">State</label><input class="field" id="ch-state" placeholder="MI" maxlength="2"></div>
<div><label class="field-label">ZIP</label><input class="field" id="ch-zip" placeholder="48201" maxlength="10"></div>
</div>
</div>
<div style="background:var(--accent-dim);border:1px solid rgba(212,168,83,.2);border-radius:.75rem;padding:1rem;margin-bottom:1.25rem;display:flex;align-items:center;gap:.75rem">
<i class="fa-solid fa-shield-halved" style="color:var(--accent);font-size:1.1rem"></i>
<div><p style="font-size:.8rem;font-weight:600">All fields sanitized through JS security engine (Web Crypto SHA-256)</p>
<p style="font-size:.7rem;color:var(--fg-muted)">CSRF: ${sec.csrfToken?sec.csrfToken.slice(0,16)+'...':'pending'}</p></div>
</div>
<div id="checkout-errors" style="margin-bottom:1rem"></div>
<button class="btn-primary" style="width:100%;padding:.85rem;font-size:1rem" id="place-order-btn" onclick="submitOrder()">
<i class="fa-solid fa-lock" style="margin-right:.5rem"></i>Place Order — $${total.toFixed(2)}
</button>`;
}
function formatCard(el){let v=el.value.replace(/\D/g,'').slice(0,16);el.value=v.replace(/(.{4})/g,'$1 ').trim()}
function formatExpiry(el){let v=el.value.replace(/\D/g,'').slice(0,4);if(v.length>=3)v=v.slice(0,2)+'/'+v.slice(2);el.value=v}
async function submitOrder(){
const btn=document.getElementById('place-order-btn');
if(!sec.checkRate('submit_order')){toast('Rate limited — please wait','warn');return}
/* Show loading state */
btn.disabled=true;btn.innerHTML='<span class="spinner"></span> Processing...';
/* Small delay so user sees the spinner */
await new Promise(r=>setTimeout(r,400));
const fields={first:document.getElementById('ch-first').value.trim(),last:document.getElementById('ch-last').value.trim(),email:document.getElementById('ch-email').value.trim(),company:document.getElementById('ch-company').value.trim(),card:document.getElementById('ch-card').value.replace(/\s/g,''),expiry:document.getElementById('ch-expiry').value.trim(),cvc:document.getElementById('ch-cvc').value.trim(),addr:document.getElementById('ch-addr').value.trim(),city:document.getElementById('ch-city').value.trim(),state:document.getElementById('ch-state').value.trim(),zip:document.getElementById('ch-zip').value.trim()};
document.querySelectorAll('.field').forEach(f=>f.classList.remove('error'));
document.getElementById('checkout-errors').innerHTML='';
let errors=[];
const sanitized={};
for(const[k,v] of Object.entries(fields)){sanitized[k]=sec.sanitize(v);if(!v&&k!=='company'&&k!=='card')errors.push(k+' is required')}
if(fields.email&&!sec.validateEmail(fields.email))errors.push('Invalid email address');
if(fields.card&&!sec.validateCC(fields.card))errors.push('Invalid card number (try 4242 4242 4242 4242)');
if(fields.expiry&&!/^\d{2}\/\d{2}$/.test(fields.expiry))errors.push('Invalid expiry (MM/YY)');
if(fields.cvc&&!/^\d{3,4}$/.test(fields.cvc))errors.push('Invalid CVC');
const orderItems=state.cart.map(c=>({name:c.product.name,sku:c.product.sku,price:getUnitPrice(c.product,c.qty),qty:c.qty}));
const ov=sec.validateOrder(orderItems,getCartTotal().toFixed(2));
if(!ov.valid)errors.push('Order validation: '+(ov.error||'unknown'));
if(errors.length){
errors.forEach(e=>{const d=document.createElement('div');d.style.cssText='color:var(--danger);font-size:.8rem;padding:.35rem 0';d.innerHTML=`<i class="fa-solid fa-circle-xmark" style="margin-right:.35rem"></i>${e}`;document.getElementById('checkout-errors').appendChild(d)});
await sec.logAudit('CHECKOUT_VALIDATION_FAIL',{errors:errors.length});
toast('Fix the errors below','error');
btn.disabled=false;btn.innerHTML=`<i class="fa-solid fa-lock" style="margin-right:.5rem"></i>Place Order — $${getCartTotal().toFixed(2)}`;
return;
}
/* STOCK DECREMENT — actually reduce product stock */
for(const c of state.cart){
const p=PRODUCTS.find(x=>x.id===c.product.id);
if(p){p.stock=Math.max(0,p.stock-c.qty)}
}
const orderId='FK-'+Date.now().toString(36).toUpperCase()+'-'+Math.random().toString(36).slice(2,6).toUpperCase();
const order={id:orderId,items:orderItems,total:getCartTotal(),customer:{first:sanitized.first,last:sanitized.last,email:sanitized.email,company:sanitized.company},address:{street:sanitized.addr,city:sanitized.city,state:sanitized.state,zip:sanitized.zip},timestamp:new Date().toISOString(),csrfValid:sec.validateCsrf(sec.csrfToken)};
state.orderHistory.push(order);
await sec.logAudit('ORDER_PLACED',{order_id:orderId,total:order.total.toFixed(2),items:order.items.length});
sec.rotateCsrf();
state.cart=[];renderCart();renderProducts();
showConfirmation(order);toast('Order placed successfully!','success');
}
function showConfirmation(order){
document.getElementById('confirm-text').textContent=`Order ${order.id} placed and logged to SHA-256 audit chain.`;
document.getElementById('confirm-details').innerHTML=`
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem">
<h3 style="font-size:.85rem;font-weight:700;margin-bottom:1rem">Order Summary</h3>
${order.items.map(i=>`<div style="display:flex;justify-content:space-between;padding:.4rem 0;font-size:.82rem;border-bottom:1px solid var(--border)"><span><span class="mono" style="color:var(--fg-muted);font-size:.7rem;margin-right:.4rem">${i.sku}</span>${i.name} x${i.qty}</span><span style="font-weight:600">$${(i.price*i.qty).toFixed(2)}</span></div>`).join('')}
<div style="display:flex;justify-content:space-between;padding:.75rem 0;font-size:1rem;font-weight:700"><span>Total</span><span style="color:var(--accent)">$${order.total.toFixed(2)}</span></div>
<div style="margin-top:.75rem;padding-top:.75rem;border-top:1px solid var(--border);font-size:.75rem;color:var(--fg-muted)">
<p><strong>Customer:</strong> ${order.customer.first} ${order.customer.last} (${order.customer.email})</p>
${order.customer.company?`<p style="margin-top:.25rem"><strong>Company:</strong> ${order.customer.company}</p>`:''}
<p style="margin-top:.25rem"><strong>Ship to:</strong> ${order.address.street}, ${order.address.city}, ${order.address.state} ${order.address.zip}</p>
<p style="margin-top:.25rem"><strong>CSRF Valid:</strong> <span style="color:${order.csrfValid?'var(--success)':'var(--danger)'}">${order.csrfValid?'Yes':'No'}</span></p>
<p style="margin-top:.25rem"><strong>WASM Demo Hash:</strong> <span class="mono" style="color:var(--info)">${wasmHash(order.id)}</span> <span style="font-size:.65rem;color:var(--fg-muted)">(demonstrational)</span></p>
<p style="margin-top:.25rem"><strong>SHA-256 Chain:</strong> <span style="color:var(--success)">Verified</span></p>
</div>
</div>`;
navigate('confirmation');
}
/* ================================================================
AUDIT LOG
================================================================ */
function renderAuditLog(){
const logs=sec.logs;
const tbody=document.getElementById('audit-tbody');
const empty=document.getElementById('audit-empty');
const persistNote=document.getElementById('audit-persist-note');
if(!logs.length){tbody.innerHTML='';empty.style.display='block';persistNote.style.display='none';return}
empty.style.display='none';
const ac={'PRODUCT_VIEW':'var(--info)','ADD_TO_CART':'var(--success)','REMOVE_FROM_CART':'var(--warn)','QTY_CHANGE':'var(--info)','CHECKOUT_START':'var(--accent)','CHECKOUT_VALIDATION_FAIL':'var(--danger)','ORDER_PLACED':'var(--success)','CSRF_GENERATE':'var(--fg-muted)','CSRF_VALIDATE_OK':'var(--success)','CSRF_FAIL':'var(--danger)','RATE_LIMITED':'var(--warn)','CATEGORY_FILTER':'var(--fg-muted)','CHAIN_VERIFY':'var(--info)','FREIGHT_BLOCKED':'var(--warn)'};
const sorted=[...logs].reverse();
tbody.innerHTML=sorted.map(l=>{
const time=new Date(l.ts).toLocaleTimeString();
const details=typeof l.details==='string'?l.details:JSON.stringify(l.details);
return `<tr><td class="mono" style="white-space:nowrap;color:var(--fg-muted)">${time}</td><td><span style="color:${ac[l.action]||'var(--fg-muted)'};font-weight:600">${l.action}</span></td><td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${details.replace(/"/g,'&quot;')}">${details}</td><td class="mono" style="color:var(--accent);font-size:.7rem">${l.hash}</td><td class="mono" style="color:var(--fg-muted);font-size:.7rem">${l.prevHash.slice(0,8)}...</td></tr>`;
}).join('');
}
async function verifyChain(){
const r=await sec.verifyChain();
const el=document.getElementById('chain-status');
if(r.valid){el.style.color='var(--success)';el.innerHTML=`<i class="fa-solid fa-link" style="margin-right:.35rem"></i>Chain valid (${r.count} entries)`;toast('SHA-256 chain integrity verified','success')}
else{el.style.color='var(--danger)';el.innerHTML=`<i class="fa-solid fa-link-slash" style="margin-right:.35rem"></i>Chain BROKEN at entry ${r.brokenAt}`;toast('Audit chain COMPROMISED','error')}
await sec.logAudit('CHAIN_VERIFY',{valid:r.valid,count:r.count||0});
}
function clearAuditLog(){sec.clearLogs();renderAuditLog();document.getElementById('chain-status').innerHTML='';toast('Audit log cleared','info')}
/* ================================================================
SECURITY DASHBOARD
================================================================ */
function renderSecurityDashboard(){
const features=[
{name:'Web Crypto SHA-256',desc:'Native OS-level SHA-256 via crypto.subtle.digest(). Falls back to auditable pure-JS implementation on non-secure contexts. Powers the audit hash chain.',status:!!(window.crypto&&crypto.subtle),icon:'fa-lock',note:sec.logs.length>0?`${sec.logs.length} entries chained`:'No entries yet'},
{name:'WASM DJB2 Module',desc:'100-byte hand-compiled WebAssembly hash function. Demonstrational — NOT cryptographic. Shows sandboxed WASM execution model.',status:wasmReady,icon:'fa-microchip',note:'Labeled as demonstrational',demo:true},
{name:'Input Sanitization',desc:'9-pattern regex XSS filter + HTML entity encoding. Runs synchronously with zero external dependencies. All user fields pass through before processing.',status:true,icon:'fa-filter'},
{name:'CSRF Protection',desc:'128-bit tokens via crypto.getRandomValues(). Validated on every mutating action. 1-hour expiry. Rotated after order placement.',status:true,icon:'fa-key'},
{name:'Audit Log Persistence',desc:'Hash chain saved to localStorage on every write. Survives page refresh. Chain integrity verified on load via SHA-256 recomputation.',status:true,icon:'fa-database'},
{name:'Rate Limiting',desc:'Sliding window: 15 requests per 60 seconds per action type. Prevents brute force and abuse patterns.',status:true,icon:'fa-gauge-high'},
{name:'Order Integrity',desc:'Server-side total recomputation prevents price manipulation. Quantity bounds (1-99) enforced. Negative price rejection.',status:true,icon:'fa-receipt'},
{name:'Credit Card Validation',desc:'Luhn algorithm. Card number never leaves this session. Demo only — production requires PCI-DSS tokenization.',status:true,icon:'fa-credit-card'},
{name:'Stock Enforcement',desc:'Cannot add out-of-stock items. Cart quantity capped at available stock. Stock decrements on order placement.',status:true,icon:'fa-boxes-stacked'},
{name:'Freight Gating',desc:'Heavy items flagged with freight:true are blocked from direct add-to-cart. Forces manual sales contact for proper shipping quotes.',status:true,icon:'fa-truck'},
{name:'PHP Backend (Architecture)',desc:'Production PHP middleware: CSRF, rate limiting, sanitization, security headers, parameterized SQL, row-level locking.',status:true,icon:'fa-server',note:'Code in Architecture tab'},
{name:'WASM Sandbox Isolation',desc:'WebAssembly runs in memory-isolated sandbox with no DOM access. Linear memory model prevents pointer exploits.',status:wasmReady,icon:'fa-box',demo:true},
];
document.getElementById('security-grid').innerHTML=features.map(f=>`
<div style="background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
<div style="display:flex;align-items:center;gap:.6rem">
<div style="width:36px;height:36px;border-radius:.5rem;background:${f.demo?'rgba(56,189,248,.12)':'var(--accent-dim)'};display:flex;align-items:center;justify-content:center">
<i class="fa-solid ${f.icon}" style="color:${f.demo?'var(--info)':'var(--accent)'};font-size:.9rem"></i></div>
<h3 style="font-size:.85rem;font-weight:700">${f.name}</h3>
</div>
<span class="sec-dot ${f.demo?'demo':(f.status?'on':'off')}" title="${f.demo?'Demonstrational':(f.status?'Active':'Inactive')}"></span>
</div>
<p style="font-size:.78rem;color:var(--fg-muted);line-height:1.5">${f.desc}</p>
${f.note?`<p style="font-size:.7rem;color:${f.demo?'var(--info)':'var(--accent)'};margin-top:.5rem;font-style:italic">${f.note}</p>`:''}
</div>`).join('');
}
/* ================================================================
ARCHITECTURE TABS
================================================================ */
function switchArchTab(tab,btn){
document.querySelectorAll('.arch-panel').forEach(p=>p.style.display='none');
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
document.getElementById('arch-'+tab).style.display='block';btn.classList.add('active');
}
function renderArchitecturePanels(){
document.getElementById('arch-php').innerHTML=`
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-brands fa-php" style="color:var(--accent);margin-right:.5rem"></i>SecurityMiddleware.php</h3><p style="font-size:.75rem;color:var(--fg-muted)">Runs on every request in production</p></div>
<div class="code-block mono"><span class="cm">&lt;?php</span>
<span class="kw">namespace</span> App\\Middleware;
<span class="kw">class</span> <span class="fn">SecurityMiddleware</span> {
<span class="kw">public function</span> <span class="fn">handle</span>(<span class="fn">Request</span> <span class="var">$req</span>, <span class="fn">Closure</span> <span class="var">$next</span>): <span class="fn">Response</span> {
<span class="cm">// CSRF validation on POST</span>
<span class="kw">if</span> (<span class="var">$req</span>-><span class="fn">isMethod</span>(<span class="str">'POST'</span>)) {
<span class="kw">if</span> (!<span class="fn">hash_equals</span>(
<span class="var">$req</span>-><span class="fn">session</span>()-><span class="fn">get</span>(<span class="str">'csrf_token'</span>),
<span class="var">$req</span>-><span class="fn">header</span>(<span class="str">'X-CSRF-Token'</span>)
)) <span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(
[<span class="str">'error'</span>=><span class="str">'CSRF failed'</span>], <span class="num">403</span>
);
}
<span class="cm">// Rate limiting</span>
<span class="var">$key</span> = <span class="str">'rate:'</span>.<span class="var">$req</span>-><span class="fn">ip</span>().<span class="str">':'</span>.<span class="var">$req</span>-><span class="fn">path</span>();
<span class="kw">if</span> (<span class="fn">RateLimiter</span>::<span class="fn">tooManyAttempts</span>(<span class="var">$key</span>, <span class="num">60</span>))
<span class="kw">return</span> <span class="fn">response</span>()-><span class="fn">json</span>(
[<span class="str">'error'</span>=><span class="str">'Rate limited'</span>], <span class="num">429</span>
);
<span class="cm">// Recursive sanitization</span>
<span class="var">$req</span>-><span class="fn">replace</span>(<span class="var">$this</span>-><span class="fn">sanitize</span>(<span class="var">$req</span>-><span class="fn">all</span>()));
<span class="cm">// Security headers</span>
<span class="var">$res</span> = <span class="var">$next</span>(<span class="var">$req</span>);
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'X-Content-Type-Options'</span>,<span class="str">'nosniff'</span>);
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'X-Frame-Options'</span>,<span class="str">'DENY'</span>);
<span class="var">$res</span>-><span class="fn">headers</span>-><span class="fn">set</span>(<span class="str">'Content-Security-Policy'</span>,
<span class="str">"default-src 'self'; script-src 'self'"</span>);
<span class="kw">return</span> <span class="var">$res</span>;
}
}</div>`;
document.getElementById('arch-js').innerHTML=`
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-brands fa-js" style="color:var(--accent);margin-right:.5rem"></i>SecurityEngine — the actual running code</h3><p style="font-size:.75rem;color:var(--fg-muted)">This class runs in your browser right now. No Python. No libraries.</p></div>
<div class="code-block mono"><span class="kw">class</span> <span class="fn">SecurityEngine</span> {
<span class="fn">constructor</span>(sessionId) {
<span class="var">this</span>.logs = [];
<span class="var">this</span>.prevHash = <span class="str">'GENESIS'</span>;
<span class="var">this</span>.sanitizerPatterns = [
/&lt;script[\\s\\S]*?&lt;\\/script&gt;/gi,
/javascript\\s*:/gi,
/on\\w+\\s*=/gi,
/&lt;iframe/gi, /&lt;object/gi,
/&lt;embed/gi, /&lt;form/gi,
/expression\\s*\\(/gi,
/@import/gi,
];
}
<span class="cm">// SHA-256 hash-chained audit log</span>
<span class="kw">async</span> <span class="fn">logAudit</span>(action, details) {
<span class="kw">const</span> entry = {
ts: <span class="fn">Date</span>.<span class="fn">now</span>(), action, details,
session: <span class="var">this</span>.sessionId.<span class="fn">slice</span>(<span class="num">0</span>,<span class="num">12</span>),
prevHash: <span class="var">this</span>.prevHash
};
<span class="kw">const</span> hash = <span class="kw">await</span> <span class="fn">sha256</span>(
<span class="fn">JSON</span>.<span class="fn">stringify</span>(entry,
<span class="fn">Object</span>.<span class="fn">keys</span>(entry).<span class="fn">sort</span>())
);
entry.hash = hash.<span class="fn">slice</span>(<span class="num">0</span>, <span class="num">16</span>);
<span class="var">this</span>.prevHash = entry.hash;
<span class="var">this</span>.logs.<span class="fn">push</span>(entry);
<span class="var">this</span>._persist(); <span class="cm">// localStorage</span>
<span class="kw">return</span> entry;
}
<span class="cm">// crypto.subtle.digest — OS-level crypto</span>
<span class="kw">async</span> <span class="fn">sha256</span>(message) {
<span class="kw">const</span> buf = <span class="kw">await</span> crypto.subtle.<span class="fn">digest</span>(
<span class="str">'SHA-256'</span>,
<span class="kw">new</span> <span class="fn">TextEncoder</span>().<span class="fn">encode</span>(message)
);
<span class="kw">return</span> [...<span class="kw">new</span> <span class="fn">Uint8Array</span>(buf)]
.<span class="fn">map</span>(b => b.<span class="fn">toString</span>(<span class="num">16</span>).<span class="fn">padStart</span>(<span class="num">8</span>,<span class="str">'0'</span>))
.<span class="fn">join</span>(<span class="str">''</span>);
}
<span class="cm">// CSRF via crypto.getRandomValues</span>
<span class="fn">generateCsrf</span>() {
<span class="kw">const</span> buf = <span class="kw">new</span> <span class="fn">Uint8Array</span>(<span class="num">16</span>);
crypto.<span class="fn">getRandomValues</span>(buf);
<span class="var">this</span>.csrfToken = [...buf]
.<span class="fn">map</span>(b => b.<span class="fn">toString</span>(<span class="num">16</span>).<span class="fn">padStart</span>(<span class="num">2</span>,<span class="str">'0'</span>))
.<span class="fn">join</span>(<span class="str">''</span>);
}
<span class="fn">sanitize</span>(text) {
<span class="kw">let</span> r = text;
<span class="kw">for</span> (<span class="kw">const</span> p <span class="kw">of</span> <span class="var">this</span>.sanitizerPatterns)
r = r.<span class="fn">replace</span>(p, <span class="str">'[SANITIZED]'</span>);
<span class="kw">return</span> r.<span class="fn">replace</span>(<span class="str">/&/g</span>,<span class="str">'&amp;amp;'</span>)
.<span class="fn">replace</span>(<span class="str">/&lt;/g</span>,<span class="str">'&amp;lt;'</span>)
.<span class="fn">replace</span>(<span class="str">/&gt;/g</span>,<span class="str">'&amp;gt;'</span>);
}
}</div>`;
document.getElementById('arch-wasm').innerHTML=`
<div style="margin-bottom:1rem"><h3 style="font-size:.9rem;font-weight:700"><i class="fa-solid fa-microchip" style="color:var(--info);margin-right:.5rem"></i>DJB2 WASM Module — Demonstrational</h3>
<p style="font-size:.75rem;color:var(--fg-muted)">NOT used for security. Included to show the WASM sandbox model. Real crypto uses Web Crypto API above.</p></div>
<div style="background:rgba(56,189,248,.08);border:1px solid rgba(56,189,248,.2);border-radius:.5rem;padding:.6rem .75rem;margin-bottom:1rem;font-size:.75rem;color:var(--info)">
<i class="fa-solid fa-info-circle" style="margin-right:.35rem"></i>DJB2 is a fast non-cryptographic hash with known collisions. It demonstrates WASM execution but does not contribute to security guarantees. The audit chain uses SHA-256 via <code>crypto.subtle.digest()</code>.
</div>
<div class="code-block mono">(<span class="kw">module</span>
(<span class="kw">memory</span> (<span class="kw">export</span> <span class="str">"mem"</span>) <span class="num">1</span>)
(<span class="kw">func</span> (<span class="kw">export</span> <span class="str">"hash"</span>) (<span class="kw">param</span> <span class="var">$len</span> <span class="type">i32</span>) (<span class="kw">result</span> <span class="type">i32</span>)
<span class="cm">;; h = 5381, loop: h = (h&lt;&lt;5)+h+mem[i]</span>
... <span class="cm">;; 100 bytes total</span>
)
)</div>`;
document.getElementById('arch-flow').innerHTML=`
<div style="display:flex;flex-direction:column;gap:0;align-items:center;padding:1rem 0">
${[
{icon:'fa-user',label:'User Action',desc:'Browse, search, add to cart, checkout',color:'var(--info)'},
{icon:'fa-shield-halved',label:'JS SecurityEngine',desc:'Input capture, sanitization (9-pattern XSS + HTML encode)',color:'var(--fg-muted)'},
{icon:'fa-key',label:'CSRF Validation',desc:'crypto.getRandomValues token — match + 1hr expiry check',color:'var(--warn)'},
{icon:'fa-gauge-high',label:'Rate Limiting',desc:'Sliding window: 15 req/60s per action type',color:'var(--warn)'},
{icon:'fa-receipt',label:'Order Validation',desc:'Total recomputation, qty bounds, negative price rejection',color:'var(--accent)'},
{icon:'fa-lock',label:'SHA-256 Audit Entry',desc:'crypto.subtle.digest() — OS-level hash chain with prev_hash',color:'var(--success)'},
{icon:'fa-database',label:'localStorage Persist',desc:'Hash chain saved on every write, survives refresh',color:'var(--success)'},
{icon:'fa-server',label:'PHP Backend (Production)',desc:'Middleware → Controller → Parameterized SQL → Headers',color:'var(--info)'},
].map((s,i,a)=>`
<div style="text-align:center;max-width:380px">
<div style="width:48px;height:48px;border-radius:50%;background:${s.color}22;border:2px solid ${s.color};display:inline-flex;align-items:center;justify-content:center;margin-bottom:.5rem">
<i class="fa-solid ${s.icon}" style="color:${s.color};font-size:1rem"></i></div>
<h3 style="font-size:.85rem;font-weight:700">${s.label}</h3>
<p style="font-size:.75rem;color:var(--fg-muted);line-height:1.5">${s.desc}</p>
</div>
${i<a.length-1?'<div style="width:2px;height:32px;background:var(--border-light);margin:.25rem 0"></div>':''}
`).join('')}
</div>`;
}
/* ================================================================
INIT
================================================================ */
async function init(){
const bar=document.getElementById('load-bar');
const status=document.getElementById('load-status');
/* Step 1: Security engine (instant — no download) */
status.textContent='Initializing security engine...';bar.style.width='20%';
sec=new SecurityEngine(state.sessionId);
sec.generateCsrf();
/* Step 2: WASM demo module */
status.textContent='Loading WASM demonstrational module...';bar.style.width='40%';
await initWasm();
bar.style.width='60%';
/* Step 3: Check crypto availability */
status.textContent='Verifying Web Crypto API...';bar.style.width='75%';
const cryptoOk=!!(window.crypto&&crypto.subtle);
document.getElementById('sec-dot-crypto').className=cryptoOk?'sec-dot on':'sec-dot off';
if(!cryptoOk)console.warn('crypto.subtle unavailable — using JS SHA-256 fallback');
/* Step 4: Restore audit log */
status.textContent='Restoring audit log...';bar.style.width='90%';
const restoredCount=sec._restoreFromStorage();
if(restoredCount>0){
document.getElementById('persist-count').textContent=restoredCount;
document.getElementById('audit-persist-note').style.display='block';
}
/* Step 5: Initial audit entry + render */
status.textContent='Ready';bar.style.width='100%';
await sec.logAudit('SESSION_START',{wasm:wasmReady,crypto:cryptoOk,restored:restoredCount});
renderCategoryFilters();renderProducts();renderCart();renderArchitecturePanels();
await new Promise(r=>setTimeout(r,300));
document.getElementById('loader').classList.add('hidden');
document.getElementById('main-nav').style.opacity='1';
}
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeProductModal();if(document.getElementById('cart-sidebar').classList.contains('open'))toggleCart()}});
init().catch(e=>{console.error('Init failed:',e);document.getElementById('load-status').textContent='Error: '+e.message;document.getElementById('load-status').style.color='var(--danger)';document.getElementById('loader').classList.add('hidden');document.getElementById('main-nav').style.opacity='1';sec=new SecurityEngine(state.sessionId);sec.generateCsrf();renderCategoryFilters();renderProducts();renderCart();renderArchitecturePanels()});
</script>
</body>
</html>