diff --git a/src/components/elements.tsx b/src/components/elements.tsx
index 1348baa6..559f340f 100644
--- a/src/components/elements.tsx
+++ b/src/components/elements.tsx
@@ -30,30 +30,30 @@ export const LI: React.FC> = (props) => (
// title is: fixed sizes here meant an h2 that matched the title at 640px and
// overtook it below that, so the deeper heading looked like the page heading.
export const H1: React.FC> = ({ className, ...props }) => (
-
+
);
export const H2: React.FC> = (props) => (
-
+
);
export const H3: React.FC> = (props) => (
-
+
);
export const H4: React.FC> = (props) => (
-
+
);
// h5 and h6 land on the body size rather than below it. A heading set smaller
// than the text it introduces reads as a caption; weight and the display face
// carry the distinction at this depth instead.
export const H5: React.FC> = (props) => (
-
+
);
export const H6: React.FC> = (props) => (
-
+
);
export const P: React.FC> = (props) => (
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 }) => (
diff --git a/src/components/hero.tsx b/src/components/hero.tsx
index 33794796..bd698f73 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,37 @@ 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.
+ Search picks up every phrase from the invisible copies above, so this one stays out of it. */}
+
+
+ {words[current]}
+
+
);
}
diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx
index 04ca8527..1339a1e4 100644
--- a/src/components/navigation.tsx
+++ b/src/components/navigation.tsx
@@ -113,16 +113,18 @@ 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. Both sit
+ below the site banner, which stays pinned to the top. */}
+
-
+