Files
pi-multifx-pedal-ui/docs/helix-stadium-concept.html
shawn f42fa1d28c fix: replace Inter with Space Grotesk (#audit-P3)
Inter was flagged as the only AI tell in the audit. Space Grotesk
fits the pedal brand — mechanical, precise character with excellent
small-screen readability.

Changes:
- src/index.css: body font-family Inter → Space Grotesk
- src/App.jsx: Google Fonts import + body font-family + inline style
- src/GlobalSettings.jsx, GlobalEQ.jsx, ModelBrowser.jsx, FocusView.jsx:
  inline fontFamily Inter → Space Grotesk
- index.html: added Google Fonts preconnect + Space Grotesk import
- docs/helix-stadium-mockup.html, docs/helix-stadium-concept.html: updated
  font imports and font-family
2026-06-14 17:26:03 -04:00

812 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NAM Pedal — Helix Stadium UX Concept</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-tap-highlight-color: transparent; }
:root {
--bg: #0A0A0C;
--panel: #141418;
--surface: #1C1C22;
--border: #2A2A32;
--amber: #E8A030;
--amberDim: #7A5218;
--blue: #3A7BA8;
--blueDim: #1E4060;
--green: #3AB87A;
--red: #C84040;
--text: #F0EDE6;
--textSec: #8888A0;
--textDim: #444458;
}
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Space Grotesk', sans-serif;
overflow: hidden;
}
/* ── Main Layout ── */
.pedal {
max-width: 800px;
margin: 0 auto;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg);
}
/* ── Status Bar ── */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 14px;
background: var(--panel);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.status-left { display: flex; align-items: center; gap: 10px; }
.status-led {
width: 8px; height: 8px; border-radius: 50%;
background: var(--green);
box-shadow: 0 0 8px var(--green);
}
.status-text { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--textSec); letter-spacing: .08em; }
.status-right { display: flex; align-items: center; gap: 14px; }
.status-chip {
font-family: 'JetBrains Mono', monospace; font-size: 10px;
padding: 3px 8px; border-radius: 4px;
background: rgba(58,123,168,.15); color: var(--blue);
border: 1px solid rgba(58,123,168,.3);
}
/* ── Scribble Strip / Info Bar ── */
.scribble-bar {
display: flex;
padding: 8px 14px;
background: #0F0F14;
border-bottom: 1px solid var(--border);
gap: 6px;
flex-shrink: 0;
}
.scribble {
flex: 1;
padding: 6px 10px;
border-radius: 5px;
background: var(--panel);
border: 1px solid var(--border);
text-align: center;
}
.scribble .label { font-size: 9px; color: var(--textDim); letter-spacing: .06em; text-transform: uppercase; }
.scribble .value { font-family: 'JetBrains Mono', monospace; font-size: 13px; font-weight: 600; color: var(--amber); margin-top: 1px; }
/* ── Block Chain ── */
.chain-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chain-scroll {
display: flex;
gap: 6px;
padding: 14px 12px;
overflow-x: auto;
flex-shrink: 0;
align-items: stretch;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.chain-scroll::-webkit-scrollbar { height: 4px; }
.chain-scroll::-webkit-scrollbar-track { background: transparent; }
.chain-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.chain-term {
display: flex;
align-items: center;
justify-content: center;
padding: 0 12px;
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 700;
letter-spacing: .1em;
color: var(--blue);
background: var(--blueDim);
border: 1px solid rgba(58,123,168,.4);
border-radius: 8px;
min-height: 72px;
flex-shrink: 0;
}
.chain-block {
min-width: 110px;
max-width: 140px;
flex-shrink: 0;
background: var(--panel);
border: 2px solid var(--border);
border-radius: 10px;
display: flex;
flex-direction: column;
cursor: pointer;
transition: all .15s;
position: relative;
}
.chain-block.selected {
border-color: var(--amber);
box-shadow: 0 0 12px rgba(232,160,48,.3);
}
.chain-block.bypassed { opacity: .45; }
.chain-block .block-led {
width: 6px; height: 6px; border-radius: 50%;
position: absolute;
top: 8px; right: 8px;
}
.block-led.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
.block-led.off { background: var(--textDim); }
.chain-block .block-scribble {
padding: 8px 10px 4px;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.block-scribble .block-type {
font-size: 9px;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--textDim);
margin-bottom: 2px;
}
.block-scribble .block-name {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
}
.block-scribble .block-sub {
font-size: 10px;
color: var(--textSec);
margin-top: 2px;
}
.chain-block .block-indicator {
height: 3px;
border-radius: 0 0 8px 8px;
margin-top: auto;
}
.chain-arrow {
display: flex;
align-items: center;
flex-shrink: 0;
color: var(--textDim);
font-size: 18px;
padding: 0 2px;
}
/* ── Parameter Panel ── */
.param-panel {
background: var(--panel);
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 12px 14px 14px;
flex-shrink: 0;
}
.param-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.param-header h3 {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--textSec);
}
.param-header .block-label {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
color: var(--amber);
}
.param-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 20px;
}
.param-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.param-group .param-label {
display: flex;
justify-content: space-between;
font-size: 11px;
}
.param-label .name { color: var(--textSec); text-transform: uppercase; letter-spacing: .05em; }
.param-label .value { font-family: 'JetBrains Mono', monospace; color: var(--amber); font-weight: 600; }
.touch-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 32px;
border-radius: 6px;
background: var(--surface);
border: 1px solid var(--border);
outline: none;
cursor: pointer;
transition: border-color .15s;
}
.touch-slider::-webkit-slider-runnable-track {
height: 32px;
border-radius: 6px;
background: linear-gradient(to right, var(--amberDim) 0%, var(--amber) 100%);
background-size: var(--pct, 50%) 100%;
background-repeat: no-repeat;
background-color: var(--surface);
}
.touch-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--amber);
border: 2px solid var(--bg);
box-shadow: 0 1px 6px rgba(0,0,0,.5);
margin-top: 1px;
cursor: pointer;
transition: transform .1s;
}
.touch-slider::-webkit-slider-thumb:active {
transform: scale(1.12);
}
.touch-slider::-moz-range-track {
height: 32px;
border-radius: 6px;
background: var(--surface);
}
.touch-slider::-moz-range-thumb {
width: 28px;
height: 28px;
border-radius: 6px;
background: var(--amber);
border: 2px solid var(--bg);
cursor: pointer;
}
/* ── Footswitch Bar ── */
.footswitch-bar {
display: flex;
gap: 6px;
padding: 10px 12px 14px;
background: var(--panel);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.footswitch {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px 6px;
border-radius: 10px;
border: 2px solid var(--border);
background: var(--surface);
cursor: pointer;
transition: all .12s;
position: relative;
min-height: 68px;
}
.footswitch:active {
transform: scale(.95);
border-color: var(--amber);
}
.footswitch.active {
border-color: var(--amber);
background: rgba(232,160,48,.1);
}
.footswitch.preset {
border-color: var(--blueDim);
}
.footswitch.preset.active {
border-color: var(--blue);
}
.footswitch .fs-scribble {
font-size: 9px;
color: var(--textDim);
text-transform: uppercase;
letter-spacing: .06em;
margin-bottom: 2px;
}
.footswitch .fs-label {
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.footswitch .fs-sub {
font-size: 9px;
color: var(--textSec);
margin-top: 1px;
}
.footswitch .fs-led {
width: 5px; height: 5px; border-radius: 50%;
position: absolute;
top: 6px; right: 6px;
}
.fs-led.green { background: var(--green); }
.fs-led.amber { background: var(--amber); }
.fs-led.off { background: var(--textDim); }
.footswitch .fs-badge {
position: absolute;
top: 5px; left: 5px;
font-size: 8px;
padding: 1px 5px;
border-radius: 3px;
background: rgba(58,123,168,.2);
color: var(--blue);
}
/* ── Animations ── */
@keyframes pulse-amber { 0%,100%{opacity:1} 50%{opacity:.6} }
.recording { animation: pulse-amber 1s infinite; }
/* ── VU Meter ── */
.vu-strip {
display: flex;
gap: 3px;
align-items: flex-end;
height: 28px;
}
.vu-seg {
width: 4px;
border-radius: 1px;
transition: background .05s;
}
/* ── Focus / Active styles for touch ── */
.touch-target {
cursor: pointer;
transition: transform .1s;
}
.touch-target:active {
transform: scale(.96);
}
/* ── Preset Selector ── */
.preset-strip {
display: flex;
gap: 6px;
padding: 8px 14px;
flex-shrink: 0;
background: rgba(0,0,0,.3);
border-bottom: 1px solid var(--border);
}
.preset-chip {
padding: 6px 12px;
border-radius: 6px;
background: var(--surface);
border: 1px solid var(--border);
text-align: center;
cursor: pointer;
transition: all .12s;
flex: 1;
}
.preset-chip.active {
border-color: var(--amber);
background: rgba(232,160,48,.1);
}
.preset-chip .pc-name { font-size: 11px; font-weight: 500; }
.preset-chip .pc-num { font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--textDim); }
/* ── Responsive: slim down for smaller screens ── */
@media (max-width: 480px) {
.chain-block { min-width: 90px; max-width: 110px; }
.footswitch { min-height: 58px; padding: 8px 4px; }
.footswitch .fs-label { font-size: 11px; }
.param-grid { grid-template-columns: 1fr; gap: 8px; }
.preset-chip .pc-name { font-size: 10px; }
}
</style>
</head>
<body>
<div class="pedal">
<!-- Status Bar -->
<div class="status-bar">
<div class="status-left">
<div class="status-led"></div>
<span class="status-text">CONNECTED</span>
<span class="status-chip">● Plexi Crunch</span>
</div>
<div class="status-right">
<div class="vu-strip" id="vuL"></div>
<span class="status-text" style="color:var(--textDim);font-size:10px;">CPU 23%</span>
</div>
</div>
<!-- Scribble Strip Info Bar -->
<div class="scribble-bar">
<div class="scribble"><div class="label">Preset</div><div class="value">Plexi Crunch</div></div>
<div class="scribble"><div class="label">Bank</div><div class="value">A · 01</div></div>
<div class="scribble"><div class="label">Routing</div><div class="value">MONO</div></div>
<div class="scribble"><div class="label">Master</div><div class="value">72%</div></div>
</div>
<!-- Preset Strip -->
<div class="preset-strip">
<div class="preset-chip" onclick="selectPreset(this)">
<div class="pc-name">Plexi Crunch</div>
<div class="pc-num">01</div>
</div>
<div class="preset-chip active" onclick="selectPreset(this)">
<div class="pc-name">Modern Hi-Gain</div>
<div class="pc-num">02</div>
</div>
<div class="preset-chip" onclick="selectPreset(this)">
<div class="pc-name">Clean Chime</div>
<div class="pc-num">03</div>
</div>
<div class="preset-chip" onclick="selectPreset(this)">
<div class="pc-name">Blues Drive</div>
<div class="pc-num">04</div>
</div>
</div>
<!-- Block Chain Area -->
<div class="chain-area">
<div class="chain-scroll" id="chainScroll">
<div class="chain-term">IN</div>
<div class="chain-arrow"></div>
<div class="chain-block selected" data-block="gate" onclick="selectBlock(this)">
<div class="block-led on"></div>
<div class="block-scribble">
<div class="block-type">Dynamics</div>
<div class="block-name">Noise Gate</div>
<div class="block-sub">Threshold 40%</div>
</div>
<div class="block-indicator" style="background:var(--green);"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-block" data-block="drive" onclick="selectBlock(this)">
<div class="block-led on"></div>
<div class="block-scribble">
<div class="block-type">Drive</div>
<div class="block-name">OD-808</div>
<div class="block-sub">Drive 65%</div>
</div>
<div class="block-indicator" style="background:var(--amber);"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-block bypassed" data-block="dist" onclick="selectBlock(this)">
<div class="block-led off"></div>
<div class="block-scribble">
<div class="block-type">Distortion</div>
<div class="block-name">RAT</div>
<div class="block-sub">— Bypassed —</div>
</div>
<div class="block-indicator" style="background:var(--textDim);"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-block" data-block="mod" onclick="selectBlock(this)">
<div class="block-led on"></div>
<div class="block-scribble">
<div class="block-type">Modulation</div>
<div class="block-name">Phase 90</div>
<div class="block-sub">Rate 35%</div>
</div>
<div class="block-indicator" style="background:var(--blue);"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-block" data-block="delay" onclick="selectBlock(this)">
<div class="block-led on"></div>
<div class="block-scribble">
<div class="block-type">Delay</div>
<div class="block-name">Digital Delay</div>
<div class="block-sub">450ms · 25%</div>
</div>
<div class="block-indicator" style="background:#A060E0;"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-block" data-block="reverb" onclick="selectBlock(this)">
<div class="block-led on"></div>
<div class="block-scribble">
<div class="block-type">Reverb</div>
<div class="block-name">Hall Reverb</div>
<div class="block-sub">Decay 60%</div>
</div>
<div class="block-indicator" style="background:#60A0E0;"></div>
</div>
<div class="chain-arrow"></div>
<div class="chain-term">OUT</div>
</div>
<!-- Parameter Panel -->
<div class="param-panel" id="paramPanel">
<div class="param-header">
<h3>Noise Gate <span class="block-label">· Dynamics</span></h3>
<span style="color:var(--green);font-size:11px;display:flex;align-items:center;gap:4px;">
<span style="width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block;"></span> Active
</span>
</div>
<div class="param-grid">
<div class="param-group">
<div class="param-label">
<span class="name">Threshold</span>
<span class="value" id="val_thresh">40</span>
</div>
<input type="range" class="touch-slider" min="0" max="100" value="40" oninput="updateSlider(this,'val_thresh')" style="--pct:40%">
</div>
<div class="param-group">
<div class="param-label">
<span class="name">Release</span>
<span class="value" id="val_rel">25</span>
</div>
<input type="range" class="touch-slider" min="0" max="100" value="25" oninput="updateSlider(this,'val_rel')" style="--pct:25%">
</div>
<div class="param-group">
<div class="param-label">
<span class="name">Depth</span>
<span class="value" id="val_dep">70</span>
</div>
<input type="range" class="touch-slider" min="0" max="100" value="70" oninput="updateSlider(this,'val_dep')" style="--pct:70%">
</div>
<div class="param-group">
<div class="param-label">
<span class="name">Mix</span>
<span class="value" id="val_mix">100</span>
</div>
<input type="range" class="touch-slider" min="0" max="100" value="100" oninput="updateSlider(this,'val_mix')" style="--pct:100%">
</div>
</div>
</div>
</div>
<!-- Footswitch Bar -->
<div class="footswitch-bar">
<div class="footswitch preset" onclick="toggleFS(this)">
<span class="fs-scribble">BANK</span>
<span class="fs-label">▲ A</span>
<span class="fs-sub">Bank Up</span>
</div>
<div class="footswitch active" data-block="gate" onclick="toggleBlock(this,'gate')">
<span class="fs-scribble">GATE</span>
<span class="fs-label">Noise Gate</span>
<span class="fs-sub">Threshold 40%</span>
<div class="fs-led green"></div>
</div>
<div class="footswitch" data-block="drive" onclick="toggleBlock(this,'drive')">
<span class="fs-scribble">DRIVE</span>
<span class="fs-label">OD-808</span>
<span class="fs-sub">Drive 65%</span>
<div class="fs-led green"></div>
</div>
<div class="footswitch" data-block="dist" onclick="toggleBlock(this,'dist')">
<span class="fs-scribble">DIST</span>
<span class="fs-label" style="color:var(--textDim);">RAT</span>
<span class="fs-sub" style="color:var(--textDim);">Bypassed</span>
<div class="fs-led off"></div>
</div>
<div class="footswitch" data-block="mod" onclick="selectBlockFromFS(this,'mod')">
<span class="fs-scribble">MOD</span>
<span class="fs-label">Phase 90</span>
<span class="fs-sub">Rate 35%</span>
<div class="fs-led green"></div>
</div>
<div class="footswitch preset" onclick="toggleFS(this)">
<span class="fs-scribble">BANK</span>
<span class="fs-label">▼ A</span>
<span class="fs-sub">Bank Down</span>
</div>
</div>
</div>
<script>
// ── Block Chain Selection ──
function selectBlock(el) {
document.querySelectorAll('.chain-block').forEach(b => b.classList.remove('selected'));
el.classList.add('selected');
// Update footswitch bar
document.querySelectorAll('.footswitch[data-block]').forEach(f => f.classList.remove('active'));
const blockId = el.dataset.block;
const fs = document.querySelector(`.footswitch[data-block="${blockId}"]`);
if (fs) fs.classList.add('active');
// Update parameter panel (simple demo)
const name = el.querySelector('.block-name').textContent;
const type = el.querySelector('.block-type').textContent;
const header = document.querySelector('.param-header h3');
header.innerHTML = `${name} <span class="block-label">· ${type}</span>`;
// Update param panel content based on block
const params = {
gate: [
['Threshold', '40', 0, 100],
['Release', '25', 0, 100],
['Depth', '70', 0, 100],
['Mix', '100', 0, 100],
],
drive: [
['Drive', '65', 0, 100],
['Tone', '55', 0, 100],
['Level', '70', 0, 100],
['Tight', '30', 0, 100],
],
dist: [
['Distortion', '75', 0, 100],
['Filter', '50', 0, 100],
['Volume', '65', 0, 100],
['Gain', '80', 0, 100],
],
mod: [
['Rate', '35', 0, 100],
['Depth', '60', 0, 100],
['Feedback', '40', 0, 100],
['Mix', '50', 0, 100],
],
delay: [
['Time', '450', 20, 2000],
['Feedback', '25', 0, 100],
['Mix', '40', 0, 100],
['Low Cut', '80', 20, 500],
],
reverb: [
['Decay', '60', 0, 100],
['Pre-Delay', '30', 0, 200],
['Damping', '50', 0, 100],
['Mix', '35', 0, 100],
],
};
const p = params[blockId] || params.gate;
const grid = document.querySelector('.param-grid');
grid.innerHTML = p.map(([name, val, min, max], i) => {
const id = `p${i}`;
const pct = Math.round((val / max) * 100);
return `<div class="param-group">
<div class="param-label">
<span class="name">${name}</span>
<span class="value" id="${id}">${val}</span>
</div>
<input type="range" class="touch-slider" min="${min}" max="${max}" value="${val}" oninput="updateSlider(this,'${id}')" style="--pct:${pct}%">
</div>`;
}).join('');
}
function updateSlider(el, valId) {
const val = el.value;
document.getElementById(valId).textContent = val;
const min = parseInt(el.min);
const max = parseInt(el.max);
const pct = Math.round(((val - min) / (max - min)) * 100);
el.style.setProperty('--pct', pct + '%');
}
// ── Footswitch Interaction ──
function toggleBlock(el, blockId) {
// Toggle bypass
const chainBlock = document.querySelector(`.chain-block[data-block="${blockId}"]`);
if (chainBlock) {
chainBlock.classList.toggle('bypassed');
const led = chainBlock.querySelector('.block-led');
const indicator = chainBlock.querySelector('.block-indicator');
if (chainBlock.classList.contains('bypassed')) {
led.className = 'block-led off';
indicator.style.background = 'var(--textDim)';
el.querySelector('.fs-label').style.color = 'var(--textDim)';
el.querySelector('.fs-sub').textContent = 'Bypassed';
el.querySelector('.fs-sub').style.color = 'var(--textDim)';
el.querySelector('.fs-led').className = 'fs-led off';
} else {
led.className = 'block-led on';
// Restore original indicator color
const colors = {gate:'var(--green)',drive:'var(--amber)',dist:'var(--red)',mod:'var(--blue)',delay:'#A060E0',reverb:'#60A0E0'};
indicator.style.background = colors[blockId] || 'var(--green)';
el.querySelector('.fs-label').style.color = '';
el.querySelector('.fs-sub').style.color = '';
// Restore sub text from chain block
const sub = chainBlock.querySelector('.block-sub');
el.querySelector('.fs-sub').textContent = sub.textContent;
el.querySelector('.fs-led').className = 'fs-led green';
}
}
el.classList.toggle('active');
}
function selectBlockFromFS(el, blockId) {
// Select block in chain
const chainBlock = document.querySelector(`.chain-block[data-block="${blockId}"]`);
if (chainBlock) selectBlock(chainBlock);
}
function toggleFS(el) {
el.classList.toggle('active');
}
function selectPreset(el) {
document.querySelectorAll('.preset-chip').forEach(c => c.classList.remove('active'));
el.classList.add('active');
}
// ── VU Meter Animation ──
(function animateVU() {
const vu = document.getElementById('vuL');
const segs = 8;
vu.innerHTML = '';
for (let i = 0; i < segs; i++) {
const seg = document.createElement('div');
seg.className = 'vu-seg';
seg.style.height = '100%';
seg.style.flex = '1';
vu.appendChild(seg);
}
function tick() {
const level = Math.random();
const active = Math.floor(level * segs);
const segs_ = vu.querySelectorAll('.vu-seg');
segs_.forEach((seg, i) => {
const on = i < active;
const color = i >= 6 ? '#C84040' : i >= 4 ? '#E8C030' : '#3AB87A';
seg.style.background = on ? color : '#2A2A32';
seg.style.boxShadow = on ? `0 0 4px ${color}` : 'none';
});
}
setInterval(tick, 100);
})();
// ── Auto-scroll to show chain fully ──
window.addEventListener('load', () => {
const scroll = document.getElementById('chainScroll');
if (scroll) scroll.scrollLeft = 120;
});
// ── Touch feedback ──
document.addEventListener('touchstart', () => {}, {passive: true});
</script>
</body>
</html>