initial landing scaffold (hyperhive/website MVP)

Tracks hyperhive#502 — landing-only first cut. Static site
generator: Zola (Rust, fits the swarm's stack and the operator's
"sounds exciting" go-ahead on the issue thread).

## Structure

- `flake.nix` — `nix build .#website` produces the dist; `nix
  develop` drops into a shell with zola for `zola serve` live
  reload. No CI runner; the flake is the validation contract.
- `config.toml` — Zola config; base URL `hyperhive.darkest.space`
  per #502. Single-page landing — feeds / search index off.
- `content/_index.md` — landing copy. Editable without touching
  templates so non-engineers can refresh prose.
- `templates/base.html` + `templates/index.html` — base layout +
  landing-specific extension. og:tags + favicon wired through.
- `sass/main.scss` — theme. Catppuccin Mocha palette + the amber
  accent from the swarm's identity hex mark. Self-contained
  (no @import) so the file is reviewable in one place.
- `static/{favicon,hex-mark,hyperhive}.svg` — copies of the
  dashboard's `branding/hyperhive.svg` (hex motif). Used as
  favicon, hero inline, and og:image respectively.

## Theme

Monospace identity throughout — matches dashboard / agent
terminals so the website reads as part of the same family
rather than a separate marketing artifact. Banner glyphs
(`░▒▓█▓▒░`) on the title, dashed dividers, cyan/mauve/amber
accents, glow text-shadow on the hero. Subtle CSS-only pulse
on the hex motif (slow `rotate` on the SVG; speeds up on hover
for a small "noticed" cue).

## Three-column "what's inside"

`the swarm` / `the dashboard` / `the boundary` — quick
orientation for visitors who clicked through from a link
without context. Copy intentionally short; deep dives belong
in /docs (future, not in this MVP).

## Scope drop

Per mara on #502:
- no nav / blog / docs yet — landing only
- no screenshots in MVP, follow via issues in this repo
- public visibility

## Provenance

Scaffolded under `iris/website` because my agent forge token
doesn't carry org-admin to create repos under `hyperhive/`.
Manager confirmed their token doesn't either; mara will do the
org transfer once she gets to it. README documents this.
This commit is contained in:
iris 2026-05-27 10:08:56 +02:00
parent b1dd949291
commit d3a55c5631
12 changed files with 873 additions and 2 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# Zola build output
public/
# nix
result
result-*
# editor / OS
.direnv/
*.swp
*.swo
.DS_Store

View file

@ -1,3 +1,56 @@
# website # hyperhive website
hyperhive marketing landing — Zola static site, Catppuccin-themed (will transfer to hyperhive/website per #502) Marketing landing for hyperhive, deployed at
[hyperhive.darkest.space](https://hyperhive.darkest.space). Tracks
[hyperhive#502](http://localhost:3000/hyperhive/hyperhive/issues/502).
Built with [Zola](https://www.getzola.org/), the Rust static site
generator. Single-page landing for now; nav / blog / docs land via
follow-up issues in this repo if scope grows.
## Build
```sh
# one-shot build to ./public
nix build .#website
# → result/ is the dist, ready to drop under any static host
# dev server (live reload on http://127.0.0.1:1111/)
nix develop
zola serve
```
## Layout
```
config.toml # zola config (single source of truth for site meta)
content/_index.md # landing page copy — edit here for prose changes
templates/
base.html # base layout (head, footer, og tags)
index.html # landing template extending base
sass/
main.scss # theme — Catppuccin Mocha + amber accent
static/
favicon.svg # hyperhive hex motif (copy of dashboard branding)
hex-mark.svg # same SVG, used inline in the hero
hyperhive.svg # og:image
flake.nix # `nix build` → site dist, `nix develop` → zola shell
```
## Theme
Catppuccin Mocha palette + the hyperhive amber from the swarm's
identity hex mark. Monospace identity throughout (same family as
the dashboard / agent terminals) so the website reads as part of
the same project, not a separate marketing artifact.
Theme variables live in `sass/main.scss` (single source of truth).
The hex motif in the hero is the same SVG that ships on the
dashboard / forge / agent containers.
## Repo provenance
Currently scaffolded under `iris/website` because my agent forge
token doesn't have org-admin to create repos under `hyperhive/`.
Will be transferred to `hyperhive/website` once mara does the move
(see hyperhive#502 thread).

34
config.toml Normal file
View file

@ -0,0 +1,34 @@
# Zola config — hyperhive marketing landing (#502).
#
# Single-page landing for now; nav / blog / docs come later if scope grows.
# Theme lives in `sass/main.scss` and `templates/`; no external Zola theme
# import — the project's identity matches the dashboard / agent terminals
# closely enough that pulling in a third-party theme would just give us
# something to fight.
base_url = "https://hyperhive.darkest.space"
title = "hyperhive"
description = "a swarm of claude agents in nspawn containers, with an operator-shaped trust boundary."
# We don't need RSS / search / sitemaps for a single-page landing. Can
# flip these on later if blog content lands.
generate_feeds = false
build_search_index = false
# Embed the SCSS pipeline; zola compiles `sass/*.scss` → `public/*.css`.
compile_sass = true
# Markdown rendering defaults — landing has very little prose so the
# fancier syntax-highlight setup isn't needed yet. Easy to turn on later.
[markdown]
highlight_code = false
smart_punctuation = true
[extra]
# Catppuccin Mocha palette + amber accent (matching the hex mark) is
# the project's identity. Single source of truth — `main.scss` reads
# these via Sass variables, not from this attrset (Zola doesn't pipe
# extras into Sass). Keeping them here as documentation only so the
# theme intent is reviewable in one place.
palette = "catppuccin-mocha"
accent = "amber"

9
content/_index.md Normal file
View file

@ -0,0 +1,9 @@
+++
title = "hyperhive"
+++
A swarm of Claude agents running in nspawn containers, gated by an
operator-shaped trust boundary. Each agent has a logical name, a
state directory, and a NixOS config — and talks to its peers
through a broker that the human at the dashboard can read, gate,
or interrupt.

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1779560665,
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

64
flake.nix Normal file
View file

@ -0,0 +1,64 @@
{
# hyperhive marketing landing — Zola static site build (#502).
#
# `nix build` → `result/` is the `public/` Zola dist, ready to drop
# under any static-file host. No CI runner needed; the flake is the
# validation contract.
description = "hyperhive marketing landing";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
in {
packages.website = pkgs.stdenv.mkDerivation {
pname = "hyperhive-website";
version = self.shortRev or "dev";
src = ./.;
nativeBuildInputs = [ pkgs.zola ];
# Zola reads `config.toml` from CWD, writes `public/` next to it,
# which we then move to $out. The build is offline / hermetic —
# no network access needed at build time, which fits the nix
# sandbox without extra fetchurls.
buildPhase = ''
runHook preBuild
zola build --output-dir public
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp -r public/. $out/
runHook postInstall
'';
# Static-site dist; no shared libs to patchelf, no executables.
dontFixup = true;
};
# `nix build` with no attribute → the website dist.
packages.default = self.packages.${system}.website;
# `nix develop` → a shell with zola for local iteration:
# `zola serve` runs the dev server with live reload.
devShells.default = pkgs.mkShell {
packages = [ pkgs.zola ];
};
# `nix run` is the dev-server shortcut: `nix run . -- serve`
# boots Zola's hot-reload server on http://127.0.0.1:1111/.
apps.default = {
type = "app";
program = "${pkgs.zola}/bin/zola";
};
});
}

249
sass/main.scss Normal file
View file

@ -0,0 +1,249 @@
// hyperhive marketing landing theme (#502).
//
// Matches the dashboard / agent terminals visually so the website
// reads as part of the same family: Catppuccin Mocha palette + amber
// accent (matching the hex mark), banner-thin block-glyph headings,
// dashed dividers, glow text-shadow on titles.
//
// Single-file SCSS landing is small enough that splitting into
// partials would be premature. Re-evaluate if /docs or /blog land.
// palette
// Catppuccin Mocha + the hyperhive amber from branding/hyperhive.svg.
// Copying them in (not importing) so this file stays self-contained.
$base: #1e1e2e;
$mantle: #181825;
$crust: #11111b;
$text: #cdd6f4;
$subtext1: #bac2de;
$subtext0: #a6adc8;
$overlay2: #9399b2;
$overlay1: #7f849c;
$overlay0: #6c7086;
$surface2: #585b70;
$surface1: #45475a;
$surface0: #313244;
$rosewater: #f5e0dc;
$flamingo: #f2cdcd;
$pink: #f5c2e7;
$mauve: #cba6f7;
$red: #f38ba8;
$maroon: #eba0ac;
$peach: #fab387;
$yellow: #f9e2af;
$green: #a6e3a1;
$teal: #94e2d5;
$sky: #89dceb;
$sapphire: #74c7ec;
$blue: #89b4fa;
$lavender: #b4befe;
// The hex-mark amber. Pure swarm identity colour.
$amber: #ffb300;
$amber-deep: #ff8f00;
$amber-glow: rgba(255, 179, 0, 0.45);
// globals
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: $base;
color: $text;
// Same font stack as the dashboard's @hive/shared base.css —
// monospace identity reads as "this is the terminal swarm" instead
// of "this is a marketing site". Body copy stays readable at this
// weight because the lines are short.
font-family: ui-monospace, "JetBrains Mono", "Fira Code",
Menlo, Consolas, monospace;
font-size: 16px;
line-height: 1.55;
// Subtle background grid same idea as the hex-mark's faint
// horizontal lines, but at body scope. Cheaper than a full SVG
// background.
background-image:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 23px,
rgba(255, 179, 0, 0.025) 23px,
rgba(255, 179, 0, 0.025) 24px
);
}
a {
color: $sky;
text-decoration: none;
border-bottom: 1px dashed transparent;
transition: border-color 0.15s, color 0.15s;
}
a:hover { color: $sapphire; border-bottom-color: $sapphire; }
::selection { background: $amber-glow; color: $crust; }
// layout
.shell {
max-width: 920px;
margin: 0 auto;
padding: 4rem 1.5rem 2rem;
}
// hero
.hero {
display: grid;
grid-template-columns: minmax(180px, 280px) 1fr;
gap: 2rem;
align-items: center;
padding-bottom: 2rem;
border-bottom: 1px dashed $surface1;
}
@media (max-width: 700px) {
.hero { grid-template-columns: 1fr; text-align: center; }
.hero-art { max-width: 220px; margin: 0 auto; }
}
.hero-art svg {
width: 100%;
height: auto;
// Subtle pulse only the outer ring + dashed orbit rotate. Quiet
// enough to read as "active" instead of "noisy".
animation: hex-orbit 24s linear infinite;
}
.hero-art:hover svg {
// Speed up on hover for a tiny "you got noticed" cue. No JS needed.
animation-duration: 8s;
}
@keyframes hex-orbit {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hero-text h1 {
margin: 0 0 0.6rem;
font-size: 2.4rem;
font-weight: 700;
letter-spacing: 0.04em;
color: $amber;
text-shadow:
0 0 4px $amber-glow,
0 0 12px rgba(255, 143, 0, 0.25);
}
.banner-glyph {
color: $amber-deep;
opacity: 0.65;
font-weight: 400;
letter-spacing: 0;
}
.hero-tagline {
margin: 0 0 1.6rem;
font-size: 1.05rem;
color: $subtext1;
}
.hero-cta { margin: 0; }
.cta-primary {
display: inline-block;
padding: 0.5rem 1rem;
border: 1px solid $amber;
color: $amber;
border-bottom-color: $amber;
text-shadow: 0 0 6px $amber-glow;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
transition: box-shadow 0.15s ease, color 0.15s, border-color 0.15s;
}
.cta-primary:hover {
color: $base;
background: $amber;
border-color: $amber;
border-bottom-style: solid;
box-shadow: 0 0 14px -2px $amber-glow;
text-shadow: none;
}
// prose
.prose {
padding: 2rem 0;
color: $text;
max-width: 64ch;
font-size: 1.02rem;
}
.prose p { margin: 0 0 1rem; }
// three-column grid
.grid-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
padding: 2rem 0;
border-top: 1px dashed $surface1;
}
@media (max-width: 760px) {
.grid-3 { grid-template-columns: 1fr; }
}
.card {
padding: 1rem 1.2rem;
background: $mantle;
border: 1px solid $surface0;
border-radius: 4px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.card:hover {
border-color: $amber;
box-shadow: 0 0 14px -6px $amber-glow;
}
.card h2 {
margin: 0 0 0.6rem;
font-size: 1.1rem;
letter-spacing: 0.04em;
color: $mauve;
}
.card-glyph { color: $amber; margin-right: 0.4em; }
.card p {
margin: 0;
font-size: 0.92rem;
color: $subtext1;
line-height: 1.5;
}
// footer
.site-footer {
max-width: 920px;
margin: 3rem auto 1.5rem;
padding: 1.5rem 1.5rem 0;
text-align: center;
border-top: 1px dashed $surface1;
}
.banner-thin {
margin: 1.5rem 0 0.5rem;
color: $amber-deep;
font-size: 0.95rem;
letter-spacing: 0.18em;
opacity: 0.7;
}
.footer-links {
margin: 0;
font-size: 0.88rem;
color: $subtext0;
}
.footer-links .sep { margin: 0 0.4em; opacity: 0.5; }

97
static/favicon.svg Normal file
View file

@ -0,0 +1,97 @@
<svg width="300" height="300" viewBox="0 0 300 300" role="img" xmlns="http://www.w3.org/2000/svg">
<title>HyperHive</title>
<desc>HyperHive icon — hexagonal hive, amber on dark</desc>
<defs>
<clipPath id="clipH"><circle cx="150" cy="150" r="140"/></clipPath>
</defs>
<g clip-path="url(#clipH)">
<rect x="0" y="0" width="300" height="300" fill="#0a0600"/>
<g stroke="#ffb300" stroke-width="0.35" opacity="0.07">
<line x1="0" x2="300" y1="10" y2="10"/> <line x1="0" x2="300" y1="20" y2="20"/>
<line x1="0" x2="300" y1="30" y2="30"/> <line x1="0" x2="300" y1="40" y2="40"/>
<line x1="0" x2="300" y1="50" y2="50"/> <line x1="0" x2="300" y1="60" y2="60"/>
<line x1="0" x2="300" y1="70" y2="70"/> <line x1="0" x2="300" y1="80" y2="80"/>
<line x1="0" x2="300" y1="90" y2="90"/> <line x1="0" x2="300" y1="100" y2="100"/>
<line x1="0" x2="300" y1="110" y2="110"/> <line x1="0" x2="300" y1="120" y2="120"/>
<line x1="0" x2="300" y1="130" y2="130"/> <line x1="0" x2="300" y1="140" y2="140"/>
<line x1="0" x2="300" y1="150" y2="150"/> <line x1="0" x2="300" y1="160" y2="160"/>
<line x1="0" x2="300" y1="170" y2="170"/> <line x1="0" x2="300" y1="180" y2="180"/>
<line x1="0" x2="300" y1="190" y2="190"/> <line x1="0" x2="300" y1="200" y2="200"/>
<line x1="0" x2="300" y1="210" y2="210"/> <line x1="0" x2="300" y1="220" y2="220"/>
<line x1="0" x2="300" y1="230" y2="230"/> <line x1="0" x2="300" y1="240" y2="240"/>
<line x1="0" x2="300" y1="250" y2="250"/> <line x1="0" x2="300" y1="260" y2="260"/>
<line x1="0" x2="300" y1="270" y2="270"/> <line x1="0" x2="300" y1="280" y2="280"/>
</g>
<circle cx="150" cy="150" r="118" fill="none" stroke="#ffb300" stroke-width="1" opacity="0.4"/>
<circle cx="150" cy="150" r="104" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.25" stroke-dasharray="4 6"/>
<!-- RING 2 — 6 between-axis hexes, centered on (150,150) instead of (170,170) -->
<g fill="#0e0800" stroke="#ffb300" stroke-width="0.7" opacity="0.4">
<polygon points="254,105 241,127.5 215,127.5 202,105 215,82.5 241,82.5"/>
<polygon points="98,105 85,127.5 59,127.5 46,105 59,82.5 85,82.5"/>
<polygon points="176,60 163,82.5 137,82.5 124,60 137,37.5 163,37.5"/>
<polygon points="254,195 241,217.5 215,217.5 202,195 215,172.5 241,172.5"/>
<polygon points="98,195 85,217.5 59,217.5 46,195 59,172.5 85,172.5"/>
<polygon points="176,240 163,262.5 137,262.5 124,240 137,217.5 163,217.5"/>
</g>
<!-- RING 1 — 6 hexes -->
<g fill="#150c00" stroke="#ffb300" stroke-width="1.2" opacity="0.8">
<polygon points="228,150 215,172.5 189,172.5 176,150 189,127.5 215,127.5"/>
<polygon points="202,105 189,127.5 163,127.5 150,105 163,82.5 189,82.5"/>
<polygon points="150,105 137,127.5 111,127.5 98,105 111,82.5 137,82.5"/>
<polygon points="124,150 111,172.5 85,172.5 72,150 85,127.5 111,127.5"/>
<polygon points="150,195 137,217.5 111,217.5 98,195 111,172.5 137,172.5"/>
<polygon points="202,195 189,217.5 163,217.5 150,195 163,172.5 189,172.5"/>
</g>
<!-- CENTER hex -->
<polygon points="176,150 163,172.5 137,172.5 124,150 137,127.5 163,127.5"
fill="#1a0f00" stroke="#ffb300" stroke-width="1.8"/>
<!-- connections -->
<g stroke="#ffb300" stroke-width="1" opacity="0.6">
<line x1="176" y1="150" x2="202" y2="150"/>
<line x1="124" y1="150" x2="98" y2="150"/>
<line x1="163" y1="128" x2="176" y2="105"/>
<line x1="137" y1="128" x2="124" y2="105"/>
<line x1="163" y1="173" x2="176" y2="195"/>
<line x1="137" y1="173" x2="124" y2="195"/>
</g>
<!-- CENTER node -->
<circle cx="150" cy="150" r="10" fill="#ffb300" opacity="0.95"/>
<circle cx="150" cy="150" r="5" fill="#0a0600"/>
<!-- ring 1 nodes -->
<g fill="#ff8f00" opacity="0.9">
<circle cx="202" cy="150" r="6"/>
<circle cx="176" cy="105" r="6"/>
<circle cx="124" cy="105" r="6"/>
<circle cx="98" cy="150" r="6"/>
<circle cx="124" cy="195" r="6"/>
<circle cx="176" cy="195" r="6"/>
</g>
<g fill="#0a0600">
<circle cx="202" cy="150" r="2.5"/>
<circle cx="176" cy="105" r="2.5"/>
<circle cx="124" cy="105" r="2.5"/>
<circle cx="98" cy="150" r="2.5"/>
<circle cx="124" cy="195" r="2.5"/>
<circle cx="176" cy="195" r="2.5"/>
</g>
<rect x="0" y="143" width="300" height="2" fill="#ffb300" opacity="0.07"/>
</g>
<circle cx="150" cy="150" r="140" fill="none" stroke="#ffb300" stroke-width="2.5"/>
<circle cx="150" cy="150" r="145" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.4" stroke-dasharray="8 4"/>
<g stroke="#ffb300" stroke-width="1.5" fill="none" opacity="0.8">
<path d="M44,44 L20,44 L20,70"/>
<path d="M256,44 L280,44 L280,70"/>
<path d="M44,256 L20,256 L20,230"/>
<path d="M256,256 L280,256 L280,230"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

97
static/hex-mark.svg Normal file
View file

@ -0,0 +1,97 @@
<svg width="300" height="300" viewBox="0 0 300 300" role="img" xmlns="http://www.w3.org/2000/svg">
<title>HyperHive</title>
<desc>HyperHive icon — hexagonal hive, amber on dark</desc>
<defs>
<clipPath id="clipH"><circle cx="150" cy="150" r="140"/></clipPath>
</defs>
<g clip-path="url(#clipH)">
<rect x="0" y="0" width="300" height="300" fill="#0a0600"/>
<g stroke="#ffb300" stroke-width="0.35" opacity="0.07">
<line x1="0" x2="300" y1="10" y2="10"/> <line x1="0" x2="300" y1="20" y2="20"/>
<line x1="0" x2="300" y1="30" y2="30"/> <line x1="0" x2="300" y1="40" y2="40"/>
<line x1="0" x2="300" y1="50" y2="50"/> <line x1="0" x2="300" y1="60" y2="60"/>
<line x1="0" x2="300" y1="70" y2="70"/> <line x1="0" x2="300" y1="80" y2="80"/>
<line x1="0" x2="300" y1="90" y2="90"/> <line x1="0" x2="300" y1="100" y2="100"/>
<line x1="0" x2="300" y1="110" y2="110"/> <line x1="0" x2="300" y1="120" y2="120"/>
<line x1="0" x2="300" y1="130" y2="130"/> <line x1="0" x2="300" y1="140" y2="140"/>
<line x1="0" x2="300" y1="150" y2="150"/> <line x1="0" x2="300" y1="160" y2="160"/>
<line x1="0" x2="300" y1="170" y2="170"/> <line x1="0" x2="300" y1="180" y2="180"/>
<line x1="0" x2="300" y1="190" y2="190"/> <line x1="0" x2="300" y1="200" y2="200"/>
<line x1="0" x2="300" y1="210" y2="210"/> <line x1="0" x2="300" y1="220" y2="220"/>
<line x1="0" x2="300" y1="230" y2="230"/> <line x1="0" x2="300" y1="240" y2="240"/>
<line x1="0" x2="300" y1="250" y2="250"/> <line x1="0" x2="300" y1="260" y2="260"/>
<line x1="0" x2="300" y1="270" y2="270"/> <line x1="0" x2="300" y1="280" y2="280"/>
</g>
<circle cx="150" cy="150" r="118" fill="none" stroke="#ffb300" stroke-width="1" opacity="0.4"/>
<circle cx="150" cy="150" r="104" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.25" stroke-dasharray="4 6"/>
<!-- RING 2 — 6 between-axis hexes, centered on (150,150) instead of (170,170) -->
<g fill="#0e0800" stroke="#ffb300" stroke-width="0.7" opacity="0.4">
<polygon points="254,105 241,127.5 215,127.5 202,105 215,82.5 241,82.5"/>
<polygon points="98,105 85,127.5 59,127.5 46,105 59,82.5 85,82.5"/>
<polygon points="176,60 163,82.5 137,82.5 124,60 137,37.5 163,37.5"/>
<polygon points="254,195 241,217.5 215,217.5 202,195 215,172.5 241,172.5"/>
<polygon points="98,195 85,217.5 59,217.5 46,195 59,172.5 85,172.5"/>
<polygon points="176,240 163,262.5 137,262.5 124,240 137,217.5 163,217.5"/>
</g>
<!-- RING 1 — 6 hexes -->
<g fill="#150c00" stroke="#ffb300" stroke-width="1.2" opacity="0.8">
<polygon points="228,150 215,172.5 189,172.5 176,150 189,127.5 215,127.5"/>
<polygon points="202,105 189,127.5 163,127.5 150,105 163,82.5 189,82.5"/>
<polygon points="150,105 137,127.5 111,127.5 98,105 111,82.5 137,82.5"/>
<polygon points="124,150 111,172.5 85,172.5 72,150 85,127.5 111,127.5"/>
<polygon points="150,195 137,217.5 111,217.5 98,195 111,172.5 137,172.5"/>
<polygon points="202,195 189,217.5 163,217.5 150,195 163,172.5 189,172.5"/>
</g>
<!-- CENTER hex -->
<polygon points="176,150 163,172.5 137,172.5 124,150 137,127.5 163,127.5"
fill="#1a0f00" stroke="#ffb300" stroke-width="1.8"/>
<!-- connections -->
<g stroke="#ffb300" stroke-width="1" opacity="0.6">
<line x1="176" y1="150" x2="202" y2="150"/>
<line x1="124" y1="150" x2="98" y2="150"/>
<line x1="163" y1="128" x2="176" y2="105"/>
<line x1="137" y1="128" x2="124" y2="105"/>
<line x1="163" y1="173" x2="176" y2="195"/>
<line x1="137" y1="173" x2="124" y2="195"/>
</g>
<!-- CENTER node -->
<circle cx="150" cy="150" r="10" fill="#ffb300" opacity="0.95"/>
<circle cx="150" cy="150" r="5" fill="#0a0600"/>
<!-- ring 1 nodes -->
<g fill="#ff8f00" opacity="0.9">
<circle cx="202" cy="150" r="6"/>
<circle cx="176" cy="105" r="6"/>
<circle cx="124" cy="105" r="6"/>
<circle cx="98" cy="150" r="6"/>
<circle cx="124" cy="195" r="6"/>
<circle cx="176" cy="195" r="6"/>
</g>
<g fill="#0a0600">
<circle cx="202" cy="150" r="2.5"/>
<circle cx="176" cy="105" r="2.5"/>
<circle cx="124" cy="105" r="2.5"/>
<circle cx="98" cy="150" r="2.5"/>
<circle cx="124" cy="195" r="2.5"/>
<circle cx="176" cy="195" r="2.5"/>
</g>
<rect x="0" y="143" width="300" height="2" fill="#ffb300" opacity="0.07"/>
</g>
<circle cx="150" cy="150" r="140" fill="none" stroke="#ffb300" stroke-width="2.5"/>
<circle cx="150" cy="150" r="145" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.4" stroke-dasharray="8 4"/>
<g stroke="#ffb300" stroke-width="1.5" fill="none" opacity="0.8">
<path d="M44,44 L20,44 L20,70"/>
<path d="M256,44 L280,44 L280,70"/>
<path d="M44,256 L20,256 L20,230"/>
<path d="M256,256 L280,256 L280,230"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

97
static/hyperhive.svg Normal file
View file

@ -0,0 +1,97 @@
<svg width="300" height="300" viewBox="0 0 300 300" role="img" xmlns="http://www.w3.org/2000/svg">
<title>HyperHive</title>
<desc>HyperHive icon — hexagonal hive, amber on dark</desc>
<defs>
<clipPath id="clipH"><circle cx="150" cy="150" r="140"/></clipPath>
</defs>
<g clip-path="url(#clipH)">
<rect x="0" y="0" width="300" height="300" fill="#0a0600"/>
<g stroke="#ffb300" stroke-width="0.35" opacity="0.07">
<line x1="0" x2="300" y1="10" y2="10"/> <line x1="0" x2="300" y1="20" y2="20"/>
<line x1="0" x2="300" y1="30" y2="30"/> <line x1="0" x2="300" y1="40" y2="40"/>
<line x1="0" x2="300" y1="50" y2="50"/> <line x1="0" x2="300" y1="60" y2="60"/>
<line x1="0" x2="300" y1="70" y2="70"/> <line x1="0" x2="300" y1="80" y2="80"/>
<line x1="0" x2="300" y1="90" y2="90"/> <line x1="0" x2="300" y1="100" y2="100"/>
<line x1="0" x2="300" y1="110" y2="110"/> <line x1="0" x2="300" y1="120" y2="120"/>
<line x1="0" x2="300" y1="130" y2="130"/> <line x1="0" x2="300" y1="140" y2="140"/>
<line x1="0" x2="300" y1="150" y2="150"/> <line x1="0" x2="300" y1="160" y2="160"/>
<line x1="0" x2="300" y1="170" y2="170"/> <line x1="0" x2="300" y1="180" y2="180"/>
<line x1="0" x2="300" y1="190" y2="190"/> <line x1="0" x2="300" y1="200" y2="200"/>
<line x1="0" x2="300" y1="210" y2="210"/> <line x1="0" x2="300" y1="220" y2="220"/>
<line x1="0" x2="300" y1="230" y2="230"/> <line x1="0" x2="300" y1="240" y2="240"/>
<line x1="0" x2="300" y1="250" y2="250"/> <line x1="0" x2="300" y1="260" y2="260"/>
<line x1="0" x2="300" y1="270" y2="270"/> <line x1="0" x2="300" y1="280" y2="280"/>
</g>
<circle cx="150" cy="150" r="118" fill="none" stroke="#ffb300" stroke-width="1" opacity="0.4"/>
<circle cx="150" cy="150" r="104" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.25" stroke-dasharray="4 6"/>
<!-- RING 2 — 6 between-axis hexes, centered on (150,150) instead of (170,170) -->
<g fill="#0e0800" stroke="#ffb300" stroke-width="0.7" opacity="0.4">
<polygon points="254,105 241,127.5 215,127.5 202,105 215,82.5 241,82.5"/>
<polygon points="98,105 85,127.5 59,127.5 46,105 59,82.5 85,82.5"/>
<polygon points="176,60 163,82.5 137,82.5 124,60 137,37.5 163,37.5"/>
<polygon points="254,195 241,217.5 215,217.5 202,195 215,172.5 241,172.5"/>
<polygon points="98,195 85,217.5 59,217.5 46,195 59,172.5 85,172.5"/>
<polygon points="176,240 163,262.5 137,262.5 124,240 137,217.5 163,217.5"/>
</g>
<!-- RING 1 — 6 hexes -->
<g fill="#150c00" stroke="#ffb300" stroke-width="1.2" opacity="0.8">
<polygon points="228,150 215,172.5 189,172.5 176,150 189,127.5 215,127.5"/>
<polygon points="202,105 189,127.5 163,127.5 150,105 163,82.5 189,82.5"/>
<polygon points="150,105 137,127.5 111,127.5 98,105 111,82.5 137,82.5"/>
<polygon points="124,150 111,172.5 85,172.5 72,150 85,127.5 111,127.5"/>
<polygon points="150,195 137,217.5 111,217.5 98,195 111,172.5 137,172.5"/>
<polygon points="202,195 189,217.5 163,217.5 150,195 163,172.5 189,172.5"/>
</g>
<!-- CENTER hex -->
<polygon points="176,150 163,172.5 137,172.5 124,150 137,127.5 163,127.5"
fill="#1a0f00" stroke="#ffb300" stroke-width="1.8"/>
<!-- connections -->
<g stroke="#ffb300" stroke-width="1" opacity="0.6">
<line x1="176" y1="150" x2="202" y2="150"/>
<line x1="124" y1="150" x2="98" y2="150"/>
<line x1="163" y1="128" x2="176" y2="105"/>
<line x1="137" y1="128" x2="124" y2="105"/>
<line x1="163" y1="173" x2="176" y2="195"/>
<line x1="137" y1="173" x2="124" y2="195"/>
</g>
<!-- CENTER node -->
<circle cx="150" cy="150" r="10" fill="#ffb300" opacity="0.95"/>
<circle cx="150" cy="150" r="5" fill="#0a0600"/>
<!-- ring 1 nodes -->
<g fill="#ff8f00" opacity="0.9">
<circle cx="202" cy="150" r="6"/>
<circle cx="176" cy="105" r="6"/>
<circle cx="124" cy="105" r="6"/>
<circle cx="98" cy="150" r="6"/>
<circle cx="124" cy="195" r="6"/>
<circle cx="176" cy="195" r="6"/>
</g>
<g fill="#0a0600">
<circle cx="202" cy="150" r="2.5"/>
<circle cx="176" cy="105" r="2.5"/>
<circle cx="124" cy="105" r="2.5"/>
<circle cx="98" cy="150" r="2.5"/>
<circle cx="124" cy="195" r="2.5"/>
<circle cx="176" cy="195" r="2.5"/>
</g>
<rect x="0" y="143" width="300" height="2" fill="#ffb300" opacity="0.07"/>
</g>
<circle cx="150" cy="150" r="140" fill="none" stroke="#ffb300" stroke-width="2.5"/>
<circle cx="150" cy="150" r="145" fill="none" stroke="#ff8f00" stroke-width="0.5" opacity="0.4" stroke-dasharray="8 4"/>
<g stroke="#ffb300" stroke-width="1.5" fill="none" opacity="0.8">
<path d="M44,44 L20,44 L20,70"/>
<path d="M256,44 L280,44 L280,70"/>
<path d="M44,256 L20,256 L20,230"/>
<path d="M256,256 L280,256 L280,230"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

34
templates/base.html Normal file
View file

@ -0,0 +1,34 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ config.title }}{% endblock %}</title>
<meta name="description" content="{{ config.description }}">
{# Open Graph / social card. Reuses the SVG hex mark as the image. #}
<meta property="og:type" content="website">
<meta property="og:title" content="{{ config.title }}">
<meta property="og:description" content="{{ config.description }}">
<meta property="og:url" content="{{ config.base_url }}">
<meta property="og:image" content="{{ get_url(path='hyperhive.svg') }}">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="{{ get_url(path='favicon.svg') }}">
<link rel="stylesheet" href="{{ get_url(path='main.css') }}">
</head>
<body>
<main class="shell">
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<pre class="banner-thin">░▒▓█▓▒░ HYPERHIVE ░▒▓█▓▒░</pre>
<p class="footer-links">
<a href="http://localhost:3000/hyperhive">code on the forge</a>
<span class="sep">·</span>
<a href="{{ get_url(path='') }}">home</a>
</p>
</footer>
</body>
</html>

64
templates/index.html Normal file
View file

@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block content %}
{# Hero — hex motif left, headline + tagline + CTA right. Stacks
vertically on narrow viewports via the .hero CSS grid. #}
<section class="hero">
<div class="hero-art" aria-hidden="true">
{# Inline so the SVG inherits the page's font + can be styled
(hover pulse animation on the rings). Same source as the
dashboard branding/hyperhive.svg. #}
{% set hex_svg = load_data(path="hex-mark.svg") %}
{{ hex_svg | safe }}
</div>
<div class="hero-text">
<h1>
<span class="banner-glyph">░▒▓</span>
hyperhive
<span class="banner-glyph">▓▒░</span>
</h1>
<p class="hero-tagline">{{ config.description }}</p>
<p class="hero-cta">
<a class="cta-primary" href="http://localhost:3000/hyperhive/hyperhive">
◆ explore the code →
</a>
</p>
</div>
</section>
{# Landing prose — pulled from _index.md so non-engineers can edit
copy without touching templates. #}
<section class="prose">
{{ section.content | safe }}
</section>
{# Three-column "what's inside" — quick orientation for visitors
who clicked through from a link without context. Copy here is
intentionally short; deep dives belong in /docs (future). #}
<section class="grid-3">
<article class="card">
<h2><span class="card-glyph"></span> the swarm</h2>
<p>
Each agent is a NixOS container with its own claude session,
a logical name, and a parent in the topology tree. Containers
come and go; the topology is the operator's facts file.
</p>
</article>
<article class="card">
<h2><span class="card-glyph"></span> the dashboard</h2>
<p>
One web page shows live agent state, the broker stream, the
approval queue, scheduled prompts, and the rebuild pipeline.
Everything that mutates the swarm passes through here.
</p>
</article>
<article class="card">
<h2><span class="card-glyph"></span> the boundary</h2>
<p>
Agents can ask, schedule, request approvals, and message peers
— but never spawn / destroy / mutate config themselves. Those
actions queue up and wait on the human at the dashboard.
</p>
</article>
</section>
{% endblock %}