# CipherCell homepage relaunch Static site for www.ciphercell.de, built with Astro. Design foundation: `CLAUDE.md` (European deep-tech minimalism: warm paper, one neo-grotesque, hairlines, no accent colour). ## Run ```sh npm install npm run dev # http://localhost:4321 npm run build # static output in dist/ npm run preview # serve dist/ npm run check # Astro + TypeScript check ``` ## Docker / Dokploy The `Dockerfile` builds the site with Node and serves `dist/` from nginx on port 80 (config in `docker/nginx.conf`: clean URLs without trailing slash, 404 page, gzip, long caching for `/_astro/`, health check at `/healthz`). ```sh docker build -t ciphercell . docker run --rm -p 8080:80 ciphercell # http://localhost:8080 ``` In Dokploy: create an Application from this repository, set Build Type to **Dockerfile** (path `Dockerfile`), and under Domains add the domain with container port **80** and HTTPS on. TLS is handled by Dokploy's Traefik. The contact form needs the SMTP variables under Environment; see *Contact form* below. ## Where things live | Path | Purpose | |---|---| | `src/styles/tokens.css` | Every colour, type step, spacing, radius and duration. Components use only these. | | `src/styles/base.css` | Font faces, reset, focus ring, primitives (`.container`, `.grid`, `.section`, `.prose`, `.link`, `.button`). | | `src/i18n/ui.ts` | All interface copy, English and German, one object per language with the same shape. | | `src/data/team.ts` | Team members, roles, portraits, links. | | `src/content/insights/*.md` | Insights posts (content collection). Front matter: title, date, lang, category, readingTime, author, cover. | | `src/components/home/*` | One file per home section, in page order: Hero, Manifesto, Method (with `MethodDiagram`), Stages (with `GridObject`), Sectors, Proof, Quote, Insights, Contact. | | `src/components/home/GridObject.astro` | The wireframe objects on the stage and product cards (sphere, cube, plane, wave, steps, nested, stack). Geometry is computed at build time; single stroke in `--ink`. Each drawing is laid out by hand, so the `viewBox` is derived from the points as they are emitted, not assumed: a square centred on the drawing, padded for the stroke. A new kind can therefore never be clipped, and the card gives the drawing the flexible row so it sits centred between title and body. Each drawing is emitted in two groups, the lines that carry its idea and the grid around them, which is what the hover and the reveal act on. | | `src/components/home/MethodDiagram.astro` | The four-stage loop, wide and tall compositions. Labels come from `ui.method.stages`. | | `src/components/verifier/*` | The CipherVerifier product page, in page order: Hero (line burst), Intro (image band), HowItWorks (five group cards, one schematic each; on wide screens the heading is sticky and the cards stack under it as you scroll), Cards, Facts, Outro. | | `src/components/verifier/SoonHero.astro`, `ComingSoon.astro` | The coming-soon version of `/cipherverifier`, shown while `verifierComingSoon` in `src/data/flags.ts` is true: hero on a build-time SVG relief of ridge lines in the logo blues, under the floating header, "Coming soon." with the alpha sign-up beside it, a status strip, and the product name set across the full width and cut by the hero's lower edge, release path, spec sheet and the first schematic. Copy in `ui.verifier.soon`. | | `src/components/verifier/diagrams.ts` | The five schematics as data: a hand-placed wide layout and an auto-generated tall spine per group. `Schematic.astro` renders either. Labels come from `ui.verifier.how.groups`; the tuple shape is in `types.ts`. Every element carries the `group` of the node it belongs to, which drives the hover focus and the draw-in order. | | `server/contact.mjs` | The contact form handler: validates, rate-limits and sends by SMTP. Its own `package.json`, so the image only carries nodemailer. | | `scripts/*.mjs` | Review tooling: `shot.mjs` (full page), `scroll-shots.mjs` (viewport by viewport), `tabs-test.mjs` (keyboard test for the sectors tabs), `contact-sheet.mjs`. Need `npx astro preview --port 4322`. | | `src/pages/_*.astro` | Shared page bodies. `index.astro` and `de/index.astro` render `_Home.astro` with a `lang` prop, and so on. | | `src/assets/images/` | Source images. Astro emits AVIF with a WebP fallback and explicit dimensions. | | `public/fonts/` | Self-hosted Inter Tight variable (latin, latin-ext). | | `public/video/` | The home hero's background loop, MP4 and WebM. Shipped as-is; Astro does not process video. | ## Routes `/`, `/cipherverifier`, `/team`, `/insights`, `/insights/`, `/imprint`, `/privacy`, `/404` and the same under `/de`. `hreflang` alternates and a sitemap are generated. The language switch sits in the footer. ## Things the host has to provide - **Contact form.** The form posts `first_name`, `last_name`, `email`, `message`, `lang` and a honeypot field `website` to `POST /contact`. That one path is handled by `server/contact.mjs` (Node, nodemailer), which sends the message by SMTP to `CONTACT_TO` with the sender as Reply-To. The form sends in place and shows the result in a toast; a post that asks for HTML is redirected to `/contact/sent` or `/contact/error` (under `/de` for German). Both pages are `noindex` and left out of the sitemap. The handler caps size and field lengths, refuses cross-site posts, and allows 5 messages per address per 10 minutes and 60 per hour overall. Against spam, every message carries the answer to a proof-of-work challenge (ALTCHA, `altcha-lib`). The form fetches a signed challenge from `GET /contact/challenge` when the visitor first focuses it and solves it in the background (about half a second on a laptop, one to two on a phone). The handler refuses a post without a valid, unexpired, unused solution, or one sent less than 3 seconds after the challenge was issued. Nothing is stored in the browser and no third party is involved, so there is nothing to consent to. The form therefore needs JavaScript; without it a `