mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Show AMPGateway maps as primary visuals
This commit is contained in:
+165
-137
@@ -1,18 +1,6 @@
|
||||
(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)",
|
||||
@@ -110,127 +98,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
if (window.CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) {
|
||||
CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) {
|
||||
this.beginPath();
|
||||
this.moveTo(x + r, y);
|
||||
@@ -246,8 +114,168 @@
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
setTopology(active);
|
||||
requestAnimationFrame(draw);
|
||||
function createTopologyMap(options) {
|
||||
const canvas = document.getElementById(options.canvasId);
|
||||
const caption = document.getElementById(options.captionId);
|
||||
const buttons = Array.from(document.querySelectorAll(options.buttonSelector));
|
||||
if (!canvas || !caption || buttons.length === 0) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let active = options.initial || "vps";
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
const 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 = options.fieldAlpha || 0.38;
|
||||
const dotCount = options.dotCount || 36;
|
||||
for (let i = 0; i < dotCount; 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 = options.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, options.packetRadius || 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 boxWidth = options.nodeWidth || 116;
|
||||
const boxHeight = options.nodeHeight || 46;
|
||||
const pulse = Math.sin(tick * 2.2 + index) * 2;
|
||||
const color = palette[colorKey] || palette.teal;
|
||||
ctx.save();
|
||||
ctx.shadowColor = color;
|
||||
ctx.shadowBlur = options.shadowBlur || 20;
|
||||
ctx.fillStyle = "rgba(16, 16, 15, 0.86)";
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x - boxWidth / 2, y - boxHeight / 2, boxWidth, boxHeight, 8);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.fillStyle = palette.text;
|
||||
ctx.font = `${options.fontWeight || 700} ${options.fontSize || 13}px 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[options.dataKey] === name;
|
||||
button.classList.toggle("active", selected);
|
||||
button.setAttribute("aria-pressed", selected ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener("click", () => setTopology(button.dataset[options.dataKey]));
|
||||
});
|
||||
|
||||
window.addEventListener("resize", resize);
|
||||
resize();
|
||||
setTopology(active);
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
createTopologyMap({
|
||||
canvasId: "topologyCanvas",
|
||||
captionId: "topologyCaption",
|
||||
buttonSelector: "[data-topology]",
|
||||
dataKey: "topology",
|
||||
initial: "vps"
|
||||
});
|
||||
|
||||
createTopologyMap({
|
||||
canvasId: "cleanTopologyCanvas",
|
||||
captionId: "cleanTopologyCaption",
|
||||
buttonSelector: "[data-clean-topology]",
|
||||
dataKey: "cleanTopology",
|
||||
initial: "vps",
|
||||
dotCount: 70,
|
||||
fieldAlpha: 0.5,
|
||||
lineWidth: 1.4,
|
||||
packetRadius: 4.5,
|
||||
nodeWidth: 144,
|
||||
nodeHeight: 54,
|
||||
fontSize: 15,
|
||||
fontWeight: 800,
|
||||
shadowBlur: 26
|
||||
});
|
||||
|
||||
if (signalWord) {
|
||||
const words = ["clearnet", "Tor", "I2P", "Gemini", "IPFS", "Reticulum"];
|
||||
let wordIndex = 0;
|
||||
window.setInterval(() => {
|
||||
wordIndex = (wordIndex + 1) % words.length;
|
||||
signalWord.textContent = words[wordIndex];
|
||||
signalWord.classList.remove("swap");
|
||||
void signalWord.offsetWidth;
|
||||
signalWord.classList.add("swap");
|
||||
}, 1800);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
<span>AMPGateway</span>
|
||||
</a>
|
||||
<nav class="top-nav" aria-label="Primary">
|
||||
<a href="#transport-flows">Transports</a>
|
||||
<a href="#live-map">Live Map</a>
|
||||
<a href="#protocol-maps">Protocol Maps</a>
|
||||
<a href="#topologies">Topologies</a>
|
||||
<a href="#domains">Domains</a>
|
||||
<a href="#flow">Flow</a>
|
||||
@@ -45,7 +46,7 @@
|
||||
without losing the map.
|
||||
</p>
|
||||
<div class="hero-actions" aria-label="Primary sections">
|
||||
<a class="button primary" href="#transport-flows">Explore transports</a>
|
||||
<a class="button primary" href="#live-map">Open the map</a>
|
||||
<a class="button secondary" href="#domains">See example domains</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,63 +64,107 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="transport-flows" class="section transport-section" aria-labelledby="transport-flows-title">
|
||||
<section id="live-map" class="section live-map-section" aria-labelledby="live-map-title">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Traffic flows</p>
|
||||
<h2 id="transport-flows-title">Same source, different paths through the network.</h2>
|
||||
<p class="eyebrow">Topology theatre</p>
|
||||
<h2 id="live-map-title">The animated maps, unobstructed.</h2>
|
||||
<p>
|
||||
AMPG keeps the authoring path consistent while each transport uses the shape
|
||||
that fits its network, daemon, and client expectations.
|
||||
The same live topology views from the hero are given a full clean stage here,
|
||||
so the moving routes, nodes, and deployment shapes can be read directly.
|
||||
</p>
|
||||
</div>
|
||||
<div class="transport-flow-grid">
|
||||
<article class="transport-flow">
|
||||
<div class="map-theatre" aria-label="Interactive topology map">
|
||||
<canvas id="cleanTopologyCanvas" aria-hidden="true"></canvas>
|
||||
<div class="map-theatre-controls">
|
||||
<div class="mode-label">View</div>
|
||||
<div class="segmented" role="group" aria-label="Clean topology selection">
|
||||
<button class="mode-button active" type="button" data-clean-topology="vps">VPS</button>
|
||||
<button class="mode-button" type="button" data-clean-topology="router">Router</button>
|
||||
<button class="mode-button" type="button" data-clean-topology="phone">Phone</button>
|
||||
<button class="mode-button" type="button" data-clean-topology="browser">Browser</button>
|
||||
</div>
|
||||
<p id="cleanTopologyCaption" class="topology-caption" aria-live="polite">
|
||||
A VPS can publish clearnet, Tor, I2P, Gemini, and Reticulum outputs from one AMPG plan.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="protocol-maps" class="section protocol-map-section" aria-labelledby="protocol-maps-title">
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">Protocol maps</p>
|
||||
<h2 id="protocol-maps-title">How traffic moves on each transport.</h2>
|
||||
<p>
|
||||
These are the big maps: what the client talks to, where AMPG output sits,
|
||||
and which network pieces carry the request.
|
||||
</p>
|
||||
</div>
|
||||
<div class="protocol-map-stack">
|
||||
<article class="protocol-map">
|
||||
<h3>Clearnet</h3>
|
||||
<ol>
|
||||
<li>Visitor resolves DNS for the site domain.</li>
|
||||
<li>HTTP or HTTPS reaches the public web server.</li>
|
||||
<li>The server reads AMPG clearnet output.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="Clearnet request path">
|
||||
<span>Visitor browser</span>
|
||||
<span>DNS</span>
|
||||
<span>HTTPS ingress</span>
|
||||
<span>Web server</span>
|
||||
<span>AMPG clearnet output</span>
|
||||
</div>
|
||||
<p>Best when a public domain and inbound web reachability are available.</p>
|
||||
</article>
|
||||
<article class="transport-flow">
|
||||
<article class="protocol-map">
|
||||
<h3>Tor onion</h3>
|
||||
<ol>
|
||||
<li>Tor client resolves the onion service descriptor.</li>
|
||||
<li>Tor circuits connect to the hidden service.</li>
|
||||
<li>The local service serves AMPG privacy HTML.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="Tor onion request path">
|
||||
<span>Tor Browser</span>
|
||||
<span>Tor circuits</span>
|
||||
<span>Onion service</span>
|
||||
<span>Local HTTP</span>
|
||||
<span>AMPG privacy HTML</span>
|
||||
</div>
|
||||
<p>The public address is the onion name; the origin can stay behind local loopback.</p>
|
||||
</article>
|
||||
<article class="transport-flow">
|
||||
<article class="protocol-map">
|
||||
<h3>I2P tunnel</h3>
|
||||
<ol>
|
||||
<li>I2P client resolves the b32 destination.</li>
|
||||
<li>Inbound and outbound tunnels carry HTTP.</li>
|
||||
<li>i2pd forwards to the local AMPG output.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="I2P tunnel request path">
|
||||
<span>I2P browser</span>
|
||||
<span>b32 destination</span>
|
||||
<span>I2P tunnels</span>
|
||||
<span>i2pd server tunnel</span>
|
||||
<span>AMPG privacy HTML</span>
|
||||
</div>
|
||||
<p>The b32 address points at the I2P destination, not a normal DNS hostname.</p>
|
||||
</article>
|
||||
<article class="transport-flow">
|
||||
<article class="protocol-map">
|
||||
<h3>Gemini</h3>
|
||||
<ol>
|
||||
<li>Client opens a TLS connection to the capsule.</li>
|
||||
<li>Gemini server maps the URL to Gemtext.</li>
|
||||
<li>The rendered page keeps links and headings simple.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="Gemini request path">
|
||||
<span>Gemini client</span>
|
||||
<span>TLS capsule</span>
|
||||
<span>Gemini daemon</span>
|
||||
<span>Gemtext tree</span>
|
||||
<span>AMPG text render</span>
|
||||
</div>
|
||||
<p>Small pages, simple links, and text-first structure make the capsule readable.</p>
|
||||
</article>
|
||||
<article class="transport-flow">
|
||||
<article class="protocol-map">
|
||||
<h3>Reticulum</h3>
|
||||
<ol>
|
||||
<li>Client reaches an RNS destination.</li>
|
||||
<li>NomadNet-style pages request compact Micron content.</li>
|
||||
<li>AMPG output keeps media and layout intentionally small.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="Reticulum request path">
|
||||
<span>RNS client</span>
|
||||
<span>Reticulum mesh</span>
|
||||
<span>Destination hash</span>
|
||||
<span>NomadNet node</span>
|
||||
<span>AMPG Micron output</span>
|
||||
</div>
|
||||
<p>Identity is destination-based, and content should stay compact by default.</p>
|
||||
</article>
|
||||
<article class="transport-flow">
|
||||
<article class="protocol-map">
|
||||
<h3>Single transport</h3>
|
||||
<ol>
|
||||
<li>Operator enables only the target protocol.</li>
|
||||
<li>AMPG builds only the selected output.</li>
|
||||
<li>Deploy stages skip unrelated daemons and DNS.</li>
|
||||
</ol>
|
||||
<div class="path-map" aria-label="Single transport deployment path">
|
||||
<span>Source site</span>
|
||||
<span>AMPG selection</span>
|
||||
<span>One renderer</span>
|
||||
<span>One daemon</span>
|
||||
<span>One published route</span>
|
||||
</div>
|
||||
<p>Useful for Tor-only, I2P-only, Gemini-only, or Reticulum-focused sites.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -112,7 +112,8 @@ a {
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
#topologyCanvas {
|
||||
#topologyCanvas,
|
||||
#cleanTopologyCanvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
@@ -299,7 +300,7 @@ h3 {
|
||||
|
||||
.topology-grid,
|
||||
.domain-grid,
|
||||
.transport-flow-grid {
|
||||
.protocol-map-stack {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
@@ -307,7 +308,7 @@ h3 {
|
||||
|
||||
.topology-grid article,
|
||||
.domain-card,
|
||||
.transport-flow {
|
||||
.protocol-map {
|
||||
min-height: 190px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -317,7 +318,7 @@ h3 {
|
||||
|
||||
.topology-grid article:nth-child(2),
|
||||
.domain-card.featured,
|
||||
.transport-flow:nth-child(2n) {
|
||||
.protocol-map:nth-child(2n) {
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
@@ -325,7 +326,7 @@ h3 {
|
||||
.domain-card p,
|
||||
.docs-section p,
|
||||
.flow-list p,
|
||||
.transport-flow li,
|
||||
.protocol-map p,
|
||||
.section-heading p {
|
||||
color: var(--muted);
|
||||
}
|
||||
@@ -334,23 +335,121 @@ h3 {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.transport-flow-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
.live-map-section {
|
||||
width: min(1440px, calc(100% - 36px));
|
||||
}
|
||||
|
||||
.transport-flow {
|
||||
min-height: 250px;
|
||||
.map-theatre {
|
||||
position: relative;
|
||||
min-height: clamp(540px, 72vw, 820px);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #10100f;
|
||||
}
|
||||
|
||||
.transport-flow h3 {
|
||||
font-size: 1.35rem;
|
||||
.map-theatre::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(rgba(16, 16, 15, 0.08), rgba(16, 16, 15, 0.28)),
|
||||
radial-gradient(circle at 50% 50%, transparent 0 56%, rgba(16, 16, 15, 0.42) 100%);
|
||||
}
|
||||
|
||||
.transport-flow ol {
|
||||
.map-theatre-controls {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
bottom: 18px;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 16px 0 0;
|
||||
padding-left: 20px;
|
||||
grid-template-columns: 84px minmax(0, 520px) minmax(260px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(16, 16, 15, 0.78);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.map-theatre-controls .mode-label,
|
||||
.map-theatre-controls .topology-caption {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.protocol-map-section {
|
||||
width: min(1360px, calc(100% - 36px));
|
||||
}
|
||||
|
||||
.protocol-map-stack {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.protocol-map {
|
||||
display: grid;
|
||||
grid-template-columns: 180px minmax(0, 1fr) 280px;
|
||||
gap: clamp(18px, 3vw, 34px);
|
||||
align-items: center;
|
||||
min-height: 210px;
|
||||
}
|
||||
|
||||
.protocol-map h3 {
|
||||
margin-bottom: 0;
|
||||
font-size: clamp(1.45rem, 2.4vw, 2.4rem);
|
||||
}
|
||||
|
||||
.path-map {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.path-map span {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 78px;
|
||||
place-items: center;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(246, 241, 228, 0.26);
|
||||
border-radius: 8px;
|
||||
background: rgba(246, 241, 228, 0.055);
|
||||
color: var(--text);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.path-map span:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -13px;
|
||||
width: 13px;
|
||||
height: 2px;
|
||||
background: var(--amber);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.path-map span:nth-child(1) {
|
||||
border-color: var(--green);
|
||||
}
|
||||
|
||||
.path-map span:nth-child(2),
|
||||
.path-map span:nth-child(3) {
|
||||
border-color: var(--amber);
|
||||
}
|
||||
|
||||
.path-map span:nth-child(4) {
|
||||
border-color: var(--coral);
|
||||
}
|
||||
|
||||
.path-map span:nth-child(5) {
|
||||
border-color: var(--teal);
|
||||
}
|
||||
|
||||
.domain-grid {
|
||||
@@ -478,11 +577,33 @@ h3 {
|
||||
}
|
||||
|
||||
.domain-grid,
|
||||
.topology-grid,
|
||||
.transport-flow-grid {
|
||||
.topology-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theatre-controls,
|
||||
.protocol-map {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theatre {
|
||||
min-height: 680px;
|
||||
}
|
||||
|
||||
.path-map {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.path-map span:not(:last-child)::after {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: -13px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 13px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.flow-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -517,6 +638,16 @@ h3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-theatre {
|
||||
min-height: 760px;
|
||||
}
|
||||
|
||||
.map-theatre-controls {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
.flow-list li {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user