diff --git a/index.html b/index.html index f87728c..063a6cd 100644 --- a/index.html +++ b/index.html @@ -246,6 +246,7 @@ Add Filter + 0 filters
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 +
+
+
+ Neon Equalizer desktop app showing the frequency graph and EQ controls. + +
+
+ +
+
+

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. +

+
+
    +
  1. Load bundled or Squiglink measurements and targets.
  2. +
  3. Generate AutoEQ filters with intensity and range limits.
  4. +
  5. Refine with graph handles, preview audio, and A/B history.
  6. +
  7. Deploy to Equalizer APO or supported hardware PEQ.
  8. +
+
+ +
+
+

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; + } +}