CSS
/* ─── 05 Audio Control Suite — warm parchment audio console ───────── */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,500;0,700;1,400&family=Space+Mono:wght@400;700&display=swap');
.nm-aud {
--nm-aud-bg: #f0e6d3;
--nm-aud-sd: #d0c0a0;
--nm-aud-sl: #fffaf0;
--nm-aud-ib: #e8ddc8;
--nm-aud-acc: #c8783a;
--nm-aud-ac2: #e0a858;
--nm-aud-ac3: #8a4a20;
--nm-aud-txt: #7a6a58;
--nm-aud-txt2:#2a1e10;
position: relative;
width: 100%;
min-height: 760px;
background: var(--nm-aud-bg);
font-family: 'Space Mono', ui-monospace, monospace;
color: var(--nm-aud-txt);
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
overflow: hidden;
box-sizing: border-box;
}
.nm-aud *,
.nm-aud *::before,
.nm-aud *::after { box-sizing: border-box; }
.nm-aud .card {
position: relative;
width: 100%;
max-width: 860px;
background: var(--nm-aud-bg);
border-radius: 44px;
padding: 44px 40px 38px;
box-shadow: 24px 24px 56px var(--nm-aud-sd), -24px -24px 56px var(--nm-aud-sl);
display: flex;
flex-direction: column;
gap: 30px;
}
/* Track header */
.nm-aud .track-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.nm-aud .track-title {
font-family: 'Playfair Display', Georgia, serif;
font-size: 26px;
font-weight: 700;
color: var(--nm-aud-txt2);
letter-spacing: -0.5px;
}
.nm-aud .track-sub {
font-size: 11px;
color: var(--nm-aud-txt);
margin-top: 4px;
letter-spacing: 0.5px;
}
.nm-aud .track-bpm {
background: var(--nm-aud-bg);
border-radius: 12px;
padding: 10px 18px;
box-shadow: inset 5px 5px 10px var(--nm-aud-sd), inset -5px -5px 10px var(--nm-aud-sl);
font-size: 11px;
color: var(--nm-aud-txt);
}
.nm-aud .track-bpm span {
color: var(--nm-aud-acc);
font-weight: 700;
font-size: 15px;
}
/* Waveform */
.nm-aud .waveform-row {
display: flex;
align-items: center;
gap: 3px;
height: 36px;
margin-bottom: 6px;
}
.nm-aud .wf-bar {
flex: 1;
border-radius: 2px;
background: linear-gradient(to top, var(--nm-aud-sd), var(--nm-aud-ib));
opacity: 0.45;
min-height: 3px;
}
.nm-aud .wf-bar.played {
background: linear-gradient(to top, var(--nm-aud-acc), var(--nm-aud-ac2));
opacity: 0.7;
}
/* Scrubber */
.nm-aud .scrubber-wrap { position: relative; }
.nm-aud .scrubber-label-row {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--nm-aud-txt);
margin-bottom: 10px;
letter-spacing: 0.5px;
}
.nm-aud .scrubber-track {
width: 100%;
height: 10px;
border-radius: 5px;
background: var(--nm-aud-ib);
box-shadow: inset 5px 5px 10px var(--nm-aud-sd), inset -5px -5px 10px var(--nm-aud-sl);
position: relative;
cursor: pointer;
}
.nm-aud .scrubber-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 38%;
border-radius: 5px;
background: linear-gradient(90deg, var(--nm-aud-ac3), var(--nm-aud-acc), var(--nm-aud-ac2));
pointer-events: none;
}
.nm-aud .scrubber-glow {
position: absolute;
top: -4px;
left: 0;
height: 18px;
width: 38%;
border-radius: 9px;
background: linear-gradient(90deg, transparent, rgba(200, 120, 58, 0.3));
filter: blur(6px);
pointer-events: none;
}
.nm-aud .scrubber {
position: absolute;
top: -4px;
left: 0;
width: 100%;
height: 18px;
opacity: 0;
cursor: pointer;
margin: 0;
z-index: 10;
}
.nm-aud .scrubber-thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--nm-aud-bg);
box-shadow: 5px 5px 12px var(--nm-aud-sd), -5px -5px 12px var(--nm-aud-sl);
left: 38%;
pointer-events: none;
transition: box-shadow 0.15s;
}
.nm-aud .scrubber-thumb::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 10px;
height: 10px;
border-radius: 50%;
background: linear-gradient(145deg, var(--nm-aud-acc), var(--nm-aud-ac2));
box-shadow: 0 0 6px rgba(200, 120, 58, 0.5);
}
/* Main controls grid */
.nm-aud .main-controls {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 28px;
align-items: center;
}
/* Play controls */
.nm-aud .play-controls {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
flex-wrap: wrap;
}
.nm-aud .ctrl {
background: var(--nm-aud-bg);
border: none;
cursor: pointer;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 7px 7px 15px var(--nm-aud-sd), -7px -7px 15px var(--nm-aud-sl);
transition: all 0.15s ease;
color: var(--nm-aud-txt2);
font-size: 16px;
}
.nm-aud .ctrl:active {
box-shadow: inset 4px 4px 10px var(--nm-aud-sd), inset -4px -4px 10px var(--nm-aud-sl);
}
.nm-aud .ctrl:hover { transform: scale(1.05); }
.nm-aud .ctrl-sm { width: 42px; height: 42px; }
.nm-aud .ctrl-md { width: 52px; height: 52px; font-size: 18px; }
.nm-aud .ctrl-play {
width: 66px;
height: 66px;
font-size: 22px;
background: linear-gradient(145deg, var(--nm-aud-acc), var(--nm-aud-ac3));
color: #fffaf0;
box-shadow: 10px 10px 22px rgba(200, 120, 58, 0.4), -6px -6px 16px var(--nm-aud-sl);
}
.nm-aud .ctrl-play:hover { transform: scale(1.05); }
.nm-aud .ctrl-play:active {
box-shadow: inset 6px 6px 14px rgba(0, 0, 0, 0.2);
transform: scale(0.97);
}
/* Volume knob */
.nm-aud .vol-knob-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.nm-aud .vol-knob {
width: 110px;
height: 110px;
border-radius: 50%;
background: var(--nm-aud-bg);
box-shadow: 14px 14px 32px var(--nm-aud-sd), -14px -14px 32px var(--nm-aud-sl);
position: relative;
cursor: grab;
user-select: none;
}
.nm-aud .vol-knob:active { cursor: grabbing; }
.nm-aud .vol-knob-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 70px;
height: 70px;
border-radius: 50%;
background: var(--nm-aud-ib);
box-shadow: inset 6px 6px 14px var(--nm-aud-sd), inset -6px -6px 14px var(--nm-aud-sl);
}
.nm-aud .vol-knob-dot {
position: absolute;
top: 12px;
left: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--nm-aud-acc);
transform: translateX(-50%);
transform-origin: 3px 43px;
box-shadow: 0 0 8px var(--nm-aud-acc);
transition: transform 0.05s;
}
.nm-aud .vol-tick-svg { position: absolute; inset: 0; }
.nm-aud .mini-lbl {
font-size: 9px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--nm-aud-txt);
}
.nm-aud .vol-val {
font-size: 20px;
font-weight: 700;
color: var(--nm-aud-txt2);
margin-top: -6px;
font-family: 'Space Mono', monospace;
}
/* EQ faders */
.nm-aud .eq-wrap {
display: flex;
gap: 14px;
align-items: flex-end;
justify-content: flex-end;
}
.nm-aud .fader-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.nm-aud .fader {
position: relative;
width: 28px;
height: 110px;
}
.nm-aud .fader-track {
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
width: 8px;
height: 100%;
border-radius: 4px;
background: var(--nm-aud-ib);
box-shadow: inset 3px 3px 7px var(--nm-aud-sd), inset -3px -3px 7px var(--nm-aud-sl);
}
.nm-aud .fader-fill {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 8px;
bottom: 0;
border-radius: 4px;
background: linear-gradient(to top, var(--nm-aud-ac3), var(--nm-aud-acc), var(--nm-aud-ac2));
}
.nm-aud .fader-thumb {
position: absolute;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 20px;
border-radius: 6px;
background: var(--nm-aud-bg);
box-shadow: 4px 4px 8px var(--nm-aud-sd), -4px -4px 8px var(--nm-aud-sl);
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
}
.nm-aud .fader-thumb::before {
content: '';
display: block;
width: 14px;
height: 2px;
border-radius: 1px;
background: var(--nm-aud-sd);
box-shadow: 0 -4px 0 var(--nm-aud-sd), 0 4px 0 var(--nm-aud-sd);
}
.nm-aud .fader-lbl {
font-size: 8px;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--nm-aud-txt);
}
.nm-aud .fader-val {
font-size: 10px;
font-weight: 700;
color: var(--nm-aud-acc);
}
/* Mini knobs */
.nm-aud .knobs-row {
display: flex;
gap: 18px;
align-items: center;
justify-content: center;
padding-top: 8px;
border-top: 1px solid rgba(160, 140, 110, 0.2);
flex-wrap: wrap;
}
.nm-aud .mini-knob-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.nm-aud .mini-knob {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--nm-aud-bg);
box-shadow: 9px 9px 20px var(--nm-aud-sd), -9px -9px 20px var(--nm-aud-sl);
position: relative;
cursor: grab;
}
.nm-aud .mini-knob:active { cursor: grabbing; }
.nm-aud .mini-knob-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--nm-aud-ib);
box-shadow: inset 3px 3px 7px var(--nm-aud-sd), inset -3px -3px 7px var(--nm-aud-sl);
}
.nm-aud .mini-knob-dot {
position: absolute;
top: 6px;
left: 50%;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--nm-aud-acc);
transform: translateX(-50%);
transform-origin: 2.5px 22px;
box-shadow: 0 0 5px var(--nm-aud-acc);
}
.nm-aud .mini-val {
font-size: 11px;
font-weight: 700;
color: var(--nm-aud-txt2);
font-family: 'Space Mono', monospace;
}
@media (max-width: 760px) {
.nm-aud .card { padding: 32px 22px; }
.nm-aud .main-controls { grid-template-columns: 1fr; }
.nm-aud .eq-wrap { justify-content: center; }
.nm-aud .play-controls { justify-content: center; }
} JS
(() => {
const root = document.querySelector('.nm-aud');
if (!root) return;
// ─── Waveform ───
const waveRow = root.querySelector('[data-nm-aud-wave]');
const waveHeights = [6,10,18,14,24,20,32,28,36,30,40,34,42,38,48,52,44,50,40,46,38,42,32,36,28,30,22,26,18,20,14,16,10,12,8,10,14,18,22,28,32,36,30,24,20,16,12,8,6];
let scrubberPct = 0.38;
waveHeights.forEach((h, i) => {
const bar = document.createElement('div');
bar.className = 'wf-bar' + (i < waveHeights.length * scrubberPct ? ' played' : '');
bar.style.height = Math.max(3, h * 0.7) + 'px';
waveRow.appendChild(bar);
});
// ─── Scrubber ───
const input = root.querySelector('[data-nm-aud-scrub]');
const fill = root.querySelector('[data-nm-aud-fill]');
const glow = root.querySelector('[data-nm-aud-glow]');
const thumb = root.querySelector('[data-nm-aud-thumb]');
const timeNow = root.querySelector('[data-nm-aud-time-now]');
const totalSec = 227;
function updateScrubber(val) {
const pct = val / 100;
fill.style.width = (pct * 100) + '%';
glow.style.width = (pct * 100) + '%';
thumb.style.left = (pct * 100) + '%';
const sec = Math.round(pct * totalSec);
timeNow.textContent = Math.floor(sec / 60) + ':' + String(sec % 60).padStart(2, '0');
waveRow.querySelectorAll('.wf-bar').forEach((b, i) => {
b.classList.toggle('played', i < waveHeights.length * pct);
});
}
input.addEventListener('input', () => updateScrubber(+input.value));
// ─── Volume Knob ───
const volKnob = root.querySelector('[data-nm-aud-volknob]');
const volDot = root.querySelector('[data-nm-aud-voldot]');
const volVal = root.querySelector('[data-nm-aud-volval]');
const volTicks = root.querySelector('[data-nm-aud-volticks]');
let volAngle = -45;
let volDragging = false, volStartY = 0, volStartAngle = -45;
// Tick marks
const svgNS = 'http://www.w3.org/2000/svg';
for (let t = 0; t <= 20; t++) {
const frac = t / 20;
const ang = -225 + frac * 270;
const rad = ang * Math.PI / 180;
const r1 = 54, r2 = t % 5 === 0 ? 48 : 51;
const line = document.createElementNS(svgNS, 'line');
line.setAttribute('x1', String(60 + r1 * Math.cos(rad)));
line.setAttribute('y1', String(60 + r1 * Math.sin(rad)));
line.setAttribute('x2', String(60 + r2 * Math.cos(rad)));
line.setAttribute('y2', String(60 + r2 * Math.sin(rad)));
line.setAttribute('stroke', frac < 0.72 ? '#c8783a' : '#d0c0a0');
line.setAttribute('stroke-width', t % 5 === 0 ? '2' : '1');
line.setAttribute('stroke-linecap', 'round');
line.setAttribute('opacity', frac < 0.72 ? '0.8' : '0.4');
volTicks.appendChild(line);
}
function setVol(angle) {
volAngle = Math.max(-140, Math.min(140, angle));
volDot.style.transform = 'translateX(-50%) rotate(' + volAngle + 'deg)';
const pct = Math.round((volAngle + 140) / 280 * 100);
volVal.textContent = pct;
}
setVol(-45);
volKnob.addEventListener('mousedown', e => {
volDragging = true; volStartY = e.clientY; volStartAngle = volAngle;
document.body.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!volDragging) return;
setVol(volStartAngle - (e.clientY - volStartY) * 1.2);
});
document.addEventListener('mouseup', () => {
if (volDragging) { volDragging = false; document.body.style.cursor = ''; }
});
// ─── EQ Faders ───
const eqData = [
{ lbl: '32Hz', val: 68 },
{ lbl: '250Hz', val: 55 },
{ lbl: '1kHz', val: 80 },
{ lbl: '4kHz', val: 62 },
{ lbl: '16kHz', val: 44 },
];
const eqWrap = root.querySelector('[data-nm-aud-eq]');
eqData.forEach(({ lbl, val }) => {
const h = 110;
const fillH = Math.round(val / 100 * h);
const g = document.createElement('div');
g.className = 'fader-group';
g.innerHTML =
'<div class="fader-val">' + (val >= 50 ? '+' : '') + (val - 50) + '</div>' +
'<div class="fader">' +
' <div class="fader-track"></div>' +
' <div class="fader-fill" style="height:' + fillH + 'px"></div>' +
' <div class="fader-thumb" style="bottom:' + (fillH - 10) + 'px"></div>' +
'</div>' +
'<div class="fader-lbl">' + lbl + '</div>';
eqWrap.appendChild(g);
const thumb2 = g.querySelector('.fader-thumb');
const fill2 = g.querySelector('.fader-fill');
const valEl = g.querySelector('.fader-val');
let dragging = false, startY2 = 0, startPct = val / 100;
thumb2.addEventListener('mousedown', e => {
dragging = true; startY2 = e.clientY;
startPct = parseInt(fill2.style.height, 10) / h;
document.body.style.cursor = 'ns-resize';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
const delta = (startY2 - e.clientY) / h;
const pct = Math.max(0, Math.min(1, startPct + delta));
const fh = Math.round(pct * h);
fill2.style.height = fh + 'px';
thumb2.style.bottom = (fh - 10) + 'px';
const v = Math.round(pct * 100);
valEl.textContent = (v >= 50 ? '+' : '') + (v - 50);
});
document.addEventListener('mouseup', () => {
if (dragging) { dragging = false; document.body.style.cursor = ''; }
});
});
// ─── Mini Knobs ───
const knobData = [
{ lbl: 'Bass', val: -20 },
{ lbl: 'Treble', val: 30 },
{ lbl: 'Reverb', val: -5 },
{ lbl: 'Delay', val: 15 },
{ lbl: 'Width', val: 50 },
{ lbl: 'Drive', val: -30 },
];
const knobsRow = root.querySelector('[data-nm-aud-knobs]');
knobData.forEach(({ lbl, val }) => {
const g = document.createElement('div');
g.className = 'mini-knob-group';
g.innerHTML =
'<div class="mini-knob">' +
' <div class="mini-knob-inner"></div>' +
' <div class="mini-knob-dot"></div>' +
'</div>' +
'<div class="mini-lbl">' + lbl + '</div>' +
'<div class="mini-val">' + (val > 0 ? '+' : '') + val + '</div>';
knobsRow.appendChild(g);
const dot = g.querySelector('.mini-knob-dot');
const valEl2 = g.querySelector('.mini-val');
let angle = val;
dot.style.transform = 'translateX(-50%) rotate(' + angle + 'deg)';
let drag = false, sy = 0, sa = angle;
g.querySelector('.mini-knob').addEventListener('mousedown', e => {
drag = true; sy = e.clientY; sa = angle;
document.body.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!drag) return;
angle = Math.max(-140, Math.min(140, sa - (e.clientY - sy) * 1.5));
dot.style.transform = 'translateX(-50%) rotate(' + angle + 'deg)';
const v = Math.round(angle);
valEl2.textContent = (v > 0 ? '+' : '') + v;
});
document.addEventListener('mouseup', () => {
if (drag) { drag = false; document.body.style.cursor = ''; }
});
});
// ─── Play toggle ───
let playing = false;
const playBtn = root.querySelector('[data-nm-aud-play]');
playBtn.addEventListener('click', () => {
playing = !playing;
playBtn.textContent = playing ? '⏸' : '▶';
});
})();