This commit is contained in:
Felix Klement committed 2026-10-07 21:20:56 +02:00
1 parent a4c2ff3b32
commit 3cb5952e6d
4 files changed
+63 -19

No files matched your search

+5 -1
View File
@@ -190,7 +190,11 @@ photography existed:
| `engineer-glass.jpg` | Method section | https://www.magnific.com/app/creation/3zdScUIREY | | `engineer-glass.jpg` | Method section | https://www.magnific.com/app/creation/3zdScUIREY |
| `sector-5g.jpg` | Sectors, tab 1 | https://www.magnific.com/app/creation/s7ZQP52l8e | | `sector-5g.jpg` | Sectors, tab 1 | https://www.magnific.com/app/creation/s7ZQP52l8e |
| `sector-substation.jpg` | Sectors, tab 2 | https://www.magnific.com/app/creation/yi0r27vPW9 | | `sector-substation.jpg` | Sectors, tab 2 | https://www.magnific.com/app/creation/yi0r27vPW9 |
| `sector-datacentre.jpg` | Sectors, tab 3 | https://www.magnific.com/app/creation/eIHAvetdqL | | `sector-datacentre.jpg` | Sectors, tab 4 | https://www.magnific.com/app/creation/eIHAvetdqL |
`sector-ot.jpg` (Sectors, tab 3, operational technology) is a real photograph from the Freepik
stock catalogue via Magnific, "Workers in control room" (free licence, Canon EOS 5D Mark III, 2017),
downloaded on 7 October 2026 and scaled to 2304 px.
They are placeholders in the sense of the brief: swap them for the company's own documentary They are placeholders in the sense of the brief: swap them for the company's own documentary
photography when it exists, keeping the same file names and the `img-doc` treatment. The grid photography when it exists, keeping the same file names and the `img-doc` treatment. The grid
Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

+48 -10
View File
@@ -2,12 +2,13 @@
import { Picture } from 'astro:assets'; import { Picture } from 'astro:assets';
import fiveG from '../../assets/images/sector-5g.jpg'; import fiveG from '../../assets/images/sector-5g.jpg';
import substation from '../../assets/images/sector-substation.jpg'; import substation from '../../assets/images/sector-substation.jpg';
import controlRoom from '../../assets/images/sector-ot.jpg';
import datacentre from '../../assets/images/sector-datacentre.jpg'; import datacentre from '../../assets/images/sector-datacentre.jpg';
import { t, type Lang } from '../../i18n/ui'; import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang } interface Props { lang: Lang }
const { lang } = Astro.props; const { lang } = Astro.props;
const ui = t(lang); const ui = t(lang);
const images = [fiveG, substation, datacentre]; const images = [fiveG, substation, controlRoom, datacentre];
const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id: `sector-${i + 1}` })); const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id: `sector-${i + 1}` }));
--- ---
<section class="section sectors" id="sectors" aria-labelledby="sectors-title"> <section class="section sectors" id="sectors" aria-labelledby="sectors-title">
@@ -36,8 +37,8 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
alt={item.imageAlt} alt={item.imageAlt}
formats={['avif']} formats={['avif']}
fallbackFormat="webp" fallbackFormat="webp"
widths={[480, 768, 1024, 1440]} widths={[480, 768, 1024, 1440, 1920]}
sizes="(min-width: 64rem) 50vw, 100vw" sizes="(min-width: 64rem) 60vw, 100vw"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
class="sectors__img img-doc" class="sectors__img img-doc"
@@ -56,8 +57,8 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
alt="" alt=""
formats={['avif']} formats={['avif']}
fallbackFormat="webp" fallbackFormat="webp"
widths={[480, 768, 1024, 1440]} widths={[480, 768, 1024, 1440, 1920]}
sizes="(min-width: 64rem) 50vw, 100vw" sizes="(min-width: 64rem) 60vw, 100vw"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
class="sectors__img img-doc" class="sectors__img img-doc"
@@ -81,11 +82,34 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
flex-direction: column; flex-direction: column;
gap: var(--space-8); gap: var(--space-8);
} }
.sectors__tabs { margin-top: auto; } /* The tabs are rows between hairlines. The selected row's rule turns ink: that is the marker. */
.sectors__tab + .sectors__tab { margin-top: var(--space-5); } .sectors__tabs {
margin-top: auto;
border-bottom: var(--hairline) solid var(--line);
}
.sectors__tab {
position: relative;
border-top: var(--hairline) solid var(--line);
}
.sectors__tab::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: calc(-1 * var(--hairline));
height: var(--hairline);
background: var(--ink);
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-slow) var(--ease);
}
.sectors__tab:has([aria-selected='true'])::before { transform: none; }
.sectors__heading { font-size: inherit; font-weight: inherit; } .sectors__heading { font-size: inherit; font-weight: inherit; }
/* The whole row is the button, so the target is as wide as the column. */
.sectors__button { .sectors__button {
display: block; display: block;
width: 100%;
padding-block: var(--space-5) var(--space-3);
text-align: left; text-align: left;
font-size: var(--step-h3); font-size: var(--step-h3);
line-height: var(--leading-h3); line-height: var(--leading-h3);
@@ -96,7 +120,11 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
} }
.sectors__button[aria-selected='false'] { color: var(--ink-muted); } .sectors__button[aria-selected='false'] { color: var(--ink-muted); }
.sectors__button[aria-selected='false']:hover { color: var(--ink); } .sectors__button[aria-selected='false']:hover { color: var(--ink); }
.sectors__panel-inner { display: grid; gap: var(--space-5); padding-top: var(--space-3); } .sectors__panel-inner { display: grid; gap: var(--space-5); }
/* The button keeps a small step below its title, so an open body sits close under it and the
focus ring never crosses the text. The rest of the row padding is the panel's margin: below
the body when open, in place of it when closed, so every row is symmetric. */
.sectors__panel { margin-bottom: var(--space-5); }
.sectors__panel p { max-width: 28em; } .sectors__panel p { max-width: 28em; }
:global(.sectors__picture) { display: block; width: 100%; } :global(.sectors__picture) { display: block; width: 100%; }
.sectors__img, :global(.sectors__img) { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; } .sectors__img, :global(.sectors__img) { width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; }
@@ -110,17 +138,27 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
display: grid; display: grid;
grid-template-rows: 0fr; grid-template-rows: 0fr;
overflow: hidden; overflow: hidden;
transition: grid-template-rows var(--dur-slow) var(--ease); transition:
grid-template-rows var(--dur-slow) var(--ease),
margin var(--dur-slow) var(--ease);
} }
:global(html.js) .sectors__panel-inner { min-height: 0; } :global(html.js) .sectors__panel-inner { min-height: 0; }
:global(html.js) .sectors__panel[data-open] { grid-template-rows: 1fr; } :global(html.js) .sectors__panel[data-open] { grid-template-rows: 1fr; }
:global(html.js) .sectors__panel:not([data-open]) { margin-bottom: calc(var(--space-5) - var(--space-3)); }
@media (min-width: 64rem) { @media (min-width: 64rem) {
.sectors__text { grid-column: 1 / span 5; } .sectors__text { grid-column: 1 / span 5; }
/* The image runs past the container to the right edge of the window: the page gutter, plus
half of whatever the window is wider than the container. The section clips, so a classic
scrollbar (counted in 100vw) cannot cause a horizontal scroll. */
.sectors { overflow-x: clip; }
.sectors__media { .sectors__media {
grid-column: 7 / span 6; grid-column: 7 / span 6;
display: grid; display: grid;
margin-right: calc(-1 * (max(var(--gutter), var(--safe-right)) + max(0px, (100vw - var(--container)) / 2)));
} }
/* Near square, but never taller than a screenful on very wide windows. */
.sectors__media :global(.sectors__img) { aspect-ratio: 5 / 4; max-height: 45rem; }
.sectors__media :global(.sectors__picture--stage) { .sectors__media :global(.sectors__picture--stage) {
grid-area: 1 / 1; grid-area: 1 / 1;
opacity: 0; opacity: 0;
@@ -132,7 +170,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.sectors__panel, .sectors__media :global(.sectors__picture--stage) { transition: none; } .sectors__panel, .sectors__tab::before, .sectors__media :global(.sectors__picture--stage) { transition: none; }
} }
</style> </style>
+10 -8
View File
@@ -82,10 +82,11 @@ const en = {
], ],
}, },
sectors: { sectors: {
title: 'Who we work with', title: 'Who we work with.',
items: [ items: [
{ title: 'Private 5G and campus networks', body: 'Software-defined networks with open interfaces and a multi-vendor supply chain. Our origin, and still the hardest case.', imageAlt: 'A private 5G antenna and radio cabinet on the parapet of a factory roof, cranes in the distance.' }, { title: 'Private 5G and campus networks', body: 'Software-defined networks with open interfaces and a multi-vendor supply chain. Our origin, and still the hardest case.', imageAlt: 'A private 5G antenna and radio cabinet on the parapet of a factory roof, cranes in the distance.' },
{ title: 'Critical infrastructure', body: 'Control and network software where a breach stops production, damages equipment or endangers people.', imageAlt: 'An electrical substation seen straight from above: transformers, switchgear and a service road.' }, { title: 'Critical infrastructure', body: 'Control and network software where a breach stops production, damages equipment or endangers people.', imageAlt: 'An electrical substation seen straight from above: transformers, switchgear and a service road.' },
{ title: 'Operational technology (OT)', body: 'Controllers, SCADA and the engineering software that configures them. Systems built to run for decades, often on protocols without authentication, and rarely patched.', imageAlt: 'Three operators in hard hats at their consoles in a plant control room, facing a wall-sized diagram of the process.' },
{ title: 'Cloud-native systems', body: 'Containers, service meshes and CI/CD pipelines that carry infrastructure workloads. Security that runs as code, alongside yours.', imageAlt: 'A cold aisle between server racks in a data centre, a technician far in the distance.' }, { title: 'Cloud-native systems', body: 'Containers, service meshes and CI/CD pipelines that carry infrastructure workloads. Security that runs as code, alongside yours.', imageAlt: 'A cold aisle between server racks in a data centre, a technician far in the distance.' },
], ],
}, },
@@ -332,10 +333,10 @@ const en = {
roles: { roles: {
ceo: 'Founder and CEO', ceo: 'Founder and CEO',
cto: 'Founder and CTO', cto: 'Founder and CTO',
engineer: 'Software engineer', engineer: 'Software Engineer',
bizdev: 'Business development', bizdev: 'Business Development',
studentF: 'Working student, development', studentF: 'Mathematical Engineer',
studentM: 'Working student, development', studentM: 'Process Optimization & Automation',
}, },
research: { research: {
title: ['Published', 'research.'], title: ['Published', 'research.'],
@@ -427,10 +428,11 @@ const de: typeof en = {
], ],
}, },
sectors: { sectors: {
title: 'Für wen wir arbeiten', title: 'Für wen wir arbeiten.',
items: [ items: [
{ title: 'Private 5G- und Campusnetze', body: 'Softwaredefinierte Netze mit offenen Schnittstellen und einer Lieferkette aus vielen Herstellern. Unser Ursprung, und noch immer der schwerste Fall.', imageAlt: 'Eine private 5G-Antenne mit Funkschrank auf der Attika eines Fabrikdachs, Kräne im Hintergrund.' }, { title: 'Private 5G- und Campusnetze', body: 'Softwaredefinierte Netze mit offenen Schnittstellen und einer Lieferkette aus vielen Herstellern. Unser Ursprung, und noch immer der schwerste Fall.', imageAlt: 'Eine private 5G-Antenne mit Funkschrank auf der Attika eines Fabrikdachs, Kräne im Hintergrund.' },
{ title: 'Kritische Infrastruktur', body: 'Steuerungs- und Netzsoftware, bei der ein Einbruch die Produktion stoppt, Anlagen beschädigt oder Menschen gefährdet.', imageAlt: 'Ein Umspannwerk senkrecht von oben: Transformatoren, Schaltanlagen und eine Zufahrt.' }, { title: 'Kritische Infrastruktur', body: 'Steuerungs- und Netzsoftware, bei der ein Einbruch die Produktion stoppt, Anlagen beschädigt oder Menschen gefährdet.', imageAlt: 'Ein Umspannwerk senkrecht von oben: Transformatoren, Schaltanlagen und eine Zufahrt.' },
{ title: 'Operational Technology (OT)', body: 'Steuerungen, SCADA und die Engineering-Software, mit der sie konfiguriert werden. Anlagen, die Jahrzehnte laufen, oft über Protokolle ohne Authentisierung, und selten Updates erhalten.', imageAlt: 'Drei Bediener mit Schutzhelm an ihren Pulten in der Leitwarte eines Werks, vor einem wandgroßen Schema der Anlage.' },
{ title: 'Cloud-native Systeme', body: 'Container, Service Meshes und CI/CD-Pipelines, die Infrastruktur tragen. Sicherheit als Code, neben Ihrem.', imageAlt: 'Ein Kaltgang zwischen Serverschränken in einem Rechenzentrum, ein Techniker weit hinten.' }, { title: 'Cloud-native Systeme', body: 'Container, Service Meshes und CI/CD-Pipelines, die Infrastruktur tragen. Sicherheit als Code, neben Ihrem.', imageAlt: 'Ein Kaltgang zwischen Serverschränken in einem Rechenzentrum, ein Techniker weit hinten.' },
], ],
}, },
@@ -679,8 +681,8 @@ const de: typeof en = {
cto: 'Gründer und CTO', cto: 'Gründer und CTO',
engineer: 'Software Engineer', engineer: 'Software Engineer',
bizdev: 'Business Development', bizdev: 'Business Development',
studentF: 'Werkstudentin, Entwicklung', studentF: 'Mathematical Engineer',
studentM: 'Werkstudent, Entwicklung', studentM: 'Process Optimization & Automation',
}, },
research: { research: {
title: ['Veröffentlichte', 'Forschung.'], title: ['Veröffentlichte', 'Forschung.'],