diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md index 0fdb13a..bbb2366 100644 --- a/.design-sync/NOTES.md +++ b/.design-sync/NOTES.md @@ -11,7 +11,7 @@ the point — the visual truth stays in the site's stylesheet, so the design system cannot drift from the site even as `base.css` changes, and each component's markup ports back to `.astro` one to one (every `.md` states the Astro equivalent). -- **Do not port the page-level `.astro` components** (Hero, Stages, HowItWorks, GridObject). +- **Do not port the page-level `.astro` components** (Hero, HowItWorks, GridObject). Their styles are scoped inside the `.astro` files, so a React copy would duplicate CSS and drift. The line is: global primitive classes are fair game, component-scoped styles are not. - `Stack` is the one component with no class behind it. Nothing in this system carries its own diff --git a/.gitignore b/.gitignore index 01b1db7..021b6ef 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ ds-bundle/ .env .env.* !.env.example +__pycache__/ diff --git a/README.md b/README.md index fe56d40..98dbd70 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,18 @@ Environment; see *Contact form* below. | `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/*` | 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, Facts, Outro. | +| `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: `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`. | +| `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 variable (latin, latin-ext). | -| `public/video/` | The home hero's background loop, MP4 and WebM. Shipped as-is; Astro does not process video. | +| `public/fonts/` | Self-hosted Inter Tight and JetBrains Mono, variable (latin, latin-ext). | ## Routes @@ -151,47 +151,31 @@ The three newest posts appear on the home page automatically. ## The card drawings -The objects on the stage cards ("What we do.") and the product cards do not animate on arrival; -they are simply drawn. Hovering a card sets `--object-grid`, which the drawing answers by letting -its grid recede to 26% while the lines that carry the idea stay: the cube's own edges, the tile -lifted out of the plane, the wave's profiles, the inner cube inside its shell, the column to fix -first. The card also sets `--object-scale`. Both are custom properties the card owns and the +The objects on the product cards do not animate on arrival; they are simply drawn. Hovering a +card sets `--object-grid`, which the drawing answers by letting its grid recede to 26% while the +lines that carry the idea stay: the inner cube inside its shell, the column to fix first, the +newest layer of the stack. The card also sets `--object-scale`. Both are custom properties the card owns and the drawing responds to, so the interaction lives with the card and the response with the drawing. It is behind `@media (hover: hover)` so touch never sticks in it, and it is the same language as the hover focus on the product page schematics. -## The hero video +## The hero -The home hero plays `public/video/hero-plant.mp4` / `.webm` behind the headline, over the -`plant-at-dusk.jpg` still. The still is the video's own first frame, so it carries the hero until -the video is ready and stays put for anyone who never gets one. - -The video is only fetched when it will be seen: not under `prefers-reduced-motion`, not on -`saveData`, and not below 48rem, where a 21:9 frame would be cropped to a sliver for the most -expensive bytes. The sources are attached by script rather than in the markup, so nothing is -requested otherwise. A blocked autoplay also just leaves the still in place. - -The source clip was generated with Magnific (Kling 2.5, 10 s, from the hero still as its start -frame, https://www.magnific.com/app/creation/5jPaI3qKxe). `scripts/encode-hero-video.sh` turns a -clip into the looping file: it plays forward, then backward, so the loop point cannot be seen, and -encodes 1920-wide H.264 and VP9. A 20-second loop lands at 862 KB and 472 KB. To replace it: - -```sh -FFMPEG=/path/to/ffmpeg ./scripts/encode-hero-video.sh source.mp4 public/video/hero-plant 1920 -``` - -Keep the first frame of any replacement equal to `plant-at-dusk.jpg`, or replace both together. +The home hero is a noise field in the logo's blues on night, drawn by a small WebGL shader +(`src/scripts/noise-field.ts`) that drifts slowly and bends towards the pointer. It renders at half +resolution, caps at 30 fps and only runs while the hero is on screen. Underneath sits +`hero-field.png`, a frame of the same shader, so the hand-over is invisible and the poster carries +the hero for anyone who never gets the canvas: reduced motion, no WebGL, no script. ## Imagery The mast at dusk (`mast-at-dusk.jpg`) is the one documentary photograph from the old site; it -remains the cover of the first post. The five other scene images were generated with Magnific +remains the cover of the first post. The four other scene images were generated with Magnific (Seedream 5 Pro) on 18 September 2026 in a desaturated documentary style, because no real photography existed: | File | Used in | Magnific creation | |---|---|---| -| `plant-at-dusk.jpg` | Hero | https://www.magnific.com/app/creation/DoQZmiipcl | | `monitoring-room-glass.jpg` | Method section | https://www.magnific.com/app/creation/bx34g4D5Y2 | | `sector-5g.jpg` | Sectors, tab 1 | https://www.magnific.com/app/creation/s7ZQP52l8e | | `sector-substation.jpg` | Sectors, tab 2 | https://www.magnific.com/app/creation/yi0r27vPW9 | @@ -210,7 +194,7 @@ the Assembly Line" (premium licence), downloaded on 8 October 2026 and scaled to They are placeholders in the sense of the brief: swap them for the company's own documentary photography when it exists, keeping the same file names and the `img-doc` treatment. The grid -objects on the stage cards were prototyped as a Magnific sheet +objects on the product cards were prototyped as a Magnific sheet (https://www.magnific.com/app/creation/aFsD5crfSh) and then rebuilt as procedural SVG so the stroke matches the rest of the site. @@ -237,9 +221,12 @@ them for review. ## Interaction and motion -Three islands of script: the mobile navigation toggle in `Header.astro`, the sectors tabs in -`Sectors.astro` (`src/scripts/tabs.ts`: ARIA tablist, arrow keys, Home and End), and the reveal -observer used by the product page's drawings. +Islands of script: the mobile navigation toggle in `Header.astro`, the hero's noise field +(`src/scripts/noise-field.ts`), the sectors tabs in `Sectors.astro` (`src/scripts/tabs.ts`: ARIA +tablist, arrow keys, Home and End), the quote picker, the two forms (`send-form.ts`, +`altcha.ts`), the research cards' figures on the team page (`chladni.ts`), the Insights filter, +the two-click embed on mention pages, the Matomo opt-out, and on the product page the pinned +steps and the verticals tabs. Motion on the product page, all of it on wide screens: the hero's seventeen lines draw in on load and meet in one point (off under `prefers-reduced-motion`); hovering a node in a schematic dims diff --git a/package-lock.json b/package-lock.json index 7e79d4c..d7c2c3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "ciphercell-homepage", "version": "0.1.0", "dependencies": { - "@fontsource-variable/inter-tight": "^5.3.0", "altcha-lib": "^2.6.0", "astro": "^7.3.3", "sharp": "^0.35.4" @@ -1141,15 +1140,6 @@ "node": ">=18" } }, - "node_modules/@fontsource-variable/inter-tight": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@fontsource-variable/inter-tight/-/inter-tight-5.3.0.tgz", - "integrity": "sha512-ZhZZ29sNZ15P1K+cUt13GeBRYON0akgmNiftshUeDLos7X7zGcRgPzt434879Xhq1QbwlW3M/apBMk7jWabwvA==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", diff --git a/package.json b/package.json index d3d4305..6434a71 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "contact": "node --env-file-if-exists=.env server/contact.mjs" }, "dependencies": { - "@fontsource-variable/inter-tight": "^5.3.0", "altcha-lib": "^2.6.0", "astro": "^7.3.3", "sharp": "^0.35.4" diff --git a/public/video/hero-plant.mp4 b/public/video/hero-plant.mp4 deleted file mode 100644 index 9bf09ca..0000000 Binary files a/public/video/hero-plant.mp4 and /dev/null differ diff --git a/public/video/hero-plant.webm b/public/video/hero-plant.webm deleted file mode 100644 index e5546ab..0000000 Binary files a/public/video/hero-plant.webm and /dev/null differ diff --git a/scripts/__pycache__/research-figures.cpython-314.pyc b/scripts/__pycache__/research-figures.cpython-314.pyc deleted file mode 100644 index 03875d9..0000000 Binary files a/scripts/__pycache__/research-figures.cpython-314.pyc and /dev/null differ diff --git a/scripts/dots-test.mjs b/scripts/dots-test.mjs deleted file mode 100644 index d84b9d1..0000000 --- a/scripts/dots-test.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import { chromium } from 'playwright'; -/* Jumps to card 3 via its dot, then back to card 1 via its dot, and reports where the page rests. */ -const [,, url, width = '1440', height = '900'] = process.argv; -const browser = await chromium.launch(); -const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) } }); -await page.goto(url, { waitUntil: 'networkidle' }); -const snaps = await page.evaluate(() => Array.from(document.querySelectorAll('[data-deck-snap]')).map((a) => Math.round(a.getBoundingClientRect().top + window.scrollY - parseFloat(getComputedStyle(a).scrollMarginTop)))); -const dots = page.locator('[data-deck-dots] a'); -await page.evaluate((y) => window.scrollTo(0, y), snaps[2]); -await page.waitForTimeout(800); -const before = await page.evaluate(() => Math.round(window.scrollY)); -await dots.nth(0).click(); -await page.waitForTimeout(1500); -const back = await page.evaluate(() => Math.round(window.scrollY)); -await dots.nth(3).click(); -await page.waitForTimeout(1500); -const fwd = await page.evaluate(() => Math.round(window.scrollY)); -const active = await page.evaluate(() => Array.from(document.querySelectorAll('[data-deck-dots] a')).map((a) => a.getAttribute('aria-current') === 'true')); -const hint = await page.evaluate(() => { const h = document.querySelector('[data-deck-hint]'); return { href: h?.getAttribute('href'), last: h?.hasAttribute('data-last') }; }); -console.log({ snaps, before, backTo1: back, expected1: snaps[0], fwdTo4: fwd, expected4: snaps[3], active, hint }); -await browser.close(); diff --git a/scripts/encode-hero-video.sh b/scripts/encode-hero-video.sh deleted file mode 100755 index bf857e6..0000000 --- a/scripts/encode-hero-video.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -# Turns a generated clip into a seamless looping hero background: -# the clip plays forward, then backward, so the loop point cannot be seen. -# Usage: encode-hero-video.sh [width] -set -euo pipefail -FFMPEG="${FFMPEG:-ffmpeg}" -SRC="$1"; OUT="$2"; W="${3:-1920}" -FILTER="[0:v]scale=${W}:-2:flags=lanczos,fps=25,setpts=PTS-STARTPTS,split[a][b];[b]reverse,trim=start_frame=1,setpts=PTS-STARTPTS[r];[a][r]concat=n=2:v=1[v]" -"$FFMPEG" -y -i "$SRC" -filter_complex "$FILTER" -map "[v]" -an \ - -c:v libx264 -profile:v high -crf 30 -preset slow -pix_fmt yuv420p -movflags +faststart "${OUT}.mp4" -"$FFMPEG" -y -i "$SRC" -filter_complex "$FILTER" -map "[v]" -an \ - -c:v libvpx-vp9 -crf 40 -b:v 0 -row-mt 1 -deadline good -cpu-used 2 "${OUT}.webm" -ls -lh "${OUT}.mp4" "${OUT}.webm" diff --git a/scripts/hero-video-test.mjs b/scripts/hero-video-test.mjs deleted file mode 100644 index 5c605fd..0000000 --- a/scripts/hero-video-test.mjs +++ /dev/null @@ -1,39 +0,0 @@ -import { chromium } from 'playwright'; -/* Checks the hero video: does it load and play, is it gated off on phones and under reduced - motion, and does the hero fill the viewport. */ -const [,, url, prefix] = process.argv; -const browser = await chromium.launch(); - -async function probe(label, opts, shots = []) { - const ctx = await browser.newContext(opts); - const page = await ctx.newPage(); - const requested = []; - page.on('request', (r) => { if (/\.(mp4|webm)$/.test(r.url())) requested.push(r.url().split('/').pop()); }); - await page.goto(url, { waitUntil: 'networkidle' }); - await page.waitForTimeout(2500); - const info = await page.evaluate(() => { - const v = document.querySelector('video'); - const hero = document.querySelector('.hero'); - return { - hasVideo: !!v, - ready: v?.hasAttribute('data-ready') ?? null, - paused: v?.paused ?? null, - currentTime: v ? Math.round(v.currentTime * 10) / 10 : null, - videoSize: v ? `${v.videoWidth}x${v.videoHeight}` : null, - heroHeight: Math.round(hero.getBoundingClientRect().height), - viewport: window.innerHeight, - headerH: Math.round(document.querySelector('header').getBoundingClientRect().height), - }; - }); - for (const t of shots) { - await page.waitForTimeout(t); - await page.screenshot({ path: `${prefix}-${label}-${t}.png` }); - } - console.log(label, JSON.stringify({ ...info, requested })); - await ctx.close(); -} - -await probe('desktop', { viewport: { width: 1440, height: 900 } }, [0, 2500]); -await probe('mobile', { viewport: { width: 390, height: 844 } }); -await probe('reduced', { viewport: { width: 1440, height: 900 }, reducedMotion: 'reduce' }); -await browser.close(); diff --git a/scripts/hint-probe.mjs b/scripts/hint-probe.mjs deleted file mode 100644 index 558e0a5..0000000 --- a/scripts/hint-probe.mjs +++ /dev/null @@ -1,20 +0,0 @@ -import { chromium } from 'playwright'; -/* Reports where the chevron hint sits at the first snap and whether anything covers it. */ -const [,, url, width = '1440', height = '900'] = process.argv; -const browser = await chromium.launch(); -const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) } }); -await page.goto(url, { waitUntil: 'networkidle' }); -const snaps = await page.evaluate(() => Array.from(document.querySelectorAll('[data-deck-snap]')).map((a) => Math.round(a.getBoundingClientRect().top + window.scrollY - parseFloat(getComputedStyle(a).scrollMarginTop)))); -await page.evaluate((y) => window.scrollTo(0, y), snaps[0]); -await page.waitForTimeout(800); -const info = await page.evaluate(() => { - const h = document.querySelector('[data-deck-hint]'); - const r = h.getBoundingClientRect(); - const cs = getComputedStyle(h); - const svg = h.querySelector('svg').getBoundingClientRect(); - const atPoint = document.elementFromPoint(window.innerWidth / 2, window.innerHeight - 10); - const next = document.querySelectorAll('[data-deck-card]')[1].getBoundingClientRect(); - return { hint: { top: r.top, bottom: r.bottom, h: r.height }, svg: { top: svg.top, bottom: svg.bottom }, position: cs.position, display: cs.display, bottom: cs.bottom, zIndex: cs.zIndex, opacity: cs.opacity, atPoint: atPoint && (atPoint.tagName + '.' + atPoint.className), nextCardTop: next.top, innerHeight: window.innerHeight }; -}); -console.log(JSON.stringify(info, null, 1)); -await browser.close(); diff --git a/scripts/mobile-reveal-test.mjs b/scripts/mobile-reveal-test.mjs deleted file mode 100644 index ee8d1f9..0000000 --- a/scripts/mobile-reveal-test.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { chromium } from 'playwright'; -const browser = await chromium.launch(); -/* Phone: the sweep should still run, the hover state should not exist. */ -const ctx = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true }); -const page = await ctx.newPage(); -await page.goto(process.argv[2], { waitUntil: 'networkidle' }); -await page.locator('.stages__list').scrollIntoViewIfNeeded(); -await page.waitForTimeout(2000); -console.log('mobile shown:', JSON.stringify(await page.evaluate(() => - Array.from(document.querySelectorAll('[data-reveal]')).map((o) => o.hasAttribute('data-shown'))))); -console.log('grid opacity (no hover):', await page.evaluate(() => - getComputedStyle(document.querySelector('.grid-object__base')).opacity)); -await page.screenshot({ path: process.argv[3] }); -await browser.close(); diff --git a/scripts/snap-test.mjs b/scripts/snap-test.mjs deleted file mode 100644 index 6cf957b..0000000 --- a/scripts/snap-test.mjs +++ /dev/null @@ -1,28 +0,0 @@ -import { chromium } from 'playwright'; -/* Scrolls to positions around each deck card's snap point and reports where the page comes to rest. */ -const [,, url, width = '1440', height = '900'] = process.argv; -const browser = await chromium.launch(); -const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) } }); -await page.goto(url, { waitUntil: 'networkidle' }); -const info = await page.evaluate(() => { - const cards = Array.from(document.querySelectorAll('[data-deck-snap]')); - const cs = getComputedStyle(document.documentElement); - return cards.map((c, i) => { - const st = getComputedStyle(c); - return { i, naturalTop: c.offsetTop + (c.offsetParent?.getBoundingClientRect().top ?? 0) + window.scrollY, marginTop: st.scrollMarginTop, snapAlign: st.scrollSnapAlign, snapStop: st.scrollSnapStop, htmlSnap: cs.scrollSnapType }; - }); -}); -console.log(JSON.stringify(info, null, 1)); -// expected snap points: naturalTop - scrollMarginTop -const results = []; -for (const c of info) { - const expected = Math.round(c.naturalTop - parseFloat(c.marginTop)); - for (const off of [-220, 180, 320, 352]) { - await page.evaluate((y) => window.scrollTo({ top: y, behavior: 'auto' }), expected + off); - await page.waitForTimeout(1200); - const y = await page.evaluate(() => Math.round(window.scrollY)); - results.push({ card: c.i, expected, offset: off, landed: y, snapped: Math.abs(y - expected) < 2 }); - } -} -console.table(results); -await browser.close(); diff --git a/scripts/stack-shots-b.mjs b/scripts/stack-shots-b.mjs deleted file mode 100644 index 03bf418..0000000 --- a/scripts/stack-shots-b.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { chromium } from 'playwright'; -const [,, url, prefix, anchor, width = '1440', height = '900', step = '450', count = '10'] = process.argv; -const browser = await chromium.launch(); -const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) } }); -await page.goto(url, { waitUntil: 'networkidle' }); -const start = await page.evaluate((sel) => document.querySelector(sel).getBoundingClientRect().top + window.scrollY, anchor); -for (let i = 0; i < Number(count); i++) { - await page.evaluate((y) => window.scrollTo(0, y), start + i * Number(step)); - await page.waitForTimeout(i === 0 ? 3000 : 1800); - await page.screenshot({ path: `${prefix}-${String(i).padStart(2, '0')}.png` }); -} -await browser.close(); -console.log('stack shots', count); diff --git a/scripts/stack-shots-c.mjs b/scripts/stack-shots-c.mjs deleted file mode 100644 index d55abb0..0000000 --- a/scripts/stack-shots-c.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { chromium } from 'playwright'; -const [,, url, prefix, anchor, width = '1440', height = '900', step = '450', count = '10'] = process.argv; -const browser = await chromium.launch(); -const page = await browser.newPage({ viewport: { width: Number(width), height: Number(height) } }); -await page.goto(url, { waitUntil: 'networkidle' }); -const start = await page.evaluate((sel) => document.querySelector(sel).getBoundingClientRect().top + window.scrollY, anchor); -for (let i = 0; i < Number(count); i++) { - await page.evaluate((y) => window.scrollTo(0, y), start + i * Number(step)); - await page.waitForTimeout(i === 0 ? 2500 : 1500); - await page.screenshot({ path: `${prefix}-${String(i).padStart(2, '0')}.png` }); -} -await browser.close(); -console.log('stack shots', count); diff --git a/server/contact.mjs b/server/contact.mjs index aa5292f..15863eb 100644 --- a/server/contact.mjs +++ b/server/contact.mjs @@ -282,7 +282,7 @@ async function notify(req, res, form, lang) { } /** Step two: the link is opened; the confirmed registration goes to the office. */ -async function verify(req, res, query) { +async function verify(res, query) { const t = unsign(query.get('t')); const lang = t?.lang === 'de' ? 'de' : 'en'; const now = Date.now(); @@ -334,7 +334,7 @@ const server = createServer(async (req, res) => { return; } if (path === '/notify/verify' && req.method === 'GET') { - await verify(req, res, new URLSearchParams(search)); + await verify(res, new URLSearchParams(search)); return; } const route = ROUTES[path]; diff --git a/src/assets/images/background.png b/src/assets/images/background.png deleted file mode 100644 index 0d2ad2a..0000000 Binary files a/src/assets/images/background.png and /dev/null differ diff --git a/src/assets/images/background_3.png b/src/assets/images/background_3.png deleted file mode 100644 index 5d0ea53..0000000 Binary files a/src/assets/images/background_3.png and /dev/null differ diff --git a/src/assets/images/plant-at-dusk.jpg b/src/assets/images/plant-at-dusk.jpg deleted file mode 100644 index c9cd4da..0000000 Binary files a/src/assets/images/plant-at-dusk.jpg and /dev/null differ diff --git a/src/assets/images/sector-rd-engineer.jpg b/src/assets/images/sector-rd-engineer.jpg deleted file mode 100644 index 23430b2..0000000 Binary files a/src/assets/images/sector-rd-engineer.jpg and /dev/null differ diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 9207fbf..1f0147d 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -15,7 +15,7 @@ const funding = lang === 'de' ? fundingDe : fundingEn; const columns = [ { title: ui.footer.capabilities, - links: ui.method.stages.map((s) => ({ href: `${home}#capability-${s.card}`, label: s.title })), + links: ui.method.stages.map((s) => ({ href: `${home}#capabilities`, label: s.title })), }, { title: ui.footer.company, diff --git a/src/components/home/Method.astro b/src/components/home/Method.astro index 1f5641e..5615646 100644 --- a/src/components/home/Method.astro +++ b/src/components/home/Method.astro @@ -7,7 +7,7 @@ interface Props { lang: Lang } const { lang } = Astro.props; const ui = t(lang); --- -
+
-
+ +

{ui.method.lines.map((line, i) => 0 }]}>{line})}

diff --git a/src/components/home/Stages.astro b/src/components/home/Stages.astro deleted file mode 100644 index 361a652..0000000 --- a/src/components/home/Stages.astro +++ /dev/null @@ -1,70 +0,0 @@ ---- -import GridObject from './GridObject.astro'; -import { t, type Lang } from '../../i18n/ui'; -interface Props { lang: Lang } -const { lang } = Astro.props; -const ui = t(lang); -const objects = ['sphere', 'cube', 'plane', 'wave'] as const; ---- -
-
-

{ui.stages.eyebrow}

-
    - {ui.stages.items.map((item, i) => ( -
  1. -

    {item.title}

    - -

    {item.body}

    -
  2. - ))} -
-
-
- - diff --git a/src/components/verifier/Cards.astro b/src/components/verifier/Cards.astro index e804efd..2c158bc 100644 --- a/src/components/verifier/Cards.astro +++ b/src/components/verifier/Cards.astro @@ -1,5 +1,5 @@ --- -import GridObject from '../home/GridObject.astro'; +import GridObject from './GridObject.astro'; import { t, type Lang } from '../../i18n/ui'; interface Props { lang: Lang } const { lang } = Astro.props; diff --git a/src/components/home/GridObject.astro b/src/components/verifier/GridObject.astro similarity index 61% rename from src/components/home/GridObject.astro rename to src/components/verifier/GridObject.astro index bb2122b..04bc467 100644 --- a/src/components/home/GridObject.astro +++ b/src/components/verifier/GridObject.astro @@ -3,12 +3,10 @@ * Grid objects: wireframe solids drawn as technical line drawings. * Single stroke in currentColor, even width, no fills. The geometry is computed * at build time so the markup stays small and the stroke stays exact. - * Kinds: sphere (a model seen from every side), cube (a built structure), - * plane (one element lifted out for inspection), wave (a signal over time), - * steps (a ranked row of columns), nested (a structure inside its own walls), + * Kinds: steps (a ranked row of columns), nested (a structure inside its own walls), * stack (layers kept one on top of the other). */ -interface Props { kind: 'sphere' | 'cube' | 'plane' | 'wave' | 'steps' | 'nested' | 'stack'; class?: string } +interface Props { kind: 'steps' | 'nested' | 'stack'; class?: string } const { kind, class: className } = Astro.props; const S = 400; @@ -60,78 +58,6 @@ function plane(n: number, u: number, y: number, cy: number, cx = ISO.cx): string const base: string[] = []; const accent: string[] = []; -if (kind === 'sphere') { - const R = 150, tilt = (22 * Math.PI) / 180, N = 48; - const p3 = (lat: number, lon: number): [number, number] => { - const x = R * Math.cos(lat) * Math.sin(lon); - const y = R * Math.sin(lat); - const z = R * Math.cos(lat) * Math.cos(lon); - const y2 = y * Math.cos(tilt) - z * Math.sin(tilt); - return [ISO.cx + x, ISO.cy - y2]; - }; - for (let d = -80; d <= 80; d += 20) { - const lat = (d * Math.PI) / 180; - const pts: [number, number][] = []; - for (let i = 0; i <= N; i++) pts.push(p3(lat, (i / N) * 2 * Math.PI)); - (d === 0 ? accent : base).push(poly(pts)); - } - for (let m = 0; m < 8; m++) { - const lon = (m * Math.PI) / 8; - const pts: [number, number][] = []; - for (let i = 0; i <= N; i++) pts.push(p3((i / N) * 2 * Math.PI - Math.PI, lon)); - (m === 0 ? accent : base).push(poly(pts)); - } - accent.push(`circle:${R}`); - track(ISO.cx - R, ISO.cy - R); - track(ISO.cx + R, ISO.cy + R); -} - -if (kind === 'cube') { - const n = 4, u = 46, L = n * u, cy = ISO.cy + L * 0.5; - for (let i = 0; i <= n; i++) { - const t = i * u; - /* The first and last line of each face are the cube's own edges. */ - const target = i === 0 || i === n ? accent : base; - target.push(poly([iso(t, L, 0, cy), iso(t, L, L, cy)])); - target.push(poly([iso(0, L, t, cy), iso(L, L, t, cy)])); - target.push(poly([iso(t, 0, 0, cy), iso(t, L, 0, cy)])); - target.push(poly([iso(0, t, 0, cy), iso(L, t, 0, cy)])); - target.push(poly([iso(0, 0, t, cy), iso(0, L, t, cy)])); - target.push(poly([iso(0, t, 0, cy), iso(0, t, L, cy)])); - } -} - -if (kind === 'plane') { - const n = 6, u = 34, L = n * u, cy = ISO.cy + 20, h = 96, tx = 2, tz = 2; - for (let i = 0; i <= n; i++) { - const t = i * u; - base.push(poly([iso(t, 0, 0, cy), iso(t, 0, L, cy)])); - base.push(poly([iso(0, 0, t, cy), iso(L, 0, t, cy)])); - } - /* The tile that has been lifted out for inspection. */ - const c: [number, number][] = [[tx, tz], [tx + 1, tz], [tx + 1, tz + 1], [tx, tz + 1]]; - const top = c.map(([x, z]) => iso(x * u, h, z * u, cy)); - accent.push(poly([...top, top[0]])); - c.forEach(([x, z], i) => accent.push(poly([iso(x * u, 0, z * u, cy), top[i]]))); -} - -if (kind === 'wave') { - const nx = 12, nz = 8, u = 26, cy = ISO.cy + 10, A = 26; - const yAt = (x: number, z: number) => A * Math.sin((x / nx) * Math.PI * 2 - 0.6) * Math.cos((z / nz) * Math.PI * 0.9); - const P = (x: number, z: number) => iso((x - nx / 2) * u, yAt(x, z), (z - nz / 2) * u, cy); - /* The lines along x carry the wave itself; the cross lines are the grid it is read against. */ - for (let z = 0; z <= nz; z++) { - const pts: [number, number][] = []; - for (let x = 0; x <= nx; x++) pts.push(P(x, z)); - accent.push(poly(pts)); - } - for (let x = 0; x <= nx; x++) { - const pts: [number, number][] = []; - for (let z = 0; z <= nz; z++) pts.push(P(x, z)); - base.push(poly(pts)); - } -} - if (kind === 'steps') { /* Five columns in a row, tallest first: a ranked list seen as a skyline. */ const heights = [168, 128, 96, 68, 44], w = 40, gap = 14, depth = 40, cy = ISO.cy + 78; @@ -167,7 +93,7 @@ if (kind === 'stack') { corners.forEach(([x, z]) => base.push(poly([iso(x, 0, z, cy), iso(x, (layers - 1) * gapY, z, cy)]))); } /* A square box centred on the drawing: wide drawings still fill the width, and every object - keeps the same footprint, so the four cards of a row stay even. The padding covers the + keeps the same footprint, so the cards of a row stay even. The padding covers the stroke, which sits half outside the geometry. */ const PAD = 8; const width = maxX - minX; @@ -178,14 +104,10 @@ const viewBox = [minX + width / 2 - side / 2, minY + height / 2 - side / 2, side diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index a57fc91..817fba1 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -1,8 +1,6 @@ import type { HowGroups } from '../components/verifier/types'; export type Lang = 'en' | 'de'; -export const langs: Lang[] = ['en', 'de']; -export const defaultLang: Lang = 'en'; export function langFromUrl(url: URL): Lang { const [, first] = url.pathname.split('/'); @@ -63,20 +61,11 @@ const en = { imageAlt: 'An empty monitoring room seen through glass, walls of screens and indicator lights blurred behind the panes.', diagramTitle: 'The CipherCell method', diagramDesc: 'A triangle of three pillars, each joined to the other two: security, engineering and research. Each pillar carries its own instruments: threat modelling, automated threat analysis and security testing; secure architecture, FPGA and hardware design, and embedded and systems development; Open RAN and 6G security, hardware roots of trust, and vehicle and IoT security. Research feeds back into security.', - /* Pillars of the diagram and the footer column. `card` is the What-we-do card the footer links to. */ + /* Pillars of the diagram and the footer column, which links them to this section. */ stages: [ - { title: 'Security', card: 1, items: ['Threat modelling', 'Automated threat analysis', 'Security testing'] }, - { title: 'Engineering', card: 2, items: ['Secure architecture', 'FPGA and hardware design', 'Embedded and systems development'] }, - { title: 'Research', card: 4, items: ['Open RAN and 6G security', 'Hardware roots of trust', 'Vehicle and IoT security'] }, - ], - }, - stages: { - eyebrow: 'What we do.', - items: [ - { title: 'Threat analysis', body: 'Automated, empirical analysis of real attack paths through your software. It runs inside your CI/CD pipeline and delivers continuous assurance.' }, - { title: 'Engineering', body: 'Zero-trust enforcement, key management and secure API design as modular, composable components for your codebase and deployments.' }, - { title: 'Verification', body: 'Independent review of code and security measures, hardening checklists and application-level testing across all components.' }, - { title: 'Monitoring', body: 'Intrusion detection and security logging built into the software, from the first deployment through ongoing resilience verification.' }, + { title: 'Security', items: ['Threat modelling', 'Automated threat analysis', 'Security testing'] }, + { title: 'Engineering', items: ['Secure architecture', 'FPGA and hardware design', 'Embedded and systems development'] }, + { title: 'Research', items: ['Open RAN and 6G security', 'Hardware roots of trust', 'Vehicle and IoT security'] }, ], }, sectors: { @@ -372,9 +361,6 @@ const en = { }, legal: { imprintEyebrow: 'Information pursuant to § 5 TMG', - responsible: 'Responsible', - mailing: 'Mailing address', - contact: 'Contact', asOf: 'As of 14 June 2025', optOut: { checking: 'Checking whether your visits are counted.', @@ -426,18 +412,9 @@ const de: typeof en = { diagramTitle: 'Die CipherCell-Methode', diagramDesc: 'Ein Dreieck aus drei Säulen, jede mit den beiden anderen verbunden: Sicherheit, Engineering und Forschung. Jede Säule hat ihre Instrumente: Bedrohungsmodellierung, automatisierte Bedrohungsanalyse und Sicherheitstests; sichere Architektur, FPGA- und Hardware-Design sowie Embedded- und Systementwicklung; Open-RAN- und 6G-Sicherheit, Hardware-Vertrauensanker sowie Fahrzeug- und IoT-Sicherheit. Die Forschung fließt zurück in die Sicherheit.', stages: [ - { title: 'Sicherheit', card: 1, items: ['Bedrohungsmodellierung', 'Automatisierte Bedrohungsanalyse', 'Sicherheitstests'] }, - { title: 'Engineering', card: 2, items: ['Sichere Architektur', 'FPGA- und Hardware-Design', 'Embedded- und Systementwicklung'] }, - { title: 'Forschung', card: 4, items: ['Open-RAN- und 6G-Sicherheit', 'Hardware-Vertrauensanker', 'Fahrzeug- und IoT-Sicherheit'] }, - ], - }, - stages: { - eyebrow: 'Was wir tun.', - items: [ - { title: 'Bedrohungs\u00ADanalyse', body: 'Automatisierte, empirische Analyse realer Angriffspfade durch Ihre Software. Sie läuft in Ihrer CI/CD-Pipeline und liefert kontinuierliche Absicherung.' }, - { title: 'Engineering', body: 'Zero-Trust-Durchsetzung, Schlüsselmanagement und sicheres API-Design als modulare, kombinierbare Komponenten für Ihre Codebasis und Deployments.' }, - { title: 'Verifikation', body: 'Unabhängige Prüfung von Code und Sicherheitsmaßnahmen, Härtungs-Checklisten und anwendungsnahe Tests über alle Komponenten.' }, - { title: 'Monitoring', body: 'Angriffserkennung und Security-Logging, in die Software eingebaut, vom ersten Deployment bis zur laufenden Resilienzprüfung.' }, + { title: 'Sicherheit', items: ['Bedrohungsmodellierung', 'Automatisierte Bedrohungsanalyse', 'Sicherheitstests'] }, + { title: 'Engineering', items: ['Sichere Architektur', 'FPGA- und Hardware-Design', 'Embedded- und Systementwicklung'] }, + { title: 'Forschung', items: ['Open-RAN- und 6G-Sicherheit', 'Hardware-Vertrauensanker', 'Fahrzeug- und IoT-Sicherheit'] }, ], }, sectors: { @@ -733,9 +710,6 @@ const de: typeof en = { }, legal: { imprintEyebrow: 'Angaben gemäß § 5 TMG', - responsible: 'Verantwortlich', - mailing: 'Postanschrift', - contact: 'Kontakt', asOf: 'Stand: 14. Juni 2025', optOut: { checking: 'Es wird geprüft, ob Ihre Besuche gezählt werden.', diff --git a/src/pages/_Home.astro b/src/pages/_Home.astro index ef208d3..b02bd67 100644 --- a/src/pages/_Home.astro +++ b/src/pages/_Home.astro @@ -3,7 +3,6 @@ import Base from '../layouts/Base.astro'; import Hero from '../components/home/Hero.astro'; import Manifesto from '../components/home/Manifesto.astro'; import Method from '../components/home/Method.astro'; -import Stages from '../components/home/Stages.astro'; import Sectors from '../components/home/Sectors.astro'; import Partners from '../components/home/Partners.astro'; import Quote from '../components/home/Quote.astro'; @@ -19,7 +18,6 @@ const ui = t(lang); - diff --git a/src/styles/tokens.css b/src/styles/tokens.css index edc6e60..45316d2 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -44,10 +44,6 @@ /* The same for a field whose text sits centred on the left (the home page Insights cards). */ --field-scrim-centre: radial-gradient(90% 85% at 20% 50%, rgb(14 16 15 / 0.72), rgb(14 16 15 / 0) 100%); - /* Signal: the one status tone the brief allows. Used only for a live state (the coming-soon - page's current phase), at most twice per page. Never for text, links or decoration. */ - --signal: #58C8D8; - /* Selected text: the logo cyan with base ink on it (about 9:1), the same on paper and night. */ --selection: var(--logo-cyan); --selection-ink: var(--ink-base);