From f68aea4e33db1589408a1848c7a469ad77d432d6 Mon Sep 17 00:00:00 2001 From: Alexander Lyall Date: Tue, 16 Dec 2025 17:18:03 +0000 Subject: [PATCH] Binary Page Beta 1 Signed-off-by: Alexander Lyall --- src/pages/binary.astro | 153 +++++------ src/scripts/binary.js | 226 ++++++++-------- src/styles/binary.css | 581 ++++++++++++++++++++++------------------- 3 files changed, 493 insertions(+), 467 deletions(-) diff --git a/src/pages/binary.astro b/src/pages/binary.astro index 8524e1e..8e27469 100644 --- a/src/pages/binary.astro +++ b/src/pages/binary.astro @@ -1,31 +1,92 @@ --- import "../styles/binary.css"; - -// ✅ Vite-bundled JS URL (works in dev + build + preview) -import binaryScriptUrl from "../scripts/binary.js?url"; - -// If you already have a site-wide Layout that adds header/footer, -// wrap the page with it here. -// Example (uncomment and adjust if you have it): -// import Layout from "../layouts/Layout.astro"; --- - + Binary | Computing:Box - + + -
+ + + + +
@@ -34,16 +95,14 @@ import binaryScriptUrl from "../scripts/binary.js?url";
0
Binary
-
0
+
0000 0000
-
-
@@ -57,64 +116,10 @@ import binaryScriptUrl from "../scripts/binary.js?url";
- - - -
- - + + diff --git a/src/scripts/binary.js b/src/scripts/binary.js index 5d64896..73d63d2 100644 --- a/src/scripts/binary.js +++ b/src/scripts/binary.js @@ -1,6 +1,6 @@ // src/scripts/binary.js // Computing:Box — Binary page logic (Unsigned + Two's Complement) -// NOTE: This file is written to match the IDs/classes in your current binary.astro HTML. +// Matches IDs/classes in src/pages/binary.astro (() => { /* ----------------------------- @@ -13,8 +13,6 @@ const modeToggle = document.getElementById("modeToggle"); const modeHint = document.getElementById("modeHint"); - const lblUnsigned = document.getElementById("lblUnsigned"); - const lblTwos = document.getElementById("lblTwos"); const btnCustomBinary = document.getElementById("btnCustomBinary"); const btnCustomDenary = document.getElementById("btnCustomDenary"); @@ -29,6 +27,9 @@ const btnBitsUp = document.getElementById("btnBitsUp"); const btnBitsDown = document.getElementById("btnBitsDown"); + const toolboxToggle = document.getElementById("toolboxToggle"); + const toolboxPanel = document.getElementById("toolboxPanel"); + /* ----------------------------- STATE ----------------------------- */ @@ -40,6 +41,10 @@ // Random run timer (brief) let randomTimer = null; + // Dynamic wrapping for the big binary display + // "nibbles per row" recalculated on resize + let nibblesPerRow = 2; // default for small widths + /* ----------------------------- HELPERS ----------------------------- */ @@ -81,7 +86,8 @@ } function unsignedBigIntToBits(vUnsigned) { - const v = ((vUnsigned % unsignedMaxExclusive(bitCount)) + unsignedMaxExclusive(bitCount)) % unsignedMaxExclusive(bitCount); + const span = unsignedMaxExclusive(bitCount); + const v = ((vUnsigned % span) + span) % span; for (let i = 0; i < bitCount; i++) { bits[i] = ((v >> BigInt(i)) & 1n) === 1n; } @@ -91,48 +97,77 @@ const u = bitsToUnsignedBigInt(); const signBit = bits[bitCount - 1] === true; if (!signBit) return u; - - // negative: u - 2^n return u - pow2Big(bitCount); } function signedBigIntToBitsTwos(vSigned) { - // wrap into range [-2^(n-1), 2^(n-1)-1] - const min = twosMin(bitCount); - const max = twosMax(bitCount); const span = pow2Big(bitCount); // 2^n - let v = vSigned; - - // wrap using modular arithmetic on signed domain - // Convert to unsigned representative: v mod 2^n v = ((v % span) + span) % span; - unsignedBigIntToBits(v); - // labels/denary will show signed later - // (No further action needed here) - } - - function formatBinaryGrouped() { - // MSB..LSB with a space every 4 bits (matches your screenshot 0000 0000) - 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; if (isTwosMode()) { - modeHint.textContent = "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; + modeHint.textContent = + "Tip: In two’s complement, the left-most bit (MSB) represents a negative value."; } else { - modeHint.textContent = "Tip: In unsigned binary, all bits represent positive values."; + modeHint.textContent = + "Tip: In unsigned binary, all bits represent positive values."; } } + /* ----------------------------- + BIG BINARY DISPLAY WRAP + - Determines how many nibbles (4 bits) fit per row + - Recalculates on window resize + ----------------------------- */ + function computeNibblesPerRow() { + if (!binaryEl) return; + + // available width for the binary number = element width + const w = binaryEl.getBoundingClientRect().width; + + // Approximate "nibble width" in pixels: + // 4 digits + a space; use font size and letter-spacing to estimate. + // This doesn't need to be perfect, just stable and responsive. + const style = window.getComputedStyle(binaryEl); + const fontSize = parseFloat(style.fontSize || "40"); // px + const letterSpacing = parseFloat(style.letterSpacing || "0"); + const digitW = fontSize * 0.62 + letterSpacing; // rough digit width + const nibbleW = digitW * 4 + digitW * 1.2; // include gap between nibbles + + // Always allow at least 2 nibbles per row + const fit = Math.max(2, Math.floor(w / nibbleW)); + nibblesPerRow = fit; + } + + function formatBinaryGroupedWrapped() { + // Build MSB..LSB and group into nibbles + const nibbles = []; + let current = ""; + + for (let i = bitCount - 1; i >= 0; i--) { + current += bits[i] ? "1" : "0"; + const posFromRight = (bitCount - i); + if (posFromRight % 4 === 0 || i === 0) { + // if last group is partial, left-pad inside the group + if (current.length < 4) current = current.padStart(4, "0"); + nibbles.push(current); + current = ""; + } + } + + // Now wrap nibbles into lines + const lines = []; + for (let i = 0; i < nibbles.length; i += nibblesPerRow) { + lines.push(nibbles.slice(i, i + nibblesPerRow).join(" ")); + } + + return lines.join("\n"); + } + /* ----------------------------- BUILD UI (BITS) ----------------------------- */ @@ -140,28 +175,18 @@ bitCount = clampInt(count, 1, 64); if (bitsInput) bitsInput.value = String(bitCount); - // reset bits array size, preserve existing LSBs where possible + // preserve existing LSBs where possible 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 = ""; - // If less than 8 bits, centre nicely using your CSS helper - bitsGrid.classList.toggle("bitsFew", bitCount < 8); - if (bitCount < 8) { - bitsGrid.style.setProperty("--cols", String(bitCount)); - } else { - bitsGrid.style.removeProperty("--cols"); - } - // Render MSB..LSB left-to-right for (let i = bitCount - 1; i >= 0; i--) { const bitEl = document.createElement("div"); bitEl.className = "bit"; - // IMPORTANT: We render the bulb as an emoji with NO circle/ring. - // We do not rely on the .bulb CSS ring/background at all. bitEl.innerHTML = `
@@ -183,36 +208,13 @@ }); }); - // Force the bulb to be "just the emoji" (removes the circle even if CSS adds it) - for (let i = 0; i < bitCount; i++) { - const bulb = document.getElementById(`bulb-${i}`); - if (!bulb) continue; - - // Strip the ring/circle coming from CSS - 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 = "26px"; - bulb.style.lineHeight = "1"; - bulb.style.display = "flex"; - bulb.style.alignItems = "center"; - bulb.style.justifyContent = "center"; - bulb.style.filter = "grayscale(1)"; - bulb.textContent = "💡"; - } - updateUI(); } /* ----------------------------- - UI UPDATE (READOUT + LABELS + BULBS + SWITCHES) + UI UPDATE ----------------------------- */ function updateBitLabels() { - // Show weights under each bit. // Unsigned: 2^i // Two's: MSB is -2^(n-1), others are 2^i for (let i = 0; i < bitCount; i++) { @@ -235,14 +237,11 @@ } function updateBulbs() { - // Bulbs should ALWAYS reflect bits, regardless of mode. for (let i = 0; i < bitCount; i++) { const bulb = document.getElementById(`bulb-${i}`); if (!bulb) continue; const on = bits[i] === true; - - // Make it look "lit" when on (no circle, just glow) if (on) { bulb.style.opacity = "1"; bulb.style.filter = "grayscale(0)"; @@ -266,7 +265,9 @@ denaryEl.textContent = unsigned.toString(); } - binaryEl.textContent = formatBinaryGrouped(); + // Ensure nibble wrapping is up-to-date + computeNibblesPerRow(); + binaryEl.textContent = formatBinaryGroupedWrapped(); } function updateUI() { @@ -284,11 +285,8 @@ const clean = String(binStr ?? "").replace(/\s+/g, ""); if (!/^[01]+$/.test(clean)) return false; - // Use rightmost bitCount bits; left pad with 0 const padded = clean.slice(-bitCount).padStart(bitCount, "0"); - for (let i = 0; i < bitCount; i++) { - // padded is MSB..LSB, bits[] is LSB..MSB const charFromRight = padded[padded.length - 1 - i]; bits[i] = charFromRight === "1"; } @@ -304,10 +302,8 @@ const raw = String(vStr ?? "").trim(); if (!raw) return false; - // BigInt parse (supports negatives) let v; try { - // Allow normal integers only if (!/^-?\d+$/.test(raw)) return false; v = BigInt(raw); } catch { @@ -315,17 +311,13 @@ } if (isTwosMode()) { - // Clamp to representable range const min = twosMin(bitCount); const max = twosMax(bitCount); if (v < min || v > max) return false; - signedBigIntToBitsTwos(v); } else { - // Unsigned only if (v < 0n) return false; if (v > unsignedMaxValue(bitCount)) return false; - unsignedBigIntToBits(v); } @@ -337,19 +329,13 @@ SHIFTS ----------------------------- */ function shiftLeft() { - // logical left shift: bits move to higher index; LSB becomes 0 - for (let i = bitCount - 1; i >= 1; i--) { - bits[i] = bits[i - 1]; - } + for (let i = bitCount - 1; i >= 1; i--) bits[i] = bits[i - 1]; bits[0] = false; updateUI(); } function shiftRight() { - // logical right shift: bits move to lower index; MSB becomes 0 - for (let i = 0; i < bitCount - 1; i++) { - bits[i] = bits[i + 1]; - } + for (let i = 0; i < bitCount - 1; i++) bits[i] = bits[i + 1]; bits[bitCount - 1] = false; updateUI(); } @@ -367,7 +353,7 @@ const min = twosMin(bitCount); const max = twosMax(bitCount); let v = bitsToSignedBigIntTwos() + 1n; - if (v > max) v = min; // wrap + if (v > max) v = min; signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); @@ -382,7 +368,7 @@ const min = twosMin(bitCount); const max = twosMax(bitCount); let v = bitsToSignedBigIntTwos() - 1n; - if (v < min) v = max; // wrap + if (v < min) v = max; signedBigIntToBitsTwos(v); } else { const span = unsignedMaxExclusive(bitCount); @@ -393,26 +379,21 @@ } /* ----------------------------- - RANDOM (FIXED: NO BigInt->Number Math.min) + RANDOM (BigInt-safe) ----------------------------- */ function cryptoRandomBigInt(maxExclusive) { - // returns 0 <= x < maxExclusive if (maxExclusive <= 0n) return 0n; const bitLen = maxExclusive.toString(2).length; const byteLen = Math.ceil(bitLen / 8); - // Rejection sampling while (true) { const bytes = new Uint8Array(byteLen); crypto.getRandomValues(bytes); let x = 0n; - for (const b of bytes) { - x = (x << 8n) | BigInt(b); - } + for (const b of bytes) x = (x << 8n) | BigInt(b); - // mask down to bitLen to reduce rejections slightly const extraBits = BigInt(byteLen * 8 - bitLen); if (extraBits > 0n) x = x >> extraBits; @@ -421,27 +402,20 @@ } function setRandomOnce() { - if (isTwosMode()) { - const span = unsignedMaxExclusive(bitCount); // 2^n - const u = cryptoRandomBigInt(span); // 0..2^n-1 - unsignedBigIntToBits(u); - } else { - const span = unsignedMaxExclusive(bitCount); - const u = cryptoRandomBigInt(span); - unsignedBigIntToBits(u); - } + const span = unsignedMaxExclusive(bitCount); + const u = cryptoRandomBigInt(span); + unsignedBigIntToBits(u); updateUI(); } function runRandomBriefly() { - // stop any existing run if (randomTimer) { clearInterval(randomTimer); randomTimer = null; } const start = Date.now(); - const durationMs = 900; // brief run then stop + const durationMs = 900; const tickMs = 80; randomTimer = setInterval(() => { @@ -454,30 +428,25 @@ } /* ----------------------------- - BIT WIDTH CONTROLS + BIT WIDTH ----------------------------- */ function setBitWidth(n) { const v = clampInt(n, 1, 64); buildBits(v); } + /* ----------------------------- + TOOLBOX TOGGLE + ----------------------------- */ + function setToolboxCollapsed(collapsed) { + document.body.classList.toggle("toolbox-collapsed", collapsed); + toolboxToggle?.setAttribute("aria-expanded", String(!collapsed)); + } + /* ----------------------------- EVENTS ----------------------------- */ - - // Collapsible right panel - const sidePanel = document.getElementById("sidePanel"); - const btnPanelToggle = document.getElementById("btnPanelToggle"); - - btnPanelToggle?.addEventListener("click", () => { - sidePanel?.classList.toggle("isCollapsed"); - // flip the chevron - btnPanelToggle.textContent = sidePanel?.classList.contains("isCollapsed") ? "❮" : "❯"; - }); - - modeToggle?.addEventListener("change", () => { - updateUI(); - }); + modeToggle?.addEventListener("change", () => updateUI()); btnCustomBinary?.addEventListener("click", () => { const v = prompt(`Enter binary (spaces allowed). Current width: ${bitCount} bits`); @@ -507,14 +476,25 @@ btnBitsUp?.addEventListener("click", () => setBitWidth(bitCount + 1)); btnBitsDown?.addEventListener("click", () => setBitWidth(bitCount - 1)); - bitsInput?.addEventListener("change", () => { - setBitWidth(Number(bitsInput.value)); + bitsInput?.addEventListener("change", () => setBitWidth(Number(bitsInput.value))); + + toolboxToggle?.addEventListener("click", () => { + const collapsed = document.body.classList.contains("toolbox-collapsed"); + setToolboxCollapsed(!collapsed); + // ensure binary re-wraps after layout change + requestAnimationFrame(() => updateReadout()); + }); + + // Recompute nibble wrapping on resize + window.addEventListener("resize", () => { + // throttled via rAF to avoid spam + requestAnimationFrame(() => updateReadout()); }); /* ----------------------------- INIT ----------------------------- */ updateModeHint(); + setToolboxCollapsed(false); buildBits(bitCount); })(); - diff --git a/src/styles/binary.css b/src/styles/binary.css index 288f2ef..030b0f3 100644 --- a/src/styles/binary.css +++ b/src/styles/binary.css @@ -1,219 +1,218 @@ :root{ - --bg: #1f2027; - --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); + --bg:#1f2027; + --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); - /* right column sizing */ - --sideW: 720px; - --sidePad: 24px; - - /* header offset (tweak if your header is taller/shorter) */ - --sideTop: 92px; + /* Toolbox sizing */ + --toolboxW: 320px; /* default width */ + --toolboxGap: 20px; /* gap from right edge */ + --toolboxTop: 92px; /* below header area */ } +/* ---------- Fonts ---------- */ +/* Numbers */ @font-face{ - font-family: "DSEG7ClassicRegular"; + font-family:"DSEG7ClassicRegular"; 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-weight:400; + font-style:normal; + font-display:swap; } +/* Text */ @font-face{ - font-family: "SevenSegment"; + 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; + font-weight:400; + font-style:normal; + font-display:swap; } +*{ box-sizing:border-box; } + body{ margin:0; - font-family: "SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--text); + background:var(--bg); + color:var(--text); + font-family:"SevenSegment", system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; } -/* Leave enough room for the fixed right panel */ +/* When toolbox is collapsed, we remove the reserved padding */ +body.toolbox-collapsed{ + --toolboxW: 0px; +} + +/* ---------- Main layout ---------- */ .wrap{ max-width: 1200px; margin: 0 auto; padding: 32px 20px 60px; - padding-right: calc(var(--sideW) + (var(--sidePad) * 2)); + + /* reserve room for the fixed toolbox so it NEVER overlaps */ + padding-right: calc(20px + var(--toolboxW) + var(--toolboxGap)); + transition: padding-right .2s ease; +} + +body.toolbox-collapsed .wrap{ + padding-right: 20px; } .topGrid{ display:grid; grid-template-columns: 1fr; - gap: 28px; + gap:28px; align-items:start; } .readout{ text-align:center; - padding: 10px 10px 0; + padding:10px 10px 0; } .label{ - letter-spacing: .18em; - font-weight: 700; - color: var(--muted); - text-transform: uppercase; - font-size: 14px; - margin-top: 10px; + letter-spacing:.18em; + font-weight:700; + color:var(--muted); + text-transform:uppercase; + font-size:14px; + margin-top:10px; } +/* All numbers */ +.num, +.bitVal, +.bitInput{ + font-family:"DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-weight:400; +} + +/* Denary/Binary display (25% smaller than before) */ .num{ - font-family: "DSEG7ClassicRegular", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; - font-weight: 400; - color: var(--accent); - text-shadow: 0 0 18px var(--accent-dim); + color:var(--accent); + text-shadow:0 0 18px var(--accent-dim); + letter-spacing: 0.03em; /* + a pixel-ish feel */ } -/* 25% smaller */ .denaryValue{ - font-size: 53px; - line-height: 1.0; - margin: 6px 0 10px; + font-size: 52px; /* was ~70 */ + line-height:1.0; + margin:6px 0 10px; } -/* 25% smaller + slightly wider spacing */ .binaryValue{ - font-size: 39px; - letter-spacing: calc(.12em + 2px); - line-height: 1.0; - margin: 6px 0 14px; - white-space: pre; /* preserve spaces */ + font-size: 39px; /* was ~52 */ + line-height:1.05; + margin:6px 0 14px; + white-space: pre-wrap; + word-break: normal; + overflow-wrap: normal; + letter-spacing: 0.06em; /* widen characters slightly */ } +/* Buttons */ .controlsStack{ - margin-top: 10px; + margin-top:10px; display:flex; flex-direction:column; - gap: 10px; + gap:10px; align-items:center; } .controlsRow{ display:flex; - gap: 12px; + gap:12px; justify-content:center; flex-wrap:wrap; } .btn{ - background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); - color: #fff; - padding: 12px 14px; - border-radius: 12px; - font-weight: 700; - cursor: pointer; - min-width: 170px; - font-family: "SevenSegment", system-ui, sans-serif; + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); + color:#fff; + padding:12px 14px; + border-radius:12px; + font-weight:700; + cursor:pointer; + min-width:170px; + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing: .06em; } .btn:active{ transform: translateY(1px); } .btnAccent{ - background: rgba(51,255,122,.18); - border-color: rgba(51,255,122,.45); + background:rgba(51,255,122,.18); + border-color:rgba(51,255,122,.45); } .divider{ - margin-top: 26px; - border-top: 1px solid var(--line); + margin-top:26px; + border-top:1px solid var(--line); } -/* ------------------------- - RIGHT COLUMN: pinned + collapsible -------------------------- */ -.panelCol{ - position: fixed; - right: var(--sidePad); - top: var(--sideTop); - width: var(--sideW); +/* ---------- Bits grid ---------- */ +.bitsWrap{ margin-top:22px; } + +/* auto-fit prevents overlap at large values without shrinking the digits */ +.bitsGrid{ + display:grid; + gap:18px; + justify-content:center; + padding-top:18px; + + /* each tile has a safe min width so large labels don't collide */ + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + max-width: 100%; +} + +.bit{ display:flex; flex-direction:column; - gap: 14px; - z-index: 50; - transition: transform .2s ease; -} - -/* collapse leaves a small handle visible */ -.panelCol.isCollapsed{ - transform: translateX(calc(100% - 52px)); -} - -.panelToggle{ - position: absolute; - left: -52px; - top: 0; - width: 44px; - height: 44px; - border-radius: 12px; - border: 1px solid rgba(255,255,255,.14); - background: rgba(255,255,255,.06); - color: #fff; - cursor: pointer; - font-weight: 900; - display:flex; align-items:center; - justify-content:center; - font-family: "SevenSegment", system-ui, sans-serif; + gap:10px; + padding:8px 4px; + text-align:center; + min-width:120px; } -/* cards */ -.card{ - background: var(--panel2); - border: 1px solid rgba(255,255,255,.10); - border-radius: 14px; - padding: 14px; +.bulb{ + /* NO circle/ring. Just the emoji. 25% bigger than prior ~26px -> ~33px */ + font-size: 33px; + line-height:1; + background:transparent; + border:none; + width:auto; + height:auto; + opacity:.45; + filter: grayscale(1); + text-shadow:none; + user-select:none; } -.cardTitle{ - letter-spacing: .18em; - font-weight: 800; - color: var(--muted); - text-transform: uppercase; - font-size: 12px; - margin: 0 0 10px; +.bitVal{ + font-size:28px; + color:var(--text); + opacity:.95; + line-height:1; + min-height:32px; + white-space: nowrap; /* stop wrapping into each other */ + letter-spacing: 0.02em; } -.hint{ - color: var(--muted); - font-size: 12px; - margin-top: 8px; - line-height: 1.35; -} - -.toggleRow{ - display:flex; - align-items:center; - justify-content:space-between; - gap: 10px; -} - -.toggleLabel{ - color: var(--text); - font-weight: 700; - font-size: 14px; -} - -/* Shared toggle switch */ +/* Shared toggle switch (bit switches + mode) */ .switch{ - position: relative; - width: 56px; - height: 34px; + position:relative; + width:56px; + height:34px; display:inline-block; - flex: 0 0 auto; + flex:0 0 auto; } .switch input{ opacity:0; @@ -223,88 +222,198 @@ body{ .slider{ position:absolute; inset:0; - background: rgba(255,255,255,.10); - border: 1px solid rgba(255,255,255,.14); - border-radius: 999px; - transition: .18s ease; + background:rgba(255,255,255,.10); + border:1px solid rgba(255,255,255,.14); + border-radius:999px; + transition:.18s ease; } .slider::before{ content:""; position:absolute; - height: 28px; - width: 28px; - left: 3px; - top: 2px; - background: rgba(255,255,255,.92); - border-radius: 50%; - transition: .18s ease; + height:28px; + width:28px; + left:3px; + top:2px; + background:rgba(255,255,255,.92); + border-radius:50%; + transition:.18s ease; } .switch input:checked + .slider{ - background: rgba(51,255,122,.20); - border-color: rgba(51,255,122,.55); + background:rgba(51,255,122,.20); + border-color:rgba(51,255,122,.55); } .switch input:checked + .slider::before{ transform: translateX(22px); - background: var(--accent); + background:var(--accent); } -/* Tools + Bit Width side-by-side */ -.sideRow{ - display:flex; - gap: 14px; -} -.sideRow .card{ - flex: 1 1 0; -} +/* ---------- Toolbox (fixed, non-overlapping) ---------- */ +.toolboxToggle{ + position: fixed; + top: 18px; + right: var(--toolboxGap); + z-index: 40; -/* Tools layout: arrows row + reset/random row */ -.toolRow, .toolRow2{ display:flex; - gap: 10px; align-items:center; -} + gap:10px; -/* Reset/Random = 50% current width (explicit) */ -.toolBtn{ - height: 48px; - border-radius: 12px; background: rgba(255,255,255,.06); - border: 1px solid rgba(255,255,255,.14); - color: #fff; - cursor: pointer; - font-weight: 800; - font-family: "SevenSegment", system-ui, sans-serif; + border:1px solid rgba(255,255,255,.14); + color:#fff; + border-radius: 12px; + padding: 10px 12px; + cursor:pointer; + + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing:.06em; } -/* Reset/Random smaller */ -.toolBtn.toolHalf{ - width: 120px; +.toolboxIcon{ font-size: 16px; line-height:1; } +.toolboxText{ font-weight:800; font-size: 14px; } + +.toolboxFixed{ + position: fixed; + top: var(--toolboxTop); + right: var(--toolboxGap); + width: var(--toolboxW); + z-index: 35; + + transition: transform .2s ease, opacity .2s ease; } -/* Arrows half of that */ -.toolBtn.toolQuarter{ - width: 60px; - font-size: 22px; - padding: 0; +body.toolbox-collapsed .toolboxFixed{ + transform: translateX(calc(100% + var(--toolboxGap))); + opacity: 0; + pointer-events: none; } -/* colour arrows */ -.toolBtn.toolUp{ - background: rgba(51,255,122,.18); - border-color: rgba(51,255,122,.45); -} -.toolBtn.toolDown{ - background: rgba(255,80,80,.18); - border-color: rgba(255,80,80,.45); +/* cards inside toolbox */ +.panelCol{ + display:flex; + flex-direction:column; + gap:14px; } -/* Bit width control */ -.bitWidthRow{ +.card{ + background: var(--panel2); + border:1px solid rgba(255,255,255,.10); + border-radius:14px; + padding:14px; + backdrop-filter: blur(6px); +} + +.cardTitle{ + letter-spacing:.18em; + font-weight:800; + color:var(--muted); + text-transform:uppercase; + font-size:12px; + margin:0 0 10px; +} + +.hint{ + color:var(--muted); + font-size:12px; + margin-top:8px; + line-height:1.35; +} + +.toggleRow{ + display:flex; + align-items:center; + justify-content:space-between; + gap:10px; +} + +.toggleLabel{ + color:var(--text); + font-weight:700; + font-size:14px; +} + +/* ---------- Tools merged layout ---------- */ +.toolsTopRow{ + display:grid; + grid-template-columns: 1fr 1fr; + gap:10px; + align-items:center; + margin-bottom: 10px; +} + +.spinGroup{ + display:grid; + grid-template-columns: 1fr 1fr; + gap:10px; +} + +.toolsBottomRow{ + display:grid; + grid-template-columns: 1.25fr 0.75fr; /* bit width gets more space than random */ + gap:10px; + align-items:end; +} + +.toolBtn{ + height:48px; + border-radius:12px; + background:rgba(255,255,255,.06); + border:1px solid rgba(255,255,255,.14); + color:#fff; + cursor:pointer; + font-weight:800; + font-family:"SevenSegment", system-ui, sans-serif; + letter-spacing:.06em; +} + +.toolSpin{ + font-size:22px; + width: 100%; /* kept narrow via grid allocation */ +} + +.toolDown{ + background: rgba(255, 70, 70, .20); + border-color: rgba(255, 70, 70, .45); +} + +.toolUp{ + background: rgba(51,255,122,.20); + border-color: rgba(51,255,122,.55); +} + +.toolReset{ + width: 100%; +} + +.toolRandom{ + width: 100%; + /* make random ~5% narrower by padding reduction (without breaking grid) */ + padding-left: 10px; + padding-right: 10px; +} + +/* bit width mini block */ +.bitWidthMini{ + display:flex; + flex-direction:column; + gap:6px; +} + +.bitWidthMiniLabel{ + color:var(--muted); + font-size:12px; + font-weight:800; + letter-spacing:.18em; + text-transform:uppercase; +} + +.bitWidthMiniRow{ display:grid; grid-template-columns: 44px 1fr 44px; - gap: 10px; + gap:10px; align-items:center; } + .miniBtn{ height:44px; width:44px; @@ -315,107 +424,39 @@ body{ cursor:pointer; font-weight:900; font-size:18px; - font-family: "SevenSegment", system-ui, sans-serif; + font-family:"SevenSegment", system-ui, sans-serif; } -.bitInputWrap{ +.bitInput{ + width: 100%; + min-width: 64px; /* MUST fit 2 digits comfortably */ + text-align:center; background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.14); border-radius:12px; - padding:10px 12px; - display:flex; - align-items:center; - justify-content:space-between; - gap:12px; -} -.bitInputLabel{ - color:var(--muted); - font-size:12px; - font-weight:800; - letter-spacing:.18em; - text-transform:uppercase; -} -.bitInput{ - width:86px; - text-align:right; - background:transparent; - border:none; + padding:10px 8px; outline:none; color:var(--accent); - font-family:"DSEG7ClassicRegular", ui-monospace, monospace; font-size:28px; } + .bitInput::-webkit-outer-spin-button, .bitInput::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; } -/* Bits: wrap every 8, centred */ -.bitsWrap{ - margin-top: 22px; -} -.bitsGrid{ - display:grid; - gap: 18px; - justify-content:center; - grid-template-columns: repeat(8, minmax(90px, 1fr)); - padding-top: 18px; -} -.bitsGrid.bitsFew{ - grid-template-columns: repeat(var(--cols, 4), minmax(90px, 1fr)); -} - -.bit{ - display:flex; - flex-direction:column; - align-items:center; - gap: 10px; - padding: 8px 4px; - text-align:center; -} - -/* Bulb: no circle here (JS controls the emoji), but keep sizing if used */ -.bulb{ - border: none; - background: transparent; - width: auto; - height: auto; - border-radius: 0; - box-shadow: none; - display:flex; - align-items:center; - justify-content:center; - line-height: 1; - opacity: .55; - font-size: 33px; /* 25% larger from 26px */ -} - -.bitVal{ - font-family:"DSEG7ClassicRegular", ui-monospace, monospace; - font-size: 28px; - color: var(--text); - opacity: .95; - line-height: 1; - min-height: 32px; -} - -/* responsive: stop pinning on small screens */ +/* ---------- Responsive ---------- */ @media (max-width: 980px){ - .panelCol{ - position: static; - width: auto; - transform: none !important; - } - .panelToggle{ display:none; } .wrap{ + /* On small screens, don’t reserve fixed sidebar space; toolbox floats */ padding-right: 20px; } + .toolboxFixed{ + width: min(320px, calc(100vw - 40px)); + } } @media (max-width: 520px){ - .btn{ min-width: 150px; } - .bitsGrid{ grid-template-columns: repeat(4, minmax(90px, 1fr)); } - .toolBtn.toolHalf{ width: 100px; } - .toolBtn.toolQuarter{ width: 52px; } + .btn{ min-width:150px; } }