This commit is contained in:
Felix Klement committed 2026-10-05 21:32:07 +02:00
1 parent cd45c5d98f
commit 2b7ac42d6b
3 files changed
+375 -74

No files matched your search

+2 -50
View File
@@ -1,13 +1,10 @@
---
import Schematic from './Schematic.astro';
import { overview } from './diagrams';
import Verticals from './Verticals.astro';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const s = ui.verifier.soon;
const g = ui.verifier.how.groups[0];
const pair = overview(g);
/* The step that is current; earlier ones read as passed, later ones as open. */
const now = 0;
---
@@ -30,30 +27,7 @@ const now = 0;
</div>
</section>
<section class="section spec" aria-labelledby="spec-title">
<div class="container grid">
<div class="spec__head">
<h2 id="spec-title" class="h2">{s.spec.title}</h2>
<p class="muted">{s.spec.body}</p>
</div>
<dl class="spec__table">
{s.spec.rows.map((r) => (
<div class="spec__row">
<dt>{r.key}</dt>
<dd>{r.value}</dd>
</div>
))}
</dl>
<figure class="spec__figure">
<div class="spec__drawing hud-frame">
<div class="spec__wide"><Schematic layout={pair.wide} title={g.title} desc={g.desc} variant="wide" id="soon-wide" /></div>
<div class="spec__tall"><Schematic layout={pair.tall} title={g.title} desc={g.desc} variant="tall" id="soon-tall" /></div>
</div>
<figcaption class="small muted">{s.spec.figure}</figcaption>
</figure>
</div>
</section>
<Verticals lang={lang} />
<style>
/* Release path: one hairline, one node per phase. */
.track__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); align-content: start; }
@@ -100,30 +74,8 @@ const now = 0;
.track__step[data-state='next'] .track__name { color: var(--ink-muted); }
.track__body { max-width: 22em; }
/* Spec sheet: key and value on hairlines, like a data sheet. */
.spec { border-top: var(--hairline) solid var(--line); }
.spec__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); align-content: start; }
.spec__head p { max-width: 28em; }
.spec__table { grid-column: 1 / -1; border-top: var(--hairline) solid var(--ink); }
.spec__row {
display: grid;
grid-template-columns: minmax(8rem, 1fr) 2fr;
gap: var(--gap);
padding-block: var(--space-4);
border-bottom: var(--hairline) solid var(--line);
}
.spec__row dt { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); padding-top: 0.15em; }
.spec__figure { grid-column: 1 / -1; display: grid; gap: var(--space-3); margin-top: var(--space-6); }
.spec__drawing { padding: var(--space-6); }
.spec__wide { display: none; }
@media (min-width: 48rem) {
.track__head { grid-column: 1 / span 4; }
.spec__head { grid-column: 1 / span 5; }
.spec__table { grid-column: 7 / span 6; }
.spec__wide { display: block; }
.spec__tall { display: none; }
.spec__drawing { padding: var(--space-7) var(--space-8); }
}
/* Wide: the rail turns horizontal and runs across all three phases. */
@media (min-width: 64rem) {
+329
View File
@@ -0,0 +1,329 @@
---
import { localePath, t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const v = t(lang).verifier.soon.verticals;
/* The first tile is the vertical that is integrated; the others are open slots. */
const tiles = v.tiles.map((tile, i) => ({ ...tile, id: `vertical-${i + 1}`, kind: i === 0 ? 'telecom' : 'open' }));
---
<section class="section verticals" aria-labelledby="verticals-title">
<div class="container grid">
<div class="verticals__head">
<h2 id="verticals-title" class="h2">{v.title}</h2>
<p class="muted">{v.body}</p>
</div>
<div class="verticals__system" data-verticals>
<p class="verticals__platform small">{v.platform}</p>
<div class="verticals__tiles" role="tablist" aria-labelledby="verticals-title">
{tiles.map((tile, i) => (
<button
class="verticals__tile hud-frame"
type="button"
role="tab"
id={`${tile.id}-tab`}
aria-controls={`${tile.id}-panel`}
aria-selected={i === 0 ? 'true' : 'false'}
tabindex={i === 0 ? undefined : -1}
data-kind={tile.kind}
>
<span class="verticals__state small">
<span class="verticals__mark" aria-hidden="true" />
{tile.state}
</span>
{tile.kind === 'telecom' ? (
/* A lattice mast with its antenna panels, radiating to both sides, two cabinets at the foot. */
<svg class="verticals__drawing" viewBox="0 0 240 150" aria-hidden="true">
<g class="verticals__idea">
<path d="M104 132 L117 34 M136 132 L123 34 M117 34 L123 34 M120 34 L120 14" />
<path d="M105 124 L134 112 L107 100 L132 88 L109 76 L130 64 L112 52 L128 42" />
<path d="M110 40 h-5 v14 h5 z M130 40 h5 v14 h-5 z" />
<path d="M96 28 A30 30 0 0 0 96 66 M86 18 A44 44 0 0 0 86 76 M76 8 A58 58 0 0 0 76 86" />
<path d="M144 28 A30 30 0 0 1 144 66 M154 18 A44 44 0 0 1 154 76 M164 8 A58 58 0 0 1 164 86" />
</g>
<g class="verticals__ground">
<path d="M40 132 H200" />
<path d="M62 132 v-14 h18 v14 M160 132 v-14 h18 v14 M80 125 H104 M136 125 H160" />
</g>
</svg>
) : (
/* An empty module, drawn dashed: the slot the next vertical plugs into. */
<svg class="verticals__drawing" viewBox="0 0 240 150" aria-hidden="true">
<g class="verticals__idea">
<path d="M120 14 V34" />
<path class="verticals__dashed" d="M120 34 L162 56 L120 78 L78 56 Z M78 56 V104 L120 126 L162 104 V56 M120 78 V126" />
<path d="M120 49 v14 M113 56 h14" />
</g>
<g class="verticals__ground">
<path d="M40 132 H200" />
<path d="M60 22 v-8 h8 M180 22 v-8 h-8 M60 118 v8 h8 M180 118 v8 h-8" />
</g>
</svg>
)}
<span class="verticals__name">{tile.name}</span>
</button>
))}
</div>
<div class="verticals__panels">
{tiles.map((tile, i) => (
<div
class="verticals__panel"
role="tabpanel"
id={`${tile.id}-panel`}
aria-labelledby={`${tile.id}-tab`}
tabindex="0"
data-open={i === 0 ? '' : undefined}
>
<div class="verticals__copy">
<h3 class="h3">{tile.title}</h3>
<p class="muted">{tile.body}</p>
</div>
{'rows' in tile && tile.rows && (
<dl class="verticals__rows">
{tile.rows.map((r) => (
<div class="verticals__row">
<dt>{r.key}</dt>
<dd>{r.value}</dd>
</div>
))}
</dl>
)}
{'cta' in tile && tile.cta && (
<p class="verticals__cta"><a class="button" href={`${localePath(lang, '/')}#contact`}>{tile.cta}</a></p>
)}
</div>
))}
</div>
</div>
</div>
</section>
<style>
/* Verticals: the platform on top, a bus below it, one tile per vertical plugged into the bus.
The selected tile turns night; its panel below says what the vertical covers. */
.verticals { border-top: var(--hairline) solid var(--line); }
.verticals__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); align-content: start; }
.verticals__head p { max-width: 24em; }
.verticals__system { grid-column: 1 / -1; display: grid; }
/* Phones: the platform sits left, a spine runs down from it and each tile plugs in from the
side. Wider: the platform is centred over a bus and each tile drops a stub from it. */
.verticals__system { --stub: var(--space-5); --spine: var(--space-4); --indent: var(--space-7); }
.verticals__platform {
justify-self: start;
position: relative;
padding: var(--space-2) var(--space-4);
border: var(--hairline) solid var(--ink);
font-weight: var(--weight-medium);
}
.verticals__platform::after {
content: '';
position: absolute;
top: 100%;
left: var(--spine);
height: var(--stub);
border-left: var(--hairline) solid var(--ink);
}
.verticals__tiles {
position: relative;
display: grid;
grid-auto-rows: 1fr;
gap: var(--gap);
margin-top: var(--stub);
padding-left: var(--indent);
}
/* .hud-frame redefines --line as a width, so the frame colour comes from the HUD token. */
.verticals__tile {
--frame-color: var(--hud-line);
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 0.8fr);
grid-template-rows: auto 1fr;
column-gap: var(--space-4);
row-gap: var(--space-5);
padding: var(--space-4);
text-align: left;
font: inherit;
color: var(--ink);
background: var(--paper);
cursor: pointer;
transition:
background var(--dur) var(--ease),
color var(--dur) var(--ease);
}
/* The plug: down the spine from the tile above (or the platform), then across into the tile.
Rows share one height, so -50% of a tile reaches the middle of the one before it. */
.verticals__tile::before {
content: '';
position: absolute;
right: 100%;
width: calc(var(--indent) - var(--spine));
top: calc(-50% - var(--gap));
bottom: 50%;
border-left: var(--hairline) solid var(--ink);
border-bottom: var(--hairline) solid var(--ink);
}
.verticals__tile:first-child::before { top: calc(-1 * var(--stub)); }
/* An open slot is plugged in with a dashed line: reserved, not yet connected. */
.verticals__tile[data-kind='open']::before { border-left-style: dashed; border-bottom-style: dashed; }
.verticals__tile[aria-selected='true'] {
--frame-color: var(--ink-base);
--ink-muted: var(--ink-muted-on-night);
background: var(--night);
color: var(--ink-on-night);
cursor: default;
}
/* The ring sits outside the tile, on paper, so it keeps the paper ink even on the night tile. */
.verticals__tile:focus-visible { outline-color: var(--ink-base); }
/* Crosses only on the selected tile: it is the one under inspection. */
.verticals__tile[aria-selected='false']::after { opacity: 0; }
.verticals__tile::after { transition: opacity var(--dur) var(--ease); }
.verticals__state {
grid-column: 1;
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--ink-muted);
}
/* A square that fills when the tile is selected, like a selector on a panel. */
.verticals__mark {
width: 0.5rem;
height: 0.5rem;
border: var(--hairline) solid currentColor;
flex: none;
transition: background var(--dur) var(--ease);
}
.verticals__tile[aria-selected='true'] .verticals__mark { background: var(--ink-on-night); border-color: var(--ink-on-night); }
.verticals__drawing {
grid-column: 2;
grid-row: 1 / span 2;
width: 100%;
max-width: 22rem;
justify-self: center;
align-self: center;
height: auto;
overflow: visible;
fill: none;
stroke: currentColor;
stroke-width: var(--hairline);
stroke-linecap: square;
vector-effect: non-scaling-stroke;
transform-origin: 50% 60%;
transition: transform var(--dur-slow) var(--ease);
}
.verticals__drawing path { vector-effect: non-scaling-stroke; }
.verticals__ground { opacity: 0.4; transition: opacity var(--dur-slow) var(--ease); }
.verticals__dashed { stroke-dasharray: 4 4; }
.verticals__name {
grid-column: 1;
align-self: end;
font-size: var(--step-h3);
font-weight: var(--weight-medium);
letter-spacing: var(--track-h3);
line-height: var(--leading-h3);
}
/* Pointing at a tile brings its drawing forward, as on the home page cards. */
@media (hover: hover) {
.verticals__tile[aria-selected='false']:hover { background: color-mix(in srgb, var(--ink) var(--tint-hover), var(--paper)); }
.verticals__tile:hover .verticals__drawing { transform: scale(1.03); }
.verticals__tile:hover .verticals__ground { opacity: 0.26; }
}
/* Panels share one cell, so switching crossfades without the section changing height. */
.verticals__panels {
display: grid;
margin-top: var(--space-6);
border-top: var(--hairline) solid var(--ink);
}
.verticals__panel {
grid-area: 1 / 1;
display: grid;
gap: var(--space-5);
align-content: start;
padding-top: var(--space-5);
opacity: 0;
visibility: hidden;
transition:
opacity var(--dur) var(--ease),
visibility 0s linear var(--dur);
}
.verticals__panel[data-open] {
opacity: 1;
visibility: visible;
transition: opacity var(--dur-slow) var(--ease) var(--dur-fast), visibility 0s;
}
.verticals__copy { display: grid; gap: var(--space-3); }
.verticals__copy p { max-width: var(--measure); }
.verticals__rows { border-top: var(--hairline) solid var(--line); }
.verticals__row {
display: grid;
grid-template-columns: minmax(7rem, 1fr) 2fr;
gap: var(--gap);
padding-block: var(--space-3);
border-bottom: var(--hairline) solid var(--line);
}
.verticals__row dt { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); padding-top: 0.15em; }
@media (min-width: 48rem) {
.verticals__system { --stub: var(--space-6); }
.verticals__platform { justify-self: center; }
.verticals__platform::after { left: 50%; }
.verticals__tiles {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: auto;
margin-top: calc(2 * var(--stub));
padding-left: 0;
}
/* The bus runs between the centres of the two tiles. */
.verticals__tiles::before {
content: '';
position: absolute;
top: calc(-1 * var(--stub));
left: calc(25% - var(--gap) / 4);
right: calc(25% - var(--gap) / 4);
border-top: var(--hairline) solid var(--ink);
}
.verticals__tile {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
row-gap: var(--space-4);
padding: var(--space-5);
}
.verticals__tile::before,
.verticals__tile:first-child::before {
top: auto;
bottom: 100%;
left: 50%;
right: auto;
width: 0;
height: var(--stub);
border-bottom: 0;
}
.verticals__state, .verticals__drawing, .verticals__name { grid-column: 1; grid-row: auto; }
.verticals__drawing { padding-block: var(--space-4); }
.verticals__panel { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); column-gap: var(--gap); }
.verticals__rows, .verticals__cta { align-self: start; margin-top: var(--space-1); }
}
@media (min-width: 64rem) {
.verticals__head { grid-column: 1 / span 4; }
.verticals__system { grid-column: 5 / span 8; }
}
@media (prefers-reduced-motion: reduce) {
.verticals__tile, .verticals__tile::after, .verticals__mark, .verticals__drawing, .verticals__ground, .verticals__panel, .verticals__panel[data-open] { transition: none; }
}
</style>
<script>
import { initTabs } from '../../scripts/tabs';
const root = document.querySelector<HTMLElement>('[data-verticals]');
if (root) initTabs(root);
</script>
+44 -24
View File
@@ -304,19 +304,29 @@ const en = {
{ state: 'Later', name: 'General availability', body: 'Date to follow.' },
],
},
spec: {
title: 'What is being built.',
body: 'CipherVerifier takes a repository, runs seventeen static analysers and two bill-of-materials generators over it, reads each finding in its context and ranks what is real by security impact.',
rows: [
{ key: 'Static analysers', value: '17, each in its own container' },
{ key: 'Languages', value: '14, plus Docker, Kubernetes, Helm, Terraform and CloudFormation' },
{ key: 'Bill of materials', value: '2 generators, merged into one CycloneDX document' },
{ key: 'Vocabulary', value: '970 CWE entries' },
{ key: 'Pipeline', value: '5 stages, each switchable per repository' },
{ key: 'Review', value: 'Two-stage verdict on every finding' },
{ key: 'Deployment', value: 'Self-hosted' },
verticals: {
title: 'Verticals.',
body: 'One pipeline underneath. Each vertical brings the knowledge of one domain to it.',
platform: 'CipherVerifier platform',
tiles: [
{
state: 'Integrated',
name: 'Telecommunication security',
title: 'Built from Open RAN security research.',
body: 'The first vertical comes from where CipherCell started: the security of Open RAN and private 5G networks, worked on with the University of Passau and the xG-ALOE alliance.',
rows: [
{ key: 'Networks', value: 'Open RAN, private 5G' },
{ key: 'Infrastructure', value: 'Critical network infrastructure' },
],
},
{
state: 'Open',
name: 'Other',
title: 'Your domain next.',
body: 'The pipeline underneath is not tied to telecommunications. Further verticals are added where operators bring a domain to us.',
cta: 'Propose a vertical',
},
],
figure: 'Figure 1. One run, five stages.',
},
},
},
@@ -644,19 +654,29 @@ const de: typeof en = {
{ state: 'Später', name: 'Allgemeine Verfügbarkeit', body: 'Termin folgt.' },
],
},
spec: {
title: 'Was entsteht.',
body: 'CipherVerifier nimmt ein Repository, lässt siebzehn statische Analysewerkzeuge und zwei Stücklisten-Generatoren darüber laufen, liest jeden Fund in seinem Kontext und ordnet, was echt ist, nach Sicherheitsrelevanz.',
rows: [
{ key: 'Statische Analyse', value: '17 Werkzeuge, jedes im eigenen Container' },
{ key: 'Sprachen', value: '14, dazu Docker, Kubernetes, Helm, Terraform und CloudFormation' },
{ key: 'Stückliste', value: '2 Generatoren, zu einem CycloneDX-Dokument zusammengeführt' },
{ key: 'Vokabular', value: '970 CWE-Einträge' },
{ key: 'Pipeline', value: '5 Stufen, jede pro Repository abschaltbar' },
{ key: 'Urteil', value: 'Zweistufig, für jeden Fund' },
{ key: 'Betrieb', value: 'Selbst gehostet' },
verticals: {
title: 'Vertikalen.',
body: 'Darunter eine Pipeline. Jede Vertikale bringt das Wissen einer Domäne mit.',
platform: 'Plattform CipherVerifier',
tiles: [
{
state: 'Integriert',
name: 'Telekommunikations\u00ADsicherheit',
title: 'Aus der Open-RAN-Sicherheitsforschung.',
body: 'Die erste Vertikale kommt von dort, wo CipherCell begonnen hat: der Sicherheit von Open-RAN- und privaten 5G-Netzen, erarbeitet mit der Universität Passau und der xG-ALOE-Allianz.',
rows: [
{ key: 'Netze', value: 'Open RAN, private 5G-Netze' },
{ key: 'Infrastruktur', value: 'Kritische Netzinfrastruktur' },
],
},
{
state: 'Offen',
name: 'Weitere',
title: 'Ihre Domäne als Nächstes.',
body: 'Die Pipeline darunter ist nicht an die Telekommunikation gebunden. Weitere Vertikalen entstehen dort, wo Betreiber uns eine Domäne bringen.',
cta: 'Vertikale vorschlagen',
},
],
figure: 'Abbildung 1. Ein Lauf, fünf Stufen.',
},
},
},