.valves-container display: flex; justify-content: space-between; gap: 2rem; flex-wrap: wrap; margin-bottom: 2rem;
.valve-title font-size: 1.5rem; font-weight: bold; margin: 0.5rem 0; lub dub valves
.valve.active .valve-icon transform: scale(1.2); .valves-container display: flex
function playDub() if (audioCtx.state === 'suspended') audioCtx.resume(); const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); osc.frequency.value = 210; // sharper gain.gain.value = 0.4; gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.22); osc.start(); osc.stop(now + 0.22); // short high ping const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.connect(gain2); gain2.connect(audioCtx.destination); osc2.frequency.value = 430; gain2.gain.value = 0.12; gain2.gain.exponentialRampToValueAtTime(0.0001, now + 0.16); osc2.start(); osc2.stop(now + 0.16); .valve-title font-size: 1.5rem
// ----- Animation & Valve Activation ----- const lubValveDiv = document.getElementById('lubValve'); const dubValveDiv = document.getElementById('dubValve'); const statusSpan = document.getElementById('statusMsg');
// ----- Auto Cycle (normal lub-dub rhythm) ----- let autoInterval = null; let isAuto = false; const bpmSlider = document.getElementById('bpmSlider'); const bpmValueSpan = document.getElementById('bpmValue');
function activateLub() lubValveDiv.classList.add('active'); setTimeout(() => lubValveDiv.classList.remove('active'), 180);