Refactoring
This commit is contained in:
1 parent
fd58a8d32f
commit
97e219456d
29 files changed
+45
-408
No files matched your search
@@ -11,7 +11,7 @@
|
|||||||
the point — the visual truth stays in the site's stylesheet, so the design system cannot
|
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
|
drift from the site even as `base.css` changes, and each component's markup ports back to
|
||||||
`.astro` one to one (every `<Name>.md` states the Astro equivalent).
|
`.astro` one to one (every `<Name>.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
|
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.
|
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
|
- `Stack` is the one component with no class behind it. Nothing in this system carries its own
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ ds-bundle/
|
|||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
__pycache__/
|
||||||
@@ -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/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/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/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/*` | One file per home section, in page order: Hero, Manifesto, Method (with `MethodDiagram`), Sectors, Partners, 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/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/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/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. |
|
| `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. |
|
| `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/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. |
|
| `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/fonts/` | Self-hosted Inter Tight and JetBrains Mono, variable (latin, latin-ext). |
|
||||||
| `public/video/` | The home hero's background loop, MP4 and WebM. Shipped as-is; Astro does not process video. |
|
|
||||||
|
|
||||||
## Routes
|
## Routes
|
||||||
|
|
||||||
@@ -151,47 +151,31 @@ The three newest posts appear on the home page automatically.
|
|||||||
|
|
||||||
## The card drawings
|
## The card drawings
|
||||||
|
|
||||||
The objects on the stage cards ("What we do.") and the product cards do not animate on arrival;
|
The objects on the product cards do not animate on arrival; they are simply drawn. Hovering a
|
||||||
they are simply drawn. Hovering a card sets `--object-grid`, which the drawing answers by letting
|
card sets `--object-grid`, which the drawing answers by letting its grid recede to 26% while the
|
||||||
its grid recede to 26% while the lines that carry the idea stay: the cube's own edges, the tile
|
lines that carry the idea stay: the inner cube inside its shell, the column to fix first, the
|
||||||
lifted out of the plane, the wave's profiles, the inner cube inside its shell, the column to fix
|
newest layer of the stack. The card also sets `--object-scale`. Both are custom properties the card owns and the
|
||||||
first. 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.
|
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
|
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 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
|
The home hero is a noise field in the logo's blues on night, drawn by a small WebGL shader
|
||||||
`plant-at-dusk.jpg` still. The still is the video's own first frame, so it carries the hero until
|
(`src/scripts/noise-field.ts`) that drifts slowly and bends towards the pointer. It renders at half
|
||||||
the video is ready and stays put for anyone who never gets one.
|
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 video is only fetched when it will be seen: not under `prefers-reduced-motion`, not on
|
the hero for anyone who never gets the canvas: reduced motion, no WebGL, no script.
|
||||||
`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.
|
|
||||||
|
|
||||||
## Imagery
|
## Imagery
|
||||||
|
|
||||||
The mast at dusk (`mast-at-dusk.jpg`) is the one documentary photograph from the old site; it
|
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
|
(Seedream 5 Pro) on 18 September 2026 in a desaturated documentary style, because no real
|
||||||
photography existed:
|
photography existed:
|
||||||
|
|
||||||
| File | Used in | Magnific creation |
|
| 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 |
|
| `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-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 |
|
| `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
|
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
|
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
|
(https://www.magnific.com/app/creation/aFsD5crfSh) and then rebuilt as procedural SVG so the
|
||||||
stroke matches the rest of the site.
|
stroke matches the rest of the site.
|
||||||
|
|
||||||
@@ -237,9 +221,12 @@ them for review.
|
|||||||
|
|
||||||
## Interaction and motion
|
## Interaction and motion
|
||||||
|
|
||||||
Three islands of script: the mobile navigation toggle in `Header.astro`, the sectors tabs in
|
Islands of script: the mobile navigation toggle in `Header.astro`, the hero's noise field
|
||||||
`Sectors.astro` (`src/scripts/tabs.ts`: ARIA tablist, arrow keys, Home and End), and the reveal
|
(`src/scripts/noise-field.ts`), the sectors tabs in `Sectors.astro` (`src/scripts/tabs.ts`: ARIA
|
||||||
observer used by the product page's drawings.
|
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
|
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
|
and meet in one point (off under `prefers-reduced-motion`); hovering a node in a schematic dims
|
||||||
|
|||||||
Generated
-10
@@ -8,7 +8,6 @@
|
|||||||
"name": "ciphercell-homepage",
|
"name": "ciphercell-homepage",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter-tight": "^5.3.0",
|
|
||||||
"altcha-lib": "^2.6.0",
|
"altcha-lib": "^2.6.0",
|
||||||
"astro": "^7.3.3",
|
"astro": "^7.3.3",
|
||||||
"sharp": "^0.35.4"
|
"sharp": "^0.35.4"
|
||||||
@@ -1141,15 +1140,6 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"contact": "node --env-file-if-exists=.env server/contact.mjs"
|
"contact": "node --env-file-if-exists=.env server/contact.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter-tight": "^5.3.0",
|
|
||||||
"altcha-lib": "^2.6.0",
|
"altcha-lib": "^2.6.0",
|
||||||
"astro": "^7.3.3",
|
"astro": "^7.3.3",
|
||||||
"sharp": "^0.35.4"
|
"sharp": "^0.35.4"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
|
||||||
@@ -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 <source.mp4> <out-basename> [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"
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
|
||||||
@@ -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);
|
|
||||||
@@ -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);
|
|
||||||
+2
-2
@@ -282,7 +282,7 @@ async function notify(req, res, form, lang) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Step two: the link is opened; the confirmed registration goes to the office. */
|
/** 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 t = unsign(query.get('t'));
|
||||||
const lang = t?.lang === 'de' ? 'de' : 'en';
|
const lang = t?.lang === 'de' ? 'de' : 'en';
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -334,7 +334,7 @@ const server = createServer(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (path === '/notify/verify' && req.method === 'GET') {
|
if (path === '/notify/verify' && req.method === 'GET') {
|
||||||
await verify(req, res, new URLSearchParams(search));
|
await verify(res, new URLSearchParams(search));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const route = ROUTES[path];
|
const route = ROUTES[path];
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.6 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 413 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 263 KiB |
@@ -15,7 +15,7 @@ const funding = lang === 'de' ? fundingDe : fundingEn;
|
|||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: ui.footer.capabilities,
|
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,
|
title: ui.footer.company,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Props { lang: Lang }
|
|||||||
const { lang } = Astro.props;
|
const { lang } = Astro.props;
|
||||||
const ui = t(lang);
|
const ui = t(lang);
|
||||||
---
|
---
|
||||||
<section class="night method" id="capabilities" aria-labelledby="method-title">
|
<section class="night method" aria-labelledby="method-title">
|
||||||
<Picture
|
<Picture
|
||||||
src={monitoringRoom}
|
src={monitoringRoom}
|
||||||
alt={ui.method.imageAlt}
|
alt={ui.method.imageAlt}
|
||||||
@@ -21,7 +21,8 @@ const ui = t(lang);
|
|||||||
pictureAttributes={{ class: 'method__picture' }}
|
pictureAttributes={{ class: 'method__picture' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="container grid method__intro">
|
<!-- The footer's pillar links land here, where the dark area begins, rather than on the image. -->
|
||||||
|
<div class="container grid method__intro" id="capabilities">
|
||||||
<h2 id="method-title" class="h2 method__title">
|
<h2 id="method-title" class="h2 method__title">
|
||||||
{ui.method.lines.map((line, i) => <span class:list={['line', { 'line--muted': i > 0 }]}>{line}</span>)}
|
{ui.method.lines.map((line, i) => <span class:list={['line', { 'line--muted': i > 0 }]}>{line}</span>)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
---
|
|
||||||
<section class="section--tight stages" aria-labelledby="stages-title">
|
|
||||||
<div class="container">
|
|
||||||
<h2 id="stages-title" class="eyebrow stages__eyebrow">{ui.stages.eyebrow}</h2>
|
|
||||||
<ol class="stages__list" role="list">
|
|
||||||
{ui.stages.items.map((item, i) => (
|
|
||||||
<li class="stages__item" id={`capability-${i + 1}`}>
|
|
||||||
<h3 class="stages__title">{item.title}</h3>
|
|
||||||
<GridObject kind={objects[i]} class="stages__object" />
|
|
||||||
<p class="muted stages__body">{item.body}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.stages__eyebrow { margin-bottom: var(--space-6); }
|
|
||||||
.stages__list {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--gap);
|
|
||||||
}
|
|
||||||
.stages__item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
/* Title, then the drawing in the row that takes the slack, then the body at the bottom. */
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
gap: var(--space-6);
|
|
||||||
padding: var(--space-6) var(--space-6) var(--space-7);
|
|
||||||
background: var(--paper-deep);
|
|
||||||
scroll-margin-top: calc(var(--header-offset) + var(--space-5));
|
|
||||||
}
|
|
||||||
.stages__title {
|
|
||||||
font-size: var(--step-title);
|
|
||||||
line-height: var(--leading-title);
|
|
||||||
letter-spacing: var(--track-h2);
|
|
||||||
font-weight: var(--weight-regular);
|
|
||||||
/* Two lines reserved, so objects and bodies line up across the row. */
|
|
||||||
min-height: calc(2 * var(--leading-title) * 1em);
|
|
||||||
hyphens: auto;
|
|
||||||
}
|
|
||||||
.stages__item :global(.stages__object) {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 16rem;
|
|
||||||
justify-self: center;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
/* Hovering a card lets its grid recede, so the shape that carries the idea stands alone. */
|
|
||||||
@media (hover: hover) {
|
|
||||||
.stages__item:hover {
|
|
||||||
--object-scale: 1.03;
|
|
||||||
--object-grid: 0.26;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stages__body { max-width: 24em; }
|
|
||||||
@media (min-width: 48rem) {
|
|
||||||
.stages__list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
}
|
|
||||||
@media (min-width: 64rem) {
|
|
||||||
.stages__list { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
||||||
.stages__item :global(.stages__object) { max-width: 13rem; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import GridObject from '../home/GridObject.astro';
|
import GridObject from './GridObject.astro';
|
||||||
import { t, type Lang } from '../../i18n/ui';
|
import { t, type Lang } from '../../i18n/ui';
|
||||||
interface Props { lang: Lang }
|
interface Props { lang: Lang }
|
||||||
const { lang } = Astro.props;
|
const { lang } = Astro.props;
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
* Grid objects: wireframe solids drawn as technical line drawings.
|
* Grid objects: wireframe solids drawn as technical line drawings.
|
||||||
* Single stroke in currentColor, even width, no fills. The geometry is computed
|
* 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.
|
* 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),
|
* Kinds: steps (a ranked row of columns), nested (a structure inside its own walls),
|
||||||
* 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),
|
|
||||||
* stack (layers kept one on top of the other).
|
* 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 { kind, class: className } = Astro.props;
|
||||||
|
|
||||||
const S = 400;
|
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 base: string[] = [];
|
||||||
const accent: 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') {
|
if (kind === 'steps') {
|
||||||
/* Five columns in a row, tallest first: a ranked list seen as a skyline. */
|
/* 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;
|
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)])));
|
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
|
/* 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. */
|
stroke, which sits half outside the geometry. */
|
||||||
const PAD = 8;
|
const PAD = 8;
|
||||||
const width = maxX - minX;
|
const width = maxX - minX;
|
||||||
@@ -178,14 +104,10 @@ const viewBox = [minX + width / 2 - side / 2, minY + height / 2 - side / 2, side
|
|||||||
<svg class:list={['grid-object', className]} viewBox={viewBox} aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
<svg class:list={['grid-object', className]} viewBox={viewBox} aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g class="grid-object__lines" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke">
|
<g class="grid-object__lines" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke">
|
||||||
<g class="grid-object__base">
|
<g class="grid-object__base">
|
||||||
{base.map((l) => l.startsWith('circle:')
|
{base.map((l) => <polyline points={l} />)}
|
||||||
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
|
|
||||||
: <polyline points={l} />)}
|
|
||||||
</g>
|
</g>
|
||||||
<g class="grid-object__accent">
|
<g class="grid-object__accent">
|
||||||
{accent.map((l) => l.startsWith('circle:')
|
{accent.map((l) => <polyline points={l} />)}
|
||||||
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
|
|
||||||
: <polyline points={l} />)}
|
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
+7
-33
@@ -1,8 +1,6 @@
|
|||||||
import type { HowGroups } from '../components/verifier/types';
|
import type { HowGroups } from '../components/verifier/types';
|
||||||
|
|
||||||
export type Lang = 'en' | 'de';
|
export type Lang = 'en' | 'de';
|
||||||
export const langs: Lang[] = ['en', 'de'];
|
|
||||||
export const defaultLang: Lang = 'en';
|
|
||||||
|
|
||||||
export function langFromUrl(url: URL): Lang {
|
export function langFromUrl(url: URL): Lang {
|
||||||
const [, first] = url.pathname.split('/');
|
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.',
|
imageAlt: 'An empty monitoring room seen through glass, walls of screens and indicator lights blurred behind the panes.',
|
||||||
diagramTitle: 'The CipherCell method',
|
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.',
|
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: [
|
stages: [
|
||||||
{ title: 'Security', card: 1, items: ['Threat modelling', 'Automated threat analysis', 'Security testing'] },
|
{ title: 'Security', items: ['Threat modelling', 'Automated threat analysis', 'Security testing'] },
|
||||||
{ title: 'Engineering', card: 2, items: ['Secure architecture', 'FPGA and hardware design', 'Embedded and systems development'] },
|
{ title: 'Engineering', 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'] },
|
{ title: 'Research', 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.' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sectors: {
|
sectors: {
|
||||||
@@ -372,9 +361,6 @@ const en = {
|
|||||||
},
|
},
|
||||||
legal: {
|
legal: {
|
||||||
imprintEyebrow: 'Information pursuant to § 5 TMG',
|
imprintEyebrow: 'Information pursuant to § 5 TMG',
|
||||||
responsible: 'Responsible',
|
|
||||||
mailing: 'Mailing address',
|
|
||||||
contact: 'Contact',
|
|
||||||
asOf: 'As of 14 June 2025',
|
asOf: 'As of 14 June 2025',
|
||||||
optOut: {
|
optOut: {
|
||||||
checking: 'Checking whether your visits are counted.',
|
checking: 'Checking whether your visits are counted.',
|
||||||
@@ -426,18 +412,9 @@ const de: typeof en = {
|
|||||||
diagramTitle: 'Die CipherCell-Methode',
|
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.',
|
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: [
|
stages: [
|
||||||
{ title: 'Sicherheit', card: 1, items: ['Bedrohungsmodellierung', 'Automatisierte Bedrohungsanalyse', 'Sicherheitstests'] },
|
{ title: 'Sicherheit', items: ['Bedrohungsmodellierung', 'Automatisierte Bedrohungsanalyse', 'Sicherheitstests'] },
|
||||||
{ title: 'Engineering', card: 2, items: ['Sichere Architektur', 'FPGA- und Hardware-Design', 'Embedded- und Systementwicklung'] },
|
{ title: 'Engineering', 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'] },
|
{ title: 'Forschung', 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.' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
sectors: {
|
sectors: {
|
||||||
@@ -733,9 +710,6 @@ const de: typeof en = {
|
|||||||
},
|
},
|
||||||
legal: {
|
legal: {
|
||||||
imprintEyebrow: 'Angaben gemäß § 5 TMG',
|
imprintEyebrow: 'Angaben gemäß § 5 TMG',
|
||||||
responsible: 'Verantwortlich',
|
|
||||||
mailing: 'Postanschrift',
|
|
||||||
contact: 'Kontakt',
|
|
||||||
asOf: 'Stand: 14. Juni 2025',
|
asOf: 'Stand: 14. Juni 2025',
|
||||||
optOut: {
|
optOut: {
|
||||||
checking: 'Es wird geprüft, ob Ihre Besuche gezählt werden.',
|
checking: 'Es wird geprüft, ob Ihre Besuche gezählt werden.',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Base from '../layouts/Base.astro';
|
|||||||
import Hero from '../components/home/Hero.astro';
|
import Hero from '../components/home/Hero.astro';
|
||||||
import Manifesto from '../components/home/Manifesto.astro';
|
import Manifesto from '../components/home/Manifesto.astro';
|
||||||
import Method from '../components/home/Method.astro';
|
import Method from '../components/home/Method.astro';
|
||||||
import Stages from '../components/home/Stages.astro';
|
|
||||||
import Sectors from '../components/home/Sectors.astro';
|
import Sectors from '../components/home/Sectors.astro';
|
||||||
import Partners from '../components/home/Partners.astro';
|
import Partners from '../components/home/Partners.astro';
|
||||||
import Quote from '../components/home/Quote.astro';
|
import Quote from '../components/home/Quote.astro';
|
||||||
@@ -19,7 +18,6 @@ const ui = t(lang);
|
|||||||
<Hero lang={lang} />
|
<Hero lang={lang} />
|
||||||
<Manifesto lang={lang} />
|
<Manifesto lang={lang} />
|
||||||
<Method lang={lang} />
|
<Method lang={lang} />
|
||||||
<!-- <Stages lang={lang} /> -->
|
|
||||||
<Sectors lang={lang} />
|
<Sectors lang={lang} />
|
||||||
<Partners lang={lang} />
|
<Partners lang={lang} />
|
||||||
<Quote lang={lang} />
|
<Quote lang={lang} />
|
||||||
|
|||||||
@@ -44,10 +44,6 @@
|
|||||||
/* The same for a field whose text sits centred on the left (the home page Insights cards). */
|
/* 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%);
|
--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. */
|
/* Selected text: the logo cyan with base ink on it (about 9:1), the same on paper and night. */
|
||||||
--selection: var(--logo-cyan);
|
--selection: var(--logo-cyan);
|
||||||
--selection-ink: var(--ink-base);
|
--selection-ink: var(--ink-base);
|
||||||
|
|||||||
Reference in new issue
Block a user