fix: redesign landing page with Nous blue palette and cleaner layout (#974)

* fix: redesign landing page with Nous blue palette and cleaner layout

* fix: add features link

* fix: misc refactors, easings

* fix: animations, easings

* fix: mobile
This commit is contained in:
Austin Pickett
2026-03-13 15:03:38 -04:00
committed by GitHub
parent b74facd119
commit ebd4f2c6a8
3 changed files with 1399 additions and 1118 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -4,339 +4,518 @@
// --- Platform install commands --- // --- Platform install commands ---
const PLATFORMS = { const PLATFORMS = {
linux: { linux: {
command: 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash', command:
prompt: '$', "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash",
note: 'Works on Linux, macOS & WSL2 · No prerequisites · Installs everything automatically', prompt: "$",
stepNote: 'Installs uv, Python 3.11, clones the repo, sets up everything. No sudo needed.', note: "Works on Linux, macOS & WSL2 · No prerequisites · Installs everything automatically",
}, stepNote:
"Installs uv, Python 3.11, clones the repo, sets up everything. No sudo needed.",
},
}; };
function detectPlatform() { function detectPlatform() {
return 'linux'; return "linux";
} }
function switchPlatform(platform) { function switchPlatform(platform) {
const cfg = PLATFORMS[platform]; const cfg = PLATFORMS[platform];
if (!cfg) return; if (!cfg) return;
// Update hero install widget // Update hero install widget
const commandEl = document.getElementById('install-command'); const commandEl = document.getElementById("install-command");
const promptEl = document.getElementById('install-prompt'); const promptEl = document.getElementById("install-prompt");
const noteEl = document.getElementById('install-note'); const noteEl = document.getElementById("install-note");
if (commandEl) commandEl.textContent = cfg.command; if (commandEl) commandEl.textContent = cfg.command;
if (promptEl) promptEl.textContent = cfg.prompt; if (promptEl) promptEl.textContent = cfg.prompt;
if (noteEl) noteEl.textContent = cfg.note; if (noteEl) noteEl.textContent = cfg.note;
// Update active tab in hero // Update active tab in hero
document.querySelectorAll('.install-tab').forEach(tab => { document.querySelectorAll(".install-tab").forEach((tab) => {
tab.classList.toggle('active', tab.dataset.platform === platform); tab.classList.toggle("active", tab.dataset.platform === platform);
}); });
// Sync the step section tabs too // Sync the step section tabs too
switchStepPlatform(platform); switchStepPlatform(platform);
} }
function switchStepPlatform(platform) { function switchStepPlatform(platform) {
const cfg = PLATFORMS[platform]; const cfg = PLATFORMS[platform];
if (!cfg) return; if (!cfg) return;
const commandEl = document.getElementById('step1-command'); const commandEl = document.getElementById("step1-command");
const copyBtn = document.getElementById('step1-copy'); const copyBtn = document.getElementById("step1-copy");
const noteEl = document.getElementById('step1-note'); const noteEl = document.getElementById("step1-note");
if (commandEl) commandEl.textContent = cfg.command; if (commandEl) commandEl.textContent = cfg.command;
if (copyBtn) copyBtn.setAttribute('data-text', cfg.command); if (copyBtn) copyBtn.setAttribute("data-text", cfg.command);
if (noteEl) noteEl.textContent = cfg.stepNote; if (noteEl) noteEl.textContent = cfg.stepNote;
// Update active tab in step section // Update active tab in step section
document.querySelectorAll('.code-tab').forEach(tab => { document.querySelectorAll(".code-tab").forEach((tab) => {
tab.classList.toggle('active', tab.dataset.platform === platform); tab.classList.toggle("active", tab.dataset.platform === platform);
});
}
function toggleMobileNav() {
document.getElementById("nav-mobile").classList.toggle("open");
document.getElementById("nav-hamburger").classList.toggle("open");
}
function toggleSpecs() {
const wrapper = document.getElementById("specs-wrapper");
const btn = document.getElementById("specs-toggle");
const label = btn.querySelector(".toggle-label");
const isOpen = wrapper.classList.contains("open");
if (isOpen) {
wrapper.style.maxHeight = wrapper.scrollHeight + "px";
requestAnimationFrame(() => {
wrapper.style.maxHeight = "0";
}); });
wrapper.classList.remove("open");
btn.classList.remove("open");
if (label) label.textContent = "More details";
} else {
wrapper.classList.add("open");
wrapper.style.maxHeight = wrapper.scrollHeight + "px";
btn.classList.add("open");
if (label) label.textContent = "Less";
wrapper.addEventListener(
"transitionend",
() => {
if (wrapper.classList.contains("open")) {
wrapper.style.maxHeight = "none";
}
},
{ once: true }
);
}
} }
// --- Copy to clipboard --- // --- Copy to clipboard ---
function copyInstall() { function copyInstall() {
const text = document.getElementById('install-command').textContent; const text = document.getElementById("install-command").textContent;
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('.install-widget-body .copy-btn'); const btn = document.querySelector(".install-widget-body .copy-btn");
const original = btn.querySelector('.copy-text').textContent; const original = btn.querySelector(".copy-text").textContent;
btn.querySelector('.copy-text').textContent = 'Copied!'; btn.querySelector(".copy-text").textContent = "Copied!";
btn.style.color = 'var(--gold)'; btn.style.color = "var(--primary-light)";
setTimeout(() => { setTimeout(() => {
btn.querySelector('.copy-text').textContent = original; btn.querySelector(".copy-text").textContent = original;
btn.style.color = ''; btn.style.color = "";
}, 2000); }, 2000);
}); });
} }
function copyText(btn) { function copyText(btn) {
const text = btn.getAttribute('data-text'); const text = btn.getAttribute("data-text");
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
const original = btn.textContent; const original = btn.textContent;
btn.textContent = 'Copied!'; btn.textContent = "Copied!";
btn.style.color = 'var(--gold)'; btn.style.color = "var(--primary-light)";
setTimeout(() => { setTimeout(() => {
btn.textContent = original; btn.textContent = original;
btn.style.color = ''; btn.style.color = "";
}, 2000); }, 2000);
}); });
} }
// --- Scroll-triggered fade-in --- // --- Scroll-triggered fade-in ---
function initScrollAnimations() { function initScrollAnimations() {
const elements = document.querySelectorAll( const elements = document.querySelectorAll(
'.feature-card, .tool-pill, .platform-group, .skill-category, ' + ".feature-card, .install-step, " +
'.install-step, .research-card, .footer-card, .section-header, ' + ".section-header, .terminal-window",
'.lead-text, .section-desc, .terminal-window' );
);
elements.forEach(el => el.classList.add('fade-in')); elements.forEach((el) => el.classList.add("fade-in"));
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver(
entries.forEach(entry => { (entries) => {
if (entry.isIntersecting) { entries.forEach((entry) => {
// Stagger children within grids if (entry.isIntersecting) {
const parent = entry.target.parentElement; // Stagger children within grids
if (parent) { const parent = entry.target.parentElement;
const siblings = parent.querySelectorAll('.fade-in'); if (parent) {
let idx = Array.from(siblings).indexOf(entry.target); const siblings = parent.querySelectorAll(".fade-in");
if (idx < 0) idx = 0; let idx = Array.from(siblings).indexOf(entry.target);
setTimeout(() => { if (idx < 0) idx = 0;
entry.target.classList.add('visible'); setTimeout(() => {
}, idx * 60); entry.target.classList.add("visible");
} else { }, idx * 60);
entry.target.classList.add('visible'); } else {
} entry.target.classList.add("visible");
observer.unobserve(entry.target); }
} observer.unobserve(entry.target);
}); }
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); });
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" },
);
elements.forEach(el => observer.observe(el)); elements.forEach((el) => observer.observe(el));
} }
// --- Terminal Demo --- // --- Terminal Demo ---
const CURSOR = '<span class="terminal-cursor">█</span>';
const demoSequence = [ const demoSequence = [
// Scene 1: Research task with delegation { type: "prompt", text: " " },
{ type: 'prompt', text: ' ' }, {
{ type: 'type', text: 'Research the latest approaches to GRPO training and write a summary', delay: 30 }, type: "type",
{ type: 'pause', ms: 600 }, text: "Research the latest approaches to GRPO training and write a summary",
{ type: 'output', lines: [ delay: 30,
'', },
'<span class="t-dim">┊ 🔍 web_search "GRPO reinforcement learning 2026" 1.2s</span>', { type: "pause", ms: 600 },
]}, {
{ type: 'pause', ms: 400 }, type: "output",
{ type: 'output', lines: [ lines: [
'<span class="t-dim">┊ 📄 web_extract arxiv.org/abs/2402.03300 3.1s</span>', "",
]}, '<span class="t-dim"> web_search "GRPO reinforcement learning 2026" 1.2s</span>',
{ type: 'pause', ms: 400 }, ],
{ type: 'output', lines: [ },
'<span class="t-dim">┊ 🔍 web_search "GRPO vs PPO ablation results" 0.9s</span>', { type: "pause", ms: 400 },
]}, {
{ type: 'pause', ms: 400 }, type: "output",
{ type: 'output', lines: [ lines: [
'<span class="t-dim">┊ 📄 web_extract huggingface.co/blog/grpo 2.8s</span>', '<span class="t-dim"> web_extract arxiv.org/abs/2402.03300 3.1s</span>',
]}, ],
{ type: 'pause', ms: 400 }, },
{ type: 'output', lines: [ { type: "pause", ms: 400 },
'<span class="t-dim">┊ ✍️ write_file ~/research/grpo-summary.md 0.1s</span>', {
]}, type: "output",
{ type: 'pause', ms: 500 }, lines: [
{ type: 'output', lines: [ '<span class="t-dim"> web_search "GRPO vs PPO ablation results" 0.9s</span>',
'', ],
'<span class="t-text">Done! I\'ve written a summary covering:</span>', },
'', { type: "pause", ms: 400 },
'<span class="t-text"> <span class="t-green">✓</span> GRPO\'s group-relative advantage (no critic model needed)</span>', {
'<span class="t-text"> <span class="t-green">✓</span> Comparison with PPO/DPO on reasoning benchmarks</span>', type: "output",
'<span class="t-text"> <span class="t-green">✓</span> Implementation notes for Axolotl and TRL</span>', lines: [
'', '<span class="t-dim"> web_extract huggingface.co/blog/grpo 2.8s</span>',
'<span class="t-text">Saved to</span> <span class="t-amber">~/research/grpo-summary.md</span>', ],
]}, },
{ type: 'pause', ms: 2500 }, { type: "pause", ms: 400 },
{
type: "output",
lines: [
'<span class="t-dim"> write_file ~/research/grpo-summary.md 0.1s</span>',
],
},
{ type: "pause", ms: 500 },
{
type: "output",
lines: [
"",
'<span class="t-text">Done! I\'ve written a summary covering:</span>',
"",
'<span class="t-text"> <span class="t-green">✓</span> GRPO\'s group-relative advantage (no critic model needed)</span>',
'<span class="t-text"> <span class="t-green">✓</span> Comparison with PPO/DPO on reasoning benchmarks</span>',
'<span class="t-text"> <span class="t-green">✓</span> Implementation notes for Axolotl and TRL</span>',
"",
'<span class="t-text">Saved to</span> <span class="t-accent">~/research/grpo-summary.md</span>',
],
},
{ type: "pause", ms: 2500 },
// Scene 2: Quick delegation { type: "clear" },
{ type: 'clear' }, { type: "prompt", text: " " },
{ type: 'prompt', text: ' ' }, {
{ type: 'type', text: 'Review the PR at NousResearch/hermes-agent#42 and fix any issues', delay: 30 }, type: "type",
{ type: 'pause', ms: 600 }, text: "Review the PR at NousResearch/hermes-agent#42 and fix any issues",
{ type: 'output', lines: [ delay: 30,
'', },
'<span class="t-dim">┊ 🔀 delegate_task "review PR #42 changes" 2.1s</span>', { type: "pause", ms: 600 },
]}, {
{ type: 'pause', ms: 500 }, type: "output",
{ type: 'output', lines: [ lines: [
'<span class="t-dim">┊ 💻 git diff main..pr-42 0.4s</span>', "",
]}, '<span class="t-dim"> delegate_task "review PR #42 changes" 2.1s</span>',
{ type: 'pause', ms: 400 }, ],
{ type: 'output', lines: [ },
'<span class="t-dim">┊ ✏️ patch tools/registry.py 0.1s</span>', { type: "pause", ms: 500 },
]}, {
{ type: 'pause', ms: 400 }, type: "output",
{ type: 'output', lines: [ lines: [
'<span class="t-dim">┊ 💻 python -m pytest tests/ -x 3.2s</span>', '<span class="t-dim"> git diff main..pr-42 0.4s</span>',
]}, ],
{ type: 'pause', ms: 400 }, },
{ type: 'output', lines: [ { type: "pause", ms: 400 },
'<span class="t-dim">┊ 💻 git commit -m "fix: handle empty tool schemas" 0.3s</span>', {
]}, type: "output",
{ type: 'pause', ms: 500 }, lines: [
{ type: 'output', lines: [ '<span class="t-dim"> patch tools/registry.py 0.1s</span>',
'', ],
'<span class="t-text">Found 2 issues in the PR and fixed both:</span>', },
'', { type: "pause", ms: 400 },
'<span class="t-text"> <span class="t-green">✓</span> Empty tool schema crash in registry.py — added guard</span>', {
'<span class="t-text"> <span class="t-green">✓</span> Missing error handling in delegate_tool.py — added try/except</span>', type: "output",
'', lines: [
'<span class="t-text">Tests pass. Committed the fix and pushed to the PR branch.</span>', '<span class="t-dim"> python -m pytest tests/ -x 3.2s</span>',
'<span class="t-text">I also saved a</span> <span class="t-amber">skill</span> <span class="t-text">for this PR review pattern.</span>', ],
]}, },
{ type: 'pause', ms: 2500 }, { type: "pause", ms: 400 },
{
type: "output",
lines: [
'<span class="t-dim"> git commit -m "fix: handle empty tool schemas" 0.3s</span>',
],
},
{ type: "pause", ms: 500 },
{
type: "output",
lines: [
"",
'<span class="t-text">Found 2 issues in the PR and fixed both:</span>',
"",
'<span class="t-text"> <span class="t-green">✓</span> Empty tool schema crash in registry.py — added guard</span>',
'<span class="t-text"> <span class="t-green">✓</span> Missing error handling in delegate_tool.py — added try/except</span>',
"",
'<span class="t-text">Tests pass. Committed the fix and pushed to the PR branch.</span>',
'<span class="t-text">I also saved a</span> <span class="t-accent">skill</span> <span class="t-text">for this PR review pattern.</span>',
],
},
{ type: "pause", ms: 2500 },
// Scene 3: Session search by keyword { type: "clear" },
{ type: 'clear' }, { type: "prompt", text: " " },
{ type: 'prompt', text: ' ' }, {
{ type: 'type', text: 'How did we fix that Docker networking issue?', delay: 35 }, type: "type",
{ type: 'pause', ms: 500 }, text: "How did we fix that Docker networking issue?",
{ type: 'output', lines: [ delay: 35,
'', },
'<span class="t-dim">┊ 🔎 session_search "Docker networking" 1.4s</span>', { type: "pause", ms: 500 },
]}, {
{ type: 'pause', ms: 500 }, type: "output",
{ type: 'output', lines: [ lines: [
'', "",
'<span class="t-text">Found it — from a session on February 12th:</span>', '<span class="t-dim"> session_search "Docker networking" 1.4s</span>',
'', ],
'<span class="t-text">The containers couldn\'t reach each other because the compose</span>', },
'<span class="t-text">file was using the default bridge network. We switched to a</span>', { type: "pause", ms: 500 },
'<span class="t-text">custom network with</span> <span class="t-amber">driver: overlay</span><span class="t-text">, added explicit</span>', {
'<span class="t-text">aliases, and set</span> <span class="t-amber">dns: 8.8.8.8</span> <span class="t-text">as a fallback.</span>', type: "output",
'', lines: [
'<span class="t-text">The fix was committed in</span> <span class="t-amber">docker-compose.prod.yml</span><span class="t-text">.</span>', "",
]}, '<span class="t-text">Found it — from a session on February 12th:</span>',
{ type: 'pause', ms: 3000 }, "",
'<span class="t-text">The containers couldn\'t reach each other because the compose</span>',
'<span class="t-text">file was using the default bridge network. We switched to a</span>',
'<span class="t-text">custom network with</span> <span class="t-accent">driver: overlay</span><span class="t-text">, added explicit</span>',
'<span class="t-text">aliases, and set</span> <span class="t-accent">dns: 8.8.8.8</span> <span class="t-text">as a fallback.</span>',
"",
'<span class="t-text">The fix was committed in</span> <span class="t-accent">docker-compose.prod.yml</span><span class="t-text">.</span>',
],
},
{ type: "pause", ms: 3000 },
]; ];
class TerminalDemo { class TerminalDemo {
constructor(element, cursorElement) { constructor(container) {
this.el = element; this.container = container;
this.cursor = cursorElement; this.running = false;
this.running = false; this.content = "";
this.content = ''; }
this.observer = null;
}
async start() { async start() {
if (this.running) return; if (this.running) return;
this.running = true; this.running = true;
while (this.running) { while (this.running) {
for (const step of demoSequence) { for (const step of demoSequence) {
if (!this.running) return; if (!this.running) return;
await this.execute(step); await this.execute(step);
} }
// Loop this.clear();
this.clear(); await this.sleep(1000);
await this.sleep(1000); }
}
stop() {
this.running = false;
}
async execute(step) {
switch (step.type) {
case "prompt":
this.append(`<span class="t-prompt">${step.text}</span>`);
break;
case "type":
for (const char of step.text) {
if (!this.running) return;
this.append(`<span class="t-cmd">${char}</span>`);
await this.sleep(step.delay || 30);
} }
} break;
case "output":
stop() { for (const line of step.lines) {
this.running = false; if (!this.running) return;
} this.append("\n" + line);
await this.sleep(50);
async execute(step) {
switch (step.type) {
case 'prompt':
this.append(`<span class="t-prompt">${step.text}</span>`);
break;
case 'type':
for (const char of step.text) {
if (!this.running) return;
this.append(`<span class="t-cmd">${char}</span>`);
await this.sleep(step.delay || 30);
}
break;
case 'output':
for (const line of step.lines) {
if (!this.running) return;
this.append('\n' + line);
await this.sleep(50);
}
break;
case 'pause':
await this.sleep(step.ms);
break;
case 'clear':
this.clear();
break;
} }
break;
case "pause":
await this.sleep(step.ms);
break;
case "clear":
this.clear();
break;
} }
}
append(html) { append(html) {
this.content += html; this.content += html;
this.el.innerHTML = this.content; this.render();
// Keep cursor at end }
this.el.parentElement.scrollTop = this.el.parentElement.scrollHeight;
}
clear() { render() {
this.content = ''; this.container.innerHTML = this.content + CURSOR;
this.el.innerHTML = ''; this.container.scrollTop = this.container.scrollHeight;
} }
sleep(ms) { clear() {
return new Promise(resolve => setTimeout(resolve, ms)); this.content = "";
} this.container.innerHTML = "";
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// --- Noise Overlay (ported from hermes-chat NoiseOverlay) ---
function initNoiseOverlay() {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
if (typeof THREE === "undefined") return;
const canvas = document.getElementById("noise-overlay");
if (!canvas) return;
const vertexShader = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec2 uRes;
uniform float uDpr, uSize, uDensity, uOpacity;
uniform vec3 uColor;
varying vec2 vUv;
float hash(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
void main() {
float n = hash(floor(vUv * uRes / (uSize * uDpr)));
gl_FragColor = vec4(uColor, step(1.0 - uDensity, n)) * uOpacity;
}
`;
function hexToVec3(hex) {
const c = hex.replace("#", "");
return new THREE.Vector3(
parseInt(c.substring(0, 2), 16) / 255,
parseInt(c.substring(2, 4), 16) / 255,
parseInt(c.substring(4, 6), 16) / 255,
);
}
const renderer = new THREE.WebGLRenderer({
alpha: true,
canvas,
premultipliedAlpha: false,
});
renderer.setClearColor(0x000000, 0);
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const geo = new THREE.PlaneGeometry(2, 2);
const mat = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
transparent: true,
uniforms: {
uColor: { value: hexToVec3("#8090BB") },
uDensity: { value: 0.1 },
uDpr: { value: 1 },
uOpacity: { value: 0.4 },
uRes: { value: new THREE.Vector2() },
uSize: { value: 1.0 },
},
});
scene.add(new THREE.Mesh(geo, mat));
function resize() {
const dpr = window.devicePixelRatio;
const w = window.innerWidth;
const h = window.innerHeight;
renderer.setSize(w, h);
renderer.setPixelRatio(dpr);
mat.uniforms.uRes.value.set(w * dpr, h * dpr);
mat.uniforms.uDpr.value = dpr;
}
resize();
window.addEventListener("resize", resize);
function loop() {
requestAnimationFrame(loop);
renderer.render(scene, camera);
}
loop();
} }
// --- Initialize --- // --- Initialize ---
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
// Auto-detect platform and set the right install command const detectedPlatform = detectPlatform();
const detectedPlatform = detectPlatform(); switchPlatform(detectedPlatform);
switchPlatform(detectedPlatform);
initScrollAnimations(); initScrollAnimations();
initNoiseOverlay();
// Terminal demo - start when visible const terminalEl = document.getElementById("terminal-demo");
const terminalEl = document.getElementById('terminal-content');
const cursorEl = document.getElementById('terminal-cursor');
if (terminalEl && cursorEl) {
const demo = new TerminalDemo(terminalEl, cursorEl);
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
demo.start();
} else {
demo.stop();
}
});
}, { threshold: 0.3 });
observer.observe(document.querySelector('.terminal-window')); if (terminalEl) {
} const demo = new TerminalDemo(terminalEl);
// Smooth nav background on scroll const observer = new IntersectionObserver(
const nav = document.querySelector('.nav'); (entries) => {
let ticking = false; entries.forEach((entry) => {
window.addEventListener('scroll', () => { if (entry.isIntersecting) {
if (!ticking) { demo.start();
requestAnimationFrame(() => { } else {
if (window.scrollY > 50) { demo.stop();
nav.style.borderBottomColor = 'rgba(255, 215, 0, 0.1)'; }
} else { });
nav.style.borderBottomColor = ''; },
} { threshold: 0.3 },
ticking = false; );
});
ticking = true; observer.observe(document.querySelector(".terminal-window"));
}
const nav = document.querySelector(".nav");
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
requestAnimationFrame(() => {
if (window.scrollY > 50) {
nav.style.borderBottomColor = "rgba(48, 80, 255, 0.15)";
} else {
nav.style.borderBottomColor = "";
} }
}); ticking = false;
});
ticking = true;
}
});
}); });

File diff suppressed because it is too large Load Diff