diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index deb3ee6..e151c7d 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState, useEffect, useRef, useSyncExternalStore } from 'react';
+import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import Image from 'next/image';
@@ -15,15 +15,6 @@ const NAV_LINKS = [
];
const MOBILE_QUERY = '(max-width: 1023px)';
-const subscribeMobileQuery = (callback: () => void) => {
- const mql = window.matchMedia(MOBILE_QUERY);
- mql.addEventListener('change', callback);
- return () => mql.removeEventListener('change', callback);
-};
-const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches;
-// Server has no viewport, so it always renders the desktop header; the client's
-// first render must match that to avoid a hydration mismatch.
-const getMobileServerSnapshot = () => false;
export default function Header() {
const pathname = usePathname();
@@ -87,7 +78,15 @@ export default function Header() {
href === '/' ? pathname === '/' : pathname.startsWith(href);
// Mobile keeps the original plain white header — the transparent hero overlay is desktop-only.
- const isMobileViewport = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
+ const [isMobileViewport, setIsMobileViewport] = useState(false);
+
+ useEffect(() => {
+ const mql = window.matchMedia(MOBILE_QUERY);
+ setIsMobileViewport(mql.matches);
+ const update = () => setIsMobileViewport(mql.matches);
+ mql.addEventListener('change', update);
+ return () => mql.removeEventListener('change', update);
+ }, []);
// Every page's hero is built with top clearance for an overlaid header, so let it
// show through transparently until the user scrolls (desktop only).
diff --git a/src/components/for-people/SpendAnywhereSection.tsx b/src/components/for-people/SpendAnywhereSection.tsx
index d54f6b2..1a60719 100644
--- a/src/components/for-people/SpendAnywhereSection.tsx
+++ b/src/components/for-people/SpendAnywhereSection.tsx
@@ -3,7 +3,6 @@
import React, { useEffect, useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
-import { PhoneMockupImage } from './PhoneMockup';
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
@@ -77,14 +76,76 @@ export default function SpendAnywhereSection() {
- {/* Right — phone mockup: where to spend */}
+ {/* Right — live LytsUp Spend preview embedded in a real phone frame */}
-
+
+ {/* Screen — locked to a real phone's aspect ratio (~9:19.5) */}
+
+ {/* Dynamic island */}
+
+ {/* Safe area — reserves the notch strip above the app's top bar */}
+
+ {/* The embed is a responsive Flutter web app. We render it at a
+ comfortable logical width (390px) and scale it down to fill the
+ screen exactly, so the UI stays proportioned as designed instead
+ of cramped at ~260px. The iframe width is exactly the design
+ width (no horizontal overscan) so the app lays out edge-to-edge
+ and stays centered — nothing on the right (e.g. the Profile tab)
+ gets clipped. Flutter's thin right-edge list scrollbar is hidden
+ by a slim mask strip rather than by cutting into the layout. */}
+
+
+ {/* Masks Flutter's canvas-painted scrollbar at the far right edge */}
+
+
+
+
+
diff --git a/src/components/home/TheLoop.tsx b/src/components/home/TheLoop.tsx
index 3678ebe..6201783 100644
--- a/src/components/home/TheLoop.tsx
+++ b/src/components/home/TheLoop.tsx
@@ -1,6 +1,6 @@
'use client';
-import React, { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react';
+import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import AnimatedDarkBg from './AnimatedDarkBg';
@@ -13,15 +13,6 @@ if (typeof window !== 'undefined') {
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const MOBILE_QUERY = '(max-width: 768px)';
-const subscribeMobileQuery = (callback: () => void) => {
- const mql = window.matchMedia(MOBILE_QUERY);
- mql.addEventListener('change', callback);
- return () => mql.removeEventListener('change', callback);
-};
-const getMobileSnapshot = () => window.matchMedia(MOBILE_QUERY).matches;
-// Server has no viewport, so it always renders the desktop layout; the client's
-// first render must match that to avoid a hydration mismatch.
-const getMobileServerSnapshot = () => false;
interface Step {
step: string;
@@ -54,7 +45,15 @@ const IMAGE_PATHS = [
export default function TheLoop() {
// Only counts uses state — everything scroll-driven is ref-mutated
const [counts, setCounts] = useState(IMPACTS.map(() => 0));
- const isMobile = useSyncExternalStore(subscribeMobileQuery, getMobileSnapshot, getMobileServerSnapshot);
+ const [isMobile, setIsMobile] = useState(false);
+
+ useEffect(() => {
+ const mql = window.matchMedia(MOBILE_QUERY);
+ setIsMobile(mql.matches);
+ const update = () => setIsMobile(mql.matches);
+ mql.addEventListener('change', update);
+ return () => mql.removeEventListener('change', update);
+ }, []);
const sectionRef = useRef(null);
const pinRef = useRef(null);