-
-
+
+
+
-
diff --git a/src/scripts/binary.js b/src/scripts/binary.js
index 41c1c50..a557ff9 100644
--- a/src/scripts/binary.js
+++ b/src/scripts/binary.js
@@ -34,11 +34,8 @@
----------------------------- */
let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
let bits = new Array(bitCount).fill(false);
- let randomTimer = null;
- // For responsive wrapping of the top binary display
- let nibblesPerLine = null;
- let wrapMeasureSpan = null;
+ let randomTimer = null;
/* -----------------------------
HELPERS
@@ -83,7 +80,6 @@
function unsignedBigIntToBits(vUnsigned) {
const span = unsignedMaxExclusive(bitCount);
const v = ((vUnsigned % span) + span) % span;
-
for (let i = 0; i < bitCount; i++) {
bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
}
@@ -102,6 +98,16 @@
unsignedBigIntToBits(v);
}
+ function formatBinaryGrouped() {
+ let s = "";
+ for (let i = bitCount - 1; i >= 0; i--) {
+ s += bits[i] ? "1" : "0";
+ const posFromRight = (bitCount - i);
+ if (i !== 0 && posFromRight % 4 === 0) s += " ";
+ }
+ return s;
+ }
+
function updateModeHint() {
if (!modeHint) return;
modeHint.textContent = isTwosMode()
@@ -109,72 +115,6 @@
: "Tip: In unsigned binary, all bits represent positive values.";
}
- /* -----------------------------
- TOP BINARY DISPLAY: responsive wrap by nibble count
- ----------------------------- */
- function ensureWrapMeasurer() {
- if (wrapMeasureSpan || !binaryEl) return;
- wrapMeasureSpan = document.createElement("span");
- wrapMeasureSpan.style.position = "absolute";
- wrapMeasureSpan.style.visibility = "hidden";
- wrapMeasureSpan.style.whiteSpace = "pre";
- wrapMeasureSpan.style.pointerEvents = "none";
- // Inherit font/letterspacing from binaryEl
- wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font;
- wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing;
- document.body.appendChild(wrapMeasureSpan);
- }
-
- function computeNibblesPerLine() {
- if (!binaryEl) return null;
- ensureWrapMeasurer();
-
- // Available width = width of the readout area (binaryEl parent)
- const host = binaryEl.parentElement;
- if (!host) return null;
-
- const hostW = host.getBoundingClientRect().width;
- if (!Number.isFinite(hostW) || hostW <= 0) return null;
-
- // Measure one nibble including trailing space ("0000 ")
- wrapMeasureSpan.textContent = "0000 ";
- const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1;
-
- // Safety: keep at least 1 nibble per line
- const max = Math.max(1, Math.floor(hostW / nibbleW));
- return max;
- }
-
- function formatBinaryWrapped() {
- // EXACT bitCount digits (no padding to 4)
- let raw = "";
- for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0";
-
- // If <= 4 bits, do NOT insert spaces/newlines at all
- if (bitCount <= 4) return raw;
-
- const groups = [];
- for (let i = 0; i < raw.length; i += 4) {
- groups.push(raw.slice(i, i + 4));
- }
-
- const perLine = nibblesPerLine ?? groups.length;
- if (perLine >= groups.length) return groups.join(" ");
-
- const lines = [];
- for (let i = 0; i < groups.length; i += perLine) {
- lines.push(groups.slice(i, i + perLine).join(" "));
- }
- return lines.join("\n");
- }
-
- function refreshBinaryWrap() {
- const next = computeNibblesPerLine();
- // Only update if it actually changes (prevents jitter)
- if (next !== nibblesPerLine) nibblesPerLine = next;
- updateReadout(); // re-render with new wrap
- }
-
/* -----------------------------
BUILD UI (BITS)
----------------------------- */
@@ -188,17 +128,9 @@
bitsGrid.innerHTML = "";
- bitsGrid.classList.toggle("bitsFew", bitCount < 8);
- if (bitCount < 8) {
- bitsGrid.style.setProperty("--cols", String(bitCount));
- } else {
- bitsGrid.style.removeProperty("--cols");
- }
-
for (let i = bitCount - 1; i >= 0; i--) {
const bitEl = document.createElement("div");
bitEl.className = "bit";
-
bitEl.innerHTML = `
💡
@@ -207,7 +139,6 @@
`;
-
bitsGrid.appendChild(bitEl);
}
@@ -219,28 +150,6 @@
});
});
- // bulb styling + 25% bigger (vs 26px previously)
- for (let i = 0; i < bitCount; i++) {
- const bulb = document.getElementById(`bulb-${i}`);
- if (!bulb) continue;
- bulb.style.width = "auto";
- bulb.style.height = "auto";
- bulb.style.border = "none";
- bulb.style.background = "transparent";
- bulb.style.borderRadius = "0";
- bulb.style.boxShadow = "none";
- bulb.style.opacity = "0.45";
- bulb.style.fontSize = "32px";
- bulb.style.lineHeight = "1";
- bulb.style.display = "flex";
- bulb.style.alignItems = "center";
- bulb.style.justifyContent = "center";
- bulb.style.filter = "grayscale(1)";
- bulb.textContent = "💡";
- }
-
- // wrapping may change when bit width changes
- refreshBinaryWrap();
updateUI();
}
@@ -252,8 +161,10 @@
const label = document.getElementById(`bitLabel-${i}`);
if (!label) continue;
+ // Keep label on ONE LINE (no wrapping)
+ label.style.whiteSpace = "nowrap";
+
if (isTwosMode() && i === bitCount - 1) {
- // Keep on one line (CSS: white-space:nowrap)
label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
} else {
label.textContent = pow2Big(i).toString();
@@ -272,25 +183,14 @@
for (let i = 0; i < bitCount; i++) {
const bulb = document.getElementById(`bulb-${i}`);
if (!bulb) continue;
-
- const on = bits[i] === true;
- if (on) {
- bulb.style.opacity = "1";
- bulb.style.filter = "grayscale(0)";
- bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)";
- } else {
- bulb.style.opacity = "0.45";
- bulb.style.filter = "grayscale(1)";
- bulb.style.textShadow = "none";
- }
+ bulb.classList.toggle("on", bits[i] === true);
}
}
function updateReadout() {
if (!denaryEl || !binaryEl) return;
-
denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
- binaryEl.textContent = formatBinaryWrapped();
+ binaryEl.textContent = formatBinaryGrouped();
}
function updateUI() {
@@ -302,18 +202,16 @@
}
/* -----------------------------
- SET FROM INPUT
+ INPUT SETTERS
----------------------------- */
function setFromBinaryString(binStr) {
const clean = String(binStr ?? "").replace(/\s+/g, "");
if (!/^[01]+$/.test(clean)) return false;
-
const padded = clean.slice(-bitCount).padStart(bitCount, "0");
for (let i = 0; i < bitCount; i++) {
const charFromRight = padded[padded.length - 1 - i];
bits[i] = charFromRight === "1";
}
-
updateUI();
return true;
}
@@ -355,15 +253,13 @@
}
function shiftRight() {
- if (isTwosMode()) {
- // arithmetic right shift: keep MSB
- const msb = bits[bitCount - 1];
- for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
- bits[bitCount - 1] = msb;
- } else {
- for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
- bits[bitCount - 1] = false;
- }
+ // Unsigned: logical right shift (MSB becomes 0)
+ // Two's complement: arithmetic right shift (MSB preserved)
+ const msb = bits[bitCount - 1];
+
+ for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
+
+ bits[bitCount - 1] = isTwosMode() ? msb : false;
updateUI();
}
@@ -384,7 +280,8 @@
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
- unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
+ const v = (bitsToUnsignedBigInt() + 1n) % span;
+ unsignedBigIntToBits(v);
}
updateUI();
}
@@ -398,16 +295,18 @@
signedBigIntToBitsTwos(v);
} else {
const span = unsignedMaxExclusive(bitCount);
- unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
+ const v = (bitsToUnsignedBigInt() - 1n + span) % span;
+ unsignedBigIntToBits(v);
}
updateUI();
}
/* -----------------------------
- RANDOM
+ RANDOM (with running pulse + longer run)
----------------------------- */
function cryptoRandomBigInt(maxExclusive) {
if (maxExclusive <= 0n) return 0n;
+
const bitLen = maxExclusive.toString(2).length;
const byteLen = Math.ceil(bitLen / 8);
@@ -420,12 +319,13 @@
const extraBits = BigInt(byteLen * 8 - bitLen);
if (extraBits > 0n) x = x >> extraBits;
+
if (x < maxExclusive) return x;
}
}
function setRandomOnce() {
- const span = unsignedMaxExclusive(bitCount);
+ const span = unsignedMaxExclusive(bitCount); // 2^n
const u = cryptoRandomBigInt(span);
unsignedBigIntToBits(u);
updateUI();
@@ -437,8 +337,11 @@
randomTimer = null;
}
+ // pulse while running
+ btnRandom?.classList.add("is-running");
+
const start = Date.now();
- const durationMs = 1125; // (your “~25% longer” vs 900ms)
+ const durationMs = 1125; // 25% longer than 900ms
const tickMs = 80;
randomTimer = setInterval(() => {
@@ -446,30 +349,31 @@
if (Date.now() - start >= durationMs) {
clearInterval(randomTimer);
randomTimer = null;
+ btnRandom?.classList.remove("is-running");
}
}, tickMs);
}
/* -----------------------------
- BIT WIDTH CONTROLS
+ BIT WIDTH
----------------------------- */
function setBitWidth(n) {
buildBits(clampInt(n, 1, 64));
}
/* -----------------------------
- TOOLBOX TOGGLE (simple open/close state)
+ TOOLBOX VISIBILITY
----------------------------- */
- function setToolboxOpen(open) {
- document.body.classList.toggle("toolboxClosed", !open);
- toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
- refreshBinaryWrap(); // width changes when toolbox closes/opens
+ function setToolboxVisible(isVisible) {
+ if (!toolboxPanel) return;
+ toolboxPanel.style.display = isVisible ? "flex" : "none";
+ toolboxToggle?.setAttribute("aria-expanded", String(isVisible));
}
/* -----------------------------
EVENTS
----------------------------- */
- modeToggle?.addEventListener("change", () => updateUI());
+ modeToggle?.addEventListener("change", updateUI);
btnCustomBinary?.addEventListener("click", () => {
const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
@@ -480,8 +384,8 @@
btnCustomDenary?.addEventListener("click", () => {
const v = prompt(
isTwosMode()
- ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
- : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
+ ? `Enter denary (${twosMin(bitCount)} to ${twosMax(bitCount)}):`
+ : `Enter denary (0 to ${unsignedMaxValue(bitCount)}):`
);
if (v === null) return;
if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
@@ -502,15 +406,8 @@
bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
toolboxToggle?.addEventListener("click", () => {
- const isOpen = !document.body.classList.contains("toolboxClosed");
- setToolboxOpen(!isOpen);
- });
-
- // Recompute wrapping live when the window size changes
- let resizeT = null;
- window.addEventListener("resize", () => {
- if (resizeT) clearTimeout(resizeT);
- resizeT = setTimeout(() => refreshBinaryWrap(), 60);
+ const isOpen = toolboxToggle.getAttribute("aria-expanded") !== "false";
+ setToolboxVisible(!isOpen);
});
/* -----------------------------
@@ -518,5 +415,5 @@
----------------------------- */
updateModeHint();
buildBits(bitCount);
- setToolboxOpen(true);
+ setToolboxVisible(true);
})();
diff --git a/src/src/assets/astro.svg b/src/src/assets/astro.svg
new file mode 100644
index 0000000..8cf8fb0
--- /dev/null
+++ b/src/src/assets/astro.svg
@@ -0,0 +1 @@
+
diff --git a/src/src/assets/background.svg b/src/src/assets/background.svg
new file mode 100644
index 0000000..4b2be0a
--- /dev/null
+++ b/src/src/assets/background.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/Footer.astro b/src/src/components/Footer.astro
similarity index 100%
rename from src/components/Footer.astro
rename to src/src/components/Footer.astro
diff --git a/src/components/Header.astro b/src/src/components/Header.astro
similarity index 100%
rename from src/components/Header.astro
rename to src/src/components/Header.astro
diff --git a/src/src/components/Welcome.astro b/src/src/components/Welcome.astro
new file mode 100644
index 0000000..52e0333
--- /dev/null
+++ b/src/src/components/Welcome.astro
@@ -0,0 +1,210 @@
+---
+import astroLogo from '../assets/astro.svg';
+import background from '../assets/background.svg';
+---
+
+
+
+
diff --git a/src/src/components/simulators/HexSimulator.astro b/src/src/components/simulators/HexSimulator.astro
new file mode 100644
index 0000000..4f212bb
--- /dev/null
+++ b/src/src/components/simulators/HexSimulator.astro
@@ -0,0 +1,104 @@
+---
+import "./hex/hex-simulator.css";
+---
+
+
+
+
+
DENARY
+
0
+
+
HEXADECIMAL
+
00
+
+
BINARY
+
0000 0000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/components/simulators/hex/hex-simulator.css b/src/src/components/simulators/hex/hex-simulator.css
new file mode 100644
index 0000000..8a15d61
--- /dev/null
+++ b/src/src/components/simulators/hex/hex-simulator.css
@@ -0,0 +1,346 @@
+/* ================= Fonts to match Binary ================= */
+/* Adjust paths to wherever you store fonts (commonly /public/fonts/...) */
+@font-face {
+ font-family: "DSEG7Classic";
+ src: url("/fonts/DSEG7Classic-Regular.woff") format("woff"),
+ url("/fonts/DSEG7Classic-Regular.ttf") format("truetype");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: "SevenSegment";
+ src: url("/fonts/Seven-Segment.woff2") format("woff2"),
+ url("/fonts/Seven-Segment.woff") format("woff");
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+.hex-sim {
+ min-height: 100vh;
+ background: #14151c;
+ color: #e7e8ee;
+ padding: 28px;
+}
+
+.hex-font-number { font-family: "DSEG7Classic", ui-monospace, monospace; }
+.hex-font-mono { font-family: "SevenSegment", ui-monospace, monospace; }
+
+.hex-main { max-width: 1200px; margin: 0 auto; width: 100%; padding-top: 40px; }
+
+.hex-readout { text-align: center; }
+.hex-label {
+ font-family: "SevenSegment", ui-sans-serif, system-ui;
+ font-size: 12px;
+ letter-spacing: 2px;
+ opacity: 0.7;
+}
+.hex-mt { margin-top: 12px; }
+
+.hex-number {
+ font-family: "DSEG7Classic", ui-monospace, monospace;
+ font-size: 76px;
+ line-height: 1;
+ font-weight: 400;
+ color: #46ff8a;
+ text-shadow: 0 0 18px rgba(70,255,138,0.18);
+}
+.hex-number--small { font-size: 64px; }
+.hex-number--tiny { font-size: 54px; letter-spacing: 6px; }
+
+.hex-divider {
+ margin: 26px auto 18px;
+ height: 1px;
+ width: min(760px, 90%);
+ background: rgba(255,255,255,0.10);
+}
+
+/* ================= Main digit columns ================= */
+.hex-digits {
+ margin-top: 18px;
+ display: flex;
+ justify-content: center;
+ gap: 18px;
+ flex-wrap: wrap;
+}
+
+.hex-digit-col {
+ width: 160px;
+ border-radius: 18px;
+ background: rgba(255,255,255,0.03);
+ border: 1px solid rgba(255,255,255,0.10);
+ padding: 12px;
+ display: grid;
+ gap: 10px;
+ justify-items: center;
+}
+
+.hex-digit-controls {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+}
+
+.hex-digit-char {
+ font-size: 64px;
+ line-height: 1;
+ color: #46ff8a;
+ text-shadow: 0 0 18px rgba(70,255,138,0.18);
+}
+
+.hex-digit-place {
+ font-family: "SevenSegment", ui-monospace, monospace;
+ opacity: 0.65;
+ font-size: 14px;
+ letter-spacing: 1px;
+}
+
+/* ================= Bulbs (brightness changes) ================= */
+.hex-bulbs {
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 10px;
+ align-items: end;
+}
+
+.hex-bulb {
+ display: grid;
+ justify-items: center;
+ gap: 6px;
+ opacity: 0.35;
+ filter: grayscale(30%);
+ transition: opacity 160ms ease, filter 160ms ease;
+}
+
+.hex-bulb .hex-bulb-cap {
+ width: 18px;
+ height: 18px;
+ border-radius: 999px;
+ background: rgba(255,255,255,0.22);
+ border: 1px solid rgba(255,255,255,0.14);
+}
+
+.hex-bulb .hex-bulb-glow {
+ width: 18px;
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(70,255,138,0.0);
+ box-shadow: 0 0 0 rgba(70,255,138,0.0);
+ transition: background 160ms ease, box-shadow 160ms ease;
+}
+
+.hex-bulb .hex-bulb-label {
+ font-family: "SevenSegment", ui-monospace, monospace;
+ font-size: 12px;
+ opacity: 0.8;
+}
+
+.hex-bulb.is-on {
+ opacity: 1;
+ filter: none;
+}
+.hex-bulb.is-on .hex-bulb-cap {
+ background: rgba(255,255,255,0.35);
+}
+.hex-bulb.is-on .hex-bulb-glow {
+ background: rgba(70,255,138,0.25);
+ box-shadow: 0 0 18px rgba(70,255,138,0.35);
+}
+
+/* ================= Buttons (toolbox style reused everywhere) ================= */
+.hex-btn {
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(255,255,255,0.14);
+ background: rgba(255,255,255,0.06);
+ color: #e7e8ee;
+ font-weight: 800;
+ cursor: pointer;
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
+}
+.hex-btn:hover { background: rgba(255,255,255,0.10); }
+
+.hex-btn--square {
+ width: 48px;
+ height: 48px;
+ padding: 0;
+ display: grid;
+ place-items: center;
+ font-size: 18px;
+}
+
+.hex-btn--wide { width: 100%; }
+
+.hex-btn--green {
+ background: rgba(46, 200, 120, 0.18);
+ border-color: rgba(46,200,120,0.35);
+}
+.hex-btn--green:hover { background: rgba(46, 200, 120, 0.26); }
+
+.hex-btn--green2 {
+ background: rgba(46, 200, 120, 0.18);
+ border-color: rgba(46,200,120,0.35);
+}
+
+.hex-btn--red {
+ background: rgba(220, 60, 70, 0.18);
+ border-color: rgba(220,60,70,0.35);
+}
+
+/* Random = green pulse while running */
+.hex-btn--random.is-running {
+ border-color: rgba(80, 255, 160, 0.55);
+ background: rgba(46, 200, 120, 0.22);
+ box-shadow: 0 0 18px rgba(80, 255, 160, 0.35);
+ animation: hexPulseGreen 900ms ease-in-out infinite;
+}
+@keyframes hexPulseGreen {
+ 0%, 100% { box-shadow: 0 0 14px rgba(80, 255, 160, 0.25); }
+ 50% { box-shadow: 0 0 26px rgba(80, 255, 160, 0.45); }
+}
+
+/* Reset = red background + pulse on hover */
+.hex-btn--reset:hover {
+ background: rgba(220, 60, 70, 0.28);
+ border-color: rgba(255, 80, 90, 0.55);
+ animation: hexPulseRed 900ms ease-in-out infinite;
+}
+@keyframes hexPulseRed {
+ 0%, 100% { box-shadow: 0 0 12px rgba(255, 80, 90, 0.20); }
+ 50% { box-shadow: 0 0 22px rgba(255, 80, 90, 0.38); }
+}
+
+/* ================= Toolbox button + panel (slide) ================= */
+.hex-toolbox-btn {
+ position: fixed;
+ top: 88px;
+ right: 28px;
+ z-index: 30;
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ border-radius: 14px;
+ border: 1px solid rgba(255,255,255,0.14);
+ background: rgba(255,255,255,0.06);
+ color: #e7e8ee;
+ font-weight: 800;
+ letter-spacing: 1px;
+ cursor: pointer;
+}
+
+.hex-toolbox-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ color: #ff4fa6;
+ filter: drop-shadow(0 0 10px rgba(255,79,166,0.35));
+}
+
+.hex-toolbox {
+ position: fixed;
+ top: 140px;
+ right: 28px;
+ width: 340px;
+ display: grid;
+ gap: 14px;
+ z-index: 25;
+
+ transform: translateX(0);
+ opacity: 1;
+ transition: transform 220ms ease, opacity 220ms ease;
+}
+.hex-toolbox:not(.is-open) {
+ transform: translateX(380px);
+ opacity: 0;
+ pointer-events: none;
+}
+
+.hex-panel {
+ border-radius: 16px;
+ background: rgba(255,255,255,0.04);
+ border: 1px solid rgba(255,255,255,0.10);
+ padding: 14px;
+}
+.hex-panel-title {
+ font-family: "SevenSegment", ui-sans-serif, system-ui;
+ font-size: 12px;
+ letter-spacing: 2px;
+ opacity: 0.7;
+ margin-bottom: 10px;
+}
+
+.hex-setting-title { font-weight: 900; opacity: 0.9; margin-bottom: 10px; }
+
+.hex-width {
+ display: grid;
+ grid-template-columns: 48px 1fr 48px;
+ gap: 10px;
+ align-items: center;
+}
+.hex-width-readout {
+ border-radius: 14px;
+ background: rgba(0,0,0,0.22);
+ border: 1px solid rgba(255,255,255,0.10);
+ padding: 10px 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+}
+.hex-width-label {
+ font-family: "SevenSegment", ui-sans-serif, system-ui;
+ opacity: 0.7;
+ font-weight: 800;
+ letter-spacing: 1px;
+ font-size: 12px;
+}
+.hex-width-number { font-size: 30px; font-weight: 900; color: #46ff8a; }
+
+.hex-hint { margin-top: 8px; opacity: 0.65; font-size: 12px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
+
+.hex-grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.hex-mt-sm { margin-top: 10px; }
+
+.hex-tools-top { display: flex; gap: 10px; justify-content: center; margin-bottom: 10px; }
+.hex-tiny-note { margin-top: 8px; font-size: 11px; opacity: 0.6; letter-spacing: 1px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
+
+/* ================= Dialog ================= */
+.hex-dialog { border: none; padding: 0; background: transparent; }
+.hex-dialog::backdrop { background: rgba(0,0,0,0.55); }
+
+.hex-dialog-card {
+ width: min(560px, 92vw);
+ border-radius: 18px;
+ background: #1a1b24;
+ border: 1px solid rgba(255,255,255,0.12);
+ padding: 16px;
+ color: #e7e8ee;
+}
+
+.hex-dialog-title { font-weight: 900; letter-spacing: 1px; margin-bottom: 10px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
+
+.hex-dialog-input {
+ width: 100%;
+ padding: 12px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(255,255,255,0.14);
+ background: rgba(0,0,0,0.25);
+ color: #e7e8ee;
+ font-size: 18px;
+}
+
+.hex-dialog-hint { margin-top: 10px; opacity: 0.7; font-size: 13px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
+.hex-dialog-error { margin-top: 8px; font-size: 13px; color: #ff6b6b; min-height: 18px; font-family: "SevenSegment", ui-sans-serif, system-ui; }
+.hex-dialog-actions { margin-top: 14px; display: flex; gap: 10px; justify-content: flex-end; }
+
+@media (max-width: 900px) {
+ .hex-toolbox { width: min(360px, 92vw); right: 16px; }
+ .hex-toolbox-btn { right: 16px; }
+ .hex-number { font-size: 60px; }
+ .hex-number--tiny { font-size: 40px; letter-spacing: 4px; }
+}
diff --git a/src/src/components/simulators/hex/hex-simulator.ts b/src/src/components/simulators/hex/hex-simulator.ts
new file mode 100644
index 0000000..08bc8aa
--- /dev/null
+++ b/src/src/components/simulators/hex/hex-simulator.ts
@@ -0,0 +1,232 @@
+type DialogMode = "hex" | "den" | "bin";
+
+const root = document.querySelector
("[data-hex-sim]");
+if (!root) throw new Error("Hex simulator root not found");
+
+const outDen = root.querySelector('[data-out="denary"]')!;
+const outHex = root.querySelector('[data-out="hex"]')!;
+const outBin = root.querySelector('[data-out="bin"]')!;
+const outDigitsRow = root.querySelector('[data-out="digitsRow"]')!;
+
+const toolbox = root.querySelector('[data-out="toolbox"]')!;
+const toolboxBtn = root.querySelector('[data-action="toggleToolbox"]')!;
+const digitsCount = root.querySelector('[data-out="digitsCount"]')!;
+const bitsHint = root.querySelector('[data-out="bitsHint"]')!;
+const randomBtn = root.querySelector("[data-random]")!;
+
+const dialog = root.querySelector('[data-out="dialog"]')!;
+const dialogTitle = root.querySelector('[data-out="dialogTitle"]')!;
+const dialogInput = root.querySelector('[data-out="dialogInput"]')!;
+const dialogHint = root.querySelector('[data-out="dialogHint"]')!;
+const dialogError = root.querySelector('[data-out="dialogError"]')!;
+
+let digits = 2; // 1..8
+let value = 0; // unsigned denary
+let randomTimer: number | null = null;
+let dialogMode: DialogMode | null = null;
+
+const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n));
+const maxForDigits = (d: number) => (16 ** d) - 1;
+
+const padHex = (n: number, d: number) => n.toString(16).toUpperCase().padStart(d, "0");
+const padBin = (n: number, b: number) => n.toString(2).padStart(b, "0");
+const groupBin = (b: string) => b.replace(/(.{4})/g, "$1 ").trim();
+
+function stopRandom(): void {
+ if (randomTimer !== null) window.clearInterval(randomTimer);
+ randomTimer = null;
+ randomBtn.classList.remove("is-running");
+}
+
+function startRandom(): void {
+ stopRandom();
+ const max = maxForDigits(digits);
+ const start = Date.now();
+
+ randomBtn.classList.add("is-running");
+
+ randomTimer = window.setInterval(() => {
+ value = Math.floor(Math.random() * (max + 1));
+ render();
+ if (Date.now() - start > 1600) stopRandom();
+ }, 90);
+}
+
+function render(): void {
+ const bits = digits * 4;
+
+ digitsCount.textContent = String(digits);
+ bitsHint.textContent = `= ${bits} bits`;
+
+ outDen.textContent = String(value);
+ outHex.textContent = padHex(value, digits);
+ outBin.textContent = groupBin(padBin(value, bits));
+
+ renderDigitsRow();
+}
+
+function renderDigitsRow(): void {
+ const hex = padHex(value, digits);
+ outDigitsRow.innerHTML = "";
+
+ for (let i = 0; i < digits; i++) {
+ const pow = digits - 1 - i;
+ const placeValue = 16 ** pow;
+
+ const digitChar = hex[i];
+ const digitVal = parseInt(digitChar, 16);
+ const nibbleBits = [(digitVal >> 3) & 1, (digitVal >> 2) & 1, (digitVal >> 1) & 1, digitVal & 1]; // 8 4 2 1
+
+ const col = document.createElement("div");
+ col.className = "hex-digit-col";
+ col.innerHTML = `
+
+
+
+
+
+ ${digitChar}
+
+
+
+ ${[8,4,2,1].map((w, idx) => {
+ const on = nibbleBits[idx] === 1;
+ return `
+
+ `;
+ }).join("")}
+
+
+ ${placeValue}
+ `;
+ outDigitsRow.appendChild(col);
+ }
+}
+
+function openDialog(mode: DialogMode): void {
+ stopRandom();
+ dialogMode = mode;
+
+ dialogError.textContent = "";
+ dialogInput.value = "";
+
+ if (mode === "hex") {
+ dialogTitle.textContent = "Custom Hexadecimal";
+ dialogHint.textContent = `Enter 1–${digits} hex digit(s) (0–9, A–F).`;
+ dialogInput.placeholder = "A1";
+ dialogInput.inputMode = "text";
+ } else if (mode === "den") {
+ dialogTitle.textContent = "Custom Denary";
+ dialogHint.textContent = `Enter a whole number from 0 to ${maxForDigits(digits)}.`;
+ dialogInput.placeholder = "42";
+ dialogInput.inputMode = "numeric";
+ } else {
+ dialogTitle.textContent = "Custom Binary";
+ dialogHint.textContent = `Enter up to ${digits * 4} bit(s) using 0 and 1.`;
+ dialogInput.placeholder = "00101010";
+ dialogInput.inputMode = "text";
+ }
+
+ dialog.showModal();
+ window.setTimeout(() => dialogInput.focus(), 0);
+}
+
+function closeDialog(): void {
+ dialogMode = null;
+ dialogError.textContent = "";
+ if (dialog.open) dialog.close();
+}
+
+function applyDialog(): void {
+ const raw = (dialogInput.value || "").trim();
+ if (!dialogMode) return closeDialog();
+ if (raw.length === 0) return closeDialog();
+
+ const max = maxForDigits(digits);
+ const bits = digits * 4;
+
+ if (dialogMode === "hex") {
+ const v = raw.toUpperCase();
+ if (!/^[0-9A-F]+$/.test(v)) { dialogError.textContent = "Hex must use 0–9 and A–F only."; return; }
+ if (v.length > digits) { dialogError.textContent = `Max length is ${digits} hex digit(s).`; return; }
+ value = clamp(parseInt(v, 16), 0, max);
+ render();
+ return closeDialog();
+ }
+
+ if (dialogMode === "den") {
+ if (!/^\d+$/.test(raw)) { dialogError.textContent = "Denary must be whole numbers only."; return; }
+ const n = Number(raw);
+ if (!Number.isFinite(n)) { dialogError.textContent = "Invalid number."; return; }
+ value = clamp(n, 0, max);
+ render();
+ return closeDialog();
+ }
+
+ // bin
+ if (!/^[01]+$/.test(raw)) { dialogError.textContent = "Binary must use 0 and 1 only."; return; }
+ if (raw.length > bits) { dialogError.textContent = `Max length is ${bits} bit(s).`; return; }
+ value = clamp(parseInt(raw, 2), 0, max);
+ render();
+ return closeDialog();
+}
+
+function applyDigitDelta(i: number, delta: number): void {
+ stopRandom();
+ const hexArr = padHex(value, digits).split("");
+ let v = parseInt(hexArr[i], 16);
+ v = (v + delta) % 16;
+ if (v < 0) v += 16;
+ hexArr[i] = v.toString(16).toUpperCase();
+ value = clamp(parseInt(hexArr.join(""), 16), 0, maxForDigits(digits));
+ render();
+}
+
+// dialog cancel / backdrop
+dialog.addEventListener("cancel", (e) => { e.preventDefault(); closeDialog(); });
+dialog.addEventListener("click", (e) => {
+ const card = dialog.querySelector(".hex-dialog-card");
+ if (card && !card.contains(e.target as Node)) closeDialog();
+});
+dialogInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") applyDialog();
+ if (e.key === "Escape") closeDialog();
+});
+
+// main click handler
+root.addEventListener("click", (e) => {
+ const btn = (e.target as HTMLElement).closest("[data-action]");
+ if (!btn) return;
+ const action = btn.getAttribute("data-action")!;
+
+ if (action === "toggleToolbox") {
+ toolbox.classList.toggle("is-open");
+ toolboxBtn.setAttribute("aria-expanded", toolbox.classList.contains("is-open") ? "true" : "false");
+ return;
+ }
+
+ if (action === "digitsMinus") { digits = clamp(digits - 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
+ if (action === "digitsPlus") { digits = clamp(digits + 1, 1, 8); value = clamp(value, 0, maxForDigits(digits)); return render(); }
+
+ if (action === "increment") { stopRandom(); value = clamp(value + 1, 0, maxForDigits(digits)); return render(); }
+ if (action === "decrement") { stopRandom(); value = clamp(value - 1, 0, maxForDigits(digits)); return render(); }
+
+ if (action === "reset") { stopRandom(); value = 0; return render(); }
+ if (action === "random") { return startRandom(); }
+
+ if (action === "customHex") return openDialog("hex");
+ if (action === "customDenary") return openDialog("den");
+ if (action === "customBinary") return openDialog("bin");
+
+ if (action === "dialogCancel") return closeDialog();
+ if (action === "dialogApply") return applyDialog();
+
+ if (action === "digitUp") return applyDigitDelta(Number(btn.getAttribute("data-i")), +1);
+ if (action === "digitDown") return applyDigitDelta(Number(btn.getAttribute("data-i")), -1);
+});
+
+render();
diff --git a/src/src/layouts/BaseLayout.astro b/src/src/layouts/BaseLayout.astro
new file mode 100644
index 0000000..022fd45
--- /dev/null
+++ b/src/src/layouts/BaseLayout.astro
@@ -0,0 +1,118 @@
+---
+const { title = "Computing:Box" } = Astro.props;
+---
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/layouts/Layout.astro b/src/src/layouts/Layout.astro
new file mode 100644
index 0000000..e455c61
--- /dev/null
+++ b/src/src/layouts/Layout.astro
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+ Astro Basics
+
+
+
+
+
+
+
diff --git a/src/src/pages/binary.astro b/src/src/pages/binary.astro
new file mode 100644
index 0000000..fc195e5
--- /dev/null
+++ b/src/src/pages/binary.astro
@@ -0,0 +1,115 @@
+---
+import BaseLayout from "../layouts/BaseLayout.astro";
+import "../styles/binary.css";
+---
+
+
+
+
+
+
+
+
+
+
+
Denary
+
0
+
+
Binary
+
+
00000000
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/src/pages/hexadecimal.astro b/src/src/pages/hexadecimal.astro
new file mode 100644
index 0000000..edfe098
--- /dev/null
+++ b/src/src/pages/hexadecimal.astro
@@ -0,0 +1,8 @@
+---
+import BaseLayout from "../layouts/BaseLayout.astro";
+import HexSimulator from "../components/simulators/HexSimulator.astro";
+---
+
+
+
+
diff --git a/src/src/pages/index.astro b/src/src/pages/index.astro
new file mode 100644
index 0000000..c04f360
--- /dev/null
+++ b/src/src/pages/index.astro
@@ -0,0 +1,11 @@
+---
+import Welcome from '../components/Welcome.astro';
+import Layout from '../layouts/Layout.astro';
+
+// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
+// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
+---
+
+
+
+
diff --git a/src/src/scripts/binary.js b/src/src/scripts/binary.js
new file mode 100644
index 0000000..41c1c50
--- /dev/null
+++ b/src/src/scripts/binary.js
@@ -0,0 +1,522 @@
+// src/scripts/binary.js
+// Computing:Box — Binary page logic (Unsigned + Two's Complement)
+
+(() => {
+ /* -----------------------------
+ DOM
+ ----------------------------- */
+ const bitsGrid = document.getElementById("bitsGrid");
+ const denaryEl = document.getElementById("denaryNumber");
+ const binaryEl = document.getElementById("binaryNumber");
+ const bitsInput = document.getElementById("bitsInput");
+
+ const modeToggle = document.getElementById("modeToggle");
+ const modeHint = document.getElementById("modeHint");
+
+ const btnCustomBinary = document.getElementById("btnCustomBinary");
+ const btnCustomDenary = document.getElementById("btnCustomDenary");
+ const btnShiftLeft = document.getElementById("btnShiftLeft");
+ const btnShiftRight = document.getElementById("btnShiftRight");
+
+ const btnDec = document.getElementById("btnDec");
+ const btnInc = document.getElementById("btnInc");
+ const btnClear = document.getElementById("btnClear");
+ const btnRandom = document.getElementById("btnRandom");
+
+ const btnBitsUp = document.getElementById("btnBitsUp");
+ const btnBitsDown = document.getElementById("btnBitsDown");
+
+ const toolboxToggle = document.getElementById("toolboxToggle");
+ const toolboxPanel = document.getElementById("toolboxPanel");
+
+ /* -----------------------------
+ STATE
+ ----------------------------- */
+ let bitCount = clampInt(Number(bitsInput?.value ?? 8), 1, 64);
+ let bits = new Array(bitCount).fill(false);
+ let randomTimer = null;
+
+ // For responsive wrapping of the top binary display
+ let nibblesPerLine = null;
+ let wrapMeasureSpan = null;
+
+ /* -----------------------------
+ HELPERS
+ ----------------------------- */
+ function clampInt(n, min, max) {
+ if (!Number.isFinite(n)) return min;
+ return Math.max(min, Math.min(max, Math.trunc(n)));
+ }
+
+ function isTwosMode() {
+ return !!modeToggle?.checked;
+ }
+
+ function pow2Big(n) {
+ return 1n << BigInt(n);
+ }
+
+ function unsignedMaxExclusive(nBits) {
+ return pow2Big(nBits);
+ }
+
+ function unsignedMaxValue(nBits) {
+ return pow2Big(nBits) - 1n;
+ }
+
+ function twosMin(nBits) {
+ return -pow2Big(nBits - 1);
+ }
+
+ function twosMax(nBits) {
+ return pow2Big(nBits - 1) - 1n;
+ }
+
+ function bitsToUnsignedBigInt() {
+ let v = 0n;
+ for (let i = 0; i < bitCount; i++) {
+ if (bits[i]) v += pow2Big(i);
+ }
+ return v;
+ }
+
+ function unsignedBigIntToBits(vUnsigned) {
+ const span = unsignedMaxExclusive(bitCount);
+ const v = ((vUnsigned % span) + span) % span;
+
+ for (let i = 0; i < bitCount; i++) {
+ bits[i] = ((v >> BigInt(i)) & 1n) === 1n;
+ }
+ }
+
+ function bitsToSignedBigIntTwos() {
+ const u = bitsToUnsignedBigInt();
+ const signBit = bits[bitCount - 1] === true;
+ if (!signBit) return u;
+ return u - pow2Big(bitCount);
+ }
+
+ function signedBigIntToBitsTwos(vSigned) {
+ const span = pow2Big(bitCount);
+ let v = ((vSigned % span) + span) % span;
+ unsignedBigIntToBits(v);
+ }
+
+ function updateModeHint() {
+ if (!modeHint) return;
+ modeHint.textContent = isTwosMode()
+ ? "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."
+ : "Tip: In unsigned binary, all bits represent positive values.";
+ }
+
+ /* -----------------------------
+ TOP BINARY DISPLAY: responsive wrap by nibble count
+ ----------------------------- */
+ function ensureWrapMeasurer() {
+ if (wrapMeasureSpan || !binaryEl) return;
+ wrapMeasureSpan = document.createElement("span");
+ wrapMeasureSpan.style.position = "absolute";
+ wrapMeasureSpan.style.visibility = "hidden";
+ wrapMeasureSpan.style.whiteSpace = "pre";
+ wrapMeasureSpan.style.pointerEvents = "none";
+ // Inherit font/letterspacing from binaryEl
+ wrapMeasureSpan.style.font = getComputedStyle(binaryEl).font;
+ wrapMeasureSpan.style.letterSpacing = getComputedStyle(binaryEl).letterSpacing;
+ document.body.appendChild(wrapMeasureSpan);
+ }
+
+ function computeNibblesPerLine() {
+ if (!binaryEl) return null;
+ ensureWrapMeasurer();
+
+ // Available width = width of the readout area (binaryEl parent)
+ const host = binaryEl.parentElement;
+ if (!host) return null;
+
+ const hostW = host.getBoundingClientRect().width;
+ if (!Number.isFinite(hostW) || hostW <= 0) return null;
+
+ // Measure one nibble including trailing space ("0000 ")
+ wrapMeasureSpan.textContent = "0000 ";
+ const nibbleW = wrapMeasureSpan.getBoundingClientRect().width || 1;
+
+ // Safety: keep at least 1 nibble per line
+ const max = Math.max(1, Math.floor(hostW / nibbleW));
+ return max;
+ }
+
+ function formatBinaryWrapped() {
+ // EXACT bitCount digits (no padding to 4)
+ let raw = "";
+ for (let i = bitCount - 1; i >= 0; i--) raw += bits[i] ? "1" : "0";
+
+ // If <= 4 bits, do NOT insert spaces/newlines at all
+ if (bitCount <= 4) return raw;
+
+ const groups = [];
+ for (let i = 0; i < raw.length; i += 4) {
+ groups.push(raw.slice(i, i + 4));
+ }
+
+ const perLine = nibblesPerLine ?? groups.length;
+ if (perLine >= groups.length) return groups.join(" ");
+
+ const lines = [];
+ for (let i = 0; i < groups.length; i += perLine) {
+ lines.push(groups.slice(i, i + perLine).join(" "));
+ }
+ return lines.join("\n");
+ }
+
+ function refreshBinaryWrap() {
+ const next = computeNibblesPerLine();
+ // Only update if it actually changes (prevents jitter)
+ if (next !== nibblesPerLine) nibblesPerLine = next;
+ updateReadout(); // re-render with new wrap
+ }
+
+ /* -----------------------------
+ BUILD UI (BITS)
+ ----------------------------- */
+ function buildBits(count) {
+ bitCount = clampInt(count, 1, 64);
+ if (bitsInput) bitsInput.value = String(bitCount);
+
+ const oldBits = bits.slice();
+ bits = new Array(bitCount).fill(false);
+ for (let i = 0; i < Math.min(oldBits.length, bitCount); i++) bits[i] = oldBits[i];
+
+ bitsGrid.innerHTML = "";
+
+ bitsGrid.classList.toggle("bitsFew", bitCount < 8);
+ if (bitCount < 8) {
+ bitsGrid.style.setProperty("--cols", String(bitCount));
+ } else {
+ bitsGrid.style.removeProperty("--cols");
+ }
+
+ for (let i = bitCount - 1; i >= 0; i--) {
+ const bitEl = document.createElement("div");
+ bitEl.className = "bit";
+
+ bitEl.innerHTML = `
+ 💡
+
+
+ `;
+
+ bitsGrid.appendChild(bitEl);
+ }
+
+ bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
+ input.addEventListener("change", () => {
+ const i = Number(input.dataset.index);
+ bits[i] = input.checked;
+ updateUI();
+ });
+ });
+
+ // bulb styling + 25% bigger (vs 26px previously)
+ for (let i = 0; i < bitCount; i++) {
+ const bulb = document.getElementById(`bulb-${i}`);
+ if (!bulb) continue;
+ bulb.style.width = "auto";
+ bulb.style.height = "auto";
+ bulb.style.border = "none";
+ bulb.style.background = "transparent";
+ bulb.style.borderRadius = "0";
+ bulb.style.boxShadow = "none";
+ bulb.style.opacity = "0.45";
+ bulb.style.fontSize = "32px";
+ bulb.style.lineHeight = "1";
+ bulb.style.display = "flex";
+ bulb.style.alignItems = "center";
+ bulb.style.justifyContent = "center";
+ bulb.style.filter = "grayscale(1)";
+ bulb.textContent = "💡";
+ }
+
+ // wrapping may change when bit width changes
+ refreshBinaryWrap();
+ updateUI();
+ }
+
+ /* -----------------------------
+ UI UPDATE
+ ----------------------------- */
+ function updateBitLabels() {
+ for (let i = 0; i < bitCount; i++) {
+ const label = document.getElementById(`bitLabel-${i}`);
+ if (!label) continue;
+
+ if (isTwosMode() && i === bitCount - 1) {
+ // Keep on one line (CSS: white-space:nowrap)
+ label.textContent = `-${pow2Big(bitCount - 1).toString()}`;
+ } else {
+ label.textContent = pow2Big(i).toString();
+ }
+ }
+ }
+
+ function syncSwitchesToBits() {
+ bitsGrid.querySelectorAll('input[type="checkbox"]').forEach((input) => {
+ const i = Number(input.dataset.index);
+ input.checked = !!bits[i];
+ });
+ }
+
+ function updateBulbs() {
+ for (let i = 0; i < bitCount; i++) {
+ const bulb = document.getElementById(`bulb-${i}`);
+ if (!bulb) continue;
+
+ const on = bits[i] === true;
+ if (on) {
+ bulb.style.opacity = "1";
+ bulb.style.filter = "grayscale(0)";
+ bulb.style.textShadow = "0 0 18px rgba(255,216,107,.75), 0 0 30px rgba(255,216,107,.45)";
+ } else {
+ bulb.style.opacity = "0.45";
+ bulb.style.filter = "grayscale(1)";
+ bulb.style.textShadow = "none";
+ }
+ }
+ }
+
+ function updateReadout() {
+ if (!denaryEl || !binaryEl) return;
+
+ denaryEl.textContent = (isTwosMode() ? bitsToSignedBigIntTwos() : bitsToUnsignedBigInt()).toString();
+ binaryEl.textContent = formatBinaryWrapped();
+ }
+
+ function updateUI() {
+ updateModeHint();
+ updateBitLabels();
+ syncSwitchesToBits();
+ updateBulbs();
+ updateReadout();
+ }
+
+ /* -----------------------------
+ SET FROM INPUT
+ ----------------------------- */
+ function setFromBinaryString(binStr) {
+ const clean = String(binStr ?? "").replace(/\s+/g, "");
+ if (!/^[01]+$/.test(clean)) return false;
+
+ const padded = clean.slice(-bitCount).padStart(bitCount, "0");
+ for (let i = 0; i < bitCount; i++) {
+ const charFromRight = padded[padded.length - 1 - i];
+ bits[i] = charFromRight === "1";
+ }
+
+ updateUI();
+ return true;
+ }
+
+ function setFromDenaryInput(vStr) {
+ const raw = String(vStr ?? "").trim();
+ if (!raw) return false;
+
+ let v;
+ try {
+ if (!/^-?\d+$/.test(raw)) return false;
+ v = BigInt(raw);
+ } catch {
+ return false;
+ }
+
+ if (isTwosMode()) {
+ const min = twosMin(bitCount);
+ const max = twosMax(bitCount);
+ if (v < min || v > max) return false;
+ signedBigIntToBitsTwos(v);
+ } else {
+ if (v < 0n) return false;
+ if (v > unsignedMaxValue(bitCount)) return false;
+ unsignedBigIntToBits(v);
+ }
+
+ updateUI();
+ return true;
+ }
+
+ /* -----------------------------
+ SHIFTS
+ ----------------------------- */
+ function shiftLeft() {
+ for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1];
+ bits[0] = false;
+ updateUI();
+ }
+
+ function shiftRight() {
+ if (isTwosMode()) {
+ // arithmetic right shift: keep MSB
+ const msb = bits[bitCount - 1];
+ for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
+ bits[bitCount - 1] = msb;
+ } else {
+ for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1];
+ bits[bitCount - 1] = false;
+ }
+ updateUI();
+ }
+
+ /* -----------------------------
+ CLEAR / INC / DEC
+ ----------------------------- */
+ function clearAll() {
+ bits.fill(false);
+ updateUI();
+ }
+
+ function increment() {
+ if (isTwosMode()) {
+ const min = twosMin(bitCount);
+ const max = twosMax(bitCount);
+ let v = bitsToSignedBigIntTwos() + 1n;
+ if (v > max) v = min;
+ signedBigIntToBitsTwos(v);
+ } else {
+ const span = unsignedMaxExclusive(bitCount);
+ unsignedBigIntToBits((bitsToUnsignedBigInt() + 1n) % span);
+ }
+ updateUI();
+ }
+
+ function decrement() {
+ if (isTwosMode()) {
+ const min = twosMin(bitCount);
+ const max = twosMax(bitCount);
+ let v = bitsToSignedBigIntTwos() - 1n;
+ if (v < min) v = max;
+ signedBigIntToBitsTwos(v);
+ } else {
+ const span = unsignedMaxExclusive(bitCount);
+ unsignedBigIntToBits((bitsToUnsignedBigInt() - 1n + span) % span);
+ }
+ updateUI();
+ }
+
+ /* -----------------------------
+ RANDOM
+ ----------------------------- */
+ function cryptoRandomBigInt(maxExclusive) {
+ if (maxExclusive <= 0n) return 0n;
+ const bitLen = maxExclusive.toString(2).length;
+ const byteLen = Math.ceil(bitLen / 8);
+
+ while (true) {
+ const bytes = new Uint8Array(byteLen);
+ crypto.getRandomValues(bytes);
+
+ let x = 0n;
+ for (const b of bytes) x = (x << 8n) | BigInt(b);
+
+ const extraBits = BigInt(byteLen * 8 - bitLen);
+ if (extraBits > 0n) x = x >> extraBits;
+ if (x < maxExclusive) return x;
+ }
+ }
+
+ function setRandomOnce() {
+ const span = unsignedMaxExclusive(bitCount);
+ const u = cryptoRandomBigInt(span);
+ unsignedBigIntToBits(u);
+ updateUI();
+ }
+
+ function runRandomBriefly() {
+ if (randomTimer) {
+ clearInterval(randomTimer);
+ randomTimer = null;
+ }
+
+ const start = Date.now();
+ const durationMs = 1125; // (your “~25% longer” vs 900ms)
+ const tickMs = 80;
+
+ randomTimer = setInterval(() => {
+ setRandomOnce();
+ if (Date.now() - start >= durationMs) {
+ clearInterval(randomTimer);
+ randomTimer = null;
+ }
+ }, tickMs);
+ }
+
+ /* -----------------------------
+ BIT WIDTH CONTROLS
+ ----------------------------- */
+ function setBitWidth(n) {
+ buildBits(clampInt(n, 1, 64));
+ }
+
+ /* -----------------------------
+ TOOLBOX TOGGLE (simple open/close state)
+ ----------------------------- */
+ function setToolboxOpen(open) {
+ document.body.classList.toggle("toolboxClosed", !open);
+ toolboxToggle?.setAttribute("aria-expanded", open ? "true" : "false");
+ refreshBinaryWrap(); // width changes when toolbox closes/opens
+ }
+
+ /* -----------------------------
+ EVENTS
+ ----------------------------- */
+ modeToggle?.addEventListener("change", () => updateUI());
+
+ btnCustomBinary?.addEventListener("click", () => {
+ const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`);
+ if (v === null) return;
+ if (!setFromBinaryString(v)) alert("Invalid binary");
+ });
+
+ btnCustomDenary?.addEventListener("click", () => {
+ const v = prompt(
+ isTwosMode()
+ ? `Enter denary (${twosMin(bitCount).toString()} to ${twosMax(bitCount).toString()}):`
+ : `Enter denary (0 to ${unsignedMaxValue(bitCount).toString()}):`
+ );
+ if (v === null) return;
+ if (!setFromDenaryInput(v)) alert("Invalid denary for current mode/bit width");
+ });
+
+ btnShiftLeft?.addEventListener("click", shiftLeft);
+ btnShiftRight?.addEventListener("click", shiftRight);
+
+ btnInc?.addEventListener("click", increment);
+ btnDec?.addEventListener("click", decrement);
+
+ btnClear?.addEventListener("click", clearAll);
+ btnRandom?.addEventListener("click", runRandomBriefly);
+
+ btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1));
+ btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1));
+
+ bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value)));
+
+ toolboxToggle?.addEventListener("click", () => {
+ const isOpen = !document.body.classList.contains("toolboxClosed");
+ setToolboxOpen(!isOpen);
+ });
+
+ // Recompute wrapping live when the window size changes
+ let resizeT = null;
+ window.addEventListener("resize", () => {
+ if (resizeT) clearTimeout(resizeT);
+ resizeT = setTimeout(() => refreshBinaryWrap(), 60);
+ });
+
+ /* -----------------------------
+ INIT
+ ----------------------------- */
+ updateModeHint();
+ buildBits(bitCount);
+ setToolboxOpen(true);
+})();
diff --git a/src/src/styles/binary.css b/src/src/styles/binary.css
new file mode 100644
index 0000000..4398c01
--- /dev/null
+++ b/src/src/styles/binary.css
@@ -0,0 +1,342 @@
+/*
+ Binary page styles (keeps the last-working simulator markup + binary.js).
+
+ Goals:
+ - Do NOT change any IDs/classes expected by src/scripts/binary.js
+ - Toolbox button toggles the ENTIRE right-hand column via body.toolboxClosed
+ - Fix toolbox button positioning (no overlap, consistent with header container)
+ - Fix spacing/consistency of cards + buttons
+ - Keep binary readout wrapping/bit-width behaviour from JS (\n in output)
+*/
+
+:root{
+ --panel-w: 360px;
+ --gap: 22px;
+}
+
+/* Page wrapper (inside BaseLayout .pageWrap) */
+.wrap{
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 22px 20px 48px;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+/* Toolbox toggle button (sits below navbar, aligned right, never overlaps) */
+.toolboxToggle{
+ align-self: flex-end;
+ position: sticky;
+ top: calc(var(--nav-h, 108px) + 14px);
+ z-index: 30;
+
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ height: 40px;
+ padding: 0 14px;
+ border-radius: 12px;
+ border: 1px solid rgba(255,255,255,.14);
+ background: rgba(255,255,255,.06);
+ color: rgba(255,255,255,.92);
+ cursor: pointer;
+}
+.toolboxToggle:hover{ background: rgba(255,255,255,.08); }
+.toolboxText{
+ letter-spacing: .12em;
+ font-weight: 900;
+}
+
+/* Main layout grid */
+.topGrid{
+ display: grid;
+ grid-template-columns: 1fr var(--panel-w);
+ gap: var(--gap);
+ align-items: start;
+}
+
+/* Hide ENTIRE toolbox column when toggled closed */
+body.toolboxClosed .topGrid{ grid-template-columns: 1fr; }
+body.toolboxClosed #toolboxPanel{ display: none; }
+
+.mainCol{ min-width: 0; }
+
+/* Readout */
+.readout{
+ text-align: center;
+ margin-top: 8px;
+}
+
+.label{
+ opacity: .8;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ font-size: 12px;
+}
+
+/* IMPORTANT: allow shrinking below 4 bits (no min-width!) */
+.num{
+ display: inline-block;
+ width: fit-content;
+ max-width: 100%;
+ white-space: pre-line; /* allows JS \n wraps */
+ letter-spacing: 2px;
+}
+
+.denaryValue{
+ font-size: 54px;
+ margin: 6px 0 10px;
+}
+
+.binaryValue{
+ font-size: 56px;
+ margin: 4px 0 18px;
+}
+
+.divider{
+ height: 1px;
+ background: rgba(255,255,255,.10);
+ margin: 14px auto 24px;
+ max-width: 900px;
+}
+
+/* Bits area */
+.bitsWrap{ padding-top: 6px; }
+
+.bitsGrid{
+ display: grid;
+ gap: 24px;
+ justify-content: center;
+ grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.bitsGrid.bitsFew{ justify-content: center; }
+
+.bit{
+ display: grid;
+ justify-items: center;
+ gap: 8px;
+}
+
+.bulb{
+ font-size: 32px; /* JS also bumps this */
+ line-height: 1;
+ opacity: .45;
+}
+
+.bitVal{
+ font-size: 22px;
+ line-height: 1.05;
+ text-align: center;
+ white-space: nowrap; /* keep -128 on one line */
+}
+
+/* Switch (existing classes assumed) */
+.switch{
+ position: relative;
+ display: inline-block;
+ width: 52px;
+ height: 28px;
+}
+.switch input{ display:none; }
+.slider{
+ position:absolute;
+ inset:0;
+ border-radius:999px;
+ background: rgba(255,255,255,.18);
+ border: 1px solid rgba(255,255,255,.14);
+}
+.slider:before{
+ content:"";
+ position:absolute;
+ height: 22px;
+ width: 22px;
+ left: 3px;
+ top: 2.5px;
+ border-radius: 999px;
+ background: #fff;
+ transition: transform .18s ease;
+}
+.switch input:checked + .slider:before{ transform: translateX(22px); }
+
+/* Toolbox column */
+.panelCol{
+ position: sticky;
+ top: calc(var(--nav-h, 108px) + 72px); /* leaves space for sticky toolbox button */
+ align-self: start;
+ display: grid;
+ gap: 16px;
+}
+
+/* Cards */
+.card{
+ border: 1px solid rgba(255,255,255,.12);
+ border-radius: 16px;
+ background: rgba(255,255,255,.05);
+ padding: 14px;
+}
+
+.cardTitle{
+ opacity: .8;
+ letter-spacing: .14em;
+ text-transform: uppercase;
+ font-size: 12px;
+ margin-bottom: 10px;
+}
+
+.hint{
+ opacity: .7;
+ font-size: 11px;
+ margin-top: 10px;
+ line-height: 1.35;
+}
+
+/* Keep mode labels on one line */
+.toggleRow{
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ gap: 10px;
+ align-items: center;
+}
+
+.toggleLabel{
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.subCard{
+ margin-top: 12px;
+ border: 1px solid rgba(255,255,255,.10);
+ border-radius: 14px;
+ background: rgba(0,0,0,.12);
+ padding: 12px;
+}
+
+.subTitle{
+ opacity: .8;
+ letter-spacing: .14em;
+ text-transform: uppercase;
+ font-size: 11px;
+ margin-bottom: 10px;
+}
+
+.bitWidthRow{
+ display: grid;
+ grid-template-columns: 44px 1fr 44px;
+ gap: 10px;
+ align-items: center;
+}
+
+.bitInputWrap{
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 10px;
+ align-items: center;
+ padding: 10px 12px;
+ border: 1px solid rgba(255,255,255,.10);
+ border-radius: 12px;
+ background: rgba(255,255,255,.04);
+}
+
+.bitInputLabel{
+ opacity: .75;
+ letter-spacing: .14em;
+ text-transform: uppercase;
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+.bitInput{
+ width: 100%;
+ min-width: 0;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: inherit;
+ font-size: 20px;
+ text-align: right;
+}
+
+.miniBtn{
+ height: 44px;
+ border-radius: 12px;
+ border: 1px solid rgba(255,255,255,.12);
+ background: rgba(255,255,255,.06);
+ color: rgba(255,255,255,.9);
+ font-size: 18px;
+ cursor: pointer;
+}
+.miniBtn:hover{ background: rgba(255,255,255,.08); }
+
+/* Buttons */
+.controlsRow{
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.btn{
+ border-radius: 12px;
+ border: 1px solid rgba(255,255,255,.12);
+ background: rgba(255,255,255,.06);
+ color: rgba(255,255,255,.92);
+ padding: 12px 12px;
+ font-weight: 800;
+ letter-spacing: .10em;
+ text-transform: uppercase;
+ cursor: pointer;
+}
+.btn:hover{ background: rgba(255,255,255,.08); }
+
+.btnWide{ width: 100%; }
+
+.btnAccent{
+ background: rgba(0,255,140,.12);
+ border-color: rgba(0,255,140,.22);
+}
+.btnAccent:hover{ background: rgba(0,255,140,.16); }
+
+.toolRowCentered{
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+ margin: 10px 0 12px;
+}
+
+.toolBtn{
+ width: 56px;
+ height: 56px;
+ border-radius: 14px;
+ border: 1px solid rgba(255,255,255,.12);
+ background: rgba(255,255,255,.06);
+ color: rgba(255,255,255,.92);
+ font-size: 18px;
+ cursor: pointer;
+}
+
+.toolDec{ background: rgba(255,0,0,.14); border-color: rgba(255,0,0,.20); }
+.toolInc{ background: rgba(0,255,140,.14); border-color: rgba(0,255,140,.20); }
+
+.toolRow2{
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+/* Reset stays white text */
+.btnReset{ color: rgba(255,255,255,.92); }
+
+/* Responsive */
+@media (max-width: 980px){
+ .topGrid{ grid-template-columns: 1fr; }
+ .panelCol{ position: static; }
+ .toolboxToggle{ position: static; align-self: flex-start; }
+}
diff --git a/src/src/styles/global.css b/src/src/styles/global.css
new file mode 100644
index 0000000..a71bbdf
--- /dev/null
+++ b/src/src/styles/global.css
@@ -0,0 +1,85 @@
+:root{
+ --bg: #1f2027;
+ --panel: #22242d;
+ --panel2: rgba(255,255,255,.04);
+ --text: #e8e8ee;
+ --muted: #a9acb8;
+ --accent: #33ff7a;
+ --accent-dim: rgba(51,255,122,.15);
+ --line: rgba(255,255,255,.12);
+}
+
+*{ box-sizing:border-box; }
+
+body{
+ margin:0;
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+}
+
+.siteHeader{
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ background: rgba(0,0,0,.15);
+ backdrop-filter: blur(8px);
+ border-bottom: 1px solid rgba(255,255,255,.06);
+}
+
+.siteHeaderInner{
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 14px 20px;
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ gap: 16px;
+}
+
+.brand{
+ color: var(--text);
+ text-decoration:none;
+ font-weight: 900;
+ letter-spacing:.02em;
+}
+
+.nav{
+ display:flex;
+ gap: 14px;
+ flex-wrap:wrap;
+ justify-content:flex-end;
+}
+.nav a{
+ color: var(--muted);
+ text-decoration:none;
+ font-weight: 700;
+ font-size: 14px;
+}
+.nav a:hover{ color: var(--text); }
+
+.siteMain{
+ min-height: calc(100vh - 140px);
+}
+
+.siteFooter{
+ border-top: 1px solid rgba(255,255,255,.08);
+ margin-top: 32px;
+ background: rgba(0,0,0,.10);
+}
+
+.siteFooterInner{
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 18px 20px 26px;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.6;
+}
+
+.footerTitle{
+ color: var(--text);
+ opacity:.9;
+ font-weight: 800;
+ margin-bottom: 6px;
+}
diff --git a/src/src/styles/site.css b/src/src/styles/site.css
new file mode 100644
index 0000000..bcaa838
--- /dev/null
+++ b/src/src/styles/site.css
@@ -0,0 +1,75 @@
+:root{
+ --bg: #1f2027;
+ --panel: rgba(255,255,255,.04);
+ --panel-border: rgba(255,255,255,.10);
+ --text: #e8e8ee;
+ --muted: #a9acb8;
+ --accent: #33ff7a;
+ --accent-dim: rgba(51,255,122,.15);
+ --line: rgba(255,255,255,.12);
+}
+
+*{ box-sizing: border-box; }
+
+body{
+ margin:0;
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+}
+
+.site-header{
+ border-bottom: 1px solid rgba(255,255,255,.08);
+ background: rgba(0,0,0,.12);
+}
+
+.site-header__inner{
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 14px 20px;
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ gap: 18px;
+}
+
+.brand{
+ font-weight: 800;
+ letter-spacing: .02em;
+}
+
+.nav{
+ display:flex;
+ gap: 14px;
+ flex-wrap: wrap;
+ justify-content:flex-end;
+}
+
+.nav__link{
+ color: var(--muted);
+ text-decoration:none;
+ font-weight: 700;
+ font-size: 13px;
+}
+.nav__link:hover{ color: var(--text); }
+
+.site-main{
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 28px 20px 40px;
+ min-height: calc(100vh - 140px);
+}
+
+.site-footer{
+ border-top: 1px solid rgba(255,255,255,.08);
+ background: rgba(0,0,0,.10);
+}
+
+.site-footer__inner{
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 16px 20px;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.5;
+}