diff --git a/package.json b/package.json
index c61763532..95606ab19 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "safe-homepage",
"homepage": "https://github.com/safe-global/safe-homepage",
- "version": "1.4.41",
+ "version": "1.4.42",
"scripts": {
"build": "next build && next export",
"lint": "next lint",
diff --git a/public/images/Header/safe-foundry-icon.svg b/public/images/Header/safe-foundry-icon.svg
new file mode 100644
index 000000000..988c08647
--- /dev/null
+++ b/public/images/Header/safe-foundry-icon.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/images/safe-foundry-logo.png b/public/images/safe-foundry-logo.png
new file mode 100644
index 000000000..ddc7a5d68
Binary files /dev/null and b/public/images/safe-foundry-logo.png differ
diff --git a/src/components/Foundry/Hero/index.tsx b/src/components/Foundry/Hero/index.tsx
new file mode 100644
index 000000000..7afc75985
--- /dev/null
+++ b/src/components/Foundry/Hero/index.tsx
@@ -0,0 +1,53 @@
+import { type BaseBlockEntry } from '@/config/types'
+import RichText from '@/components/common/RichText'
+import ButtonsWrapper from '@/components/Token/ButtonsWrapper'
+import { isAsset, isEntryTypeButton } from '@/lib/typeGuards'
+import { Container, Typography } from '@mui/material'
+import { useIsMediumScreen } from '@/hooks/useMaxWidth'
+import css from './styles.module.css'
+
+const Hero = (props: BaseBlockEntry) => {
+ const isMediumScreen = useIsMediumScreen()
+ const { title, text, buttons, image, bgImage } = props.fields
+
+ const buttonsList = buttons?.filter(isEntryTypeButton) || []
+
+ const imageURL = isAsset(image) && image.fields.file?.url ? image.fields.file.url : ''
+ const bgImageURL = isAsset(bgImage) && bgImage.fields.file?.url ? bgImage.fields.file.url : ''
+
+ return (
+
+
+
+
+
+ {/* Networks image does not show in smaller resolutions */}
+
+
+
+
+
+
+
+
+
+ {text && (
+
+
+
+ )}
+
+ {buttonsList.length > 0 && (
+
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+export default Hero
diff --git a/src/components/Foundry/Hero/styles.module.css b/src/components/Foundry/Hero/styles.module.css
new file mode 100644
index 000000000..0946db2bc
--- /dev/null
+++ b/src/components/Foundry/Hero/styles.module.css
@@ -0,0 +1,118 @@
+.bg {
+ background-position-x: center;
+ background-repeat: no-repeat;
+ overflow: visible;
+ background-size: cover;
+ position: relative;
+}
+
+.spot1 {
+ position: absolute;
+ left: -300px;
+ top: 200px;
+ width: 600px;
+ height: 600px;
+ background-image: radial-gradient(at top left, rgba(18, 255, 128, 0.4) 0%, rgba(246, 247, 248, 0) 80%);
+ filter: blur(70px);
+}
+
+.container {
+ margin-top: 70px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.textBlock {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 48px;
+}
+
+.title {
+ max-width: 1170px;
+ text-align: center;
+}
+
+.title p,
+.text p {
+ margin: 0;
+}
+
+.logo {
+ height: 32px;
+ margin-bottom: 16px;
+}
+
+@media (min-width: 600px) {
+ .container {
+ margin-top: 150px;
+ }
+}
+
+@media (min-width: 900px) {
+ .spot1 {
+ left: -100px;
+ top: 100px;
+ width: 700px;
+ height: 700px;
+ }
+
+ .spot2 {
+ position: absolute;
+ right: 0px;
+ top: 50px;
+ width: 800px;
+ height: 800px;
+ background-image: radial-gradient(at right, rgba(41, 182, 246, 0.4) 0%, rgba(246, 247, 248, 0) 80%);
+ filter: blur(50px);
+ }
+
+ .image {
+ background-repeat: no-repeat;
+ overflow: hidden;
+ background-size: contain;
+ background-position: center top 140px;
+ position: relative;
+ z-index: 1;
+ }
+
+ .gradientHorizontal:before,
+ .gradientHorizontal:after {
+ content: '';
+ top: 0;
+ display: block;
+ height: 100%;
+ position: absolute;
+ width: 30px;
+ pointer-events: none;
+ }
+
+ .gradientHorizontal:before {
+ background: linear-gradient(-90deg, rgba(18, 19, 18, 0) 0%, rgb(20, 20, 20) 100%);
+ }
+
+ .gradientHorizontal:after {
+ width: 1px;
+ background: linear-gradient(270deg, rgba(18, 19, 18, 0.1) 0%, rgba(18, 19, 18, 0) 100%);
+ right: 0px;
+ }
+
+ .text {
+ text-align: center;
+ max-width: 780px;
+ }
+
+ .logo {
+ height: 40px;
+ }
+}
+
+@media (min-width: 1536px) {
+ .spot1,
+ .spot2 {
+ width: 1000px;
+ height: 1000px;
+ }
+}
diff --git a/src/components/Foundry/POCs/Card.tsx b/src/components/Foundry/POCs/Card.tsx
new file mode 100644
index 000000000..2a8105cb4
--- /dev/null
+++ b/src/components/Foundry/POCs/Card.tsx
@@ -0,0 +1,46 @@
+import { Typography } from '@mui/material'
+import RichText from '@/components/common/RichText'
+import ButtonsWrapper from '@/components/Token/ButtonsWrapper'
+import { isAsset, isEntryTypeButton } from '@/lib/typeGuards'
+import { type BaseBlockEntry } from '@/config/types'
+import GithubIcon from '@/public/images/github-icon.svg'
+import css from './styles.module.css'
+
+const Card = (props: BaseBlockEntry) => {
+ const { caption, title, text, link, image, buttons } = props.fields
+
+ const buttonsList = buttons?.filter(isEntryTypeButton) || []
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {text ?
: null}
+
+
+
{caption}
+
+ {isAsset(image) && image.fields.file?.url ? (
+
+
+
{image.fields.description}
+
+ ) : null}
+
+
+
+
+
+
+
+ )
+}
+
+export default Card
diff --git a/src/components/Foundry/POCs/index.tsx b/src/components/Foundry/POCs/index.tsx
new file mode 100644
index 000000000..23d351a1a
--- /dev/null
+++ b/src/components/Foundry/POCs/index.tsx
@@ -0,0 +1,39 @@
+import { Container, Grid, Typography } from '@mui/material'
+import RichText from '@/components/common/RichText'
+import Card from '@/components/Foundry/POCs/Card'
+import { type BaseBlockEntry } from '@/config/types'
+import { isEntryTypeBaseBlock } from '@/lib/typeGuards'
+import layoutCss from '@/components/common/styles.module.css'
+import css from './styles.module.css'
+
+const POCs = (props: BaseBlockEntry) => {
+ const { caption, title, text, items } = props.fields
+
+ const itemsList = items?.filter(isEntryTypeBaseBlock) || []
+
+ return (
+
+
+
+
+
+ {text && (
+
+
+
+ )}
+
+
+ {itemsList.map((item, index) => (
+
+
+
+ ))}
+
+
+ {caption}
+
+ )
+}
+
+export default POCs
diff --git a/src/components/Foundry/POCs/styles.module.css b/src/components/Foundry/POCs/styles.module.css
new file mode 100644
index 000000000..75886f69a
--- /dev/null
+++ b/src/components/Foundry/POCs/styles.module.css
@@ -0,0 +1,107 @@
+.title {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.text {
+ margin: 16px auto 0;
+ max-width: 725px;
+ text-align: center;
+}
+
+.title p,
+.text p {
+ margin: 0;
+}
+
+.caption {
+ margin-top: 80px;
+ text-align: center;
+}
+
+.gridContainer {
+ justify-content: center;
+ margin-top: 40px;
+}
+
+.cardHeader {
+ height: 100px;
+ padding: 32px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ border: 1px solid var(--mui-palette-border-light);
+ border-top-left-radius: 16px;
+ border-top-right-radius: 16px;
+}
+
+.cardHeader p,
+.cardBody p {
+ margin-top: 0;
+ margin-bottom: auto;
+}
+
+.cardHeader svg {
+ width: 24px;
+ height: 24px;
+}
+
+.cardBody {
+ padding: 32px;
+ border: 1px solid var(--mui-palette-border-light);
+ border-bottom-left-radius: 16px;
+ border-bottom-right-radius: 16px;
+}
+
+.cardBody a {
+ text-decoration: underline;
+ color: var(--mui-palette-primary-main);
+}
+
+.extraText {
+ margin-top: 24px;
+}
+
+.partner {
+ height: 32px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 8px;
+}
+
+.partner img {
+ width: 32px;
+ height: 32px;
+}
+
+.buttons {
+ margin-top: 32px;
+ display: flex;
+ justify-content: flex-start;
+}
+
+@media (min-width: 900px) {
+ .buttons {
+ justify-content: center;
+ }
+
+ .gridContainer {
+ margin-top: 80px;
+ }
+
+ .card {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ .cardBody {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+}
diff --git a/src/components/common/Header/navCategories.tsx b/src/components/common/Header/navCategories.tsx
index d3a0dc5ce..9525b329b 100644
--- a/src/components/common/Header/navCategories.tsx
+++ b/src/components/common/Header/navCategories.tsx
@@ -12,6 +12,7 @@ import CareersIcon from '@/public/images/Header/careers-icon.svg'
import PressRoomIcon from '@/public/images/Header/press-room-icon.svg'
import HelpCenterIcon from '@/public/images/Header/help-center-icon.svg'
import GasStationIcon from '@/public/images/Header/gas-station-icon.svg'
+import SafeFoundryIcon from '@/public/images/Header/safe-foundry-icon.svg'
export type NavItem = {
label: string
@@ -66,6 +67,11 @@ export const navCategories: NavCategory[] = [
href: AppRoutes.gasStation,
icon: ,
},
+ {
+ label: 'Safe{Foundry}',
+ href: AppRoutes.foundry,
+ icon: ,
+ },
],
},
{
diff --git a/src/components/commonCMS/CenteredTextBlock/index.tsx b/src/components/commonCMS/CenteredTextBlock/index.tsx
new file mode 100644
index 000000000..b50edf492
--- /dev/null
+++ b/src/components/commonCMS/CenteredTextBlock/index.tsx
@@ -0,0 +1,39 @@
+import { Container, Typography } from '@mui/material'
+import { type BaseBlockEntry } from '@/config/types'
+import RichText from '@/components/common/RichText'
+import layoutCss from '@/components/common/styles.module.css'
+import css from './styles.module.css'
+import ButtonsWrapper from '@/components/Token/ButtonsWrapper'
+import { isEntryTypeButton } from '@/lib/typeGuards'
+
+const CenteredTextBlock = (props: BaseBlockEntry) => {
+ const { caption, title, text, buttons } = props.fields
+
+ const buttonsList = buttons?.filter(isEntryTypeButton) || []
+
+ return (
+
+ {caption}
+
+
+
+
+
+
+ {text && (
+
+
+
+ )}
+
+
+ {buttonsList.length > 0 && (
+
+
+
+ )}
+
+ )
+}
+
+export default CenteredTextBlock
diff --git a/src/components/commonCMS/CenteredTextBlock/styles.module.css b/src/components/commonCMS/CenteredTextBlock/styles.module.css
new file mode 100644
index 000000000..2caf7a8c5
--- /dev/null
+++ b/src/components/commonCMS/CenteredTextBlock/styles.module.css
@@ -0,0 +1,22 @@
+.title {
+ margin-top: 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+.text {
+ margin: 24px auto 0;
+ max-width: 725px;
+}
+
+.text p {
+ margin: 0;
+}
+
+.buttons {
+ margin-top: 40px;
+ display: flex;
+ justify-content: center;
+}
diff --git a/src/components/commonCMS/Faq/index.tsx b/src/components/commonCMS/Faq/index.tsx
new file mode 100644
index 000000000..57cb584a7
--- /dev/null
+++ b/src/components/commonCMS/Faq/index.tsx
@@ -0,0 +1,77 @@
+import { useState } from 'react'
+import {
+ Accordion,
+ AccordionDetails,
+ type AccordionProps,
+ AccordionSummary,
+ Container,
+ Grid,
+ Typography,
+} from '@mui/material'
+import PlusIcon from '@/public/images/plus-sign-icon.svg'
+import MinusIcon from '@/public/images/minus-sign-icon.svg'
+import { isEntryTypeBaseBlock } from '@/lib/typeGuards'
+import RichText from '@/components/common/RichText'
+import { type BaseBlockEntry } from '@/config/types'
+import layoutCss from '@/components/common/styles.module.css'
+import css from './styles.module.css'
+
+const Faq = (props: BaseBlockEntry) => {
+ const { title, items } = props.fields
+
+ // Tracks which accordion is open
+ const [openMap, setOpenMap] = useState>()
+
+ const itemsList = items?.filter(isEntryTypeBaseBlock) ?? []
+
+ return (
+
+
+
+
+
+
+
+
+
+ {itemsList.map((item, index) => {
+ const { title, text } = item.fields
+
+ const handleChange: AccordionProps['onChange'] = (_, expanded) => {
+ setOpenMap((prev) => ({
+ ...prev,
+ [index]: expanded,
+ }))
+ }
+ const expanded = openMap?.[index] ?? false
+
+ return (
+
+ : }
+ onClick={() => {
+ !expanded
+ }}
+ >
+
+
+
+
+ {text && }
+
+ )
+ })}
+
+
+
+ )
+}
+
+export default Faq
diff --git a/src/components/commonCMS/Faq/styles.module.css b/src/components/commonCMS/Faq/styles.module.css
new file mode 100644
index 000000000..d66527ecf
--- /dev/null
+++ b/src/components/commonCMS/Faq/styles.module.css
@@ -0,0 +1,57 @@
+.gridContainer {
+ position: relative;
+}
+
+.spot {
+ position: absolute;
+ left: -370px;
+ top: -330px;
+ z-index: -1;
+ width: 800px;
+ height: 800px;
+ background-image: radial-gradient(rgba(18, 255, 128, 0.5), rgba(18, 19, 18, 1) 80%);
+ filter: blur(50px);
+}
+
+.accordion {
+ background-color: unset;
+ box-shadow: none;
+ border-bottom: 1px solid var(--mui-palette-border-main);
+}
+
+.accordion :global .MuiAccordionSummary-root {
+ padding: 0;
+ display: flex;
+ gap: 40px;
+}
+
+.accordion :global .MuiAccordionSummary-content {
+ margin: 32px 0;
+}
+
+.accordion :global .MuiAccordionDetails-root {
+ padding: 0 0 32px;
+}
+
+.details :global .MuiAccordionDetails-root a {
+ text-decoration: underline;
+}
+
+/* Resets the paragraph margins on accordion's summary and details */
+.accordion :global .MuiAccordionSummary-content p {
+ margin: 0;
+}
+
+.details p:first-of-type {
+ margin-top: 0;
+}
+
+.details p:last-of-type {
+ margin-bottom: 0;
+}
+
+@media (max-width: 600px) {
+ .spot {
+ width: 700px;
+ }
+}
diff --git a/src/config/routes.ts b/src/config/routes.ts
index 629958c2f..691e36799 100644
--- a/src/config/routes.ts
+++ b/src/config/routes.ts
@@ -11,6 +11,7 @@ export const AppRoutes = {
imprint: '/imprint',
governance: '/governance',
gasStation: '/gas-station',
+ foundry: '/foundry',
ecosystem: '/ecosystem',
disclaimer: '/disclaimer',
core: '/core',