feat: add scroll-driven product storytelling
@@ -70,5 +70,8 @@ Export **in-app content only** from the simulator or device — no simulator chr
|
|||||||
The CSS frame dimensions assume tall iPhone screenshots and are controlled in
|
The CSS frame dimensions assume tall iPhone screenshots and are controlled in
|
||||||
`src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old
|
`src/components/phone-mockup.tsx`. Use `captureMode="simulator"` only for old
|
||||||
captures that already include device chrome. Homepage product images are
|
captures that already include device chrome. Homepage product images are
|
||||||
composited separately at 810 × 1656 with transparent backgrounds and rendered
|
composited at the bezel source size of 1350 × 2760 with transparent
|
||||||
through `src/components/product-device.tsx`.
|
backgrounds and rendered responsively through
|
||||||
|
`src/components/product-device.tsx`. The homepage uses CSS view timelines for
|
||||||
|
scroll-scrubbed device and chapter motion, with a static fallback and a
|
||||||
|
reduced-motion override.
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
import "./src/env.js";
|
import "./src/env.js";
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {};
|
const config = {
|
||||||
|
images: {
|
||||||
|
qualities: [75, 92],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 185 KiB |
@@ -131,6 +131,7 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/summary-natural.webp"
|
src="/screenshots/pitch/summary-natural.webp"
|
||||||
darkSrc="/screenshots/pitch/summary-natural-dark.webp"
|
darkSrc="/screenshots/pitch/summary-natural-dark.webp"
|
||||||
alt="Medscribe visit summary in a natural titanium iPhone product bezel"
|
alt="Medscribe visit summary in a natural titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-left"
|
||||||
sizes="(min-width: 1024px) 416px, (min-width: 640px) 352px, 272px"
|
sizes="(min-width: 1024px) 416px, (min-width: 640px) 352px, 272px"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
@@ -140,6 +141,7 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/home-black.webp"
|
src="/screenshots/pitch/home-black.webp"
|
||||||
darkSrc="/screenshots/pitch/home-black-dark.webp"
|
darkSrc="/screenshots/pitch/home-black-dark.webp"
|
||||||
alt="Medscribe home screen in a black titanium iPhone product bezel"
|
alt="Medscribe home screen in a black titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-right"
|
||||||
sizes="(min-width: 1024px) 448px, (min-width: 640px) 368px, 288px"
|
sizes="(min-width: 1024px) 448px, (min-width: 640px) 368px, 288px"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
@@ -151,7 +153,7 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="pitch-lazy border-border-soft bg-surface/80 flex min-h-[78svh] items-center border-y px-4 py-24 sm:px-6 lg:py-32">
|
<section className="pitch-lazy border-border-soft bg-surface/80 flex min-h-[78svh] items-center border-y px-4 py-24 sm:px-6 lg:py-32">
|
||||||
<div className="mx-auto max-w-6xl text-center">
|
<div className="pitch-copy-motion mx-auto max-w-6xl text-center">
|
||||||
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
The missing picture
|
The missing picture
|
||||||
</p>
|
</p>
|
||||||
@@ -190,10 +192,10 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol className="space-y-8 sm:space-y-12">
|
<ol className="space-y-8 sm:space-y-12">
|
||||||
{visitMoments.map((moment) => (
|
{visitMoments.map((moment, index) => (
|
||||||
<li
|
<li
|
||||||
key={moment.number}
|
key={moment.number}
|
||||||
className="border-border-soft bg-surface relative overflow-hidden rounded-[2.5rem] border px-5 pt-8 shadow-[0_30px_90px_rgba(15,23,42,0.08)] sm:px-10 sm:pt-10 dark:shadow-[0_30px_90px_rgba(0,0,0,0.25)]"
|
className="pitch-card-motion border-border-soft bg-surface relative overflow-hidden rounded-[2.5rem] border px-5 pt-8 shadow-[0_30px_90px_rgba(15,23,42,0.08)] sm:px-10 sm:pt-10 dark:shadow-[0_30px_90px_rgba(0,0,0,0.25)]"
|
||||||
>
|
>
|
||||||
<div className="relative z-10 max-w-xl">
|
<div className="relative z-10 max-w-xl">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -217,6 +219,7 @@ export default function HomePage() {
|
|||||||
src={moment.image}
|
src={moment.image}
|
||||||
darkSrc={moment.darkImage}
|
darkSrc={moment.darkImage}
|
||||||
alt={moment.alt}
|
alt={moment.alt}
|
||||||
|
className={`pitch-device-motion ${index % 2 === 0 ? "pitch-device-drift-right" : "pitch-device-drift-left"}`}
|
||||||
sizes="(min-width: 1024px) 336px, (min-width: 640px) 320px, 72vw"
|
sizes="(min-width: 1024px) 336px, (min-width: 640px) 320px, 72vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +232,7 @@ export default function HomePage() {
|
|||||||
<section className="pitch-lazy relative isolate overflow-hidden bg-[#082f2c] px-4 py-24 text-white sm:px-6 lg:py-36">
|
<section className="pitch-lazy relative isolate overflow-hidden bg-[#082f2c] px-4 py-24 text-white sm:px-6 lg:py-36">
|
||||||
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_78%_18%,rgba(45,212,191,0.25),transparent_32%),radial-gradient(circle_at_16%_80%,rgba(20,184,166,0.15),transparent_28%)]" />
|
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_78%_18%,rgba(45,212,191,0.25),transparent_32%),radial-gradient(circle_at_16%_80%,rgba(20,184,166,0.15),transparent_28%)]" />
|
||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
<div className="mx-auto max-w-4xl text-center">
|
<div className="pitch-copy-motion mx-auto max-w-4xl text-center">
|
||||||
<p className="text-xs font-bold tracking-[0.2em] text-teal-200 uppercase">
|
<p className="text-xs font-bold tracking-[0.2em] text-teal-200 uppercase">
|
||||||
Between visits
|
Between visits
|
||||||
</p>
|
</p>
|
||||||
@@ -249,6 +252,7 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/medication-detail-natural.webp"
|
src="/screenshots/pitch/medication-detail-natural.webp"
|
||||||
darkSrc="/screenshots/pitch/medication-detail-natural-dark.webp"
|
darkSrc="/screenshots/pitch/medication-detail-natural-dark.webp"
|
||||||
alt="Medscribe medication detail in a natural titanium iPhone product bezel"
|
alt="Medscribe medication detail in a natural titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-left"
|
||||||
sizes="(min-width: 1024px) 368px, (min-width: 640px) 336px, 272px"
|
sizes="(min-width: 1024px) 368px, (min-width: 640px) 336px, 272px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -257,6 +261,7 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/medications-black.webp"
|
src="/screenshots/pitch/medications-black.webp"
|
||||||
darkSrc="/screenshots/pitch/medications-black-dark.webp"
|
darkSrc="/screenshots/pitch/medications-black-dark.webp"
|
||||||
alt="Medscribe medication list in a black titanium iPhone product bezel"
|
alt="Medscribe medication list in a black titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-right"
|
||||||
sizes="(min-width: 1024px) 384px, (min-width: 640px) 352px, 288px"
|
sizes="(min-width: 1024px) 384px, (min-width: 640px) 352px, 288px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -297,7 +302,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<section className="pitch-lazy bg-[#f0ece4] px-4 py-24 sm:px-6 lg:py-36 dark:bg-[#111827]">
|
<section className="pitch-lazy bg-[#f0ece4] px-4 py-24 sm:px-6 lg:py-36 dark:bg-[#111827]">
|
||||||
<div className="mx-auto grid max-w-7xl items-center gap-14 lg:grid-cols-[1fr_0.9fr] lg:gap-24">
|
<div className="mx-auto grid max-w-7xl items-center gap-14 lg:grid-cols-[1fr_0.9fr] lg:gap-24">
|
||||||
<div className="order-2 lg:order-1">
|
<div className="pitch-copy-motion order-2 lg:order-1">
|
||||||
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
Ask from the record
|
Ask from the record
|
||||||
</p>
|
</p>
|
||||||
@@ -319,6 +324,7 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/ask-black.webp"
|
src="/screenshots/pitch/ask-black.webp"
|
||||||
darkSrc="/screenshots/pitch/ask-black-dark.webp"
|
darkSrc="/screenshots/pitch/ask-black-dark.webp"
|
||||||
alt="Medscribe Ask response grounded in local health context, shown in a black titanium iPhone product bezel"
|
alt="Medscribe Ask response grounded in local health context, shown in a black titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-right"
|
||||||
sizes="(min-width: 1024px) 432px, (min-width: 640px) 384px, 78vw"
|
sizes="(min-width: 1024px) 432px, (min-width: 640px) 384px, 78vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -334,10 +340,11 @@ export default function HomePage() {
|
|||||||
src="/screenshots/pitch/know-me-natural.webp"
|
src="/screenshots/pitch/know-me-natural.webp"
|
||||||
darkSrc="/screenshots/pitch/know-me-natural-dark.webp"
|
darkSrc="/screenshots/pitch/know-me-natural-dark.webp"
|
||||||
alt="Medscribe Know Me handoff record in a natural titanium iPhone product bezel"
|
alt="Medscribe Know Me handoff record in a natural titanium iPhone product bezel"
|
||||||
|
className="pitch-device-motion pitch-device-drift-left"
|
||||||
sizes="(min-width: 1024px) 448px, (min-width: 640px) 400px, 80vw"
|
sizes="(min-width: 1024px) 448px, (min-width: 640px) 400px, 80vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="pitch-copy-motion">
|
||||||
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
At the handoff
|
At the handoff
|
||||||
</p>
|
</p>
|
||||||
@@ -425,7 +432,7 @@ export default function HomePage() {
|
|||||||
<section className="pitch-lazy relative isolate overflow-hidden bg-[#070d15] px-4 py-24 text-white sm:px-6 lg:py-36">
|
<section className="pitch-lazy relative isolate overflow-hidden bg-[#070d15] px-4 py-24 text-white sm:px-6 lg:py-36">
|
||||||
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_0%,rgba(45,212,191,0.13),transparent_36%)]" />
|
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_50%_0%,rgba(45,212,191,0.13),transparent_36%)]" />
|
||||||
<div className="mx-auto max-w-7xl">
|
<div className="mx-auto max-w-7xl">
|
||||||
<div className="mx-auto max-w-4xl text-center">
|
<div className="pitch-copy-motion mx-auto max-w-4xl text-center">
|
||||||
<p className="text-xs font-bold tracking-[0.2em] text-teal-300 uppercase">
|
<p className="text-xs font-bold tracking-[0.2em] text-teal-300 uppercase">
|
||||||
A precise privacy boundary
|
A precise privacy boundary
|
||||||
</p>
|
</p>
|
||||||
@@ -486,7 +493,7 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="pitch-lazy flex min-h-[82svh] items-center px-4 py-24 sm:px-6 lg:py-32">
|
<section className="pitch-lazy flex min-h-[82svh] items-center px-4 py-24 sm:px-6 lg:py-32">
|
||||||
<div className="mx-auto w-full max-w-6xl overflow-hidden rounded-[3rem] border border-teal-900/10 bg-[linear-gradient(145deg,#ccfbf1_0%,#f0fdfa_44%,#ffffff_100%)] px-6 py-16 text-center shadow-[0_35px_110px_rgba(15,118,110,0.15)] sm:px-10 lg:px-20 lg:py-24 dark:border-teal-300/10 dark:bg-[linear-gradient(145deg,#134e4a_0%,#0f2930_48%,#111827_100%)]">
|
<div className="pitch-copy-motion mx-auto w-full max-w-6xl overflow-hidden rounded-[3rem] border border-teal-900/10 bg-[linear-gradient(145deg,#ccfbf1_0%,#f0fdfa_44%,#ffffff_100%)] px-6 py-16 text-center shadow-[0_35px_110px_rgba(15,118,110,0.15)] sm:px-10 lg:px-20 lg:py-24 dark:border-teal-300/10 dark:bg-[linear-gradient(145deg,#134e4a_0%,#0f2930_48%,#111827_100%)]">
|
||||||
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
<p className="text-primary text-xs font-bold tracking-[0.2em] uppercase">
|
||||||
The right beta participant
|
The right beta participant
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ export function ProductDevice({
|
|||||||
}: ProductDeviceProps) {
|
}: ProductDeviceProps) {
|
||||||
const shared = {
|
const shared = {
|
||||||
alt,
|
alt,
|
||||||
width: 810,
|
width: 1350,
|
||||||
height: 1656,
|
height: 2760,
|
||||||
sizes,
|
sizes,
|
||||||
priority,
|
priority,
|
||||||
|
quality: 92,
|
||||||
className: cn(
|
className: cn(
|
||||||
"h-auto w-full drop-shadow-[0_35px_35px_rgba(15,23,42,0.22)] dark:drop-shadow-[0_38px_42px_rgba(0,0,0,0.5)]",
|
"h-auto w-full translate-z-0 drop-shadow-[0_35px_35px_rgba(15,23,42,0.22)] dark:drop-shadow-[0_38px_42px_rgba(0,0,0,0.5)]",
|
||||||
className,
|
className,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,6 +99,97 @@
|
|||||||
contain-intrinsic-size: auto 1000px;
|
contain-intrinsic-size: auto 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Scroll-scrubbed product motion. This stays CSS-only so the long homepage
|
||||||
|
* remains a server component and ships no scroll listener or animation
|
||||||
|
* runtime. Browsers without view timelines simply render the resting layout.
|
||||||
|
*/
|
||||||
|
.pitch-device-motion {
|
||||||
|
--pitch-enter-x: 0px;
|
||||||
|
--pitch-enter-rotate: 0deg;
|
||||||
|
--pitch-exit-x: 0px;
|
||||||
|
--pitch-exit-rotate: 0deg;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pitch-device-drift-left {
|
||||||
|
--pitch-enter-x: -2.25rem;
|
||||||
|
--pitch-enter-rotate: -2deg;
|
||||||
|
--pitch-exit-x: 1.5rem;
|
||||||
|
--pitch-exit-rotate: 1.25deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pitch-device-drift-right {
|
||||||
|
--pitch-enter-x: 2.25rem;
|
||||||
|
--pitch-enter-rotate: 2deg;
|
||||||
|
--pitch-exit-x: -1.5rem;
|
||||||
|
--pitch-exit-rotate: -1.25deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pitch-device-scroll {
|
||||||
|
0% {
|
||||||
|
opacity: 0.42;
|
||||||
|
transform: translate3d(var(--pitch-enter-x), 7rem, 0) scale(0.92)
|
||||||
|
rotate(var(--pitch-enter-rotate));
|
||||||
|
}
|
||||||
|
38% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
|
||||||
|
}
|
||||||
|
72% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, -1.5rem, 0) scale(1.018) rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.78;
|
||||||
|
transform: translate3d(var(--pitch-exit-x), -5rem, 0) scale(0.97)
|
||||||
|
rotate(var(--pitch-exit-rotate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pitch-copy-scroll {
|
||||||
|
from {
|
||||||
|
opacity: 0.2;
|
||||||
|
transform: translate3d(0, 3.5rem, 0) scale(0.985);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pitch-card-scroll {
|
||||||
|
from {
|
||||||
|
opacity: 0.58;
|
||||||
|
transform: translate3d(0, 4rem, 0) scale(0.97);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate3d(0, 0, 0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (animation-timeline: view()) {
|
||||||
|
.pitch-device-motion {
|
||||||
|
animation: pitch-device-scroll linear both;
|
||||||
|
animation-timeline: view(block);
|
||||||
|
animation-range: entry 0% exit 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pitch-copy-motion {
|
||||||
|
animation: pitch-copy-scroll linear both;
|
||||||
|
animation-timeline: view(block);
|
||||||
|
animation-range: entry 5% cover 38%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pitch-card-motion {
|
||||||
|
animation: pitch-card-scroll linear both;
|
||||||
|
animation-timeline: view(block);
|
||||||
|
animation-range: entry 0% cover 32%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
html {
|
html {
|
||||||
scroll-behavior: auto;
|
scroll-behavior: auto;
|
||||||
@@ -116,6 +207,14 @@
|
|||||||
.card-hover:hover {
|
.card-hover:hover {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pitch-device-motion,
|
||||||
|
.pitch-copy-motion,
|
||||||
|
.pitch-card-motion {
|
||||||
|
animation: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||