From 58d1fbe4ed2c5cfd3d1a8586fc62b186e6a64290 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Thu, 19 Feb 2026 10:58:38 +0000 Subject: [PATCH 01/20] adding WIP stats slice --- .../Molecules/StatsSlice/StatsSlice.js | 131 ++++++++++++++++++ .../Molecules/StatsSlice/StatsSlice.md | 40 ++++++ .../Molecules/StatsSlice/StatsSlice.style.js | 47 +++++++ src/index.js | 1 + 4 files changed, 219 insertions(+) create mode 100644 src/components/Molecules/StatsSlice/StatsSlice.js create mode 100644 src/components/Molecules/StatsSlice/StatsSlice.md create mode 100644 src/components/Molecules/StatsSlice/StatsSlice.style.js diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js new file mode 100644 index 000000000..03464bc0f --- /dev/null +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -0,0 +1,131 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + OuterWrapper, + InnerWrapper, + StatContainer, + StatValue, + AnimatedDigit +} from './StatsSlice.style'; +import Text from '../../Atoms/Text/Text'; + +/** + stats slice + */ +const StatsSlice = ({ stats }) => { + const localStats = stats; + return ( + + + {localStats?.map((stat, index) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + + ); +}; + +const StatPropTypes = { + prefix: PropTypes.string, + value: PropTypes.string.isRequired, + suffix: PropTypes.string, + description: PropTypes.string +}; + +StatsSlice.propTypes = { + stats: PropTypes.arrayOf(PropTypes.shape(StatPropTypes)) +}; + +function StatComponent({ + prefix, + value, + suffix, + description +}) { + return ( + + + + {/* {prefix} + {value} + {suffix} */} + + {description} + + ); +} + +StatComponent.propTypes = StatPropTypes; + +function AnimatedString({ value, delay }) { + const totalDelay = String(value).length * 100; + return ( +
+ {String(value) + .split('') + .map((character, index) => ( + + ))} +
+ ); +} + +function AnimatedCharacter({ value, type, delay }) { + const digitRef = useRef(null); + + useEffect(() => { + digitRef.current.style.transform = `translateY(-${(100 / 11) * +value}%)`; + }, [value]); + + switch (type) { + case 'string': { + return ( +
+
+ {value} +
+ ); + } + case 'number': + default: { + return ( +
+ {value} + +
 
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
+
+ ); + } + } +} + +export const ValueTypes = { + String: 'string', + Number: 'number' +}; + +AnimatedCharacter.propTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + type: PropTypes.oneOf(Object.values(ValueTypes)), + delay: PropTypes.number +}; + +export default StatsSlice; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md new file mode 100644 index 000000000..c355b776f --- /dev/null +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -0,0 +1,40 @@ +# Stats Slice + +### Empty stats + +```js + +``` + +### Basic stats + +```js + +``` + +### Multiple stats + +```js + +``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js new file mode 100644 index 000000000..1192c139a --- /dev/null +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -0,0 +1,47 @@ +import styled from 'styled-components'; + +export const OuterWrapper = styled.div` + padding: 2rem; + background: #eeeeee; +`; + +export const InnerWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + flex-direction: row; + gap: 2rem; + } +`; + +export const StatContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; + background: #ffffff; + padding: 1rem; + flex: 1 1 auto; + border-radius: 1rem; +`; + +export const StatValue = styled.div` + display: flex; + text-transform: uppercase; +// font-family: Courier New, Courier, monospace; + font-family: ${({ theme }) => theme.fontFamilies('Anton')}; + font-size: ${({ theme }) => theme.fontSize('l')}; + line-height: 1; +`; + +export const AnimatedDigit = styled.div` + position: absolute; + top: 0; + left: 0; + + transition-delay: var(--delay); + transition-property: transform; + transition-duration: 2s; + transition-timing-function: linear(0, 0.192 4.1%, 0.361 8.2%, 0.513 12.5%, 0.645 16.9%, 0.703 19.1%, 0.758 21.4%, 0.808 23.7%, 0.854 26.1%, 0.895 28.5%, 0.932 31%, 0.964 33.5%, 0.993 36.1%, 1.013 38.3%, 1.031 40.6%, 1.046 42.9%, 1.058 45.3%, 1.067 47.8%, 1.074 50.3%, 1.078 53%, 1.08 55.8%, 1.077 60.4%, 1.069 65.5%, 1.057 70.4%, 1.024 82.4%, 1.01 88.7%, 1.002 94.4%, 1); +`; diff --git a/src/index.js b/src/index.js index 5c837092d..2601d9886 100644 --- a/src/index.js +++ b/src/index.js @@ -70,6 +70,7 @@ export { default as SimpleSchoolLookup } from './components/Molecules/SimpleScho export { default as LogoLinked } from './components/Molecules/LogoLinked/LogoLinked'; export { default as HeroBanner } from './components/Molecules/HeroBanner/HeroBanner'; export { default as QuoteSlice } from './components/Molecules/QuoteSlice/QuoteSlice'; +export { default as StatsSlice } from './components/Molecules/StatsSlice/StatsSlice'; /* Organisms */ export { From c02f48ab9b8653309bf19bf3aac290fa281121c2 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Thu, 19 Feb 2026 10:59:28 +0000 Subject: [PATCH 02/20] adding type detection --- .../Molecules/StatsSlice/StatsSlice.js | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 03464bc0f..2d17c4351 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -37,16 +37,11 @@ StatsSlice.propTypes = { stats: PropTypes.arrayOf(PropTypes.shape(StatPropTypes)) }; -function StatComponent({ - prefix, - value, - suffix, - description -}) { +function StatComponent({ prefix, value, suffix, description }) { return ( - + {/* {prefix} {value} {suffix} */} @@ -58,21 +53,29 @@ function StatComponent({ StatComponent.propTypes = StatPropTypes; +function getValueType(value) { + return Number.isNaN(parseInt(value, 10)) ? 'string' : 'number'; +} + function AnimatedString({ value, delay }) { const totalDelay = String(value).length * 100; + return (
{String(value) .split('') - .map((character, index) => ( - - ))} + .map((character, index) => { + const type = getValueType(character); + return ( + + ); + })}
); } From 313f2f245d416788777be2c399896f287f80b2a5 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Mon, 23 Feb 2026 10:58:22 +0000 Subject: [PATCH 03/20] removing pre/suffix --- .../Molecules/StatsSlice/StatsSlice.js | 135 ++++++++++-------- .../Molecules/StatsSlice/StatsSlice.md | 13 +- 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 2d17c4351..bdd29325c 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -12,6 +12,7 @@ import Text from '../../Atoms/Text/Text'; /** stats slice */ +// MARK: stats slice const StatsSlice = ({ stats }) => { const localStats = stats; return ( @@ -27,9 +28,7 @@ const StatsSlice = ({ stats }) => { }; const StatPropTypes = { - prefix: PropTypes.string, - value: PropTypes.string.isRequired, - suffix: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, description: PropTypes.string }; @@ -37,14 +36,12 @@ StatsSlice.propTypes = { stats: PropTypes.arrayOf(PropTypes.shape(StatPropTypes)) }; -function StatComponent({ prefix, value, suffix, description }) { +// MARK: stat +function StatComponent({ value, description }) { return ( - - {/* {prefix} - {value} - {suffix} */} + {description} @@ -57,67 +54,90 @@ function getValueType(value) { return Number.isNaN(parseInt(value, 10)) ? 'string' : 'number'; } -function AnimatedString({ value, delay }) { +// MARK: string +function AnimatedText({ value, delay }) { const totalDelay = String(value).length * 100; + const characters = String(value).split('').map(character => { + const type = getValueType(character); + return { + character, + type + }; + }); + + console.log(characters); + return (
- {String(value) - .split('') - .map((character, index) => { - const type = getValueType(character); - return ( - - ); + {characters + .map(({ character, type }, index) => { + const key = index + character; + const delay = totalDelay - index * 100; + switch (type) { + case 'string': + return ; + case 'number': + return ; + default: + return null; + } })}
); } -function AnimatedCharacter({ value, type, delay }) { +// MARK: character +function AnimatedString({ value, delay }) { + const digitRef = useRef(null); + + useEffect(() => { + const transform = `translateY(-50%)`; + digitRef.current?.style.setProperty('transform', transform); + }, [value]); + + return ( +
+ {value} + +
 
+
{value}
+
+
+ ); +} + +AnimatedString.propTypes = { + value: PropTypes.string.isRequired, + delay: PropTypes.number +}; + +function AnimatedNumber({ value, delay }) { const digitRef = useRef(null); useEffect(() => { - digitRef.current.style.transform = `translateY(-${(100 / 11) * +value}%)`; + const transform = `translateY(-${(100 / 11) * (+value || 10)}%)`; + digitRef.current?.style.setProperty('transform', transform); }, [value]); - switch (type) { - case 'string': { - return ( -
-
- {value} -
- ); - } - case 'number': - default: { - return ( -
- {value} - -
 
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
0
-
-
- ); - } - } + return ( +
+ {value} + +
 
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
+
+ ); } export const ValueTypes = { @@ -125,9 +145,8 @@ export const ValueTypes = { Number: 'number' }; -AnimatedCharacter.propTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - type: PropTypes.oneOf(Object.values(ValueTypes)), +AnimatedNumber.propTypes = { + value: PropTypes.number.isRequired, delay: PropTypes.number }; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index c355b776f..35f3d37aa 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -10,9 +10,7 @@ ```js ``` @@ -21,20 +19,15 @@ ```js ``` From 576133eb09281abb015c291f76554b4a3fe54676 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Mon, 23 Feb 2026 10:59:41 +0000 Subject: [PATCH 04/20] cleaning up --- src/components/Molecules/StatsSlice/StatsSlice.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index bdd29325c..df4e04b11 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -66,8 +66,6 @@ function AnimatedText({ value, delay }) { }; }); - console.log(characters); - return (
{characters @@ -96,6 +94,8 @@ function AnimatedString({ value, delay }) { digitRef.current?.style.setProperty('transform', transform); }, [value]); + // TODO add more characters? + return (
{value} @@ -140,11 +140,6 @@ function AnimatedNumber({ value, delay }) { ); } -export const ValueTypes = { - String: 'string', - Number: 'number' -}; - AnimatedNumber.propTypes = { value: PropTypes.number.isRequired, delay: PropTypes.number From c3e44dcb97bbccbe0a8fdc61539d8a049202db2b Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 25 Feb 2026 09:56:39 +0000 Subject: [PATCH 05/20] updating delay --- .../Molecules/StatsSlice/StatsSlice.js | 93 ++++++++++++------- .../Molecules/StatsSlice/StatsSlice.md | 8 +- .../Molecules/StatsSlice/StatsSlice.style.js | 34 ++++++- 3 files changed, 94 insertions(+), 41 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index df4e04b11..d0f37247c 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -5,9 +5,19 @@ import { InnerWrapper, StatContainer, StatValue, - AnimatedDigit + AnimatedDigit, + ValueContainer, + ValueUnderline } from './StatsSlice.style'; import Text from '../../Atoms/Text/Text'; +import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; + +// prop type for an individual stat +const StatPropTypes = { + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + delay: PropTypes.number, + description: PropTypes.string +}; /** stats slice @@ -20,29 +30,27 @@ const StatsSlice = ({ stats }) => { {localStats?.map((stat, index) => ( // eslint-disable-next-line react/no-array-index-key - + ))} ); }; -const StatPropTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - description: PropTypes.string -}; - StatsSlice.propTypes = { stats: PropTypes.arrayOf(PropTypes.shape(StatPropTypes)) }; -// MARK: stat -function StatComponent({ value, description }) { +// MARK: individual stat +function StatComponent({ value, description, delay }) { return ( - - - + + + + + + {description} ); @@ -58,34 +66,52 @@ function getValueType(value) { function AnimatedText({ value, delay }) { const totalDelay = String(value).length * 100; - const characters = String(value).split('').map(character => { - const type = getValueType(character); - return { - character, - type - }; - }); + const characters = String(value) + .split('') + .map((character) => { + const type = getValueType(character); + return { + character, + type + }; + }); return (
- {characters - .map(({ character, type }, index) => { - const key = index + character; - const delay = totalDelay - index * 100; - switch (type) { - case 'string': - return ; - case 'number': - return ; - default: - return null; - } - })} + {characters.map(({ character, type }, index) => { + const key = index + character; + const characterDelay = delay + (totalDelay - index * 100); + switch (type) { + case 'string': + return ( + + ); + case 'number': + return ( + + ); + default: + return null; + } + })}
); } -// MARK: character +AnimatedText.propTypes = { + value: PropTypes.string.isRequired, + delay: PropTypes.number +}; + +// MARK: string char function AnimatedString({ value, delay }) { const digitRef = useRef(null); @@ -112,6 +138,7 @@ AnimatedString.propTypes = { delay: PropTypes.number }; +// MARK: numeric char function AnimatedNumber({ value, delay }) { const digitRef = useRef(null); diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index 35f3d37aa..81088f76f 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -11,7 +11,7 @@ ```js ``` @@ -20,14 +20,14 @@ ```js ``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 1192c139a..da8a5aa02 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -19,20 +19,46 @@ export const InnerWrapper = styled.div` export const StatContainer = styled.div` display: flex; flex-direction: column; - gap: 0.5rem; + align-items: flex-start; + gap: 1rem; background: #ffffff; - padding: 1rem; - flex: 1 1 auto; + padding: 2rem; + flex: 1; border-radius: 1rem; `; +export const ValueContainer = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + padding-bottom: 1rem; +`; + +export const ValueUnderline = styled.img` + position: absolute; + width: 100%; + height: 4px; + left: 0; + bottom: 0px; + clip-path: inset(0px 50% 0px 0px); +`; + export const StatValue = styled.div` display: flex; text-transform: uppercase; -// font-family: Courier New, Courier, monospace; font-family: ${({ theme }) => theme.fontFamilies('Anton')}; font-size: ${({ theme }) => theme.fontSize('l')}; line-height: 1; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + font-size: ${({ theme }) => theme.fontSize('xl')}; + } + + @media ${({ theme }) => theme.breakpoints2026('L')} { + font-size: ${({ theme }) => theme.fontSize('xxl')}; + } `; export const AnimatedDigit = styled.div` From 299f4ea070809c6e618459d6bd8a868bfd8b97e0 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 25 Feb 2026 11:33:53 +0000 Subject: [PATCH 06/20] making delay consistent; adding underline --- .../Molecules/StatsSlice/StatsSlice.js | 167 ++++++++++-------- .../Molecules/StatsSlice/StatsSlice.md | 2 +- .../Molecules/StatsSlice/StatsSlice.style.js | 19 +- 3 files changed, 107 insertions(+), 81 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index d0f37247c..0d22ae323 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -12,63 +12,69 @@ import { import Text from '../../Atoms/Text/Text'; import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; -// prop type for an individual stat -const StatPropTypes = { - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - delay: PropTypes.number, - description: PropTypes.string -}; +const characterDelayMs = 100; -/** - stats slice - */ // MARK: stats slice const StatsSlice = ({ stats }) => { - const localStats = stats; + const delays = stats?.map( + stat => String(stat.value).length * characterDelayMs + ); + let startDelay = 0; + return ( - {localStats?.map((stat, index) => ( - // eslint-disable-next-line react/no-array-index-key - - ))} + {stats?.map((stat, index) => { + const key = index + String(stat.value); + + const delay = delays[index]; + startDelay += delay; + + return ( + + ); + })} ); }; - StatsSlice.propTypes = { - stats: PropTypes.arrayOf(PropTypes.shape(StatPropTypes)) + stats: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) + .isRequired, + delay: PropTypes.number, + description: PropTypes.string + }) + ) }; -// MARK: individual stat -function StatComponent({ value, description, delay }) { - return ( - - - - - - - - {description} - - ); -} - -StatComponent.propTypes = StatPropTypes; - -function getValueType(value) { - return Number.isNaN(parseInt(value, 10)) ? 'string' : 'number'; +/** + * check whether a string character is a number or a string + * @param {string} character + * @returns {'string' | 'number'} + */ +function getValueType(character) { + return Number.isNaN(parseInt(character, 10)) ? 'string' : 'number'; } -// MARK: string -function AnimatedText({ value, delay }) { - const totalDelay = String(value).length * 100; - - const characters = String(value) +// MARK: individual stat +function StatComponent({ + value, + delay, + startDelay, + description +}) { + // split the value into characters and get the type of each character + const characters = value .split('') - .map((character) => { + .map(character => { const type = getValueType(character); return { character, @@ -77,47 +83,56 @@ function AnimatedText({ value, delay }) { }); return ( -
- {characters.map(({ character, type }, index) => { - const key = index + character; - const characterDelay = delay + (totalDelay - index * 100); - switch (type) { - case 'string': - return ( - - ); - case 'number': - return ( - - ); - default: - return null; - } - })} -
+ + + + {characters.map(({ character, type }, index) => { + const key = index + character; + const characterDelay = startDelay + (delay - index * characterDelayMs); + switch (type) { + case 'string': + return ( + + ); + case 'number': + return ( + + ); + default: + return null; + } + })} + + + + {description} + ); } - -AnimatedText.propTypes = { +StatComponent.propTypes = { value: PropTypes.string.isRequired, - delay: PropTypes.number + delay: PropTypes.number, + startDelay: PropTypes.number, + description: PropTypes.string }; -// MARK: string char +// MARK: string function AnimatedString({ value, delay }) { const digitRef = useRef(null); useEffect(() => { - const transform = `translateY(-50%)`; - digitRef.current?.style.setProperty('transform', transform); + digitRef.current?.style.setProperty('transform', 'translateY(-50%)'); }, [value]); // TODO add more characters? @@ -132,13 +147,12 @@ function AnimatedString({ value, delay }) {
); } - AnimatedString.propTypes = { value: PropTypes.string.isRequired, delay: PropTypes.number }; -// MARK: numeric char +// MARK: number function AnimatedNumber({ value, delay }) { const digitRef = useRef(null); @@ -166,9 +180,8 @@ function AnimatedNumber({ value, delay }) {
); } - AnimatedNumber.propTypes = { - value: PropTypes.number.isRequired, + value: PropTypes.string.isRequired, delay: PropTypes.number }; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index 81088f76f..78173ed53 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -10,7 +10,7 @@ ```js ``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index da8a5aa02..7265f9c7e 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -1,4 +1,4 @@ -import styled from 'styled-components'; +import styled, { keyframes } from 'styled-components'; export const OuterWrapper = styled.div` padding: 2rem; @@ -36,13 +36,26 @@ export const ValueContainer = styled.div` padding-bottom: 1rem; `; +const clipIn = keyframes` + 0% { + clip-path: inset(0px 100% 0px 0px); + } + 100% { + clip-path: inset(0px 0% 0px 0px); + } +`; + export const ValueUnderline = styled.img` position: absolute; width: 100%; height: 4px; left: 0; bottom: 0px; - clip-path: inset(0px 50% 0px 0px); + animation-name: ${clipIn}; + animation-duration: 0.7s; + animation-timing-function: cubic-bezier(0.219, -0.011, 0.164, 0.987); + animation-delay: ${({ delay }) => delay}ms; + animation-fill-mode: both; `; export const StatValue = styled.div` @@ -69,5 +82,5 @@ export const AnimatedDigit = styled.div` transition-delay: var(--delay); transition-property: transform; transition-duration: 2s; - transition-timing-function: linear(0, 0.192 4.1%, 0.361 8.2%, 0.513 12.5%, 0.645 16.9%, 0.703 19.1%, 0.758 21.4%, 0.808 23.7%, 0.854 26.1%, 0.895 28.5%, 0.932 31%, 0.964 33.5%, 0.993 36.1%, 1.013 38.3%, 1.031 40.6%, 1.046 42.9%, 1.058 45.3%, 1.067 47.8%, 1.074 50.3%, 1.078 53%, 1.08 55.8%, 1.077 60.4%, 1.069 65.5%, 1.057 70.4%, 1.024 82.4%, 1.01 88.7%, 1.002 94.4%, 1); + transition-timing-function: linear(0, 0.329 8.8%, 0.59 18%, 0.787 27.7%, 0.863 32.8%, 0.926 38.2%, 0.968 43.1%, 1 48.3%, 1.022 53.7%, 1.034 59.6%, 1.035 69.8%, 1.006 90.7%, 1); `; From f04fcd3a52b1a092dc80527b349938322ac7390b Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 25 Feb 2026 11:44:36 +0000 Subject: [PATCH 07/20] fix: formatting styles --- .../Molecules/StatsSlice/StatsSlice.style.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 7265f9c7e..07fbc5ec5 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -75,12 +75,26 @@ export const StatValue = styled.div` `; export const AnimatedDigit = styled.div` - position: absolute; - top: 0; - left: 0; + position: absolute; + top: 0; + left: 0; transition-delay: var(--delay); transition-property: transform; transition-duration: 2s; - transition-timing-function: linear(0, 0.329 8.8%, 0.59 18%, 0.787 27.7%, 0.863 32.8%, 0.926 38.2%, 0.968 43.1%, 1 48.3%, 1.022 53.7%, 1.034 59.6%, 1.035 69.8%, 1.006 90.7%, 1); + transition-timing-function: linear( + 0, + 0.329 8.8%, + 0.59 18%, + 0.787 27.7%, + 0.863 32.8%, + 0.926 38.2%, + 0.968 43.1%, + 1 48.3%, + 1.022 53.7%, + 1.034 59.6%, + 1.035 69.8%, + 1.006 90.7%, + 1 + ); `; From fce81509e95fc640b208193f781f48a59e84e7fb Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 25 Feb 2026 13:20:00 +0000 Subject: [PATCH 08/20] fixing delay --- src/components/Molecules/StatsSlice/StatsSlice.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 0d22ae323..2740e2689 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -26,11 +26,8 @@ const StatsSlice = ({ stats }) => { {stats?.map((stat, index) => { const key = index + String(stat.value); - const delay = delays[index]; - startDelay += delay; - - return ( + const statComponent = ( { description={stat.description} /> ); + // update the start delay for the next stat + startDelay += delay; + return statComponent; })} From d91e89fda5a0f6b393ba0669fa19149345a36a8a Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Fri, 27 Feb 2026 16:24:37 +0000 Subject: [PATCH 09/20] props and behaviour tweaks from CMS integration --- .../Molecules/StatsSlice/StatsSlice.js | 237 +++++------------- .../Molecules/StatsSlice/StatsSlice.md | 26 +- .../Molecules/StatsSlice/StatsSlice.style.js | 26 +- .../Molecules/StatsSlice/_StatNode.js | 152 +++++++++++ src/components/Molecules/StatsSlice/_utils.js | 3 + 5 files changed, 262 insertions(+), 182 deletions(-) create mode 100644 src/components/Molecules/StatsSlice/_StatNode.js create mode 100644 src/components/Molecules/StatsSlice/_utils.js diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 2740e2689..f468c7285 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -1,188 +1,89 @@ -import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; +import React, { useEffect, useRef, useState } from 'react'; +import StatNodeComponent from './_StatNode'; import { - OuterWrapper, InnerWrapper, - StatContainer, - StatValue, - AnimatedDigit, - ValueContainer, - ValueUnderline + OuterWrapper } from './StatsSlice.style'; -import Text from '../../Atoms/Text/Text'; -import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; +import StatContext from './_utils'; -const characterDelayMs = 100; +// stagger between characters in milliseconds +const characterStagger = 100; // MARK: stats slice -const StatsSlice = ({ stats }) => { - const delays = stats?.map( - stat => String(stat.value).length * characterDelayMs +const StatsSlice = ({ + nodes, + pageBackgroundColour = 'teal_dark', + paddingTop = '2rem', + paddingBottom = '2rem' +}) => { + // calculate the duration for each stat + // based on the number of characters and the stagger + const durations = nodes?.map( + node => String(node.stat).length * characterStagger ); let startDelay = 0; + // use an intersection observer to delay the animation + // until the stats slice is visible + const elRef = useRef(); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + setIsVisible(true); + observer.disconnect(); + } + }); + // threshold 0.5 = wait until 50% of the element is visible + // delay 500ms = wait 500ms before starting the animation + }, { threshold: 0.5, delay: 250 }); + observer.observe(elRef.current); + }, []); + return ( - - - {stats?.map((stat, index) => { - const key = index + String(stat.value); - const delay = delays[index]; - const statComponent = ( - - ); - // update the start delay for the next stat - startDelay += delay; - return statComponent; - })} - + + + + {nodes?.map((node, index) => { + const key = index + String(node.title); + const duration = durations[index]; + const nodeComponent = ( + + ); + // update the start delay for the next stat + startDelay += duration; + return nodeComponent; + })} + + ); }; StatsSlice.propTypes = { - stats: PropTypes.arrayOf( + nodes: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]) - .isRequired, - delay: PropTypes.number, - description: PropTypes.string + title: PropTypes.string.isRequired, + stat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + body: PropTypes.node }) - ) -}; - -/** - * check whether a string character is a number or a string - * @param {string} character - * @returns {'string' | 'number'} - */ -function getValueType(character) { - return Number.isNaN(parseInt(character, 10)) ? 'string' : 'number'; -} - -// MARK: individual stat -function StatComponent({ - value, - delay, - startDelay, - description -}) { - // split the value into characters and get the type of each character - const characters = value - .split('') - .map(character => { - const type = getValueType(character); - return { - character, - type - }; - }); - - return ( - - - - {characters.map(({ character, type }, index) => { - const key = index + character; - const characterDelay = startDelay + (delay - index * characterDelayMs); - switch (type) { - case 'string': - return ( - - ); - case 'number': - return ( - - ); - default: - return null; - } - })} - - - - {description} - - ); -} -StatComponent.propTypes = { - value: PropTypes.string.isRequired, - delay: PropTypes.number, - startDelay: PropTypes.number, - description: PropTypes.string -}; - -// MARK: string -function AnimatedString({ value, delay }) { - const digitRef = useRef(null); - - useEffect(() => { - digitRef.current?.style.setProperty('transform', 'translateY(-50%)'); - }, [value]); - - // TODO add more characters? - - return ( -
- {value} - -
 
-
{value}
-
-
- ); -} -AnimatedString.propTypes = { - value: PropTypes.string.isRequired, - delay: PropTypes.number -}; - -// MARK: number -function AnimatedNumber({ value, delay }) { - const digitRef = useRef(null); - - useEffect(() => { - const transform = `translateY(-${(100 / 11) * (+value || 10)}%)`; - digitRef.current?.style.setProperty('transform', transform); - }, [value]); - - return ( -
- {value} - -
 
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
0
-
-
- ); -} -AnimatedNumber.propTypes = { - value: PropTypes.string.isRequired, - delay: PropTypes.number + ), + pageBackgroundColour: PropTypes.string, + paddingTop: PropTypes.string, + paddingBottom: PropTypes.string }; export default StatsSlice; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index 78173ed53..aaff66d5d 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -3,31 +3,37 @@ ### Empty stats ```js +
+
``` ### Basic stats ```js - ``` ### Multiple stats ```js - ``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 07fbc5ec5..b1fb312b9 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -1,8 +1,13 @@ import styled, { keyframes } from 'styled-components'; export const OuterWrapper = styled.div` - padding: 2rem; - background: #eeeeee; + padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`}; + background: ${({ theme, pageBackgroundColour }) => theme.color(pageBackgroundColour)}; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + padding-left: 2rem; + padding-right: 2rem; + } `; export const InnerWrapper = styled.div` @@ -74,14 +79,27 @@ export const StatValue = styled.div` } `; +export const AnimatedCharacter = styled.div` + position: relative; + overflow: hidden; + // small amount of extra padding to avoid descending characters like "," from being cut off; + // we shouldn't need to worry about longer descenders like "g" as we're using caps + padding-bottom: 0.06rem; +`; + +export const SpacingCharacter = styled.div` + visibility: hidden; + white-space: pre; +`; + export const AnimatedDigit = styled.div` position: absolute; top: 0; left: 0; - transition-delay: var(--delay); + transition-delay: ${({ delay }) => delay}ms; transition-property: transform; - transition-duration: 2s; + transition-duration: ${({ duration }) => duration}ms; transition-timing-function: linear( 0, 0.329 8.8%, diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js new file mode 100644 index 000000000..dc3eece82 --- /dev/null +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -0,0 +1,152 @@ +import PropTypes from 'prop-types'; +import React, { useRef, useContext } from 'react'; +import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; +import Text from '../../Atoms/Text/Text'; +import { + AnimatedDigit, + StatContainer, + StatValue, + ValueContainer, + ValueUnderline, + AnimatedCharacter, + SpacingCharacter + +} from './StatsSlice.style'; +import StatContext from './_utils'; + +/** + * check whether a string character is a number or a string + * @param {string} character + * @returns {'string' | 'number'} + */ +function getValueType(character) { + return Number.isNaN(parseInt(character, 10)) ? 'string' : 'number'; +} + +// MARK: stat +export default function StatNodeComponent({ + stat, + duration, + startDelay, + characterStagger, + body +}) { + const isVisible = useContext(StatContext); + + // split the value into characters and get the type of each character + const characters = stat.split('').map(character => { + const type = getValueType(character); + return { + character, + type + }; + }); + + return ( + + + + {characters.map(({ character, type }, index) => { + const key = index + character; + const characterDelay = startDelay + (duration - index * characterStagger); + switch (type) { + case 'string': + return ( + + ); + case 'number': + return ( + + ); + default: + return null; + } + })} + + {isVisible && ( + + )} + + {body} + + ); +} +StatNodeComponent.propTypes = { + stat: PropTypes.string.isRequired, + duration: PropTypes.number, + startDelay: PropTypes.number, + characterStagger: PropTypes.number, + body: PropTypes.string +}; + +// MARK: string +function AnimatedStringCharacter({ character, delay }) { + const isVisible = useContext(StatContext); + const digitRef = useRef(); + + if (isVisible) { + digitRef.current?.style.setProperty('transform', 'translateY(-50%)'); + } + + // TODO add more characters? + + return ( + + {character} + +
 
+
{character}
+
+
+ ); +} +AnimatedStringCharacter.propTypes = { + character: PropTypes.string.isRequired, + delay: PropTypes.number +}; + +// MARK: number +function AnimatedNumberCharacter({ character, delay }) { + const isVisible = useContext(StatContext); + const digitRef = useRef(); + + if (isVisible) { + // calculate offset to show the required digit + const transform = `translateY(-${(100 / 11) * (+character || 10)}%)`; + digitRef.current?.style.setProperty('transform', transform); + } + + return ( + + {character} + +
 
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
+
+ ); +} +AnimatedNumberCharacter.propTypes = { + character: PropTypes.string.isRequired, + delay: PropTypes.number +}; diff --git a/src/components/Molecules/StatsSlice/_utils.js b/src/components/Molecules/StatsSlice/_utils.js new file mode 100644 index 000000000..1c4b9d4bf --- /dev/null +++ b/src/components/Molecules/StatsSlice/_utils.js @@ -0,0 +1,3 @@ +import React from 'react'; + +export default React.createContext(false); From b0e577c94e26eb7411c71252b596d9eab8c49a69 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Mon, 2 Mar 2026 11:19:52 +0000 Subject: [PATCH 10/20] adding accessible stats --- .../Molecules/StatsSlice/StatsSlice.style.js | 24 +++++++++++++++---- .../Molecules/StatsSlice/_StatNode.js | 6 +++-- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index b1fb312b9..9054d8dde 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -2,7 +2,7 @@ import styled, { keyframes } from 'styled-components'; export const OuterWrapper = styled.div` padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`}; - background: ${({ theme, pageBackgroundColour }) => theme.color(pageBackgroundColour)}; + background: ${({ theme, backgroundColour }) => theme.color(backgroundColour)}; @media ${({ theme }) => theme.breakpoints2026('M')} { padding-left: 2rem; @@ -15,7 +15,7 @@ export const InnerWrapper = styled.div` flex-direction: column; gap: 1rem; - @media ${({ theme }) => theme.breakpoints2026('M')} { + @media ${({ theme }) => theme.breakpoints2026('L')} { flex-direction: row; gap: 2rem; } @@ -27,9 +27,13 @@ export const StatContainer = styled.div` align-items: flex-start; gap: 1rem; background: #ffffff; - padding: 2rem; + padding: 1rem; flex: 1; border-radius: 1rem; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + padding: 2rem; + } `; export const ValueContainer = styled.div` @@ -67,11 +71,11 @@ export const StatValue = styled.div` display: flex; text-transform: uppercase; font-family: ${({ theme }) => theme.fontFamilies('Anton')}; - font-size: ${({ theme }) => theme.fontSize('l')}; + font-size: ${({ theme }) => theme.fontSize('xl')}; line-height: 1; @media ${({ theme }) => theme.breakpoints2026('M')} { - font-size: ${({ theme }) => theme.fontSize('xl')}; + font-size: ${({ theme }) => theme.fontSize('xxl')}; } @media ${({ theme }) => theme.breakpoints2026('L')} { @@ -116,3 +120,13 @@ export const AnimatedDigit = styled.div` 1 ); `; + +export const AccessibleValue = styled.div` + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: 1; + opacity: 0; +`; diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index dc3eece82..deb4cd30a 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -9,7 +9,8 @@ import { ValueContainer, ValueUnderline, AnimatedCharacter, - SpacingCharacter + SpacingCharacter, + AccessibleValue } from './StatsSlice.style'; import StatContext from './_utils'; @@ -45,7 +46,7 @@ export default function StatNodeComponent({ return ( - + {body} From b259ba72670443a4dc74205d92d01f8f1d4c18c7 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Mon, 2 Mar 2026 16:06:20 +0000 Subject: [PATCH 11/20] adding animation options --- .../Molecules/StatsSlice/StatsSlice.js | 10 ++++-- .../Molecules/StatsSlice/StatsSlice.style.js | 16 +++++++-- .../Molecules/StatsSlice/_StatNode.js | 34 +++++++++++++++---- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index f468c7285..b0c17bcd6 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -15,7 +15,9 @@ const StatsSlice = ({ nodes, pageBackgroundColour = 'teal_dark', paddingTop = '2rem', - paddingBottom = '2rem' + paddingBottom = '2rem', + ease = 'cubic', + characterDuration = '2000ms' }) => { // calculate the duration for each stat // based on the number of characters and the stagger @@ -61,6 +63,8 @@ const StatsSlice = ({ duration={duration} startDelay={startDelay} characterStagger={characterStagger} + characterDuration={characterDuration} + ease={ease} body={node.body} /> ); @@ -83,7 +87,9 @@ StatsSlice.propTypes = { ), pageBackgroundColour: PropTypes.string, paddingTop: PropTypes.string, - paddingBottom: PropTypes.string + paddingBottom: PropTypes.string, + ease: PropTypes.string, + characterDuration: PropTypes.string }; export default StatsSlice; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 9054d8dde..301393a2a 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -3,7 +3,7 @@ import styled, { keyframes } from 'styled-components'; export const OuterWrapper = styled.div` padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`}; background: ${({ theme, backgroundColour }) => theme.color(backgroundColour)}; - + @media ${({ theme }) => theme.breakpoints2026('M')} { padding-left: 2rem; padding-right: 2rem; @@ -103,8 +103,14 @@ export const AnimatedDigit = styled.div` transition-delay: ${({ delay }) => delay}ms; transition-property: transform; - transition-duration: ${({ duration }) => duration}ms; - transition-timing-function: linear( + transition-duration: ${({ duration }) => duration}; + + // easing functions from https://easingwizard.com/ + &[data-ease="cubic"] { + cubic-bezier(0.22, 1, 0.36, 1); + } + &[data-ease="overshoot"] { + transition-timing-function: linear( 0, 0.329 8.8%, 0.59 18%, @@ -119,6 +125,10 @@ export const AnimatedDigit = styled.div` 1.006 90.7%, 1 ); + } + &[data-ease="bounce"] { + transition-timing-function: linear(0, 0.384 15.4%, 0.833 35.8%, 1 44.7%, 0.919 51.5%, 0.9 54.7%, 0.894 58%, 0.911 63.8%, 1 77.4%, 0.985 84.4%, 1); + } `; export const AccessibleValue = styled.div` diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index deb4cd30a..6e47d414b 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -30,6 +30,8 @@ export default function StatNodeComponent({ duration, startDelay, characterStagger, + characterDuration, + ease, body }) { const isVisible = useContext(StatContext); @@ -57,6 +59,8 @@ export default function StatNodeComponent({ key={key} character={character} delay={characterDelay} + ease={ease} + characterDuration={characterDuration} /> ); case 'number': @@ -65,6 +69,8 @@ export default function StatNodeComponent({ key={key} character={character} delay={characterDelay} + ease={ease} + characterDuration={characterDuration} /> ); default: @@ -89,11 +95,18 @@ StatNodeComponent.propTypes = { duration: PropTypes.number, startDelay: PropTypes.number, characterStagger: PropTypes.number, + ease: PropTypes.string, + characterDuration: PropTypes.string, body: PropTypes.string }; // MARK: string -function AnimatedStringCharacter({ character, delay }) { +function AnimatedStringCharacter({ + character, + delay, + ease, + characterDuration +}) { const isVisible = useContext(StatContext); const digitRef = useRef(); @@ -106,7 +119,7 @@ function AnimatedStringCharacter({ character, delay }) { return ( {character} - +
 
{character}
@@ -115,11 +128,18 @@ function AnimatedStringCharacter({ character, delay }) { } AnimatedStringCharacter.propTypes = { character: PropTypes.string.isRequired, - delay: PropTypes.number + delay: PropTypes.number, + ease: PropTypes.string, + characterDuration: PropTypes.string }; // MARK: number -function AnimatedNumberCharacter({ character, delay }) { +function AnimatedNumberCharacter({ + character, + delay, + ease, + characterDuration +}) { const isVisible = useContext(StatContext); const digitRef = useRef(); @@ -132,7 +152,7 @@ function AnimatedNumberCharacter({ character, delay }) { return ( {character} - +
 
1
2
@@ -150,5 +170,7 @@ function AnimatedNumberCharacter({ character, delay }) { } AnimatedNumberCharacter.propTypes = { character: PropTypes.string.isRequired, - delay: PropTypes.number + delay: PropTypes.number, + ease: PropTypes.string, + characterDuration: PropTypes.string }; From 1eb23423c84ea7d55a2fb4336bc5d93a2d3332b9 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Tue, 3 Mar 2026 15:00:10 +0000 Subject: [PATCH 12/20] adding "none" animation option --- .../Molecules/StatsSlice/StatsSlice.js | 7 +++--- .../Molecules/StatsSlice/StatsSlice.style.js | 23 ++++++++++++++++++- .../Molecules/StatsSlice/_StatNode.js | 4 +--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index b0c17bcd6..7d8816c04 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -8,7 +8,7 @@ import { import StatContext from './_utils'; // stagger between characters in milliseconds -const characterStagger = 100; +const characterStagger = 80; // MARK: stats slice const StatsSlice = ({ @@ -17,7 +17,7 @@ const StatsSlice = ({ paddingTop = '2rem', paddingBottom = '2rem', ease = 'cubic', - characterDuration = '2000ms' + characterDuration = '1600ms' }) => { // calculate the duration for each stat // based on the number of characters and the stagger @@ -40,8 +40,7 @@ const StatsSlice = ({ } }); // threshold 0.5 = wait until 50% of the element is visible - // delay 500ms = wait 500ms before starting the animation - }, { threshold: 0.5, delay: 250 }); + }, { threshold: 0.5 }); observer.observe(elRef.current); }, []); diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 301393a2a..33fd5ac02 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -42,7 +42,7 @@ export const ValueContainer = styled.div` flex-direction: column; align-items: flex-start; gap: 0.5rem; - padding-bottom: 1rem; + padding-bottom: 0.5rem; `; const clipIn = keyframes` @@ -65,6 +65,17 @@ export const ValueUnderline = styled.img` animation-timing-function: cubic-bezier(0.219, -0.011, 0.164, 0.987); animation-delay: ${({ delay }) => delay}ms; animation-fill-mode: both; + + // ease = none and reduced motion both disable the transition + + @media (prefers-reduced-motion: reduce) { + animation-delay: 0; + animation-name: none; + } + &[data-ease="none"] { + animation-delay: 0; + animation-name: none; + } `; export const StatValue = styled.div` @@ -105,6 +116,16 @@ export const AnimatedDigit = styled.div` transition-property: transform; transition-duration: ${({ duration }) => duration}; + // ease = none and reduced motion both disable the transition + @media (prefers-reduced-motion: reduce) { + transition-property: none; + transition-delay: 0; + } + &[data-ease="none"] { + transition-property: none; + transition-delay: 0; + } + // easing functions from https://easingwizard.com/ &[data-ease="cubic"] { cubic-bezier(0.22, 1, 0.36, 1); diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index 6e47d414b..c2f13071c 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -11,7 +11,6 @@ import { AnimatedCharacter, SpacingCharacter, AccessibleValue - } from './StatsSlice.style'; import StatContext from './_utils'; @@ -82,6 +81,7 @@ export default function StatNodeComponent({ )} {stat} @@ -114,8 +114,6 @@ function AnimatedStringCharacter({ digitRef.current?.style.setProperty('transform', 'translateY(-50%)'); } - // TODO add more characters? - return ( {character} From 259335997566a0a78c77f8a59059bad59f5bd784 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 10:56:39 +0000 Subject: [PATCH 13/20] multiples updates from design feedbnack - font sizes - shadows - animation timings - "none" animation option - text wrapping for stats --- .../Molecules/StatsSlice/StatsSlice.js | 78 +++----- .../Molecules/StatsSlice/StatsSlice.md | 14 +- .../Molecules/StatsSlice/StatsSlice.style.js | 35 +++- .../Molecules/StatsSlice/_StatNode.js | 169 ++++++++++++------ src/components/Molecules/StatsSlice/_utils.js | 3 - 5 files changed, 184 insertions(+), 115 deletions(-) delete mode 100644 src/components/Molecules/StatsSlice/_utils.js diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 7d8816c04..82efe9e01 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -1,11 +1,11 @@ +import { isEmpty } from 'lodash'; import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; +import React from 'react'; import StatNodeComponent from './_StatNode'; import { InnerWrapper, OuterWrapper } from './StatsSlice.style'; -import StatContext from './_utils'; // stagger between characters in milliseconds const characterStagger = 80; @@ -17,32 +17,13 @@ const StatsSlice = ({ paddingTop = '2rem', paddingBottom = '2rem', ease = 'cubic', - characterDuration = '1600ms' + stringCharacterDuration = '1600ms', + numberCharacterDuration = '2000ms' }) => { - // calculate the duration for each stat - // based on the number of characters and the stagger - const durations = nodes?.map( - node => String(node.stat).length * characterStagger - ); - let startDelay = 0; - - // use an intersection observer to delay the animation - // until the stats slice is visible - const elRef = useRef(); - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - const observer = new IntersectionObserver(entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - setIsVisible(true); - observer.disconnect(); - } - }); - // threshold 0.5 = wait until 50% of the element is visible - }, { threshold: 0.5 }); - observer.observe(elRef.current); - }, []); + // if no nodes have been provided, don't render anything + if (isEmpty(nodes)) { + return null; + } return ( - - - {nodes?.map((node, index) => { - const key = index + String(node.title); - const duration = durations[index]; - const nodeComponent = ( - - ); - // update the start delay for the next stat - startDelay += duration; - return nodeComponent; - })} - - + + {nodes?.map((node, index) => { + const key = index + String(node.title); + const nodeComponent = ( + + ); + return nodeComponent; + })} + ); }; @@ -88,7 +63,8 @@ StatsSlice.propTypes = { paddingTop: PropTypes.string, paddingBottom: PropTypes.string, ease: PropTypes.string, - characterDuration: PropTypes.string + stringCharacterDuration: PropTypes.string, + numberCharacterDuration: PropTypes.string }; export default StatsSlice; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index aaff66d5d..46a998e06 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -28,12 +28,22 @@ }, { title: "456", - stat: "456 people", + stat: "over 456 lives were saved", body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam." }, { title: "789", - stat: "over $800bn", + stat: "456,789,123 people who", + body: "Ut enim ad minima veniam, quis nostrum exercitationem." +}, +{ + title: "asd", + stat: "456 people", + body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam." +}, +{ + title: "xcv", + stat: "$800bn", body: "Ut enim ad minima veniam, quis nostrum exercitationem." }]} /> ``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 33fd5ac02..d6a956628 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -1,4 +1,5 @@ import styled, { keyframes } from 'styled-components'; +import Text from '../../Atoms/Text/Text'; export const OuterWrapper = styled.div` padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`}; @@ -13,7 +14,9 @@ export const OuterWrapper = styled.div` export const InnerWrapper = styled.div` display: flex; flex-direction: column; + flex-wrap: wrap; gap: 1rem; + max-width: 1152px; @media ${({ theme }) => theme.breakpoints2026('L')} { flex-direction: row; @@ -28,8 +31,10 @@ export const StatContainer = styled.div` gap: 1rem; background: #ffffff; padding: 1rem; - flex: 1; + flex: 1 1 0px; + min-width: 30%; border-radius: 1rem; + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 1rem; @media ${({ theme }) => theme.breakpoints2026('M')} { padding: 2rem; @@ -80,6 +85,8 @@ export const ValueUnderline = styled.img` export const StatValue = styled.div` display: flex; + flex-wrap: wrap; + gap: 0.5rem; text-transform: uppercase; font-family: ${({ theme }) => theme.fontFamilies('Anton')}; font-size: ${({ theme }) => theme.fontSize('xl')}; @@ -94,6 +101,10 @@ export const StatValue = styled.div` } `; +export const Word = styled.span` + display: flex; +`; + export const AnimatedCharacter = styled.div` position: relative; overflow: hidden; @@ -104,7 +115,7 @@ export const AnimatedCharacter = styled.div` export const SpacingCharacter = styled.div` visibility: hidden; - white-space: pre; + white-space: pre-wrap; `; export const AnimatedDigit = styled.div` @@ -160,4 +171,24 @@ export const AccessibleValue = styled.div` top: 0; z-index: 1; opacity: 0; + text-transform: uppercase; + font-family: ${({ theme }) => theme.fontFamilies('Anton')}; + font-size: ${({ theme }) => theme.fontSize('xl')}; + line-height: 1; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + font-size: ${({ theme }) => theme.fontSize('xxl')}; + } + + @media ${({ theme }) => theme.breakpoints2026('L')} { + font-size: ${({ theme }) => theme.fontSize('xxl')}; + } +`; + +export const Body = styled(Text)` + font-size: ${({ theme }) => theme.fontSize('s')}; + + @media ${({ theme }) => theme.breakpoints2026('M')} { + font-size: ${({ theme }) => theme.fontSize('m')}; + } `; diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index c2f13071c..13c57d2cc 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -1,18 +1,18 @@ import PropTypes from 'prop-types'; -import React, { useRef, useContext } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; -import Text from '../../Atoms/Text/Text'; import { + AccessibleValue, + AnimatedCharacter, AnimatedDigit, + Body, + SpacingCharacter, StatContainer, StatValue, ValueContainer, ValueUnderline, - AnimatedCharacter, - SpacingCharacter, - AccessibleValue + Word } from './StatsSlice.style'; -import StatContext from './_utils'; /** * check whether a string character is a number or a string @@ -26,77 +26,130 @@ function getValueType(character) { // MARK: stat export default function StatNodeComponent({ stat, - duration, - startDelay, characterStagger, - characterDuration, + stringCharacterDuration, + numberCharacterDuration, ease, body }) { - const isVisible = useContext(StatContext); + // calculate the duration for the animation + // based on the number of characters and the stagger; + // limit the duration to 1600ms to avoid overly long animations + const duration = Math.min(String(stat).length, 16) * characterStagger; + + // use an intersection observer to postpone the animation until the stats slice is visible + const elRef = useRef(); + const [isVisible, setIsVisible] = useState(false); + const startDelayRef = useRef(0); + + useEffect(() => { + const observer = new IntersectionObserver(entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + // set the start delay based on the x position of the element; + // this lets us delay stats incrementally, showing columns one after another (ish), + // while allowing rows to control their own entry times + startDelayRef.current = (entry.boundingClientRect.left / window.innerWidth) * 1600; + setIsVisible(true); + observer.disconnect(); + } + }); + }, { threshold: 0 }); + + if (elRef.current) { + observer.observe(elRef.current); + } - // split the value into characters and get the type of each character - const characters = stat.split('').map(character => { - const type = getValueType(character); - return { - character, - type - }; - }); + return () => observer.disconnect(); + }, []); + + // split value into words and characters, + // and get the type of each character; + // we animate string and numeric values differently + const words = stat + .split(' ') + .map(word => word.split('').map(character => { + const type = getValueType(character); + return { + word, + character, + type + }; + })); + + // track delay for each character for a staggered effect + let characterDelay = startDelayRef.current; return ( - - {body} + {body} ); } StatNodeComponent.propTypes = { stat: PropTypes.string.isRequired, - duration: PropTypes.number, - startDelay: PropTypes.number, characterStagger: PropTypes.number, ease: PropTypes.string, - characterDuration: PropTypes.string, + stringCharacterDuration: PropTypes.string, + numberCharacterDuration: PropTypes.string, body: PropTypes.string }; @@ -105,9 +158,9 @@ function AnimatedStringCharacter({ character, delay, ease, - characterDuration + characterDuration, + isVisible }) { - const isVisible = useContext(StatContext); const digitRef = useRef(); if (isVisible) { @@ -128,7 +181,8 @@ AnimatedStringCharacter.propTypes = { character: PropTypes.string.isRequired, delay: PropTypes.number, ease: PropTypes.string, - characterDuration: PropTypes.string + characterDuration: PropTypes.string, + isVisible: PropTypes.bool }; // MARK: number @@ -136,9 +190,9 @@ function AnimatedNumberCharacter({ character, delay, ease, - characterDuration + characterDuration, + isVisible }) { - const isVisible = useContext(StatContext); const digitRef = useRef(); if (isVisible) { @@ -170,5 +224,6 @@ AnimatedNumberCharacter.propTypes = { character: PropTypes.string.isRequired, delay: PropTypes.number, ease: PropTypes.string, - characterDuration: PropTypes.string + characterDuration: PropTypes.string, + isVisible: PropTypes.bool }; diff --git a/src/components/Molecules/StatsSlice/_utils.js b/src/components/Molecules/StatsSlice/_utils.js deleted file mode 100644 index 1c4b9d4bf..000000000 --- a/src/components/Molecules/StatsSlice/_utils.js +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export default React.createContext(false); From a78e8675feac8de4084a6ec43a32a612a32dea3d Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 11:50:07 +0000 Subject: [PATCH 14/20] adding tests --- .../Molecules/StatsSlice/StatsSlice.md | 11 + .../Molecules/StatsSlice/StatsSlice.style.js | 34 +- .../Molecules/StatsSlice/StatsSlice.test.js | 38 + .../__snapshots__/StatsSlice.test.js.snap | 4059 +++++++++++++++++ .../Molecules/StatsSlice/_test-utils.js | 6 + 5 files changed, 4131 insertions(+), 17 deletions(-) create mode 100644 src/components/Molecules/StatsSlice/StatsSlice.test.js create mode 100644 src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap create mode 100644 src/components/Molecules/StatsSlice/_test-utils.js diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index 46a998e06..b9c888ae1 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -47,3 +47,14 @@ body: "Ut enim ad minima veniam, quis nostrum exercitationem." }]} /> ``` + + +### No animation + +```js + +``` diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index d6a956628..4739c366c 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -139,27 +139,27 @@ export const AnimatedDigit = styled.div` // easing functions from https://easingwizard.com/ &[data-ease="cubic"] { - cubic-bezier(0.22, 1, 0.36, 1); + transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1); } &[data-ease="overshoot"] { - transition-timing-function: linear( - 0, - 0.329 8.8%, - 0.59 18%, - 0.787 27.7%, - 0.863 32.8%, - 0.926 38.2%, - 0.968 43.1%, - 1 48.3%, - 1.022 53.7%, - 1.034 59.6%, - 1.035 69.8%, - 1.006 90.7%, - 1 - ); + transition-timing-function: linear( + 0, + 0.329 8.8%, + 0.59 18%, + 0.787 27.7%, + 0.863 32.8%, + 0.926 38.2%, + 0.968 43.1%, + 1 48.3%, + 1.022 53.7%, + 1.034 59.6%, + 1.035 69.8%, + 1.006 90.7%, + 1 + ); } &[data-ease="bounce"] { - transition-timing-function: linear(0, 0.384 15.4%, 0.833 35.8%, 1 44.7%, 0.919 51.5%, 0.9 54.7%, 0.894 58%, 0.911 63.8%, 1 77.4%, 0.985 84.4%, 1); + transition-timing-function: linear(0, 0.384 15.4%, 0.833 35.8%, 1 44.7%, 0.919 51.5%, 0.9 54.7%, 0.894 58%, 0.911 63.8%, 1 77.4%, 0.985 84.4%, 1); } `; diff --git a/src/components/Molecules/StatsSlice/StatsSlice.test.js b/src/components/Molecules/StatsSlice/StatsSlice.test.js new file mode 100644 index 000000000..f8623985c --- /dev/null +++ b/src/components/Molecules/StatsSlice/StatsSlice.test.js @@ -0,0 +1,38 @@ +import 'jest-styled-components'; +import React from 'react'; +import renderWithTheme from '../../../../tests/hoc/shallowWithTheme'; +import StatsSlice from './StatsSlice'; +import './_test-utils'; + +it('Stats Slices with no nodes should not render', () => { + const statsEl = renderWithTheme().toJSON(); + expect(statsEl).toBeNull(); +}); + +it('Stats Slices with a single node should render', () => { + const statsEl = renderWithTheme().toJSON(); + expect(statsEl).toMatchSnapshot(); +}); + +it('Stats Slices with multiple nodes should render', () => { + const statsEl = renderWithTheme().toJSON(); + expect(statsEl).toMatchSnapshot(); +}); \ No newline at end of file diff --git a/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap b/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap new file mode 100644 index 000000000..037a3b5d5 --- /dev/null +++ b/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap @@ -0,0 +1,4059 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Stats Slices with a single node should render 1`] = ` +.c23 { + font-family: 'Montserrat',Helvetica,Arial,sans-serif; + font-weight: 400; + text-transform: inherit; + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + font-size: 1rem; + line-height: 1.25rem; +} + +.c23 span { + font-size: inherit; + line-height: inherit; +} + +.c0 { + padding: 2rem 1rem 2rem; + background: #13767C; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 1rem; + max-width: 1152px; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + gap: 1rem; + background: #ffffff; + padding: 1rem; + -webkit-flex: 1 1 0px; + -ms-flex: 1 1 0px; + flex: 1 1 0px; + min-width: 30%; + border-radius: 1rem; + box-shadow: rgba(0,0,0,0.15) 0px 0px 1rem; +} + +.c3 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + gap: 0.5rem; + padding-bottom: 0.5rem; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 0.5rem; + text-transform: uppercase; + font-family: 'Anton',Impact,sans-serif; + font-size: 2rem; + line-height: 1; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6 { + position: relative; + overflow: hidden; + padding-bottom: 0.06rem; +} + +.c7 { + visibility: hidden; + white-space: pre-wrap; +} + +.c8 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 80ms; + transition-delay: 80ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c8[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c8[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c8[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c8[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c9 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 160ms; + transition-delay: 160ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c9[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c9[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c9[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c9[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c10 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 240ms; + transition-delay: 240ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c10[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c10[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c10[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c10[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c11 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 320ms; + transition-delay: 320ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c11[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c11[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c11[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c11[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c12 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 400ms; + transition-delay: 400ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c12[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c12[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c12[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c12[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c13 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 480ms; + transition-delay: 480ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c13[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c13[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c13[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c13[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c14 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 560ms; + transition-delay: 560ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c14[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c14[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c14[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c14[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c15 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 640ms; + transition-delay: 640ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c15[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c15[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c15[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c15[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c16 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 720ms; + transition-delay: 720ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c16[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c16[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c16[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c16[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c17 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 800ms; + transition-delay: 800ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c17[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c17[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c17[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c17[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c18 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 880ms; + transition-delay: 880ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c18[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c18[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c18[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c18[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c19 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 960ms; + transition-delay: 960ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c19[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c19[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c19[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c19[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c20 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 1040ms; + transition-delay: 1040ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c20[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c20[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c20[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c20[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c21 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 1120ms; + transition-delay: 1120ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c21[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c21[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c21[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c21[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c22 { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: 1; + opacity: 0; + text-transform: uppercase; + font-family: 'Anton',Impact,sans-serif; + font-size: 2rem; + line-height: 1; +} + +.c24 { + font-size: 1rem; +} + +@media (min-width:740px) { + .c23 { + font-size: 1rem; + line-height: 1.25rem; + } +} + +@media (min-width:1024px) { + .c23 { + font-size: 1.125rem; + line-height: 1.375rem; + } +} + +@media (min-width:740px) { + .c0 { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width:1024px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2rem; + } +} + +@media (min-width:740px) { + .c2 { + padding: 2rem; + } +} + +@media (min-width:740px) { + .c4 { + font-size: 3rem; + } +} + +@media (min-width:1024px) { + .c4 { + font-size: 3rem; + } +} + +@media (prefers-reduced-motion:reduce) { + .c8 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c9 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c10 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c11 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c12 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c13 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c14 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c15 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c16 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c17 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c18 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c19 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c20 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c21 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (min-width:740px) { + .c22 { + font-size: 3rem; + } +} + +@media (min-width:1024px) { + .c22 { + font-size: 3rem; + } +} + +@media (min-width:740px) { + .c24 { + font-size: 1.25rem; + } +} + +
+
+
+
+ +
+ 123,456,789.02 +
+
+ + hello + +
+
+
+`; + +exports[`Stats Slices with multiple nodes should render 1`] = ` +.c23 { + font-family: 'Montserrat',Helvetica,Arial,sans-serif; + font-weight: 400; + text-transform: inherit; + -webkit-letter-spacing: 0; + -moz-letter-spacing: 0; + -ms-letter-spacing: 0; + letter-spacing: 0; + font-size: 1rem; + line-height: 1.25rem; +} + +.c23 span { + font-size: inherit; + line-height: inherit; +} + +.c0 { + padding: 2rem 1rem 2rem; + background: #13767C; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 1rem; + max-width: 1152px; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + gap: 1rem; + background: #ffffff; + padding: 1rem; + -webkit-flex: 1 1 0px; + -ms-flex: 1 1 0px; + flex: 1 1 0px; + min-width: 30%; + border-radius: 1rem; + box-shadow: rgba(0,0,0,0.15) 0px 0px 1rem; +} + +.c3 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + gap: 0.5rem; + padding-bottom: 0.5rem; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + gap: 0.5rem; + text-transform: uppercase; + font-family: 'Anton',Impact,sans-serif; + font-size: 2rem; + line-height: 1; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c6 { + position: relative; + overflow: hidden; + padding-bottom: 0.06rem; +} + +.c7 { + visibility: hidden; + white-space: pre-wrap; +} + +.c8 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 80ms; + transition-delay: 80ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c8[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c8[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c8[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c8[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c9 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 160ms; + transition-delay: 160ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c9[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c9[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c9[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c9[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c10 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 240ms; + transition-delay: 240ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c10[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c10[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c10[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c10[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c11 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 320ms; + transition-delay: 320ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c11[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c11[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c11[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c11[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c12 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 400ms; + transition-delay: 400ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c12[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c12[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c12[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c12[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c13 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 480ms; + transition-delay: 480ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c13[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c13[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c13[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c13[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c14 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 560ms; + transition-delay: 560ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c14[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c14[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c14[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c14[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c15 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 640ms; + transition-delay: 640ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c15[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c15[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c15[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c15[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c16 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 720ms; + transition-delay: 720ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c16[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c16[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c16[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c16[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c17 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 800ms; + transition-delay: 800ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c17[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c17[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c17[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c17[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c18 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 880ms; + transition-delay: 880ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c18[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c18[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c18[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c18[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c19 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 960ms; + transition-delay: 960ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 1600ms; + transition-duration: 1600ms; +} + +.c19[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c19[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c19[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c19[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c20 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 1040ms; + transition-delay: 1040ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c20[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c20[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c20[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c20[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c21 { + position: absolute; + top: 0; + left: 0; + -webkit-transition-delay: 1120ms; + transition-delay: 1120ms; + -webkit-transition-property: -webkit-transform; + -webkit-transition-property: transform; + transition-property: transform; + -webkit-transition-duration: 2000ms; + transition-duration: 2000ms; +} + +.c21[data-ease="none"] { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; +} + +.c21[data-ease="cubic"] { + -webkit-transition-timing-function: cubic-bezier(0.22,1,0.36,1); + transition-timing-function: cubic-bezier(0.22,1,0.36,1); +} + +.c21[data-ease="overshoot"] { + -webkit-transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); + transition-timing-function: linear( 0,0.329 8.8%,0.59 18%,0.787 27.7%,0.863 32.8%,0.926 38.2%,0.968 43.1%,1 48.3%,1.022 53.7%,1.034 59.6%,1.035 69.8%,1.006 90.7%,1 ); +} + +.c21[data-ease="bounce"] { + -webkit-transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); + transition-timing-function: linear(0,0.384 15.4%,0.833 35.8%,1 44.7%,0.919 51.5%,0.9 54.7%,0.894 58%,0.911 63.8%,1 77.4%,0.985 84.4%,1); +} + +.c22 { + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: 1; + opacity: 0; + text-transform: uppercase; + font-family: 'Anton',Impact,sans-serif; + font-size: 2rem; + line-height: 1; +} + +.c24 { + font-size: 1rem; +} + +@media (min-width:740px) { + .c23 { + font-size: 1rem; + line-height: 1.25rem; + } +} + +@media (min-width:1024px) { + .c23 { + font-size: 1.125rem; + line-height: 1.375rem; + } +} + +@media (min-width:740px) { + .c0 { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width:1024px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2rem; + } +} + +@media (min-width:740px) { + .c2 { + padding: 2rem; + } +} + +@media (min-width:740px) { + .c4 { + font-size: 3rem; + } +} + +@media (min-width:1024px) { + .c4 { + font-size: 3rem; + } +} + +@media (prefers-reduced-motion:reduce) { + .c8 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c9 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c10 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c11 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c12 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c13 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c14 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c15 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c16 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c17 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c18 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c19 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c20 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (prefers-reduced-motion:reduce) { + .c21 { + -webkit-transition-property: none; + transition-property: none; + -webkit-transition-delay: 0; + transition-delay: 0; + } +} + +@media (min-width:740px) { + .c22 { + font-size: 3rem; + } +} + +@media (min-width:1024px) { + .c22 { + font-size: 3rem; + } +} + +@media (min-width:740px) { + .c24 { + font-size: 1.25rem; + } +} + +
+
+
+
+ +
+ 123,456,789.02 +
+
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. + +
+
+
+ +
+ 456,789,123.02 +
+
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. + +
+
+
+ +
+ 789,123,456.02 +
+
+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam. + +
+
+
+`; diff --git a/src/components/Molecules/StatsSlice/_test-utils.js b/src/components/Molecules/StatsSlice/_test-utils.js new file mode 100644 index 000000000..1f8df50f3 --- /dev/null +++ b/src/components/Molecules/StatsSlice/_test-utils.js @@ -0,0 +1,6 @@ +const intersectionObserverMock = () => ({ + observe: () => null +}); +window.IntersectionObserver = jest + .fn() + .mockImplementation(intersectionObserverMock); From 2d30b9eb76ab2d26001a0632a53c6bf84b5e470b Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 13:15:26 +0000 Subject: [PATCH 15/20] updating demo --- src/components/Molecules/StatsSlice/StatsSlice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.md b/src/components/Molecules/StatsSlice/StatsSlice.md index b9c888ae1..c6cd00de0 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.md +++ b/src/components/Molecules/StatsSlice/StatsSlice.md @@ -33,7 +33,7 @@ }, { title: "789", - stat: "456,789,123 people who", + stat: "456,789,123 people", body: "Ut enim ad minima veniam, quis nostrum exercitationem." }, { @@ -43,7 +43,7 @@ }, { title: "xcv", - stat: "$800bn", + stat: "$800bn raised", body: "Ut enim ad minima veniam, quis nostrum exercitationem." }]} /> ``` From 2b025d9029a1944f428cca304187bbcfa4ea7907 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 13:31:20 +0000 Subject: [PATCH 16/20] centre-aligning wrapper --- src/components/Molecules/StatsSlice/StatsSlice.style.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.style.js b/src/components/Molecules/StatsSlice/StatsSlice.style.js index 4739c366c..8bcd9dbef 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.style.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.style.js @@ -2,6 +2,8 @@ import styled, { keyframes } from 'styled-components'; import Text from '../../Atoms/Text/Text'; export const OuterWrapper = styled.div` + display: flex; + justify-content: center; padding: ${({ paddingTop, paddingBottom }) => `${paddingTop} 1rem ${paddingBottom}`}; background: ${({ theme, backgroundColour }) => theme.color(backgroundColour)}; From aa7637da9f353ab37575e0d9afc4be54c8c7db67 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 13:52:20 +0000 Subject: [PATCH 17/20] update body prop type --- src/components/Molecules/StatsSlice/_StatNode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index 13c57d2cc..fad8113d3 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -150,7 +150,7 @@ StatNodeComponent.propTypes = { ease: PropTypes.string, stringCharacterDuration: PropTypes.string, numberCharacterDuration: PropTypes.string, - body: PropTypes.string + body: PropTypes.node }; // MARK: string From a5a1ad8bc63c2b5651e6f428e0f689a6dee5312c Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 13:55:36 +0000 Subject: [PATCH 18/20] Update StatsSlice.test.js.snap --- .../__snapshots__/StatsSlice.test.js.snap | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap b/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap index 037a3b5d5..b379fc498 100644 --- a/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap +++ b/src/components/Molecules/StatsSlice/__snapshots__/StatsSlice.test.js.snap @@ -19,6 +19,14 @@ exports[`Stats Slices with a single node should render 1`] = ` } .c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; padding: 2rem 1rem 2rem; background: #13767C; } @@ -1444,6 +1452,14 @@ exports[`Stats Slices with multiple nodes should render 1`] = ` } .c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; padding: 2rem 1rem 2rem; background: #13767C; } From 3c342a86f07cadf626afae6b495f583b51c19503 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Wed, 4 Mar 2026 15:45:06 +0000 Subject: [PATCH 19/20] making characterStagger a prop --- src/components/Molecules/StatsSlice/StatsSlice.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Molecules/StatsSlice/StatsSlice.js b/src/components/Molecules/StatsSlice/StatsSlice.js index 82efe9e01..dcf6ede2e 100644 --- a/src/components/Molecules/StatsSlice/StatsSlice.js +++ b/src/components/Molecules/StatsSlice/StatsSlice.js @@ -7,9 +7,6 @@ import { OuterWrapper } from './StatsSlice.style'; -// stagger between characters in milliseconds -const characterStagger = 80; - // MARK: stats slice const StatsSlice = ({ nodes, @@ -17,6 +14,7 @@ const StatsSlice = ({ paddingTop = '2rem', paddingBottom = '2rem', ease = 'cubic', + characterStagger = 80, stringCharacterDuration = '1600ms', numberCharacterDuration = '2000ms' }) => { @@ -63,6 +61,7 @@ StatsSlice.propTypes = { paddingTop: PropTypes.string, paddingBottom: PropTypes.string, ease: PropTypes.string, + characterStagger: PropTypes.number, stringCharacterDuration: PropTypes.string, numberCharacterDuration: PropTypes.string }; From 3be8c03fe95b8b1509b80cdd5933c5dee3ee5165 Mon Sep 17 00:00:00 2001 From: Tom Evans Date: Thu, 5 Mar 2026 10:53:43 +0000 Subject: [PATCH 20/20] splitting out code into separate files --- .../Molecules/StatsSlice/_Characters.js | 88 +++++++++++ .../Molecules/StatsSlice/_StatNode.js | 148 +++--------------- src/components/Molecules/StatsSlice/_utils.js | 26 +++ 3 files changed, 140 insertions(+), 122 deletions(-) create mode 100644 src/components/Molecules/StatsSlice/_Characters.js create mode 100644 src/components/Molecules/StatsSlice/_utils.js diff --git a/src/components/Molecules/StatsSlice/_Characters.js b/src/components/Molecules/StatsSlice/_Characters.js new file mode 100644 index 000000000..a08d5c309 --- /dev/null +++ b/src/components/Molecules/StatsSlice/_Characters.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { useRef } from 'react'; +import { + AnimatedCharacter, + AnimatedDigit, + SpacingCharacter +} from './StatsSlice.style'; + +const propTypes = { + character: PropTypes.string.isRequired, + delay: PropTypes.number, + ease: PropTypes.string, + characterDuration: PropTypes.string, + isVisible: PropTypes.bool.isRequired +}; + +// MARK: string +export function AnimatedStringCharacter({ + character, + delay, + ease, + characterDuration, + isVisible +}) { + const digitRef = useRef(); + + if (isVisible) { + digitRef.current?.style.setProperty('transform', 'translateY(-50%)'); + } + + return ( + + {character} + +
 
+
{character}
+
+
+ ); +} +AnimatedStringCharacter.propTypes = propTypes; + +// MARK: number +export function AnimatedNumberCharacter({ + character, + delay, + ease, + characterDuration, + isVisible +}) { + const digitRef = useRef(); + + if (isVisible) { + // calculate offset to show the required digit + const transform = `translateY(-${(100 / 11) * (+character || 10)}%)`; + digitRef.current?.style.setProperty('transform', transform); + } + + return ( + + {character} + +
 
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
+
+ ); +} +AnimatedNumberCharacter.propTypes = propTypes; diff --git a/src/components/Molecules/StatsSlice/_StatNode.js b/src/components/Molecules/StatsSlice/_StatNode.js index fad8113d3..dc16e194b 100644 --- a/src/components/Molecules/StatsSlice/_StatNode.js +++ b/src/components/Molecules/StatsSlice/_StatNode.js @@ -3,25 +3,15 @@ import React, { useRef, useState, useEffect } from 'react'; import altCtaUnderline from '../../../theme/shared/assets/alt_cta_underline.svg'; import { AccessibleValue, - AnimatedCharacter, - AnimatedDigit, Body, - SpacingCharacter, StatContainer, StatValue, ValueContainer, ValueUnderline, Word } from './StatsSlice.style'; - -/** - * check whether a string character is a number or a string - * @param {string} character - * @returns {'string' | 'number'} - */ -function getValueType(character) { - return Number.isNaN(parseInt(character, 10)) ? 'string' : 'number'; -} +import splitStatString from './_utils'; +import { AnimatedStringCharacter, AnimatedNumberCharacter } from './_Characters'; // MARK: stat export default function StatNodeComponent({ @@ -66,16 +56,7 @@ export default function StatNodeComponent({ // split value into words and characters, // and get the type of each character; // we animate string and numeric values differently - const words = stat - .split(' ') - .map(word => word.split('').map(character => { - const type = getValueType(character); - return { - word, - character, - type - }; - })); + const wordObjList = splitStatString(stat); // track delay for each character for a staggered effect let characterDelay = startDelayRef.current; @@ -84,7 +65,7 @@ export default function StatNodeComponent({