diff --git a/public/css/main.css b/public/css/main.css
index de143cc01..432fe4adb 100644
--- a/public/css/main.css
+++ b/public/css/main.css
@@ -630,6 +630,14 @@ code {
z-index: 1;
}
+.split-content {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
.split-after {
/* Angled clip-path matching divider's skewX(-10deg): top-right, bottom-left */
clip-path: polygon(78% 0%, 100% 0%, 100% 100%, 62% 100%);
@@ -1202,6 +1210,131 @@ code {
}
}
+/* ============================================
+ MOBILE COMMANDS LAYOUT
+ ============================================ */
+
+.mobile-commands-layout {
+ display: none;
+}
+
+@media (max-width: 900px) {
+ .mobile-commands-layout {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-md);
+ }
+
+ .commands-container {
+ display: none;
+ }
+}
+
+.mobile-carousel-wrapper {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ padding: var(--spacing-xs) 0;
+}
+
+.mobile-carousel-wrapper::-webkit-scrollbar {
+ display: none;
+}
+
+.mobile-carousel {
+ display: flex;
+ gap: var(--spacing-xs);
+ padding-right: var(--spacing-md);
+}
+
+.mobile-cmd-pill {
+ flex-shrink: 0;
+ padding: var(--spacing-sm) var(--spacing-md);
+ min-height: 44px;
+ font-family: var(--font-mono);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ color: var(--color-ash);
+ background: var(--color-cream);
+ border: 1px solid var(--color-mist);
+ border-radius: 100px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+}
+
+.mobile-cmd-pill:hover {
+ color: var(--color-charcoal);
+ border-color: var(--color-charcoal);
+}
+
+.mobile-cmd-pill.active {
+ color: var(--color-paper);
+ background: var(--color-ink);
+ border-color: var(--color-ink);
+}
+
+.mobile-demo-area {
+ background: var(--color-cream);
+ border: 1px solid var(--color-mist);
+ border-radius: 8px;
+ padding: var(--spacing-sm);
+}
+
+.mobile-demo-area .demo-split-comparison {
+ width: 100%;
+}
+
+.mobile-demo-area .split-container {
+ width: 100%;
+ max-width: 100%;
+ height: 320px;
+}
+
+.mobile-demo-area .demo-caption {
+ font-size: 0.75rem;
+ margin-top: var(--spacing-sm);
+}
+
+.mobile-info-area {
+ padding-top: var(--spacing-sm);
+}
+
+.mobile-cmd-info {
+ display: none;
+ padding: var(--spacing-sm) 0;
+}
+
+.mobile-cmd-info.active {
+ display: block;
+}
+
+.mobile-cmd-name {
+ font-family: var(--font-mono);
+ font-size: 1.125rem;
+ font-weight: 600;
+ color: var(--color-ink);
+ margin: 0 0 var(--spacing-xs) 0;
+}
+
+.mobile-cmd-desc {
+ font-size: 0.875rem;
+ color: var(--color-charcoal);
+ line-height: 1.5;
+ margin: 0;
+}
+
+.mobile-cmd-rel {
+ margin-top: var(--spacing-xs);
+ font-size: 0.75rem;
+ color: var(--color-ash);
+}
+
+.mobile-cmd-rel code {
+ font-family: var(--font-mono);
+ color: var(--color-ink);
+}
+
/* ============================================
DOWNLOADS SECTION
============================================ */
@@ -1684,20 +1817,18 @@ code {
flex-wrap: wrap;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-lg);
- border-bottom: 1px solid var(--color-mist);
- padding-bottom: var(--spacing-sm);
}
.pattern-tab {
background: none;
border: none;
+ border-bottom: 2px solid transparent;
font-family: var(--font-body);
font-size: 0.875rem;
color: var(--color-ash);
padding: var(--spacing-xs) var(--spacing-sm);
cursor: pointer;
- transition: color var(--duration-fast) var(--ease-out);
- position: relative;
+ transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out);
}
.pattern-tab:hover {
@@ -1707,16 +1838,7 @@ code {
.pattern-tab.active {
color: var(--color-ink);
font-weight: 500;
-}
-
-.pattern-tab.active::after {
- content: '';
- position: absolute;
- bottom: calc(-1 * var(--spacing-sm) - 1px);
- left: 0;
- right: 0;
- height: 2px;
- background: var(--color-accent);
+ border-bottom-color: var(--color-accent);
}
.pattern-tab:focus-visible {
@@ -1829,9 +1951,10 @@ code {
}
.btn-small {
- padding: var(--spacing-xs) var(--spacing-md);
+ padding: var(--spacing-sm) var(--spacing-md);
font-size: 0.8rem;
gap: var(--spacing-xs);
+ min-height: 44px; /* Touch target minimum */
}
.btn-small svg {
diff --git a/public/js/components/framework-viz.js b/public/js/components/framework-viz.js
index ee5bb3899..c7f428cdb 100644
--- a/public/js/components/framework-viz.js
+++ b/public/js/components/framework-viz.js
@@ -137,6 +137,10 @@ export class PeriodicTable {
}
showDefaultInfo() {
+ // Use "Tap" on touch devices, "Hover over" on desktop
+ const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
+ const action = isTouchDevice ? 'Tap' : 'Hover over';
+
this.infoPanel.innerHTML = `
- Hover over a command to see details
+ ${action} a command to see details
`;
}
@@ -342,7 +346,7 @@ export class PeriodicTable {
el.style.boxShadow = 'none';
};
- // Mouse events
+ // Mouse events (desktop)
el.addEventListener('mouseenter', activate);
el.addEventListener('mouseleave', deactivate);
@@ -350,8 +354,15 @@ export class PeriodicTable {
el.addEventListener('focus', activate);
el.addEventListener('blur', deactivate);
+ // Touch events - activate on tap, stay active until another tap
+ el.addEventListener('touchstart', (e) => {
+ e.preventDefault(); // Prevent double-firing with click
+ activate();
+ }, { passive: false });
+
// Click/Enter to scroll to command
el.addEventListener('click', () => {
+ activate(); // Also activate on click for touch devices
const target = document.getElementById(`cmd-${cmd}`);
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
diff --git a/public/js/components/glass-terminal.js b/public/js/components/glass-terminal.js
index 8a0fce3b7..88673a87b 100644
--- a/public/js/components/glass-terminal.js
+++ b/public/js/components/glass-terminal.js
@@ -2,6 +2,16 @@ import { renderCommandDemo } from "../demo-renderer.js";
import { initSplitCompare } from "../effects/split-compare.js";
import { commandProcessSteps, commandCategories, commandRelationships } from "../data.js";
+// Track current split instance and command for cleanup
+let currentSplitInstance = null;
+let currentCommandId = null;
+
+const MOBILE_BREAKPOINT = 900;
+
+function isMobile() {
+ return window.innerWidth <= MOBILE_BREAKPOINT;
+}
+
export function initGlassTerminal() {
// Initial setup if needed
}
@@ -10,7 +20,34 @@ export function renderTerminalLayout(commands) {
const container = document.querySelector('.commands-gallery');
if (!container) return;
- // Group commands by category
+ if (isMobile()) {
+ renderMobileLayout(container, commands);
+ } else {
+ renderDesktopLayout(container, commands);
+ }
+
+ // Re-render on resize crossing breakpoint
+ let wasMobile = isMobile();
+ window.addEventListener('resize', () => {
+ const nowMobile = isMobile();
+ if (nowMobile !== wasMobile) {
+ wasMobile = nowMobile;
+ currentSplitInstance = null;
+ currentCommandId = null;
+ if (nowMobile) {
+ renderMobileLayout(container, commands);
+ } else {
+ renderDesktopLayout(container, commands);
+ }
+ }
+ });
+}
+
+// ============================================
+// DESKTOP LAYOUT (unchanged)
+// ============================================
+
+function renderDesktopLayout(container, commands) {
const categoryOrder = ['diagnostic', 'quality', 'intensity', 'adaptation', 'enhancement', 'system'];
const categoryLabels = {
'diagnostic': 'Diagnose',
@@ -28,7 +65,6 @@ export function renderTerminalLayout(commands) {
grouped[cat].push(cmd);
});
- // Build manual with category headers
let manualHTML = '';
categoryOrder.forEach(cat => {
if (grouped[cat] && grouped[cat].length > 0) {
@@ -51,7 +87,6 @@ export function renderTerminalLayout(commands) {
-
➜
Waiting for input...
@@ -62,9 +97,8 @@ export function renderTerminalLayout(commands) {
`;
- setupScrollSpy(commands);
+ setupDesktopScrollSpy(commands);
- // Select first command initially
if (commands.length > 0) {
updateTerminal(commands[0], document.getElementById('terminal-content'), commands);
const firstEntry = document.querySelector('.manual-entry');
@@ -95,19 +129,16 @@ function renderManualEntry(cmd) {
`;
}
-function setupScrollSpy(commands) {
+function setupDesktopScrollSpy(commands) {
const entries = document.querySelectorAll('.manual-entry');
const terminalContent = document.getElementById('terminal-content');
- // Intersection Observer for Scroll Spy - trigger around vertical center
const observer = new IntersectionObserver((observerEntries) => {
observerEntries.forEach(entry => {
if (entry.isIntersecting) {
- // Update active state
document.querySelectorAll('.manual-entry').forEach(e => e.classList.remove('active'));
entry.target.classList.add('active');
- // Update terminal
const cmdId = entry.target.dataset.id;
const cmd = commands.find(c => c.id === cmdId);
if (cmd) {
@@ -117,15 +148,13 @@ function setupScrollSpy(commands) {
});
}, {
threshold: 0.1,
- rootMargin: "-35% 0px -55% 0px" // Trigger zone biased toward upper-middle of viewport
+ rootMargin: "-35% 0px -55% 0px"
});
entries.forEach(e => observer.observe(e));
- // Click interaction - immediate selection + smooth scroll
entries.forEach(e => {
e.addEventListener('click', () => {
- // Immediately update state
document.querySelectorAll('.manual-entry').forEach(el => el.classList.remove('active'));
e.classList.add('active');
@@ -135,42 +164,32 @@ function setupScrollSpy(commands) {
updateTerminal(cmd, terminalContent, commands);
}
- // Then scroll into view
e.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
});
}
-// Track current split instance and command for cleanup
-let currentSplitInstance = null;
-let currentCommandId = null;
-
function updateTerminal(cmd, container, allCommands) {
if (!cmd || !container) return;
- // Skip if already showing this command
if (currentCommandId === cmd.id) return;
currentCommandId = cmd.id;
- // Cleanup previous split instance
if (currentSplitInstance) {
currentSplitInstance.destroy();
currentSplitInstance = null;
}
- // Get process steps for this command (or use generic fallback)
const steps = commandProcessSteps[cmd.id] || ['Analyze', 'Transform', 'Verify'];
const stepsOutput = steps.map((step, i) =>
`
${i + 1}. ${step}...`
).join('
');
- // Content update - no extra indentation to avoid whitespace
container.innerHTML = `
➜/${cmd.id}
${stepsOutput}
✓ Complete
${renderCommandDemo(cmd.id)}
➜
`;
- // Initialize split-compare effect for the demo
const splitComparison = container.querySelector('.demo-split-comparison');
if (splitComparison) {
currentSplitInstance = initSplitCompare(splitComparison, {
@@ -182,4 +201,115 @@ function updateTerminal(cmd, container, allCommands) {
}
}
+// ============================================
+// MOBILE LAYOUT - Carousel + Sticky Demo
+// ============================================
+function renderMobileLayout(container, commands) {
+ // Build carousel pills
+ const carouselHTML = commands.map((cmd, i) => `
+
+ `).join('');
+
+ // Build command info cards (one per command, only active one shown)
+ const infoCardsHTML = commands.map((cmd, i) => {
+ const relationship = commandRelationships[cmd.id];
+ let relationshipHTML = '';
+
+ if (relationship) {
+ if (relationship.pairs) {
+ relationshipHTML = `
↔ pairs with /${relationship.pairs}
`;
+ } else if (relationship.leadsTo && relationship.leadsTo.length > 0) {
+ relationshipHTML = `
→ leads to ${relationship.leadsTo.map(c => `/${c}`).join(', ')}
`;
+ }
+ }
+
+ return `
+
+
/${cmd.id}
+
${cmd.description}
+ ${relationshipHTML}
+
+ `;
+ }).join('');
+
+ container.innerHTML = `
+
+
+
+ ${renderCommandDemo(commands[0]?.id || 'audit')}
+
+
+ ${infoCardsHTML}
+
+
+ `;
+
+ setupMobileInteractions(commands);
+}
+
+function setupMobileInteractions(commands) {
+ const pills = document.querySelectorAll('.mobile-cmd-pill');
+ const demoArea = document.getElementById('mobile-demo-content');
+ const infoCards = document.querySelectorAll('.mobile-cmd-info');
+
+ // Initialize first demo's split compare
+ const initialSplit = demoArea.querySelector('.demo-split-comparison');
+ if (initialSplit) {
+ currentSplitInstance = initSplitCompare(initialSplit, {
+ defaultPosition: 50,
+ skewOffset: 6,
+ minPosition: 10,
+ maxPosition: 90
+ });
+ }
+
+ // Pill click/tap handler
+ pills.forEach(pill => {
+ pill.addEventListener('click', () => {
+ const cmdId = pill.dataset.id;
+ const cmd = commands.find(c => c.id === cmdId);
+ if (!cmd || currentCommandId === cmdId) return;
+
+ currentCommandId = cmdId;
+
+ // Update active pill
+ pills.forEach(p => p.classList.remove('active'));
+ pill.classList.add('active');
+
+ // Scroll pill into view horizontally
+ pill.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
+
+ // Update info card
+ infoCards.forEach(card => {
+ card.classList.toggle('active', card.dataset.id === cmdId);
+ });
+
+ // Cleanup previous split
+ if (currentSplitInstance) {
+ currentSplitInstance.destroy();
+ currentSplitInstance = null;
+ }
+
+ // Update demo
+ demoArea.innerHTML = renderCommandDemo(cmdId);
+
+ // Init new split compare
+ const splitComparison = demoArea.querySelector('.demo-split-comparison');
+ if (splitComparison) {
+ currentSplitInstance = initSplitCompare(splitComparison, {
+ defaultPosition: 50,
+ skewOffset: 6,
+ minPosition: 10,
+ maxPosition: 90
+ });
+ }
+ });
+ });
+}