diff --git a/src/assets/partners/university-of-passau.webp b/src/assets/partners/university-of-passau.webp new file mode 100644 index 0000000..1dc58c7 Binary files /dev/null and b/src/assets/partners/university-of-passau.webp differ diff --git a/src/assets/partners/xg-aloe.webp b/src/assets/partners/xg-aloe.webp new file mode 100644 index 0000000..34bca65 Binary files /dev/null and b/src/assets/partners/xg-aloe.webp differ diff --git a/src/assets/partners/xg-incubator.webp b/src/assets/partners/xg-incubator.webp new file mode 100644 index 0000000..8ecd33b Binary files /dev/null and b/src/assets/partners/xg-incubator.webp differ diff --git a/src/components/Field.astro b/src/components/Field.astro new file mode 100644 index 0000000..46acaaa --- /dev/null +++ b/src/components/Field.astro @@ -0,0 +1,62 @@ +--- +/* An abstract colour field with film grain: the picture for an Insights card that has none. + Palette and blob positions follow from `seed` (the entry's slug), so a card keeps its field + wherever it appears and however the list is sorted or filtered. Slot content sits on the + lower edge in paper-coloured type. */ +interface Props { seed: string; class?: string } +const { seed, class: className } = Astro.props; + +/* A base and three blobs per palette, chosen so neighbouring hues sit well together. */ +const palettes = [ + ['field-plum', 'field-ember', 'field-peach', 'field-rose'], + ['field-cobalt', 'field-sky', 'field-lilac', 'field-peach'], + ['field-jade', 'field-mint', 'field-sun', 'field-sky'], + ['field-violet', 'field-rose', 'field-lilac', 'field-sun'], + ['field-ocean', 'logo-cyan', 'field-mint', 'field-ember'], + ['field-ember', 'field-sun', 'field-rose', 'field-peach'], +]; + +/* FNV-1a, then a small LCG for the positions: stable across builds and machines. */ +let h = 2166136261; +for (const ch of seed) h = Math.imul(h ^ ch.codePointAt(0)!, 16777619) >>> 0; +const next = () => (h = (Math.imul(h, 1664525) + 1013904223) >>> 0) / 2 ** 32; +const [base, ...blobs] = palettes[h % palettes.length]; +const at = (lo: number, hi: number) => Math.round(lo + next() * (hi - lo)); +/* Blobs stay mostly in the upper two thirds, so the type at the bottom keeps a calmer ground. */ +const style = [`--base: var(--${base})`] + .concat(blobs.map((c, k) => `--c${k + 1}: var(--${c}); --x${k + 1}: ${at(0, 100)}%; --y${k + 1}: ${at(0, 70)}%`)) + .join('; '); +--- +
+ +
+ + diff --git a/src/components/home/Insights.astro b/src/components/home/Insights.astro index 05cdab3..7298440 100644 --- a/src/components/home/Insights.astro +++ b/src/components/home/Insights.astro @@ -1,6 +1,7 @@ --- import { getFeed } from '../../data/insights'; import { Picture } from 'astro:assets'; +import Field from '../Field.astro'; import { localePath, t, type Lang } from '../../i18n/ui'; interface Props { lang: Lang } const { lang } = Astro.props; @@ -20,9 +21,9 @@ const m = ui.insights.mention;
  • {item.kind === 'post' ? ( - + /> : }

    {item.entry.data.category}

    {item.entry.data.title}

    ) : ( - +

    {m.category(item.entry.data.platform)}

    {item.entry.data.title}

    @@ -45,7 +46,7 @@ const m = ui.insights.mention; ))}
  • -

    {ui.insights.event.when}

    + {ui.insights.event.when}

    {ui.insights.event.category}

    {ui.insights.event.text}

    @@ -77,14 +78,10 @@ const m = ui.insights.mention; .insights__card:hover h3 { text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.12em; } .insights__img, :global(.insights__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; margin-bottom: var(--space-2); } .insights__card h3 { max-width: 18em; } - .insights__event-when, .insights__panel { - display: flex; - align-items: flex-end; + /* No picture: a colour field carries the source (or the event date) in large type. */ + :global(.insights__panel) { aspect-ratio: 3 / 2; - padding: var(--space-5); margin-bottom: var(--space-2); - background: var(--paper-deep); - color: var(--ink-muted); font-size: var(--step-title); line-height: var(--leading-title); letter-spacing: var(--track-h2); diff --git a/src/components/home/PartnerLogo.astro b/src/components/home/PartnerLogo.astro new file mode 100644 index 0000000..326f6a8 --- /dev/null +++ b/src/components/home/PartnerLogo.astro @@ -0,0 +1,35 @@ +--- +/* One partner logo, two states. At rest: an ink silhouette (the image's alpha is the mask), so a + logo drawn for a dark ground reads as print on paper. On hover of the surrounding + `[data-partner]`: the original colours on night, the ground they were drawn for. */ +interface Props { name: string; src: ImageMetadata; url: string; h: string } +const { name, src, url, h } = Astro.props; +--- + + + diff --git a/src/components/home/Proof.astro b/src/components/home/Proof.astro index 3bd9495..6427dcf 100644 --- a/src/components/home/Proof.astro +++ b/src/components/home/Proof.astro @@ -1,8 +1,22 @@ --- +import { getImage } from 'astro:assets'; import { t, type Lang } from '../../i18n/ui'; +import PartnerLogo from './PartnerLogo.astro'; +import incubator from '../../assets/partners/xg-incubator.webp'; +import aloe from '../../assets/partners/xg-aloe.webp'; +import passau from '../../assets/partners/university-of-passau.webp'; interface Props { lang: Lang } const { lang } = Astro.props; 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 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 }))); ---
    @@ -15,6 +29,34 @@ const ui = t(lang);
  • ))} + +
    +

    {ui.proof.partners}

    + +
    + + {/* Variant B, for comparison: one column per partner across the full width, with its role. + Keep one of the two and delete the other. */} +
    +

    {ui.proof.partners}

    + +
    @@ -31,9 +73,63 @@ const ui = t(lang); .proof__item:last-child { border-bottom: var(--hairline) solid var(--line); } .proof__name { font-weight: var(--weight-medium); } .proof__body { max-width: 30em; } + + .partners { + grid-column: 1 / -1; + margin-top: var(--space-9); + display: grid; + gap: var(--space-5); + } + .partners__list { + display: grid; + border-top: var(--hairline) solid var(--line); + } + .partners__item { + display: flex; + align-items: center; + min-height: var(--space-9); + padding: var(--space-5) var(--space-4); + border-bottom: var(--hairline) solid var(--line); + } + /* 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); } + @media (hover: hover) { + [data-partner]:hover { background-color: var(--night); color: var(--ink-on-night); } + } + @media (prefers-reduced-motion: reduce) { [data-partner] { transition: none; } } + + /* Variant B */ + .partners-b { + grid-column: 1 / -1; + margin-top: var(--space-9); + display: grid; + gap: var(--space-5); + } + .partners-b__list { display: grid; border-top: var(--hairline) solid var(--line); } + .partners-b__item { + display: grid; + align-content: space-between; + gap: var(--space-7); + min-height: calc(var(--space-9) * 2.5); + padding: var(--space-6) var(--space-5); + border-bottom: var(--hairline) solid var(--line); + } + .partners-b__text { display: grid; gap: var(--space-1); } + .partners-b__name { font-weight: var(--weight-medium); } + .partners-b__role { color: var(--ink-muted); transition: color var(--dur-fast) var(--ease); } + @media (hover: hover) { + .partners-b__item:hover .partners-b__role { color: var(--ink-muted-on-night); } + } + @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 { grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--gap); align-items: start; } + .partners__label { grid-column: 1 / span 6; padding-top: var(--space-5); } + .partners__list { grid-column: 7 / span 6; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--gap); } + .partners-b__list { grid-template-columns: repeat(3, minmax(0, 1fr)); border-bottom: var(--hairline) solid var(--line); } + .partners-b__item { border-bottom: 0; } + .partners-b__item + .partners-b__item { border-left: var(--hairline) solid var(--line); } } diff --git a/src/content.config.ts b/src/content.config.ts index f91e2d3..0a79a9a 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -15,10 +15,12 @@ const insights = defineCollection({ readingTime: z.number().int().positive(), author: z.string(), authorRole: z.string(), - cover: image(), - coverAlt: z.string(), + /* Optional: without a cover the cards show a colour field (src/components/Field.astro) + and the essay page opens without an image. */ + cover: image().optional(), + coverAlt: z.string().optional(), tags: z.array(z.string()).default([]), - }), + }).refine((d) => !d.cover || d.coverAlt, { message: 'coverAlt is required with a cover', path: ['coverAlt'] }), }); /* Mentions: short references to posts elsewhere (LinkedIn first) that CipherCell took part in. diff --git a/src/pages/_InsightsIndex.astro b/src/pages/_InsightsIndex.astro index 64c4447..bddf43e 100644 --- a/src/pages/_InsightsIndex.astro +++ b/src/pages/_InsightsIndex.astro @@ -2,6 +2,7 @@ import { feedType, feedTypes, getFeed } from '../data/insights'; import { Picture } from 'astro:assets'; import Base from '../layouts/Base.astro'; +import Field from '../components/Field.astro'; import { formatDate, localePath, t, type Lang } from '../i18n/ui'; interface Props { lang: Lang } @@ -45,19 +46,25 @@ const filters = [ {feed.map((item) => item.kind === 'post' ? (
  • -
    - -
    + {item.entry.data.cover ? ( +
    + +
    + ) : ( + + + + )}
  • - + + + +
    -
    + {d.cover &&
    -
    +
    }
    diff --git a/src/styles/tokens.css b/src/styles/tokens.css index c37accb..0a017cc 100644 --- a/src/styles/tokens.css +++ b/src/styles/tokens.css @@ -29,6 +29,18 @@ --field-jade: #3B8C78; --field-slate: #3A4862; --field-plum: #66508E; + /* Warmer tones for the Insights card fields (cards without a picture), so the feed reads as + colour rather than paper. Fields only, never text or UI. Combined in src/components/Field.astro. */ + --field-ember: #DE5B32; + --field-peach: #F2AE84; + --field-rose: #D4547C; + --field-violet: #6E4FCF; + --field-lilac: #B7A1EE; + --field-sun: #EFB443; + --field-mint: #86CFB0; + --field-sky: #6AAEE8; + /* Darkens the lower edge of a field so paper-coloured type on it holds 3:1 or better. */ + --field-scrim: linear-gradient(to top, rgb(14 16 15 / 0.55), rgb(14 16 15 / 0) 65%); /* 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. */