This commit is contained in:
Felix Klement committed 2026-10-07 22:39:28 +02:00
1 parent 3cb5952e6d
commit ab6495ad10
9 files changed
+63 -72

No files matched your search

@@ -11,28 +11,18 @@ const ui = t(lang);
/* The partner logos as on ciphercell.de (see PartnerLogo.astro for the two states). `h` balances
optical size: the incubator wordmark is long and thin, the other two carry a mark. */
const roles = ui.proof.partnerRoles;
const roles = ui.partners.roles;
const logos = await Promise.all([
{ name: 'xG-Incubator', role: roles.incubator, src: incubator, h: 'var(--space-5)' },
{ name: 'xG-ALOE', role: roles.aloe, src: aloe, h: 'var(--space-7)' },
{ name: lang === 'de' ? 'Universität Passau' : 'University of Passau', role: roles.passau, src: passau, h: 'var(--space-7)' },
].map(async (l) => ({ ...l, url: (await getImage({ src: l.src, format: 'webp' })).src })));
---
<section class="section proof" aria-labelledby="proof-title">
<section class="section section--tight partners" aria-labelledby="partners-title">
<div class="container grid">
<h2 id="proof-title" class="h2 proof__title">{ui.proof.title}</h2>
<ul class="proof__list" role="list">
{ui.proof.items.map((p) => (
<li class="proof__item">
<p class="proof__name">{p.name}</p>
<p class="muted proof__body">{p.body}</p>
</li>
))}
</ul>
<div class="partners">
<p class="small muted" id="partners-label">{ui.proof.partners}</p>
<ul class="partners__list" role="list" aria-labelledby="partners-label">
<div class="partners__block">
<h2 class="small muted partners__title" id="partners-title">{ui.partners.title}</h2>
<ul class="partners__list" role="list">
{logos.map((l) => (
<li class="partners__item" data-partner>
<PartnerLogo name={l.name} src={l.src} url={l.url} h={l.h} />
@@ -48,18 +38,8 @@ const logos = await Promise.all([
</section>
<style>
.proof { border-top: var(--hairline) solid var(--line); }
.proof__title { grid-column: 1 / -1; }
.proof__list { grid-column: 1 / -1; }
.proof__item {
display: grid;
gap: var(--space-2);
padding-block: var(--space-5);
border-top: var(--hairline) solid var(--line);
}
.proof__item:last-child { border-bottom: var(--hairline) solid var(--line); }
.proof__name { font-weight: var(--weight-medium); }
.proof__body { max-width: 30em; }
.partners__title { font-weight: var(--weight-regular); }
/* Hover answers the pointer with the logo's own ground: night, original colours. */
[data-partner] { transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease); }
@@ -68,9 +48,8 @@ const logos = await Promise.all([
}
@media (prefers-reduced-motion: reduce) { [data-partner] { transition: none; } }
.partners {
.partners__block {
grid-column: 1 / -1;
margin-top: var(--space-9);
display: grid;
gap: var(--space-5);
}
@@ -115,9 +94,6 @@ const logos = await Promise.all([
}
@media (min-width: 48rem) {
.proof__title { grid-column: 1 / span 6; }
.proof__list { grid-column: 7 / span 6; }
.proof__item { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); column-gap: var(--gap); }
.partners__list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.partners__item + .partners__item { border-top: 0; border-left: var(--hairline) solid var(--line); }
}
+2 -4
View File
@@ -223,7 +223,7 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
<script>
/**
* Rotates the founders' statements. Plays on its own only without reduced motion; holds while
* the pointer or focus is inside or the section is off screen; picking a statement stops it.
* keyboard focus is inside or the section is off screen; picking a statement stops it.
*/
const root = document.querySelector<HTMLElement>('[data-quotes]');
if (root) {
@@ -259,10 +259,8 @@ const quoted = (text: string) => (lang === 'de' ? `„${text}“` : `“${text}
picks.forEach((pick, i) => pick.addEventListener('click', () => { setPlaying(false); show(i); }));
const hold = (on: boolean) => root.toggleAttribute('data-held', on);
root.addEventListener('pointerenter', () => hold(true));
root.addEventListener('pointerleave', () => hold(root.contains(document.activeElement)));
root.addEventListener('focusin', () => hold(true));
root.addEventListener('focusout', (e) => { if (!root.contains(e.relatedTarget as Node)) hold(root.matches(':hover')); });
root.addEventListener('focusout', (e) => { if (!root.contains(e.relatedTarget as Node)) hold(false); });
// Off screen, the clock stops, so a reader arriving from above starts on a fresh statement.
new IntersectionObserver(([entry]) => root.toggleAttribute('data-away', !entry.isIntersecting), { threshold: 0.4 }).observe(root);
+33 -8
View File
@@ -3,13 +3,16 @@ import { Picture } from 'astro:assets';
import fiveG from '../../assets/images/sector-5g.jpg';
import substation from '../../assets/images/sector-substation.jpg';
import controlRoom from '../../assets/images/sector-ot.jpg';
import products from '../../assets/images/sector-rd-engineer.jpg';
import datacentre from '../../assets/images/sector-datacentre.jpg';
import { t, type Lang } from '../../i18n/ui';
interface Props { lang: Lang }
const { lang } = Astro.props;
const ui = t(lang);
const images = [fiveG, substation, controlRoom, datacentre];
const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id: `sector-${i + 1}` }));
const images = [fiveG, substation, controlRoom, products, datacentre];
/* Where each picture keeps its subject when cropped; the R&D engineer sits on the left. */
const focus = ['center', 'center', 'center', '30% center', 'center'];
const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], focus: focus[i], id: `sector-${i + 1}` }));
---
<section class="section sectors" id="sectors" aria-labelledby="sectors-title">
<div class="container sectors__grid" data-sectors>
@@ -42,6 +45,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
loading="lazy"
decoding="async"
class="sectors__img img-doc"
style={`object-position: ${item.focus}`}
pictureAttributes={{ class: 'sectors__picture' }}
/>
</div>
@@ -62,6 +66,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
loading="lazy"
decoding="async"
class="sectors__img img-doc"
style={`object-position: ${item.focus}`}
pictureAttributes={{ class: 'sectors__picture sectors__picture--stage', 'data-stage-item': '', 'data-active': i === 0 ? '' : undefined }}
/>
))}
@@ -70,6 +75,7 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
</section>
<style>
.sectors { border-top: var(--hairline) solid var(--line); }
.sectors__grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
@@ -144,7 +150,16 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
}
: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:not([data-open]) { margin-bottom: calc(var(--space-5) - var(--space-3)); }
/* A closed panel is hidden, not only collapsed, so it does not take focus while invisible.
Visibility switches at the end of the collapse and at the start of the expand. */
:global(html.js) .sectors__panel:not([data-open]) {
margin-bottom: calc(var(--space-5) - var(--space-3));
visibility: hidden;
transition:
grid-template-rows var(--dur-slow) var(--ease),
margin var(--dur-slow) var(--ease),
visibility 0s linear var(--dur-slow);
}
@media (min-width: 64rem) {
.sectors__text { grid-column: 1 / span 5; }
@@ -152,15 +167,21 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
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; }
/* The image box is at least 5:4 (never taller than 45rem), and stretches with the row when
the list beside it is taller, so its top and bottom always meet the heading and the last rule. */
.sectors__media {
grid-column: 7 / span 6;
display: grid;
display: block;
position: relative;
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; }
/* The minimum: a spacer as tall as 5:4 of the width, capped at 45rem. A grid item stretches
to its row, so a taller list beside it makes the box taller; the pictures fill the box. */
.sectors__media::before { content: ''; display: block; padding-top: min(80%, 45rem); }
.sectors__media :global(.sectors__img) { height: 100%; aspect-ratio: auto; }
.sectors__media :global(.sectors__picture--stage) {
grid-area: 1 / 1;
position: absolute;
inset: 0;
opacity: 0;
transition: opacity var(--dur-slow) var(--ease);
}
@@ -170,7 +191,11 @@ const items = ui.sectors.items.map((item, i) => ({ ...item, image: images[i], id
}
@media (prefers-reduced-motion: reduce) {
.sectors__panel, .sectors__tab::before, .sectors__media :global(.sectors__picture--stage) { transition: none; }
.sectors__panel,
:global(html.js) .sectors__panel,
:global(html.js) .sectors__panel:not([data-open]),
.sectors__tab::before,
.sectors__media :global(.sectors__picture--stage) { transition: none; }
}
</style>