Compare commits
6
Commits
5ece51b567
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
327f53922d | ||
|
|
d1a9214ffd | ||
|
|
5b02ee8072 | ||
|
|
5da9e161f8 | ||
|
|
97e219456d | ||
|
|
fd58a8d32f |
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
|
||||
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).
|
||||
- **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
|
||||
|
||||
@@ -14,3 +14,4 @@ ds-bundle/
|
||||
.env
|
||||
.env.*
|
||||
!.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/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
|
||||
|
||||
@@ -106,8 +106,13 @@ Environment; see *Contact form* below.
|
||||
after the alpha mail is sent or on withdrawal. The result pages `/notify/confirmed` and
|
||||
`/notify/error` are `noindex` and left out of the sitemap. Set the flag to false to bring back
|
||||
the full product page.
|
||||
- **Matomo.** The privacy page links to the existing opt-out on `cipher.uber.space`. The tracking
|
||||
snippet itself is not included; add it in `src/layouts/Base.astro` if analytics should continue.
|
||||
- **Matomo.** Production builds load the tracker from the self-hosted Matomo at
|
||||
`matomo.ciphercell.de` (`src/components/Matomo.astro`, included in `src/layouts/Base.astro`);
|
||||
URL and site ID are in `src/data/analytics.ts`. It runs without cookies and honours Do Not
|
||||
Track, as the privacy page states; `astro dev` never loads it. The opt-out on the privacy page
|
||||
(`src/components/OptOut.astro`) asks the same tracker: opting out stores the one cookie
|
||||
`mtm_consent_removed`, opting back in deletes it. With Do Not Track, or when a blocker keeps
|
||||
`matomo.js` from loading, it says that nothing is counted instead of showing the button.
|
||||
|
||||
## Insights filter
|
||||
|
||||
@@ -146,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 |
|
||||
@@ -205,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.
|
||||
|
||||
@@ -232,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
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@ export default defineConfig({
|
||||
},
|
||||
build: { inlineStylesheets: 'auto' },
|
||||
// In development the form posts to the contact handler started with `npm run contact`.
|
||||
vite: { server: { proxy: { '^/(contact(/challenge)?|notify(/verify)?)$': { target: 'http://127.0.0.1:3000', changeOrigin: false } } } },
|
||||
vite: { server: { proxy: { '^/(contact(/challenge)?|notify(/verify)?)(\\?|$)': { target: 'http://127.0.0.1:3000', changeOrigin: false } } } },
|
||||
});
|
||||
Generated
-10
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
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. */
|
||||
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];
|
||||
|
||||
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 = [
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -250,6 +250,14 @@ const isCurrent = (href: string) => href.split('#')[0] === path && !href.include
|
||||
/* Tone: watch a one-pixel line through the middle of the bar and switch to night while a
|
||||
.night section crosses it. An IntersectionObserver, so nothing runs on scroll. */
|
||||
const sections = document.querySelectorAll<HTMLElement>('.night');
|
||||
/* The browser chrome follows the bar: theme-color tints the strip around the iPhone's notch and
|
||||
clock (and the toolbar in Chrome on Android), so it changes with the tone underneath. */
|
||||
const themeColor = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
||||
const tokens = getComputedStyle(document.documentElement);
|
||||
const chrome = (tone: string) => {
|
||||
const value = tokens.getPropertyValue(tone === 'night' ? '--night' : '--paper').trim();
|
||||
if (themeColor && value && themeColor.content !== value) themeColor.content = value;
|
||||
};
|
||||
// Transitions come back once the first tone is drawn (two frames, so it has been painted).
|
||||
const settle = () => requestAnimationFrame(() => requestAnimationFrame(() => header?.removeAttribute('data-tone-init')));
|
||||
if (header && sections.length && 'IntersectionObserver' in window) {
|
||||
@@ -264,6 +272,7 @@ const isCurrent = (href: string) => href.split('#')[0] === path && !href.include
|
||||
if (entry.isIntersecting) under.add(entry.target); else under.delete(entry.target);
|
||||
}
|
||||
header.dataset.tone = under.size ? 'night' : 'paper';
|
||||
chrome(header.dataset.tone);
|
||||
if (header.hasAttribute('data-tone-init')) settle();
|
||||
}, { rootMargin: `-${mid}px 0px -${window.innerHeight - mid - 1}px 0px` });
|
||||
sections.forEach((section) => observer!.observe(section));
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
/**
|
||||
* The Matomo tracker: a page view per page, outgoing links and downloads, no cookies, Do Not
|
||||
* Track honoured. matomo.js loads async from our own Matomo, so it never holds up the page.
|
||||
*/
|
||||
import { matomo } from '../data/analytics';
|
||||
const { url, siteId, enabled } = matomo;
|
||||
---
|
||||
{enabled && (
|
||||
<script is:inline define:vars={{ url, siteId }}>
|
||||
var _paq = (window._paq = window._paq || []);
|
||||
_paq.push(['setDoNotTrack', true]);
|
||||
_paq.push(['disableCookies']);
|
||||
_paq.push(['trackPageView']);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
_paq.push(['setTrackerUrl', url + 'matomo.php']);
|
||||
_paq.push(['setSiteId', siteId]);
|
||||
var s = document.createElement('script');
|
||||
s.async = true;
|
||||
s.src = url + 'matomo.js';
|
||||
document.head.appendChild(s);
|
||||
</script>
|
||||
)}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
/**
|
||||
* The Matomo opt-out on the privacy page, in the site's own type rather than Matomo's embed. It
|
||||
* asks the tracker already on the page: optUserOut stores the one cookie the tracker keeps with
|
||||
* cookies disabled (mtm_consent_removed), forgetUserOptOut removes it. If the tracker never
|
||||
* arrives (a content blocker, or a development build), the page says so: nothing is counted.
|
||||
*/
|
||||
import { t, type Lang } from '../i18n/ui';
|
||||
interface Props { lang: Lang }
|
||||
const { lang } = Astro.props;
|
||||
const o = t(lang).legal.optOut;
|
||||
---
|
||||
<div class="optout" data-optout data-status-in={o.in} data-status-out={o.out} data-status-dnt={o.dnt} data-status-missing={o.missing} data-label-out={o.stop} data-label-in={o.resume}>
|
||||
<p class="optout__status" role="status" data-optout-status>{o.checking}</p>
|
||||
<button class="button button--small" type="button" data-optout-toggle hidden></button>
|
||||
<noscript><p class="optout__status">{o.noscript}</p></noscript>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.optout { display: grid; gap: var(--space-4); justify-items: start; }
|
||||
.optout__status { color: var(--ink-muted); }
|
||||
.optout .button { font: inherit; font-size: var(--step-small); font-weight: var(--weight-medium); border: 0; cursor: pointer; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
type Tracker = { isUserOptedOut(): boolean; optUserOut(): void; forgetUserOptOut(): void };
|
||||
type Paq = { push(cmd: unknown[]): void };
|
||||
|
||||
const root = document.querySelector<HTMLElement>('[data-optout]');
|
||||
const status = root?.querySelector<HTMLElement>('[data-optout-status]');
|
||||
const toggle = root?.querySelector<HTMLButtonElement>('[data-optout-toggle]');
|
||||
|
||||
if (root && status && toggle) {
|
||||
const d = root.dataset;
|
||||
const dnt = navigator.doNotTrack === '1' || (window as { doNotTrack?: string }).doNotTrack === '1';
|
||||
let tracker: Tracker | undefined;
|
||||
|
||||
const render = () => {
|
||||
if (dnt) { status.textContent = d.statusDnt!; toggle.hidden = true; return; }
|
||||
if (!tracker) { status.textContent = d.statusMissing!; toggle.hidden = true; return; }
|
||||
const out = tracker.isUserOptedOut();
|
||||
status.textContent = out ? d.statusOut! : d.statusIn!;
|
||||
toggle.textContent = out ? d.labelIn! : d.labelOut!;
|
||||
toggle.hidden = false;
|
||||
};
|
||||
|
||||
toggle.addEventListener('click', () => {
|
||||
if (!tracker) return;
|
||||
if (tracker.isUserOptedOut()) tracker.forgetUserOptOut(); else tracker.optUserOut();
|
||||
render();
|
||||
});
|
||||
|
||||
// A function pushed onto _paq runs with the tracker as `this` once matomo.js has loaded.
|
||||
const paq = (window as { _paq?: Paq })._paq;
|
||||
if (paq && !dnt) {
|
||||
paq.push([function (this: Tracker) { tracker = this; render(); }]);
|
||||
// If it has not arrived in a few seconds, it is blocked or not part of this build.
|
||||
window.setTimeout(() => { if (!tracker) render(); }, 4000);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@ interface Props { lang: Lang }
|
||||
const { lang } = Astro.props;
|
||||
const ui = t(lang);
|
||||
---
|
||||
<section class="night method" id="capabilities" aria-labelledby="method-title">
|
||||
<section class="night method" aria-labelledby="method-title">
|
||||
<Picture
|
||||
src={monitoringRoom}
|
||||
alt={ui.method.imageAlt}
|
||||
@@ -21,7 +21,8 @@ const ui = t(lang);
|
||||
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">
|
||||
{ui.method.lines.map((line, i) => <span class:list={['line', { 'line--muted': i > 0 }]}>{line}</span>)}
|
||||
</h2>
|
||||
|
||||
@@ -16,7 +16,7 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
|
||||
---
|
||||
<section class="section--tight quote" aria-roledescription="carousel" aria-label={ui.quote.label} data-quotes>
|
||||
<div class="container">
|
||||
<div class="quote__frame">
|
||||
<div class="quote__frame" data-frame>
|
||||
<div class="quote__panel">
|
||||
<div class="quote__stack" aria-live="off" data-live>
|
||||
{items.map((item, i) => (
|
||||
@@ -85,6 +85,8 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
|
||||
display: grid;
|
||||
background: var(--paper-deep);
|
||||
}
|
||||
/* The whole card advances on click once the script has set it up. */
|
||||
.quote[data-ready] .quote__frame { cursor: pointer; }
|
||||
.quote__panel {
|
||||
display: grid;
|
||||
gap: var(--space-7);
|
||||
@@ -232,6 +234,7 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
|
||||
const picks = Array.from(root.querySelectorAll<HTMLButtonElement>('[data-pick]'));
|
||||
const live = root.querySelector<HTMLElement>('[data-live]')!;
|
||||
const controls = root.querySelector<HTMLElement>('[data-controls]')!;
|
||||
const frame = root.querySelector<HTMLElement>('[data-frame]')!;
|
||||
const fills = picks.map((p) => p.querySelector<HTMLElement>('.quote__fill')!);
|
||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
let index = 0;
|
||||
@@ -258,6 +261,16 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
|
||||
fills.forEach((fill) => fill.addEventListener('animationend', () => { if (playing) show(index + 1); }));
|
||||
picks.forEach((pick, i) => pick.addEventListener('click', () => { setPlaying(false); show(i); }));
|
||||
|
||||
// A click anywhere else on the card moves on to the next statement and (re)starts the rotation,
|
||||
// so the new statement gets its full hold. Links and the picks keep their own behaviour, and
|
||||
// selecting the quote to copy it does not count.
|
||||
frame.addEventListener('click', (e) => {
|
||||
if ((e.target as Element).closest('a, button')) return;
|
||||
if (window.getSelection()?.toString()) return;
|
||||
show(index + 1);
|
||||
setPlaying(!reduced);
|
||||
});
|
||||
|
||||
const hold = (on: boolean) => root.toggleAttribute('data-held', on);
|
||||
root.addEventListener('focusin', () => hold(true));
|
||||
root.addEventListener('focusout', (e) => { if (!root.contains(e.relatedTarget as Node)) hold(false); });
|
||||
@@ -266,6 +279,7 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
|
||||
new IntersectionObserver(([entry]) => root.toggleAttribute('data-away', !entry.isIntersecting), { threshold: 0.4 }).observe(root);
|
||||
|
||||
controls.hidden = false;
|
||||
root.setAttribute('data-ready', '');
|
||||
setPlaying(!reduced);
|
||||
}
|
||||
</script>
|
||||
@@ -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';
|
||||
interface Props { lang: Lang }
|
||||
const { lang } = Astro.props;
|
||||
|
||||
@@ -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
|
||||
<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__base">
|
||||
{base.map((l) => l.startsWith('circle:')
|
||||
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
|
||||
: <polyline points={l} />)}
|
||||
{base.map((l) => <polyline points={l} />)}
|
||||
</g>
|
||||
<g class="grid-object__accent">
|
||||
{accent.map((l) => l.startsWith('circle:')
|
||||
? <circle cx={S / 2} cy={S / 2} r={Number(l.slice(7))} />
|
||||
: <polyline points={l} />)}
|
||||
{accent.map((l) => <polyline points={l} />)}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
/**
|
||||
* The seal on the sign-up result pages: a target drawn as a technical line drawing. On load the
|
||||
* corner brackets close in and the result is drawn in the centre: a check when the link verified,
|
||||
* a cross when it did not. Then it stays still.
|
||||
* Geometry is computed at build time; the motion is CSS only and off under reduced motion.
|
||||
*/
|
||||
interface Props { state: 'confirmed' | 'error'; label: string }
|
||||
const { state, label } = Astro.props;
|
||||
|
||||
const C = 200, R = 184, N = 60;
|
||||
const f = (n: number) => Math.round(n * 10) / 10;
|
||||
const at = (r: number, a: number): [number, number] => [C + r * Math.sin(a), C - r * Math.cos(a)];
|
||||
|
||||
/* Ticks clockwise from twelve o'clock; every fifth is long, so the ring reads as a scale. */
|
||||
const ticks = Array.from({ length: N }, (_, i) => {
|
||||
const a = (i / N) * Math.PI * 2;
|
||||
const [x1, y1] = at(R, a);
|
||||
const [x2, y2] = at(R - (i % 5 === 0 ? 20 : 10), a);
|
||||
return { x1: f(x1), y1: f(y1), x2: f(x2), y2: f(y2) };
|
||||
});
|
||||
|
||||
/* Corner brackets, each an L drawn about the centre and moved in by CSS. */
|
||||
const B = 112, arm = 30;
|
||||
const brackets = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([sx, sy]) =>
|
||||
`M${C + sx * B},${C + sy * (B - arm)} L${C + sx * B},${C + sy * B} L${C + sx * (B - arm)},${C + sy * B}`);
|
||||
---
|
||||
<svg class={`seal seal--${state}`} viewBox="0 0 400 400" role="img" aria-label={label} xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="seal__ticks">
|
||||
{ticks.map((t) => <line x1={t.x1} y1={t.y1} x2={t.x2} y2={t.y2} />)}
|
||||
</g>
|
||||
<g class="seal__rings">
|
||||
<circle cx={C} cy={C} r="150" />
|
||||
<circle class="seal__dashed" cx={C} cy={C} r="122" />
|
||||
<line x1={C} y1="58" x2={C} y2="132" />
|
||||
<line x1={C} y1="268" x2={C} y2="342" />
|
||||
<line x1="58" y1={C} x2="132" y2={C} />
|
||||
<line x1="268" y1={C} x2="342" y2={C} />
|
||||
</g>
|
||||
<g class="seal__brackets">
|
||||
{brackets.map((d) => <path d={d} />)}
|
||||
</g>
|
||||
<circle class="seal__core" cx={C} cy={C} r="52" pathLength="1" />
|
||||
{state === 'confirmed'
|
||||
? <path class="seal__mark" d="M176,201 L194,219 L226,184" pathLength="1" />
|
||||
: <path class="seal__mark" d="M182,182 L218,218 M218,182 L182,218" pathLength="1" />}
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.seal {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
fill: none;
|
||||
stroke: var(--ink);
|
||||
/* Drawn small (a few rem), so the strokes are set heavy in the 400-unit box to land near 1px. */
|
||||
stroke-width: 3;
|
||||
stroke-linecap: square;
|
||||
overflow: visible;
|
||||
/* The sequence: lock, then the mark. */
|
||||
--t-lock: 200ms;
|
||||
--t-mark: 700ms;
|
||||
}
|
||||
.seal__ticks { stroke: var(--ink-muted); }
|
||||
.seal__rings { stroke: var(--ink-muted); stroke-opacity: 0.45; }
|
||||
.seal__dashed { stroke-dasharray: 5 11; }
|
||||
|
||||
.seal__brackets path {
|
||||
stroke-width: 6;
|
||||
transform-origin: 200px 200px;
|
||||
animation: seal-lock 520ms var(--ease) both var(--t-lock);
|
||||
}
|
||||
|
||||
.seal__core { stroke-width: 6; }
|
||||
.seal__core, .seal__mark {
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
animation: seal-draw 600ms var(--ease) forwards var(--t-mark);
|
||||
}
|
||||
.seal__mark { stroke-width: 11; stroke-linecap: butt; animation-delay: calc(var(--t-mark) + 200ms); }
|
||||
.seal--error .seal__core { stroke: var(--ink-muted); stroke-dasharray: 0.02 0.03; }
|
||||
.seal--error .seal__brackets path { animation-name: seal-miss; }
|
||||
|
||||
@keyframes seal-fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes seal-lock {
|
||||
from { opacity: 0; transform: scale(1.5); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
/* A failed lock: the brackets come in but stop short. */
|
||||
@keyframes seal-miss {
|
||||
from { opacity: 0; transform: scale(1.5); }
|
||||
to { opacity: 0.5; transform: scale(1.2); }
|
||||
}
|
||||
@keyframes seal-draw { to { stroke-dashoffset: 0; } }
|
||||
.seal--error .seal__core { animation: seal-fade 600ms var(--ease) both var(--t-mark); stroke-dashoffset: 0; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.seal * { animation: none !important; }
|
||||
.seal__core, .seal__mark { stroke-dashoffset: 0; }
|
||||
.seal--error .seal__brackets path { opacity: 0.5; transform: scale(1.2); }
|
||||
.seal--error .seal__core { stroke-dasharray: 0.02 0.03; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Matomo, self-hosted. The tracker runs without cookies and honours Do Not Track; the privacy
|
||||
page states both, and its opt-out (src/components/OptOut.astro) talks to the same tracker.
|
||||
Only production builds load it, so local development is never counted. */
|
||||
export const matomo = {
|
||||
url: 'https://matomo.ciphercell.de/',
|
||||
/* The site's ID in Matomo: Administration, Websites, Manage. */
|
||||
siteId: '1',
|
||||
enabled: import.meta.env.PROD,
|
||||
};
|
||||
+57
-39
@@ -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('/');
|
||||
@@ -49,7 +47,7 @@ const en = {
|
||||
homeLabel: 'CipherCell, home',
|
||||
},
|
||||
hero: {
|
||||
lines: ['Security for the software', 'everything else runs on.'],
|
||||
lines: ['Security for the software', 'everything runs on.'],
|
||||
},
|
||||
manifesto: {
|
||||
lines: ['Security built in,', 'not bolted on.'],
|
||||
@@ -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: {
|
||||
@@ -276,11 +265,23 @@ const en = {
|
||||
title: 'Registered.',
|
||||
body: 'Your address is confirmed. You will hear from us once, when the CipherVerifier alpha is released.',
|
||||
link: 'Back to CipherVerifier',
|
||||
seal: 'A target, locked, with a check at its centre.',
|
||||
rows: [
|
||||
{ key: 'Status', value: 'Confirmed' },
|
||||
{ key: 'Signature', value: 'Valid' },
|
||||
{ key: 'List', value: 'CipherVerifier alpha' },
|
||||
{ key: 'Next email', value: 'At release' },
|
||||
],
|
||||
},
|
||||
error: {
|
||||
title: 'Not registered.',
|
||||
body: 'The link has expired or is incomplete, or the registration did not go through. Register again, or write to office@ciphercell.de.',
|
||||
link: 'Register again',
|
||||
seal: 'A target that did not lock, with a cross at its centre.',
|
||||
rows: [
|
||||
{ key: 'Status', value: 'Not confirmed' },
|
||||
{ key: 'Address', value: 'Not stored' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -359,11 +360,18 @@ const en = {
|
||||
link: 'Go to the start page',
|
||||
},
|
||||
legal: {
|
||||
imprintEyebrow: 'Information pursuant to § 5 TMG',
|
||||
responsible: 'Responsible',
|
||||
mailing: 'Mailing address',
|
||||
contact: 'Contact',
|
||||
asOf: 'As of 14 June 2025',
|
||||
imprintEyebrow: 'Information pursuant to § 5 DDG',
|
||||
asOf: 'As of 12 October 2026',
|
||||
optOut: {
|
||||
checking: 'Checking whether your visits are counted.',
|
||||
in: 'Your visits are counted, anonymously and without cookies.',
|
||||
out: 'Your visits are not counted. To remember that, this browser keeps one cookie, mtm_consent_removed.',
|
||||
dnt: 'Your browser sends Do Not Track. Your visits are not counted.',
|
||||
missing: 'The statistics script is not loaded in this browser, for example because of a content blocker. Your visits are not counted.',
|
||||
noscript: 'Without JavaScript the statistics script does not run. Your visits are not counted.',
|
||||
stop: 'Stop counting my visits',
|
||||
resume: 'Count my visits again',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -389,7 +397,7 @@ const de: typeof en = {
|
||||
homeLabel: 'CipherCell, Startseite',
|
||||
},
|
||||
hero: {
|
||||
lines: ['Sicherheit für die Software,', 'auf der alles andere läuft.'],
|
||||
lines: ['Sicherheit für die Software,', 'auf der alles läuft.'],
|
||||
},
|
||||
manifesto: {
|
||||
lines: ['Sicherheit eingebaut,', 'nicht nachträglich angefügt.'],
|
||||
@@ -404,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: {
|
||||
@@ -615,11 +614,23 @@ const de: typeof en = {
|
||||
title: 'Registriert.',
|
||||
body: 'Ihre Adresse ist bestätigt. Sie hören einmal von uns, sobald die Alpha von CipherVerifier erscheint.',
|
||||
link: 'Zurück zu CipherVerifier',
|
||||
seal: 'Ein Zielkreuz, eingerastet, mit einem Haken in der Mitte.',
|
||||
rows: [
|
||||
{ key: 'Status', value: 'Bestätigt' },
|
||||
{ key: 'Signatur', value: 'Gültig' },
|
||||
{ key: 'Liste', value: 'CipherVerifier-Alpha' },
|
||||
{ key: 'Nächste E-Mail', value: 'Zur Veröffentlichung' },
|
||||
],
|
||||
},
|
||||
error: {
|
||||
title: 'Nicht registriert.',
|
||||
body: 'Der Link ist abgelaufen oder unvollständig, oder die Registrierung ist nicht angekommen. Registrieren Sie sich erneut oder schreiben Sie an office@ciphercell.de.',
|
||||
link: 'Erneut registrieren',
|
||||
seal: 'Ein Zielkreuz, nicht eingerastet, mit einem Kreuz in der Mitte.',
|
||||
rows: [
|
||||
{ key: 'Status', value: 'Nicht bestätigt' },
|
||||
{ key: 'Adresse', value: 'Nicht gespeichert' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -698,11 +709,18 @@ const de: typeof en = {
|
||||
link: 'Zur Startseite',
|
||||
},
|
||||
legal: {
|
||||
imprintEyebrow: 'Angaben gemäß § 5 TMG',
|
||||
responsible: 'Verantwortlich',
|
||||
mailing: 'Postanschrift',
|
||||
contact: 'Kontakt',
|
||||
asOf: 'Stand: 14. Juni 2025',
|
||||
imprintEyebrow: 'Angaben gemäß § 5 DDG',
|
||||
asOf: 'Stand: 12. Oktober 2026',
|
||||
optOut: {
|
||||
checking: 'Es wird geprüft, ob Ihre Besuche gezählt werden.',
|
||||
in: 'Ihre Besuche werden gezählt, anonymisiert und ohne Cookies.',
|
||||
out: 'Ihre Besuche werden nicht gezählt. Damit das so bleibt, speichert dieser Browser ein Cookie: mtm_consent_removed.',
|
||||
dnt: 'Ihr Browser sendet „Do Not Track“. Ihre Besuche werden nicht gezählt.',
|
||||
missing: 'Das Statistik-Skript ist in diesem Browser nicht geladen, etwa wegen eines Inhaltsblockers. Ihre Besuche werden nicht gezählt.',
|
||||
noscript: 'Ohne JavaScript läuft das Statistik-Skript nicht. Ihre Besuche werden nicht gezählt.',
|
||||
stop: 'Besuche nicht mehr zählen',
|
||||
resume: 'Besuche wieder zählen',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import '../styles/base.css';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Matomo from '../components/Matomo.astro';
|
||||
import { langFromUrl, switchLang, t, type Lang } from '../i18n/ui';
|
||||
|
||||
interface Props {
|
||||
@@ -34,7 +35,7 @@ const altDe = new URL(switchLang(Astro.url, 'de'), Astro.site);
|
||||
{noindex && <meta name="robots" content="noindex" />}
|
||||
<meta name="color-scheme" content="light" />
|
||||
<!-- Tints the browser chrome (the iOS status bar) to match the surface the page opens on:
|
||||
--night for a dark hero, --paper otherwise. -->
|
||||
--night for a dark hero, --paper otherwise. The header script updates it on scroll. -->
|
||||
<meta name="theme-color" content={tone === 'night' ? '#0E100F' : '#E9E5DF'} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
<link rel="alternate" hreflang="en" href={altEn} />
|
||||
@@ -51,6 +52,7 @@ const altDe = new URL(switchLang(Astro.url, 'de'), Astro.site);
|
||||
<meta property="og:locale" content={lang === 'de' ? 'de_DE' : 'en_GB'} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<script is:inline>document.documentElement.classList.add('js');</script>
|
||||
<Matomo />
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main">{ui.nav.skip}</a>
|
||||
|
||||
@@ -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);
|
||||
<Hero lang={lang} />
|
||||
<Manifesto lang={lang} />
|
||||
<Method lang={lang} />
|
||||
<!-- <Stages lang={lang} /> -->
|
||||
<Sectors lang={lang} />
|
||||
<Partners lang={lang} />
|
||||
<Quote lang={lang} />
|
||||
|
||||
+64
-11
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import Seal from '../components/verifier/Seal.astro';
|
||||
import { localePath, t, type Lang } from '../i18n/ui';
|
||||
interface Props { lang: Lang; page: 'confirmed' | 'error' }
|
||||
const { lang, page } = Astro.props;
|
||||
@@ -8,22 +9,74 @@ const r = p[page];
|
||||
const product = localePath(lang, '/cipherverifier');
|
||||
---
|
||||
<Base lang={lang} title={r.title} description={r.body} noindex>
|
||||
<section class="section">
|
||||
<div class="container grid">
|
||||
<h1 class="display result__title">{r.title}</h1>
|
||||
<div class="result__body">
|
||||
<p class="muted">{r.body}</p>
|
||||
<a class="link" href={page === 'error' ? `${product}#notify` : product}>{r.link}</a>
|
||||
<section class="section result" aria-labelledby="result-title">
|
||||
<div class="container result__inner">
|
||||
<div class="result__head">
|
||||
<h1 id="result-title" class="display result__title">{r.title}</h1>
|
||||
<div class="result__seal">
|
||||
<Seal state={page} label={r.seal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result__text">
|
||||
<p class="result__body">{r.body}</p>
|
||||
<a class="button" href={page === 'error' ? `${product}#notify` : product}>{r.link}</a>
|
||||
</div>
|
||||
|
||||
<dl class="result__readout hud-frame" aria-label={r.title}>
|
||||
{r.rows.map((row) => (
|
||||
<div class="result__cell">
|
||||
<dt>{row.key}</dt>
|
||||
<dd>{row.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.result__title { grid-column: 1 / -1; }
|
||||
.result__body { grid-column: 1 / -1; display: grid; gap: var(--space-5); max-width: var(--measure); justify-items: start; }
|
||||
@media (min-width: 48rem) {
|
||||
.result__title { grid-column: 1 / span 8; }
|
||||
.result__body { grid-column: 1 / span 5; }
|
||||
.result__inner { display: grid; gap: var(--space-7); }
|
||||
/* The seal sits beside the headline as a mark, about the height of its cap line. */
|
||||
/* On narrow screens it stands above the headline; from 64rem it follows it on the same line. */
|
||||
.result__head { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-5); }
|
||||
.result__seal { order: -1; width: 4.5rem; flex: none; }
|
||||
.result__text { display: grid; gap: var(--space-5); justify-items: start; }
|
||||
.result__body { max-width: var(--measure); color: var(--ink-muted); }
|
||||
|
||||
.result__readout {
|
||||
display: grid;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--step-micro);
|
||||
letter-spacing: var(--track-caps);
|
||||
text-transform: uppercase;
|
||||
line-height: var(--leading-small);
|
||||
}
|
||||
.result__cell {
|
||||
display: grid;
|
||||
grid-template-columns: 9rem 1fr;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.result__cell + .result__cell { border-top: var(--hairline) solid var(--line-base); }
|
||||
.result__readout dt { color: var(--ink-muted); }
|
||||
|
||||
/* The text arrives as the seal locks; the readout last. */
|
||||
.result__text, .result__readout { animation: result-in var(--dur-rise) var(--ease) both; }
|
||||
.result__text { animation-delay: 500ms; }
|
||||
.result__readout { animation-delay: 1000ms; }
|
||||
@keyframes result-in { from { opacity: 0; } to { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.result__text, .result__readout { animation: none; }
|
||||
}
|
||||
|
||||
@media (min-width: 64rem) {
|
||||
.result__inner { grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gap); row-gap: var(--space-7); }
|
||||
.result__head { grid-column: 1 / -1; flex-direction: row; align-items: center; gap: var(--space-6); }
|
||||
.result__seal { order: 0; width: 7.5rem; }
|
||||
.result__text { grid-column: 1 / span 6; }
|
||||
.result__readout { grid-column: 1 / -1; grid-auto-flow: column; grid-auto-columns: 1fr; }
|
||||
.result__cell { grid-template-columns: 1fr; gap: var(--space-1); padding: var(--space-4) var(--space-5); }
|
||||
.result__cell + .result__cell { border-top: 0; border-left: var(--hairline) solid var(--line-base); }
|
||||
}
|
||||
</style>
|
||||
@@ -4,16 +4,29 @@ import { t } from '../../i18n/ui';
|
||||
const ui = t('de');
|
||||
---
|
||||
<Legal lang="de" title={ui.meta.imprint.title} description={ui.meta.imprint.description} eyebrow={ui.legal.imprintEyebrow}>
|
||||
<h2>Verantwortlich</h2>
|
||||
<h2>Diensteanbieter</h2>
|
||||
<p>Dr. Felix Klement<br />CipherCell<br />Innstr. 43<br />94032 Passau<br />Deutschland</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>Telefon: +49 851 509 3218<br />E-Mail: felix.klement@ciphercell.de<br />Website: https://ciphercell.de</p>
|
||||
|
||||
<h2>Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV</h2>
|
||||
<p>Dr. Felix Klement<br />Innstr. 43<br />94032 Passau<br />Deutschland</p>
|
||||
<p>Telefon: <a href="tel:+498515093218">+49 851 509 3218</a><br />E-Mail: <a href="mailto:felix.klement@ciphercell.de">felix.klement@ciphercell.de</a></p>
|
||||
|
||||
<h2>Inhalt der Website</h2>
|
||||
<p>CipherCell (der Webseitenbetreiber) prüft und aktualisiert die Informationen auf seinen Webseiten regelmäßig. Trotz aller Sorgfalt übernimmt der Webseitenbetreiber keine Haftung oder Garantie für die Aktualität, Richtigkeit, Vollständigkeit oder Qualität der online verfügbaren Informationen. Alle Angebote sind freibleibend und unverbindlich. Der Webseitenbetreiber behält es sich ausdrücklich vor, einzelne Seiten oder das gesamte Onlineangebot ohne vorhergehende Ankündigung zu verändern, zu ergänzen, zu löschen oder das Angebot auf bestimmte Zeit oder auch endgültig einzustellen.</p>
|
||||
<h2>Haftung für Inhalte</h2>
|
||||
<p>Als Diensteanbieter sind wir gemäß § 7 Abs. 1 DDG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 DDG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.</p>
|
||||
|
||||
<h2>Verweise und Links</h2>
|
||||
<p>CipherCell haftet nicht für den Inhalt von fremden Webseiten, auf die im Onlineangebot von CipherCell z. B. mittels Hyperlinks verwiesen wird. Dies gilt auch für alle Einträge Dritter in vom Webseitenbetreiber eingerichteten Gästebüchern, Diskussionsforen, Pinnwänden und Mailinglisten.</p>
|
||||
<p>Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.</p>
|
||||
|
||||
<h2>Urheber- und Kennzeichenrecht</h2>
|
||||
<p>Inhalt und Struktur der Webseite von CipherCell sind urheberrechtlich geschützt. Die Vervielfältigung von Grafiken, Tondokumenten, Bildmaterialien, Film- und Videoausschnitten und Texten in anderen elektronischen oder gedruckten Medien und Publikationen ist ohne die ausdrückliche Erlaubnis von CipherCell nicht gestattet.</p>
|
||||
<h2>Haftung für Links</h2>
|
||||
<p>Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.</p>
|
||||
|
||||
<p>Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.</p>
|
||||
|
||||
<h2>Urheberrecht</h2>
|
||||
<p>Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.</p>
|
||||
|
||||
<p>Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.</p>
|
||||
</Legal>
|
||||
|
||||
|
||||
+105
-17
@@ -1,31 +1,119 @@
|
||||
---
|
||||
import Legal from '../_Legal.astro';
|
||||
import OptOut from '../../components/OptOut.astro';
|
||||
import { t } from '../../i18n/ui';
|
||||
const ui = t('de');
|
||||
---
|
||||
<Legal lang="de" title={ui.meta.privacy.title} description={ui.meta.privacy.description} eyebrow={ui.legal.asOf} lead="CipherCell nimmt die Belange des Datenschutzes ernst und erhebt, verarbeitet und nutzt Ihre personenbezogenen Daten ausschließlich in Übereinstimmung mit den nachfolgend beschriebenen Grundsätzen und unter Beachtung der anwendbaren Datenschutzgesetze.">
|
||||
<h2>Verantwortliche Stelle</h2>
|
||||
<p>Die verantwortliche Stelle für den Betrieb der CipherCell-Website (im Folgenden „Website“) sowie die Erhebung, Verarbeitung und Nutzung Ihrer personenbezogenen Daten im Sinne des anwendbaren Datenschutzrechts ist Dr. Felix Klement, Innstr. 43, 94032 Passau, Deutschland.</p>
|
||||
<h2>Datenschutzerklärung</h2>
|
||||
|
||||
<h2>Webanalyse mit Matomo (ohne Cookies)</h2>
|
||||
<p>Wir verwenden auf unserer Website die Open-Source-Webanalysesoftware Matomo, um das Nutzungsverhalten auf datenschutzfreundliche Weise auszuwerten und unsere Website technisch und inhaltlich zu verbessern. Dabei haben wir Matomo so konfiguriert, dass keine Cookies gesetzt werden und Ihre IP-Adresse anonymisiert gespeichert wird. Es werden keine personenbezogenen Daten verarbeitet, und es erfolgt keine Weitergabe an Dritte. Die Verarbeitung erfolgt auf Grundlage unseres berechtigten Interesses gemäß Art. 6 Abs. 1 lit. f DSGVO an der statistischen Analyse des Nutzerverhaltens zur Optimierung unseres Webangebots. Wenn Ihr Browser die „Do Not Track“-Funktion unterstützt und aktiviert ist, wird Ihr Besuch von Matomo automatisch ignoriert. Sie können der anonymisierten Analyse Ihres Besuchs dennoch jederzeit widersprechen. Nutzen Sie dazu bitte den folgenden Opt-out.</p>
|
||||
<p><a href="https://cipher.uber.space/matomo/index.php?module=CoreAdminHome&action=optOut&language=de" rel="noopener">Matomo Opt-out</a></p>
|
||||
<p>Diese Datenschutzerklärung erläutert, wie CipherCell personenbezogene Daten verarbeitet, wenn Sie unsere Website besuchen, mit uns Kontakt aufnehmen, sich für die Benachrichtigung zur Alpha-Version von CipherVerifier anmelden oder eingebettete LinkedIn-Beiträge laden.</p>
|
||||
|
||||
<h2>Datensicherheit</h2>
|
||||
<p>Ihre Nutzerdaten sind bei der Übermittlung auf unsere Server durch den Einsatz von TLS in Verbindung mit einer 256-Bit-Verschlüsselung geschützt. Sie können die Sicherheit der Verbindung anhand der Angaben in der Adresszeile Ihres Browsers prüfen. Beginnt die Adresszeile mit „https“, liegt eine gesicherte Verbindung vor.</p>
|
||||
<h2>1. Verantwortlicher und Kontakt</h2>
|
||||
<p>Verantwortlicher für die Datenverarbeitung auf dieser Website ist:</p>
|
||||
<p>Dr. Felix Klement<br />CipherCell<br />Innstr. 43<br />94032 Passau<br />Deutschland</p>
|
||||
|
||||
<h2>Kontaktaufnahme</h2>
|
||||
<p>Wenn Sie uns per E-Mail oder auf anderem Wege kontaktieren, speichern wir Ihre übermittelten personenbezogenen Daten (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht), um Ihre Anfrage zu bearbeiten. Diese Daten werden ausschließlich für die Kommunikation mit Ihnen verwendet und nicht an Dritte weitergegeben. Rechtsgrundlage für die Verarbeitung der Daten ist Art. 6 Abs. 1 lit. b DSGVO (Anbahnung eines Vertragsverhältnisses) oder Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Bearbeitung der Anfrage).</p>
|
||||
<p>E-Mail: office@ciphercell.de</p>
|
||||
|
||||
<h2>Benachrichtigung zur CipherVerifier-Alpha</h2>
|
||||
<p>Wenn Sie sich für die Benachrichtigung zur Alpha von CipherVerifier eintragen, speichern wir Ihre E-Mail-Adresse und den Zeitpunkt Ihrer Einwilligung, um Ihnen mitzuteilen, wann die erste Alpha veröffentlicht ist. Die Registrierung erfolgt im Double-Opt-in-Verfahren: Wir senden Ihnen zunächst eine E-Mail mit einem Bestätigungslink und speichern Ihre Adresse erst, wenn Sie bestätigt haben. Bestätigen Sie nicht innerhalb von 7 Tagen, verfällt der Link und es wird nichts gespeichert. Wir nutzen sie für nichts anderes und geben sie nicht an Dritte weiter. Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a> widerrufen; wir löschen Ihre Adresse dann. Nach dem Versand der Benachrichtigung löschen wir sie ebenfalls.</p>
|
||||
<h2>2. Bereitstellung und Sicherheit der Website</h2>
|
||||
<p>Wenn Sie unsere Website besuchen, werden technische Informationen verarbeitet, um die von Ihnen angeforderten Seiten bereitzustellen und die Verfügbarkeit und Sicherheit der Website zu gewährleisten.</p>
|
||||
|
||||
<h2>Eingebettete LinkedIn-Beiträge</h2>
|
||||
<p>Einige Seiten unter Einblicke können einen Beitrag von LinkedIn (LinkedIn Ireland Unlimited Company, Wilton Place, Dublin 2, Irland) anzeigen. Beim Aufruf der Seite wird der Beitrag nicht geladen. Erst wenn Sie auf die Schaltfläche zum Laden klicken, verbindet sich Ihr Browser mit den Servern von LinkedIn; diese erhalten Ihre IP-Adresse und die aufgerufene Seite und können Cookies setzen. Sind Sie bei LinkedIn angemeldet, kann LinkedIn den Besuch Ihrem Konto zuordnen. Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO, die Sie mit diesem Klick erteilen. Zur Verarbeitung durch LinkedIn siehe dessen Datenschutzrichtlinie unter <a href="https://de.linkedin.com/legal/privacy-policy" rel="noopener">linkedin.com/legal/privacy-policy</a>.</p>
|
||||
<p>Hierzu gehören Ihre IP-Adresse, Datum und Uhrzeit der Anfrage, die angeforderte URL, der Antwortstatus, Browserinformationen und die zuvor besuchte Seite, von der aus Sie auf unsere Website gelangt sind.</p>
|
||||
|
||||
<h2>Recht auf Auskunft, Berichtigung, Sperrung oder Löschung</h2>
|
||||
<p>Nach dem anwendbaren Datenschutzrecht haben Sie ein Recht auf unentgeltliche Auskunft über Ihre gespeicherten Daten sowie ggf. ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Wenden Sie sich bei weitergehenden Fragen zum Datenschutz und zur Verarbeitung Ihrer personenbezogenen Daten, bei Auskünften oder zur Ausübung Ihres Rechts auf Berichtigung, Sperrung oder Löschung oder für den Fall des Widerrufs einer Einwilligung bitte per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. Selbstverständlich können Sie auch die im Impressum angegebenen Kontaktdaten oder das Kontaktformular auf der Website nutzen. CipherCell verarbeitet und nutzt die von Ihnen mitgeteilten Informationen ausschließlich für die Bearbeitung Ihres Anliegens.</p>
|
||||
<p>Das Hosting erfolgt durch Dr. Felix Klement, Anschrift wie oben angegeben, Deutschland.</p>
|
||||
|
||||
<h2>Anpassungen</h2>
|
||||
<p>CipherCell behält sich vor, diese Datenschutzbestimmungen von Zeit zu Zeit anzupassen. Wir werden Sie von jeder Änderung oder Ergänzung vorab rechtzeitig unterrichten und, soweit erforderlich, Ihre Einwilligung in die geänderte Verarbeitung oder Nutzung Ihrer personenbezogenen Daten einholen.</p>
|
||||
<p>Wir verarbeiten diese Daten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Unsere berechtigten Interessen bestehen darin, eine funktionsfähige Website zu betreiben, Störungen zu erkennen sowie Missbrauch und Angriffe zu verhindern.</p>
|
||||
|
||||
<p>Serverprotokolle werden nach sieben Tagen gelöscht. Soweit einzelne Protokolleinträge zur Untersuchung eines Sicherheitsvorfalls oder zur Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen erforderlich sind, bewahren wir diese so lange auf, wie dies für den jeweiligen Zweck notwendig ist.</p>
|
||||
|
||||
<p>Die Verarbeitung der technischen Informationen, die zur Bereitstellung einer angeforderten Seite erforderlich sind, ist für den Betrieb der Website notwendig.</p>
|
||||
|
||||
<h2>3. Kontaktanfragen</h2>
|
||||
<p>Wenn Sie uns per E-Mail oder telefonisch kontaktieren, verarbeiten wir die von Ihnen übermittelten Kontaktdaten, Nachrichteninhalte und Anhänge, um Ihre Anfrage zu beantworten.</p>
|
||||
|
||||
<p>Soweit die Verarbeitung zur Durchführung vorvertraglicher Maßnahmen auf Ihre Anfrage hin oder zur Erfüllung eines Vertrags mit Ihnen erforderlich ist, erfolgt sie auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO. Bei sonstigen Anfragen ist Art. 6 Abs. 1 lit. f DSGVO die Rechtsgrundlage. Unser berechtigtes Interesse besteht darin, an uns gerichtete Anfragen zu beantworten.</p>
|
||||
|
||||
<p>Unser E-Mail-Dienst wird von uberspace, Jonas Pasche, Deutschland, bereitgestellt. Weitere Informationen finden Sie unter <a href="https://uberspace.de" rel="noopener">https://uberspace.de</a>.</p>
|
||||
|
||||
<p>Allgemeine Anfragen löschen wir nach 30 Tagen. Soweit für die Korrespondenz gesetzliche Aufbewahrungspflichten bestehen, bewahren wir die erforderlichen Unterlagen für die gesetzlich vorgeschriebene Dauer auf. Rechtsgrundlage hierfür ist Art. 6 Abs. 1 lit. c DSGVO.</p>
|
||||
|
||||
<p>Die Bereitstellung Ihrer Angaben ist freiwillig. Um Ihre Anfrage beantworten zu können, benötigen wir jedoch ausreichende Informationen und eine Möglichkeit, Sie zu kontaktieren.</p>
|
||||
|
||||
<h2>4. Websiteanalyse mit Matomo</h2>
|
||||
<p>Wir verwenden Matomo, um die Nutzung unserer Website besser zu verstehen und ihre Inhalte sowie ihre technische Leistungsfähigkeit zu verbessern.</p>
|
||||
|
||||
<p>Unsere Matomo-Installation ist so konfiguriert, dass keine Analyse-Cookies eingesetzt und IP-Adressen gekürzt werden.</p>
|
||||
|
||||
<p>Matomo wird von Dr. Felix Klement, Anschrift wie oben angegeben, Deutschland, gehostet. Ausschließlich CipherCell erhält die Analysedaten.</p>
|
||||
|
||||
<p>Rechtsgrundlage ist Ihre Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO. Soweit im Rahmen der Analyse Informationen auf Ihrem Endgerät gespeichert werden oder auf dort gespeicherte Informationen zugegriffen wird, holen wir hierfür zusätzlich Ihre Einwilligung gemäß § 25 Abs. 1 TDDDG ein.</p>
|
||||
|
||||
<p>Wenn Ihr Browser die Funktion „Do Not Track“ unterstützt und Sie diese aktiviert haben, wird Ihr Besuch von Matomo automatisch nicht erfasst. Sie können der anonymisierten Auswertung Ihres Besuchs auch jederzeit widersprechen. Nutzen Sie hierfür bitte die nachfolgende Opt-out-Möglichkeit.</p>
|
||||
|
||||
<OptOut lang="de" />
|
||||
|
||||
<h2>5. Benachrichtigung zur Alpha-Version von CipherVerifier</h2>
|
||||
<p>Sie können sich anmelden, um einmalig per E-Mail über die Veröffentlichung der ersten Alpha-Version von CipherVerifier informiert zu werden. Wir verwenden diese Anmeldung nicht für einen allgemeinen Newsletter oder für Werbung zu anderen Themen.</p>
|
||||
|
||||
<p>Wir verarbeiten Ihre E-Mail-Adresse, um Ihnen eine Bestätigungs-E-Mail zu senden. Erst nachdem Sie Ihre Anmeldung über den darin enthaltenen Link bestätigt haben, nehmen wir Ihre Adresse in die aktive Benachrichtigungsliste auf.</p>
|
||||
|
||||
<p>Der Bestätigungslink ist sieben Tage gültig. Wenn Sie Ihre Anmeldung nicht innerhalb dieser Frist bestätigen, werden die bei der Anmeldung erfassten Daten </p>unverzüglich gelöscht.
|
||||
|
||||
<p>Bei bestätigten Anmeldungen speichern wir Ihre E-Mail-Adresse und die weiteren Daten, die Sie bei der Anmeldung angegeben haben.</p>
|
||||
|
||||
<p>Rechtsgrundlage für die Anmeldung und den Versand der Benachrichtigung ist Ihre Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO.</p>
|
||||
|
||||
<p>Unser Dienstleister für den E-Mail-Versand ist uberspace, Jonas Pasche, Deutschland. Weitere Informationen finden Sie unter <a href="https://uberspace.de" rel="noopener">https://uberspace.de</a>.</p>
|
||||
|
||||
<p>Sie können Ihre Einwilligung jederzeit widerrufen, indem Sie den Abmeldelink in einer unserer E-Mails zur Benachrichtigung über die Alpha-Version von CipherVerifier anklicken oder eine E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a> senden. Wir entfernen Ihre Adresse anschließend aus der aktiven Benachrichtigungsliste.</p>
|
||||
|
||||
<p>Wir entfernen Ihre Adresse außerdem aus dieser Liste, sobald wir die Benachrichtigung versandt haben oder die Veröffentlichung abgesagt wird – je nachdem, welches </p>Ereignis zuerst eintritt.
|
||||
|
||||
<p>Die Anmeldung ist freiwillig. Ohne Ihre E-Mail-Adresse und die Bestätigung Ihrer Anmeldung können wir Ihnen keine Benachrichtigung senden.</p>
|
||||
|
||||
<h2>6. Eingebettete LinkedIn-Beiträge</h2>
|
||||
<p>Auf einigen Seiten bieten wir Beiträge an, die von LinkedIn Ireland Unlimited Company, Irland, gehostet werden. Diese Beiträge werden zunächst als Platzhalter angezeigt. LinkedIn-Inhalte werden erst geladen, wenn Sie <i>„Beitrag von LinkedIn laden“</i> auswählen.</p>
|
||||
|
||||
<p>Vor dem Laden eines Beitrags weisen wir Sie darauf hin, dass dadurch eine Verbindung zwischen Ihrem Browser und LinkedIn hergestellt wird und LinkedIn möglicherweise Cookies setzt. Der Platzhalter enthält außerdem einen Link zu dieser Datenschutzerklärung.</p>
|
||||
|
||||
<p>Mit der Auswahl von „Beitrag von LinkedIn laden“ willigen Sie in das Laden des ausgewählten Beitrags und die damit verbundene Übermittlung personenbezogener Daten an LinkedIn ein. Ihr Browser übermittelt LinkedIn technische Informationen, darunter Ihre IP-Adresse und Browserinformationen. Dabei kann LinkedIn auch erfahren, welche Seite Sie gerade besuchen. LinkedIn kann Cookies setzen oder auslesen oder ähnliche Technologien einsetzen. Wenn Sie bei LinkedIn angemeldet sind, kann LinkedIn diese Interaktion Ihrem Benutzerkonto zuordnen.</p>
|
||||
|
||||
<p>Unser Zweck ist es, den von Ihnen ausgewählten LinkedIn-Beitrag anzuzeigen. Rechtsgrundlage für die Verarbeitung, für die wir verantwortlich sind, ist Ihre Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO. Die Einwilligung umfasst auch die damit verbundene Speicherung von Informationen auf Ihrem Endgerät oder den Zugriff auf dort gespeicherte Informationen gemäß § 25 Abs. 1 TDDDG.</p>
|
||||
|
||||
<p>Sie können unsere Website weiterhin nutzen, ohne den Beitrag zu laden.</p>
|
||||
|
||||
<p>Sie können Ihre Einwilligung jederzeit widerrufen, indem Sie die Seite neu laden. Durch den Widerruf werden das erneute Laden und weitere Datenübermittlungen über die betreffende Einbettung verhindert. Die Rechtmäßigkeit der bis zum Widerruf erfolgten Verarbeitung bleibt unberührt. Bereits an LinkedIn übermittelte Informationen lassen sich dadurch nicht zurückholen.</p>
|
||||
|
||||
<p>LinkedIn kann Daten außerhalb des Europäischen Wirtschaftsraums verarbeiten, unter anderem in den Vereinigten Staaten.</p>
|
||||
|
||||
<p>Einzelheiten zu den Verarbeitungszwecken, der Speicherdauer, internationalen Datenübermittlungen und Ihren Datenschutzrechten bei LinkedIn finden Sie in der <a href="https://de.linkedin.com/legal/privacy-policy" rel="noopener">Datenschutzrichtlinie von LinkedIn</a>.</p>
|
||||
|
||||
<h2>7. Dienstleister und internationale Datenübermittlungen</h2>
|
||||
<p>Die oben genannten Dienstleister erhalten personenbezogene Daten nur, soweit dies für die Erbringung ihrer jeweiligen Leistungen erforderlich ist. Soweit sie Daten in unserem Auftrag verarbeiten, erfolgt dies auf Grundlage von Auftragsverarbeitungsverträgen und nach unseren Weisungen.</p>
|
||||
|
||||
<h2>8. Ihre Rechte</h2>
|
||||
<p>Unter den in der DSGVO geregelten Voraussetzungen haben Sie das Recht auf Auskunft über Ihre personenbezogenen Daten, auf Berichtigung unrichtiger Daten, auf Löschung sowie auf Einschränkung der Verarbeitung.</p>
|
||||
|
||||
<p>Wenn die Verarbeitung automatisiert erfolgt und auf einer Einwilligung oder einem Vertrag beruht, kann Ihnen außerdem das Recht zustehen, die von Ihnen bereitgestellten Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten und diese Daten einem anderen Verantwortlichen zu übermitteln.</p>
|
||||
|
||||
<p><b>Widerspruchsrecht:</b> Soweit wir Daten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO verarbeiten, können Sie aus Gründen, die sich aus Ihrer besonderen Situation ergeben, Widerspruch gegen die Verarbeitung einlegen. Wir verarbeiten die Daten dann nicht mehr, es sei denn, wir können zwingende schutzwürdige Gründe für die Verarbeitung nachweisen, die Ihre Interessen, Rechte und Freiheiten überwiegen, oder die Verarbeitung dient der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen. Der Verarbeitung Ihrer Daten zum Zweck der Direktwerbung können Sie jederzeit widersprechen. Wir werden Ihre Daten dann nicht mehr für diesen Zweck verarbeiten.</p>
|
||||
|
||||
<p><b>Widerruf einer Einwilligung:</b> Sie können eine erteilte Einwilligung jederzeit widerrufen. Die Rechtmäßigkeit der aufgrund Ihrer Einwilligung bis zum Widerruf erfolgten Verarbeitung bleibt hiervon unberührt.</p>
|
||||
|
||||
<p>Um Ihre Rechte auszuüben, kontaktieren Sie uns unter office@ciphercell.de oder schreiben Sie an die oben angegebene Anschrift. Die Bearbeitung Ihrer Anträge ist grundsätzlich unentgeltlich. Wir beantworten Ihre Anträge in der Regel innerhalb eines Monats. Sollte eine gesetzlich zulässige Fristverlängerung erforderlich sein, informieren wir Sie innerhalb dieses Monats darüber und erläutern die Gründe.</p>
|
||||
|
||||
<p>Sie haben außerdem das Recht, Beschwerde bei einer Datenschutzaufsichtsbehörde einzulegen, insbesondere in dem EU-Mitgliedstaat Ihres gewöhnlichen Aufenthaltsorts, Ihres Arbeitsplatzes oder des Orts des mutmaßlichen Verstoßes.</p>
|
||||
|
||||
<p>Für Verantwortliche im nichtöffentlichen Bereich in Bayern ist das Bayerische Landesamt für Datenschutzaufsicht (BayLDA) zuständig. Informationen und Beschwerdemöglichkeiten finden Sie auf der<a href="https://www.lda.bayern.de/de/beschwerde.html" rel="noopener">Website des BayLDA</a>.</p>
|
||||
|
||||
<h2>9. Automatisierte Entscheidungen</h2>
|
||||
<p>Wir treffen derzeit keine ausschließlich auf einer automatisierten Verarbeitung – einschließlich Profiling – beruhenden Entscheidungen über Sie, die Ihnen gegenüber rechtliche Wirkung entfalten oder Sie in ähnlicher Weise erheblich beeinträchtigen, im Sinne von Art. 22 DSGVO.</p>
|
||||
|
||||
<h2>10. Datensicherheit</h2>
|
||||
<p>Wir verwenden TLS-Verschlüsselung für Verbindungen zu unserer Website und setzen technische und organisatorische Maßnahmen ein, die den mit der Verarbeitung personenbezogener Daten verbundenen Risiken angemessen sind.</p>
|
||||
|
||||
<h2>11. Änderungen dieser Datenschutzerklärung</h2>
|
||||
<p>Wir aktualisieren diese Datenschutzerklärung, wenn sich unsere Datenverarbeitung oder die geltenden Anforderungen ändern. Das Datum der letzten Aktualisierung finden Sie am Anfang dieser Erklärung.</p>
|
||||
|
||||
<p>Wenn wir beabsichtigen, personenbezogene Daten für einen neuen Zweck zu verwenden, stellen wir Ihnen vorab die erforderlichen Informationen zur Verfügung und holen, soweit erforderlich, eine neue Einwilligung ein.</p>
|
||||
</Legal>
|
||||
+13
-8
@@ -4,16 +4,21 @@ import { t } from '../i18n/ui';
|
||||
const ui = t('en');
|
||||
---
|
||||
<Legal lang="en" title={ui.meta.imprint.title} description={ui.meta.imprint.description} eyebrow={ui.legal.imprintEyebrow}>
|
||||
<h2>Responsible</h2>
|
||||
<h2>Provider</h2>
|
||||
<p>Dr. Felix Klement<br />CipherCell<br />Innstr. 43<br />94032 Passau<br />Germany</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Phone: +49 851 509 3218<br />E-Mail: felix.klement@ciphercell.de<br />Website: https://ciphercell.de</p>
|
||||
|
||||
<h2>Person Responsible for Editorial Content (pursuant to § 18 (2) MStV)</h2>
|
||||
<p>Dr. Felix Klement<br />Innstr. 43<br />94032 Passau<br />Germany</p>
|
||||
<p>Telephone: <a href="tel:+498515093218">+49 851 509 3218</a><br />Email: <a href="mailto:felix.klement@ciphercell.de">felix.klement@ciphercell.de</a></p>
|
||||
|
||||
<h2>Website content</h2>
|
||||
<p>CipherCell (the website operator) regularly reviews and updates the information on its websites. Despite all due care, the website operator accepts no liability or guarantee for the timeliness, accuracy, completeness, or quality of the information available online. All offers are subject to change and non-binding. The website operator expressly reserves the right to change, supplement, delete or discontinue the offer at any time without prior notice. The website operator is not responsible for the content of external links.</p>
|
||||
<h2>Liability for Content</h2>
|
||||
<p>As a service provider, we are responsible for our own content on these pages under general statutory law in accordance with § 7 (1) DDG. However, pursuant to §§ 8 to 10 DDG, we are not obligated to monitor transmitted or stored third-party information or to investigate circumstances pointing to illegal activity. Obligations to remove or block the use of information under general statutory law remain unaffected.</p>
|
||||
|
||||
<h2>References and links</h2>
|
||||
<p>CipherCell is not liable for the content of external websites referred to in CipherCell's online offering, e.g. via hyperlinks. This also applies to all entries by third parties in guest books, discussion forums, message boards, and mailing lists set up by the website operator.</p>
|
||||
<h2>Liability for Links</h2>
|
||||
<p>Our website may contain links to external third-party websites over whose content we have no control. Therefore, we cannot assume any liability for such external content. The respective provider or operator of the linked pages is always responsible for their content. Should any legal infringements become known to us, we will remove such links immediately.</p>
|
||||
|
||||
<h2>Copyright and trademark law</h2>
|
||||
<p>The content and structure of the CipherCell website are protected by copyright. The reproduction of graphics, audio documents, image material, film and video excerpts, and texts in other electronic or printed media and publications is not permitted without the express permission of CipherCell. The use of trademarks and logos is also subject to the provisions of the applicable trademark law.</p>
|
||||
<h2>Copyright</h2>
|
||||
<p>The content and works published on these pages are governed by German copyright law. Any reproduction, processing, distribution, or any form of commercialization beyond the scope of the copyright law requires the prior written consent of the respective author or creator.</p>
|
||||
</Legal>
|
||||
+105
-17
@@ -1,31 +1,119 @@
|
||||
---
|
||||
import Legal from './_Legal.astro';
|
||||
import OptOut from '../components/OptOut.astro';
|
||||
import { t } from '../i18n/ui';
|
||||
const ui = t('en');
|
||||
---
|
||||
<Legal lang="en" title={ui.meta.privacy.title} description={ui.meta.privacy.description} eyebrow={ui.legal.asOf} lead="CipherCell takes data protection seriously and collects, processes and uses your personal data exclusively in accordance with the principles described below and in compliance with applicable data protection law.">
|
||||
<h2>Responsible body</h2>
|
||||
<p>The responsible body for the operation of the CipherCell website (hereinafter referred to as "website") and the collection, processing, and use of your personal data within the meaning of applicable data protection law is Dr. Felix Klement, Innstr. 43, 94032 Passau, Germany.</p>
|
||||
<h2>Privacy Notice</h2>
|
||||
<p>This notice explains how CipherCell processes personal data when you visit our website, contact us, subscribe to the CipherVerifier alpha notification, or choose to display embedded LinkedIn posts.</p>
|
||||
|
||||
<h2>Web analytics with Matomo (without cookies)</h2>
|
||||
<p>We use the open source web analytics software Matomo on our website to evaluate user behavior in a privacy-friendly manner and to improve our website in terms of technology and content. We have configured Matomo so that no cookies are set and your IP address is stored anonymously. No personal data is processed and no data is passed on to third parties. Processing is based on our legitimate interest pursuant to Art. 6 (1) lit. f GDPR in the statistical analysis of user behavior for the purpose of optimizing our website. If your browser supports and has activated the "Do Not Track" function, your visit will be automatically ignored by Matomo. You can still object to the anonymized analysis of your visit at any time. To do so, please use the opt-out below.</p>
|
||||
<p><a href="https://cipher.uber.space/matomo/index.php?module=CoreAdminHome&action=optOut&language=en" rel="noopener">Matomo opt-out</a></p>
|
||||
<h2>1. Controller and contact</h2>
|
||||
<p>The controller responsible for this website is:</p>
|
||||
|
||||
<h2>Data security</h2>
|
||||
<p>Your user data is protected during transmission to our servers through the use of TLS in conjunction with 256-bit encryption. You can check the security of the connection by looking at the information in your browser's address bar. If the beginning of the address line reads "https", the connection is secure.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>If you contact us by email or other means, we will store the personal data you provide (e.g. name, email address, content of the message) in order to process your request. This data will be used exclusively for communication with you and will not be passed on to third parties. The legal basis for processing the data is Art. 6 para. 1 lit. b GDPR (initiation of a contractual relationship) or Art. 6 para. 1 lit. f GDPR (legitimate interest in processing the request).</p>
|
||||
<p>Dr. Felix Klement<br />CipherCell<br />Innstr. 43<br />94032 Passau<br />Germany</p>
|
||||
|
||||
<h2>CipherVerifier alpha notification</h2>
|
||||
<p>If you register for the CipherVerifier alpha notification, we store your email address and the time of your consent in order to tell you when the first alpha is released. Registration is double opt-in: we first send you an email with a link to confirm it, and only store your address once you have confirmed. If you do not confirm within 7 days, the link expires and nothing is kept. We use it for nothing else and do not pass it on to third parties. The legal basis is your consent under Art. 6 para. 1 lit. a GDPR. You can withdraw your consent at any time by email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>; we then delete your address. We also delete it once the notification has been sent.</p>
|
||||
<p>Email: office@ciphercell.de</p>
|
||||
|
||||
<h2>Embedded LinkedIn posts</h2>
|
||||
<p>Some pages in Insights can show a post from LinkedIn (LinkedIn Ireland Unlimited Company, Wilton Place, Dublin 2, Ireland). The post is not loaded when you open the page. Only when you click the button to load it does your browser connect to LinkedIn's servers, which receive your IP address and the page you are on and may set cookies; if you are logged in to LinkedIn, it can link the visit to your account. The legal basis is your consent under Art. 6 para. 1 lit. a GDPR, given by that click. For LinkedIn's processing, see its privacy policy at <a href="https://www.linkedin.com/legal/privacy-policy" rel="noopener">linkedin.com/legal/privacy-policy</a>.</p>
|
||||
<h2>2. Website delivery and security</h2>
|
||||
<p>When you visit our website, technical information is processed to deliver the pages you request and maintain the website’s availability and security.</p>
|
||||
|
||||
<h2>Right to information, correction, blocking, or deletion</h2>
|
||||
<p>Under applicable data protection law, you have the right to obtain information about your stored data free of charge and, if necessary, the right to correct, block, or delete this data. If you have any further questions about data protection and the processing of your personal data, if you require information, or if you wish to exercise your right to correction, blocking, or deletion, or in the event of revocation of consent, please send an email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. You can also use the contact details provided in the legal notice or the contact form on the website. CipherCell processes and uses the information you provide exclusively for the purpose of processing your request.</p>
|
||||
<p>This includes the IP address, request date and time, requested URL, response status, browser information, and referring page.</p>
|
||||
|
||||
<h2>Adjustments</h2>
|
||||
<p>CipherCell reserves the right to amend these data protection provisions from time to time. We will inform you in advance of any changes or additions and, where necessary, obtain your consent to the amended processing or use of your personal data.</p>
|
||||
<p>Hosting is provided by Dr. Felix Klement, adddress as above, Germany.</p>
|
||||
|
||||
<p>We process these data under Article 6(1)(f) GDPR. Our legitimate interests are operating a functional website, identifying faults, and preventing misuse and attacks.</p>
|
||||
|
||||
<p>Server logs are deleted after 7 days. Where specific records are needed to investigate a security incident or establish, exercise, or defend legal claims, we retain those records for as long as they are needed for the described purpose.</p>
|
||||
|
||||
<p>The technical information needed to deliver a requested page must be processed for the website to function.</p>
|
||||
|
||||
<h2>3. Contact enquiries</h2>
|
||||
<p>If you contact us by email or telephone, we process the contact details, message content, and attachments you provide to respond to your enquiry.</p>
|
||||
|
||||
<p>Where processing is necessary to take steps at your request before entering into a contract, or to perform a contract with you, the legal basis is Article 6(1)(b) GDPR. For other enquiries, the legal basis is Article 6(1)(f) GDPR; our legitimate interest is responding to communications addressed to us.</p>
|
||||
|
||||
<p>Our email service is provided by uberspace, Jonas Pasche, Germany. More information: <a href="https://uberspace.de" rel="noopener">https://uberspace.de</a>.</p>
|
||||
|
||||
<p>We delete ordinary enquiries after 30 days. Where correspondence must be retained under applicable legal obligations, we retain the necessary records for as long as we are obligated, based on Article 6(1)(c) GDPR.</p>
|
||||
|
||||
<p>Providing information is voluntary. However, we need sufficient information and a way to contact you to answer your enquiry.</p>
|
||||
|
||||
<h2>4. Website analytics with Matomo</h2>
|
||||
<p>We use Matomo to understand how visitors use our website and to improve its content and technical performance.</p>
|
||||
|
||||
<p>Our installation is configured without analytics cookies and masks IP addresses.</p>
|
||||
|
||||
<p>Matomo is hosted by Dr. Felix Klement, address as above, Germany. Only CipherCell recieves analytics data.</p>
|
||||
|
||||
<p>The legal basis is Article 6(1)(a) GDPR. Where analytics involves storing information on or accessing information from your device, we also obtain consent under §25(1) TDDDG.</p>
|
||||
|
||||
<p>If your browser supports and has activated the "Do Not Track" function, your visit will be automatically ignored by Matomo. You can still object to the anonymized analysis of your visit at any time. To do so, please use the opt-out below.</p>
|
||||
<OptOut lang="en" />
|
||||
|
||||
<h2>5. CipherVerifier alpha notification</h2>
|
||||
<p>You can register to receive one email announcing the first CipherVerifier alpha release. We do not use this registration for a general newsletter or unrelated advertising.</p>
|
||||
|
||||
<p>We process your email address to send a confirmation email. Your address is added to the active notification list only after you confirm using the link in that email.</p>
|
||||
|
||||
<p>The confirmation link expires after seven days. If not confirmed in time, the data from your registration is deleted immediately. </p>
|
||||
|
||||
<p>For confirmed registrations, we keep your email address and the additional data that you submitted during the registration.</p>
|
||||
|
||||
<p>The legal basis for registration and the notification is your consent under Article 6(1)(a) GDPR.</p>
|
||||
|
||||
<p>Our email delivery provider is uberspace, Jonas Pasche, Germany. More information: <a href="https://uberspace.de" rel="noopener">https://uberspace.de</a></p>
|
||||
|
||||
<p>You may withdraw your consent at any time through clicking the unsubscribe link in any of our emails related to the CipherVerifier alpha notification or by emailing <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. We then remove your address from the active notification list.</p>
|
||||
|
||||
<p>We also remove your address from that list after sending the notification or if the release is cancelled whichever occurs first.</p>
|
||||
|
||||
<p>Registration is optional. Without your email address and confirmation, we cannot send you the notification.</p>
|
||||
|
||||
<h2>6. Embedded LinkedIn posts</h2>
|
||||
<p>Some pages offer posts hosted by LinkedIn Ireland Unlimited Company, Ireland. These posts are initially displayed as placeholders. LinkedIn content is loaded only when you select <i>“Load post from LinkedIn.”</i></p>
|
||||
|
||||
<p>Before loading a post, we inform you that doing so connects your browser to LinkedIn and that LinkedIn may set cookies. The placeholder also links to this privacy notice.</p>
|
||||
|
||||
<p>By selecting “Load post from LinkedIn,” you consent to loading the selected post and the associated transmission of personal data to LinkedIn. Your browser sends LinkedIn technical information, including your IP address and browser information, and may disclose the page you are visiting. LinkedIn may set or read cookies or use similar technologies. If you are logged in to LinkedIn, it may associate this interaction with your account.</p>
|
||||
|
||||
<p>Our purpose is to display the LinkedIn post you choose to view. The legal basis for the processing for which we are responsible is your consent under Article 6(1)(a) GDPR. Consent also covers associated storage of information on, or access to information from, your device under §25(1) TDDDG.</p>
|
||||
|
||||
<p>You can leave the post unloaded and continue using our website.</p>
|
||||
|
||||
<p>You can withdraw your consent at any time by reloading the page. Withdrawal prevents further loading and transmission through the affected embed. It does not affect the lawfulness of processing before withdrawal or undo information already transmitted to LinkedIn.</p>
|
||||
|
||||
<p>LinkedIn may process data outside the European Economic Area, including in the United States.</p>
|
||||
|
||||
<p>Details about LinkedIn’s purposes, retention practices, international transfers, and privacy rights are available in <a href="https://de.linkedin.com/legal/privacy-policy" rel="noopener">LinkedIn’s Privacy Policy</a>.</p>
|
||||
|
||||
<h2>7. Service providers and international transfers</h2>
|
||||
<p>The providers identified above receive personal data only as needed for their respective services. Where they process data on our behalf, we engage them under data processing agreements and instructions.</p>
|
||||
|
||||
<h2>8. Your rights</h2>
|
||||
<p>Subject to the conditions set out in the GDPR, you may request access to your personal data, correction of inaccurate data, erasure, and restriction of processing.</p>
|
||||
|
||||
<p>Where processing is automated and based on consent or a contract, you may also have the right to receive data you provided in a structured, commonly used, machine-readable format and to transmit those data to another controller.</p>
|
||||
|
||||
<p><b>Right to object:</b> Where we process data under Article 6(1)(f) GDPR, you may object on grounds relating to your particular situation. We will stop the processing unless we demonstrate compelling legitimate grounds that override your interests, rights, and freedoms, or need the data for legal claims. You may object to processing for direct marketing at any time; we will then stop that processing.</p>
|
||||
|
||||
<p><b>Withdrawal of consent:</b> You may withdraw consent at any time without affecting the lawfulness of processing carried out before withdrawal.</p>
|
||||
|
||||
<p>To exercise your rights, contact office@ciphercell.de or write to the address above. Requests are normally free of charge. We generally respond within one month; if a legally permitted extension is necessary, we will explain this within that month.</p>
|
||||
|
||||
<p>You may also complain to a supervisory authority, particularly in the EU Member State where you habitually live or work, or where you consider an infringement occurred.</p>
|
||||
|
||||
<p>For private-sector controllers in Bavaria, the relevant authority is the Bavarian State Office for Data Protection Supervision (BayLDA). Information and complaint options are available on the <a href="https://www.lda.bayern.de/de/beschwerde.html" rel="noopener">BayLDA website</a>.</p>
|
||||
|
||||
<h2>9. Automated decisions</h2>
|
||||
<p>We do not currently make decisions about you based solely on automated processing, including profiling, that produce legal effects concerning you or similarly significantly affect you within the meaning of Article 22 GDPR.</p>
|
||||
|
||||
<h2>10. Data security</h2>
|
||||
<p>We use TLS encryption for connections to our website and maintain technical and organisational safeguards appropriate to the risks of processing personal data.</p>
|
||||
|
||||
<h2>11. Changes to this notice</h2>
|
||||
<p>We update this notice when our processing practices or applicable requirements change. The revision date appears above.</p>
|
||||
|
||||
<p>If we intend to use personal data for a new purpose, we will provide the required information before doing so and obtain new consent where necessary.</p>
|
||||
</Legal>
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user