# 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`), Sectors, Partners, Quote, Insights, Contact. | | `src/components/verifier/GridObject.astro` | The wireframe objects on the product cards (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 acts 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 (with `GridObject`), 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`, `server/mail/` | The handler for both forms (contact, alpha sign-up): validates, checks the proof of work, rate-limits and sends by SMTP. `server/mail/confirm.mjs` lays out the confirmation email (tables, inline styles, logo as an inline attachment). Its own `package.json`, so the image only carries nodemailer and altcha-lib. | | `scripts/*.mjs` | Review tooling (Playwright): `shot.mjs` (full page), `scroll-shots.mjs` and `stack-shots.mjs` (viewport by viewport), `motion-shots.mjs` (a draw-in over time), `hover-shot.mjs`, `tabs-test.mjs` and `tabs-shots.mjs` (sectors tabs), `panel-shots.mjs`, `object-fit-test.mjs` (grid objects inside their viewBox), and for the full product page's pinned steps `pinned-steps-test.mjs`, `panel-align-test.mjs`, `step-exit-test.mjs`. `contact-sheet.mjs` tiles a folder of images. Need `npx astro preview --port 4322`. | | `scripts/research-figures.py` | Gives every paper in `src/data/research.json` its Chladni figure and colour field. `src/data/scholar/*.json` are the raw Google Scholar exports that file was built from. | | `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 and JetBrains Mono, variable (latin, latin-ext). | ## 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 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 `