diff --git a/package-lock.json b/package-lock.json
index aefdee0..8f2f941 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "neon-equalizer",
- "version": "2.1.0",
+ "version": "2.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neon-equalizer",
- "version": "2.1.0",
+ "version": "2.1.1",
"dependencies": {
"adm-zip": "^0.5.17",
"electron-updater": "^6.8.3"
@@ -2238,9 +2238,9 @@
}
},
"node_modules/@xmldom/xmldom": {
- "version": "0.8.12",
- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz",
- "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==",
+ "version": "0.8.13",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz",
+ "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/package.json b/package.json
index 95c9dd1..e990fd5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "neon-equalizer",
- "version": "2.1.0",
+ "version": "2.1.1",
"description": "Professional Neon Equalizer GUI for Equalizer APO with AutoEQ and Squiglink integration",
"keywords": [
"equalizer-apo",
diff --git a/src/index.css b/src/index.css
index ff201c3..4542905 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1533,6 +1533,13 @@ body.panel-collapsed .graph-resize-handle { display: none; }
font-size: 11px;
white-space: nowrap;
}
+.tool-reset-action {
+ height: 26px;
+ padding: 0 10px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+.tool-header-action + .tool-reset-action { margin-left: 4px; }
.tool-hint,
.tool-empty {
font-size: 11px;
@@ -2522,6 +2529,16 @@ body.panel-collapsed .graph-resize-handle { display: none; }
text-transform: uppercase;
box-shadow: 0 12px 28px rgba(0,212,255,0.16);
}
+.aeq-actions {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 8px;
+ align-items: stretch;
+}
+.aeq-actions .btn-ghost {
+ min-width: 76px;
+ justify-content: center;
+}
.aeq-run-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
@@ -3156,9 +3173,16 @@ body.help-mode [data-help] {
flex-shrink: 0;
flex-wrap: wrap;
}
-.surround-title-row { display: flex; flex-direction: column; gap: 2px; flex-shrink: 0; }
+.surround-title-row { display: flex; flex-direction: column; gap: 2px; flex: 1 1 300px; min-width: 240px; }
.surround-title { font-size: 13px; font-weight: 700; color: var(--text-primary); }
.surround-desc { font-size: 11px; color: var(--text-muted); max-width: 360px; }
+.surround-reset-btn {
+ height: 30px;
+ padding: 0 11px;
+ margin-left: auto;
+ font-size: 11px;
+ flex-shrink: 0;
+}
/* Mode toggle (Physical / HRTF) */
.surround-mode-toggle {
@@ -3222,6 +3246,23 @@ body.help-mode [data-help] {
.surround-preset-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.surround-preset-btn.active { background: var(--cyan-dim); border-color: var(--cyan); color: var(--cyan); }
+.section-title-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+.section-title-row .section-title {
+ margin: 0;
+}
+.mini-reset-btn {
+ height: 26px;
+ padding: 0 10px;
+ font-size: 11px;
+ white-space: nowrap;
+}
+
.surround-body {
display: flex;
flex: 1;
@@ -3430,6 +3471,16 @@ body.help-mode [data-help] {
border-radius: 3px;
color: var(--cyan);
}
+.inline-link {
+ color: var(--cyan);
+ font-weight: 700;
+ text-decoration: none;
+ border-bottom: 1px solid rgba(0, 212, 255, 0.36);
+}
+.inline-link:hover {
+ color: var(--text-primary);
+ border-bottom-color: var(--text-primary);
+}
/* HRTF mode */
.surround-hrtf-left {
diff --git a/src/main.js b/src/main.js
index 0b32640..a017b85 100644
--- a/src/main.js
+++ b/src/main.js
@@ -491,6 +491,7 @@ document.addEventListener('DOMContentLoaded', () => {
initHIDPanel();
initAdvancedPanel();
initAboutPanel();
+ initExternalLinks();
initHelpSystem();
initUpdateChecker();
detectAPO();
@@ -645,16 +646,28 @@ const SURROUND_LAYOUTS = {
],
};
-let surroundState = {
- layout: '5.1',
- channels: {},
-};
+function createDefaultSurroundState() {
+ return {
+ mode: 'speakers',
+ layout: '5.1',
+ channels: {},
+ hrtfPath: '',
+ hrtfPreamp: -6,
+ hrtfCrossfeed: 0,
+ };
+}
+
+let surroundState = createDefaultSurroundState();
function surroundLoadState() {
try {
const saved = JSON.parse(localStorage.getItem(SURROUND_KEY) || '{}');
+ if (saved.mode === 'speakers' || saved.mode === 'hrtf') surroundState.mode = saved.mode;
if (saved.layout && SURROUND_LAYOUTS[saved.layout]) surroundState.layout = saved.layout;
if (saved.channels) surroundState.channels = saved.channels;
+ if (typeof saved.hrtfPath === 'string') surroundState.hrtfPath = saved.hrtfPath;
+ if (Number.isFinite(Number(saved.hrtfPreamp))) surroundState.hrtfPreamp = Number(saved.hrtfPreamp);
+ if (Number.isFinite(Number(saved.hrtfCrossfeed))) surroundState.hrtfCrossfeed = Number(saved.hrtfCrossfeed);
} catch (_) {}
}
@@ -808,6 +821,31 @@ function updateHrtfPreview() {
if (el) el.textContent = buildHrtfConfigText();
}
+function resetSurroundPanel() {
+ surroundState = createDefaultSurroundState();
+ surroundSaveState();
+ document.querySelectorAll('.surround-preset-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.layout === surroundState.layout);
+ });
+ surroundSwitchMode('speakers');
+ const nameEl = document.getElementById('hrtf-file-name');
+ if (nameEl) {
+ nameEl.textContent = 'No file selected';
+ nameEl.style.color = '';
+ }
+ const cfSlider = document.getElementById('hrtf-crossfeed');
+ const cfVal = document.getElementById('hrtf-crossfeed-val');
+ if (cfSlider) cfSlider.value = String(surroundState.hrtfCrossfeed);
+ if (cfVal) cfVal.textContent = `${surroundState.hrtfCrossfeed}%`;
+ const preampInput = document.getElementById('hrtf-preamp');
+ if (preampInput) preampInput.value = String(surroundState.hrtfPreamp);
+ renderSurroundRoom();
+ renderSurroundChannels();
+ updateHrtfPreview();
+ markDirty();
+ showToast('Surround settings reset', 'success');
+}
+
function initSurroundPanel() {
surroundLoadState();
@@ -815,6 +853,7 @@ function initSurroundPanel() {
document.querySelectorAll('.surround-mode-btn').forEach(btn => {
btn.addEventListener('click', () => surroundSwitchMode(btn.dataset.mode));
});
+ document.getElementById('btn-surround-reset')?.addEventListener('click', resetSurroundPanel);
surroundSwitchMode(surroundState.mode || 'speakers');
// Layout presets
@@ -2002,6 +2041,15 @@ async function openExternalUrl(url) {
window.open(url, '_blank', 'noopener');
}
+function initExternalLinks() {
+ document.querySelectorAll('[data-external-link]').forEach(link => {
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+ openExternalUrl(link.href);
+ });
+ });
+}
+
// ═══════════════════════════════════════════════════════════
// PARAMETRIC EQ
// ═══════════════════════════════════════════════════════════
@@ -2020,6 +2068,16 @@ function initParametricEQ() {
parametricEQ.addFilter();
});
+ document.getElementById('btn-reset-parametric')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.filters = [];
+ appState.config.preamp = 0;
+ refreshUI();
+ markDirty();
+ audioPlayer?.refreshEQ();
+ showToast('Parametric EQ reset', 'success');
+ });
+
document.getElementById('filter-preset-select').addEventListener('change', (e) => {
if (e.target.value) {
parametricEQ.loadPreset(e.target.value);
@@ -2852,6 +2910,40 @@ function initAutoEQControls() {
const intensityEl = document.getElementById('aeq-intensity');
const intensityVal = document.getElementById('aeq-intensity-val');
+ const setAutoEQDefaults = () => {
+ const values = {
+ 'aeq-max-filters': '10',
+ 'aeq-normalization': 'auto',
+ 'aeq-region': 'custom',
+ 'aeq-freq-min': '20',
+ 'aeq-freq-max': '16000',
+ 'aeq-intensity': '100',
+ 'aeq-rolloff-freq': '45',
+ 'aeq-rolloff-gain': '-6',
+ 'aeq-q-min': '0.4',
+ 'aeq-q-max': '4.0',
+ 'aeq-gain-min': '-16',
+ 'aeq-gain-max': '16',
+ };
+ Object.entries(values).forEach(([id, value]) => {
+ const el = document.getElementById(id);
+ if (el) el.value = value;
+ });
+ const shelves = document.getElementById('aeq-use-shelf-filter');
+ const smooth = document.getElementById('aeq-smooth-input');
+ const rolloff = document.getElementById('aeq-subbass-rolloff');
+ if (shelves) shelves.checked = true;
+ if (smooth) smooth.checked = true;
+ if (rolloff) rolloff.checked = false;
+ if (intensityVal) intensityVal.textContent = '100%';
+ if (statusEl) statusEl.style.display = 'none';
+ };
+
+ document.getElementById('btn-aeq-reset')?.addEventListener('click', () => {
+ setAutoEQDefaults();
+ showToast('AutoEQ parameters reset', 'success');
+ });
+
regionSelect?.addEventListener('change', () => {
const range = regionRanges[regionSelect.value];
if (!range) return;
@@ -3474,16 +3566,28 @@ function initConvolution() {
if (file) setConvolutionFile(file);
});
document.getElementById('btn-remove-ir')?.addEventListener('click', () => {
- appState.config.convolution = null;
- document.getElementById('ir-drop-zone').style.display = '';
- document.getElementById('ir-file-info').style.display = 'none';
+ clearConvolutionFile();
markDirty();
});
+ document.getElementById('btn-reset-ir')?.addEventListener('click', () => {
+ pushUndo();
+ clearConvolutionFile();
+ markDirty();
+ showToast('Impulse response cleared', 'success');
+ });
document.getElementById('ir-enabled')?.addEventListener('change', (e) => {
if (appState.config.convolution) { appState.config.convolution.enabled = e.target.checked; markDirty(); }
});
}
+function clearConvolutionFile() {
+ appState.config.convolution = null;
+ const drop = document.getElementById('ir-drop-zone');
+ const info = document.getElementById('ir-file-info');
+ if (drop) drop.style.display = '';
+ if (info) info.style.display = 'none';
+}
+
function setConvolutionFile(filePath) {
const fileName = filePath.split(/[/\\]/).pop();
appState.config.convolution = { file: filePath, enabled: true };
@@ -3502,6 +3606,13 @@ function initVSTPlugins() {
renderVSTPlugins();
markDirty();
});
+ document.getElementById('btn-reset-vst')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.vstPlugins = [];
+ renderVSTPlugins();
+ markDirty();
+ showToast('VST plugins reset', 'success');
+ });
}
function renderVSTPlugins() {
@@ -3692,6 +3803,17 @@ function initChannelRouting() {
renderCopyRoutes();
markDirty();
});
+ document.getElementById('btn-reset-routing')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.channels = 'all';
+ appState.config.copies = (appState.config.copies || []).filter(c => c.isEffect);
+ document.querySelectorAll('#channel-buttons .ch-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.channel === 'all');
+ });
+ renderCopyRoutes();
+ markDirty();
+ showToast('Channel routing reset', 'success');
+ });
}
function renderCopyRoutes() {
@@ -3726,6 +3848,13 @@ function initDelay() {
renderDelays();
markDirty();
});
+ document.getElementById('btn-reset-delay')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.delays = [];
+ renderDelays();
+ markDirty();
+ showToast('Delay entries reset', 'success');
+ });
}
function renderDelays() {
@@ -3757,6 +3886,20 @@ function renderDelays() {
}
function initEffects() {
+ const renderEffectDefaults = () => {
+ const bass = document.getElementById('effect-bass');
+ const treble = document.getElementById('effect-treble');
+ const balance = document.getElementById('effect-balance');
+ const crossfeed = document.getElementById('effect-crossfeed-enabled');
+ if (bass) bass.value = '0';
+ if (treble) treble.value = '0';
+ if (balance) balance.value = '0';
+ if (crossfeed) crossfeed.checked = false;
+ document.getElementById('effect-bass-val').textContent = '0.0 dB';
+ document.getElementById('effect-treble-val').textContent = '0.0 dB';
+ document.getElementById('effect-balance-val').textContent = 'Center';
+ };
+
const update = () => {
const bass = parseFloat(document.getElementById('effect-bass').value);
const treble = parseFloat(document.getElementById('effect-treble').value);
@@ -3771,7 +3914,7 @@ function initEffects() {
if (bass !== 0) appState.config.filters.push({ isEffect:true, enabled:true, type:'LSC', frequency:105, gain:bass, q:0.71 });
if (treble !== 0) appState.config.filters.push({ isEffect:true, enabled:true, type:'HSC', frequency:10000, gain:treble, q:0.71 });
- appState.config.copies = appState.config.copies.filter(c => !c.isEffect);
+ appState.config.copies = (appState.config.copies || []).filter(c => !c.isEffect);
if (xfeed) {
appState.config.copies.push({ isEffect:true, enabled:true, target:'L', expression:'L+0.2*R' });
appState.config.copies.push({ isEffect:true, enabled:true, target:'R', expression:'R+0.2*L' });
@@ -3791,6 +3934,16 @@ function initEffects() {
document.getElementById(id).addEventListener('input', update);
});
document.getElementById('effect-crossfeed-enabled').addEventListener('change', update);
+ document.getElementById('btn-reset-effects')?.addEventListener('click', () => {
+ pushUndo();
+ renderEffectDefaults();
+ appState.config.filters = appState.config.filters.filter(f => !f.isEffect);
+ appState.config.copies = (appState.config.copies || []).filter(c => !c.isEffect);
+ refreshUI();
+ markDirty();
+ audioPlayer?.refreshEQ();
+ showToast('Audio effects reset', 'success');
+ });
}
// ═══════════════════════════════════════════════════════════
@@ -3806,6 +3959,16 @@ function initAdvancedPanel() {
markDirty();
});
});
+ document.getElementById('btn-reset-stage')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.stage = 'post-mix';
+ document.querySelectorAll('.stage-btn').forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.stage === 'post-mix');
+ });
+ updateRawConfigEditor();
+ markDirty();
+ showToast('Processing stage reset', 'success');
+ });
// Presets — shared localStorage store, APO file as optional bonus
document.getElementById('btn-save-new-preset').addEventListener('click', async () => {
@@ -3851,8 +4014,20 @@ function initAdvancedPanel() {
const file = await selectFile({ filters: [{ name: 'Config', extensions: ['txt','cfg'] }] });
if (file) { appState.config.includes.push({ file, enabled: true }); renderIncludes(); markDirty(); }
});
+ document.getElementById('btn-reset-includes')?.addEventListener('click', () => {
+ pushUndo();
+ appState.config.includes = [];
+ renderIncludes();
+ updateRawConfigEditor();
+ markDirty();
+ showToast('Include files reset', 'success');
+ });
// Raw editor
+ document.getElementById('btn-reset-raw')?.addEventListener('click', () => {
+ updateRawConfigEditor();
+ showToast('Raw text regenerated from GUI', 'success');
+ });
document.getElementById('btn-parse-raw')?.addEventListener('click', () => {
const raw = document.getElementById('raw-config-editor').value;
if (raw.trim()) { loadConfigFromText(raw); showToast('Parsed to GUI', 'success'); }
diff --git a/vercel.json b/vercel.json
new file mode 100644
index 0000000..b56a52a
--- /dev/null
+++ b/vercel.json
@@ -0,0 +1,5 @@
+{
+ "buildCommand": "npm --prefix website run build",
+ "outputDirectory": "website/dist",
+ "installCommand": "echo No install needed"
+}
diff --git a/website/assets/app-screenshot.png b/website/assets/app-screenshot.png
new file mode 100644
index 0000000..7591d2d
Binary files /dev/null and b/website/assets/app-screenshot.png differ
diff --git a/website/assets/logo.png b/website/assets/logo.png
new file mode 100644
index 0000000..c5b17fd
Binary files /dev/null and b/website/assets/logo.png differ
diff --git a/website/build.mjs b/website/build.mjs
new file mode 100644
index 0000000..d6dbeab
--- /dev/null
+++ b/website/build.mjs
@@ -0,0 +1,13 @@
+import { cp, mkdir, rm } from 'node:fs/promises';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = dirname(fileURLToPath(import.meta.url));
+const dist = join(root, 'dist');
+
+await rm(dist, { recursive: true, force: true });
+await mkdir(dist, { recursive: true });
+await cp(join(root, 'index.html'), join(dist, 'index.html'));
+await cp(join(root, 'styles.css'), join(dist, 'styles.css'));
+await cp(join(root, 'script.js'), join(dist, 'script.js'));
+await cp(join(root, 'assets'), join(dist, 'assets'), { recursive: true });
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..a4fd058
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
Neon Equalizer
+
+
+
+
+
+
+
+
+
Equalizer APO control center
+
Neon Equalizer
+
+ Parametric EQ, AutoEQ, Squiglink targets, hardware PEQ, surround tools, and APO power features in one focused Windows app.
+
+
+
+ v2.1.1 current build
+ AutoEQ with safety controls
+ Device PEQ peak and shelf aware
+
+
+
+

+
+
+
+
+
+
+
+
+
Built for tuning work
+
Fast enough for experiments. Precise enough for daily listening.
+
+
+
+ 01
+ EQ That Stays Legible
+ Parametric and Graphic EQ controls sit beside a live response graph, A/B snapshots, preview audio, and reset points where you need them.
+
+
+ 02
+ AutoEQ With Guardrails
+ Choose the correction region, intensity, filter budget, normalization, and sub-bass roll-off so open-back headphones do not get reckless bass boosts.
+
+
+ 03
+ Hardware-Aware PEQ
+ Push and pull supported peak, low-shelf, and high-shelf bands from compatible devices through USB HID, serial, Bluetooth LE, or network paths.
+
+
+ 04
+ APO Tools In Reach
+
+ Surround routing, HRTF previews, convolution, VST plugins, loudness correction, channel routing, delay, and raw APO editing live in the same workspace.
+ HRTF users can start with HeSuVi or build custom BRIRs with ASH-Toolset.
+
+
+
+
+
+
+
+
Workflow
+
Measure, target, shape, save.
+
+ Bring in a measurement, load a target, generate filters, then trim the last few dB by ear. Neon Equalizer keeps the signal path visible from graph to APO output.
+
+
+
+ - Load bundled or Squiglink measurements and targets.
+ - Generate AutoEQ filters with intensity and range limits.
+ - Refine with graph handles, preview audio, and A/B history.
+ - Deploy to Equalizer APO or supported hardware PEQ.
+
+
+
+
+
+
Windows desktop
+
Download the latest build from GitHub Releases.
+
Portable and installer builds are published with release notes so you can see exactly what changed before updating.
+
+ Get Neon Equalizer
+
+
+
+
+
+
+
+
diff --git a/website/package.json b/website/package.json
new file mode 100644
index 0000000..32da457
--- /dev/null
+++ b/website/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "neon-equalizer-site",
+ "private": true,
+ "version": "2.1.1",
+ "type": "module",
+ "scripts": {
+ "build": "node build.mjs"
+ }
+}
diff --git a/website/script.js b/website/script.js
new file mode 100644
index 0000000..a79c294
--- /dev/null
+++ b/website/script.js
@@ -0,0 +1,8 @@
+const nav = document.querySelector('.site-nav');
+
+const onScroll = () => {
+ nav?.classList.toggle('is-scrolled', window.scrollY > 12);
+};
+
+window.addEventListener('scroll', onScroll, { passive: true });
+onScroll();
diff --git a/website/styles.css b/website/styles.css
new file mode 100644
index 0000000..2c7838c
--- /dev/null
+++ b/website/styles.css
@@ -0,0 +1,421 @@
+:root {
+ color-scheme: dark;
+ --bg: #05070d;
+ --panel: rgba(13, 18, 31, 0.78);
+ --panel-strong: rgba(18, 27, 45, 0.92);
+ --line: rgba(148, 163, 184, 0.2);
+ --text: #eef6ff;
+ --muted: #9ba9bd;
+ --cyan: #12d9ff;
+ --blue: #4f7dff;
+ --green: #24e0a4;
+ --pink: #ff3ea5;
+ --yellow: #f7d66b;
+ --shadow: 0 28px 90px rgba(0, 0, 0, 0.5);
+}
+
+* { box-sizing: border-box; }
+html { scroll-behavior: smooth; }
+body {
+ margin: 0;
+ min-width: 320px;
+ background:
+ radial-gradient(circle at 72% 8%, rgba(18, 217, 255, 0.12), transparent 30vw),
+ radial-gradient(circle at 14% 18%, rgba(255, 62, 165, 0.12), transparent 28vw),
+ linear-gradient(180deg, #04060b 0%, #07111c 45%, #05070d 100%);
+ color: var(--text);
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ letter-spacing: 0;
+}
+
+a { color: inherit; text-decoration: none; }
+
+.site-nav {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 20px;
+ padding: 14px clamp(18px, 4vw, 64px);
+ background: rgba(5, 7, 13, 0.76);
+ border-bottom: 1px solid var(--line);
+ backdrop-filter: blur(18px);
+}
+
+.site-nav.is-scrolled {
+ background: rgba(5, 7, 13, 0.9);
+ box-shadow: 0 14px 38px rgba(0, 0, 0, 0.28);
+}
+
+.brand,
+.site-nav nav {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+.brand {
+ font-weight: 800;
+ color: white;
+}
+
+.brand-mark {
+ width: 34px;
+ height: 34px;
+ object-fit: contain;
+}
+
+.site-nav nav a {
+ color: var(--muted);
+ font-size: 14px;
+}
+
+.site-nav nav a:hover { color: var(--text); }
+
+.hero {
+ min-height: min(760px, calc(100vh - 96px));
+ display: grid;
+ grid-template-columns: minmax(0, 0.85fr) minmax(420px, 1.15fr);
+ gap: clamp(28px, 5vw, 72px);
+ align-items: center;
+ padding: clamp(44px, 7vw, 84px) clamp(18px, 4vw, 64px) clamp(36px, 5vw, 70px);
+}
+
+.hero-copy {
+ max-width: 620px;
+}
+
+.eyebrow {
+ margin: 0 0 14px;
+ color: var(--cyan);
+ font-size: 12px;
+ font-weight: 800;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+
+h1,
+h2,
+h3,
+p {
+ margin-top: 0;
+}
+
+h1 {
+ margin-bottom: 18px;
+ font-size: clamp(48px, 9vw, 108px);
+ line-height: 0.92;
+ letter-spacing: 0;
+}
+
+h2 {
+ font-size: clamp(31px, 5vw, 58px);
+ line-height: 1;
+ letter-spacing: 0;
+}
+
+h3 {
+ font-size: 20px;
+ margin-bottom: 10px;
+}
+
+.hero-lede,
+.workflow p,
+.download p,
+.feature-grid p {
+ color: var(--muted);
+ font-size: 17px;
+ line-height: 1.65;
+}
+
+.feature-grid a {
+ color: var(--cyan);
+ font-weight: 800;
+ border-bottom: 1px solid rgba(18, 217, 255, 0.35);
+}
+
+.feature-grid a:hover {
+ color: var(--text);
+ border-bottom-color: var(--text);
+}
+
+.hero-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin: 28px 0 22px;
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 46px;
+ padding: 0 20px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--text);
+ font-weight: 800;
+ transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
+}
+
+.button:hover {
+ transform: translateY(-1px);
+ border-color: rgba(18, 217, 255, 0.55);
+ background: rgba(18, 217, 255, 0.09);
+}
+
+.button.primary {
+ border-color: rgba(18, 217, 255, 0.75);
+ background: linear-gradient(135deg, rgba(18, 217, 255, 0.95), rgba(79, 125, 255, 0.95));
+ color: #021018;
+ box-shadow: 0 14px 42px rgba(18, 217, 255, 0.24);
+}
+
+.button.large {
+ min-height: 54px;
+ padding-inline: 26px;
+}
+
+.hero-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ color: var(--muted);
+ font-size: 13px;
+}
+
+.hero-stats span {
+ padding: 9px 11px;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.hero-stats b {
+ color: var(--text);
+}
+
+.hero-visual {
+ position: relative;
+ min-height: 420px;
+ border: 1px solid rgba(18, 217, 255, 0.28);
+ border-radius: 18px;
+ background:
+ linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
+ var(--panel);
+ background-size: 34px 34px;
+ box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ overflow: hidden;
+}
+
+.hero-visual img {
+ position: absolute;
+ width: min(1120px, 118%);
+ max-width: none;
+ right: -12%;
+ top: 48%;
+ transform: translateY(-50%);
+ border-radius: 12px;
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.48);
+}
+
+.signal-strip {
+ position: absolute;
+ left: 22px;
+ bottom: 18px;
+ display: flex;
+ gap: 8px;
+ align-items: flex-end;
+ height: 42px;
+}
+
+.signal-strip span {
+ display: block;
+ width: 7px;
+ border-radius: 99px;
+ background: linear-gradient(180deg, var(--cyan), var(--pink));
+}
+
+.signal-strip span:nth-child(1) { height: 16px; }
+.signal-strip span:nth-child(2) { height: 34px; }
+.signal-strip span:nth-child(3) { height: 24px; }
+.signal-strip span:nth-child(4) { height: 40px; }
+.signal-strip span:nth-child(5) { height: 20px; }
+
+.feature-band,
+.workflow,
+.download {
+ margin: 0 clamp(18px, 4vw, 64px) clamp(26px, 4vw, 54px);
+ padding: clamp(28px, 5vw, 56px);
+ border: 1px solid var(--line);
+ border-radius: 18px;
+ background: linear-gradient(180deg, var(--panel), rgba(6, 10, 18, 0.82));
+ box-shadow: 0 18px 70px rgba(0, 0, 0, 0.28);
+}
+
+.section-heading {
+ max-width: 820px;
+ margin-bottom: 28px;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 14px;
+}
+
+.feature-grid article {
+ min-height: 260px;
+ padding: 22px;
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.035);
+}
+
+.feature-index {
+ display: inline-block;
+ margin-bottom: 34px;
+ color: var(--yellow);
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-weight: 800;
+}
+
+.workflow {
+ display: grid;
+ grid-template-columns: minmax(0, 0.8fr) minmax(320px, 1fr);
+ gap: 34px;
+ align-items: start;
+}
+
+.workflow-list {
+ display: grid;
+ gap: 12px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ counter-reset: step;
+}
+
+.workflow-list li {
+ position: relative;
+ padding: 18px 18px 18px 62px;
+ border: 1px solid var(--line);
+ border-radius: 12px;
+ background: var(--panel-strong);
+ color: var(--muted);
+}
+
+.workflow-list li::before {
+ counter-increment: step;
+ content: counter(step);
+ position: absolute;
+ left: 18px;
+ top: 18px;
+ width: 28px;
+ height: 28px;
+ display: grid;
+ place-items: center;
+ border-radius: 7px;
+ background: rgba(36, 224, 164, 0.14);
+ color: var(--green);
+ font-weight: 900;
+}
+
+.workflow-list span {
+ display: block;
+ color: var(--text);
+ font-weight: 800;
+ margin-bottom: 3px;
+}
+
+.download {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 28px;
+ background:
+ linear-gradient(135deg, rgba(18, 217, 255, 0.1), transparent 42%),
+ linear-gradient(180deg, var(--panel), rgba(6, 10, 18, 0.9));
+}
+
+.download div {
+ max-width: 760px;
+}
+
+footer {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 26px clamp(18px, 4vw, 64px) 38px;
+ color: var(--muted);
+}
+
+footer span {
+ color: var(--text);
+ font-weight: 800;
+}
+
+footer a:hover {
+ color: var(--cyan);
+}
+
+@media (max-width: 980px) {
+ .site-nav nav { display: none; }
+ .hero {
+ grid-template-columns: 1fr;
+ min-height: auto;
+ }
+ .hero-visual {
+ min-height: 360px;
+ }
+ .hero-visual img {
+ width: 980px;
+ right: -280px;
+ }
+ .feature-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ .workflow,
+ .download {
+ grid-template-columns: 1fr;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+@media (max-width: 620px) {
+ .hero {
+ padding-top: 34px;
+ }
+ .brand span {
+ font-size: 14px;
+ }
+ .hero-actions,
+ .download {
+ align-items: stretch;
+ }
+ .button {
+ width: 100%;
+ }
+ .hero-visual {
+ min-height: 270px;
+ }
+ .hero-visual img {
+ width: 760px;
+ right: -300px;
+ }
+ .feature-grid {
+ grid-template-columns: 1fr;
+ }
+ .feature-grid article {
+ min-height: 0;
+ }
+ footer {
+ flex-direction: column;
+ }
+}