* feat: recursive resolution + full DNSSEC validation Numa becomes a true DNS resolver — resolves from root nameservers with complete DNSSEC chain-of-trust verification. Recursive resolution: - Iterative RFC 1034 from configurable root hints (13 default) - CNAME chasing (depth 8), referral following (depth 10) - A+AAAA glue extraction, IPv6 nameserver support - TLD priming: NS + DS + DNSKEY for 34 gTLDs + EU ccTLDs - Config: mode = "recursive" in [upstream], root_hints, prime_tlds DNSSEC (all 4 phases): - EDNS0 OPT pseudo-record (DO bit, 1232 payload per DNS Flag Day 2020) - DNSKEY, DS, RRSIG, NSEC, NSEC3 record types with wire read/write - Signature verification via ring: RSA/SHA-256, ECDSA P-256, Ed25519 - Chain-of-trust: zone DNSKEY → parent DS → root KSK (key tag 20326) - DNSKEY RRset self-signature verification (RRSIG(DNSKEY) by KSK) - RRSIG expiration/inception time validation - NSEC: NXDOMAIN gap proofs, NODATA type absence, wildcard denial - NSEC3: SHA-1 iterated hashing, closest encloser proof, hash range - Authority RRSIG verification for denial proofs - Config: [dnssec] enabled/strict (default false, opt-in) - AD bit on Secure, SERVFAIL on Bogus+strict - DnssecStatus cached per entry, ValidationStats logging Performance: - TLD chain pre-warmed on startup (root DNSKEY + TLD DS/DNSKEY) - Referral DS piggybacking from authority sections - DNSKEY prefetch before validation loop - Cold-cache validation: ~1 DNSKEY fetch (down from 5) - Benchmarks: RSA 10.9µs, ECDSA 174ns, DS verify 257ns Also: - write_qname fix for root domain "." (was producing malformed queries) - write_record_header() dedup, write_bytes() bulk writes - DnsRecord::domain() + query_type() accessors - UpstreamMode enum, DEFAULT_EDNS_PAYLOAD const - Real glue TTL (was hardcoded 3600) - DNSSEC restricted to recursive mode only Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: TCP fallback, query minimization, UDP auto-disable Transport resilience for restrictive networks (ISPs blocking UDP:53): - DNS-over-TCP fallback: UDP fail/truncation → automatic TCP retry - UDP auto-disable: after 3 consecutive failures, switch to TCP-first - IPv6 → TCP directly (UDP socket binds 0.0.0.0, can't reach IPv6) - Network change resets UDP detection for re-probing - Root hint rotation in TLD priming Privacy: - RFC 7816 query minimization: root servers see TLD only, not full name Code quality: - Merged find_starting_ns + find_starting_zone → find_closest_ns - Extracted resolve_ns_addrs_from_glue shared helper - Removed overall timeout wrapper (per-hop timeouts sufficient) - forward_tcp for DNS-over-TCP (RFC 1035 §4.2.2) Testing: - Mock TCP-only DNS server for fallback tests (no network needed) - tcp_fallback_resolves_when_udp_blocked - tcp_only_iterative_resolution - tcp_fallback_handles_nxdomain - udp_auto_disable_resets - Integration test suite (4 suites, 51 tests) - Network probe script (tests/network-probe.sh) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: DNSSEC verified badge in dashboard query log - Add dnssec field to QueryLogEntry, track validation status per query - DnssecStatus::as_str() for API serialization - Dashboard shows green checkmark next to DNSSEC-verified responses - Blog post: add "How keys get there" section, transport resilience section, trim code blocks, update What's Next Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use SVG shield for DNSSEC badge, update blog HTML Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: NS cache lookup from authorities, UDP re-probe, shield alignment - find_closest_ns checks authorities (not just answers) for NS records, fixing TLD priming cache misses that caused redundant root queries - Periodic UDP re-probe every 5min when disabled — re-enables UDP after switching from a restrictive network to an open one - Dashboard DNSSEC shield uses fixed-width container for alignment - Blog post: tuck key-tag into trust anchor paragraph Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: TCP single-write, mock server consistency, integration tests - TCP single-write fix: combine length prefix + message to avoid split segments that Microsoft/Azure DNS servers reject - Mock server (spawn_tcp_dns_server) updated to use single-write too - Tests: forward_tcp_wire_format, forward_tcp_single_segment_write - Integration: real-server checks for Microsoft/Office/Azure domains Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: recursive bar in dashboard, special-use domain interception Dashboard: - Add Recursive bar to resolution paths chart (cyan, distinct from Override) - Add RECURSIVE path tag style in query log Special-use domains (RFC 6761/6303/8880/9462): - .localhost → 127.0.0.1 (RFC 6761) - Private reverse PTR (10.x, 192.168.x, 172.16-31.x) → NXDOMAIN - _dns.resolver.arpa (DDR) → NXDOMAIN - ipv4only.arpa (NAT64) → 192.0.0.170/171 - mDNS service discovery for private ranges → NXDOMAIN Eliminates ~900ms SERVFAILs for macOS system queries that were hitting root servers unnecessarily. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: move generated blog HTML to site/blog/posts/, gitignore - Generated HTML now in site/blog/posts/ (gitignored) - CI workflow runs pandoc + make blog before deploy - Updated all internal blog links to /blog/posts/ path - blog/*.md remains the source of truth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: review feedback — memory ordering, RRSIG time, NS resolution - Ordering::Relaxed → Acquire/Release for UDP_DISABLED/UDP_FAILURES (ARM correctness for cross-thread coordination) - RRSIG time validation: serial number arithmetic (RFC 4034 §3.1.5) + 300s clock skew fudge factor (matches BIND) - resolve_ns_addrs_from_glue collects addresses from ALL NS names, not just the first with glue (improves failover) - is_special_use_domain: eliminate 16 format! allocations per .in-addr.arpa query (parse octet instead) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: API endpoint tests, coverage target - 8 new axum handler tests: health, stats, query-log, overrides CRUD, cache, blocking stats, services CRUD, dashboard HTML - Tests use tower::oneshot — no network, no server startup - test_ctx() builds minimal ServerCtx for isolated testing - `make coverage` target (cargo-tarpaulin), separate from `make all` - 82 total tests (was 74) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1691 lines
50 KiB
HTML
1691 lines
50 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Numa — DNS you own. Everywhere you go.</title>
|
|
<meta name="description" content="DNS you own. Recursive resolver with full DNSSEC validation, ad blocking, .numa local domains, developer overrides. A single portable binary built from scratch in Rust.">
|
|
<link rel="canonical" href="https://numa.rs">
|
|
<meta property="og:title" content="Numa — DNS you own. Everywhere you go.">
|
|
<meta property="og:description" content="Recursive DNS resolver with full DNSSEC validation, ad blocking, .numa local domains, and developer overrides. Built from scratch in Rust.">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://numa.rs">
|
|
<link rel="stylesheet" href="/fonts/fonts.css">
|
|
<style>
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
:root {
|
|
/* Roman Stone */
|
|
--bg-deep: #f5f0e8;
|
|
--bg-surface: #ece5da;
|
|
--bg-elevated: #e3dbce;
|
|
--bg-card: #faf7f2;
|
|
--amber: #c0623a;
|
|
--amber-dim: #9e4e2d;
|
|
--amber-glow: rgba(192, 98, 58, 0.07);
|
|
--amber-glow-strong: rgba(192, 98, 58, 0.14);
|
|
--teal: #6b7c4e;
|
|
--teal-dim: #566540;
|
|
--teal-glow: rgba(107, 124, 78, 0.06);
|
|
--violet: #64748b;
|
|
--violet-dim: #4a5568;
|
|
--violet-glow: rgba(100, 116, 139, 0.06);
|
|
--emerald: #527a52;
|
|
--emerald-dim: #3f6340;
|
|
--rose: #b5443a;
|
|
--rose-dim: #943832;
|
|
--rose-glow: rgba(181, 68, 58, 0.05);
|
|
--cyan: #4a7c8a;
|
|
--text-primary: #2c2418;
|
|
--text-secondary: #6b5e4f;
|
|
--text-dim: #a39888;
|
|
--border: rgba(0, 0, 0, 0.08);
|
|
--border-amber: rgba(192, 98, 58, 0.22);
|
|
--border-teal: rgba(107, 124, 78, 0.20);
|
|
--border-violet: rgba(100, 116, 139, 0.18);
|
|
--font-display: 'Instrument Serif', Georgia, serif;
|
|
--font-body: 'DM Sans', system-ui, sans-serif;
|
|
--font-mono: 'JetBrains Mono', monospace;
|
|
}
|
|
|
|
html { scroll-behavior: smooth; }
|
|
|
|
body {
|
|
background: var(--bg-deep);
|
|
color: var(--text-primary);
|
|
font-family: var(--font-body);
|
|
font-weight: 400;
|
|
line-height: 1.7;
|
|
-webkit-font-smoothing: antialiased;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* --- Grain overlay --- */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.025'/%3E%3C/svg%3E");
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* --- Layout --- */
|
|
.container {
|
|
max-width: 1120px;
|
|
margin: 0 auto;
|
|
padding: 0 2rem;
|
|
}
|
|
|
|
section {
|
|
padding: 7rem 0;
|
|
position: relative;
|
|
}
|
|
|
|
/* --- Reveal animations --- */
|
|
.reveal {
|
|
opacity: 0;
|
|
transform: translateY(28px);
|
|
transition: opacity 0.7s cubic-bezier(0.22, 1, 0.36, 1), transform 0.7s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
.reveal.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
.reveal-delay-1 { transition-delay: 0.1s; }
|
|
.reveal-delay-2 { transition-delay: 0.2s; }
|
|
.reveal-delay-3 { transition-delay: 0.3s; }
|
|
.reveal-delay-4 { transition-delay: 0.4s; }
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.reveal { opacity: 1; transform: none; transition: none; }
|
|
.hero .wordmark, .hero .tagline, .hero .description, .hero-actions { opacity: 1; animation: none; }
|
|
}
|
|
|
|
/* --- Accessibility --- */
|
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0; }
|
|
.btn:focus-visible { outline: 2px solid var(--amber); outline-offset: 2px; }
|
|
.btn-ghost:focus-visible { outline: 2px solid var(--amber-dim); outline-offset: 2px; }
|
|
|
|
/* --- Roman road brick pattern --- */
|
|
.roman-bricks {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg width='120' height='60' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='61' y='1' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='31' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='-29' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3Crect x='91' y='31' width='56' height='27' rx='1' fill='none' stroke='%23a39888' stroke-width='0.5' opacity='0.18'/%3E%3C/svg%3E");
|
|
background-size: 120px 60px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Hero bricks: fade from right/bottom, invisible where text sits */
|
|
.hero .roman-bricks {
|
|
-webkit-mask-image: linear-gradient(135deg, transparent 25%, rgba(0,0,0,0.25) 55%, rgba(0,0,0,0.5) 100%);
|
|
mask-image: linear-gradient(135deg, transparent 25%, rgba(0,0,0,0.25) 55%, rgba(0,0,0,0.5) 100%);
|
|
}
|
|
|
|
/* Section divider road band */
|
|
.section-road {
|
|
height: 28px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.section-road .roman-bricks {
|
|
opacity: 0.5;
|
|
}
|
|
.section-road::before,
|
|
.section-road::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 140px;
|
|
z-index: 1;
|
|
}
|
|
.section-road::before { left: 0; background: linear-gradient(to right, var(--bg-deep), transparent); }
|
|
.section-road::after { right: 0; background: linear-gradient(to left, var(--bg-deep), transparent); }
|
|
.section-road.on-surface::before { background: linear-gradient(to right, var(--bg-surface), transparent); }
|
|
.section-road.on-surface::after { background: linear-gradient(to left, var(--bg-surface), transparent); }
|
|
|
|
/* --- Section labels --- */
|
|
.section-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.15em;
|
|
text-transform: uppercase;
|
|
color: var(--amber);
|
|
margin-bottom: 1.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
.section-label::before {
|
|
content: '';
|
|
width: 1.5rem;
|
|
height: 1px;
|
|
background: var(--amber-dim);
|
|
}
|
|
|
|
h2 {
|
|
font-family: var(--font-display);
|
|
font-weight: 400;
|
|
font-size: clamp(2rem, 4vw, 3rem);
|
|
line-height: 1.2;
|
|
margin-bottom: 1.5rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
h3 {
|
|
font-family: var(--font-body);
|
|
font-weight: 500;
|
|
font-size: 1.15rem;
|
|
margin-bottom: 0.75rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
p.lead {
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
max-width: 640px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
/* ===========================
|
|
HERO
|
|
=========================== */
|
|
.hero {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
padding: 6rem 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Radial glows behind hero */
|
|
.hero::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -20%;
|
|
right: -10%;
|
|
width: 700px;
|
|
height: 700px;
|
|
background: radial-gradient(circle, rgba(192, 98, 58, 0.06) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
.hero::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: -30%;
|
|
left: -15%;
|
|
width: 600px;
|
|
height: 600px;
|
|
background: radial-gradient(circle, rgba(107, 124, 78, 0.05) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.hero-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.hero .wordmark {
|
|
font-family: var(--font-display);
|
|
font-weight: 400;
|
|
font-size: clamp(4.5rem, 12vw, 9rem);
|
|
line-height: 0.9;
|
|
letter-spacing: -0.03em;
|
|
background: linear-gradient(135deg, #2c2418 0%, var(--amber) 55%, var(--teal) 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 1rem;
|
|
opacity: 0;
|
|
animation: wordmark-in 1s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards;
|
|
}
|
|
|
|
@keyframes wordmark-in {
|
|
from { opacity: 0; transform: translateY(40px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.hero .tagline {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: clamp(1.3rem, 3vw, 1.8rem);
|
|
color: var(--amber);
|
|
margin-bottom: 2rem;
|
|
opacity: 0;
|
|
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.5s forwards;
|
|
}
|
|
|
|
@keyframes fade-up {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.hero .epigraph {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: 0.92rem;
|
|
color: var(--text-dim);
|
|
margin-bottom: 1.5rem;
|
|
opacity: 0;
|
|
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.6s forwards;
|
|
}
|
|
|
|
.hero .description {
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
max-width: 560px;
|
|
line-height: 1.8;
|
|
margin-bottom: 3rem;
|
|
opacity: 0;
|
|
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.7s forwards;
|
|
}
|
|
|
|
.hero-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
opacity: 0;
|
|
animation: fade-up 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.9s forwards;
|
|
}
|
|
|
|
.btn {
|
|
font-family: var(--font-body);
|
|
font-weight: 500;
|
|
font-size: 0.9rem;
|
|
padding: 0.75rem 1.75rem;
|
|
border-radius: 2px;
|
|
text-decoration: none;
|
|
transition: all 0.25s ease;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--amber);
|
|
color: #faf7f2;
|
|
}
|
|
.btn-primary:hover {
|
|
background: #a85230;
|
|
box-shadow: 0 4px 20px rgba(192, 98, 58, 0.25);
|
|
}
|
|
|
|
.btn-ghost {
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.btn-ghost:hover {
|
|
border-color: var(--amber-dim);
|
|
color: var(--amber);
|
|
}
|
|
|
|
/* Side decoration — vertical line */
|
|
.hero-line {
|
|
position: absolute;
|
|
right: 12%;
|
|
top: 15%;
|
|
bottom: 15%;
|
|
width: 1px;
|
|
background: linear-gradient(to bottom, transparent, var(--teal), var(--amber), var(--violet), transparent);
|
|
opacity: 0;
|
|
animation: line-in 1.5s ease 1.2s forwards;
|
|
}
|
|
@keyframes line-in {
|
|
to { opacity: 0.35; }
|
|
}
|
|
.hero-line .dot {
|
|
position: absolute;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
left: -2.5px;
|
|
}
|
|
.hero-line .dot:nth-child(1) { top: 20%; background: var(--teal); animation: pulse-dot-teal 3s ease-in-out infinite 0s; }
|
|
.hero-line .dot:nth-child(2) { top: 50%; background: var(--amber); animation: pulse-dot-amber 3s ease-in-out infinite 1s; }
|
|
.hero-line .dot:nth-child(3) { top: 80%; background: var(--violet); animation: pulse-dot-violet 3s ease-in-out infinite 2s; }
|
|
|
|
@keyframes pulse-dot-teal {
|
|
0%, 100% { opacity: 0.5; box-shadow: none; }
|
|
50% { opacity: 1; box-shadow: 0 0 10px rgba(107, 124, 78, 0.5); }
|
|
}
|
|
@keyframes pulse-dot-amber {
|
|
0%, 100% { opacity: 0.5; box-shadow: none; }
|
|
50% { opacity: 1; box-shadow: 0 0 10px rgba(192, 98, 58, 0.5); }
|
|
}
|
|
@keyframes pulse-dot-violet {
|
|
0%, 100% { opacity: 0.5; box-shadow: none; }
|
|
50% { opacity: 1; box-shadow: 0 0 10px rgba(100, 116, 139, 0.5); }
|
|
}
|
|
|
|
/* ===========================
|
|
THE PROBLEM
|
|
=========================== */
|
|
.problem {
|
|
background: var(--bg-surface);
|
|
position: relative;
|
|
}
|
|
.problem::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 50%;
|
|
height: 100%;
|
|
background: radial-gradient(ellipse at 80% 30%, var(--rose-glow), transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.problem-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 4rem;
|
|
align-items: center;
|
|
margin-top: 3rem;
|
|
}
|
|
|
|
.problem-text p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
/* Centralized DNS diagram */
|
|
.dns-diagram {
|
|
position: relative;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.dns-node {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.6rem 1rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 2px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
background: var(--bg-elevated);
|
|
margin-bottom: 0.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.dns-node.central {
|
|
border-color: var(--rose);
|
|
background: rgba(181, 68, 58, 0.06);
|
|
color: var(--rose);
|
|
}
|
|
|
|
.dns-node .node-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
.dns-node .node-dot.red { background: var(--rose); }
|
|
.dns-node .node-dot.dim { background: var(--text-dim); }
|
|
|
|
.bottleneck-label {
|
|
text-align: center;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.65rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.12em;
|
|
color: var(--rose);
|
|
margin: 1.5rem 0 0.75rem;
|
|
}
|
|
|
|
.dns-arrows {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin: 0.75rem 0;
|
|
color: var(--text-dim);
|
|
font-family: var(--font-mono);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
/* ===========================
|
|
HOW IT WORKS — 3 LAYERS
|
|
=========================== */
|
|
.layers-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1.5rem;
|
|
margin-top: 3rem;
|
|
}
|
|
|
|
.layer-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
padding: 2rem 1.75rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04), 0 4px 14px rgba(44,36,24,0.04);
|
|
}
|
|
.layer-card:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06), 0 8px 24px rgba(44,36,24,0.06);
|
|
}
|
|
|
|
.layer-card:nth-child(1):hover { border-color: var(--border-teal); }
|
|
.layer-card:nth-child(2):hover { border-color: var(--border-amber); }
|
|
.layer-card:nth-child(3):hover { border-color: var(--border-violet); }
|
|
|
|
.layer-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
}
|
|
|
|
.layer-card:nth-child(1)::before { background: var(--teal); }
|
|
.layer-card:nth-child(2)::before { background: var(--amber); }
|
|
.layer-card:nth-child(3)::before { background: var(--violet); }
|
|
|
|
/* Subtle glow per card */
|
|
.layer-card:nth-child(1)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--teal-glow), transparent); pointer-events: none; }
|
|
.layer-card:nth-child(2)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--amber-glow), transparent); pointer-events: none; }
|
|
.layer-card:nth-child(3)::after { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, var(--violet-glow), transparent); pointer-events: none; }
|
|
|
|
.layer-badge {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.65rem;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
margin-bottom: 1rem;
|
|
display: inline-block;
|
|
padding: 0.2rem 0.5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.layer-card:nth-child(1) .layer-badge { color: var(--teal); border: 1px solid var(--border-teal); }
|
|
.layer-card:nth-child(2) .layer-badge { color: var(--amber); border: 1px solid var(--border-amber); }
|
|
.layer-card:nth-child(3) .layer-badge { color: var(--violet); border: 1px solid var(--border-violet); }
|
|
|
|
.layer-card h3 {
|
|
font-family: var(--font-display);
|
|
font-size: 1.4rem;
|
|
font-weight: 400;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
.layer-card ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.layer-card li {
|
|
font-size: 0.88rem;
|
|
color: var(--text-secondary);
|
|
padding: 0.35rem 0;
|
|
padding-left: 1.2rem;
|
|
position: relative;
|
|
}
|
|
|
|
.layer-card li::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0.75rem;
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
}
|
|
.layer-card:nth-child(1) li::before { background: var(--teal-dim); }
|
|
.layer-card:nth-child(2) li::before { background: var(--amber-dim); }
|
|
.layer-card:nth-child(3) li::before { background: var(--violet-dim); }
|
|
|
|
/* ===========================
|
|
ARCHITECTURE
|
|
=========================== */
|
|
.architecture {
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.arch-subsection {
|
|
margin-top: 4rem;
|
|
}
|
|
|
|
.arch-subsection h3 {
|
|
font-family: var(--font-display);
|
|
font-size: 1.5rem;
|
|
font-weight: 400;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
/* Pipeline diagram */
|
|
.pipeline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0;
|
|
overflow-x: auto;
|
|
padding: 2rem 0;
|
|
}
|
|
|
|
.pipeline-node {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
min-width: 100px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pipeline-box {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
padding: 0.6rem 0.9rem;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.pipeline-box:hover {
|
|
border-color: var(--amber-dim);
|
|
color: var(--amber);
|
|
}
|
|
|
|
.pipeline-box.highlight {
|
|
border-color: var(--amber-dim);
|
|
background: rgba(192, 98, 58, 0.06);
|
|
color: var(--amber);
|
|
}
|
|
.pipeline-box.hl-teal {
|
|
border-color: var(--teal-dim);
|
|
background: rgba(107, 124, 78, 0.06);
|
|
color: var(--teal);
|
|
}
|
|
.pipeline-box.hl-teal:hover { border-color: var(--teal); color: var(--teal); }
|
|
.pipeline-box.hl-violet {
|
|
border-color: var(--violet-dim);
|
|
background: rgba(100, 116, 139, 0.06);
|
|
color: var(--violet);
|
|
}
|
|
.pipeline-box.hl-violet:hover { border-color: var(--violet); color: var(--violet); }
|
|
.pipeline-box.hl-emerald {
|
|
border-color: var(--emerald-dim);
|
|
background: rgba(82, 122, 82, 0.06);
|
|
color: var(--emerald);
|
|
}
|
|
.pipeline-box.hl-emerald:hover { border-color: var(--emerald); color: var(--emerald); }
|
|
.pipeline-box.hl-cyan {
|
|
border-color: rgba(74, 124, 138, 0.35);
|
|
background: rgba(74, 124, 138, 0.06);
|
|
color: var(--cyan);
|
|
}
|
|
.pipeline-box.hl-cyan:hover { border-color: var(--cyan); color: var(--cyan); }
|
|
|
|
.pipeline-arrow {
|
|
font-family: var(--font-mono);
|
|
color: var(--text-dim);
|
|
font-size: 0.9rem;
|
|
padding: 0 0.4rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Layer stack */
|
|
.layer-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
max-width: 700px;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.stack-row {
|
|
display: grid;
|
|
grid-template-columns: 180px 1fr;
|
|
border: 1px solid var(--border);
|
|
border-bottom: none;
|
|
transition: background 0.3s ease;
|
|
}
|
|
.stack-row:last-child { border-bottom: 1px solid var(--border); }
|
|
.stack-row:hover { background: var(--bg-elevated); }
|
|
|
|
.stack-label {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.05em;
|
|
color: var(--amber);
|
|
padding: 0.9rem 1.2rem;
|
|
border-right: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.stack-value {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.78rem;
|
|
color: var(--text-secondary);
|
|
padding: 0.9rem 1.2rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Network diagram */
|
|
.network-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1rem;
|
|
margin: 2rem 0;
|
|
}
|
|
|
|
.network-actor {
|
|
text-align: center;
|
|
padding: 1.5rem 1rem;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
position: relative;
|
|
}
|
|
|
|
.network-actor .actor-icon {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
display: block;
|
|
}
|
|
|
|
.network-actor h4 {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--amber);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.network-actor p {
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Connecting lines between network actors */
|
|
.network-connections {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
margin: -0.5rem 0 0;
|
|
padding: 0 3rem;
|
|
}
|
|
.network-conn-line {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--border-amber);
|
|
position: relative;
|
|
top: 0;
|
|
}
|
|
|
|
/* ===========================
|
|
COMPARISON TABLE
|
|
=========================== */
|
|
.comparison-wrapper {
|
|
overflow-x: auto;
|
|
margin-top: 3rem;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.comparison-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.85rem;
|
|
min-width: 700px;
|
|
}
|
|
|
|
.comparison-table thead th {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--text-dim);
|
|
padding: 1rem 1.2rem;
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.comparison-table thead th:last-child {
|
|
color: var(--emerald);
|
|
}
|
|
|
|
.comparison-table tbody td {
|
|
padding: 0.8rem 1.2rem;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.comparison-table tbody tr:hover {
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.comparison-table tbody td:first-child {
|
|
font-weight: 400;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.comparison-table tbody td:last-child {
|
|
color: var(--emerald);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.check { color: var(--emerald); }
|
|
.check::before { content: '\2713\00a0'; }
|
|
.cross { color: var(--text-dim); }
|
|
.cross::before { content: '\2014\00a0'; }
|
|
.muted { color: var(--text-dim); font-style: italic; }
|
|
|
|
.comparison-table thead th:last-child {
|
|
background: rgba(82, 122, 82, 0.07);
|
|
}
|
|
.comparison-table tbody td:last-child {
|
|
background: rgba(82, 122, 82, 0.04);
|
|
}
|
|
|
|
/* ===========================
|
|
PERFORMANCE
|
|
=========================== */
|
|
.perf-section {
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.perf-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 3rem;
|
|
margin-top: 3rem;
|
|
align-items: start;
|
|
}
|
|
|
|
.perf-table-wrapper {
|
|
overflow-x: auto;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.perf-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.85rem;
|
|
min-width: 380px;
|
|
}
|
|
|
|
.perf-table thead th {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--text-dim);
|
|
padding: 0.8rem 1rem;
|
|
text-align: right;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--bg-elevated);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.perf-table thead th:first-child {
|
|
text-align: left;
|
|
}
|
|
|
|
.perf-table tbody td {
|
|
padding: 0.65rem 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
text-align: right;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.perf-table tbody td:first-child {
|
|
font-family: var(--font-body);
|
|
font-size: 0.85rem;
|
|
color: var(--text-primary);
|
|
text-align: left;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.perf-table tbody tr:hover {
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.perf-table tbody tr.perf-highlight td {
|
|
color: var(--emerald);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.perf-table tbody tr.perf-highlight td:first-child {
|
|
color: var(--emerald);
|
|
}
|
|
|
|
.perf-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.perf-stat {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
padding: 1.5rem;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
|
}
|
|
|
|
.perf-stat-value {
|
|
font-family: var(--font-display);
|
|
font-size: 2.2rem;
|
|
font-weight: 400;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.perf-stat-value.emerald { color: var(--emerald); }
|
|
.perf-stat-value.teal { color: var(--teal); }
|
|
.perf-stat-value.amber { color: var(--amber); }
|
|
|
|
.perf-stat-label {
|
|
font-size: 0.82rem;
|
|
color: var(--text-secondary);
|
|
margin-top: 0.4rem;
|
|
}
|
|
|
|
.perf-bar-group {
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.perf-bar-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.6rem;
|
|
}
|
|
|
|
.perf-bar-label {
|
|
font-size: 0.75rem;
|
|
color: var(--text-secondary);
|
|
width: 80px;
|
|
flex-shrink: 0;
|
|
text-align: right;
|
|
}
|
|
|
|
.perf-bar-track {
|
|
flex: 1;
|
|
height: 18px;
|
|
background: var(--bg-elevated);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.perf-bar-fill {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
transition: width 0.6s ease;
|
|
}
|
|
|
|
.perf-bar-fill.emerald { background: var(--emerald); }
|
|
.perf-bar-fill.teal { background: var(--teal); }
|
|
.perf-bar-fill.dim { background: var(--text-dim); }
|
|
|
|
.perf-bar-ms {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
width: 42px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.perf-note {
|
|
font-size: 0.78rem;
|
|
color: var(--text-dim);
|
|
margin-top: 2rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.perf-note a {
|
|
color: var(--teal-dim);
|
|
text-decoration: none;
|
|
border-bottom: 1px solid var(--border-teal);
|
|
}
|
|
|
|
/* ===========================
|
|
TECHNICAL
|
|
=========================== */
|
|
.tech-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 3rem;
|
|
margin-top: 3rem;
|
|
align-items: start;
|
|
}
|
|
|
|
.tech-specs dt {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.72rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--teal-dim);
|
|
margin-top: 1.25rem;
|
|
}
|
|
.tech-specs dt:first-child { margin-top: 0; }
|
|
|
|
.tech-specs dd {
|
|
font-size: 0.92rem;
|
|
color: var(--text-secondary);
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.code-block {
|
|
background: #1a1612;
|
|
color: #d6d0c6;
|
|
border: 1px solid rgba(44,36,24,0.15);
|
|
border-radius: 4px;
|
|
padding: 1.5rem;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.8rem;
|
|
line-height: 1.9;
|
|
color: var(--text-secondary);
|
|
overflow-x: auto;
|
|
position: relative;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.code-block::before {
|
|
content: 'terminal';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.6rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: #7a7060;
|
|
padding: 0.4rem 0.8rem;
|
|
background: #241f1a;
|
|
border-left: 1px solid rgba(255,255,255,0.06);
|
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
}
|
|
|
|
.code-block .prompt { color: #8baa6e; }
|
|
.code-block .comment { color: #7a7060; }
|
|
.code-block .cmd { color: #d48a5a; }
|
|
.code-block .flag { color: #8b9fbb; }
|
|
.code-block .str { color: #7dab7d; }
|
|
|
|
/* ===========================
|
|
ROADMAP
|
|
=========================== */
|
|
.roadmap {
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.roadmap-list {
|
|
margin-top: 3rem;
|
|
position: relative;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
/* Vertical line */
|
|
.roadmap-list::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 5px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 1px;
|
|
background: linear-gradient(to bottom, var(--emerald), var(--teal), var(--amber), var(--violet), var(--border));
|
|
}
|
|
|
|
.roadmap-item {
|
|
position: relative;
|
|
padding: 0.75rem 0;
|
|
padding-left: 1rem;
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 0.75rem;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.roadmap-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -2rem;
|
|
top: 1.05rem;
|
|
width: 11px;
|
|
height: 11px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--text-dim);
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.roadmap-item.done::before {
|
|
background: var(--emerald);
|
|
border-color: var(--emerald);
|
|
box-shadow: 0 0 6px rgba(82, 122, 82, 0.4);
|
|
}
|
|
|
|
.roadmap-item.phase-teal::before { border-color: var(--teal-dim); }
|
|
.roadmap-item.phase-amber::before { border-color: var(--amber-dim); }
|
|
.roadmap-item.phase-violet::before { border-color: var(--violet-dim); }
|
|
|
|
.roadmap-item .phase {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
color: var(--text-dim);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.roadmap-item.done .phase {
|
|
color: var(--emerald);
|
|
}
|
|
.roadmap-item.phase-teal .phase { color: var(--teal-dim); }
|
|
.roadmap-item.phase-amber .phase { color: var(--amber-dim); }
|
|
.roadmap-item.phase-violet .phase { color: var(--violet-dim); }
|
|
|
|
.roadmap-item .phase-desc {
|
|
font-size: 0.92rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.roadmap-item.done .phase-desc {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* ===========================
|
|
FOOTER
|
|
=========================== */
|
|
footer {
|
|
padding: 5rem 0 3rem;
|
|
text-align: center;
|
|
}
|
|
|
|
footer .origin {
|
|
font-family: var(--font-display);
|
|
font-style: italic;
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
max-width: 580px;
|
|
margin: 0 auto 2.5rem;
|
|
line-height: 1.7;
|
|
}
|
|
|
|
footer .footer-links {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 2rem;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
|
|
footer .footer-links a {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.75rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--text-dim);
|
|
text-decoration: none;
|
|
transition: color 0.2s ease;
|
|
}
|
|
footer .footer-links a:hover { color: var(--amber); }
|
|
|
|
footer .closing {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.7rem;
|
|
background: linear-gradient(90deg, var(--teal), var(--amber), var(--violet));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
/* ===========================
|
|
RESPONSIVE
|
|
=========================== */
|
|
@media (max-width: 900px) {
|
|
.problem-grid { grid-template-columns: 1fr; gap: 2rem; }
|
|
.layers-grid { grid-template-columns: 1fr; }
|
|
.tech-grid { grid-template-columns: 1fr; }
|
|
.perf-grid { grid-template-columns: 1fr; }
|
|
.network-grid { grid-template-columns: repeat(2, 1fr); }
|
|
.network-connections { display: none; }
|
|
.hero-line { display: none; }
|
|
.stack-row { grid-template-columns: 120px 1fr; }
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
section { padding: 4rem 0; }
|
|
.container { padding: 0 1.25rem; }
|
|
.network-grid { grid-template-columns: 1fr; }
|
|
.pipeline { flex-direction: column; align-items: stretch; gap: 0; }
|
|
.pipeline-arrow { transform: rotate(90deg); padding: 0.15rem 0; align-self: center; }
|
|
.pipeline-node { min-width: 100%; }
|
|
.pipeline-box { width: 100%; text-align: center; }
|
|
.comparison-table { font-size: 0.75rem; }
|
|
.section-road { height: 20px; }
|
|
}
|
|
</style>
|
|
<noscript><style>.reveal { opacity: 1 !important; transform: none !important; } .hero .wordmark, .hero .tagline, .hero .description, .hero-actions { opacity: 1 !important; animation: none !important; }</style></noscript>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ==================== HERO ==================== -->
|
|
<section class="hero">
|
|
<div class="roman-bricks" aria-hidden="true"></div>
|
|
<div class="container hero-content">
|
|
<h1 class="wordmark">Numa</h1>
|
|
<div class="tagline">DNS you own. Everywhere you go.</div>
|
|
<p class="epigraph">After Numa Pompilius, who built institutions that outlasted kings.</p>
|
|
<p class="description">
|
|
Block ads and trackers. Override DNS for development. Name your local services with <code style="font-size:0.9em;color:var(--cyan)">.numa</code> domains. A single portable binary built from scratch in Rust — no Raspberry Pi, no cloud, no account.
|
|
</p>
|
|
<div class="hero-actions">
|
|
<a href="#technical" class="btn btn-primary">Get Started</a>
|
|
<a href="#architecture" class="btn btn-ghost">Read the Architecture</a>
|
|
<a href="https://github.com/razvandimescu/numa" class="btn btn-ghost" target="_blank" rel="noopener">View on GitHub</a>
|
|
</div>
|
|
</div>
|
|
<div class="hero-line" aria-hidden="true">
|
|
<div class="dot"></div>
|
|
<div class="dot"></div>
|
|
<div class="dot"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== THE PROBLEM ==================== -->
|
|
<section class="problem">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label" style="color: var(--rose)">The Problem</div>
|
|
<h2>DNS is a single point of control</h2>
|
|
</div>
|
|
<div class="problem-grid">
|
|
<div class="problem-text reveal reveal-delay-1">
|
|
<p>Every time you visit a website, you ask a DNS resolver where to go. That resolver sees every domain you visit, when, and how often. Your ISP logs these queries by default.</p>
|
|
<p>Ad blockers work in one browser. Pi-hole needs a Raspberry Pi. Your local dev services live at <code>localhost:5173</code> and you can never remember which port is which.</p>
|
|
<p>DNS is the foundation of everything you do on the internet, but the tools for controlling it locally are either too complex (dnsmasq + nginx + mkcert) or too limited (cloud-only, appliance-only).</p>
|
|
</div>
|
|
<div class="dns-diagram reveal reveal-delay-2">
|
|
<div class="dns-node"><span class="node-dot dim"></span>Your browser</div>
|
|
<div class="dns-node"><span class="node-dot dim"></span>Your ISP / OS resolver</div>
|
|
<div class="dns-arrows">|</div>
|
|
<div class="bottleneck-label">Single point of failure</div>
|
|
<div class="dns-node central"><span class="node-dot red"></span>Cloudflare 1.1.1.1 / Google 8.8.8.8</div>
|
|
<div class="dns-arrows">|</div>
|
|
<div class="dns-node"><span class="node-dot dim"></span>ICANN root servers</div>
|
|
<div class="dns-node"><span class="node-dot dim"></span>TLD registrars (.com, .io, ...)</div>
|
|
<div class="dns-node"><span class="node-dot dim"></span>Authoritative nameservers</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== HOW IT WORKS ==================== -->
|
|
<section id="layers">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label">How It Works</div>
|
|
<h2>What it does today</h2>
|
|
<p class="lead">A recursive DNS resolver with DNSSEC validation, ad blocking, local service domains, and a REST API. Everything runs in a single binary.</p>
|
|
</div>
|
|
<div class="layers-grid">
|
|
<div class="layer-card reveal reveal-delay-1">
|
|
<div class="layer-badge">Layer 1</div>
|
|
<h3>Resolve & Protect</h3>
|
|
<ul>
|
|
<li>Recursive resolution — resolve from root nameservers, no upstream needed</li>
|
|
<li>DNSSEC validation — chain-of-trust + NSEC/NSEC3 denial proofs (RSA, ECDSA, Ed25519)</li>
|
|
<li>Ad & tracker blocking — 385K+ domains, zero config</li>
|
|
<li>DNS-over-HTTPS — encrypted upstream as alternative to recursive mode</li>
|
|
<li>TTL-aware caching (sub-ms lookups)</li>
|
|
<li>Single binary, portable — macOS, Linux, and Windows</li>
|
|
</ul>
|
|
</div>
|
|
<div class="layer-card reveal reveal-delay-2">
|
|
<div class="layer-badge">Layer 2</div>
|
|
<h3>Developer Tools</h3>
|
|
<ul>
|
|
<li>Local service proxy — <code>frontend.numa</code> instead of <code>localhost:5173</code></li>
|
|
<li>Path-based routing — <code>app.numa/api</code> → <code>:5001</code></li>
|
|
<li>Ephemeral DNS overrides with auto-revert</li>
|
|
<li>LAN service discovery via mDNS</li>
|
|
<li>Conditional forwarding — plays nice with Tailscale/VPN split-DNS</li>
|
|
<li>REST API — script everything, automate anything</li>
|
|
<li>Live dashboard with real-time stats and controls</li>
|
|
</ul>
|
|
</div>
|
|
<div class="layer-card reveal reveal-delay-3">
|
|
<div class="layer-badge">Coming Next</div>
|
|
<h3>Self-Sovereign DNS</h3>
|
|
<ul>
|
|
<li>pkarr integration — DNS via Mainline DHT, no registrar needed</li>
|
|
<li>Global <code>.numa</code> names — self-publish, DHT-backed</li>
|
|
<li>.onion bridge — human-readable names for Tor hidden services</li>
|
|
<li>Ed25519 same-key binding — zero new trust assumptions</li>
|
|
<li>No blockchain required for core naming</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== ARCHITECTURE ==================== -->
|
|
<section class="architecture" id="architecture">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label">Architecture</div>
|
|
<h2>Resolution pipeline</h2>
|
|
<p class="lead">Every query walks through the same deterministic pipeline. Local data takes priority; the network is the fallback.</p>
|
|
</div>
|
|
|
|
<div class="reveal reveal-delay-1">
|
|
<div class="pipeline">
|
|
<div class="pipeline-node"><div class="pipeline-box hl-teal">Query</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box highlight">Overrides</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box hl-cyan">.numa TLD</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box">Blocklist</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box">Local Zones</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box">Cache</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box hl-violet">Recursive / Forward (DoH)</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box highlight">DNSSEC Validate</div></div>
|
|
<span class="pipeline-arrow">→</span>
|
|
<div class="pipeline-node"><div class="pipeline-box hl-emerald">Respond</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== WHY NUMA IS DIFFERENT ==================== -->
|
|
<section>
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label">Comparison</div>
|
|
<h2>Why Numa is different</h2>
|
|
</div>
|
|
<div class="comparison-wrapper reveal reveal-delay-1">
|
|
<table class="comparison-table">
|
|
<caption class="sr-only">Comparison of Numa with existing DNS solutions</caption>
|
|
<thead>
|
|
<tr>
|
|
<th></th>
|
|
<th>Pi-hole</th>
|
|
<th>NextDNS</th>
|
|
<th>Cloudflare</th>
|
|
<th>AdGuard Home</th>
|
|
<th>Numa</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Recursive resolver</td>
|
|
<td class="cross">No (needs Unbound)</td>
|
|
<td class="cross">Cloud only</td>
|
|
<td class="cross">Cloud only</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">Root hints + full DNSSEC</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Ad & tracker blocking</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">Yes</td>
|
|
<td class="muted">Limited</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">385K+ domains</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Portable (travels with laptop)</td>
|
|
<td class="cross">No (Raspberry Pi)</td>
|
|
<td class="muted">Cloud only</td>
|
|
<td class="muted">Cloud only</td>
|
|
<td class="cross">No (network appliance)</td>
|
|
<td class="check">Single binary</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Developer overrides</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">REST API + auto-expiry</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Local service proxy</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">.numa domains + WebSocket</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Data stays local</td>
|
|
<td class="check">Yes</td>
|
|
<td class="cross">Cloud</td>
|
|
<td class="cross">Cloud</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">100% local</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Live dashboard</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">Yes</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">Real-time + controls</td>
|
|
</tr>
|
|
<tr>
|
|
<td>DNS-over-HTTPS upstream</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">Yes</td>
|
|
<td class="cross">No</td>
|
|
<td class="check">Built in (HTTP/2 + rustls)</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Conditional forwarding</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="cross">No</td>
|
|
<td class="muted">Manual</td>
|
|
<td class="check">Auto-detects Tailscale/VPN</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Zero config needed</td>
|
|
<td class="cross">Complex setup</td>
|
|
<td class="check">Yes</td>
|
|
<td class="check">Yes</td>
|
|
<td class="cross">Docker/setup</td>
|
|
<td class="check">Works out of the box</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== PERFORMANCE ==================== -->
|
|
<section class="perf-section" id="performance">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label" style="color: var(--emerald)">Performance</div>
|
|
<h2>Measured, not claimed</h2>
|
|
<p class="lead">Benchmarked with <code style="font-size:0.85em">dig</code> against public resolvers on the same machine. Cached queries resolve in under a microsecond.</p>
|
|
</div>
|
|
|
|
<div class="perf-grid">
|
|
<div class="reveal reveal-delay-1">
|
|
<div class="perf-table-wrapper">
|
|
<table class="perf-table">
|
|
<caption class="sr-only">DNS resolver latency comparison</caption>
|
|
<thead>
|
|
<tr>
|
|
<th>Resolver</th>
|
|
<th>Avg</th>
|
|
<th>P50</th>
|
|
<th>P99</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr class="perf-highlight">
|
|
<td>Numa (cached)</td>
|
|
<td><1ms</td>
|
|
<td><1ms</td>
|
|
<td><1ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Numa (cold)</td>
|
|
<td>9ms</td>
|
|
<td>9ms</td>
|
|
<td>18ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>System resolver</td>
|
|
<td>9ms</td>
|
|
<td>8ms</td>
|
|
<td>44ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Quad9</td>
|
|
<td>15ms</td>
|
|
<td>13ms</td>
|
|
<td>43ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Cloudflare</td>
|
|
<td>19ms</td>
|
|
<td>14ms</td>
|
|
<td>132ms</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Google</td>
|
|
<td>22ms</td>
|
|
<td>17ms</td>
|
|
<td>37ms</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="perf-bar-group">
|
|
<div class="perf-bar-row">
|
|
<span class="perf-bar-label">Numa</span>
|
|
<div class="perf-bar-track"><div class="perf-bar-fill emerald" style="width: 2%"></div></div>
|
|
<span class="perf-bar-ms"><1ms</span>
|
|
</div>
|
|
<div class="perf-bar-row">
|
|
<span class="perf-bar-label">System</span>
|
|
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 20%"></div></div>
|
|
<span class="perf-bar-ms">9ms</span>
|
|
</div>
|
|
<div class="perf-bar-row">
|
|
<span class="perf-bar-label">Quad9</span>
|
|
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 33%"></div></div>
|
|
<span class="perf-bar-ms">15ms</span>
|
|
</div>
|
|
<div class="perf-bar-row">
|
|
<span class="perf-bar-label">Cloudflare</span>
|
|
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 42%"></div></div>
|
|
<span class="perf-bar-ms">19ms</span>
|
|
</div>
|
|
<div class="perf-bar-row">
|
|
<span class="perf-bar-label">Google</span>
|
|
<div class="perf-bar-track"><div class="perf-bar-fill dim" style="width: 49%"></div></div>
|
|
<span class="perf-bar-ms">22ms</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="perf-sidebar reveal reveal-delay-2">
|
|
<div class="perf-stat">
|
|
<div class="perf-stat-value emerald">689 ns</div>
|
|
<div class="perf-stat-label">Cached round-trip — parse query, cache lookup, serialize response</div>
|
|
</div>
|
|
<div class="perf-stat">
|
|
<div class="perf-stat-value teal">2.0M</div>
|
|
<div class="perf-stat-label">Queries per second (single-threaded pipeline throughput, batched)</div>
|
|
</div>
|
|
<div class="perf-stat">
|
|
<div class="perf-stat-value amber">0 allocations</div>
|
|
<div class="perf-stat-label">Heap allocations in the I/O path — 4KB stack buffers, inline serialization</div>
|
|
</div>
|
|
<div class="perf-stat">
|
|
<div class="perf-stat-value teal">174 ns</div>
|
|
<div class="perf-stat-label">ECDSA P-256 signature verification (DNSSEC). RSA/SHA-256: 10.9µs. DS digest: 257ns.</div>
|
|
</div>
|
|
<div class="perf-stat">
|
|
<div class="perf-stat-value emerald">~90 ms</div>
|
|
<div class="perf-stat-label">Cold-cache DNSSEC validation — only 1 network fetch needed (TLD chain pre-warmed on startup)</div>
|
|
</div>
|
|
|
|
<p class="perf-note">
|
|
Cold queries match system resolver speed — the bottleneck is upstream RTT, not Numa. We don't claim to be faster when the network is the limit.
|
|
<br><br>
|
|
Benchmarks are reproducible: <code style="font-size:0.85em">cargo bench</code> for micro-benchmarks, <code style="font-size:0.85em">python3 bench/dns-bench.sh</code> for end-to-end.
|
|
<a href="https://github.com/razvandimescu/numa/tree/main/bench">Methodology →</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road on-surface" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== TECHNICAL ==================== -->
|
|
<section id="technical">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label">Under the Hood</div>
|
|
<h2>Technical details</h2>
|
|
</div>
|
|
<div class="tech-grid">
|
|
<dl class="tech-specs reveal reveal-delay-1">
|
|
<dt>Runtime</dt>
|
|
<dd>Rust + tokio async (rt-multi-thread)</dd>
|
|
|
|
<dt>DNS Libraries</dt>
|
|
<dd>Zero — wire protocol parsed from scratch</dd>
|
|
|
|
<dt>Resolution Modes</dt>
|
|
<dd>Recursive (iterative from root hints, CNAME chasing, glue extraction) or Forward (DoH / plain UDP)</dd>
|
|
|
|
<dt>DNSSEC</dt>
|
|
<dd>Chain-of-trust via ring — RSA/SHA-256, ECDSA P-256, Ed25519. NSEC/NSEC3 denial proofs. EDNS0 DO bit, 1232-byte payload (DNS Flag Day 2020).</dd>
|
|
|
|
<dt>Dependencies</dt>
|
|
<dd>19 runtime crates — tokio, axum, hyper, ring (DNSSEC), reqwest (DoH), rcgen + rustls (TLS), socket2 (multicast), serde, and more</dd>
|
|
|
|
<dt>Packet Format</dt>
|
|
<dd>RFC 1035 compliant. EDNS0 OPT pseudo-record. Parses A, AAAA, NS, CNAME, MX, SOA, SRV, HTTPS, DNSKEY, DS, RRSIG, NSEC, NSEC3.</dd>
|
|
|
|
<dt>Concurrency</dt>
|
|
<dd>Arc<ServerCtx> + RwLock for reads, Mutex for writes (never across .await)</dd>
|
|
</dl>
|
|
<div class="code-block reveal reveal-delay-2">
|
|
<span class="comment"># Install (pick one)</span>
|
|
<span class="prompt">$</span> <span class="cmd">brew install</span> razvandimescu/tap/numa
|
|
<span class="prompt">$</span> <span class="cmd">cargo install</span> numa
|
|
<span class="prompt">$</span> <span class="cmd">curl</span> <span class="flag">-fsSL</span> https://raw.githubusercontent.com/razvandimescu/numa/main/install.sh <span class="flag">|</span> <span class="cmd">sh</span>
|
|
|
|
<span class="comment"># Run</span>
|
|
<span class="prompt">$</span> <span class="cmd">sudo numa</span> <span class="comment"># bind to :53, :80, :5380</span>
|
|
<span class="prompt">$</span> <span class="cmd">dig</span> <span class="flag">@127.0.0.1</span> google.com <span class="comment"># test resolution</span>
|
|
<span class="prompt">$</span> <span class="cmd">open</span> http://localhost:5380 <span class="comment"># dashboard</span>
|
|
<span class="prompt">$</span> <span class="cmd">curl</span> <span class="flag">-X POST</span> localhost:5380/services \
|
|
<span class="flag">-d</span> <span class="str">'{"name":"frontend",
|
|
"target_port":5173}'</span> <span class="comment"># https://frontend.numa</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="section-road" aria-hidden="true"><div class="roman-bricks"></div></div>
|
|
|
|
<!-- ==================== ROADMAP ==================== -->
|
|
<section class="roadmap">
|
|
<div class="container">
|
|
<div class="reveal">
|
|
<div class="section-label">Roadmap</div>
|
|
<h2>Where we're going</h2>
|
|
</div>
|
|
<div class="roadmap-list reveal reveal-delay-1">
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 0</span>
|
|
<span class="phase-desc">DNS proxy core — zones, caching, forwarding, async tokio runtime</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 1</span>
|
|
<span class="phase-desc">Override layer + REST API for programmatic DNS control</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 2</span>
|
|
<span class="phase-desc">Ad & tracker blocking — 385K+ domains, live dashboard, one-click allowlist</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 3</span>
|
|
<span class="phase-desc">System integration — auto-discovery of OS DNS routing, one-command install</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 4</span>
|
|
<span class="phase-desc">Local service proxy — .numa domains, HTTP/HTTPS reverse proxy, auto TLS, WebSocket</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 5</span>
|
|
<span class="phase-desc">DNS-over-HTTPS — encrypted upstream, HTTP/2 connection pooling</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 6</span>
|
|
<span class="phase-desc">Recursive resolution — resolve from root nameservers, no upstream dependency</span>
|
|
</div>
|
|
<div class="roadmap-item done">
|
|
<span class="phase">Phase 7</span>
|
|
<span class="phase-desc">DNSSEC validation — chain-of-trust, NSEC/NSEC3 denial proofs, RSA + ECDSA + Ed25519</span>
|
|
</div>
|
|
<div class="roadmap-item phase-teal">
|
|
<span class="phase">Phase 8</span>
|
|
<span class="phase-desc">pkarr integration — self-sovereign DNS via Mainline DHT, no registrar needed</span>
|
|
</div>
|
|
<div class="roadmap-item phase-teal">
|
|
<span class="phase">Phase 9</span>
|
|
<span class="phase-desc">Global .numa names — self-publish, DHT-backed, first-come-first-served</span>
|
|
</div>
|
|
<div class="roadmap-item phase-teal">
|
|
<span class="phase">Phase 10</span>
|
|
<span class="phase-desc">.onion bridge — human-readable Tor naming via Ed25519 same-key binding</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ==================== FOOTER ==================== -->
|
|
<footer>
|
|
<div class="container">
|
|
<p class="origin reveal">
|
|
Named after Numa Pompilius — the second king of Rome who established the institutions, laws, and religious practices that outlasted the monarchy itself.
|
|
</p>
|
|
<div class="footer-links reveal reveal-delay-1">
|
|
<a href="https://github.com/razvandimescu/numa" target="_blank" rel="noopener">GitHub</a>
|
|
<a href="/blog/">Blog</a>
|
|
<a href="https://github.com/razvandimescu/numa/blob/main/LICENSE" target="_blank" rel="noopener">MIT License</a>
|
|
</div>
|
|
<p class="closing reveal reveal-delay-2">Built from scratch in Rust. No dependencies on trust.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
// Intersection Observer for reveal animations
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
entry.target.classList.add('visible');
|
|
}
|
|
});
|
|
}, {
|
|
threshold: 0.1,
|
|
rootMargin: '0px 0px -40px 0px'
|
|
});
|
|
|
|
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|