Add AMPGateway visual site

This commit is contained in:
jwinterm
2026-07-07 11:10:14 -04:00
parent 7199431a99
commit d48ff76585
5 changed files with 1015 additions and 0 deletions
+2
View File
@@ -110,6 +110,8 @@ API remains internal even when the public site is available on Tor or I2P.
- [Interactive applications](docs/interactive-apps.md)
- [Wownero integration](docs/wownero-integration.md)
- [Autodocs and drift gates](docs/autodocs.md)
- [AMPGateway visual site source](sites/ampgateway-online/index.html)
- [AMPGateway visual site config](examples/ampgateway-online.gateway.toml)
## Checks
+76
View File
@@ -0,0 +1,76 @@
[gateway]
state_dir = "./.ampg/ampgateway-online/state"
cache_dir = "./.ampg/ampgateway-online/cache"
run_dir = "./.ampg/ampgateway-online/run"
user = "ampg"
[profiles.visual-vps]
description = "Full VPS deployment for the AMPGateway visual docs site."
protocols = ["clearnet", "tor", "i2p", "gemini"]
platform = "linux-systemd"
write_artifacts = true
[profiles.visual-lan]
description = "Clearnet-only LAN deployment for the AMPGateway visual docs site."
protocols = ["clearnet"]
platform = "linux-user"
[[site]]
id = "ampgateway-online"
domain = "ampgateway.online"
[site.source]
kind = "static-html"
path = "../sites/ampgateway-online"
canonical_url = "https://ampgateway.online"
[site.outputs]
root = "../dist/ampgateway-online"
plan_root = "../dist/ampg-plan"
[site.interactions]
default_tier = "static"
deny_routes = ["/admin/*", "/internal/*", "/webhooks/*"]
[site.protocols.clearnet]
enabled = true
renderer = "clearnet"
daemon = "nginx"
daemon_policy = "external"
[site.protocols.tor]
enabled = true
renderer = "privacy-html"
daemon = "tor"
daemon_policy = "auto"
local_port = 18180
hidden_service_dir = "/var/lib/ampg/tor/ampgateway-online"
onion_location = "auto"
max_asset_bytes = 1048576
script_policy = "strip"
[site.protocols.i2p]
enabled = true
renderer = "privacy-html"
daemon = "i2pd"
daemon_policy = "auto"
local_port = 18181
keys_file = "ampgateway-online-web.dat"
tunnel_name = "ampgateway-online-web"
max_asset_bytes = 1048576
script_policy = "strip"
[site.protocols.gemini]
enabled = true
renderer = "gemtext"
daemon = "agate"
daemon_policy = "auto"
port = 1965
max_asset_bytes = 1048576
[site.protocols.reticulum]
enabled = false
renderer = "micron"
daemon = "rnsd"
daemon_policy = "auto"
aspect = "web"
+248
View File
@@ -0,0 +1,248 @@
(function () {
const canvas = document.getElementById("topologyCanvas");
const caption = document.getElementById("topologyCaption");
const buttons = Array.from(document.querySelectorAll("[data-topology]"));
const signalWord = document.getElementById("signalWord");
if (!canvas || !caption || buttons.length === 0) {
return;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
return;
}
const palette = {
text: "#f6f1e4",
muted: "rgba(246, 241, 228, 0.52)",
line: "rgba(246, 241, 228, 0.18)",
teal: "#39d4c5",
green: "#8fe388",
amber: "#f0b84a",
coral: "#f06f5f"
};
const topologies = {
vps: {
caption: "A VPS can publish clearnet, Tor, I2P, and Gemini outputs from one AMPG plan.",
nodes: [
["source", "source site", 0.16, 0.42, "amber"],
["ampg", "AMPG", 0.34, 0.42, "teal"],
["vps", "VPS", 0.52, 0.42, "green"],
["web", "clearnet", 0.72, 0.22, "teal"],
["tor", "Tor", 0.78, 0.39, "coral"],
["i2p", "I2P", 0.76, 0.56, "amber"],
["gemini", "Gemini", 0.68, 0.72, "green"]
],
links: [
["source", "ampg"],
["ampg", "vps"],
["vps", "web"],
["vps", "tor"],
["vps", "i2p"],
["vps", "gemini"]
]
},
router: {
caption: "Behind a router, clearnet may need DNS and port choices; Tor and I2P can avoid inbound port forwarding.",
nodes: [
["visitor", "visitor", 0.13, 0.33, "green"],
["dns", "DNS/DDNS", 0.3, 0.24, "amber"],
["router", "router", 0.45, 0.38, "coral"],
["host", "home host", 0.61, 0.38, "teal"],
["ampg", "AMPG", 0.76, 0.38, "teal"],
["tor", "Tor", 0.64, 0.66, "coral"],
["i2p", "I2P", 0.82, 0.66, "amber"]
],
links: [
["visitor", "dns"],
["dns", "router"],
["router", "host"],
["host", "ampg"],
["ampg", "tor"],
["ampg", "i2p"],
["visitor", "tor"],
["visitor", "i2p"]
]
},
phone: {
caption: "A spare Android phone can run Termux packages, AMPG state, and user-space transport services.",
nodes: [
["phone", "Android", 0.24, 0.45, "green"],
["pkg", "pkg", 0.43, 0.26, "amber"],
["ampg", "AMPG", 0.45, 0.45, "teal"],
["state", "state", 0.43, 0.64, "coral"],
["services", "services", 0.64, 0.45, "green"],
["routes", "routes", 0.82, 0.45, "teal"]
],
links: [
["phone", "pkg"],
["phone", "ampg"],
["phone", "state"],
["pkg", "services"],
["ampg", "services"],
["state", "services"],
["services", "routes"]
]
},
browser: {
caption: "Fixture manifests let AMPB or another checker verify every route through the expected transport profile.",
nodes: [
["site", "site", 0.17, 0.5, "amber"],
["ampg", "AMPG", 0.33, 0.5, "teal"],
["manifest", "manifest", 0.5, 0.5, "green"],
["ampb", "AMPB", 0.66, 0.5, "coral"],
["transport", "transport", 0.82, 0.5, "teal"]
],
links: [
["site", "ampg"],
["ampg", "manifest"],
["manifest", "ampb"],
["ampb", "transport"],
["transport", "ampb"]
]
}
};
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
let wordIndex = 0;
let active = "vps";
let width = 0;
let height = 0;
let start = performance.now();
function resize() {
const rect = canvas.getBoundingClientRect();
const ratio = Math.min(window.devicePixelRatio || 1, 2);
width = Math.max(1, rect.width);
height = Math.max(1, rect.height);
canvas.width = Math.floor(width * ratio);
canvas.height = Math.floor(height * ratio);
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
}
function nodeMap(topology) {
return new Map(topology.nodes.map((node) => [node[0], node]));
}
function draw(now) {
const topology = topologies[active];
const nodes = nodeMap(topology);
const tick = (now - start) / 1000;
ctx.clearRect(0, 0, width, height);
drawField(tick);
topology.links.forEach((link, index) => drawLink(nodes.get(link[0]), nodes.get(link[1]), tick, index));
topology.nodes.forEach((node, index) => drawNode(node, tick, index));
requestAnimationFrame(draw);
}
function drawField(tick) {
ctx.save();
ctx.globalAlpha = 0.38;
for (let i = 0; i < 36; i += 1) {
const x = ((i * 137 + tick * 16) % (width + 120)) - 60;
const y = ((i * 71 + tick * 9) % (height + 120)) - 60;
ctx.fillStyle = i % 3 === 0 ? palette.teal : i % 3 === 1 ? palette.amber : palette.green;
ctx.beginPath();
ctx.arc(x, y, i % 2 === 0 ? 1.6 : 1, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawLink(from, to, tick, index) {
if (!from || !to) return;
const ax = from[2] * width;
const ay = from[3] * height;
const bx = to[2] * width;
const by = to[3] * height;
ctx.save();
ctx.strokeStyle = palette.line;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(ax, ay);
ctx.lineTo(bx, by);
ctx.stroke();
const progress = (tick * 0.22 + index * 0.13) % 1;
const px = ax + (bx - ax) * progress;
const py = ay + (by - ay) * progress;
ctx.fillStyle = index % 2 === 0 ? palette.teal : palette.amber;
ctx.beginPath();
ctx.arc(px, py, 3.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawNode(node, tick, index) {
const [, label, nx, ny, colorKey] = node;
const x = nx * width;
const y = ny * height;
const pulse = Math.sin(tick * 2.2 + index) * 2;
const color = palette[colorKey] || palette.teal;
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = 20;
ctx.fillStyle = "rgba(16, 16, 15, 0.86)";
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.roundRect(x - 58, y - 23, 116, 46, 8);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = palette.text;
ctx.font = "700 13px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x, y + pulse * 0.25);
ctx.restore();
}
function setTopology(name) {
if (!topologies[name]) return;
active = name;
caption.textContent = topologies[name].caption;
buttons.forEach((button) => {
const selected = button.dataset.topology === name;
button.classList.toggle("active", selected);
button.setAttribute("aria-pressed", selected ? "true" : "false");
});
}
buttons.forEach((button) => {
button.addEventListener("click", () => setTopology(button.dataset.topology));
});
if (signalWord) {
window.setInterval(() => {
wordIndex = (wordIndex + 1) % words.length;
signalWord.textContent = words[wordIndex];
signalWord.classList.remove("swap");
void signalWord.offsetWidth;
signalWord.classList.add("swap");
}, 1800);
}
if (!CanvasRenderingContext2D.prototype.roundRect) {
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
this.beginPath();
this.moveTo(x + r, y);
this.lineTo(x + w - r, y);
this.quadraticCurveTo(x + w, y, x + w, y + r);
this.lineTo(x + w, y + h - r);
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
this.lineTo(x + r, y + h);
this.quadraticCurveTo(x, y + h, x, y + h - r);
this.lineTo(x, y + r);
this.quadraticCurveTo(x, y, x + r, y);
this.closePath();
};
}
window.addEventListener("resize", resize);
resize();
setTopology(active);
requestAnimationFrame(draw);
})();
+177
View File
@@ -0,0 +1,177 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AMPGateway</title>
<meta
name="description"
content="Visual guide for Adaptive Multi-Protocol Gateway deployments, transports, and example network topologies."
>
<link rel="stylesheet" href="styles.css">
<script defer src="app.js"></script>
</head>
<body>
<a class="skip-link" href="#content">Skip to content</a>
<header class="site-header" aria-label="AMPGateway header">
<a class="brand" href="#top" aria-label="AMPGateway home">
<span class="brand-mark" aria-hidden="true"></span>
<span>AMPGateway</span>
</a>
<nav class="top-nav" aria-label="Primary">
<a href="#topologies">Topologies</a>
<a href="#domains">Domains</a>
<a href="#flow">Flow</a>
<a href="#docs">Spec</a>
</nav>
</header>
<main id="content">
<section id="top" class="hero" aria-labelledby="hero-title">
<canvas id="topologyCanvas" aria-hidden="true"></canvas>
<div class="hero-inner">
<p class="eyebrow">Adaptive Multi-Protocol Gateway</p>
<h1 id="hero-title">AMPGateway</h1>
<p class="hero-copy">
One site source, many transport outputs, one staged deploy spine from package
install to published health checks.
</p>
<p class="signal-line">
publish over
<span id="signalWord" class="signal-word">clearnet</span>
without losing the map.
</p>
<div class="hero-actions" aria-label="Primary sections">
<a class="button primary" href="#topologies">Explore topologies</a>
<a class="button secondary" href="#domains">See example domains</a>
</div>
</div>
<div class="topology-panel" aria-label="Topology modes">
<div class="mode-label">Topology</div>
<div class="segmented" role="group" aria-label="Topology selection">
<button class="mode-button active" type="button" data-topology="vps">VPS</button>
<button class="mode-button" type="button" data-topology="router">Router</button>
<button class="mode-button" type="button" data-topology="phone">Phone</button>
<button class="mode-button" type="button" data-topology="browser">Browser</button>
</div>
<p id="topologyCaption" class="topology-caption" aria-live="polite">
A VPS can publish clearnet, Tor, I2P, and Gemini outputs from one AMPG plan.
</p>
</div>
</section>
<section id="topologies" class="section split" aria-labelledby="topologies-title">
<div>
<p class="eyebrow">Network topologies</p>
<h2 id="topologies-title">Deploy from a clean VPS, a laptop behind a router, or an old phone.</h2>
</div>
<div class="topology-grid">
<article>
<h3>Full VPS</h3>
<p>
<code>ampgateway.site</code> is the target for a full public example with
clearnet, Tor, I2P, and Gemini routes managed from one deploy plan.
</p>
</article>
<article>
<h3>Behind router</h3>
<p>
<code>ampgateway.website</code> is the target for a home or lab deployment
that shows Dynamic DNS, router constraints, and transports that avoid
inbound ports.
</p>
</article>
<article>
<h3>Mobile server</h3>
<p>
Android and Termux can run user-space daemons for small static sites,
demos, and transport experiments on hardware that would otherwise sit idle.
</p>
</article>
<article>
<h3>Single transport</h3>
<p>
AMPG can deploy only Tor, only I2P, only Gemini, or a selected subset when
the operator wants a narrow surface.
</p>
</article>
</div>
</section>
<section id="domains" class="section domain-section" aria-labelledby="domains-title">
<div class="section-heading">
<p class="eyebrow">Example domains</p>
<h2 id="domains-title">Three public names, three jobs.</h2>
</div>
<div class="domain-grid">
<article class="domain-card featured">
<p class="domain-label">Visual hub</p>
<h3>ampgateway.online</h3>
<p>
Public explanation site with interactive topology views, deployment flow,
and links into the clean docs.
</p>
<a href="https://ampgateway.online">ampgateway.online</a>
</article>
<article class="domain-card">
<p class="domain-label">VPS example</p>
<h3>ampgateway.site</h3>
<p>
Planned all-transport reference deployment on a normal server, useful for
showing the complete package, state, supervisor, start, address, and health path.
</p>
<a href="https://ampgateway.site">ampgateway.site</a>
</article>
<article class="domain-card">
<p class="domain-label">Behind-router example</p>
<h3>ampgateway.website</h3>
<p>
Planned home-network reference deployment for Dynamic DNS, port-forwarding
choices, and transports that do not need clearnet inbound reachability.
</p>
<a href="https://ampgateway.website">ampgateway.website</a>
</article>
</div>
</section>
<section id="flow" class="section flow-section" aria-labelledby="flow-title">
<div class="section-heading">
<p class="eyebrow">Deploy spine</p>
<h2 id="flow-title">Narrow stages, visible state, reversible thinking.</h2>
</div>
<ol class="flow-list">
<li><span>Build</span><p>Generate clearnet, privacy HTML, Gemtext, and other selected outputs.</p></li>
<li><span>Packages</span><p>Install selected daemon packages with allowlisted platform commands.</p></li>
<li><span>State</span><p>Copy approved generated config into AMPG-owned state.</p></li>
<li><span>Supervisor</span><p>Install AMPG-named service files after config exists.</p></li>
<li><span>Start</span><p>Start AMPG-owned services with structured platform commands.</p></li>
<li><span>Addresses</span><p>Capture daemon-written onion, I2P, or other public identities.</p></li>
<li><span>Health</span><p>Verify published fixture URLs through the selected transport profiles.</p></li>
</ol>
</section>
<section id="docs" class="section docs-section" aria-labelledby="docs-title">
<div>
<p class="eyebrow">Public docs</p>
<h2 id="docs-title">The visual site stays tied to the public spec.</h2>
<p>
The source for this page lives in the AMPG repository and can be rendered by
AMPG like any other static site. The Markdown docs stay concise; this site
turns the same concepts into topology-first views.
</p>
</div>
<div class="doc-links">
<a href="#topologies">Topology guide</a>
<a href="#flow">Deploy stages</a>
<a href="#domains">Domain examples</a>
<a href="#top">Transport map</a>
</div>
</section>
</main>
<footer class="site-footer">
<p>AMPGateway is the public visual companion for Adaptive Multi-Protocol Gateway.</p>
<p>Static-first. Transport-aware. Operator-reviewable.</p>
</footer>
</body>
</html>
+512
View File
@@ -0,0 +1,512 @@
:root {
color-scheme: dark;
--bg: #10100f;
--panel: #1b1c1a;
--panel-strong: #25241f;
--text: #f6f1e4;
--muted: #b8b0a0;
--line: rgba(246, 241, 228, 0.18);
--teal: #39d4c5;
--green: #8fe388;
--amber: #f0b84a;
--coral: #f06f5f;
--ink: #080808;
--max: 1160px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background:
linear-gradient(rgba(246, 241, 228, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(246, 241, 228, 0.03) 1px, transparent 1px),
var(--bg);
background-size: 36px 36px;
color: var(--text);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
line-height: 1.5;
}
a {
color: inherit;
}
.skip-link {
position: fixed;
top: 12px;
left: 12px;
z-index: 20;
padding: 8px 10px;
background: var(--text);
color: var(--ink);
transform: translateY(-140%);
}
.skip-link:focus {
transform: translateY(0);
}
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 18px clamp(18px, 4vw, 48px);
background: rgba(16, 16, 15, 0.78);
border-bottom: 1px solid var(--line);
backdrop-filter: blur(18px);
}
.brand,
.top-nav {
display: flex;
align-items: center;
}
.brand {
gap: 10px;
font-weight: 800;
text-decoration: none;
}
.brand-mark {
width: 18px;
height: 18px;
border: 2px solid var(--teal);
box-shadow: inset 0 0 0 4px var(--bg), 0 0 18px rgba(57, 212, 197, 0.4);
}
.top-nav {
gap: clamp(10px, 2vw, 22px);
color: var(--muted);
font-size: 0.95rem;
}
.top-nav a {
text-decoration: none;
}
.top-nav a:hover,
.top-nav a:focus-visible {
color: var(--text);
}
.hero {
position: relative;
min-height: min(780px, 88vh);
overflow: hidden;
border-bottom: 1px solid var(--line);
}
#topologyCanvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.hero::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(90deg, rgba(16, 16, 15, 0.94), rgba(16, 16, 15, 0.58) 48%, rgba(16, 16, 15, 0.2));
}
.hero-inner {
position: relative;
z-index: 1;
max-width: var(--max);
margin: 0 auto;
padding: clamp(118px, 16vh, 168px) clamp(18px, 4vw, 48px) 160px;
}
.eyebrow,
.domain-label,
.mode-label {
margin: 0 0 12px;
color: var(--amber);
font-size: 0.76rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-top: 0;
}
h1 {
max-width: 760px;
margin-bottom: 16px;
font-size: clamp(3.8rem, 10vw, 8.5rem);
line-height: 0.9;
letter-spacing: 0;
}
h2 {
max-width: 780px;
margin-bottom: 22px;
font-size: clamp(2rem, 4vw, 4.25rem);
line-height: 1.02;
letter-spacing: 0;
}
h3 {
margin-bottom: 10px;
font-size: 1.16rem;
letter-spacing: 0;
}
.hero-copy {
max-width: 680px;
color: var(--muted);
font-size: clamp(1.08rem, 2vw, 1.42rem);
}
.signal-line {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: baseline;
max-width: 680px;
color: var(--text);
font-size: clamp(1rem, 1.8vw, 1.28rem);
}
.signal-word {
color: var(--teal);
font-weight: 800;
}
.signal-word.swap {
animation: text-scan 420ms ease both;
}
@keyframes text-scan {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 34px;
}
.button {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 10px 16px;
border: 1px solid var(--line);
border-radius: 6px;
font-weight: 800;
text-decoration: none;
}
.button.primary {
background: var(--teal);
color: var(--ink);
border-color: var(--teal);
}
.button.secondary {
background: rgba(246, 241, 228, 0.06);
}
.topology-panel {
position: absolute;
right: clamp(18px, 4vw, 48px);
bottom: 30px;
z-index: 2;
width: min(440px, calc(100% - 36px));
padding: 16px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(27, 28, 26, 0.82);
backdrop-filter: blur(18px);
}
.segmented {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
}
.mode-button {
min-height: 38px;
border: 1px solid var(--line);
border-radius: 6px;
background: rgba(246, 241, 228, 0.05);
color: var(--text);
font: inherit;
font-size: 0.9rem;
font-weight: 800;
cursor: pointer;
}
.mode-button.active,
.mode-button:focus-visible {
background: var(--amber);
color: var(--ink);
border-color: var(--amber);
}
.topology-caption {
min-height: 48px;
margin: 14px 0 0;
color: var(--muted);
font-size: 0.95rem;
}
.section {
width: min(var(--max), calc(100% - 36px));
margin: 0 auto;
padding: clamp(64px, 10vw, 112px) 0;
}
.split {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: clamp(28px, 6vw, 72px);
align-items: start;
}
.topology-grid,
.domain-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.topology-grid article,
.domain-card {
min-height: 190px;
padding: 20px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.topology-grid article:nth-child(2),
.domain-card.featured {
background: var(--panel-strong);
}
.topology-grid p,
.domain-card p,
.docs-section p,
.flow-list p {
color: var(--muted);
}
.section-heading {
margin-bottom: 28px;
}
.domain-grid {
grid-template-columns: 1.2fr 1fr 1fr;
}
.domain-card {
display: flex;
flex-direction: column;
}
.domain-card h3 {
font-size: clamp(1.35rem, 2.2vw, 2rem);
word-break: break-word;
}
.domain-card a {
margin-top: auto;
color: var(--teal);
font-weight: 800;
text-decoration: none;
}
.domain-card a:hover,
.domain-card a:focus-visible {
text-decoration: underline;
}
.flow-list {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 10px;
padding: 0;
list-style: none;
counter-reset: flow;
}
.flow-list li {
position: relative;
min-height: 220px;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
counter-increment: flow;
}
.flow-list li::before {
content: counter(flow, decimal-leading-zero);
display: block;
margin-bottom: 26px;
color: var(--coral);
font-weight: 900;
}
.flow-list span {
display: block;
margin-bottom: 10px;
font-weight: 900;
}
.docs-section {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: clamp(28px, 6vw, 72px);
align-items: start;
}
.doc-links {
display: grid;
gap: 10px;
}
.doc-links a {
display: flex;
align-items: center;
min-height: 48px;
padding: 12px 14px;
border: 1px solid var(--line);
border-radius: 6px;
background: rgba(246, 241, 228, 0.05);
color: var(--text);
font-weight: 800;
text-decoration: none;
}
.doc-links a:hover,
.doc-links a:focus-visible {
border-color: var(--teal);
}
.site-footer {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 28px clamp(18px, 4vw, 48px);
border-top: 1px solid var(--line);
color: var(--muted);
}
.site-footer p {
margin: 0;
}
@media (max-width: 960px) {
.top-nav {
display: none;
}
.hero {
min-height: 860px;
}
.hero::after {
background: linear-gradient(rgba(16, 16, 15, 0.94), rgba(16, 16, 15, 0.48));
}
.hero-inner {
padding-bottom: 280px;
}
.split,
.docs-section {
grid-template-columns: 1fr;
}
.domain-grid,
.topology-grid {
grid-template-columns: 1fr;
}
.flow-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.site-header {
padding: 14px 18px;
}
h1 {
font-size: clamp(3rem, 18vw, 5rem);
}
.hero-actions,
.signal-line {
display: grid;
}
.button,
.mode-button {
width: 100%;
justify-content: center;
}
.topology-panel {
bottom: 18px;
}
.segmented,
.flow-list {
grid-template-columns: 1fr;
}
.flow-list li {
min-height: 0;
}
.site-footer {
display: grid;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
scroll-behavior: auto !important;
transition-duration: 1ms !important;
}
}