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 ( ); } 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. */} +
- +