diff --git a/web-app/css/styles.css b/web-app/css/styles.css
index c59dea6..6fa9d19 100644
--- a/web-app/css/styles.css
+++ b/web-app/css/styles.css
@@ -3189,16 +3189,25 @@ body {
}
.hero-features {
- flex-direction: column;
- align-items: flex-start;
- gap: 0.55rem;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ width: 100%;
+ justify-content: flex-start;
+ gap: 0.6rem;
margin-bottom: 1.5rem;
}
+ .hero-features::-webkit-scrollbar {
+ display: none;
+ }
+
.feature-badge {
- width: auto;
- justify-content: flex-start;
- padding: 0.15rem 0 0.35rem;
+ flex-shrink: 0;
+ padding: 0.5rem 0.9rem;
+ font-size: 0.85rem;
}
.btn-explore {
diff --git a/web-app/index.html b/web-app/index.html
index e45672f..76bd085 100644
--- a/web-app/index.html
+++ b/web-app/index.html
@@ -609,8 +609,72 @@
@media (max-width: 768px) {
.navbar { padding: 10px 8px; }
- .nav-island { height: 64px; padding: 0 16px; border-radius: 16px; }
- .mobile-menu-toggle { display: flex; z-index: 1100; }
+ .nav-island { height: auto; padding: 0.8rem 1.15rem; border-radius: 20px; }
+
+ .nav-wrapper {
+ flex-wrap: wrap;
+ justify-content: space-between;
+ border-radius: 28px;
+ padding: 0.8rem 1.2rem;
+ }
+
+ .navbar-brand {
+ order: 1;
+ }
+
+ .nav-controls {
+ order: 2;
+ position: static;
+ width: auto;
+ height: auto;
+ background: transparent;
+ border: none;
+ padding: 0;
+ flex-direction: row;
+ justify-content: flex-end;
+ gap: 10px;
+ z-index: auto;
+ transition: none;
+ }
+ [data-theme="light"] .nav-controls {
+ background: transparent;
+ border: none;
+ }
+
+ .sound-toggle, .theme-toggle {
+ width: 42px;
+ height: 42px;
+ border-radius: 12px;
+ }
+
+ .container1 {
+ order: 3;
+ width: 100%;
+ min-width: 0;
+ margin-top: 0.5rem;
+ }
+
+ .hero-features {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ width: 100%;
+ justify-content: flex-start;
+ gap: 0.6rem;
+ padding-bottom: 4px;
+ margin-bottom: 0;
+ }
+ .hero-features::-webkit-scrollbar {
+ display: none;
+ }
+ .hero-features .feature-badge {
+ flex-shrink: 0;
+ padding: 0.5rem 0.9rem;
+ font-size: 0.85rem;
+ }
.search-box {
position: absolute;
@@ -627,29 +691,6 @@
background: #ffffff;
border-color: rgba(0,0,0,0.08);
}
-
- .nav-controls {
- position: fixed;
- top: 0;
- right: -100%;
- width: 260px;
- height: 100vh;
- background: #071227;
- border-left: 1px solid rgba(255, 255, 255, 0.08);
- flex-direction: column;
- justify-content: center;
- gap: 20px;
- padding: 2rem;
- z-index: 1050;
- transition: right 0.35s cubic-bezier(0.4, 0, 0.2, 1);
- }
- [data-theme="light"] .nav-controls {
- background: #ffffff;
- border-left: 1px solid rgba(0,0,0,0.06);
- }
-
- .nav-controls.mobile-active { right: 0; }
- .sound-toggle, .theme-toggle { width: 50px; height: 50px; border-radius: 14px; }
}
/* ── Reduced-motion overrides for playground ────────────────── */
@@ -859,6 +900,7 @@
+
diff --git a/web-app/js/main.js b/web-app/js/main.js
index c04163d..a2a62e7 100644
--- a/web-app/js/main.js
+++ b/web-app/js/main.js
@@ -648,6 +648,99 @@ if (stickyFilterBar && heroSection) {
renderRecentSearches();
+ // ── Central Dynamic Auto-Scaling (ResizeObserver) ─────────────────
+ var modalResizeObserver = null;
+
+ function applyModalScaling() {
+ var modalContent = document.querySelector('.modal-content');
+ var modalBody = document.getElementById('modalBody');
+ if (!modalContent || !modalBody) return;
+
+ // Reset scroll position to top to avoid viewport clippings during calculations
+ modalContent.scrollTop = 0;
+ modalBody.scrollTop = 0;
+
+ // Hide scrollbars on the container
+ modalContent.style.overflow = 'hidden';
+
+ // Reset inline styles to capture natural dimensions
+ modalBody.style.transform = '';
+ modalBody.style.transformOrigin = '';
+ modalBody.style.width = '';
+ modalBody.style.height = '';
+
+ var targetEl = Array.from(modalBody.children).find(function (el) {
+ return el.tagName.toLowerCase() !== 'style';
+ }) || modalBody.firstElementChild;
+ if (!targetEl) return;
+
+ targetEl.style.transform = '';
+ targetEl.style.transformOrigin = '';
+
+ var computedStyle = window.getComputedStyle(modalContent);
+ var paddingTop = parseFloat(computedStyle.paddingTop) || 32;
+ var paddingBottom = parseFloat(computedStyle.paddingBottom) || 32;
+ var paddingLeft = parseFloat(computedStyle.paddingLeft) || 32;
+ var paddingRight = parseFloat(computedStyle.paddingRight) || 32;
+
+ var availableHeight = modalContent.clientHeight - paddingTop - paddingBottom;
+ var availableWidth = modalContent.clientWidth - paddingLeft - paddingRight;
+
+ var contentHeight = targetEl.scrollHeight;
+ var contentWidth = targetEl.scrollWidth;
+
+ if (contentHeight <= 0 || contentWidth <= 0) return;
+
+ var zoom = 1;
+ var heightZoom = availableHeight / contentHeight;
+ var widthZoom = availableWidth / contentWidth;
+
+ zoom = Math.min(heightZoom, widthZoom);
+ if (zoom > 1) {
+ zoom = 1;
+ }
+
+ // Apply scale transform and origins
+ targetEl.style.transform = 'scale(' + zoom + ')';
+ targetEl.style.transformOrigin = 'top center';
+
+ // Constrain wrapper block size to prevent scroll triggering
+ modalBody.style.height = (contentHeight * zoom) + 'px';
+ modalBody.style.width = '100%';
+ modalBody.style.display = 'flex';
+ modalBody.style.flexDirection = 'column';
+ modalBody.style.alignItems = 'center';
+ }
+
+ function initModalScaling() {
+ applyModalScaling();
+
+ if (modalResizeObserver) {
+ modalResizeObserver.disconnect();
+ }
+
+ var modalBody = document.getElementById('modalBody');
+ var targetEl = modalBody ? Array.from(modalBody.children).find(function (el) {
+ return el.tagName.toLowerCase() !== 'style';
+ }) || modalBody.firstElementChild : null;
+ if (targetEl) {
+ modalResizeObserver = new ResizeObserver(function () {
+ requestAnimationFrame(applyModalScaling);
+ });
+ modalResizeObserver.observe(targetEl);
+ }
+
+ window.addEventListener('resize', applyModalScaling);
+ }
+
+ function destroyModalScaling() {
+ if (modalResizeObserver) {
+ modalResizeObserver.disconnect();
+ modalResizeObserver = null;
+ }
+ window.removeEventListener('resize', applyModalScaling);
+ }
+
// ── Focus Trap for Modal ──────────────────────────────────────────
function getFocusableElements(root) {
var selector =
@@ -668,9 +761,9 @@ if (stickyFilterBar && heroSection) {
var first = focusables[0];
var last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
- e.preventDefault(); last.focus();
+ e.preventDefault(); last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
- e.preventDefault(); first.focus();
+ e.preventDefault(); first.focus({ preventScroll: true });
}
};
document.addEventListener('keydown', handler, true);
@@ -710,25 +803,37 @@ if (stickyFilterBar && heroSection) {
if (typeof initializeProject === 'function') initializeProject(name);
});
+ // Initialize reactive scale calculations
+ initModalScaling();
+
removeTrap = trapFocus(modal);
var focusables = getFocusableElements(modalBody);
var firstFocusable = focusables[0] || modalClose;
if (firstFocusable && typeof firstFocusable.focus === 'function') {
- firstFocusable.focus();
+ firstFocusable.focus({ preventScroll: true });
}
}
function closeProjectSafe() {
if (!modal || !modal.classList.contains('active')) return;
+
+ destroyModalScaling();
+
modal.classList.remove('active');
modal.setAttribute('aria-hidden', 'true');
document.body.style.paddingRight = '';
document.body.style.overflow = '';
setMainInert(false);
if (removeTrap) { removeTrap(); removeTrap = null; }
- if (modalBody) modalBody.innerHTML = '';
+ if (modalBody) {
+ modalBody.innerHTML = '';
+ modalBody.style.transform = '';
+ modalBody.style.transformOrigin = '';
+ modalBody.style.width = '';
+ modalBody.style.height = '';
+ }
if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
- lastFocusedElement.focus();
+ lastFocusedElement.focus({ preventScroll: true });
}
lastFocusedElement = null;
}
diff --git a/web-app/js/projects.js b/web-app/js/projects.js
index 269a23f..04c42f7 100644
--- a/web-app/js/projects.js
+++ b/web-app/js/projects.js
@@ -1,4 +1,4 @@
-// Project Registry
+// Project Registry
// Each project's HTML and logic lives in its own file under js/projects/
function getProjectHTML(projectName) {
@@ -2091,7 +2091,7 @@ function initCollatz() {
const yScale = graphHeight / maxValue;
// Draw axes
- ctx.strokeStyle = 'var(--text-secondary)';
+ ctx.strokeStyle = '#64748b';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
diff --git a/web-app/js/projects/collatz.js b/web-app/js/projects/collatz.js
index ca5e197..d712e02 100644
--- a/web-app/js/projects/collatz.js
+++ b/web-app/js/projects/collatz.js
@@ -230,7 +230,7 @@ function initCollatz() {
const xStep = graphWidth / (sequence.length - 1);
const yScale = graphHeight / maxValue;
- ctx.strokeStyle = 'var(--text-secondary)';
+ ctx.strokeStyle = '#64748b';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, padding);
diff --git a/web-app/js/projects/color-palette.js b/web-app/js/projects/color-palette.js
index 8dade8a..3e7fce1 100644
--- a/web-app/js/projects/color-palette.js
+++ b/web-app/js/projects/color-palette.js
@@ -713,7 +713,6 @@ function initColorPalette() {
output.style.display = 'block';
controls.style.display = 'none';
- output.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
copyBtn.addEventListener('click', () => {
diff --git a/web-app/js/projects/typing-speed-tester.js b/web-app/js/projects/typing-speed-tester.js
index edeecaf..3138f48 100644
--- a/web-app/js/projects/typing-speed-tester.js
+++ b/web-app/js/projects/typing-speed-tester.js
@@ -257,7 +257,7 @@ function getTypingSpeedTesterHTML() {
inputElement.value = "";
inputElement.disabled = false;
inputElement.removeAttribute("aria-disabled");
- inputElement.focus();
+ inputElement.focus({ preventScroll: true });
result.innerHTML = "";
startTime = Date.now();
@@ -733,7 +733,7 @@ resultDetails.innerHTML = `
requestAnimationFrame(() => sentenceElement.classList.remove('sentence-loading'));
inputElement.value = '';
inputElement.disabled = false;
- inputElement.focus();
+ inputElement.focus({ preventScroll: true });
if (startSession && !sessionStarted) {
sessionStarted = true;
startTime = Date.now();
@@ -743,9 +743,6 @@ resultDetails.innerHTML = `
// ensure pending state
const spans = sentenceElement.querySelectorAll('span');
spans.forEach(s => s.className = 'pending');
- if (inputElement.scrollIntoView) {
- inputElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
}
function setDifficulty(mode, { resetGame = false } = {}) {
diff --git a/web-app/js/projects/word-scramble.js b/web-app/js/projects/word-scramble.js
index 3396c47..ffe46ed 100644
--- a/web-app/js/projects/word-scramble.js
+++ b/web-app/js/projects/word-scramble.js
@@ -422,7 +422,7 @@ function initWordScramble() {
renderWord(shuffleWord(current.word));
updateStats();
setMessage(keepStreak ? 'Unscramble the letters.' : 'Fresh word loaded.');
- guessInput.focus();
+ guessInput.focus({ preventScroll: true });
// Launch countdown engine for the active round
startTimer();
@@ -506,7 +506,7 @@ function checkGuess() {
shuffleBtn.addEventListener('click', () => {
if (!current) return;
renderWord(shuffleWord(current.word));
- guessInput.focus();
+ guessInput.focus({ preventScroll: true });
});
nextBtn.addEventListener('click', () => {