From f53c27eddf546f51b874c47df7182c994ee05694 Mon Sep 17 00:00:00 2001
From: Yasin
Date: Thu, 22 Oct 2026 10:45:43 +0200
Subject: [PATCH 01/11] fix(footer): restore the dash in the ELIXIR brand
sentence
The brand guidelines fix the wording as "the Norwegian Node of ELIXIR –
the European infrastructure for life science data". The dash went missing in
the March redesign, so the footer read "ELIXIR the European infrastructure".
---
src/components/footer.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/footer.tsx b/src/components/footer.tsx
index 20939a13..4bf98427 100644
--- a/src/components/footer.tsx
+++ b/src/components/footer.tsx
@@ -50,7 +50,7 @@ const Footer = () => {
>
ELIXIR
- {' '} the European infrastructure for life science data.
+ {' '}– the European infrastructure for life science data.
{socials.map(({ name, href, icon: Icon }) => (
From fd919c0c71fef0e7f0cc1bbc941b2f81fd05b601 Mon Sep 17 00:00:00 2001
From: Yasin
Date: Thu, 22 Oct 2026 10:45:43 +0200
Subject: [PATCH 02/11] fix(hero): swap the rotating word with motion instead
of a CSS flip
The word flipped through a 3D rotation: the old phrase turned edge-on while
the new one turned in from edge-on, both faded to a quarter, with
backface-visibility on, and timers swapping the CSS classes alongside the
animations. On screen that read as a flicker on every swap.
The phrases now slide up through motion's AnimatePresence: the old one rises
and fades out while the new one rises and fades in over it, so each frame
shows legible text, and motion removes the old phrase when its exit ends
instead of a second timer. Every phrase still sits invisible in the same grid
cell, so the headline keeps its size. Reduced motion still shows only the
first phrase, and pausing still holds the current one.
---
src/components/hero.tsx | 36 ++++++++++++++++++------------------
src/styles/global.scss | 22 ----------------------
2 files changed, 18 insertions(+), 40 deletions(-)
diff --git a/src/components/hero.tsx b/src/components/hero.tsx
index 33794796..7de20b07 100644
--- a/src/components/hero.tsx
+++ b/src/components/hero.tsx
@@ -1,4 +1,4 @@
-import { MotionConfig, motion, useReducedMotion } from 'framer-motion';
+import { AnimatePresence, MotionConfig, motion, useReducedMotion } from 'framer-motion';
import React, { useState, useEffect } from 'react';
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
import { PauseIcon, PlayIcon } from '@heroicons/react/20/solid';
@@ -6,35 +6,35 @@ import Button from './button';
import HeroVideo from './hero-video';
import type { HeroCopy } from '../lib/site';
-const FLIP_MS = 600;
+const SWAP = { duration: 0.5, ease: [0.22, 1, 0.36, 1] } as const;
// Multi-word phrases stay a little longer than single fields so they can be read.
const dwellMs = (word: string, words: string[]) => (word.includes(' ') && word !== words[0] ? 2600 : 2300);
function RotatingWord({ words, playing }: { words: string[]; playing: boolean }) {
- const [{ current, leaving }, setFlip] = useState<{ current: number; leaving: number | null }>({ current: 0, leaving: null });
+ const [current, setCurrent] = useState(0);
useEffect(() => {
if (!playing) return;
- const id = setTimeout(() => {
- setFlip(({ current }) => ({ current: (current + 1) % words.length, leaving: current }));
- }, FLIP_MS + dwellMs(words[current], words));
+ const id = setTimeout(() => setCurrent((i) => (i + 1) % words.length), SWAP.duration * 1000 + dwellMs(words[current], words));
return () => clearTimeout(id);
}, [playing, current, words]);
- useEffect(() => {
- if (leaving === null) return;
- const id = setTimeout(() => setFlip(flip => ({ ...flip, leaving: null })), FLIP_MS);
- return () => clearTimeout(id);
- }, [leaving]);
-
return (
- {words.map((word, i) => {
- const state = i === current
- ? (leaving === null ? 'is-on' : 'is-on is-in')
- : i === leaving ? 'is-out' : '';
- return {word};
- })}
+ {words.map((word) => {word})}
+ {/* The old phrase rises out while the new one rises in over it, so the line is never empty mid-swap. */}
+
+
+ {words[current]}
+
+
);
}
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 20ecfd9b..d39271a3 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -125,25 +125,13 @@
.rotor {
display: grid;
place-items: center;
- perspective: 6em;
}
.rotor-word {
grid-area: 1 / 1;
- visibility: hidden;
max-width: 100%;
text-align: center;
text-wrap: balance;
- transform-origin: 50% 50% -0.5em;
- backface-visibility: hidden;
-
- &.is-on, &.is-out { visibility: visible; }
- &.is-in { animation: rotor-in 0.6s cubic-bezier(0.22, 0.8, 0.26, 1) both; }
- &.is-out { animation: rotor-out 0.6s cubic-bezier(0.22, 0.8, 0.26, 1) both; }
-
- @media (prefers-reduced-motion: reduce) {
- animation: none !important;
- }
}
/* Soft paper halo that keeps hero copy legible over the video without a box behind it. */
@@ -154,16 +142,6 @@
0 0 30px rgb(var(--color-paper) / 0.55);
}
- @keyframes rotor-in {
- from { transform: rotateX(90deg); opacity: 0.25; }
- to { transform: rotateX(0); opacity: 1; }
- }
-
- @keyframes rotor-out {
- from { transform: rotateX(0); opacity: 1; }
- to { transform: rotateX(-90deg); opacity: 0.25; }
- }
-
.content {
& :first-child {
@apply mt-0;
From 6fe4b6be2a88d5f2c9183e0b5666b10fb7d576b7 Mon Sep 17 00:00:00 2001
From: Yasin
Date: Thu, 22 Oct 2026 10:45:43 +0200
Subject: [PATCH 03/11] feat(nav): make the scrolled navigation a full-width
bar
Once the page scrolls, the navigation merged into a floating rounded pill
inset from the edges. It now becomes a rectangle across the full width,
flush with the top, with the same surface and shadow. The bar's padding
matches the floating insets, so the logo and the buttons keep their
horizontal place when it merges, and the morph between the two states
still runs through motion's layout animation.
---
src/components/navigation.tsx | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
index 04ca8527..d5f6271f 100644
--- a/src/components/navigation.tsx
+++ b/src/components/navigation.tsx
@@ -113,16 +113,17 @@ export const Navigation = ({ pathname }: { pathname: string }) => {
return (
-
+ {/* Floats inset from the edges at the top of the page; once merged it becomes a full-width
+ bar, padded so the logo and actions keep the same place across the edges. */}
+
-
+