diff --git a/public/images/20261024_banner_open_day.webp b/public/images/20261024_banner_open_day.webp
new file mode 100644
index 0000000..4f82b3a
Binary files /dev/null and b/public/images/20261024_banner_open_day.webp differ
diff --git a/src/components/molecules/EventDialog/EventDialog.test.tsx b/src/components/molecules/EventDialog/EventDialog.test.tsx
new file mode 100644
index 0000000..0460136
--- /dev/null
+++ b/src/components/molecules/EventDialog/EventDialog.test.tsx
@@ -0,0 +1,61 @@
+import { act, fireEvent, render, screen } from '@testing-library/react'
+
+import EventDialog from './index.js'
+
+describe('EventDialog', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('appears after a delay', () => {
+ vi.setSystemTime(new Date(2026, 9, 19))
+ render()
+
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
+
+ act(() => {
+ vi.advanceTimersByTime(3000)
+ })
+
+ expect(screen.getByRole('img', { name: /open day/i })).toBeInTheDocument()
+ })
+
+ it('can be dismissed', () => {
+ vi.setSystemTime(new Date(2026, 9, 19))
+ render()
+ act(() => {
+ vi.advanceTimersByTime(3000)
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: /close/i }))
+ act(() => {
+ vi.runAllTimers()
+ })
+
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
+ })
+
+ it('still appears on October 25th', () => {
+ vi.setSystemTime(new Date(2026, 9, 25, 23, 59))
+ render()
+ act(() => {
+ vi.advanceTimersByTime(3000)
+ })
+
+ expect(screen.getByRole('img', { name: /open day/i })).toBeInTheDocument()
+ })
+
+ it('does not appear after October 25th', () => {
+ vi.setSystemTime(new Date(2026, 9, 26))
+ render()
+ act(() => {
+ vi.advanceTimersByTime(10000)
+ })
+
+ expect(screen.queryByRole('img')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/components/molecules/EventDialog/index.tsx b/src/components/molecules/EventDialog/index.tsx
new file mode 100644
index 0000000..67d668a
--- /dev/null
+++ b/src/components/molecules/EventDialog/index.tsx
@@ -0,0 +1,43 @@
+import { useEffect, useState } from 'react'
+import { Box, Dialog, IconButton } from '@mui/material'
+import CloseIcon from '@mui/icons-material/Close'
+
+const SHOW_DELAY_MS = 3000
+// Local midnight after Oct 25th, so the event is promoted through the whole day.
+const EXPIRES_AT = new Date(2026, 9, 26)
+
+const EventDialog = () => {
+ const [open, setOpen] = useState(false)
+
+ useEffect(() => {
+ if (Date.now() >= EXPIRES_AT.getTime()) return
+ const timer = setTimeout(() => setOpen(true), SHOW_DELAY_MS)
+ return () => clearTimeout(timer)
+ }, [])
+
+ return (
+
+ )
+}
+
+export default EventDialog
diff --git a/src/components/templates/RouteTracker/index.tsx b/src/components/templates/RouteTracker/index.tsx
index 49e55a0..4504aac 100644
--- a/src/components/templates/RouteTracker/index.tsx
+++ b/src/components/templates/RouteTracker/index.tsx
@@ -1,6 +1,8 @@
import { useEffect } from 'react'
import { Outlet, useLocation } from 'react-router-dom'
+import EventDialog from 'components/molecules/EventDialog/index.js'
+
/**
* Route tracking component that wraps all routes and includes analytics page tracking
*/
@@ -11,7 +13,12 @@ const RouteTracker = () => {
window.scrollTo(0, 0)
}, [location])
- return
+ return (
+ <>
+
+
+ >
+ )
}
export default RouteTracker