diff --git a/app/public/audio/we-build-tomorrow.m4a b/app/public/audio/we-build-tomorrow.m4a
new file mode 100644
index 0000000..42a4981
Binary files /dev/null and b/app/public/audio/we-build-tomorrow.m4a differ
diff --git a/app/src/App.tsx b/app/src/App.tsx
index 8d04dfa..22618b4 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,4 +1,5 @@
import { Nav } from '@/components/Nav'
+import { SoundToggle } from '@/components/SoundToggle'
import { ScrollScene } from '@/components/ScrollScene'
import { Intro } from '@/components/Intro'
import { Services, WhyUs } from '@/components/Services'
@@ -13,6 +14,7 @@ export default function App() {
Skip the fly-through
+
diff --git a/app/src/components/SoundToggle.tsx b/app/src/components/SoundToggle.tsx
new file mode 100644
index 0000000..2920b70
--- /dev/null
+++ b/app/src/components/SoundToggle.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useRef, useState } from 'react'
+import { Volume2, VolumeX } from 'lucide-react'
+
+const SRC = '/audio/we-build-tomorrow.m4a'
+const VOLUME = 0.6
+
+// Background narration. Browsers block sound until the visitor asks for it,
+// so it starts on click, loops, and fades in and out rather than cutting.
+export function SoundToggle() {
+ const audio = useRef(null)
+ const fade = useRef(undefined)
+ const [on, setOn] = useState(false)
+
+ const ramp = (to: number, done?: () => void) => {
+ const a = audio.current
+ if (!a) return
+ window.clearInterval(fade.current)
+ fade.current = window.setInterval(() => {
+ const step = to > a.volume ? 0.05 : -0.05
+ a.volume = Math.min(1, Math.max(0, a.volume + step))
+ if (Math.abs(a.volume - to) < 0.051) {
+ a.volume = to
+ window.clearInterval(fade.current)
+ done?.()
+ }
+ }, 40)
+ }
+
+ useEffect(() => () => window.clearInterval(fade.current), [])
+
+ const toggle = () => {
+ if (!audio.current) {
+ const a = new Audio(SRC)
+ a.loop = true
+ a.volume = 0
+ audio.current = a
+ }
+ const a = audio.current
+ if (on) {
+ ramp(0, () => a.pause())
+ setOn(false)
+ } else {
+ a.play().then(() => ramp(VOLUME)).catch(() => setOn(false))
+ setOn(true)
+ }
+ }
+
+ return (
+
+ )
+}